diff --git a/.codex/skills/add-oliphaunt-extension/SKILL.md b/.codex/skills/add-oliphaunt-extension/SKILL.md new file mode 100644 index 00000000..1c6526d2 --- /dev/null +++ b/.codex/skills/add-oliphaunt-extension/SKILL.md @@ -0,0 +1,75 @@ +--- +name: add-oliphaunt-extension +description: Add, update, promote, or remove an Oliphaunt PostgreSQL contrib or external extension, including source pins, build recipes, explicit target support, evidence, SDK metadata, release products, carrier identities, and package verification. Use whenever extension catalog, compatibility, packaging, or supported OS/runtime claims change. +--- + +# Add Oliphaunt Extension + +Make support claims fail closed. A runtime target existing does not prove an extension supports that target. + +## Classify + +- contrib: source is PostgreSQL 18. The SQL member belongs to the single + `oliphaunt-extension-contrib-pg18` distribution product at + `src/extensions/contrib/`; that product is `runtime-bound` and is linked to + both liboliphaunt runtimes. A contrib member does not own a leaf `VERSION`, + changelog, `release.toml`, tag, or registry identity. +- external: source uses an immutable upstream commit, packaging versioning is `upstream-bound`, and runtime versions are compatibility metadata rather than release coupling. +- blocked: keep it out of promoted/public catalogs and record the concrete blocker. + +Keep the SQL extension name distinct from the release product id and upstream project name. + +## Implement + +1. Add or update source pins, checksums, patches/dependency recipes, and Moon + metadata. For an external extension, also maintain its product-local + `release.toml`, `VERSION`, and empty first-release `CHANGELOG.md`. For a + contrib member, update the canonical `postgres18.toml` inventory and the + shared contrib product metadata; never create leaf release state. Check + whether the upstream project operates an authoritative HTTPS Git mirror. + When it does, record that reviewed endpoint as `mirror_url` and prove that + it serves the exact pinned commit; never infer a mirror or use a community + fork merely for availability. +2. Declare every supported and intentionally unsupported carrier in `targets/artifacts.toml`. Include evidence references; never rely on derived defaults. +3. Declare the stable Cargo façade plus native, mobile, WASIX portable/AOT, npm, + and Maven carriers actually required by the owning release product. Contrib + members use the shared bundle carriers and retain exact nested member + paths/checksums; external extensions use their independent carriers. Let + size-required Cargo package parts remain dynamic implementation carriers. +4. Regenerate the shared extension model: + +```sh +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --write +cargo run -p xtask -- assets verify-committed +``` + +If and only if the verification reports that source pins, patches, recipes, +compiler inputs, or binary producer code changed, refresh it with +`cargo run -p xtask -- assets input-fingerprint --write` and then verify again. +Version, changelog, registry coordinate, and `targets/artifacts.toml` edits are +package-envelope changes and must leave it unchanged. `--write-evidence` +regenerates claims only; it never creates or updates an observed passing run. + +5. Verify the model and release graph: + +```sh +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check +tools/dev/bun.sh tools/release/release-check.mjs +``` + +When source acquisition or `mirror_url` changes, also run the source-fetch +fault suite, validate the real manifest, and perform one live exact-commit +fetch from each newly declared endpoint. The canonical upstream must remain +the durable origin and every transport must resolve to the same immutable pin. + +6. Build the exact extension artifacts for all declared published targets. Require package-shape, archive safety, checksums, runtime load/create, restart, and dump/restore evidence where the target contract promises them. The exact-SHA CI lane must run `src/extensions/tools/collect-wasix-evidence.sh` against portable and host-AOT artifacts from that same workflow run. Only that collector may record `wasix-full-lifecycle-v1`; its immutable record must identify the exact commit, tree, workflow run, attempt, and job, and qualification must pass `--require-current-evidence`. +7. Run a clean local-registry install for each ecosystem façade. For a contrib + bundle, select at least two members and prove that only those nested members + are staged even though one target carrier contains all contrib bytes. Also + combine one contrib member with an independently versioned external member. + Confirm target selection fetches only the expected carriers and an + unsupported target fails with a useful error. + +## Review + +Reject the change if a declared target lacks a produced artifact/evidence row, an actual package lacks a declared identity, an external extension is runtime-version-coupled, or generated SDK support tables disagree. Report upstream source identity separately from Oliphaunt package version. diff --git a/.codex/skills/add-oliphaunt-extension/agents/openai.yaml b/.codex/skills/add-oliphaunt-extension/agents/openai.yaml new file mode 100644 index 00000000..a503f731 --- /dev/null +++ b/.codex/skills/add-oliphaunt-extension/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Add Oliphaunt Extension" + short_description: "Add or update a supported PostgreSQL extension" + default_prompt: "Use $add-oliphaunt-extension to add or update an Oliphaunt PostgreSQL extension." diff --git a/.codex/skills/qualify-oliphaunt-change/SKILL.md b/.codex/skills/qualify-oliphaunt-change/SKILL.md new file mode 100644 index 00000000..c53186bf --- /dev/null +++ b/.codex/skills/qualify-oliphaunt-change/SKILL.md @@ -0,0 +1,106 @@ +--- +name: qualify-oliphaunt-change +description: Select, run, and diagnose Oliphaunt local and GitHub CI qualification for code, package, extension, SDK, policy, workflow, or release changes. Use before merge/release, when checks are slow or duplicated, or when an exact commit must be proven publishable. +--- + +# Qualify Oliphaunt Change + +Use the repository graph to select work, but require the full exact-SHA gate for releases. + +## Local feedback + +1. Inspect the diff and ask Moon for affected projects/tasks. Do not infer affected products from directory names alone. +2. Run formatting/static checks and focused unit/package tests first. Run expensive producer/E2E lanes only when their inputs or release contract changed. +3. If the diff intentionally changes WASIX binary-semantic inputs (source pins, + patches, build recipes, the WASIX toolchain, or producer code), refresh the + committed fingerprint before qualification with + `cargo run -p xtask -- assets input-fingerprint --write`. Do not refresh it + for version, changelog, package-description, smoke-expectation, or + target-envelope-only changes. See `docs/maintainers/assets.md`. +4. For any release, package identity, workflow, version, or extension change, run: + +```sh +tools/dev/bun.sh tools/release/release-check.mjs +cargo run -p xtask -- assets verify-committed +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check +``` + +The canonical `release-check` runs the live repository-structure policy before +release metadata and mutation tests. Do not substitute the policy unit tests: +they prove the classifier but do not scan the candidate tree. + +For source-acquisition policy or a source `mirror_url`, run +`tools/dev/bun.sh test tools/policy/source-fetch-core.test.mjs` and +`tools/dev/bun.sh tools/policy/fetch-sources.mjs all --validate-only`. Prove a +new endpoint with a live exact-commit fetch, but keep reachability out of the +deterministic unit gate. Qualification must show bounded canonical-to-mirror +failover, exact-pin rejection, canonical durable origin, and transactional +preservation of an existing checkout when every endpoint fails. + +5. Use `actionlint` for workflow changes. Validate shell/JS/Python syntax before waiting for CI. If a reusable-workflow interface, nested permission, caller input, or caller job changed, also push the exact candidate to a disposable branch and dispatch one supported `publish-dry-run` compiler probe. Require GitHub to materialize the called job graph, then cancel it before expensive qualification and delete the probe branch. `actionlint` cannot prove GitHub's reusable-workflow permission ceiling or dispatch-time input compilation. +6. Declare runner capabilities on the narrowest Moon task that needs them. Use + `ci-rust` for Cargo, rustc, rustfmt, or another Rust-toolchain command; + `ci-maintainer-tools` for the pinned tools installed by + `tools/dev/bootstrap-tools.sh`; and `ci-android-sdk` for Android SDK work. + Capabilities propagate through task dependencies. The planner keeps + capability-bearing checks dedicated and combines only compatible static + checks into bounded shards. +7. Treat a hosted runner-image pin as a toolchain dependency. Never introduce a mutable `*-latest` alias; after changing an explicit runner pin, inspect the image delta and run the platform binary contract for every affected release target. + +For a WASIX Docker, APT snapshot, or bootstrap trust change, also run the +product-owned fault test and source verifier before the expensive build: + +```sh +bash src/runtimes/liboliphaunt/wasix/assets/build/docker/install-pinned-apt-packages.test.sh +tools/dev/bun.sh tools/policy/fetch-sources.mjs wasix-runtime --verify-only +cargo run -p xtask -- assets source-spine --strict-local +``` + +Then build the pinned Dockerfile from a clean builder context. Require a +successful TLS-verified snapshot transaction and the exact declared wasixcc, +Clang, and Binaryen versions; a source-spine/static check alone does not prove +that the pinned trust chain still reaches the snapshot service. + +For an SDK change, run `moon run sdk-contracts:check`, then run every affected +SDK's `package` target in one Moon invocation. SDK package targets own their +same-project `check` and `test` dependencies, so this is the compact product +gate without the platform artifact or E2E matrix. Set `MOON_BASE` and +`MOON_HEAD`, then select SDK project IDs with +`moon query projects --affected --downstream deep --tags sdk --tasks package`. +Pass the exact `:package` targets to `moon run`; a workspace-wide +`:package` selector also selects non-SDK products and is not this lane. Confirm +ownership with +`moon query tasks --project --id package` when changing task +topology. Never replace the product task with a narrower native command: for +example, `cargo test -p oliphaunt --lib` excludes Rust executable tests under +`src/bin/**`, while `moon run oliphaunt-rust:test` includes the library, +executable, integration, build-crate, and documentation tests. Add +`release-check` when package or registry behavior changes, and run +`moon run extension-model:check` when an extension catalog or generated SDK +extension surface changes. Put new guarantees in a parsed schema/generated +contract, clean-consumer package check, or product-owned behavioral test. Do +not qualify SDK behavior by grepping prose, test names, or +implementation-source spellings. + +Advisory cleanup is not qualification. Use +`moon run dev-tools:helper-reference-audit` or +`moon run dev-tools:source-reference-audit` when intentionally looking for +possibly unreferenced helpers or modules, then inspect each result before +removing it. Do not turn reference counts into a required CI gate. + +## GitHub qualification + +- Identify runs by workflow plus exact `headSha`; never accept “latest successful on branch.” +- The release prerequisite is the non-cancelled `Qualified` gate for that SHA, including required checks, builds, policy, tests, and selected E2E. +- When WASIX or an extension is selected, require the same-run full lifecycle evidence artifact. It must cover every promoted extension in direct, server, restart, materialization, and dump/restore modes and satisfy `--require-current-evidence` for the candidate source digest. +- Ensure artifact attestations and the publication lock reference the same SHA/tree. +- Require artifact evidence for the compatibility floors in + `docs/maintainers/release.md`: inspect Mach-O load commands, Android API/ELF + metadata, and Linux ELF symbol versions rather than inferring support from a + runner or package label. +- Do not rerun duplicate downstream E2E workflows when the same evidence is already part of the required gate. +- On failure, inspect the failing job log and earliest causal error. Fix the cause, push a new SHA, and restart qualification; do not reuse artifacts from the failed SHA. + +## Report + +List commands and outcomes, skipped lanes with reasons, exact GitHub run/SHA, required gate state, produced artifact/lock evidence, WASIX lifecycle evidence when selected, and residual platform gaps. “Green CI” without exact-SHA and gate names is not release evidence. diff --git a/.codex/skills/qualify-oliphaunt-change/agents/openai.yaml b/.codex/skills/qualify-oliphaunt-change/agents/openai.yaml new file mode 100644 index 00000000..be864a8d --- /dev/null +++ b/.codex/skills/qualify-oliphaunt-change/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Qualify Oliphaunt Change" + short_description: "Run and diagnose the appropriate CI qualification" + default_prompt: "Use $qualify-oliphaunt-change to qualify this Oliphaunt change before merge or release." diff --git a/.codex/skills/release-oliphaunt/SKILL.md b/.codex/skills/release-oliphaunt/SKILL.md new file mode 100644 index 00000000..c160dc66 --- /dev/null +++ b/.codex/skills/release-oliphaunt/SKILL.md @@ -0,0 +1,102 @@ +--- +name: release-oliphaunt +description: Prepare, audit, bootstrap, publish, verify, or recover Oliphaunt releases across GitHub, crates.io, npm, JSR, Maven Central, and SwiftPM. Use for release PRs, version bumps, changelogs, registry setup, publication failures, missing tags/packages, or first-release/history-repair work. +--- + +# Release Oliphaunt + +Treat a release as a frozen, exact-SHA promotion of already-qualified +artifacts. Never rebuild binary producer outputs or substitute artifacts. +Normal publish may deterministically reassemble carrier packages only from the +same qualified inputs, and the resulting publication lock must byte-match the +approved dry-run lock; bootstrap publishes the approved capsule bytes directly. + +Select release products and versions from the publication catalog and +product-local metadata. PostgreSQL 18 contrib SQL members belong to the single +runtime-bound `oliphaunt-extension-contrib-pg18` product; they remain exact +member artifacts inside its target carriers rather than leaf release products. +External extensions own independent packaging SemVer and record their upstream +source identity separately. Never infer one repository-wide extension version, +and do not treat target/ecosystem carriers as additional products. + +## Start + +1. Read `docs/maintainers/release.md` and `references/invariants.md`. +2. For registry/GitHub setup, identity bootstrap, or trusted-publisher work, + also read `docs/maintainers/release-setup.md`. +3. For a failed or partially public release, also read `references/recovery.md` before changing state. +4. Record the candidate commit with `git rev-parse HEAD`; keep that SHA unchanged through qualification, lock creation, and publish. +5. Inspect `git status`, product versions, existing product tags/releases, registry identities, and the latest exact-SHA CI run. Report any public collision before attempting a mutation. +6. Run `tools/dev/bun.sh tools/release/audit-github-release-controls.mjs` with the truthful credential lifecycle before any external mutation. Use `--governance solo --bootstrap-state idle` for history repair, qualification, release-PR preparation, and dry-run while bootstrap tokens are absent. Rerun with `--bootstrap-state ready` only for an imminent first-identity bootstrap after every reviewed short-lived token required by the approved lock is installed (one registry or both; the current first release needs both); use `retired` after trusted publishers are configured and every provisioned token is revoked. Select `team` only with an independent maintainer. Treat `FAIL` as a blocker; report but do not promote `WARN` to a solo-release blocker. +7. Generate trusted-publisher work from the approved publication lock with `tools/dev/bun.sh tools/release/trusted-publisher-config.mjs`. Its default mode is offline/read-only. Use authenticated `--audit` before considering `--apply`; mutation additionally requires the exact printed lock digest. Configure the top-level caller `release.yml` and `release-publish` environment, never the reusable implementation filename. +8. On a generated release PR, treat Release Please as the direct-candidate + authority and `sync-release-pr.mjs` as the deterministic dependent-candidate + closer. Inspect its dependency-only changelog reasons; do not manually copy + candidates, broaden build-only Moon scopes, or guess a first version for a + dependent still at `0.0.0`. + +## Choose the operation + +Do not stack mutating release dispatches. GitHub concurrency protects the +active mutation but retains only one pending run, so a newer dispatch can +replace an older pending dispatch. Wait for the active bootstrap, publish, or +release-PR mutation to finish before starting another. + +- Prepare: synchronize release-owned files, run release checks, create the generated release PR, and stop for review. +- Bootstrap: use the dedicated bootstrap environment only for identities that cannot use trusted publishing until their first package exists, including generated part identities introduced by a future lock. For npm, require a short-lived granular token with explicit `@oliphaunt` scope selection, Packages and scopes `Read and write`, and 2FA bypass, owned by a 2FA-enabled actor with scope write access; an ordinary token can authenticate yet fail the noninteractive publish with `EOTP`. Require one successful exact-SHA dry-run containing both `oliphaunt-publication-lock` and `oliphaunt-bootstrap-capsule`; select one run ID, verify the capsule's embedded lock against the separately downloaded lock, and publish only those frozen Cargo/npm bytes without rebuilding. Inventory the exact lock first. Model crates.io's documented token bucket; never accept an unverifiable numeric capacity assertion. Execute one sequential Cargo lane and one sequential npm lane, overlap only independent carriers, and preserve every lock dependency as a barrier. If one hosted job cannot finish, flush and upload the canonical hash-chained checkpoint before a separate credential-free job dispatches a bounded exact-parent continuation. Bind it to release/lock/package identity and exact artifact ID/digest/size, and permit zero-progress recursion only for explicitly typed, finite-budget rate-limit/deadline continuations. A valid `429 Retry-After` may defer; ambiguous uploads, timeouts, integrity mismatches, malformed responses, and checkpoint failures remain hard failures. After every identity has a receipt, use that exact lock with `tools/release/trusted-publisher-config.mjs`: its default plan has no network access, `--audit` is read-only, and mutation requires both `--apply` and the exact `--confirm-lock-digest`. Require caller `release.yml`, environment `release-publish`, and npm publish-only permission; reject extra or mismatched configurations. Revoke long-lived credentials, then resume normal publish. +- Publish: require a successful exact-SHA `Qualified` gate, complete artifact set with binary compatibility-floor evidence, current full-lifecycle WASIX evidence when selected, frozen publication lock, and the all-registry rate-aware admission preflight. Before the first mutation, build and validate the complete signed Maven Central bundle locally (including sources/javadocs and the strict size ceiling), and prove the lock-derived Swift semantic tag is absent or already resolves to the exact deterministic manifest commit. Normal publish remains logically ordered as stage GitHub drafts/assets/attestations, publish the exact registry topology, then verify public consumers and promote. A registry continuation may span hosted jobs but must reuse the original exact stage handoff and immutable checkpoint; finalization is disabled until receipts are exhaustive. Every normal continuation must also carry the latest root-lineage-bound GitHub content-write pacer and core-request journal, merge the child's pre-install reads monotonically, and reject reset, replay, or substitution. Transfer state only through manifest-exact artifacts downloaded by immutable ID: reuse the approved Cargo/npm capsule rather than retransferring its carriers, send only required non-capsule registry inputs in the stage handoff, and send receipts only to finalization. Reuse a complete verified bootstrap ledger rather than serially reproving its Cargo/npm identities. Execute one bounded sequential lane per registry, overlap independent lanes, honor cross-registry DAG barriers, and assemble callback-returned receipts into an exhaustive exact-lock receipt set (an empty topology is valid for source-only products). Model crates.io's version token bucket with upload work overlapping refill and treat only valid server `Retry-After` state as authoritative; never require an unverifiable capacity secret. After receipt verification, run the lock-derived anonymous public Cargo/npm/Maven/JSR and Git/Swift consumer lanes concurrently from fresh caches under one shared deadline; resolve each entry root independently, require its platform-independent resolver lock to cover the complete frozen dependency closure, retry only transient visibility/network failures in a new cache, preserve deterministic receipt-bound evidence, and then promote GitHub drafts as the literal final step. Distinguish resolver coverage from host-installed/fetched payloads; never relabel a missing lock dependency as receipt-only. Treat Swift as a public source-tag/manifest proof before promotion, never as a claim that draft binary-target assets are anonymously downloadable. npm's trusted credential cannot move dist-tags, so each exact npm version receives its normal tag during publication. +- Recover: inventory external state first. Resume idempotently from the publication ledger; never delete or overwrite immutable public versions. +- History repair: use only before any affected product tag/package is public. Follow `references/recovery.md` and require explicit maintainer authorization for protection changes or force-push. + +## Local gates + +If the candidate intentionally changes WASIX binary-semantic inputs, refresh +`asset-inputs.sha256` first with +`cargo run -p xtask -- assets input-fingerprint --write`; a pure release +version/changelog/envelope change must leave that fingerprint unchanged. See +`docs/maintainers/assets.md` for the exact boundary. + +Run these from the repository root: + +```sh +tools/dev/bun.sh tools/release/sync-release-semantic-inputs.mjs --check +tools/dev/bun.sh tools/release/release-check.mjs +cargo run -p xtask -- assets verify-committed +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check +``` + +When a shared packager, archive encoder, carrier generator, or public target +contract changes, update its exact product ownership in +`tools/release/release-semantic-inputs.toml`, run the synchronizer with +`--write`, and inspect the product-local fingerprint diff before `--check`. +Workflow, validation, registry-transport, test, and documentation-only files +must remain outside that ownership map. + +For a normalized generated release PR, also run the synchronizer in write mode +and immediately in check mode. It follows only Moon production/peer edges, +directed compatibility source-to-owner edges, and the runtime linked group. A +direct external-extension candidate therefore stays minimal, while a runtime +candidate may require separately versioned external compatibility dependents. +An incomplete linked group or unversioned first-release dependent is a Release +Please/configuration blocker, not permission to weaken the graph. + +```sh +tools/dev/bun.sh tools/release/sync-release-pr.mjs +tools/dev/bun.sh tools/release/sync-release-pr.mjs --check +``` + +Use `tools/release/release-product-dry-run.mjs --product ` only after downloading the exact-SHA artifact inputs required by that product. Use the publication-lock command shown by `--help` to create, freeze, and verify the lock from those staged artifacts. + +The committed extension evidence table may say `requires-exact-candidate-ci`; that is an honest pre-qualification state, not permission to skip the lane. The selected CI run must provide the current evidence artifact. Do not use `--allow-dirty` for release evidence. Do not publish from a local rebuild, a different workflow run, a branch name, or a moving ref. + +After a first-identity bootstrap seals, run the trusted-publisher helper without +flags first and record its exact `lockDigest` and npm batch count. Audit Cargo +and every npm batch before explicit apply, rerun the same batch after an +interruption, and retain final reports showing no missing or conflicting +configuration. Never enter `release-execute.yml` as a registry publisher; it is +the reusable implementation, while GitHub's registry-facing caller claim is +`release.yml`. + +## Handoff + +State the candidate SHA, selected products and versions, exact CI run, lock digest, registry/bootstrap state, completed publication phases, and any remaining irreversible action. Distinguish product releases from target/ecosystem carrier packages. diff --git a/.codex/skills/release-oliphaunt/agents/openai.yaml b/.codex/skills/release-oliphaunt/agents/openai.yaml new file mode 100644 index 00000000..f4c11dac --- /dev/null +++ b/.codex/skills/release-oliphaunt/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Release Oliphaunt" + short_description: "Prepare, qualify, publish, and recover releases" + default_prompt: "Use $release-oliphaunt to prepare and qualify an Oliphaunt release." diff --git a/.codex/skills/release-oliphaunt/references/invariants.md b/.codex/skills/release-oliphaunt/references/invariants.md new file mode 100644 index 00000000..e5c47412 --- /dev/null +++ b/.codex/skills/release-oliphaunt/references/invariants.md @@ -0,0 +1,27 @@ +# Release invariants + +- A product owns SemVer, changelog, source identity, product tag, and GitHub release. +- A carrier is an ecosystem/target package for one product version. Carrier count is driven by consumer selection and registry limits, not by product count. +- PostgreSQL 18 contrib SQL members share the single runtime-bound + `oliphaunt-extension-contrib-pg18` distribution product while retaining exact + member paths/checksums inside each target carrier. External extensions own + independent packaging SemVer and record their upstream version/commit + separately. +- The release commit, qualified workflow head, artifact attestations, publication lock source SHA/tree, and product tags must agree exactly. +- Extension evidence runs are immutable observations. Claim regeneration never changes them, and current WASIX support is qualified only by the full lifecycle collector running against same-workflow exact-SHA artifacts and recording that commit/tree/run identity. +- The publication lock is exhaustive: reject undeclared and missing packages/assets as well as hash, size, dependency, target, or version drift. +- Every shared published-byte producer or public target contract has exactly + one declarative ownership rule and content-addressed Release Please + fingerprints under every affected product root; policy and transport-only + edits do not create product releases. +- Generate the lock after artifact assembly. Freeze it before any external write. Preserve it with the release ledger. +- Publish leaves/parts before aggregators, target carriers before façades, runtime artifacts before SDKs, and packages before public GitHub release promotion. +- Every extension Cargo `*-wasix` portable carrier and each of its dynamic payload parts records the explicit canonical target `wasix-portable`; portable extension targets are never inferred from a null target. +- Cross-registry publication is resumable, not atomic. Treat already-matching immutable identities as success; treat mismatched identities as a stop condition. +- Normal publish is a strict GitHub-stage -> registry -> finalization DAG with a fresh deadline per job. Cross-job state must be a manifest-exact immutable artifact downloaded by ID; bind source SHA/tree, selected products, lock/catalog/package-envelope, approved dry-run run/artifact metadata, and every file digest/size. Carry Cargo/npm packages once in the approved capsule, only non-capsule registry inputs in the stage handoff, and receipts only after registry publication. +- Bootstrap writes a genesis checkpoint before any external mutation, executes one sequential lane per immutable-name registry with DAG barriers between them, and serializes canonical completed-ID receipts into a content-addressed hash-chain checkpoint after each bounded batch and final failure drain. Resume only an intact chain for the same source SHA, lock, catalog, selected products, complete carrier order, and package envelope. +- An existence check never authorizes an immutable-version skip. Prove crates.io checksums, npm SRI, Maven payload bytes, and the JSR file manifest against the publication lock, and preserve final receipts before release promotion. +- Before promotion, derive every applicable public consumer surface and dependency closure from the exact lock; probe each anonymous Cargo/npm/Maven/JSR entry independently plus Git/Swift in fresh caches under one deadline, require every resolver lock to contain its complete frozen closure, retry only transient visibility failures, and preserve deterministic evidence bound to both immutable receipt sets. Never hide a missing lock dependency in a receipt-only category. A macOS host install does not prove every OS carrier, and a pre-promotion Swift probe proves the public source tag/manifest rather than draft binary-target availability. +- Normal npm and JSR publication uses GitHub-hosted OIDC. Normal Cargo publication exchanges OIDC for a fresh temporary token per bounded carrier batch and revokes it in `finally`; Maven credentials remain protected environment secrets. Bootstrap credentials are short-lived, isolated, and revoked after exact trusted-publisher configuration is audited; npm bootstrap specifically requires a granular `@oliphaunt` read/write token with 2FA bypass from a 2FA-enabled actor. +- A pure version/changelog update may change the package envelope and lock, but must not change the WASIX binary-semantic input fingerprint. +- Never reuse a public version, move a public product tag, or force-push a history containing affected public releases. diff --git a/.codex/skills/release-oliphaunt/references/recovery.md b/.codex/skills/release-oliphaunt/references/recovery.md new file mode 100644 index 00000000..6528d986 --- /dev/null +++ b/.codex/skills/release-oliphaunt/references/recovery.md @@ -0,0 +1,23 @@ +# Release and history recovery + +## Failed or partial publish + +1. Freeze further publication and save the workflow URL, candidate SHA, publication lock, complete checkpoint chain, and registry responses. +2. Validate every checkpoint digest and previous-checkpoint link. Confirm the source SHA/tree, lock/catalog digests, package envelope, and selected products are unchanged. Never hand-edit or truncate the chain. +3. Query every expected identity and GitHub tag/release. Classify it as absent, present-and-byte-matching, or conflicting. An existence-only response is not matching evidence. +4. Fix only the failed phase. Requalify a new commit if repository code/configuration changes; never attach old artifacts or a prior ledger to it. +5. Resume in dependency order from the newest validated checkpoint. Re-inventory the complete exact lock first: a carrier accepted before an ambiguous response or checkpoint interruption is recovery input, not permission to upload again. Skip it only after its registry checksum/SRI/payload/file manifest matches the frozen bytes. Bootstrap resumes with one sequential Cargo lane and one sequential npm lane, preserves cross-lane dependency barriers, and serializes canonical checkpoint appends. +6. Seal bootstrap only after every expected identity has a receipt. Promote draft GitHub releases only after the final all-registry receipt proof and exact-lock anonymous public-consumer probes produce their deterministic receipt-bound evidence. Retry a transient visibility failure only from a fresh cache under the original shared deadline; do not retry an exact identity/source/closure mismatch. Swift remains a source-tag/manifest probe before promotion because draft binary-target assets are not anonymously public. npm's normal tag is attached by its immutable version publish because OIDC does not authorize a later dist-tag mutation. + +## Pre-publication main-history repair + +This path is forbidden after any affected product tag/package is public. + +1. Freeze main and verify the exact old tip. Create an archive ref and offline bundle. +2. Build the desired tree on a temporary branch and qualify it before touching main. +3. Create one tree-identical introduction commit on the intended stable parent. Put one generated release-bump commit above it. +4. Temporarily allow only the minimum force-push authority. Push with `--force-with-lease=
:`. +5. Immediately restore branch protection, require pull requests/checks, and disable force-push. +6. Qualify the new introduction SHA. Prepare, review, and qualify the release commit. Publish only that exact SHA. + +Do not automate repository-setting changes or the force-push without explicit maintainer authorization at execution time. diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..e7fadacc --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,22 @@ +[profile.default] +retries = 0 +fail-fast = false + +[profile.ci] +retries = 0 +fail-fast = false +status-level = "fail" +final-status-level = "slow" + +[profile.ci.junit] +path = "junit.xml" + +[profile.slow] +retries = 0 +fail-fast = false +slow-timeout = { period = "120s", terminate-after = 2 } + +[profile.release] +retries = 0 +fail-fast = false +slow-timeout = { period = "300s", terminate-after = 2 } diff --git a/.gitattributes b/.gitattributes index 43b8b9ae..69883c90 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,7 @@ -spikes/wasix-postgres-build/patches/*.patch whitespace=-blank-at-eol,-space-before-tab +# Repository text must retain identical bytes on every checkout. Several +# security and release fingerprints intentionally hash tracked source bytes. +* text=auto eol=lf + +*.patch text eol=lf whitespace=-blank-at-eol,-space-before-tab +*.diff text eol=lf whitespace=-blank-at-eol,-space-before-tab +src/**/patches/series text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 42c43678..cc505cf8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,8 +1,25 @@ name: Bug report -description: Report incorrect behavior in pglite-oxide. +description: Report incorrect Oliphaunt behavior in a product, SDK, runtime, or tool. title: "bug: " labels: ["bug"] body: + - type: dropdown + id: product + attributes: + label: Product area + description: Which product or surface is affected? + options: + - liboliphaunt + - Rust SDK + - Swift SDK + - Kotlin SDK + - React Native SDK + - TypeScript SDK + - WASM/WASIX runtime + - Repo tooling / CI / release + - Documentation + validations: + required: true - type: textarea id: summary attributes: @@ -14,14 +31,13 @@ body: id: repro attributes: label: Reproduction - description: Minimal Rust code or commands that reproduce the issue. - render: rust + description: Minimal code, commands, SQL, or app steps that reproduce the issue. validations: required: true - type: input id: versions attributes: label: Versions - description: pglite-oxide, Rust, OS, and architecture. + description: Oliphaunt package versions, platform versions, OS, architecture, and relevant toolchain versions. validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 22162a28..e98f2e36 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -3,6 +3,23 @@ description: Suggest a focused API, runtime, or packaging improvement. title: "feat: " labels: ["enhancement"] body: + - type: dropdown + id: product + attributes: + label: Product area + description: Which product or surface would this improve? + options: + - liboliphaunt + - Rust SDK + - Swift SDK + - Kotlin SDK + - React Native SDK + - TypeScript SDK + - WASM/WASIX runtime + - Repo tooling / CI / release + - Documentation + validations: + required: true - type: textarea id: use_case attributes: diff --git a/.github/actions/collect-ci-summary/action.yml b/.github/actions/collect-ci-summary/action.yml new file mode 100644 index 00000000..4586fd34 --- /dev/null +++ b/.github/actions/collect-ci-summary/action.yml @@ -0,0 +1,16 @@ +name: Collect CI summary +description: Append a small Moon/release summary to the GitHub step summary. + +runs: + using: composite + steps: + - name: Write CI summary + shell: bash + run: | + { + echo "## Oliphaunt CI" + echo + echo "- Moon projects: \`moon query projects\`" + echo "- Moon tasks: \`moon query tasks\`" + echo "- Release plan: \`tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --head-ref \`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/actions/setup-android/action.yml b/.github/actions/setup-android/action.yml new file mode 100644 index 00000000..a70214f4 --- /dev/null +++ b/.github/actions/setup-android/action.yml @@ -0,0 +1,153 @@ +name: Set up Android +description: Set up Java and expose Android SDK paths for Gradle/Expo jobs. + +inputs: + ndk-version: + description: Android NDK side-by-side version required by native SDK builds. + required: false + default: "27.0.12077973" + cmake-version: + description: Android CMake version required by native SDK builds. + required: false + default: "3.22.1" + compile-sdk: + description: Android platform API level used by SDK checks. + required: false + default: "36" + native-ccache: + description: Whether to install and cache ccache for native Android C/C++ builds. + required: false + default: "false" + gradle-cache: + description: Whether setup-java should restore and save the Gradle dependency cache. + required: false + default: "true" + gradle-cache-scope-file: + description: Optional repository-relative file whose contents isolate a Gradle cache consumer. + required: false + default: "" + +runs: + using: composite + steps: + - name: Validate Android setup capabilities + shell: bash + env: + GRADLE_CACHE_INPUT: ${{ inputs.gradle-cache }} + GRADLE_CACHE_SCOPE_FILE: ${{ inputs.gradle-cache-scope-file }} + NATIVE_CCACHE_INPUT: ${{ inputs.native-ccache }} + run: | + set -euo pipefail + case "$GRADLE_CACHE_INPUT" in true|false) ;; *) echo "gradle-cache must be true or false" >&2; exit 1 ;; esac + case "$NATIVE_CCACHE_INPUT" in true|false) ;; *) echo "native-ccache must be true or false" >&2; exit 1 ;; esac + case "$GRADLE_CACHE_SCOPE_FILE" in + "") ;; + /*|../*|*/../*|*/..) echo "gradle-cache-scope-file must be repository-relative and cannot traverse parents" >&2; exit 1 ;; + *) + if [[ ! -f "$GITHUB_WORKSPACE/$GRADLE_CACHE_SCOPE_FILE" ]]; then + echo "gradle-cache-scope-file does not exist: $GRADLE_CACHE_SCOPE_FILE" >&2 + exit 1 + fi + ;; + esac + + - name: Set up Java with Gradle cache + if: ${{ inputs.gradle-cache == 'true' }} + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a + with: + distribution: temurin + java-version: "17" + cache: gradle + cache-dependency-path: | + src/sdks/kotlin/**/*.gradle* + src/sdks/kotlin/**/gradle-wrapper.properties + src/sdks/kotlin/**/libs.versions.toml + src/sdks/react-native/examples/expo/package.json + src/sdks/react-native/package.json + pnpm-lock.yaml + ${{ inputs.gradle-cache-scope-file }} + + - name: Set up Java without Gradle cache + if: ${{ inputs.gradle-cache != 'true' }} + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a + with: + distribution: temurin + java-version: "17" + + - name: Prepare native Android ccache directory + if: ${{ inputs.native-ccache == 'true' }} + shell: bash + run: | + set -euo pipefail + mkdir -p "$HOME/.cache/oliphaunt-ccache/android" + + - name: Restore native Android ccache + if: ${{ inputs.native-ccache == 'true' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ~/.cache/oliphaunt-ccache/android + key: android-ccache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.ndk-version }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/runtimes/liboliphaunt/native/**') }} + restore-keys: | + android-ccache-${{ runner.os }}-${{ runner.arch }}-${{ inputs.ndk-version }}- + + - name: Set up native Android ccache + if: ${{ inputs.native-ccache == 'true' }} + shell: bash + run: | # zizmor: ignore[github-env] ccache environment values are fixed runner-owned paths and constants. + set -euo pipefail + if ! command -v ccache >/dev/null 2>&1; then + case "$RUNNER_OS" in + Linux) + .github/scripts/prepare-linux-apt.sh + sudo apt-get update + sudo apt-get install -y ccache + ;; + macOS) + brew install ccache + ;; + *) + echo "ccache is not provisioned for RUNNER_OS=$RUNNER_OS" >&2 + exit 1 + ;; + esac + fi + echo "CCACHE_DIR=$HOME/.cache/oliphaunt-ccache/android" >> "$GITHUB_ENV" + echo "CCACHE_BASEDIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV" + echo "CCACHE_NOHASHDIR=true" >> "$GITHUB_ENV" + echo "CCACHE_COMPILERCHECK=content" >> "$GITHUB_ENV" + mkdir -p "$HOME/.cache/oliphaunt-ccache/android" + ccache --set-config=max_size=2G + ccache --zero-stats + + - name: Configure Android SDK + shell: bash + env: + NDK_VERSION: ${{ inputs.ndk-version }} + CMAKE_VERSION: ${{ inputs.cmake-version }} + COMPILE_SDK: ${{ inputs.compile-sdk }} + run: | # zizmor: ignore[github-env] Android SDK paths are runner-owned or HOME-scoped and validated before export. + set -euo pipefail + if [[ -z "${ANDROID_HOME:-}" ]]; then + if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then + export ANDROID_HOME="${ANDROID_SDK_ROOT}" + elif [[ -d "$HOME/Library/Android/sdk" ]]; then + export ANDROID_HOME="$HOME/Library/Android/sdk" + elif [[ -d "$HOME/Android/Sdk" ]]; then + export ANDROID_HOME="$HOME/Android/Sdk" + else + export ANDROID_HOME="$HOME/android-sdk" + fi + fi + tools/dev/setup-android-sdk.sh \ + --sdk-root "$ANDROID_HOME" \ + --ndk-version "$NDK_VERSION" \ + --cmake-version "$CMAKE_VERSION" \ + --compile-sdk "$COMPILE_SDK" + + echo "ANDROID_HOME=${ANDROID_HOME}" >> "$GITHUB_ENV" + echo "ANDROID_SDK_ROOT=${ANDROID_HOME}" >> "$GITHUB_ENV" + echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${NDK_VERSION}" >> "$GITHUB_ENV" + echo "${ANDROID_HOME}/cmdline-tools/latest/bin" >> "$GITHUB_PATH" + echo "${ANDROID_HOME}/platform-tools" >> "$GITHUB_PATH" + echo "ANDROID_HOME=${ANDROID_HOME}" + echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${NDK_VERSION}" diff --git a/.github/actions/setup-apple/action.yml b/.github/actions/setup-apple/action.yml new file mode 100644 index 00000000..042b573f --- /dev/null +++ b/.github/actions/setup-apple/action.yml @@ -0,0 +1,113 @@ +name: Set up Apple +description: Select and verify the supported Xcode toolchain for Apple SDK jobs. + +inputs: + xcode-minor: + description: Supported Xcode minor version installed on the pinned macOS runner image. + required: false + default: "26.5" + +runs: + using: composite + steps: + - name: Select and verify Apple toolchain + shell: bash + env: + REQUESTED_XCODE_MINOR: ${{ inputs.xcode-minor }} + SUPPORTED_XCODE_MINOR: "26.5" + run: | # zizmor: ignore[github-env] DEVELOPER_DIR is an exact, allowlisted runner-owned Xcode bundle path. + set -euo pipefail + + if [[ "$REQUESTED_XCODE_MINOR" != "$SUPPORTED_XCODE_MINOR" ]]; then + echo "setup-apple: unsupported Xcode minor '$REQUESTED_XCODE_MINOR'; expected $SUPPORTED_XCODE_MINOR" >&2 + exit 1 + fi + + developer_dir="/Applications/Xcode_26.5.app/Contents/Developer" + if [[ ! -d "$developer_dir" ]]; then + echo "setup-apple: required Xcode developer directory was not found at $developer_dir" >&2 + exit 1 + fi + if [[ -z "${GITHUB_ENV:-}" ]]; then + echo "setup-apple: GITHUB_ENV is not set" >&2 + exit 1 + fi + + export DEVELOPER_DIR="$developer_dir" + xcode_version_output="$(xcodebuild -version)" + observed_xcode_version="$(printf '%s\n' "$xcode_version_output" | sed -n '1s/^Xcode //p')" + xcode_build="$(printf '%s\n' "$xcode_version_output" | sed -n 's/^Build version //p')" + if [[ "$observed_xcode_version" != "$SUPPORTED_XCODE_MINOR" ]]; then + echo "setup-apple: $developer_dir reported Xcode '$observed_xcode_version'; expected $SUPPORTED_XCODE_MINOR" >&2 + exit 1 + fi + if [[ -z "$xcode_build" ]]; then + echo "setup-apple: xcodebuild did not report an Xcode build version" >&2 + exit 1 + fi + + macos_sdk="$(xcrun --sdk macosx --show-sdk-version)" + iphoneos_sdk="$(xcrun --sdk iphoneos --show-sdk-version)" + iphonesimulator_sdk="$(xcrun --sdk iphonesimulator --show-sdk-version)" + for sdk_record in \ + "macOS SDK:$macos_sdk" \ + "iPhoneOS SDK:$iphoneos_sdk" \ + "iPhoneSimulator SDK:$iphonesimulator_sdk"; do + sdk_name="${sdk_record%%:*}" + sdk_version="${sdk_record#*:}" + if [[ ! "$sdk_version" =~ ^[0-9]+([.][0-9]+)*$ ]]; then + echo "setup-apple: $sdk_name reported invalid version '$sdk_version'" >&2 + exit 1 + fi + done + if [[ -z "${ImageOS:-}" || -z "${ImageVersion:-}" ]]; then + echo "setup-apple: hosted runner ImageOS and ImageVersion provenance must be available" >&2 + exit 1 + fi + + printf 'DEVELOPER_DIR=%s\n' "$developer_dir" >> "$GITHUB_ENV" + printf 'Apple toolchain: Xcode=%s build=%s DEVELOPER_DIR=%s\n' \ + "$observed_xcode_version" "$xcode_build" "$developer_dir" + printf 'Apple SDKs: macOS=%s iPhoneOS=%s iPhoneSimulator=%s\n' \ + "$macos_sdk" "$iphoneos_sdk" "$iphonesimulator_sdk" + printf 'Apple runner image: ImageOS=%s ImageVersion=%s\n' "$ImageOS" "$ImageVersion" + + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + { + printf '### Apple toolchain\n\n' + printf '| Property | Observed value |\n| --- | --- |\n' + printf '| ImageOS | `%s` |\n' "$ImageOS" + printf '| ImageVersion | `%s` |\n' "$ImageVersion" + printf '| Xcode | `%s` |\n' "$observed_xcode_version" + printf '| Xcode build | `%s` |\n' "$xcode_build" + printf '| macOS SDK | `%s` |\n' "$macos_sdk" + printf '| iPhoneOS SDK | `%s` |\n' "$iphoneos_sdk" + printf '| iPhoneSimulator SDK | `%s` |\n' "$iphonesimulator_sdk" + } >> "$GITHUB_STEP_SUMMARY" + fi + + ruby --version + gem --version + + - name: Install Apple build dependencies + shell: bash + run: | + .github/scripts/prepare-macos-homebrew.sh + if ! brew list bison >/dev/null 2>&1; then + installed=0 + for attempt in 1 2 3; do + if HOMEBREW_NO_AUTO_UPDATE=1 brew install bison; then + installed=1 + break + fi + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 15)) + fi + done + if [ "$installed" != "1" ]; then + echo "setup-apple: Homebrew failed to install bison after 3 attempts" >&2 + exit 1 + fi + fi + bison_prefix="$(brew --prefix bison)" + "$bison_prefix/bin/bison" --version | head -n 1 diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml new file mode 100644 index 00000000..1e58243d --- /dev/null +++ b/.github/actions/setup-bun/action.yml @@ -0,0 +1,37 @@ +name: Set up Bun +description: Install the pinned Bun toolchain for TypeScript npm consumer checks. + +inputs: + bun-version: + description: Bun version. + required: false + default: "1.3.14" + +outputs: + execution-envelope: + description: Absolute digest-verified Bun execution envelope. + value: ${{ steps.install.outputs.execution-envelope }} + +runs: + using: composite + steps: + - name: Install verified Bun + id: install + shell: bash + env: + BUN_VERSION_INPUT: ${{ inputs.bun-version }} + OLIPHAUNT_PINNED_TOOL_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-pinned-tools + run: | # zizmor: ignore[github-env] the installer returns a digest- and version-verified runner-temp path. + set -euo pipefail + binary="$(tools/dev/install-pinned-js-runtime.sh bun --expected-version "$BUN_VERSION_INPUT")" + binary_dir="$(dirname "$binary")" + execution_envelope="$(cd "$binary_dir/.." && pwd -P)" + output_envelope="$execution_envelope" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + binary_dir="$(cygpath -w "$binary_dir")" + output_envelope="$(cygpath -w "$execution_envelope")" + fi + echo "$binary_dir" >> "$GITHUB_PATH" + echo "execution-envelope=$output_envelope" >> "$GITHUB_OUTPUT" + "$binary" --version diff --git a/.github/actions/setup-deno/action.yml b/.github/actions/setup-deno/action.yml new file mode 100644 index 00000000..d91bb920 --- /dev/null +++ b/.github/actions/setup-deno/action.yml @@ -0,0 +1,37 @@ +name: Set up Deno +description: Install the pinned Deno toolchain for TypeScript/JSR release checks. + +inputs: + deno-version: + description: Deno version. + required: false + default: "v2.8.1" + +outputs: + execution-envelope: + description: Absolute digest-verified Deno execution envelope. + value: ${{ steps.install.outputs.execution-envelope }} + +runs: + using: composite + steps: + - name: Install verified Deno + id: install + shell: bash + env: + DENO_VERSION_INPUT: ${{ inputs.deno-version }} + OLIPHAUNT_PINNED_TOOL_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-pinned-tools + run: | # zizmor: ignore[github-env] the installer returns a digest- and version-verified runner-temp path. + set -euo pipefail + binary="$(tools/dev/install-pinned-js-runtime.sh deno --expected-version "$DENO_VERSION_INPUT")" + binary_dir="$(dirname "$binary")" + execution_envelope="$(cd "$binary_dir/.." && pwd -P)" + output_envelope="$execution_envelope" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + binary_dir="$(cygpath -w "$binary_dir")" + output_envelope="$(cygpath -w "$execution_envelope")" + fi + echo "$binary_dir" >> "$GITHUB_PATH" + echo "execution-envelope=$output_envelope" >> "$GITHUB_OUTPUT" + "$binary" --version diff --git a/.github/actions/setup-maestro/action.yml b/.github/actions/setup-maestro/action.yml new file mode 100644 index 00000000..332bd595 --- /dev/null +++ b/.github/actions/setup-maestro/action.yml @@ -0,0 +1,15 @@ +name: Set up Maestro +description: Install the pinned open-source Maestro CLI for local emulator/simulator E2E. + +runs: + using: composite + steps: + - name: Set up Java + uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a + with: + distribution: temurin + java-version: "17" + + - name: Install Maestro CLI + shell: bash + run: tools/dev/setup-maestro.sh diff --git a/.github/actions/setup-moon/action.yml b/.github/actions/setup-moon/action.yml new file mode 100644 index 00000000..4bfacb06 --- /dev/null +++ b/.github/actions/setup-moon/action.yml @@ -0,0 +1,90 @@ +name: Set up Moon +description: Install verified Node.js, Moon, pnpm, and Bun binaries and optionally hydrate JavaScript workspace dependencies. + +inputs: + install-workspace: + description: Install pnpm workspace dependencies for JavaScript-family tasks. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Set up exact Node.js + uses: ./.github/actions/setup-node-runtime + with: + node-version: "22.22.3" + + - name: Restore verified tool archives + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: | + ${{ runner.temp }}/oliphaunt-moon-toolchain + ${{ runner.temp }}/oliphaunt-pinned-tools + key: verified-moon-toolchain-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.prototools', '.moon/toolchains.yml', '.github/actions/setup-moon/action.yml', '.github/actions/setup-moon/install-pinned-toolchain.sh', '.github/actions/setup-moon/toolchain-archive.py', 'src/sources/toolchains/moon-cli.toml', 'src/sources/toolchains/moon-plugins.toml', 'src/sources/toolchains/pnpm.toml', 'src/sources/toolchains/proto.toml', 'src/sources/toolchains/bun.toml', 'tools/dev/install-pinned-js-runtime.sh', 'tools/dev/extract-pinned-zip.sh', 'tools/dev/curl-platform-flags.sh') }} + + - name: Install verified Moon and pnpm + shell: bash + env: + OLIPHAUNT_MOON_TOOLCHAIN_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-moon-toolchain + run: | # zizmor: ignore[github-env] only digest-verified runner-temp paths are exported. + set -euo pipefail + toolchain_root="$(bash .github/actions/setup-moon/install-pinned-toolchain.sh)" + moon_home="$RUNNER_TEMP/oliphaunt-moon-home" + moon_home_fs="$moon_home" + moon_home_env="$moon_home" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + moon_home_fs="$(cygpath -u "$moon_home")" + moon_home_env="$(cygpath -w "$moon_home_fs")" + fi + rm -rf "$moon_home_fs" + mkdir -p "$moon_home_fs/plugins/toolchains" + cp "$toolchain_root/plugins/"*.wasm "$moon_home_fs/plugins/toolchains/" + toolchain_bin="$toolchain_root/bin" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + toolchain_bin="$(cygpath -w "$toolchain_bin")" + fi + echo "$toolchain_bin" >> "$GITHUB_PATH" + echo "MOON_HOME=$moon_home_env" >> "$GITHUB_ENV" + echo "MOON_TOOLCHAIN_FORCE_GLOBALS=true" >> "$GITHUB_ENV" + + - name: Install verified Bun + shell: bash + env: + OLIPHAUNT_PINNED_TOOL_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-pinned-tools + run: | # zizmor: ignore[github-env] the installer returns a digest- and version-verified runner-temp path. + set -euo pipefail + binary="$(bash tools/dev/install-pinned-js-runtime.sh bun --expected-version 1.3.14)" + export_dir="$RUNNER_TEMP/oliphaunt-bun-path" + export_dir_fs="$export_dir" + github_path_entry="$export_dir" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + export_dir_fs="$(cygpath -u "$export_dir")" + github_path_entry="$(cygpath -w "$export_dir_fs")" + fi + rm -rf "$export_dir_fs" + mkdir -p "$export_dir_fs" + cp "$binary" "$export_dir_fs/$(basename "$binary")" + chmod 0555 "$export_dir_fs/$(basename "$binary")" + echo "$github_path_entry" >> "$GITHUB_PATH" + + - name: Verify toolchain + shell: bash + run: | + set -euo pipefail + [[ "$(moon --version)" == "moon 2.3.2" ]] + [[ "$(node --version)" == "v22.22.3" ]] + [[ "$(pnpm --version)" == "11.5.0" ]] + [[ "$(bun --version)" == "1.3.14" ]] + + - name: Install workspace dependencies + if: ${{ inputs.install-workspace == 'true' }} + shell: bash + run: pnpm install --frozen-lockfile + + - name: Hydrate Moon plugins + shell: bash + run: moon query projects >/dev/null diff --git a/.github/actions/setup-moon/install-pinned-node.sh b/.github/actions/setup-moon/install-pinned-node.sh new file mode 100755 index 00000000..94517fd1 --- /dev/null +++ b/.github/actions/setup-moon/install-pinned-node.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "install-pinned-node.sh: $*" >&2 + exit 1 +} + +root="${OLIPHAUNT_NODE_RUNTIME_ROOT:-}" +if [ -z "$root" ]; then + root="$(git rev-parse --show-toplevel 2>/dev/null)" || + fail "must run inside the Oliphaunt checkout" +fi + +action_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +manifest="${OLIPHAUNT_NODE_RUNTIME_MANIFEST:-$root/src/sources/toolchains/node-runtime.toml}" +proto_file="${OLIPHAUNT_NODE_RUNTIME_PROTO_FILE:-$root/.prototools}" +extractor="${OLIPHAUNT_NODE_RUNTIME_ARCHIVE_EXTRACTOR:-$action_dir/toolchain-archive.py}" +curl_platform_flags="$root/tools/dev/curl-platform-flags.sh" +cache_root="${OLIPHAUNT_NODE_RUNTIME_CACHE_ROOT:-${RUNNER_TEMP:-$root/target}/oliphaunt-node-runtime}" + +case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) + if command -v cygpath >/dev/null 2>&1; then + cache_root="$(cygpath -u "$cache_root")" + fi + ;; +esac + +for path in "$manifest" "$proto_file" "$extractor" "$curl_platform_flags"; do + [ -f "$path" ] && [ ! -L "$path" ] || fail "missing regular bootstrap input: $path" +done + +# shellcheck source=tools/dev/curl-platform-flags.sh +. "$curl_platform_flags" + +python="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python="$candidate" + break + fi +done +[ -n "$python" ] || fail "python3 or python is required for safe archive extraction" + +manifest_value() { + local section="$1" + local key="$2" + awk -v wanted_section="$section" -v wanted_key="$key" ' + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + current=$0 + gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", current) + next + } + current == wanted_section && $0 ~ "^[[:space:]]*" wanted_key "[[:space:]]*=" { + count++ + line=$0 + sub(/^[^=]*=[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line) + value=line + } + END { if (count != 1 || value == "") exit 1; print value } + ' "$manifest" +} + +proto_version() { + awk -F '=' ' + $1 ~ "^[[:space:]]*node[[:space:]]*$" { + count++ + value=$2 + gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value) + } + END { if (count != 1 || value == "") exit 1; print value } + ' "$proto_file" +} + +validate_digest() { + local label="$1" + local digest="$2" + [ "${#digest}" -eq 64 ] && [[ ! "$digest" =~ [^0-9a-f] ]] || + fail "$label must contain exactly 64 lowercase hexadecimal characters" +} + +validate_count() { + local label="$1" + local value="$2" + case "$value" in + '' | *[!0-9]*) fail "$label must be a positive integer" ;; + esac + [ "$value" -gt 0 ] || fail "$label must be a positive integer" +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + "$python" - "$1" <<'PY' +import hashlib +import pathlib +import sys + +digest = hashlib.sha256() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +node_version="$(manifest_value toolchain version)" || + fail "$manifest must contain exactly one quoted toolchain.version" +[[ "$node_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid Node.js version: $node_version" +configured="$(proto_version)" || fail "$proto_file must contain exactly one node version" +configured="${configured#v}" +[ "$configured" = "$node_version" ] || + fail "$proto_file node version $configured does not match pinned version $node_version" + +if [ -n "${OLIPHAUNT_NODE_RUNTIME_TARGET:-}" ]; then + [ "${OLIPHAUNT_NODE_RUNTIME_TESTING:-0}" = "1" ] || + fail "OLIPHAUNT_NODE_RUNTIME_TARGET is test-only" + target="$OLIPHAUNT_NODE_RUNTIME_TARGET" +else + case "$(uname -s):$(uname -m)" in + Darwin:arm64 | Darwin:aarch64) target="aarch64-apple-darwin" ;; + Darwin:x86_64) target="x86_64-apple-darwin" ;; + Linux:arm64 | Linux:aarch64) target="aarch64-unknown-linux-gnu" ;; + Linux:x86_64) target="x86_64-unknown-linux-gnu" ;; + MINGW*:x86_64 | MSYS*:x86_64 | CYGWIN*:x86_64 | MINGW*:AMD64 | MSYS*:AMD64 | CYGWIN*:AMD64) + target="x86_64-pc-windows-msvc" + ;; + *) fail "unsupported Node.js host: $(uname -s)-$(uname -m)" ;; + esac +fi + +section="assets.$target" +url="$(manifest_value "$section" url)" || fail "$manifest is missing $section.url" +archive_sha256="$(manifest_value "$section" sha256)" || fail "$manifest is missing $section.sha256" +archive_bytes="$(manifest_value "$section" bytes)" || fail "$manifest is missing $section.bytes" +archive_format="$(manifest_value "$section" format)" || fail "$manifest is missing $section.format" +binary_path="$(manifest_value "$section" binary_path)" || fail "$manifest is missing $section.binary_path" +binary_sha256="$(manifest_value "$section" binary_sha256)" || fail "$manifest is missing $section.binary_sha256" +binary_bytes="$(manifest_value "$section" binary_bytes)" || fail "$manifest is missing $section.binary_bytes" + +case "$target" in + aarch64-apple-darwin) archive_name="node-v$node_version-darwin-arm64.tar.gz"; expected_format="tar.gz"; expected_binary="node-v$node_version-darwin-arm64/bin/node"; binary_name="node" ;; + x86_64-apple-darwin) archive_name="node-v$node_version-darwin-x64.tar.gz"; expected_format="tar.gz"; expected_binary="node-v$node_version-darwin-x64/bin/node"; binary_name="node" ;; + aarch64-unknown-linux-gnu) archive_name="node-v$node_version-linux-arm64.tar.xz"; expected_format="tar.xz"; expected_binary="node-v$node_version-linux-arm64/bin/node"; binary_name="node" ;; + x86_64-unknown-linux-gnu) archive_name="node-v$node_version-linux-x64.tar.xz"; expected_format="tar.xz"; expected_binary="node-v$node_version-linux-x64/bin/node"; binary_name="node" ;; + x86_64-pc-windows-msvc) archive_name="node-v$node_version-win-x64.zip"; expected_format="zip"; expected_binary="node-v$node_version-win-x64/node.exe"; binary_name="node.exe" ;; + *) fail "unsupported pinned Node.js target: $target" ;; +esac +expected_url="https://nodejs.org/download/release/v$node_version/$archive_name" +[ "$url" = "$expected_url" ] || fail "$manifest $section.url must be $expected_url" +[ "$archive_format" = "$expected_format" ] || fail "$manifest $section.format must be $expected_format" +[ "$binary_path" = "$expected_binary" ] || fail "$manifest $section.binary_path must be $expected_binary" +validate_digest "$manifest $section.sha256" "$archive_sha256" +validate_digest "$manifest $section.binary_sha256" "$binary_sha256" +validate_count "$manifest $section.bytes" "$archive_bytes" +validate_count "$manifest $section.binary_bytes" "$binary_bytes" + +curl_command="${OLIPHAUNT_NODE_CURL:-curl}" +command -v "$curl_command" >/dev/null 2>&1 || fail "missing required command: $curl_command" +command -v mktemp >/dev/null 2>&1 || fail "missing required command: mktemp" + +if [ -L "$cache_root" ]; then + fail "Node.js cache root must not be a symbolic link: $cache_root" +fi +umask 077 +mkdir -p "$cache_root" +[ -d "$cache_root" ] && [ ! -L "$cache_root" ] || fail "Node.js cache root is not a real directory" +archive_root="$cache_root/archives" +install_parent="$cache_root/installations/node-$node_version" +for directory in "$archive_root" "$cache_root/installations" "$install_parent"; do + [ ! -L "$directory" ] || fail "Node.js cache must not contain symbolic-link directories" + mkdir -p "$directory" + [ -d "$directory" ] && [ ! -L "$directory" ] || fail "Node.js cache directory is not real: $directory" +done + +final="$install_parent/$target" +binary="$final/bin/$binary_name" +receipt_text="$(printf 'node_version=%s\ntarget=%s\narchive_sha256=%s\nbinary_sha256=%s\nbinary_bytes=%s' \ + "$node_version" "$target" "$archive_sha256" "$binary_sha256" "$binary_bytes")" + +binary_version() { + "$1" --version 2>/dev/null | awk 'NF { print $1; exit }' +} + +cache_valid() { + local candidate="$1" + local candidate_binary="$candidate/bin/$binary_name" + [ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1 + [ "$(find "$candidate" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "2" ] || return 1 + [ -d "$candidate/bin" ] && [ ! -L "$candidate/bin" ] || return 1 + [ "$(find "$candidate/bin" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "1" ] || return 1 + [ -f "$candidate_binary" ] && [ ! -L "$candidate_binary" ] || return 1 + [ -f "$candidate/receipt" ] && [ ! -L "$candidate/receipt" ] || return 1 + [ "$(wc -c <"$candidate_binary" | tr -d '[:space:]')" = "$binary_bytes" ] || return 1 + [ "$(sha256_file "$candidate_binary")" = "$binary_sha256" ] || return 1 + case "$target" in + x86_64-pc-windows-msvc) ;; + *) [ -x "$candidate_binary" ] || return 1 ;; + esac + [ "$(binary_version "$candidate_binary")" = "v$node_version" ] || return 1 + [ "$(cat "$candidate/receipt")" = "$receipt_text" ] || return 1 +} + +if cache_valid "$final"; then + printf '%s\n' "$binary" + exit 0 +fi + +archive="$archive_root/$archive_sha256.$archive_format" +archive_valid=0 +if [ -f "$archive" ] && [ ! -L "$archive" ] && + [ "$(wc -c <"$archive" | tr -d '[:space:]')" = "$archive_bytes" ] && + [ "$(sha256_file "$archive")" = "$archive_sha256" ]; then + archive_valid=1 +fi +if [ "$archive_valid" != "1" ]; then + rm -f "$archive" + partial="$(mktemp "$archive_root/.download.XXXXXX")" + curl_tls_flag="$(oliphaunt_curl_platform_tls_flag)" + curl_args=( + --fail --location --silent --show-error + --proto '=https' --proto-redir '=https' --tlsv1.2 + --retry 5 --retry-all-errors --retry-connrefused --retry-delay 2 --retry-max-time 300 + --connect-timeout 20 --max-time 300 --speed-limit 1024 --speed-time 30 + --remove-on-error --max-filesize "$archive_bytes" --output "$partial" + ) + if [ -n "$curl_tls_flag" ]; then + curl_args+=("$curl_tls_flag") + fi + curl_args+=("$url") + if ! "$curl_command" "${curl_args[@]}"; then + rm -f "$partial" + fail "could not download pinned Node.js archive $url" + fi + [ "$(wc -c <"$partial" | tr -d '[:space:]')" = "$archive_bytes" ] && + [ "$(sha256_file "$partial")" = "$archive_sha256" ] || { + rm -f "$partial" + fail "downloaded Node.js archive integrity mismatch for $url" + } + chmod 0444 "$partial" + mv "$partial" "$archive" +fi + +stage="$(mktemp -d "$install_parent/.$target.stage.XXXXXX")" +backup="" +old_moved=0 +cleanup() { + local rc="$?" + trap - EXIT HUP INT TERM + rm -rf "$stage" + if [ "$old_moved" = "1" ] && [ -n "$backup" ] && [ -e "$backup" ] && [ ! -e "$final" ]; then + mv "$backup" "$final" || rc=1 + elif [ -n "$backup" ]; then + rm -rf "$backup" + fi + exit "$rc" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +mkdir -p "$stage/bin" +"$python" "$extractor" extract-file \ + --archive "$archive" \ + --format "$archive_format" \ + --expected-bytes "$archive_bytes" \ + --member "$binary_path" \ + --member-bytes "$binary_bytes" \ + --member-sha256 "$binary_sha256" \ + --destination "$stage/bin/$binary_name" \ + --executable +chmod 0555 "$stage/bin/$binary_name" +printf '%s\n' "$receipt_text" >"$stage/receipt" +chmod 0444 "$stage/receipt" +cache_valid "$stage" || fail "staged Node.js runtime failed integrity or version validation" + +if [ -e "$final" ] || [ -L "$final" ]; then + backup="$(mktemp -d "$install_parent/.$target.backup.XXXXXX")" + rmdir "$backup" + mv "$final" "$backup" + old_moved=1 +fi +if [ "${OLIPHAUNT_NODE_RUNTIME_TEST_INTERRUPT_AFTER_BACKUP:-0}" = "1" ]; then + [ "${OLIPHAUNT_NODE_RUNTIME_TESTING:-0}" = "1" ] || + fail "OLIPHAUNT_NODE_RUNTIME_TEST_INTERRUPT_AFTER_BACKUP is test-only" + kill -TERM "$$" +fi +mv "$stage" "$final" +stage="" +if [ "$old_moved" = "1" ]; then + rm -rf "$backup" + backup="" + old_moved=0 +fi +printf '%s\n' "$binary" diff --git a/.github/actions/setup-moon/install-pinned-node.test.sh b/.github/actions/setup-moon/install-pinned-node.test.sh new file mode 100755 index 00000000..bdab9a01 --- /dev/null +++ b/.github/actions/setup-moon/install-pinned-node.test.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +installer="$root/.github/actions/setup-moon/install-pinned-node.sh" +action="$root/.github/actions/setup-node-runtime/action.yml" +extractor="$root/.github/actions/setup-moon/toolchain-archive.py" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +grep -Fq 'export_dir="$(cygpath -w "$export_dir")"' "$action" +grep -Fq 'echo "$export_dir" >> "$GITHUB_PATH"' "$action" + +mkdir -p "$work/payload/node-v22.22.3-linux-x64/bin" "$work/bin" +cat >"$work/payload/node-v22.22.3-linux-x64/bin/node" <<'EOF' +#!/usr/bin/env bash +if [ "${1:-}" = "--version" ]; then + echo v22.22.3 +else + echo "fixture Node only supports --version" >&2 + exit 2 +fi +EOF +chmod 0755 "$work/payload/node-v22.22.3-linux-x64/bin/node" +tar -C "$work/payload" -cJf "$work/node.tar.xz" node-v22.22.3-linux-x64 +archive_bytes="$(wc -c <"$work/node.tar.xz" | tr -d '[:space:]')" +archive_sha256="$(sha256sum "$work/node.tar.xz" | awk '{print $1}')" +binary="$work/payload/node-v22.22.3-linux-x64/bin/node" +binary_bytes="$(wc -c <"$binary" | tr -d '[:space:]')" +binary_sha256="$(sha256sum "$binary" | awk '{print $1}')" + +cat >"$work/node-runtime.toml" <"$work/.prototools" + +cat >"$work/bin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +output="" +last="" +joined=" $* " +for required in "--fail" "--location" "--proto =https" "--proto-redir =https" "--tlsv1.2" "--retry-all-errors" "--retry-connrefused" "--remove-on-error" "--max-filesize"; do + [[ "$joined" == *" $required "* ]] || { + echo "missing hardened curl argument: $required" >&2 + exit 91 + } +done +if [ "${RUNNER_OS:-}" = Windows ]; then + [[ "$joined" == *" --ssl-revoke-best-effort "* ]] || exit 92 +fi +while (($#)); do + case "$1" in + --output) output="$2"; shift 2 ;; + *) last="$1"; shift ;; + esac +done +[ "$last" = "https://nodejs.org/download/release/v22.22.3/node-v22.22.3-linux-x64.tar.xz" ] || exit 93 +printf '%s\n' "$joined" >>"$OLIPHAUNT_NODE_TEST_REQUEST_LOG" +cp "$OLIPHAUNT_NODE_TEST_ARCHIVE" "$output" +EOF +chmod 0755 "$work/bin/curl" + +common=( + OLIPHAUNT_NODE_RUNTIME_ROOT="$root" + OLIPHAUNT_NODE_RUNTIME_MANIFEST="$work/node-runtime.toml" + OLIPHAUNT_NODE_RUNTIME_PROTO_FILE="$work/.prototools" + OLIPHAUNT_NODE_RUNTIME_ARCHIVE_EXTRACTOR="$extractor" + OLIPHAUNT_NODE_RUNTIME_CACHE_ROOT="$work/cache" + OLIPHAUNT_NODE_RUNTIME_TARGET=x86_64-unknown-linux-gnu + OLIPHAUNT_NODE_RUNTIME_TESTING=1 + OLIPHAUNT_NODE_CURL="$work/bin/curl" + OLIPHAUNT_NODE_TEST_ARCHIVE="$work/node.tar.xz" + OLIPHAUNT_NODE_TEST_REQUEST_LOG="$work/requests.log" + RUNNER_OS=Windows +) + +installed="$(env "${common[@]}" bash "$installer")" +[ "$("$installed" --version)" = v22.22.3 ] +[ "$(wc -l <"$work/requests.log" | tr -d '[:space:]')" = 1 ] + +# A valid installation must return before any network command is invoked. +cached="$(env "${common[@]}" OLIPHAUNT_NODE_CURL=false bash "$installer")" +[ "$cached" = "$installed" ] +[ "$(wc -l <"$work/requests.log" | tr -d '[:space:]')" = 1 ] + +# A modified executable is rebuilt from the already verified archive, also offline. +chmod u+w "$installed" +printf 'tampered\n' >"$installed" +repaired="$(env "${common[@]}" OLIPHAUNT_NODE_CURL=false bash "$installer")" +[ "$repaired" = "$installed" ] +[ "$("$installed" --version)" = v22.22.3 ] + +# An interrupted replacement restores the previous directory instead of exposing a partial stage. +chmod u+w "$installed" +printf 'tampered again\n' >"$installed" +set +e +env "${common[@]}" OLIPHAUNT_NODE_CURL=false \ + OLIPHAUNT_NODE_RUNTIME_TEST_INTERRUPT_AFTER_BACKUP=1 bash "$installer" >/dev/null 2>&1 +status=$? +set -e +[ "$status" -eq 143 ] +grep -Fxq 'tampered again' "$installed" +env "${common[@]}" OLIPHAUNT_NODE_CURL=false bash "$installer" >/dev/null + +# Target overrides are unavailable outside explicit fault-test mode. +set +e +env "${common[@]}" OLIPHAUNT_NODE_RUNTIME_TESTING=0 bash "$installer" >/dev/null 2>&1 +status=$? +set -e +[ "$status" -ne 0 ] + +echo "Pinned Node.js bootstrap fault tests passed." diff --git a/.github/actions/setup-moon/install-pinned-toolchain.sh b/.github/actions/setup-moon/install-pinned-toolchain.sh new file mode 100755 index 00000000..c5cb63df --- /dev/null +++ b/.github/actions/setup-moon/install-pinned-toolchain.sh @@ -0,0 +1,756 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "install-pinned-toolchain.sh: $*" >&2 + exit 1 +} + +root="${OLIPHAUNT_MOON_TOOLCHAIN_ROOT:-}" +if [ -z "$root" ]; then + root="$(git rev-parse --show-toplevel 2>/dev/null)" || + fail "must run inside the Oliphaunt checkout" +fi + +action_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +moon_manifest="${OLIPHAUNT_MOON_MANIFEST:-$root/src/sources/toolchains/moon-cli.toml}" +pnpm_manifest="${OLIPHAUNT_PNPM_MANIFEST:-$root/src/sources/toolchains/pnpm.toml}" +proto_manifest="${OLIPHAUNT_PROTO_MANIFEST:-$root/src/sources/toolchains/proto.toml}" +plugin_manifest="${OLIPHAUNT_MOON_PLUGIN_MANIFEST:-$root/src/sources/toolchains/moon-plugins.toml}" +proto_file="${OLIPHAUNT_MOON_PROTO_FILE:-$root/.prototools}" +moon_config="${OLIPHAUNT_MOON_TOOLCHAINS_CONFIG:-$root/.moon/toolchains.yml}" +extractor="${OLIPHAUNT_MOON_ARCHIVE_EXTRACTOR:-$action_dir/toolchain-archive.py}" +curl_platform_flags="$root/tools/dev/curl-platform-flags.sh" +cache_root="${OLIPHAUNT_MOON_TOOLCHAIN_CACHE_ROOT:-${RUNNER_TEMP:-$root/target}/oliphaunt-moon-toolchain}" + +case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) + if command -v cygpath >/dev/null 2>&1; then + cache_root="$(cygpath -u "$cache_root")" + fi + ;; +esac + +for path in \ + "$moon_manifest" \ + "$pnpm_manifest" \ + "$proto_manifest" \ + "$plugin_manifest" \ + "$proto_file" \ + "$moon_config" \ + "$extractor" \ + "$curl_platform_flags"; do + [ -f "$path" ] && [ ! -L "$path" ] || fail "missing regular bootstrap input: $path" +done + +# shellcheck source=tools/dev/curl-platform-flags.sh +. "$curl_platform_flags" + +python="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python="$candidate" + break + fi +done +[ -n "$python" ] || fail "python3 or python is required for safe archive extraction" + +manifest_value() { + local manifest="$1" + local section="$2" + local key="$3" + awk -v wanted_section="$section" -v wanted_key="$key" ' + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + current=$0 + gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", current) + next + } + current == wanted_section && $0 ~ "^[[:space:]]*" wanted_key "[[:space:]]*=" { + count++ + line=$0 + sub(/^[^=]*=[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line) + value=line + } + END { + if (count != 1 || value == "") exit 1 + print value + } + ' "$manifest" +} + +prototool_version() { + local tool="$1" + awk -F '=' -v wanted="$tool" ' + $1 ~ "^[[:space:]]*" wanted "[[:space:]]*$" { + count++ + value=$2 + gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value) + } + END { if (count != 1 || value == "") exit 1; print value } + ' "$proto_file" +} + +moon_proto_version() { + awk ' + /^[[:space:]]*proto:[[:space:]]*$/ { proto_count++; in_proto=1; next } + in_proto && /^[^[:space:]]/ { in_proto=0 } + in_proto && /^[[:space:]]+version:[[:space:]]*"[^"]+"[[:space:]]*$/ { + version_count++ + line=$0 + sub(/^[^:]*:[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line) + value=line + } + END { + if (proto_count != 1 || version_count != 1 || value == "") exit 1 + print value + } + ' "$moon_config" +} + +validate_version() { + local label="$1" + local version="$2" + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid $label version: $version" +} + +validate_digest() { + local label="$1" + local digest="$2" + [ "${#digest}" -eq 64 ] && [[ ! "$digest" =~ [^0-9a-f] ]] || + fail "$label must contain exactly 64 lowercase hexadecimal characters" +} + +validate_sha512() { + local label="$1" + local digest="$2" + [ "${#digest}" -eq 128 ] && [[ ! "$digest" =~ [^0-9a-f] ]] || + fail "$label must contain exactly 128 lowercase hexadecimal characters" +} + +validate_count() { + local label="$1" + local value="$2" + case "$value" in + '' | *[!0-9]*) fail "$label must be a positive integer" ;; + esac + [ "$value" -gt 0 ] || fail "$label must be a positive integer" +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + "$python" - "$1" <<'PY' +import hashlib +import pathlib +import sys + +digest = hashlib.sha256() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +sha512_file() { + if command -v sha512sum >/dev/null 2>&1; then + sha512sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 512 "$1" | awk '{print $1}' + else + "$python" - "$1" <<'PY' +import hashlib +import pathlib +import sys + +digest = hashlib.sha512() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +moon_version="$(manifest_value "$moon_manifest" toolchain version)" || + fail "$moon_manifest must contain exactly one quoted toolchain.version" +pnpm_version="$(manifest_value "$pnpm_manifest" toolchain version)" || + fail "$pnpm_manifest must contain exactly one quoted toolchain.version" +proto_version="$(manifest_value "$proto_manifest" toolchain version)" || + fail "$proto_manifest must contain exactly one quoted toolchain.version" +validate_version Moon "$moon_version" +validate_version pnpm "$pnpm_version" +validate_version proto "$proto_version" + +for tool in moon pnpm; do + configured="$(prototool_version "$tool")" || + fail "$proto_file must contain exactly one $tool version" + configured="${configured#v}" + case "$tool" in + moon) expected="$moon_version" ;; + pnpm) expected="$pnpm_version" ;; + esac + [ "$configured" = "$expected" ] || + fail "$proto_file $tool version $configured does not match pinned version $expected" +done +configured_proto="$(moon_proto_version)" || + fail "$moon_config must contain exactly one quoted proto.version" +configured_proto="${configured_proto#v}" +[ "$configured_proto" = "$proto_version" ] || + fail "$moon_config proto version $configured_proto does not match pinned version $proto_version" + +if [ -n "${OLIPHAUNT_MOON_TOOLCHAIN_TARGET:-}" ]; then + [ "${OLIPHAUNT_MOON_TOOLCHAIN_TESTING:-0}" = "1" ] || + fail "OLIPHAUNT_MOON_TOOLCHAIN_TARGET is test-only" + target="$OLIPHAUNT_MOON_TOOLCHAIN_TARGET" +else + case "$(uname -s):$(uname -m)" in + Darwin:arm64 | Darwin:aarch64) target="aarch64-apple-darwin" ;; + Darwin:x86_64) target="x86_64-apple-darwin" ;; + Linux:arm64 | Linux:aarch64) target="aarch64-unknown-linux-gnu" ;; + Linux:x86_64) target="x86_64-unknown-linux-gnu" ;; + MINGW*:x86_64 | MSYS*:x86_64 | CYGWIN*:x86_64 | MINGW*:AMD64 | MSYS*:AMD64 | CYGWIN*:AMD64) + target="x86_64-pc-windows-msvc" + ;; + *) fail "unsupported Moon host: $(uname -s)-$(uname -m)" ;; + esac +fi + +moon_section="assets.$target" +moon_url="$(manifest_value "$moon_manifest" "$moon_section" url)" || fail "$moon_manifest is missing $moon_section.url" +moon_archive_sha256="$(manifest_value "$moon_manifest" "$moon_section" sha256)" || fail "$moon_manifest is missing $moon_section.sha256" +moon_archive_bytes="$(manifest_value "$moon_manifest" "$moon_section" bytes)" || fail "$moon_manifest is missing $moon_section.bytes" +moon_expanded_bytes="$(manifest_value "$moon_manifest" "$moon_section" expanded_bytes)" || fail "$moon_manifest is missing $moon_section.expanded_bytes" +moon_format="$(manifest_value "$moon_manifest" "$moon_section" format)" || fail "$moon_manifest is missing $moon_section.format" +moon_prefix="$(manifest_value "$moon_manifest" "$moon_section" prefix)" || fail "$moon_manifest is missing $moon_section.prefix" +moon_entry_count="$(manifest_value "$moon_manifest" "$moon_section" entry_count)" || fail "$moon_manifest is missing $moon_section.entry_count" +moon_binary_path="$(manifest_value "$moon_manifest" "$moon_section" binary_path)" || fail "$moon_manifest is missing $moon_section.binary_path" +moon_binary_sha256="$(manifest_value "$moon_manifest" "$moon_section" binary_sha256)" || fail "$moon_manifest is missing $moon_section.binary_sha256" +moon_companion_path="$(manifest_value "$moon_manifest" "$moon_section" companion_path)" || fail "$moon_manifest is missing $moon_section.companion_path" +moon_companion_sha256="$(manifest_value "$moon_manifest" "$moon_section" companion_sha256)" || fail "$moon_manifest is missing $moon_section.companion_sha256" + +case "$target" in + x86_64-pc-windows-msvc) + expected_moon_format="zip" + expected_moon_prefix="." + expected_moon_binary="moon.exe" + expected_moon_companion="moonx.exe" + expected_moon_entries="5" + moon_archive_suffix="zip" + moon_archive_executables=() + ;; + aarch64-apple-darwin | x86_64-apple-darwin | aarch64-unknown-linux-gnu | x86_64-unknown-linux-gnu) + expected_moon_format="tar.xz" + expected_moon_prefix="moon_cli-$target" + expected_moon_binary="moon" + expected_moon_companion="moonx" + expected_moon_entries="6" + moon_archive_suffix="tar.xz" + moon_archive_executables=("$expected_moon_binary" "$expected_moon_companion") + ;; + *) fail "unsupported pinned Moon target: $target" ;; +esac +expected_moon_url="https://github.com/moonrepo/moon/releases/download/v$moon_version/moon_cli-$target.$moon_archive_suffix" +[ "$moon_url" = "$expected_moon_url" ] || fail "$moon_manifest $moon_section.url must be $expected_moon_url" +[ "$moon_format" = "$expected_moon_format" ] || fail "$moon_manifest $moon_section.format must be $expected_moon_format" +[ "$moon_prefix" = "$expected_moon_prefix" ] || fail "$moon_manifest $moon_section.prefix must be $expected_moon_prefix" +[ "$moon_binary_path" = "$expected_moon_binary" ] || fail "$moon_manifest $moon_section.binary_path must be $expected_moon_binary" +[ "$moon_companion_path" = "$expected_moon_companion" ] || fail "$moon_manifest $moon_section.companion_path must be $expected_moon_companion" +[ "$moon_entry_count" = "$expected_moon_entries" ] || fail "$moon_manifest $moon_section.entry_count must be $expected_moon_entries" +for digest in "$moon_archive_sha256" "$moon_binary_sha256" "$moon_companion_sha256"; do + validate_digest "$moon_manifest $moon_section digest" "$digest" +done +for value in "$moon_archive_bytes" "$moon_expanded_bytes" "$moon_entry_count"; do + validate_count "$moon_manifest $moon_section count" "$value" +done + +pnpm_url="$(manifest_value "$pnpm_manifest" package url)" || fail "$pnpm_manifest is missing package.url" +pnpm_archive_sha256="$(manifest_value "$pnpm_manifest" package sha256)" || fail "$pnpm_manifest is missing package.sha256" +pnpm_archive_sha512="$(manifest_value "$pnpm_manifest" package sha512)" || fail "$pnpm_manifest is missing package.sha512" +pnpm_archive_bytes="$(manifest_value "$pnpm_manifest" package bytes)" || fail "$pnpm_manifest is missing package.bytes" +pnpm_expanded_bytes="$(manifest_value "$pnpm_manifest" package expanded_bytes)" || fail "$pnpm_manifest is missing package.expanded_bytes" +pnpm_format="$(manifest_value "$pnpm_manifest" package format)" || fail "$pnpm_manifest is missing package.format" +pnpm_prefix="$(manifest_value "$pnpm_manifest" package prefix)" || fail "$pnpm_manifest is missing package.prefix" +pnpm_entry_count="$(manifest_value "$pnpm_manifest" package entry_count)" || fail "$pnpm_manifest is missing package.entry_count" +pnpm_file_count="$(manifest_value "$pnpm_manifest" package file_count)" || fail "$pnpm_manifest is missing package.file_count" +pnpm_tree_sha256="$(manifest_value "$pnpm_manifest" package tree_sha256)" || fail "$pnpm_manifest is missing package.tree_sha256" +pnpm_executable_paths="$(manifest_value "$pnpm_manifest" package executable_paths)" || fail "$pnpm_manifest is missing package.executable_paths" +pnpm_binary_path="$(manifest_value "$pnpm_manifest" package binary_path)" || fail "$pnpm_manifest is missing package.binary_path" +pnpm_binary_sha256="$(manifest_value "$pnpm_manifest" package binary_sha256)" || fail "$pnpm_manifest is missing package.binary_sha256" +pnpm_companion_path="$(manifest_value "$pnpm_manifest" package companion_path)" || fail "$pnpm_manifest is missing package.companion_path" +pnpm_companion_sha256="$(manifest_value "$pnpm_manifest" package companion_sha256)" || fail "$pnpm_manifest is missing package.companion_sha256" +pnpm_payload_path="$(manifest_value "$pnpm_manifest" package payload_path)" || fail "$pnpm_manifest is missing package.payload_path" +pnpm_payload_sha256="$(manifest_value "$pnpm_manifest" package payload_sha256)" || fail "$pnpm_manifest is missing package.payload_sha256" + +expected_pnpm_url="https://registry.npmjs.org/pnpm/-/pnpm-$pnpm_version.tgz" +[ "$pnpm_url" = "$expected_pnpm_url" ] || fail "$pnpm_manifest package.url must be $expected_pnpm_url" +[ "$pnpm_format" = "tar.gz" ] || fail "$pnpm_manifest package.format must be tar.gz" +[ "$pnpm_prefix" = "package" ] || fail "$pnpm_manifest package.prefix must be package" +[ "$pnpm_binary_path" = "bin/pnpm.mjs" ] || fail "$pnpm_manifest package.binary_path must be bin/pnpm.mjs" +[ "$pnpm_companion_path" = "bin/pnpx.mjs" ] || fail "$pnpm_manifest package.companion_path must be bin/pnpx.mjs" +[ "$pnpm_payload_path" = "dist/pnpm.mjs" ] || fail "$pnpm_manifest package.payload_path must be dist/pnpm.mjs" +expected_pnpm_executable_paths="bin/pnpm.mjs,bin/pnpx.mjs,dist/node-gyp-bin/node-gyp,dist/node-gyp-bin/node-gyp.cmd,dist/node_modules/node-gyp/bin/node-gyp.js" +[ "$pnpm_executable_paths" = "$expected_pnpm_executable_paths" ] || + fail "$pnpm_manifest package.executable_paths must be $expected_pnpm_executable_paths" +IFS=',' read -r -a pnpm_executables <<<"$pnpm_executable_paths" +for digest in "$pnpm_archive_sha256" "$pnpm_tree_sha256" "$pnpm_binary_sha256" "$pnpm_companion_sha256" "$pnpm_payload_sha256"; do + validate_digest "$pnpm_manifest package digest" "$digest" +done +validate_sha512 "$pnpm_manifest package.sha512" "$pnpm_archive_sha512" +for value in "$pnpm_archive_bytes" "$pnpm_expanded_bytes" "$pnpm_entry_count" "$pnpm_file_count"; do + validate_count "$pnpm_manifest package count" "$value" +done + +plugin_records=() +for plugin_id in javascript node pnpm rust; do + section="plugins.$plugin_id" + locator="$(manifest_value "$plugin_manifest" "$section" locator)" || fail "$plugin_manifest is missing $section.locator" + repository="$(manifest_value "$plugin_manifest" "$section" repository)" || fail "$plugin_manifest is missing $section.repository" + manifest_sha256="$(manifest_value "$plugin_manifest" "$section" manifest_sha256)" || fail "$plugin_manifest is missing $section.manifest_sha256" + manifest_bytes="$(manifest_value "$plugin_manifest" "$section" manifest_bytes)" || fail "$plugin_manifest is missing $section.manifest_bytes" + blob_sha256="$(manifest_value "$plugin_manifest" "$section" blob_sha256)" || fail "$plugin_manifest is missing $section.blob_sha256" + blob_bytes="$(manifest_value "$plugin_manifest" "$section" bytes)" || fail "$plugin_manifest is missing $section.bytes" + cache_file="$(manifest_value "$plugin_manifest" "$section" cache_file)" || fail "$plugin_manifest is missing $section.cache_file" + case "$plugin_id" in + javascript) expected_repository="moonrepo/javascript_toolchain" ;; + node) expected_repository="moonrepo/node_toolchain" ;; + pnpm) expected_repository="moonrepo/node_depman_toolchain" ;; + rust) expected_repository="moonrepo/rust_toolchain" ;; + esac + [ "$repository" = "$expected_repository" ] || fail "$plugin_manifest $section.repository must be $expected_repository" + validate_digest "$plugin_manifest $section.manifest_sha256" "$manifest_sha256" + validate_count "$plugin_manifest $section.manifest_bytes" "$manifest_bytes" + validate_digest "$plugin_manifest $section.blob_sha256" "$blob_sha256" + validate_count "$plugin_manifest $section.bytes" "$blob_bytes" + expected_locator="registry://ghcr.io/$repository@sha256:$manifest_sha256" + [ "$locator" = "$expected_locator" ] || fail "$plugin_manifest $section.locator must be $expected_locator" + [[ "$cache_file" =~ ^${plugin_id}-[0-9a-f]{64}\.wasm$ ]] || fail "$plugin_manifest $section.cache_file is not a safe Moon cache name" + [ "$(grep -Fxc " plugin: \"$locator\"" "$moon_config")" = "1" ] || + fail "$moon_config must pin $plugin_id exactly once to $locator" + plugin_records+=("$plugin_id|$repository|$manifest_sha256|$manifest_bytes|$blob_sha256|$blob_bytes|$cache_file") +done + +for command_name in "${OLIPHAUNT_MOON_CURL:-curl}" mktemp node; do + command -v "$command_name" >/dev/null 2>&1 || fail "missing required command: $command_name" +done + +if [ -L "$cache_root" ]; then + fail "toolchain cache root must not be a symbolic link: $cache_root" +fi +umask 077 +mkdir -p "$cache_root" +[ -d "$cache_root" ] && [ ! -L "$cache_root" ] || fail "toolchain cache root is not a real directory: $cache_root" +archive_root="$cache_root/archives" +if [ -L "$archive_root" ]; then + fail "toolchain archive cache must not be a symbolic link: $archive_root" +fi +mkdir -p "$archive_root" +[ -d "$archive_root" ] && [ ! -L "$archive_root" ] || fail "toolchain archive cache is not a real directory" + +curl_tls_flag="$(oliphaunt_curl_platform_tls_flag)" +curl_common=( + --fail --location --silent --show-error + --proto '=https' --proto-redir '=https' --tlsv1.2 + --retry 5 --retry-all-errors --retry-connrefused --retry-delay 2 --retry-max-time 300 + --connect-timeout 20 --max-time 300 --speed-limit 1024 --speed-time 30 + --remove-on-error +) +if [ -n "$curl_tls_flag" ]; then + curl_common+=("$curl_tls_flag") +fi + +download_verified() { + local url="$1" + local expected_sha256="$2" + local expected_bytes="$3" + local output="$4" + local expected_sha512="${5:-}" + local bearer="${6:-}" + local actual_size + if [ -f "$output" ] && [ ! -L "$output" ]; then + actual_size="$(wc -c <"$output" | tr -d '[:space:]')" + if [ "$actual_size" = "$expected_bytes" ] && + [ "$(sha256_file "$output")" = "$expected_sha256" ] && + { [ -z "$expected_sha512" ] || [ "$(sha512_file "$output")" = "$expected_sha512" ]; }; then + return 0 + fi + fi + rm -f "$output" + local partial + partial="$(mktemp "$archive_root/.download.XXXXXX")" + local rc=0 + local args=("${curl_common[@]}" --max-filesize "$expected_bytes" --output "$partial") + if [ -n "$bearer" ]; then + args+=(--header "Authorization: Bearer $bearer") + fi + args+=("$url") + if "${OLIPHAUNT_MOON_CURL:-curl}" "${args[@]}"; then + : + else + rc=$? + rm -f "$partial" + return "$rc" + fi + actual_size="$(wc -c <"$partial" | tr -d '[:space:]')" + [ "$actual_size" = "$expected_bytes" ] || { + rm -f "$partial" + fail "downloaded byte-size mismatch for $url: expected $expected_bytes, got $actual_size" + } + [ "$(sha256_file "$partial")" = "$expected_sha256" ] || { + rm -f "$partial" + fail "downloaded SHA-256 mismatch for $url" + } + if [ -n "$expected_sha512" ] && [ "$(sha512_file "$partial")" != "$expected_sha512" ]; then + rm -f "$partial" + fail "downloaded SHA-512 mismatch for $url" + fi + chmod 0444 "$partial" + mv "$partial" "$output" +} + +registry_token() { + local repository="$1" + local response + response="$(mktemp "$archive_root/.token.XXXXXX")" + local args=( + "${curl_common[@]}" + --max-filesize 16384 + --output "$response" + "https://ghcr.io/token?scope=repository:$repository:pull" + ) + if ! "${OLIPHAUNT_MOON_CURL:-curl}" "${args[@]}"; then + rm -f "$response" + fail "could not obtain a bounded read-only GHCR token for $repository" + fi + local token + token="$($python - "$response" <<'PY' +import json +import pathlib +import sys + +data = pathlib.Path(sys.argv[1]).read_bytes() +if len(data) > 16384: + raise SystemExit(1) +value = json.loads(data).get("token") +if not isinstance(value, str): + raise SystemExit(1) +print(value) +PY + )" || { + rm -f "$response" + fail "GHCR returned an invalid token response for $repository" + } + rm -f "$response" + [ "${#token}" -le 8192 ] && [[ "$token" =~ ^[-A-Za-z0-9._~+/=]+$ ]] || + fail "GHCR returned an unsafe token for $repository" + printf '%s\n' "$token" +} + +download_oci_manifest() { + local repository="$1" + local digest="$2" + local expected_bytes="$3" + local output="$4" + local bearer="$5" + if [ -f "$output" ] && [ ! -L "$output" ] && + [ "$(wc -c <"$output" | tr -d '[:space:]')" = "$expected_bytes" ] && + [ "$(sha256_file "$output")" = "$digest" ]; then + return 0 + fi + rm -f "$output" + local partial headers + partial="$(mktemp "$archive_root/.oci-manifest.XXXXXX")" + headers="$(mktemp "$archive_root/.oci-headers.XXXXXX")" + local args=( + "${curl_common[@]}" + --max-filesize "$expected_bytes" + --header 'Accept: application/vnd.oci.image.manifest.v1+json' + --header "Authorization: Bearer $bearer" + --dump-header "$headers" + --output "$partial" + "https://ghcr.io/v2/$repository/manifests/sha256:$digest" + ) + if ! "${OLIPHAUNT_MOON_CURL:-curl}" "${args[@]}"; then + rm -f "$partial" "$headers" + fail "could not fetch pinned OCI manifest $repository@sha256:$digest" + fi + grep -Eiq '^content-type:[[:space:]]*application/vnd[.]oci[.]image[.]manifest[.]v1[+]json[[:space:]]*\r?$' "$headers" || { + rm -f "$partial" "$headers" + fail "OCI manifest response for $repository had an unexpected content type" + } + grep -Eiq "^docker-content-digest:[[:space:]]*sha256:${digest}[[:space:]]*\r?$" "$headers" || { + rm -f "$partial" "$headers" + fail "OCI registry did not confirm the requested manifest digest for $repository" + } + rm -f "$headers" + [ "$(wc -c <"$partial" | tr -d '[:space:]')" = "$expected_bytes" ] && + [ "$(sha256_file "$partial")" = "$digest" ] || { + rm -f "$partial" + fail "OCI manifest body integrity mismatch for $repository" + } + chmod 0444 "$partial" + mv "$partial" "$output" +} + +validate_oci_manifest() { + local manifest_path="$1" + local expected_blob_sha256="$2" + local expected_blob_bytes="$3" + "$python" - "$manifest_path" "$expected_blob_sha256" "$expected_blob_bytes" <<'PY' +import json +import pathlib +import sys + +value = json.loads(pathlib.Path(sys.argv[1]).read_bytes()) +expected_digest = f"sha256:{sys.argv[2]}" +expected_size = int(sys.argv[3]) +if value.get("schemaVersion") != 2: + raise SystemExit("OCI manifest schemaVersion must be 2") +if value.get("mediaType") != "application/vnd.oci.image.manifest.v1+json": + raise SystemExit("OCI manifest has the wrong mediaType") +layers = value.get("layers") +if not isinstance(layers, list): + raise SystemExit("OCI manifest layers must be an array") +wasm = [layer for layer in layers if isinstance(layer, dict) and layer.get("mediaType") == "application/wasm"] +if len(wasm) != 1 or wasm[0].get("digest") != expected_digest or wasm[0].get("size") != expected_size: + raise SystemExit("OCI manifest does not bind exactly one expected WASM blob") +PY +} + +identity="moon-$moon_version-pnpm-$pnpm_version" +install_parent="$cache_root/installations/$identity" +if [ -L "$cache_root/installations" ] || [ -L "$install_parent" ]; then + fail "toolchain installation cache must not contain symbolic-link directories" +fi +mkdir -p "$install_parent" +final="$install_parent/$target" +moon_exe="$expected_moon_binary" +moonx_exe="$expected_moon_companion" + +receipt_text="$(printf 'moon_version=%s\npnpm_version=%s\nproto_contract_version=%s\ntarget=%s\nmoon_archive_sha256=%s\npnpm_archive_sha256=%s\npnpm_tree_sha256=%s' \ + "$moon_version" \ + "$pnpm_version" \ + "$proto_version" \ + "$target" \ + "$moon_archive_sha256" \ + "$pnpm_archive_sha256" \ + "$pnpm_tree_sha256")" +pnpm_wrapper_text="$(printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../pnpm/bin/pnpm.mjs" "$@"')" +pnpx_wrapper_text="$(printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../pnpm/bin/pnpx.mjs" "$@"')" +pnpm_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\pnpm\bin\pnpm.mjs" %*')" +pnpx_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\pnpm\bin\pnpx.mjs" %*')" + +moon_binary_version() { + "$1" --version 2>/dev/null | awk '$1 == "moon" { print $2; exit }' +} + +pnpm_binary_version() { + node "$1" --version 2>/dev/null | awk 'NF { print $1; exit }' +} + +cache_valid() { + local candidate="$1" + [ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1 + [ "$(find "$candidate" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "4" ] || return 1 + [ -d "$candidate/bin" ] && [ ! -L "$candidate/bin" ] || return 1 + [ -d "$candidate/pnpm" ] && [ ! -L "$candidate/pnpm" ] || return 1 + [ -d "$candidate/plugins" ] && [ ! -L "$candidate/plugins" ] || return 1 + [ "$(find "$candidate/bin" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "6" ] || return 1 + for path in \ + "$candidate/bin/$moon_exe" \ + "$candidate/bin/$moonx_exe" \ + "$candidate/pnpm/$pnpm_binary_path" \ + "$candidate/pnpm/$pnpm_companion_path" \ + "$candidate/pnpm/$pnpm_payload_path" \ + "$candidate/bin/pnpm" \ + "$candidate/bin/pnpx" \ + "$candidate/bin/pnpm.cmd" \ + "$candidate/bin/pnpx.cmd" \ + "$candidate/receipt"; do + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + done + [ "$(sha256_file "$candidate/bin/$moon_exe")" = "$moon_binary_sha256" ] || return 1 + [ "$(sha256_file "$candidate/bin/$moonx_exe")" = "$moon_companion_sha256" ] || return 1 + [ "$(sha256_file "$candidate/pnpm/$pnpm_binary_path")" = "$pnpm_binary_sha256" ] || return 1 + [ "$(sha256_file "$candidate/pnpm/$pnpm_companion_path")" = "$pnpm_companion_sha256" ] || return 1 + [ "$(sha256_file "$candidate/pnpm/$pnpm_payload_path")" = "$pnpm_payload_sha256" ] || return 1 + [ "$(cat "$candidate/bin/pnpm")" = "$pnpm_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpx")" = "$pnpx_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpm.cmd")" = "$pnpm_cmd_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpx.cmd")" = "$pnpx_cmd_text" ] || return 1 + if [ "$target" != "x86_64-pc-windows-msvc" ]; then + [ -x "$candidate/bin/$moon_exe" ] && [ -x "$candidate/bin/$moonx_exe" ] || return 1 + [ -x "$candidate/bin/pnpm" ] && [ -x "$candidate/bin/pnpx" ] || return 1 + fi + local tree_result + local tree_args=(tree-digest --root "$candidate/pnpm") + local executable + for executable in "${pnpm_executables[@]}"; do + tree_args+=(--executable "$executable") + done + tree_result="$($python "$extractor" "${tree_args[@]}" 2>/dev/null)" || return 1 + [ "$tree_result" = "$pnpm_file_count $pnpm_tree_sha256" ] || return 1 + [ "$(moon_binary_version "$candidate/bin/$moon_exe")" = "$moon_version" ] || return 1 + [ "$(pnpm_binary_version "$candidate/pnpm/$pnpm_binary_path")" = "$pnpm_version" ] || return 1 + [ "$(cat "$candidate/receipt")" = "$receipt_text" ] || return 1 + local plugin_count=0 + for record in "${plugin_records[@]}"; do + IFS='|' read -r plugin_id repository manifest_sha256 manifest_bytes blob_sha256 blob_bytes cache_file <<<"$record" + path="$candidate/plugins/$cache_file" + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + [ "$(wc -c <"$path" | tr -d '[:space:]')" = "$blob_bytes" ] || return 1 + [ "$(sha256_file "$path")" = "$blob_sha256" ] || return 1 + plugin_count=$((plugin_count + 1)) + done + [ "$(find "$candidate/plugins" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "$plugin_count" ] || return 1 +} + +if cache_valid "$final"; then + printf '%s\n' "$final" + exit 0 +fi + +moon_archive="$archive_root/$moon_archive_sha256.$moon_archive_suffix" +pnpm_archive="$archive_root/$pnpm_archive_sha256.tgz" +download_verified "$moon_url" "$moon_archive_sha256" "$moon_archive_bytes" "$moon_archive" +download_verified "$pnpm_url" "$pnpm_archive_sha256" "$pnpm_archive_bytes" "$pnpm_archive" "$pnpm_archive_sha512" + +for record in "${plugin_records[@]}"; do + IFS='|' read -r plugin_id repository manifest_sha256 manifest_bytes blob_sha256 blob_bytes cache_file <<<"$record" + oci_manifest="$archive_root/$manifest_sha256.oci.json" + blob="$archive_root/$blob_sha256.wasm" + token="" + if ! { [ -f "$oci_manifest" ] && [ ! -L "$oci_manifest" ] && [ "$(wc -c <"$oci_manifest" | tr -d '[:space:]')" = "$manifest_bytes" ] && [ "$(sha256_file "$oci_manifest")" = "$manifest_sha256" ]; }; then + token="$(registry_token "$repository")" + download_oci_manifest "$repository" "$manifest_sha256" "$manifest_bytes" "$oci_manifest" "$token" + fi + validate_oci_manifest "$oci_manifest" "$blob_sha256" "$blob_bytes" || + fail "pinned OCI manifest does not bind the expected $plugin_id WASM blob" + if ! { [ -f "$blob" ] && [ ! -L "$blob" ] && [ "$(wc -c <"$blob" | tr -d '[:space:]')" = "$blob_bytes" ] && [ "$(sha256_file "$blob")" = "$blob_sha256" ]; }; then + if [ -z "$token" ]; then + token="$(registry_token "$repository")" + fi + download_verified \ + "https://ghcr.io/v2/$repository/blobs/sha256:$blob_sha256" \ + "$blob_sha256" \ + "$blob_bytes" \ + "$blob" \ + "" \ + "$token" + fi +done + +stage="$(mktemp -d "$install_parent/.$target.stage.XXXXXX")" +backup="" +old_moved=0 +cleanup() { + local rc="$?" + trap - EXIT HUP INT TERM + if [ -n "$stage" ]; then + rm -rf "$stage" + fi + if [ "$old_moved" = "1" ] && [ -n "$backup" ] && [ -e "$backup" ] && [ ! -e "$final" ]; then + mv "$backup" "$final" || rc=1 + elif [ -n "$backup" ]; then + rm -rf "$backup" + fi + exit "$rc" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +moon_extract="$stage/moon-extract" +pnpm_extract="$stage/pnpm" +moon_extract_args=( + extract \ + --archive "$moon_archive" \ + --format "$moon_format" \ + --prefix "$moon_prefix" \ + --entry-count "$moon_entry_count" \ + --expected-bytes "$moon_archive_bytes" \ + --expanded-bytes "$moon_expanded_bytes" \ + --destination "$moon_extract" \ + --required "$moon_binary_path" \ + --required "$moon_companion_path" +) +for executable in "${moon_archive_executables[@]}"; do + moon_extract_args+=(--executable "$executable") +done +"$python" "$extractor" "${moon_extract_args[@]}" +pnpm_extract_args=( + extract + --archive "$pnpm_archive" \ + --format "$pnpm_format" \ + --prefix "$pnpm_prefix" \ + --entry-count "$pnpm_entry_count" \ + --expected-bytes "$pnpm_archive_bytes" \ + --expanded-bytes "$pnpm_expanded_bytes" \ + --destination "$pnpm_extract" \ + --required "$pnpm_binary_path" \ + --required "$pnpm_companion_path" \ + --required "$pnpm_payload_path" \ + --required package.json +) +for executable in "${pnpm_executables[@]}"; do + pnpm_extract_args+=(--required "$executable" --executable "$executable") +done +"$python" "$extractor" "${pnpm_extract_args[@]}" + +mkdir -p "$stage/bin" "$stage/plugins" +mv "$moon_extract/$moon_binary_path" "$stage/bin/$moon_exe" +mv "$moon_extract/$moon_companion_path" "$stage/bin/$moonx_exe" +rm -rf "$moon_extract" +chmod 0555 "$stage/bin/$moon_exe" "$stage/bin/$moonx_exe" + +printf '%s\n' "$pnpm_wrapper_text" >"$stage/bin/pnpm" +printf '%s\n' "$pnpx_wrapper_text" >"$stage/bin/pnpx" +printf '%s\n' "$pnpm_cmd_text" >"$stage/bin/pnpm.cmd" +printf '%s\n' "$pnpx_cmd_text" >"$stage/bin/pnpx.cmd" +chmod 0555 "$stage/bin/pnpm" "$stage/bin/pnpx" +chmod 0444 "$stage/bin/pnpm.cmd" "$stage/bin/pnpx.cmd" + +for record in "${plugin_records[@]}"; do + IFS='|' read -r plugin_id repository manifest_sha256 manifest_bytes blob_sha256 blob_bytes cache_file <<<"$record" + cp "$archive_root/$blob_sha256.wasm" "$stage/plugins/$cache_file" + chmod 0444 "$stage/plugins/$cache_file" +done +printf '%s\n' "$receipt_text" >"$stage/receipt" +chmod 0444 "$stage/receipt" + +cache_valid "$stage" || fail "staged Moon toolchain failed integrity or version validation" + +if [ -e "$final" ] || [ -L "$final" ]; then + backup="$(mktemp -d "$install_parent/.$target.backup.XXXXXX")" + rmdir "$backup" + mv "$final" "$backup" + old_moved=1 +fi +if [ "${OLIPHAUNT_MOON_TOOLCHAIN_TEST_INTERRUPT_AFTER_BACKUP:-0}" = "1" ]; then + [ "${OLIPHAUNT_MOON_TOOLCHAIN_TESTING:-0}" = "1" ] || + fail "OLIPHAUNT_MOON_TOOLCHAIN_TEST_INTERRUPT_AFTER_BACKUP is test-only" + kill -TERM "$$" +fi +mv "$stage" "$final" +stage="" +if [ "$old_moved" = "1" ]; then + rm -rf "$backup" + backup="" + old_moved=0 +fi +printf '%s\n' "$final" diff --git a/.github/actions/setup-moon/install-pinned-toolchain.test.sh b/.github/actions/setup-moon/install-pinned-toolchain.test.sh new file mode 100755 index 00000000..915ff548 --- /dev/null +++ b/.github/actions/setup-moon/install-pinned-toolchain.test.sh @@ -0,0 +1,434 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +installer="$root/.github/actions/setup-moon/install-pinned-toolchain.sh" +extractor="$root/.github/actions/setup-moon/toolchain-archive.py" +action="$root/.github/actions/setup-moon/action.yml" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +fail() { + echo "install-pinned-toolchain.test.sh: $*" >&2 + exit 1 +} + +python="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python="$candidate" + break + fi +done +[ -n "$python" ] || fail "python3 or python is required" +grep -Fq 'moon_home_fs="$(cygpath -u "$moon_home")"' "$action" +grep -Fq 'moon_home_env="$(cygpath -w "$moon_home_fs")"' "$action" +grep -Fq 'echo "MOON_HOME=$moon_home_env" >> "$GITHUB_ENV"' "$action" +grep -Fq 'export_dir_fs="$(cygpath -u "$export_dir")"' "$action" +grep -Fq 'github_path_entry="$(cygpath -w "$export_dir_fs")"' "$action" +grep -Fq 'cp "$binary" "$export_dir_fs/$(basename "$binary")"' "$action" + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + shasum -a 256 "$1" | awk '{print $1}' + fi +} + +sha512_file() { + if command -v sha512sum >/dev/null 2>&1; then + sha512sum "$1" | awk '{print $1}' + else + shasum -a 512 "$1" | awk '{print $1}' + fi +} + +fixture="$tmp/fixture" +mkdir -p \ + "$fixture/.moon" \ + "$fixture/src/sources/toolchains" \ + "$fixture/tools/dev" \ + "$fixture/content/pnpm/bin" \ + "$fixture/content/pnpm/dist/node-gyp-bin" \ + "$fixture/content/pnpm/dist/node_modules/node-gyp/bin" \ + "$fixture/blobs" +cp "$root/tools/dev/curl-platform-flags.sh" "$fixture/tools/dev/curl-platform-flags.sh" + +moon_version="9.8.7" +pnpm_version="8.7.6" +proto_version="7.6.5" +moon_target="x86_64-unknown-linux-gnu" + +printf '%s\n' '#!/bin/sh' "echo 'moon $moon_version'" >"$fixture/content/moon" +printf '%s\n' '#!/bin/sh' "echo 'moonx $moon_version'" >"$fixture/content/moonx" +chmod 0755 "$fixture/content/moon" "$fixture/content/moonx" +printf '%s\n' 'fixture readme' >"$fixture/content/README.md" +printf '%s\n' 'fixture changelog' >"$fixture/content/CHANGELOG.md" +printf '%s\n' 'fixture license' >"$fixture/content/LICENSE" + +printf '%s\n' '#!/usr/bin/env node' "console.log('$pnpm_version');" >"$fixture/content/pnpm/bin/pnpm.mjs" +printf '%s\n' '#!/usr/bin/env node' "console.log('$pnpm_version');" >"$fixture/content/pnpm/bin/pnpx.mjs" +printf '%s\n' 'fixture pnpm payload' >"$fixture/content/pnpm/dist/pnpm.mjs" +printf '%s\n' '#!/bin/sh' 'exit 0' >"$fixture/content/pnpm/dist/node-gyp-bin/node-gyp" +printf '%s\r\n' '@exit /b 0' >"$fixture/content/pnpm/dist/node-gyp-bin/node-gyp.cmd" +printf '%s\n' '#!/usr/bin/env node' >"$fixture/content/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js" +printf '%s\n' '{"name":"pnpm-fixture"}' >"$fixture/content/pnpm/package.json" +chmod 0755 \ + "$fixture/content/pnpm/bin/pnpm.mjs" \ + "$fixture/content/pnpm/bin/pnpx.mjs" \ + "$fixture/content/pnpm/dist/node-gyp-bin/node-gyp" \ + "$fixture/content/pnpm/dist/node-gyp-bin/node-gyp.cmd" \ + "$fixture/content/pnpm/dist/node_modules/node-gyp/bin/node-gyp.js" + +moon_archive="$fixture/moon.tar.xz" +pnpm_archive="$fixture/pnpm.tgz" +"$python" - "$fixture/content" "$moon_archive" "$pnpm_archive" "$moon_target" <<'PY' +import io +import pathlib +import tarfile +import sys + +content = pathlib.Path(sys.argv[1]) +moon_archive = pathlib.Path(sys.argv[2]) +pnpm_archive = pathlib.Path(sys.argv[3]) +target = sys.argv[4] + +with tarfile.open(moon_archive, "w:xz", format=tarfile.PAX_FORMAT) as archive: + root = tarfile.TarInfo(f"moon_cli-{target}") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + for name, mode in [ + ("moon", 0o755), + ("moonx", 0o755), + ("README.md", 0o644), + ("CHANGELOG.md", 0o644), + ("LICENSE", 0o644), + ]: + payload = (content / name).read_bytes() + info = tarfile.TarInfo(f"moon_cli-{target}/{name}") + info.mode = mode + info.size = len(payload) + archive.addfile(info, io.BytesIO(payload)) + +pnpm = content / "pnpm" +with tarfile.open(pnpm_archive, "w:gz", format=tarfile.PAX_FORMAT) as archive: + root = tarfile.TarInfo("package") + root.type = tarfile.DIRTYPE + root.mode = 0o755 + archive.addfile(root) + for path in sorted(candidate for candidate in pnpm.rglob("*") if candidate.is_file()): + relative = path.relative_to(pnpm).as_posix() + payload = path.read_bytes() + info = tarfile.TarInfo(f"package/{relative}") + info.mode = path.stat().st_mode & 0o777 + info.size = len(payload) + archive.addfile(info, io.BytesIO(payload)) +PY + +moon_archive_sha256="$(sha256_file "$moon_archive")" +moon_archive_bytes="$(wc -c <"$moon_archive" | tr -d '[:space:]')" +moon_expanded_bytes="$(find "$fixture/content" -maxdepth 1 -type f -printf '%s\n' | awk '{sum += $1} END {print sum}')" +moon_sha256="$(sha256_file "$fixture/content/moon")" +moonx_sha256="$(sha256_file "$fixture/content/moonx")" +pnpm_archive_sha256="$(sha256_file "$pnpm_archive")" +pnpm_archive_sha512="$(sha512_file "$pnpm_archive")" +pnpm_archive_bytes="$(wc -c <"$pnpm_archive" | tr -d '[:space:]')" +pnpm_expanded_bytes="$(find "$fixture/content/pnpm" -type f -printf '%s\n' | awk '{sum += $1} END {print sum}')" +pnpm_tree_result="$("$python" "$extractor" tree-digest \ + --root "$fixture/content/pnpm" \ + --executable bin/pnpm.mjs \ + --executable bin/pnpx.mjs \ + --executable dist/node-gyp-bin/node-gyp \ + --executable dist/node-gyp-bin/node-gyp.cmd \ + --executable dist/node_modules/node-gyp/bin/node-gyp.js)" +pnpm_tree_sha256="${pnpm_tree_result#* }" +pnpm_binary_sha256="$(sha256_file "$fixture/content/pnpm/bin/pnpm.mjs")" +pnpm_companion_sha256="$(sha256_file "$fixture/content/pnpm/bin/pnpx.mjs")" +pnpm_payload_sha256="$(sha256_file "$fixture/content/pnpm/dist/pnpm.mjs")" + +cat >"$fixture/src/sources/toolchains/moon-cli.toml" <"$fixture/src/sources/toolchains/pnpm.toml" <"$fixture/src/sources/toolchains/proto.toml" <"$plugin_manifest" +moon_config="$fixture/.moon/toolchains.yml" +cat >"$moon_config" <"$fixture/blob-$plugin_id.wasm" + blob_sha256="$(sha256_file "$fixture/blob-$plugin_id.wasm")" + blob_bytes="$(wc -c <"$fixture/blob-$plugin_id.wasm" | tr -d '[:space:]')" + cp "$fixture/blob-$plugin_id.wasm" "$fixture/blobs/$blob_sha256" + printf '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","layers":[{"mediaType":"application/wasm","digest":"sha256:%s","size":%s}]}\n' \ + "$blob_sha256" "$blob_bytes" >"$fixture/manifest-$plugin_id.json" + manifest_sha256="$(sha256_file "$fixture/manifest-$plugin_id.json")" + manifest_bytes="$(wc -c <"$fixture/manifest-$plugin_id.json" | tr -d '[:space:]')" + cp "$fixture/manifest-$plugin_id.json" "$fixture/blobs/$manifest_sha256.manifest" + cat >>"$plugin_manifest" <>"$moon_config" <"$fixture/.prototools" <"$fake_curl" <<'SH' +#!/usr/bin/env bash +set -euo pipefail +output="" +headers="" +url="" +printf 'CALL' >>"$FAKE_CURL_LOG" +printf ' %q' "$@" >>"$FAKE_CURL_LOG" +printf '\n' >>"$FAKE_CURL_LOG" +while [ "$#" -gt 0 ]; do + case "$1" in + --output) + output="$2" + shift 2 + ;; + --dump-header) + headers="$2" + shift 2 + ;; + --header | --max-filesize | --retry | --retry-delay | --retry-max-time | --connect-timeout | --max-time | --speed-limit | --speed-time | --proto | --proto-redir) + shift 2 + ;; + https://*) + url="$1" + shift + ;; + *) shift ;; + esac +done +[ -n "$output" ] && [ -n "$url" ] || exit 64 +if [ "${FAKE_CURL_FAIL:-0}" = "1" ]; then + exit 22 +fi +case "$url" in + https://ghcr.io/token?*) printf '%s\n' '{"token":"fixture-token=="}' >"$output" ;; + https://ghcr.io/v2/*/manifests/sha256:*) + digest="${url##*:}" + cp "$FAKE_BLOB_DIR/$digest.manifest" "$output" + printf 'HTTP/1.1 200 OK\r\ncontent-type: application/vnd.oci.image.manifest.v1+json\r\ndocker-content-digest: sha256:%s\r\n\r\n' "$digest" >"$headers" + ;; + https://ghcr.io/v2/*/blobs/sha256:*) cp "$FAKE_BLOB_DIR/${url##*:}" "$output" ;; + https://github.com/moonrepo/moon/*) cp "$FAKE_MOON_ARCHIVE" "$output" ;; + https://registry.npmjs.org/pnpm/*) cp "$FAKE_PNPM_ARCHIVE" "$output" ;; + *) exit 65 ;; +esac +SH +chmod 0755 "$fake_curl" + +export OLIPHAUNT_MOON_TOOLCHAIN_ROOT="$fixture" +export OLIPHAUNT_MOON_MANIFEST="$fixture/src/sources/toolchains/moon-cli.toml" +export OLIPHAUNT_PNPM_MANIFEST="$fixture/src/sources/toolchains/pnpm.toml" +export OLIPHAUNT_PROTO_MANIFEST="$fixture/src/sources/toolchains/proto.toml" +export OLIPHAUNT_MOON_PLUGIN_MANIFEST="$plugin_manifest" +export OLIPHAUNT_MOON_PROTO_FILE="$fixture/.prototools" +export OLIPHAUNT_MOON_TOOLCHAINS_CONFIG="$moon_config" +export OLIPHAUNT_MOON_ARCHIVE_EXTRACTOR="$extractor" +export OLIPHAUNT_MOON_TOOLCHAIN_CACHE_ROOT="$tmp/cache" +export OLIPHAUNT_MOON_TOOLCHAIN_TARGET="$moon_target" +export OLIPHAUNT_MOON_TOOLCHAIN_TESTING=1 +export OLIPHAUNT_MOON_CURL="$fake_curl" +export FAKE_CURL_LOG="$tmp/curl.log" +export FAKE_MOON_ARCHIVE="$moon_archive" +export FAKE_PNPM_ARCHIVE="$pnpm_archive" +export FAKE_BLOB_DIR="$fixture/blobs" +export RUNNER_OS=Windows +: >"$FAKE_CURL_LOG" + +final="$(bash "$installer")" +[ -d "$final" ] || fail "installer did not return an installation directory" +[ "$("$final/bin/moon" --version)" = "moon $moon_version" ] || fail "wrong Moon version" +[ "$("$final/bin/pnpm" --version)" = "$pnpm_version" ] || fail "wrong pnpm version" +[ "$(find "$final/plugins" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "4" ] || fail "wrong plugin count" +[ "$(wc -l <"$FAKE_CURL_LOG" | tr -d '[:space:]')" = "14" ] || fail "unexpected first-install request count" +while IFS= read -r call; do + for flag in --ssl-revoke-best-effort --tlsv1.2 --retry-all-errors --retry-connrefused --max-filesize --max-time --speed-limit; do + [[ "$call" == *"$flag"* ]] || fail "curl request omitted $flag" + done +done <"$FAKE_CURL_LOG" + +# A fully valid cache must not consult the network at all. +OLIPHAUNT_MOON_CURL=false bash "$installer" >"$tmp/cache-hit" +[ "$(cat "$tmp/cache-hit")" = "$final" ] || fail "cache hit returned a different installation" + +# Full-tree validation repairs unpinned PATH entries and non-component pnpm files. +chmod u+w "$final/bin/pnpm" +printf '%s\n' 'malicious wrapper' >"$final/bin/pnpm" +printf '%s\n' 'shadow node' >"$final/bin/node" +printf '%s\n' 'mutated package metadata' >"$final/pnpm/package.json" +OLIPHAUNT_MOON_CURL=false bash "$installer" >"$tmp/repaired" +[ ! -e "$final/bin/node" ] || fail "cache repair retained an unexpected PATH entry" +[ "$("$final/bin/pnpm" --version)" = "$pnpm_version" ] || fail "cache repair did not restore pnpm" +grep -Fq 'pnpm-fixture' "$final/pnpm/package.json" || fail "tree-digest repair did not restore package metadata" + +# A corrupt cached archive is re-downloaded before rebuilding an invalid installation. +moon_cached="$tmp/cache/archives/$moon_archive_sha256.tar.xz" +chmod u+w "$moon_cached" +printf '%s\n' corrupt >"$moon_cached" +chmod u+w "$final/bin/moon" +printf '%s\n' corrupt >"$final/bin/moon" +before_requests="$(wc -l <"$FAKE_CURL_LOG" | tr -d '[:space:]')" +bash "$installer" >"$tmp/archive-repaired" +after_requests="$(wc -l <"$FAKE_CURL_LOG" | tr -d '[:space:]')" +[ "$after_requests" -eq $((before_requests + 1)) ] || fail "corrupt archive repair did not make exactly one request" +[ "$("$final/bin/moon" --version)" = "moon $moon_version" ] || fail "corrupt archive repair failed" + +# Promotion interruption restores the previous installation transactionally. +chmod u+w "$final/bin/pnpm" +printf '%s\n' 'previous installation' >"$final/bin/pnpm" +before_wrapper="$(sha256_file "$final/bin/pnpm")" +set +e +OLIPHAUNT_MOON_CURL=false \ + OLIPHAUNT_MOON_TOOLCHAIN_TEST_INTERRUPT_AFTER_BACKUP=1 \ + bash "$installer" >"$tmp/interrupted.out" 2>"$tmp/interrupted.err" +interrupt_status="$?" +set -e +[ "$interrupt_status" -eq 143 ] || fail "interruption hook returned $interrupt_status instead of 143" +[ "$(sha256_file "$final/bin/pnpm")" = "$before_wrapper" ] || fail "interrupted promotion did not restore the prior installation" +OLIPHAUNT_MOON_CURL=false bash "$installer" >/dev/null + +# Executable intent is part of the portable tree fingerprint on POSIX. +chmod 0644 "$final/pnpm/bin/pnpm.mjs" +set +e +"$python" "$extractor" tree-digest \ + --root "$final/pnpm" \ + --executable bin/pnpm.mjs \ + --executable bin/pnpx.mjs \ + --executable dist/node-gyp-bin/node-gyp \ + --executable dist/node-gyp-bin/node-gyp.cmd \ + --executable dist/node_modules/node-gyp/bin/node-gyp.js >/dev/null 2>"$tmp/mode.err" +mode_status="$?" +set -e +[ "$mode_status" -ne 0 ] || fail "tree digest accepted executable-mode drift" +chmod 0755 "$final/pnpm/bin/pnpm.mjs" + +# Reject traversal and non-zero directory payload metadata before extraction. +"$python" - "$tmp/unsafe.tar.xz" "$tmp/directory-payload.tar.xz" "$tmp/pax-payload.tar.xz" <<'PY' +import io +import tarfile +import sys + +with tarfile.open(sys.argv[1], "w:xz") as archive: + root = tarfile.TarInfo("root") + root.type = tarfile.DIRTYPE + archive.addfile(root) + payload = b"x" + bad = tarfile.TarInfo("root/../escape") + bad.size = len(payload) + archive.addfile(bad, io.BytesIO(payload)) + +with tarfile.open(sys.argv[2], "w:xz") as archive: + root = tarfile.TarInfo("root") + root.type = tarfile.DIRTYPE + root.size = 1 + archive.addfile(root, io.BytesIO(b"x")) + payload = b"x" + regular = tarfile.TarInfo("root/file") + regular.size = len(payload) + archive.addfile(regular, io.BytesIO(payload)) + +# Extended metadata is rejected from the raw stream before a decompression bomb +# can be materialized by tarfile's PAX parser. +with tarfile.open(sys.argv[3], "w:xz", format=tarfile.PAX_FORMAT) as archive: + payload = b"x" + regular = tarfile.TarInfo("root/file") + regular.size = len(payload) + regular.pax_headers = {"comment": "x" * (2 * 1024 * 1024)} + archive.addfile(regular, io.BytesIO(payload)) +PY + +for unsafe in "$tmp/unsafe.tar.xz" "$tmp/directory-payload.tar.xz" "$tmp/pax-payload.tar.xz"; do + set +e + "$python" "$extractor" extract \ + --archive "$unsafe" \ + --format tar.xz \ + --prefix root \ + --entry-count 2 \ + --expected-bytes "$(wc -c <"$unsafe" | tr -d '[:space:]')" \ + --expanded-bytes 1 \ + --destination "$unsafe.out" \ + --required file >/dev/null 2>"$tmp/unsafe.err" + unsafe_status="$?" + set -e + [ "$unsafe_status" -ne 0 ] || fail "unsafe archive was accepted: $unsafe" + [ ! -e "$unsafe.out" ] || fail "unsafe archive left a partial extraction tree" +done + +echo "Pinned Moon toolchain bootstrap fault tests passed." diff --git a/.github/actions/setup-moon/toolchain-archive.py b/.github/actions/setup-moon/toolchain-archive.py new file mode 100755 index 00000000..1276a475 --- /dev/null +++ b/.github/actions/setup-moon/toolchain-archive.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +"""Safely extract and fingerprint the pinned bootstrap tool archives.""" + +from __future__ import annotations + +import argparse +import gzip +import hashlib +import lzma +import os +import shutil +import stat +import sys +import tarfile +import unicodedata +import zipfile +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import BinaryIO, Iterable + + +COPY_BYTES = 1024 * 1024 +MAX_ARCHIVE_BYTES = 250 * 1024 * 1024 +MAX_EXPANDED_BYTES = 750 * 1024 * 1024 +MAX_ENTRY_BYTES = 250 * 1024 * 1024 +MAX_ENTRIES = 4096 +MAX_TARGET_SCAN_ENTRIES = 8192 +MAX_TARGET_SCAN_BYTES = 500 * 1024 * 1024 +MAX_PATH_BYTES = 4096 +MAX_COMPONENT_BYTES = 255 +WINDOWS_RESERVED_NAMES = { + "aux", + "con", + "nul", + "prn", + *(f"com{number}" for number in range(1, 10)), + *(f"lpt{number}" for number in range(1, 10)), + "com¹", + "com²", + "com³", + "conin$", + "conout$", + "clock$", + "lpt¹", + "lpt²", + "lpt³", +} + + +class UnsafeArchive(ValueError): + pass + + +@dataclass(frozen=True) +class Entry: + name: str + relative: str + size: int + directory: bool + executable: bool + source: object + + +def portable_path(value: str, label: str, *, allow_root: bool = False) -> str: + if not value or any(ord(character) < 32 or ord(character) == 127 for character in value): + raise UnsafeArchive(f"{label} is empty or contains control characters") + if "\\" in value or value.startswith("/") or (len(value) > 1 and value[1] == ":"): + raise UnsafeArchive(f"{label} is absolute or non-portable: {value!r}") + normalized = value.rstrip("/") + while normalized.startswith("./"): + normalized = normalized[2:] + if normalized in {"", "."}: + if allow_root: + return "" + raise UnsafeArchive(f"{label} resolves to the archive root") + parts = normalized.split("/") + if any(part in {"", ".", ".."} for part in parts): + raise UnsafeArchive(f"{label} contains an empty, dot, or traversal component") + for part in parts: + encoded = part.encode("utf-8") + if len(encoded) > MAX_COMPONENT_BYTES: + raise UnsafeArchive(f"{label} contains an oversized path component") + if part.endswith((" ", ".")) or any(character in part for character in '<>:"|?*'): + raise UnsafeArchive(f"{label} is ambiguous on Windows") + if part.split(".", 1)[0].casefold() in WINDOWS_RESERVED_NAMES: + raise UnsafeArchive(f"{label} contains a reserved Windows name") + result = "/".join(parts) + if len(result.encode("utf-8")) > MAX_PATH_BYTES: + raise UnsafeArchive(f"{label} exceeds the portable path-length limit") + return result + + +def relative_path(name: str, prefix: str, *, directory: bool) -> str: + normalized = portable_path(name, f"archive member {name!r}", allow_root=directory) + if prefix == ".": + return normalized + if normalized == prefix: + if not directory: + raise UnsafeArchive(f"archive root {prefix!r} is not a directory") + return "" + expected = f"{prefix}/" + if not normalized.startswith(expected): + raise UnsafeArchive(f"archive member {name!r} is outside required root {prefix!r}") + return normalized[len(expected) :] + + +def validate_entries( + entries: Iterable[Entry], + *, + expected_count: int, + expected_expanded_bytes: int, + required: set[str], + executables: set[str], +) -> list[Entry]: + checked = list(entries) + if len(checked) != expected_count: + raise UnsafeArchive( + f"archive entry count mismatch: expected {expected_count}, got {len(checked)}" + ) + paths: dict[str, Entry] = {} + portable: dict[str, str] = {} + expanded_bytes = 0 + for entry in checked: + if entry.relative == "": + continue + if entry.relative in paths: + raise UnsafeArchive(f"archive contains duplicate path {entry.relative!r}") + parts = entry.relative.split("/") + for depth in range(1, len(parts) + 1): + candidate = "/".join(parts[:depth]) + key = unicodedata.normalize("NFC", candidate).casefold() + prior = portable.get(key) + if prior is not None and prior != candidate: + raise UnsafeArchive( + f"archive paths {prior!r} and {candidate!r} collide on a portable filesystem" + ) + portable[key] = candidate + paths[entry.relative] = entry + if not entry.directory: + if entry.size < 0 or entry.size > MAX_ENTRY_BYTES: + raise UnsafeArchive(f"archive member {entry.relative!r} exceeds its file-size bound") + expanded_bytes += entry.size + if expanded_bytes > MAX_EXPANDED_BYTES: + raise UnsafeArchive("archive exceeds its expanded-size safety bound") + if expanded_bytes != expected_expanded_bytes: + raise UnsafeArchive( + f"archive expanded byte-size mismatch: expected {expected_expanded_bytes}, got {expanded_bytes}" + ) + missing = sorted(required - set(paths)) + if missing: + raise UnsafeArchive(f"archive is missing required files: {', '.join(missing)}") + for path in required: + entry = paths[path] + if entry.directory or entry.size == 0: + raise UnsafeArchive(f"required archive path is not a non-empty regular file: {path}") + actual_executables = { + path for path, entry in paths.items() if not entry.directory and entry.executable + } + if actual_executables != executables: + raise UnsafeArchive( + "archive executable paths mismatch: expected " + f"{sorted(executables)!r}, got {sorted(actual_executables)!r}" + ) + for entry in checked: + if entry.relative == "": + continue + parts = PurePosixPath(entry.relative).parts + for depth in range(1, len(parts)): + ancestor = paths.get("/".join(parts[:depth])) + if ancestor is not None and not ancestor.directory: + raise UnsafeArchive( + f"archive path {entry.relative!r} descends through a regular file" + ) + return checked + + +def tar_entries(archive: tarfile.TarFile, prefix: str, entry_limit: int) -> list[Entry]: + result: list[Entry] = [] + for member in archive: + if len(result) >= entry_limit: + raise UnsafeArchive("tar archive exceeds its entry-count bound") + if member.mode & (stat.S_ISUID | stat.S_ISGID): + raise UnsafeArchive(f"archive member {member.name!r} has set-id mode bits") + if not (member.isdir() or member.isreg()): + raise UnsafeArchive(f"archive member {member.name!r} is not a regular file or directory") + if member.isdir() and member.size != 0: + raise UnsafeArchive(f"archive directory {member.name!r} has a non-zero payload") + relative = relative_path(member.name, prefix, directory=member.isdir()) + result.append( + Entry( + member.name, + relative, + member.size if member.isreg() else 0, + member.isdir(), + bool(member.mode & 0o111), + member, + ) + ) + return result + + +def zip_entries(archive: zipfile.ZipFile, prefix: str, entry_limit: int) -> list[Entry]: + result: list[Entry] = [] + members = archive.infolist() + if len(members) > entry_limit: + raise UnsafeArchive("ZIP archive exceeds its entry-count bound") + for member in members: + mode = (member.external_attr >> 16) & 0xFFFF + file_type = stat.S_IFMT(mode) + directory = member.is_dir() or file_type == stat.S_IFDIR + if file_type not in {0, stat.S_IFREG, stat.S_IFDIR}: + raise UnsafeArchive(f"archive member {member.filename!r} is not a regular file or directory") + if bool(member.is_dir()) != directory: + raise UnsafeArchive(f"archive member {member.filename!r} has inconsistent directory metadata") + if directory and (member.file_size != 0 or member.compress_size != 0): + raise UnsafeArchive(f"archive directory {member.filename!r} has a non-zero payload") + if member.flag_bits & 0x1: + raise UnsafeArchive(f"archive member {member.filename!r} is encrypted") + if member.compress_type not in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED}: + raise UnsafeArchive(f"archive member {member.filename!r} uses unsupported compression") + relative = relative_path(member.filename, prefix, directory=directory) + result.append( + Entry( + member.filename, + relative, + member.file_size if not directory else 0, + directory, + bool(mode & 0o111), + member, + ) + ) + return result + + +def safe_parent(destination: Path, relative: str) -> Path: + parent = destination.joinpath(*PurePosixPath(relative).parts).parent + current = destination + for part in parent.relative_to(destination).parts: + current /= part + if current.exists() or current.is_symlink(): + mode = current.lstat().st_mode + if not stat.S_ISDIR(mode) or stat.S_ISLNK(mode): + raise UnsafeArchive(f"extraction ancestor is not a real directory: {current}") + else: + current.mkdir(mode=0o755) + return parent + + +def copy_regular(source: BinaryIO, output: Path, expected_bytes: int, executable: bool) -> None: + descriptor = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + copied = 0 + try: + with source, os.fdopen(descriptor, "wb") as sink: + descriptor = -1 + while True: + block = source.read(COPY_BYTES) + if not block: + break + copied += len(block) + if copied > expected_bytes: + raise UnsafeArchive(f"archive member {output.name!r} exceeded its declared size") + sink.write(block) + finally: + if descriptor >= 0: + os.close(descriptor) + if copied != expected_bytes: + raise UnsafeArchive(f"archive member {output.name!r} was truncated") + os.chmod(output, 0o755 if executable else 0o644) + + +def read_exact(stream: BinaryIO, size: int, label: str) -> bytes: + chunks: list[bytes] = [] + remaining = size + while remaining: + chunk = stream.read(remaining) + if not chunk: + raise UnsafeArchive(f"truncated {label}") + chunks.append(chunk) + remaining -= len(chunk) + return b"".join(chunks) + + +def tar_octal(field: bytes, label: str) -> int: + if field and field[0] & 0x80: + raise UnsafeArchive(f"{label} uses unsupported base-256 encoding") + value = field.strip(b" \0") + if not value: + return 0 + if any(character not in b"01234567" for character in value): + raise UnsafeArchive(f"{label} is not a canonical octal value") + return int(value, 8) + + +def checked_tar_header(header: bytes) -> tuple[str, int, bytes]: + if len(header) != 512: + raise UnsafeArchive("truncated tar header") + stored_checksum = tar_octal(header[148:156], "tar header checksum") + checksum_header = bytearray(header) + checksum_header[148:156] = b" " + if sum(checksum_header) != stored_checksum: + raise UnsafeArchive("tar header checksum mismatch") + name_bytes = header[0:100].split(b"\0", 1)[0] + prefix_bytes = header[345:500].split(b"\0", 1)[0] + try: + name = name_bytes.decode("utf-8") + prefix = prefix_bytes.decode("utf-8") + except UnicodeDecodeError as error: + raise UnsafeArchive("tar header path is not UTF-8") from error + if prefix: + name = f"{prefix}/{name}" + size = tar_octal(header[124:136], f"tar member {name!r} size") + typeflag = header[156:157] or b"\0" + return name, size, typeflag + + +def open_raw_tar(archive_path: Path, archive_format: str) -> BinaryIO: + if archive_format == "tar.xz": + return lzma.open(archive_path, "rb") + return gzip.open(archive_path, "rb") + + +def skip_exact(stream: BinaryIO, size: int, label: str) -> None: + remaining = size + while remaining: + block = stream.read(min(COPY_BYTES, remaining)) + if not block: + raise UnsafeArchive(f"truncated {label}") + remaining -= len(block) + + +def scan_simple_tar( + archive_path: Path, + archive_format: str, + *, + expected_count: int, + expected_expanded_bytes: int, +) -> None: + count = 0 + expanded_bytes = 0 + with open_raw_tar(archive_path, archive_format) as stream: + while True: + header = read_exact(stream, 512, "tar header") + if not any(header): + second = read_exact(stream, 512, "tar end marker") + if any(second): + raise UnsafeArchive("tar archive has only one zero end marker") + break + name, size, typeflag = checked_tar_header(header) + count += 1 + if count > expected_count or count > MAX_ENTRIES: + raise UnsafeArchive("tar archive exceeds its entry-count bound") + if typeflag in {b"x", b"g", b"L", b"K", b"S"}: + raise UnsafeArchive(f"tar member {name!r} uses unsupported extended metadata") + expanded_bytes += size + if size > MAX_ENTRY_BYTES or expanded_bytes > MAX_EXPANDED_BYTES: + raise UnsafeArchive("tar archive exceeds its raw payload bound") + skip_exact(stream, ((size + 511) // 512) * 512, f"tar member {name!r}") + if count != expected_count: + raise UnsafeArchive(f"archive entry count mismatch: expected {expected_count}, got {count}") + if expanded_bytes != expected_expanded_bytes: + raise UnsafeArchive( + "archive expanded byte-size mismatch: " + f"expected {expected_expanded_bytes}, got {expanded_bytes}" + ) + + +def validate_archive_file(archive_path: Path, expected_bytes: int) -> None: + if not archive_path.is_file() or archive_path.is_symlink(): + raise UnsafeArchive(f"archive is not a regular file: {archive_path}") + actual_bytes = archive_path.stat().st_size + if actual_bytes != expected_bytes: + raise UnsafeArchive(f"archive byte-size mismatch: expected {expected_bytes}, got {actual_bytes}") + if actual_bytes < 1 or actual_bytes > MAX_ARCHIVE_BYTES: + raise UnsafeArchive("archive exceeds its compressed-size safety bound") + + +def extract(arguments: argparse.Namespace) -> None: + archive_path = arguments.archive + validate_archive_file(archive_path, arguments.expected_bytes) + if arguments.entry_count < 1 or arguments.entry_count > MAX_ENTRIES: + raise UnsafeArchive(f"entry count must be between 1 and {MAX_ENTRIES}") + if arguments.expanded_bytes < 1 or arguments.expanded_bytes > MAX_EXPANDED_BYTES: + raise UnsafeArchive("expanded byte count exceeds its safety bound") + prefix = "." if arguments.prefix == "." else portable_path(arguments.prefix, "strip prefix") + required = {portable_path(value, "required archive path") for value in arguments.required} + executables = {portable_path(value, "executable archive path") for value in arguments.executable} + if not executables.issubset(required): + raise UnsafeArchive("every executable archive path must also be required") + destination = arguments.destination + if destination.exists() or destination.is_symlink(): + raise UnsafeArchive(f"private extraction destination already exists: {destination}") + destination.parent.mkdir(parents=True, exist_ok=True) + destination.mkdir(mode=0o700) + try: + if arguments.format == "zip": + with zipfile.ZipFile(archive_path) as stream: + entries = validate_entries( + zip_entries(stream, prefix, arguments.entry_count), + expected_count=arguments.entry_count, + expected_expanded_bytes=arguments.expanded_bytes, + required=required, + executables=executables, + ) + for entry in entries: + if entry.relative == "": + continue + output = destination.joinpath(*PurePosixPath(entry.relative).parts) + safe_parent(destination, entry.relative) + if entry.directory: + if output.exists() or output.is_symlink(): + if not output.is_dir() or output.is_symlink(): + raise UnsafeArchive(f"cannot create archive directory {entry.relative!r}") + else: + output.mkdir(mode=0o755) + else: + copy_regular(stream.open(entry.source, "r"), output, entry.size, entry.executable) + else: + scan_simple_tar( + archive_path, + arguments.format, + expected_count=arguments.entry_count, + expected_expanded_bytes=arguments.expanded_bytes, + ) + mode = "r:xz" if arguments.format == "tar.xz" else "r:gz" + with tarfile.open(archive_path, mode=mode) as stream: + entries = validate_entries( + tar_entries(stream, prefix, arguments.entry_count), + expected_count=arguments.entry_count, + expected_expanded_bytes=arguments.expanded_bytes, + required=required, + executables=executables, + ) + with tarfile.open(archive_path, mode=mode) as stream: + by_name = {member.name: member for member in stream} + for entry in entries: + if entry.relative == "": + continue + output = destination.joinpath(*PurePosixPath(entry.relative).parts) + safe_parent(destination, entry.relative) + if entry.directory: + if output.exists() or output.is_symlink(): + if not output.is_dir() or output.is_symlink(): + raise UnsafeArchive(f"cannot create archive directory {entry.relative!r}") + else: + output.mkdir(mode=0o755) + else: + source = stream.extractfile(by_name[entry.name]) + if source is None: + raise UnsafeArchive(f"cannot read archive member {entry.name!r}") + copy_regular(source, output, entry.size, entry.executable) + except BaseException: + shutil.rmtree(destination, ignore_errors=True) + raise + + +def extract_one(arguments: argparse.Namespace) -> None: + archive_path = arguments.archive + validate_archive_file(archive_path, arguments.expected_bytes) + member = portable_path(arguments.member, "target archive member") + output = arguments.destination + if output.exists() or output.is_symlink(): + raise UnsafeArchive(f"target extraction path already exists: {output}") + output.parent.mkdir(parents=True, exist_ok=True) + if output.parent.is_symlink(): + raise UnsafeArchive(f"target extraction parent is a symbolic link: {output.parent}") + try: + if arguments.format == "zip": + with zipfile.ZipFile(archive_path) as archive: + infos = archive.infolist() + if len(infos) > MAX_ENTRIES: + raise UnsafeArchive("ZIP archive exceeds its entry-count bound") + matches = [info for info in infos if info.filename.rstrip("/") == member] + if len(matches) != 1: + raise UnsafeArchive(f"ZIP archive must contain target member exactly once: {member}") + info = matches[0] + mode = (info.external_attr >> 16) & 0xFFFF + if info.is_dir() or stat.S_IFMT(mode) not in {0, stat.S_IFREG}: + raise UnsafeArchive(f"target ZIP member is not a regular file: {member}") + if info.flag_bits & 0x1 or info.compress_type not in {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED}: + raise UnsafeArchive(f"target ZIP member uses unsafe encoding: {member}") + if info.file_size != arguments.member_bytes: + raise UnsafeArchive(f"target ZIP member has unexpected size: {member}") + copy_regular( + archive.open(info, "r"), output, arguments.member_bytes, arguments.executable + ) + else: + scanned_entries = 0 + scanned_bytes = 0 + with open_raw_tar(archive_path, arguments.format) as stream: + while True: + header = read_exact(stream, 512, "tar header") + if not any(header): + raise UnsafeArchive(f"tar archive does not contain target member: {member}") + name, size, typeflag = checked_tar_header(header) + scanned_entries += 1 + scanned_bytes += size + if scanned_entries > MAX_TARGET_SCAN_ENTRIES or scanned_bytes > MAX_TARGET_SCAN_BYTES: + raise UnsafeArchive("target tar scan exceeds its bounded search envelope") + padded_size = ((size + 511) // 512) * 512 + if name == member: + if typeflag not in {b"0", b"\0"} or size != arguments.member_bytes: + raise UnsafeArchive(f"target tar member is not the expected regular file: {member}") + descriptor = os.open(output, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + with os.fdopen(descriptor, "wb") as sink: + descriptor = -1 + remaining = size + digest = hashlib.sha256() + while remaining: + block = stream.read(min(COPY_BYTES, remaining)) + if not block: + raise UnsafeArchive(f"target tar member is truncated: {member}") + sink.write(block) + digest.update(block) + remaining -= len(block) + finally: + if descriptor >= 0: + os.close(descriptor) + if digest.hexdigest() != arguments.member_sha256: + raise UnsafeArchive(f"target tar member checksum mismatch: {member}") + os.chmod(output, 0o755 if arguments.executable else 0o644) + return + skip_exact(stream, padded_size, f"tar member {name!r}") + if hashlib.sha256(output.read_bytes()).hexdigest() != arguments.member_sha256: + raise UnsafeArchive(f"target archive member checksum mismatch: {member}") + except BaseException: + output.unlink(missing_ok=True) + raise + + +def tree_digest(root: Path, expected_executables: set[str]) -> tuple[int, str]: + if not root.is_dir() or root.is_symlink(): + raise UnsafeArchive(f"tree root is not a real directory: {root}") + files: list[tuple[str, Path]] = [] + portable: dict[str, str] = {} + for current, directories, names in os.walk(root, followlinks=False): + current_path = Path(current) + for name in directories: + candidate = current_path / name + if candidate.is_symlink(): + raise UnsafeArchive(f"tree contains symbolic link: {candidate}") + for name in names: + candidate = current_path / name + mode = candidate.lstat().st_mode + if not stat.S_ISREG(mode) or stat.S_ISLNK(mode): + raise UnsafeArchive(f"tree contains non-regular file: {candidate}") + relative = candidate.relative_to(root).as_posix() + portable_path(relative, "tree path") + key = unicodedata.normalize("NFC", relative).casefold() + prior = portable.get(key) + if prior is not None and prior != relative: + raise UnsafeArchive(f"tree paths {prior!r} and {relative!r} collide") + portable[key] = relative + files.append((relative, candidate)) + files.sort(key=lambda item: item[0].encode("utf-8")) + actual_paths = {relative for relative, _ in files} + missing_executables = sorted(expected_executables - actual_paths) + if missing_executables: + raise UnsafeArchive( + f"tree is missing expected executable files: {', '.join(missing_executables)}" + ) + if os.name != "nt": + actual_executables = { + relative for relative, candidate in files if candidate.stat().st_mode & 0o111 + } + if actual_executables != expected_executables: + raise UnsafeArchive( + "tree executable paths mismatch: expected " + f"{sorted(expected_executables)!r}, got {sorted(actual_executables)!r}" + ) + digest = hashlib.sha256(b"oliphaunt-bootstrap-tree-v2\0") + for relative, candidate in files: + size = candidate.stat().st_size + digest.update(relative.encode("utf-8")) + digest.update(b"\0") + digest.update(str(size).encode("ascii")) + digest.update(b"\0") + digest.update(b"x" if relative in expected_executables else b"-") + digest.update(b"\0") + with candidate.open("rb") as stream: + while block := stream.read(COPY_BYTES): + digest.update(block) + digest.update(b"\0") + return len(files), digest.hexdigest() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + extract_parser = subparsers.add_parser("extract") + extract_parser.add_argument("--archive", type=Path, required=True) + extract_parser.add_argument("--format", choices=("zip", "tar.xz", "tar.gz"), required=True) + extract_parser.add_argument("--prefix", required=True) + extract_parser.add_argument("--entry-count", type=int, required=True) + extract_parser.add_argument("--expected-bytes", type=int, required=True) + extract_parser.add_argument("--expanded-bytes", type=int, required=True) + extract_parser.add_argument("--destination", type=Path, required=True) + extract_parser.add_argument("--required", action="append", default=[], required=True) + extract_parser.add_argument("--executable", action="append", default=[]) + one_parser = subparsers.add_parser("extract-file") + one_parser.add_argument("--archive", type=Path, required=True) + one_parser.add_argument("--format", choices=("zip", "tar.xz", "tar.gz"), required=True) + one_parser.add_argument("--expected-bytes", type=int, required=True) + one_parser.add_argument("--member", required=True) + one_parser.add_argument("--member-bytes", type=int, required=True) + one_parser.add_argument("--member-sha256", required=True) + one_parser.add_argument("--destination", type=Path, required=True) + one_parser.add_argument("--executable", action="store_true") + digest_parser = subparsers.add_parser("tree-digest") + digest_parser.add_argument("--root", type=Path, required=True) + digest_parser.add_argument("--executable", action="append", default=[]) + return parser.parse_args() + + +def main() -> int: + arguments = parse_args() + try: + if arguments.command == "extract": + extract(arguments) + elif arguments.command == "extract-file": + if len(arguments.member_sha256) != 64 or any( + character not in "0123456789abcdef" for character in arguments.member_sha256 + ): + raise UnsafeArchive("target member SHA-256 is not lowercase hexadecimal") + if arguments.member_bytes < 1 or arguments.member_bytes > MAX_ENTRY_BYTES: + raise UnsafeArchive("target member byte count exceeds its safety bound") + extract_one(arguments) + else: + executables = { + portable_path(value, "expected executable tree path") + for value in arguments.executable + } + count, digest = tree_digest(arguments.root, executables) + print(f"{count} {digest}") + except (OSError, UnsafeArchive, tarfile.TarError, zipfile.BadZipFile) as error: + print(f"pinned bootstrap archive rejected: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/actions/setup-msvc/action.yml b/.github/actions/setup-msvc/action.yml new file mode 100644 index 00000000..ce7740af --- /dev/null +++ b/.github/actions/setup-msvc/action.yml @@ -0,0 +1,9 @@ +name: Set up MSVC +description: Configure the MSVC developer environment and force Rust to use the MSVC linker under Git Bash. + +runs: + using: composite + steps: + - name: Configure MSVC developer command prompt + shell: pwsh + run: .github/scripts/setup-msvc.ps1 # zizmor: ignore[github-env] the script exports only environment changes produced by the runner-owned, locally validated Visual Studio developer shell. diff --git a/.github/actions/setup-node-pnpm/action.yml b/.github/actions/setup-node-pnpm/action.yml new file mode 100644 index 00000000..dbdf4d7d --- /dev/null +++ b/.github/actions/setup-node-pnpm/action.yml @@ -0,0 +1,93 @@ +name: Set up Node and pnpm +description: Install the pinned Node.js and pnpm toolchain for Oliphaunt. + +inputs: + node-version: + description: Node.js version. + required: false + default: "22.22.3" + pnpm-version: + description: pnpm version. + required: false + default: "11.5.0" + +outputs: + node-execution-envelope: + description: Absolute digest-verified Node.js execution envelope. + value: ${{ steps.node_runtime.outputs.execution-envelope }} + pnpm-execution-envelope: + description: Absolute digest-verified pnpm execution envelope. + value: ${{ steps.install.outputs.execution-envelope }} + +runs: + using: composite + steps: + - name: Set up verified Node.js + id: node_runtime + uses: ./.github/actions/setup-node-runtime + with: + node-version: ${{ inputs.node-version }} + + - name: Restore verified pnpm archive + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ runner.temp }}/oliphaunt-pnpm-runtime + key: verified-pnpm-runtime-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.prototools', '.github/actions/setup-node-pnpm/action.yml', '.github/actions/setup-node-pnpm/install-pinned-pnpm.sh', '.github/actions/setup-moon/toolchain-archive.py', 'src/sources/toolchains/pnpm.toml', 'tools/dev/curl-platform-flags.sh') }} + + - name: Install verified pnpm + id: install + shell: bash + env: + EXPECTED_NODE_VERSION: ${{ inputs.node-version }} + EXPECTED_PNPM_VERSION: ${{ inputs.pnpm-version }} + OLIPHAUNT_PNPM_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-pnpm-runtime + run: | # zizmor: ignore[github-env] only a fully verified runner-temp path is exported. + set -euo pipefail + installation="$(bash .github/actions/setup-node-pnpm/install-pinned-pnpm.sh)" + observed_node="$(node --version)" + observed_pnpm="$("$installation/bin/pnpm" --version)" + if [[ "$observed_node" != "v$EXPECTED_NODE_VERSION" ]]; then + echo "Expected Node.js v${EXPECTED_NODE_VERSION}; observed ${observed_node}" >&2 + exit 1 + fi + if [[ "$observed_pnpm" != "$EXPECTED_PNPM_VERSION" ]]; then + echo "Expected pnpm ${EXPECTED_PNPM_VERSION}; observed ${observed_pnpm}" >&2 + exit 1 + fi + export_dir="$installation/bin" + output_envelope="$installation" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + export_dir="$(cygpath -w "$export_dir")" + output_envelope="$(cygpath -w "$installation")" + fi + echo "$export_dir" >> "$GITHUB_PATH" + echo "execution-envelope=$output_envelope" >> "$GITHUB_OUTPUT" + + - name: Resolve pnpm store + id: pnpm-store + shell: bash + run: | + set -euo pipefail + store_path="$(pnpm store path --silent)" + if [[ -z "$store_path" || "$store_path" == *$'\n'* || "$store_path" == *$'\r'* ]]; then + echo "pnpm returned an invalid store path" >&2 + exit 1 + fi + mkdir_path="$store_path" + cache_path="$store_path" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + mkdir_path="$(cygpath -u "$store_path")" + cache_path="$(cygpath -w "$mkdir_path")" + fi + mkdir -p "$mkdir_path" + echo "path=$cache_path" >> "$GITHUB_OUTPUT" + + - name: Restore pnpm store + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ steps.pnpm-store.outputs.path }} + key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-pnpm-${{ inputs.pnpm-version }}-${{ hashFiles('pnpm-lock.yaml', 'tools/release/verdaccio-runtime/pnpm-lock.yaml') }} + restore-keys: | + pnpm-store-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}-pnpm-${{ inputs.pnpm-version }}- diff --git a/.github/actions/setup-node-pnpm/install-pinned-pnpm.sh b/.github/actions/setup-node-pnpm/install-pinned-pnpm.sh new file mode 100755 index 00000000..7664261c --- /dev/null +++ b/.github/actions/setup-node-pnpm/install-pinned-pnpm.sh @@ -0,0 +1,424 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "install-pinned-pnpm.sh: $*" >&2 + exit 1 +} + +root="$(git rev-parse --show-toplevel 2>/dev/null)" || + fail "must run inside the Oliphaunt checkout" +manifest="${OLIPHAUNT_PNPM_MANIFEST:-$root/src/sources/toolchains/pnpm.toml}" +proto_file="${OLIPHAUNT_PNPM_PROTO_FILE:-$root/.prototools}" +extractor="${OLIPHAUNT_PNPM_ARCHIVE_EXTRACTOR:-$root/.github/actions/setup-moon/toolchain-archive.py}" +curl_platform_flags="${OLIPHAUNT_PNPM_CURL_PLATFORM_FLAGS:-$root/tools/dev/curl-platform-flags.sh}" +cache_root="${OLIPHAUNT_PNPM_CACHE_ROOT:-${RUNNER_TEMP:-$root/target}/oliphaunt-pnpm-runtime}" +curl_command="${OLIPHAUNT_PNPM_CURL:-curl}" +testing="${OLIPHAUNT_PNPM_TESTING:-0}" + +for override in \ + OLIPHAUNT_PNPM_MANIFEST \ + OLIPHAUNT_PNPM_PROTO_FILE \ + OLIPHAUNT_PNPM_ARCHIVE_EXTRACTOR \ + OLIPHAUNT_PNPM_CURL_PLATFORM_FLAGS \ + OLIPHAUNT_PNPM_CURL; do + if [ -n "${!override:-}" ] && [ "$testing" != "1" ]; then + fail "$override is test-only" + fi +done + +case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) + if command -v cygpath >/dev/null 2>&1; then + cache_root="$(cygpath -u "$cache_root")" + fi + ;; +esac + +for path in "$manifest" "$proto_file" "$extractor" "$curl_platform_flags"; do + if [ ! -f "$path" ] || [ -L "$path" ]; then + fail "missing regular bootstrap input: $path" + fi +done + +# shellcheck source=tools/dev/curl-platform-flags.sh +. "$curl_platform_flags" + +python="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then + python="$candidate" + break + fi +done +[ -n "$python" ] || fail "python3 or python is required for safe archive extraction" +for command_name in "$curl_command" mktemp node; do + command -v "$command_name" >/dev/null 2>&1 || fail "missing required command: $command_name" +done + +manifest_value() { + local source="$1" + local section="$2" + local key="$3" + awk -v wanted_section="$section" -v wanted_key="$key" ' + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + current=$0 + gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", current) + next + } + current == wanted_section && $0 ~ "^[[:space:]]*" wanted_key "[[:space:]]*=" { + count++ + line=$0 + sub(/^[^=]*=[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line) + value=line + } + END { + if (count != 1 || value == "") exit 1 + print value + } + ' "$source" +} + +prototool_version() { + awk -F '=' ' + $1 ~ "^[[:space:]]*pnpm[[:space:]]*$" { + count++ + value=$2 + gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value) + } + END { if (count != 1 || value == "") exit 1; print value } + ' "$proto_file" +} + +validate_version() { + [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid pnpm version: $1" +} + +validate_sha256() { + if [ "${#1}" -ne 64 ] || [[ "$1" =~ [^0-9a-f] ]]; then + fail "$2 must contain exactly 64 lowercase hexadecimal characters" + fi +} + +validate_sha512() { + if [ "${#1}" -ne 128 ] || [[ "$1" =~ [^0-9a-f] ]]; then + fail "$2 must contain exactly 128 lowercase hexadecimal characters" + fi +} + +validate_count() { + case "$1" in + '' | *[!0-9]*) fail "$2 must be a positive integer" ;; + esac + [ "$1" -gt 0 ] || fail "$2 must be a positive integer" +} + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + "$python" - "$1" <<'PY' +import hashlib +import pathlib +import sys + +digest = hashlib.sha256() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +sha512_file() { + if command -v sha512sum >/dev/null 2>&1; then + sha512sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 512 "$1" | awk '{print $1}' + else + "$python" - "$1" <<'PY' +import hashlib +import pathlib +import sys + +digest = hashlib.sha512() +with pathlib.Path(sys.argv[1]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +version="$(manifest_value "$manifest" toolchain version)" || + fail "$manifest must contain exactly one quoted toolchain.version" +url="$(manifest_value "$manifest" package url)" || fail "$manifest is missing package.url" +archive_sha256="$(manifest_value "$manifest" package sha256)" || fail "$manifest is missing package.sha256" +archive_sha512="$(manifest_value "$manifest" package sha512)" || fail "$manifest is missing package.sha512" +archive_bytes="$(manifest_value "$manifest" package bytes)" || fail "$manifest is missing package.bytes" +expanded_bytes="$(manifest_value "$manifest" package expanded_bytes)" || fail "$manifest is missing package.expanded_bytes" +format="$(manifest_value "$manifest" package format)" || fail "$manifest is missing package.format" +prefix="$(manifest_value "$manifest" package prefix)" || fail "$manifest is missing package.prefix" +entry_count="$(manifest_value "$manifest" package entry_count)" || fail "$manifest is missing package.entry_count" +file_count="$(manifest_value "$manifest" package file_count)" || fail "$manifest is missing package.file_count" +tree_sha256="$(manifest_value "$manifest" package tree_sha256)" || fail "$manifest is missing package.tree_sha256" +executable_paths="$(manifest_value "$manifest" package executable_paths)" || fail "$manifest is missing package.executable_paths" +binary_path="$(manifest_value "$manifest" package binary_path)" || fail "$manifest is missing package.binary_path" +binary_sha256="$(manifest_value "$manifest" package binary_sha256)" || fail "$manifest is missing package.binary_sha256" +companion_path="$(manifest_value "$manifest" package companion_path)" || fail "$manifest is missing package.companion_path" +companion_sha256="$(manifest_value "$manifest" package companion_sha256)" || fail "$manifest is missing package.companion_sha256" +payload_path="$(manifest_value "$manifest" package payload_path)" || fail "$manifest is missing package.payload_path" +payload_sha256="$(manifest_value "$manifest" package payload_sha256)" || fail "$manifest is missing package.payload_sha256" + +validate_version "$version" +configured="$(prototool_version)" || fail "$proto_file must contain exactly one pnpm version" +configured="${configured#v}" +[ "$configured" = "$version" ] || + fail "$proto_file pnpm version $configured does not match pinned version $version" +if [ -n "${EXPECTED_PNPM_VERSION:-}" ]; then + validate_version "$EXPECTED_PNPM_VERSION" + [ "$EXPECTED_PNPM_VERSION" = "$version" ] || + fail "requested pnpm $EXPECTED_PNPM_VERSION does not match pinned version $version" +fi + +expected_url="https://registry.npmjs.org/pnpm/-/pnpm-$version.tgz" +expected_executable_paths="bin/pnpm.mjs,bin/pnpx.mjs,dist/node-gyp-bin/node-gyp,dist/node-gyp-bin/node-gyp.cmd,dist/node_modules/node-gyp/bin/node-gyp.js" +[ "$url" = "$expected_url" ] || fail "$manifest package.url must be $expected_url" +[ "$format" = "tar.gz" ] || fail "$manifest package.format must be tar.gz" +[ "$prefix" = "package" ] || fail "$manifest package.prefix must be package" +[ "$binary_path" = "bin/pnpm.mjs" ] || fail "$manifest package.binary_path must be bin/pnpm.mjs" +[ "$companion_path" = "bin/pnpx.mjs" ] || fail "$manifest package.companion_path must be bin/pnpx.mjs" +[ "$payload_path" = "dist/pnpm.mjs" ] || fail "$manifest package.payload_path must be dist/pnpm.mjs" +[ "$executable_paths" = "$expected_executable_paths" ] || + fail "$manifest package.executable_paths must be $expected_executable_paths" +IFS=',' read -r -a executables <<<"$executable_paths" +for digest in "$archive_sha256" "$tree_sha256" "$binary_sha256" "$companion_sha256" "$payload_sha256"; do + validate_sha256 "$digest" "$manifest package digest" +done +validate_sha512 "$archive_sha512" "$manifest package.sha512" +for value in "$archive_bytes" "$expanded_bytes" "$entry_count" "$file_count"; do + validate_count "$value" "$manifest package count" +done + +case "$cache_root" in + '' | /) fail "unsafe pnpm cache root: $cache_root" ;; +esac +if [ -L "$cache_root" ]; then + fail "pnpm cache root must not be a symbolic link: $cache_root" +fi +umask 077 +mkdir -p "$cache_root" +if [ ! -d "$cache_root" ] || [ -L "$cache_root" ]; then + fail "pnpm cache root is not a real directory: $cache_root" +fi +archive_root="$cache_root/archives" +installations_root="$cache_root/installations" +for path in "$archive_root" "$installations_root"; do + [ ! -L "$path" ] || fail "pnpm cache must not contain symbolic-link directories: $path" + mkdir -p "$path" + if [ ! -d "$path" ] || [ -L "$path" ]; then + fail "pnpm cache path is not a real directory: $path" + fi +done + +curl_tls_flag="$(oliphaunt_curl_platform_tls_flag)" +curl_common=( + --fail --location --silent --show-error + --proto '=https' --proto-redir '=https' --tlsv1.2 + --retry 5 --retry-all-errors --retry-connrefused --retry-delay 2 --retry-max-time 300 + --connect-timeout 20 --max-time 300 --speed-limit 1024 --speed-time 30 + --remove-on-error +) +if [ -n "$curl_tls_flag" ]; then + curl_common+=("$curl_tls_flag") +fi + +download_verified() { + local output="$1" + local actual_size + if [ -f "$output" ] && [ ! -L "$output" ]; then + actual_size="$(wc -c <"$output" | tr -d '[:space:]')" + if [ "$actual_size" = "$archive_bytes" ] && + [ "$(sha256_file "$output")" = "$archive_sha256" ] && + [ "$(sha512_file "$output")" = "$archive_sha512" ]; then + return 0 + fi + fi + rm -f "$output" + local partial + local rc=0 + partial="$(mktemp "$archive_root/.download.XXXXXX")" + local args=("${curl_common[@]}" --max-filesize "$archive_bytes" --output "$partial" "$url") + if "$curl_command" "${args[@]}"; then + : + else + rc=$? + rm -f "$partial" + return "$rc" + fi + actual_size="$(wc -c <"$partial" | tr -d '[:space:]')" + [ "$actual_size" = "$archive_bytes" ] || { + rm -f "$partial" + fail "downloaded byte-size mismatch for $url: expected $archive_bytes, got $actual_size" + } + [ "$(sha256_file "$partial")" = "$archive_sha256" ] || { + rm -f "$partial" + fail "downloaded SHA-256 mismatch for $url" + } + [ "$(sha512_file "$partial")" = "$archive_sha512" ] || { + rm -f "$partial" + fail "downloaded SHA-512 mismatch for $url" + } + chmod 0444 "$partial" + mv "$partial" "$output" +} + +identity="pnpm-$version-$archive_sha256" +install_parent="$installations_root/$identity" +[ ! -L "$install_parent" ] || fail "pnpm installation parent must not be a symbolic link" +mkdir -p "$install_parent" +if [ ! -d "$install_parent" ] || [ -L "$install_parent" ]; then + fail "pnpm installation parent is not a real directory" +fi +final="$install_parent/verified" +receipt_text="$(printf 'pnpm_version=%s\narchive_sha256=%s\narchive_sha512=%s\ntree_sha256=%s' \ + "$version" "$archive_sha256" "$archive_sha512" "$tree_sha256")" +# These literal lines become the cached wrapper. +# shellcheck disable=SC2016 +pnpm_wrapper_text="$(printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../pnpm/bin/pnpm.mjs" "$@"')" +# These literal lines become the cached wrapper. +# shellcheck disable=SC2016 +pnpx_wrapper_text="$(printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../pnpm/bin/pnpx.mjs" "$@"')" +pnpm_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\pnpm\bin\pnpm.mjs" %*')" +pnpx_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\pnpm\bin\pnpx.mjs" %*')" + +cache_valid() { + local candidate="$1" + [ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1 + [ "$(find "$candidate" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "3" ] || return 1 + [ -d "$candidate/bin" ] && [ ! -L "$candidate/bin" ] || return 1 + [ -d "$candidate/pnpm" ] && [ ! -L "$candidate/pnpm" ] || return 1 + [ "$(find "$candidate/bin" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = "4" ] || return 1 + for path in \ + "$candidate/pnpm/$binary_path" \ + "$candidate/pnpm/$companion_path" \ + "$candidate/pnpm/$payload_path" \ + "$candidate/bin/pnpm" \ + "$candidate/bin/pnpx" \ + "$candidate/bin/pnpm.cmd" \ + "$candidate/bin/pnpx.cmd" \ + "$candidate/receipt"; do + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + done + [ "$(sha256_file "$candidate/pnpm/$binary_path")" = "$binary_sha256" ] || return 1 + [ "$(sha256_file "$candidate/pnpm/$companion_path")" = "$companion_sha256" ] || return 1 + [ "$(sha256_file "$candidate/pnpm/$payload_path")" = "$payload_sha256" ] || return 1 + [ "$(cat "$candidate/bin/pnpm")" = "$pnpm_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpx")" = "$pnpx_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpm.cmd")" = "$pnpm_cmd_text" ] || return 1 + [ "$(cat "$candidate/bin/pnpx.cmd")" = "$pnpx_cmd_text" ] || return 1 + if [ "$(uname -s)" != "Windows_NT" ]; then + [ -x "$candidate/bin/pnpm" ] && [ -x "$candidate/bin/pnpx" ] || return 1 + fi + local tree_result + local tree_args=(tree-digest --root "$candidate/pnpm") + local executable + for executable in "${executables[@]}"; do + tree_args+=(--executable "$executable") + done + tree_result="$("$python" "$extractor" "${tree_args[@]}" 2>/dev/null)" || return 1 + [ "$tree_result" = "$file_count $tree_sha256" ] || return 1 + [ "$(node "$candidate/pnpm/$binary_path" --version 2>/dev/null | awk 'NF { print $1; exit }')" = "$version" ] || return 1 + [ "$(cat "$candidate/receipt")" = "$receipt_text" ] || return 1 +} + +if cache_valid "$final"; then + printf '%s\n' "$final" + exit 0 +fi + +archive="$archive_root/$archive_sha256.tgz" +download_verified "$archive" + +stage="$(mktemp -d "$install_parent/.verified.stage.XXXXXX")" +backup="" +old_moved=0 +cleanup() { + local rc="$?" + trap - EXIT HUP INT TERM + if [ -n "$stage" ]; then + rm -rf "$stage" + fi + if [ "$old_moved" = "1" ] && [ -n "$backup" ] && [ -e "$backup" ] && [ ! -e "$final" ]; then + mv "$backup" "$final" || rc=1 + elif [ -n "$backup" ]; then + rm -rf "$backup" + fi + exit "$rc" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +extract_args=( + extract + --archive "$archive" + --format "$format" + --prefix "$prefix" + --entry-count "$entry_count" + --expected-bytes "$archive_bytes" + --expanded-bytes "$expanded_bytes" + --destination "$stage/pnpm" + --required "$binary_path" + --required "$companion_path" + --required "$payload_path" + --required package.json +) +for executable in "${executables[@]}"; do + extract_args+=(--required "$executable" --executable "$executable") +done +"$python" "$extractor" "${extract_args[@]}" + +mkdir -p "$stage/bin" +printf '%s\n' "$pnpm_wrapper_text" >"$stage/bin/pnpm" +printf '%s\n' "$pnpx_wrapper_text" >"$stage/bin/pnpx" +printf '%s\n' "$pnpm_cmd_text" >"$stage/bin/pnpm.cmd" +printf '%s\n' "$pnpx_cmd_text" >"$stage/bin/pnpx.cmd" +chmod 0555 "$stage/bin/pnpm" "$stage/bin/pnpx" +chmod 0444 "$stage/bin/pnpm.cmd" "$stage/bin/pnpx.cmd" +printf '%s\n' "$receipt_text" >"$stage/receipt" +chmod 0444 "$stage/receipt" + +cache_valid "$stage" || fail "staged pnpm installation failed integrity or version validation" + +if [ -e "$final" ] || [ -L "$final" ]; then + backup="$(mktemp -d "$install_parent/.verified.backup.XXXXXX")" + rmdir "$backup" + mv "$final" "$backup" + old_moved=1 +fi +if [ "${OLIPHAUNT_PNPM_TEST_INTERRUPT_AFTER_BACKUP:-0}" = "1" ]; then + [ "$testing" = "1" ] || fail "OLIPHAUNT_PNPM_TEST_INTERRUPT_AFTER_BACKUP is test-only" + kill -TERM "$$" +fi +mv "$stage" "$final" +stage="" +if [ "$old_moved" = "1" ]; then + rm -rf "$backup" + backup="" + old_moved=0 +fi +printf '%s\n' "$final" diff --git a/.github/actions/setup-node-pnpm/install-pinned-pnpm.test.sh b/.github/actions/setup-node-pnpm/install-pinned-pnpm.test.sh new file mode 100755 index 00000000..4667a308 --- /dev/null +++ b/.github/actions/setup-node-pnpm/install-pinned-pnpm.test.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "install-pinned-pnpm.test.sh: $*" >&2 + exit 1 +} + +root="$(git rev-parse --show-toplevel)" +installer="$root/.github/actions/setup-node-pnpm/install-pinned-pnpm.sh" +action="$root/.github/actions/setup-node-pnpm/action.yml" +extractor="$root/.github/actions/setup-moon/toolchain-archive.py" +curl_flags="$root/tools/dev/curl-platform-flags.sh" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +if grep -Eq 'uses:[[:space:]]+actions/setup-node@|corepack([[:space:]]|$)|install-pinned-toolchain|moon-plugins|moon-cli' \ + "$action" "$installer"; then + fail "standalone Node/pnpm action reintroduced an unverified or Moon bootstrap" +fi +grep -Fq 'uses: ./.github/actions/setup-node-runtime' "$action" || + fail "standalone action does not compose the verified Node runtime" +grep -Fq 'export_dir="$(cygpath -w "$export_dir")"' "$action" || + fail "standalone action does not export a native Windows pnpm path" +grep -Fq 'mkdir_path="$(cygpath -u "$store_path")"' "$action" || + fail "standalone action does not normalize the Windows pnpm store for Bash" +grep -Fq 'cache_path="$(cygpath -w "$mkdir_path")"' "$action" || + fail "standalone action does not return a native Windows pnpm cache path" + +fixture="$tmp/pnpm-11.5.0.tgz" +manifest="$tmp/pnpm.toml" +proto_file="$tmp/prototools" +metadata="$tmp/metadata.env" +"${PYTHON:-python3}" - "$fixture" "$manifest" "$metadata" <<'PY' +import hashlib +import io +import pathlib +import sys +import tarfile + +archive = pathlib.Path(sys.argv[1]) +manifest = pathlib.Path(sys.argv[2]) +metadata = pathlib.Path(sys.argv[3]) +version = "11.5.0" +files = { + "bin/pnpm.mjs": b"#!/usr/bin/env node\nprocess.stdout.write('11.5.0\\n');\n", + "bin/pnpx.mjs": b"#!/usr/bin/env node\nprocess.stdout.write('fixture-pnpx\\n');\n", + "dist/node-gyp-bin/node-gyp": b"#!/usr/bin/env sh\nexit 0\n", + "dist/node-gyp-bin/node-gyp.cmd": b"@ECHO OFF\r\nEXIT /B 0\r\n", + "dist/node_modules/node-gyp/bin/node-gyp.js": b"#!/usr/bin/env node\nprocess.exit(0);\n", + "dist/pnpm.mjs": b"export const fixture = true;\n", + "package.json": b'{"name":"pnpm","version":"11.5.0"}\n', +} +executables = { + "bin/pnpm.mjs", + "bin/pnpx.mjs", + "dist/node-gyp-bin/node-gyp", + "dist/node-gyp-bin/node-gyp.cmd", + "dist/node_modules/node-gyp/bin/node-gyp.js", +} +directories = {"package"} +for relative in files: + parts = pathlib.PurePosixPath("package", relative).parts + directories.update("/".join(parts[:depth]) for depth in range(2, len(parts))) + +with tarfile.open(archive, "w:gz", format=tarfile.USTAR_FORMAT) as stream: + for directory in sorted(directories, key=lambda value: value.encode("utf-8")): + info = tarfile.TarInfo(f"{directory}/") + info.type = tarfile.DIRTYPE + info.mode = 0o755 + info.mtime = 0 + info.uid = info.gid = 0 + info.uname = info.gname = "" + stream.addfile(info) + for relative, content in sorted(files.items(), key=lambda item: item[0].encode("utf-8")): + info = tarfile.TarInfo(f"package/{relative}") + info.type = tarfile.REGTYPE + info.size = len(content) + info.mode = 0o755 if relative in executables else 0o644 + info.mtime = 0 + info.uid = info.gid = 0 + info.uname = info.gname = "" + stream.addfile(info, io.BytesIO(content)) + +archive_bytes = archive.read_bytes() +archive_sha256 = hashlib.sha256(archive_bytes).hexdigest() +archive_sha512 = hashlib.sha512(archive_bytes).hexdigest() +tree = hashlib.sha256(b"oliphaunt-bootstrap-tree-v2\0") +for relative, content in sorted(files.items(), key=lambda item: item[0].encode("utf-8")): + tree.update(relative.encode("utf-8")) + tree.update(b"\0") + tree.update(str(len(content)).encode("ascii")) + tree.update(b"\0") + tree.update(b"x" if relative in executables else b"-") + tree.update(b"\0") + tree.update(content) + tree.update(b"\0") + +executable_paths = ( + "bin/pnpm.mjs,bin/pnpx.mjs,dist/node-gyp-bin/node-gyp," + "dist/node-gyp-bin/node-gyp.cmd,dist/node_modules/node-gyp/bin/node-gyp.js" +) +manifest.write_text( + f'''[toolchain] +version = "{version}" + +[package] +url = "https://registry.npmjs.org/pnpm/-/pnpm-{version}.tgz" +sha256 = "{archive_sha256}" +sha512 = "{archive_sha512}" +bytes = "{len(archive_bytes)}" +expanded_bytes = "{sum(map(len, files.values()))}" +format = "tar.gz" +prefix = "package" +entry_count = "{len(directories) + len(files)}" +file_count = "{len(files)}" +tree_sha256 = "{tree.hexdigest()}" +executable_paths = "{executable_paths}" +binary_path = "bin/pnpm.mjs" +binary_sha256 = "{hashlib.sha256(files['bin/pnpm.mjs']).hexdigest()}" +companion_path = "bin/pnpx.mjs" +companion_sha256 = "{hashlib.sha256(files['bin/pnpx.mjs']).hexdigest()}" +payload_path = "dist/pnpm.mjs" +payload_sha256 = "{hashlib.sha256(files['dist/pnpm.mjs']).hexdigest()}" +''', + encoding="utf-8", +) +metadata.write_text( + f"ARCHIVE_SHA256={archive_sha256}\nARCHIVE_BYTES={len(archive_bytes)}\n", + encoding="utf-8", +) +PY +printf '%s\n' 'pnpm = "11.5.0"' >"$proto_file" +# shellcheck source=/dev/null +. "$metadata" + +fake_curl="$tmp/curl" +# These literal lines become the fake curl script. +# shellcheck disable=SC2016 +printf '%s\n' \ + '#!/usr/bin/env bash' \ + 'set -euo pipefail' \ + ': "${FAKE_CURL_LOG:?}" "${FAKE_CURL_SOURCE:?}"' \ + 'printf "%s\n" "" >> "$FAKE_CURL_LOG"' \ + 'for argument in "$@"; do printf "<%s>\n" "$argument" >> "$FAKE_CURL_LOG"; done' \ + 'output=""' \ + 'url=""' \ + 'bound=""' \ + 'while (( $# )); do' \ + ' case "$1" in' \ + ' --output) output="$2"; shift 2 ;;' \ + ' --max-filesize) bound="$2"; shift 2 ;;' \ + ' https://*) url="$1"; shift ;;' \ + ' *) shift ;;' \ + ' esac' \ + 'done' \ + '[[ -n "$output" && -n "$url" && -n "$bound" ]]' \ + 'case "${FAKE_CURL_MODE:-copy}" in' \ + ' copy) cp "$FAKE_CURL_SOURCE" "$output" ;;' \ + ' bad) printf "%s" bad > "$output" ;;' \ + ' corrupt) cp "$FAKE_CURL_SOURCE" "$output"; printf Z | dd of="$output" bs=1 seek=1 count=1 conv=notrunc 2>/dev/null ;;' \ + ' fail) exit 97 ;;' \ + ' *) exit 98 ;;' \ + 'esac' >"$fake_curl" +chmod 0555 "$fake_curl" + +cache="$tmp/cache" +curl_log="$tmp/curl.log" +run_install() { + local selected_cache="$1" + local mode="${2:-copy}" + local selected_manifest="${3:-$manifest}" + env \ + EXPECTED_PNPM_VERSION=11.5.0 \ + FAKE_CURL_LOG="$curl_log" \ + FAKE_CURL_MODE="$mode" \ + FAKE_CURL_SOURCE="$fixture" \ + OLIPHAUNT_PNPM_ARCHIVE_EXTRACTOR="$extractor" \ + OLIPHAUNT_PNPM_CACHE_ROOT="$selected_cache" \ + OLIPHAUNT_PNPM_CURL="$fake_curl" \ + OLIPHAUNT_PNPM_CURL_PLATFORM_FLAGS="$curl_flags" \ + OLIPHAUNT_PNPM_MANIFEST="$selected_manifest" \ + OLIPHAUNT_PNPM_PROTO_FILE="$proto_file" \ + OLIPHAUNT_PNPM_TESTING=1 \ + RUNNER_OS=Windows \ + bash "$installer" +} + +installation="$(run_install "$cache")" +[ "$("$installation/bin/pnpm" --version)" = "11.5.0" ] || fail "fixture pnpm version mismatch" +if [ -e "$installation/plugins" ] || [ -e "$installation/moon" ]; then + fail "standalone installation unexpectedly contains Moon material" +fi +[ "$(grep -Fxc '' "$curl_log")" = "1" ] || fail "initial install did not perform one download" +for expected in \ + '<--fail>' \ + '<--location>' \ + '<--proto>' \ + '<=https>' \ + '<--proto-redir>' \ + '<--tlsv1.2>' \ + '<--retry-all-errors>' \ + '<--retry-connrefused>' \ + '<--connect-timeout>' \ + '<--max-time>' \ + '<--speed-limit>' \ + '<--speed-time>' \ + '<--remove-on-error>' \ + '<--ssl-revoke-best-effort>' \ + '<--max-filesize>' \ + "<$ARCHIVE_BYTES>" \ + ''; do + grep -Fqx "$expected" "$curl_log" || fail "curl invocation omitted $expected" +done + +rm -f "$curl_log" +cached="$(run_install "$cache" fail)" +[ "$cached" = "$installation" ] || fail "cache hit changed the installation path" +[ ! -e "$curl_log" ] || fail "valid cache hit attempted network access" + +chmod 0644 "$installation/pnpm/dist/pnpm.mjs" +printf '%s\n' 'tampered payload' >"$installation/pnpm/dist/pnpm.mjs" +rm -f "$curl_log" +repaired="$(run_install "$cache" fail)" +[ "$repaired" = "$installation" ] || fail "tree repair changed the installation path" +[ ! -e "$curl_log" ] || fail "tree repair ignored the verified archive cache" +[ "$("$installation/bin/pnpm" --version)" = "11.5.0" ] || fail "tree repair did not restore pnpm" + +chmod 0644 "$installation/pnpm/bin/pnpm.mjs" +rm -f "$curl_log" +run_install "$cache" fail >/dev/null +[ ! -e "$curl_log" ] || fail "mode repair ignored the verified archive cache" +[ -x "$installation/pnpm/bin/pnpm.mjs" ] || fail "executable-mode repair did not restore pnpm" + +chmod 0755 "$installation/bin/pnpm" +printf '%s\n' '#!/usr/bin/env bash' 'echo injected' >"$installation/bin/pnpm" +rm -f "$curl_log" +run_install "$cache" fail >/dev/null +[ ! -e "$curl_log" ] || fail "wrapper repair ignored the verified archive cache" +[ "$("$installation/bin/pnpm" --version)" = "11.5.0" ] || fail "wrapper repair did not restore pnpm" + +rm -rf "$installation" +chmod 0644 "$cache/archives/$ARCHIVE_SHA256.tgz" +printf '%s\n' corrupt >"$cache/archives/$ARCHIVE_SHA256.tgz" +rm -f "$curl_log" +installation="$(run_install "$cache")" +[ "$(grep -Fxc '' "$curl_log")" = "1" ] || fail "corrupt archive was not redownloaded once" +[ "$("$installation/bin/pnpm" --version)" = "11.5.0" ] || fail "corrupt archive repair failed" + +bad_cache="$tmp/bad-cache" +rm -f "$curl_log" +if run_install "$bad_cache" bad >"$tmp/bad.out" 2>"$tmp/bad.err"; then + fail "size-invalid download was accepted" +fi +if find "$bad_cache/installations" -type d -name verified -print -quit 2>/dev/null | grep -q .; then + fail "size-invalid download committed an installation" +fi + +corrupt_cache="$tmp/corrupt-cache" +rm -f "$curl_log" +if run_install "$corrupt_cache" corrupt >"$tmp/corrupt.out" 2>"$tmp/corrupt.err"; then + fail "same-size SHA-256-invalid download was accepted" +fi +grep -Fq 'downloaded SHA-256 mismatch' "$tmp/corrupt.err" || + fail "same-size corrupt download did not fail at SHA-256 verification" +if find "$corrupt_cache/installations" -type d -name verified -print -quit 2>/dev/null | grep -q .; then + fail "SHA-256-invalid download committed an installation" +fi + +wrong_sha512_manifest="$tmp/wrong-sha512.toml" +zeros="$(printf '0%.0s' {1..128})" +sed "s/^sha512 = \".*\"$/sha512 = \"$zeros\"/" "$manifest" >"$wrong_sha512_manifest" +sha512_cache="$tmp/sha512-cache" +rm -f "$curl_log" +if run_install "$sha512_cache" copy "$wrong_sha512_manifest" >"$tmp/sha512.out" 2>"$tmp/sha512.err"; then + fail "SHA-512-invalid manifest was accepted" +fi +grep -Fq 'downloaded SHA-512 mismatch' "$tmp/sha512.err" || + fail "SHA-512 mismatch did not reach SHA-512 verification" +if find "$sha512_cache/installations" -type d -name verified -print -quit 2>/dev/null | grep -q .; then + fail "SHA-512-invalid download committed an installation" +fi + +chmod 0755 "$installation/bin/pnpm" +printf '%s\n' '#!/usr/bin/env bash' 'echo preserved-after-interrupt' >"$installation/bin/pnpm" +rm -f "$curl_log" +if env \ + EXPECTED_PNPM_VERSION=11.5.0 \ + FAKE_CURL_LOG="$curl_log" \ + FAKE_CURL_MODE=fail \ + FAKE_CURL_SOURCE="$fixture" \ + OLIPHAUNT_PNPM_ARCHIVE_EXTRACTOR="$extractor" \ + OLIPHAUNT_PNPM_CACHE_ROOT="$cache" \ + OLIPHAUNT_PNPM_CURL="$fake_curl" \ + OLIPHAUNT_PNPM_CURL_PLATFORM_FLAGS="$curl_flags" \ + OLIPHAUNT_PNPM_MANIFEST="$manifest" \ + OLIPHAUNT_PNPM_PROTO_FILE="$proto_file" \ + OLIPHAUNT_PNPM_TEST_INTERRUPT_AFTER_BACKUP=1 \ + OLIPHAUNT_PNPM_TESTING=1 \ + RUNNER_OS=Windows \ + bash "$installer" >"$tmp/interrupt.out" 2>"$tmp/interrupt.err"; then + fail "test interrupt unexpectedly succeeded" +fi +grep -Fq 'preserved-after-interrupt' "$installation/bin/pnpm" || + fail "transactional interrupt did not restore the prior installation" +[ ! -e "$curl_log" ] || fail "transactional repair ignored the verified archive cache" +if find "$(dirname "$installation")" -mindepth 1 -maxdepth 1 -name '.verified.*' -print -quit | grep -q .; then + fail "transactional interrupt left a staging or backup directory" +fi +run_install "$cache" fail >/dev/null + +wrong_manifest="$tmp/wrong-url.toml" +sed 's#https://registry.npmjs.org/pnpm/-/pnpm-11.5.0.tgz#https://example.invalid/pnpm.tgz#' \ + "$manifest" >"$wrong_manifest" +rm -f "$curl_log" +if env \ + EXPECTED_PNPM_VERSION=11.5.0 \ + FAKE_CURL_LOG="$curl_log" \ + FAKE_CURL_SOURCE="$fixture" \ + OLIPHAUNT_PNPM_CACHE_ROOT="$tmp/wrong-url-cache" \ + OLIPHAUNT_PNPM_CURL="$fake_curl" \ + OLIPHAUNT_PNPM_MANIFEST="$wrong_manifest" \ + OLIPHAUNT_PNPM_PROTO_FILE="$proto_file" \ + OLIPHAUNT_PNPM_TESTING=1 \ + bash "$installer" >"$tmp/url.out" 2>"$tmp/url.err"; then + fail "non-canonical pnpm URL was accepted" +fi +[ ! -e "$curl_log" ] || fail "non-canonical URL reached the downloader" + +if OLIPHAUNT_PNPM_MANIFEST="$manifest" bash "$installer" >"$tmp/gate.out" 2>"$tmp/gate.err"; then + fail "test manifest override was accepted outside test mode" +fi +grep -Fq 'OLIPHAUNT_PNPM_MANIFEST is test-only' "$tmp/gate.err" || + fail "test override gate failed unclearly" + +mkdir "$tmp/real-cache-root" +ln -s "$tmp/real-cache-root" "$tmp/symlink-cache" +rm -f "$curl_log" +if run_install "$tmp/symlink-cache" >"$tmp/symlink.out" 2>"$tmp/symlink.err"; then + fail "symbolic-link cache root was accepted" +fi +[ ! -e "$curl_log" ] || fail "symbolic-link cache root reached the downloader" + +printf '%s\n' "Pinned standalone pnpm bootstrap fault tests passed." diff --git a/.github/actions/setup-node-runtime/action.yml b/.github/actions/setup-node-runtime/action.yml new file mode 100644 index 00000000..8ece1f65 --- /dev/null +++ b/.github/actions/setup-node-runtime/action.yml @@ -0,0 +1,47 @@ +name: Set up verified Node.js runtime +description: Install the exact digest-verified Node.js runtime pinned by Oliphaunt. + +inputs: + node-version: + description: Exact Node.js version expected from the repository pin. + required: false + default: "22.22.3" + +outputs: + execution-envelope: + description: Absolute digest-verified Node.js execution envelope. + value: ${{ steps.install.outputs.execution-envelope }} + +runs: + using: composite + steps: + - name: Restore verified Node.js archive + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ runner.temp }}/oliphaunt-node-runtime + key: verified-node-runtime-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.prototools', '.github/actions/setup-node-runtime/action.yml', '.github/actions/setup-moon/install-pinned-node.sh', '.github/actions/setup-moon/toolchain-archive.py', 'src/sources/toolchains/node-runtime.toml', 'tools/dev/curl-platform-flags.sh') }} + + - name: Install verified Node.js runtime + id: install + shell: bash + env: + EXPECTED_NODE_VERSION: ${{ inputs.node-version }} + OLIPHAUNT_NODE_RUNTIME_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-node-runtime + run: | # zizmor: ignore[github-env] only a digest-verified runner-temp path is exported. + set -euo pipefail + binary="$(bash .github/actions/setup-moon/install-pinned-node.sh)" + observed="$($binary --version)" + if [[ "$observed" != "v$EXPECTED_NODE_VERSION" ]]; then + echo "Expected Node.js v${EXPECTED_NODE_VERSION}; observed ${observed}" >&2 + exit 1 + fi + export_dir="$(dirname "$binary")" + execution_envelope="$(cd "$export_dir/.." && pwd -P)" + output_envelope="$execution_envelope" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + export_dir="$(cygpath -w "$export_dir")" + output_envelope="$(cygpath -w "$execution_envelope")" + fi + echo "$export_dir" >> "$GITHUB_PATH" + echo "execution-envelope=$output_envelope" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/setup-npm-publisher/action.yml b/.github/actions/setup-npm-publisher/action.yml new file mode 100644 index 00000000..77c33fea --- /dev/null +++ b/.github/actions/setup-npm-publisher/action.yml @@ -0,0 +1,77 @@ +name: Set up npm publisher +description: Install and verify the exact npm CLI used for trusted publication. + +inputs: + npm-version: + description: Exact stable npm version. + required: true + +outputs: + node-executable: + description: Absolute verified Node.js executable used to launch npm. + value: ${{ steps.install.outputs.node-executable }} + npm-cli: + description: Absolute digest-verified npm CLI entrypoint. + value: ${{ steps.install.outputs.npm-cli }} + execution-envelope: + description: Absolute digest-verified npm execution envelope. + value: ${{ steps.install.outputs.execution-envelope }} + +runs: + using: composite + steps: + - name: Restore verified npm publisher archive + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ runner.temp }}/oliphaunt-npm-publisher + key: verified-npm-publisher-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.github/actions/setup-npm-publisher/action.yml', '.github/actions/setup-npm-publisher/install.sh', '.github/actions/setup-moon/toolchain-archive.py', 'src/sources/toolchains/npm-publisher.toml', 'tools/dev/curl-platform-flags.sh') }} + + - name: Install exact npm publisher + id: install + shell: bash + env: + NPM_PUBLISHER_VERSION: ${{ inputs.npm-version }} + OLIPHAUNT_NPM_PUBLISHER_CACHE_ROOT: ${{ runner.temp }}/oliphaunt-npm-publisher + run: | # zizmor: ignore[github-env] only a full-tree digest-verified runner-temp path is exported. + set -euo pipefail + publisher_bin="$(bash .github/actions/setup-npm-publisher/install.sh)" + node_executable="$(command -v node)" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + node_executable="$(cygpath -u "$node_executable")" + fi + npm_cli="$(cd "$publisher_bin/../npm/bin" && pwd -P)/npm-cli.js" + execution_envelope="$(cd "$publisher_bin/.." && pwd -P)" + if [[ "$node_executable" != /* || ! -f "$node_executable" || -L "$node_executable" ]]; then + echo "The verified Node.js executable is not a regular absolute path: ${node_executable}" >&2 + exit 1 + fi + if [[ ! -f "$npm_cli" || -L "$npm_cli" ]]; then + echo "The verified npm CLI is not a regular file: ${npm_cli}" >&2 + exit 1 + fi + node_version="$("$node_executable" --version)" + npm_version="$("$node_executable" "$npm_cli" --version)" + if [[ "$npm_version" != "$NPM_PUBLISHER_VERSION" ]]; then + echo "Expected npm ${NPM_PUBLISHER_VERSION}; observed ${npm_version}" >&2 + exit 1 + fi + tools/dev/bun.sh tools/release/npm-trusted-publishing.mjs check-runtime \ + --node "$node_version" \ + --npm "$npm_version" + export_dir="$publisher_bin" + output_node="$node_executable" + output_npm_cli="$npm_cli" + output_envelope="$execution_envelope" + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + export_dir="$(cygpath -w "$export_dir")" + output_node="$(cygpath -w "$output_node")" + output_npm_cli="$(cygpath -w "$output_npm_cli")" + output_envelope="$(cygpath -w "$execution_envelope")" + fi + echo "$export_dir" >> "$GITHUB_PATH" + { + echo "node-executable=$output_node" + echo "npm-cli=$output_npm_cli" + echo "execution-envelope=$output_envelope" + } >> "$GITHUB_OUTPUT" diff --git a/.github/actions/setup-npm-publisher/install.sh b/.github/actions/setup-npm-publisher/install.sh new file mode 100755 index 00000000..a4478509 --- /dev/null +++ b/.github/actions/setup-npm-publisher/install.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +set -euo pipefail + +fail() { + echo "setup-npm-publisher/install.sh: $*" >&2 + exit 1 +} + +root="${OLIPHAUNT_NPM_PUBLISHER_ROOT:-}" +if [ -z "$root" ]; then + root="$(git rev-parse --show-toplevel 2>/dev/null)" || + fail "must run inside the Oliphaunt checkout" +fi +manifest="${OLIPHAUNT_NPM_PUBLISHER_MANIFEST:-$root/src/sources/toolchains/npm-publisher.toml}" +extractor="${OLIPHAUNT_NPM_PUBLISHER_ARCHIVE_EXTRACTOR:-$root/.github/actions/setup-moon/toolchain-archive.py}" +curl_platform_flags="$root/tools/dev/curl-platform-flags.sh" +cache_root="${OLIPHAUNT_NPM_PUBLISHER_CACHE_ROOT:-${RUNNER_TEMP:-$root/target}/oliphaunt-npm-publisher}" + +case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) + command -v cygpath >/dev/null 2>&1 && cache_root="$(cygpath -u "$cache_root")" + ;; +esac +for path in "$manifest" "$extractor" "$curl_platform_flags"; do + [ -f "$path" ] && [ ! -L "$path" ] || fail "missing regular bootstrap input: $path" +done +# shellcheck source=tools/dev/curl-platform-flags.sh +. "$curl_platform_flags" + +python="" +for candidate in python3 python; do + if command -v "$candidate" >/dev/null 2>&1; then python="$candidate"; break; fi +done +[ -n "$python" ] || fail "python3 or python is required for safe archive extraction" +command -v node >/dev/null 2>&1 || fail "the verified Node.js runtime must be on PATH" + +manifest_value() { + local section="$1" key="$2" + awk -v wanted_section="$section" -v wanted_key="$key" ' + /^[[:space:]]*\[[^]]+\][[:space:]]*$/ { + current=$0; gsub(/^[[:space:]]*\[|\][[:space:]]*$/, "", current); next + } + current == wanted_section && $0 ~ "^[[:space:]]*" wanted_key "[[:space:]]*=" { + count++; line=$0; sub(/^[^=]*=[[:space:]]*"/, "", line) + sub(/"[[:space:]]*$/, "", line); value=line + } + END { if (count != 1 || value == "") exit 1; print value } + ' "$manifest" +} +validate_digest() { + [ "${#2}" -eq "$1" ] && [[ ! "$2" =~ [^0-9a-f] ]] || + fail "invalid lowercase hexadecimal digest in $manifest" +} +validate_count() { + case "$1" in '' | *[!0-9]*) fail "invalid positive count in $manifest" ;; esac + [ "$1" -gt 0 ] || fail "invalid positive count in $manifest" +} +hash_file() { + local algorithm="$1" path="$2" + if command -v "${algorithm}sum" >/dev/null 2>&1; then + "${algorithm}sum" "$path" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + case "$algorithm" in + sha256) shasum -a 256 "$path" | awk '{print $1}' ;; + sha512) shasum -a 512 "$path" | awk '{print $1}' ;; + esac + else + "$python" - "$algorithm" "$path" <<'PY' +import hashlib +import pathlib +import sys +digest = hashlib.new(sys.argv[1]) +with pathlib.Path(sys.argv[2]).open("rb") as stream: + while block := stream.read(1024 * 1024): + digest.update(block) +print(digest.hexdigest()) +PY + fi +} + +version="$(manifest_value toolchain version)" || fail "$manifest is missing toolchain.version" +url="$(manifest_value package url)" || fail "$manifest is missing package.url" +archive_sha256="$(manifest_value package sha256)" || fail "$manifest is missing package.sha256" +archive_sha512="$(manifest_value package sha512)" || fail "$manifest is missing package.sha512" +archive_bytes="$(manifest_value package bytes)" || fail "$manifest is missing package.bytes" +expanded_bytes="$(manifest_value package expanded_bytes)" || fail "$manifest is missing package.expanded_bytes" +format="$(manifest_value package format)" || fail "$manifest is missing package.format" +prefix="$(manifest_value package prefix)" || fail "$manifest is missing package.prefix" +entry_count="$(manifest_value package entry_count)" || fail "$manifest is missing package.entry_count" +file_count="$(manifest_value package file_count)" || fail "$manifest is missing package.file_count" +tree_sha256="$(manifest_value package tree_sha256)" || fail "$manifest is missing package.tree_sha256" +executable_paths="$(manifest_value package executable_paths)" || fail "$manifest is missing package.executable_paths" +binary_path="$(manifest_value package binary_path)" || fail "$manifest is missing package.binary_path" +binary_sha256="$(manifest_value package binary_sha256)" || fail "$manifest is missing package.binary_sha256" +binary_bytes="$(manifest_value package binary_bytes)" || fail "$manifest is missing package.binary_bytes" +companion_path="$(manifest_value package companion_path)" || fail "$manifest is missing package.companion_path" +companion_sha256="$(manifest_value package companion_sha256)" || fail "$manifest is missing package.companion_sha256" +companion_bytes="$(manifest_value package companion_bytes)" || fail "$manifest is missing package.companion_bytes" +package_json_sha256="$(manifest_value package package_json_sha256)" || fail "$manifest is missing package.package_json_sha256" +package_json_bytes="$(manifest_value package package_json_bytes)" || fail "$manifest is missing package.package_json_bytes" + +[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "npm version must be exact stable semver" +[ "$url" = "https://registry.npmjs.org/npm/-/npm-$version.tgz" ] || fail "npm archive URL is not canonical" +[ "$format" = tar.gz ] && [ "$prefix" = package ] || fail "npm archive shape is not canonical" +[ "$binary_path" = bin/npm-cli.js ] && [ "$companion_path" = bin/npx-cli.js ] || + fail "npm CLI paths are not canonical" +validate_digest 64 "$archive_sha256" +validate_digest 128 "$archive_sha512" +for digest in "$tree_sha256" "$binary_sha256" "$companion_sha256" "$package_json_sha256"; do + validate_digest 64 "$digest" +done +for value in "$archive_bytes" "$expanded_bytes" "$entry_count" "$file_count" "$binary_bytes" "$companion_bytes" "$package_json_bytes"; do + validate_count "$value" +done +IFS=',' read -r -a executables <<<"$executable_paths" +[ "${#executables[@]}" -gt 0 ] || fail "npm executable path inventory is empty" + +curl_command="${OLIPHAUNT_NPM_PUBLISHER_CURL:-curl}" +command -v "$curl_command" >/dev/null 2>&1 || fail "missing required command: $curl_command" +command -v mktemp >/dev/null 2>&1 || fail "missing required command: mktemp" +[ ! -L "$cache_root" ] || fail "npm publisher cache root must not be a symbolic link" +umask 077 +mkdir -p "$cache_root" +[ -d "$cache_root" ] && [ ! -L "$cache_root" ] || fail "npm publisher cache root is not a real directory" +archive_root="$cache_root/archives" +install_parent="$cache_root/installations/npm-$version" +for directory in "$archive_root" "$cache_root/installations" "$install_parent"; do + [ ! -L "$directory" ] || fail "npm publisher cache must not contain symbolic-link directories" + mkdir -p "$directory" +done + +final="$install_parent/verified" +receipt_text="$(printf 'npm_version=%s\narchive_sha256=%s\ntree_sha256=%s' "$version" "$archive_sha256" "$tree_sha256")" +npm_wrapper_text="$(printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../npm/bin/npm-cli.js" "$@"')" +npx_wrapper_text="$(printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' \ + 'script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"' \ + 'exec node "$script_dir/../npm/bin/npx-cli.js" "$@"')" +npm_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\npm\bin\npm-cli.js" %*')" +npx_cmd_text="$(printf '%s\r\n' '@ECHO OFF' 'node "%~dp0..\npm\bin\npx-cli.js" %*')" + +cache_valid() { + local candidate="$1" + [ -d "$candidate" ] && [ ! -L "$candidate" ] || return 1 + [ "$(find "$candidate" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = 3 ] || return 1 + [ -d "$candidate/bin" ] && [ ! -L "$candidate/bin" ] || return 1 + [ -d "$candidate/npm" ] && [ ! -L "$candidate/npm" ] || return 1 + [ "$(find "$candidate/bin" -mindepth 1 -maxdepth 1 | wc -l | tr -d '[:space:]')" = 4 ] || return 1 + for path in "$candidate/npm/$binary_path" "$candidate/npm/$companion_path" \ + "$candidate/npm/package.json" "$candidate/bin/npm" "$candidate/bin/npx" \ + "$candidate/bin/npm.cmd" "$candidate/bin/npx.cmd" "$candidate/receipt"; do + [ -f "$path" ] && [ ! -L "$path" ] || return 1 + done + [ "$(wc -c <"$candidate/npm/$binary_path" | tr -d '[:space:]')" = "$binary_bytes" ] || return 1 + [ "$(wc -c <"$candidate/npm/$companion_path" | tr -d '[:space:]')" = "$companion_bytes" ] || return 1 + [ "$(wc -c <"$candidate/npm/package.json" | tr -d '[:space:]')" = "$package_json_bytes" ] || return 1 + [ "$(hash_file sha256 "$candidate/npm/$binary_path")" = "$binary_sha256" ] || return 1 + [ "$(hash_file sha256 "$candidate/npm/$companion_path")" = "$companion_sha256" ] || return 1 + [ "$(hash_file sha256 "$candidate/npm/package.json")" = "$package_json_sha256" ] || return 1 + [ "$(cat "$candidate/bin/npm")" = "$npm_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/npx")" = "$npx_wrapper_text" ] || return 1 + [ "$(cat "$candidate/bin/npm.cmd")" = "$npm_cmd_text" ] || return 1 + [ "$(cat "$candidate/bin/npx.cmd")" = "$npx_cmd_text" ] || return 1 + case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) ;; + *) [ -x "$candidate/bin/npm" ] && [ -x "$candidate/bin/npx" ] || return 1 ;; + esac + local args=(tree-digest --root "$candidate/npm") executable + for executable in "${executables[@]}"; do args+=(--executable "$executable"); done + [ "$("$python" "$extractor" "${args[@]}" 2>/dev/null)" = "$file_count $tree_sha256" ] || return 1 + [ "$(node "$candidate/npm/$binary_path" --version 2>/dev/null)" = "$version" ] || return 1 + [ "$(cat "$candidate/receipt")" = "$receipt_text" ] || return 1 +} + +if cache_valid "$final"; then printf '%s\n' "$final/bin"; exit 0; fi +archive="$archive_root/$archive_sha256.tgz" +if ! { [ -f "$archive" ] && [ ! -L "$archive" ] && + [ "$(wc -c <"$archive" | tr -d '[:space:]')" = "$archive_bytes" ] && + [ "$(hash_file sha256 "$archive")" = "$archive_sha256" ] && + [ "$(hash_file sha512 "$archive")" = "$archive_sha512" ]; }; then + rm -f "$archive" + partial="$(mktemp "$archive_root/.download.XXXXXX")" + tls_flag="$(oliphaunt_curl_platform_tls_flag)" + curl_args=(--fail --location --silent --show-error --proto '=https' --proto-redir '=https' + --tlsv1.2 --retry 5 --retry-all-errors --retry-connrefused --retry-delay 2 + --retry-max-time 300 --connect-timeout 20 --max-time 300 --speed-limit 1024 + --speed-time 30 --remove-on-error --max-filesize "$archive_bytes" --output "$partial") + [ -z "$tls_flag" ] || curl_args+=("$tls_flag") + curl_args+=("$url") + if ! "$curl_command" "${curl_args[@]}"; then + rm -f "$partial"; fail "could not download pinned npm publisher archive" + fi + [ "$(wc -c <"$partial" | tr -d '[:space:]')" = "$archive_bytes" ] && + [ "$(hash_file sha256 "$partial")" = "$archive_sha256" ] && + [ "$(hash_file sha512 "$partial")" = "$archive_sha512" ] || { + rm -f "$partial"; fail "downloaded npm publisher archive integrity mismatch" + } + chmod 0444 "$partial" + mv "$partial" "$archive" +fi + +stage="$(mktemp -d "$install_parent/.verified.stage.XXXXXX")" +backup="" +old_moved=0 +cleanup() { + local rc="$?" + trap - EXIT HUP INT TERM + rm -rf "$stage" + if [ "$old_moved" = 1 ] && [ -n "$backup" ] && [ -e "$backup" ] && [ ! -e "$final" ]; then + mv "$backup" "$final" || rc=1 + elif [ -n "$backup" ]; then rm -rf "$backup"; fi + exit "$rc" +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +extract_args=(extract --archive "$archive" --format "$format" --prefix "$prefix" + --entry-count "$entry_count" --expected-bytes "$archive_bytes" + --expanded-bytes "$expanded_bytes" --destination "$stage/npm" + --required "$binary_path" --required "$companion_path" --required package.json) +for executable in "${executables[@]}"; do + extract_args+=(--required "$executable" --executable "$executable") +done +"$python" "$extractor" "${extract_args[@]}" +mkdir -p "$stage/bin" +printf '%s\n' "$npm_wrapper_text" >"$stage/bin/npm" +printf '%s\n' "$npx_wrapper_text" >"$stage/bin/npx" +printf '%s\n' "$npm_cmd_text" >"$stage/bin/npm.cmd" +printf '%s\n' "$npx_cmd_text" >"$stage/bin/npx.cmd" +chmod 0555 "$stage/bin/npm" "$stage/bin/npx" +chmod 0444 "$stage/bin/npm.cmd" "$stage/bin/npx.cmd" +printf '%s\n' "$receipt_text" >"$stage/receipt" +chmod 0444 "$stage/receipt" +cache_valid "$stage" || fail "staged npm publisher failed integrity or version validation" + +if [ -e "$final" ] || [ -L "$final" ]; then + backup="$(mktemp -d "$install_parent/.verified.backup.XXXXXX")"; rmdir "$backup" + mv "$final" "$backup"; old_moved=1 +fi +if [ "${OLIPHAUNT_NPM_PUBLISHER_TEST_INTERRUPT_AFTER_BACKUP:-0}" = 1 ]; then + [ "${OLIPHAUNT_NPM_PUBLISHER_TESTING:-0}" = 1 ] || + fail "OLIPHAUNT_NPM_PUBLISHER_TEST_INTERRUPT_AFTER_BACKUP is test-only" + kill -TERM "$$" +fi +mv "$stage" "$final"; stage="" +if [ "$old_moved" = 1 ]; then rm -rf "$backup"; backup=""; old_moved=0; fi +printf '%s\n' "$final/bin" diff --git a/.github/actions/setup-npm-publisher/install.test.sh b/.github/actions/setup-npm-publisher/install.test.sh new file mode 100755 index 00000000..1c5311c5 --- /dev/null +++ b/.github/actions/setup-npm-publisher/install.test.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +installer="$root/.github/actions/setup-npm-publisher/install.sh" +action="$root/.github/actions/setup-npm-publisher/action.yml" +extractor="$root/.github/actions/setup-moon/toolchain-archive.py" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/payload/package/bin" "$work/bin" "$work/blockers" +# These are intentionally literal composite-action source assertions. +# shellcheck disable=SC2016 +{ + grep -Fq 'export_dir="$(cygpath -w "$export_dir")"' "$action" + grep -Fq 'echo "$export_dir" >> "$GITHUB_PATH"' "$action" + grep -Fq 'value: ${{ steps.install.outputs.node-executable }}' "$action" + grep -Fq 'value: ${{ steps.install.outputs.npm-cli }}' "$action" + grep -Fq 'npm_version="$("$node_executable" "$npm_cli" --version)"' "$action" + grep -Fq 'node_executable="$(cygpath -u "$node_executable")"' "$action" + grep -Fq 'output_node="$(cygpath -w "$output_node")"' "$action" + grep -Fq 'output_npm_cli="$(cygpath -w "$output_npm_cli")"' "$action" +} +if grep -Eq '(^|[[:space:]])npm[[:space:]]+--version' "$action"; then + echo "setup action reintroduced an ambient npm version probe" >&2 + exit 1 +fi + +cat >"$work/payload/package/bin/npm-cli.js" <<'EOF' +#!/usr/bin/env node +console.log("11.18.0"); +EOF +cat >"$work/payload/package/bin/npx-cli.js" <<'EOF' +#!/usr/bin/env node +console.log("11.18.0"); +EOF +printf '{"name":"npm","version":"11.18.0"}\n' >"$work/payload/package/package.json" +chmod 0755 "$work/payload/package/bin/npm-cli.js" "$work/payload/package/bin/npx-cli.js" +tar -C "$work/payload" -czf "$work/npm.tgz" package +archive_bytes="$(wc -c <"$work/npm.tgz" | tr -d '[:space:]')" +archive_sha256="$(sha256sum "$work/npm.tgz" | awk '{print $1}')" +archive_sha512="$(sha512sum "$work/npm.tgz" | awk '{print $1}')" +entry_count="$(tar -tzf "$work/npm.tgz" | wc -l | tr -d '[:space:]')" +expanded_bytes="$( + find "$work/payload/package" -type f -printf '%s\n' | awk '{ total += $1 } END { print total }' +)" +python3 "$extractor" extract --archive "$work/npm.tgz" --format tar.gz --prefix package \ + --entry-count "$entry_count" --expected-bytes "$archive_bytes" \ + --expanded-bytes "$expanded_bytes" --destination "$work/extracted" \ + --required bin/npm-cli.js --executable bin/npm-cli.js \ + --required bin/npx-cli.js --executable bin/npx-cli.js \ + --required package.json +tree_result="$(python3 "$extractor" tree-digest --root "$work/extracted" \ + --executable bin/npm-cli.js --executable bin/npx-cli.js)" +file_count="${tree_result%% *}" +tree_sha256="${tree_result#* }" + +cat >"$work/npm.toml" <"$work/bin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +joined=" $* " +for required in "--fail" "--location" "--proto =https" "--proto-redir =https" "--tlsv1.2" "--retry-all-errors" "--retry-connrefused" "--remove-on-error" "--max-filesize" "--ssl-revoke-best-effort"; do + [[ "$joined" == *" $required "* ]] || exit 91 +done +output="" +last="" +while (($#)); do + case "$1" in --output) output="$2"; shift 2 ;; *) last="$1"; shift ;; esac +done +[ "$last" = "https://registry.npmjs.org/npm/-/npm-11.18.0.tgz" ] || exit 92 +printf '%s\n' "$joined" >>"$OLIPHAUNT_NPM_TEST_REQUEST_LOG" +cp "$OLIPHAUNT_NPM_TEST_ARCHIVE" "$output" +EOF +chmod 0755 "$work/bin/curl" +for blocked in npm corepack; do + cat >"$work/blockers/$blocked" <>"$work/ambient.log" +exit 99 +EOF + chmod 0755 "$work/blockers/$blocked" +done + +common=( + OLIPHAUNT_NPM_PUBLISHER_ROOT="$root" + OLIPHAUNT_NPM_PUBLISHER_MANIFEST="$work/npm.toml" + OLIPHAUNT_NPM_PUBLISHER_ARCHIVE_EXTRACTOR="$extractor" + OLIPHAUNT_NPM_PUBLISHER_CACHE_ROOT="$work/cache" + OLIPHAUNT_NPM_PUBLISHER_CURL="$work/bin/curl" + OLIPHAUNT_NPM_TEST_ARCHIVE="$work/npm.tgz" + OLIPHAUNT_NPM_TEST_REQUEST_LOG="$work/requests.log" + RUNNER_OS=Windows + PATH="$work/blockers:$PATH" +) + +publisher_bin="$(env "${common[@]}" bash "$installer")" +[ "$(PATH="$publisher_bin:$work/blockers:$PATH" npm --version)" = 11.18.0 ] +[ ! -e "$work/ambient.log" ] +[ "$(wc -l <"$work/requests.log" | tr -d '[:space:]')" = 1 ] + +# Valid caches are fully offline and a modified CLI tree is repaired from the verified archive. +[ "$(env "${common[@]}" OLIPHAUNT_NPM_PUBLISHER_CURL=false bash "$installer")" = "$publisher_bin" ] +cli="$work/cache/installations/npm-11.18.0/verified/npm/bin/npm-cli.js" +chmod u+w "$cli" +printf 'tampered\n' >"$cli" +env "${common[@]}" OLIPHAUNT_NPM_PUBLISHER_CURL=false bash "$installer" >/dev/null +[ "$(PATH="$publisher_bin:$work/blockers:$PATH" npm --version)" = 11.18.0 ] +[ ! -e "$work/ambient.log" ] +[ "$(wc -l <"$work/requests.log" | tr -d '[:space:]')" = 1 ] + +# A corrupt cached archive is redownloaded before a damaged installation is replaced. +archive="$work/cache/archives/$archive_sha256.tgz" +chmod u+w "$archive" "$cli" +printf 'corrupt archive\n' >"$archive" +printf 'corrupt cli\n' >"$cli" +env "${common[@]}" bash "$installer" >/dev/null +[ "$(PATH="$publisher_bin:$work/blockers:$PATH" npm --version)" = 11.18.0 ] +[ "$(wc -l <"$work/requests.log" | tr -d '[:space:]')" = 2 ] + +# A wrong Node runtime cannot bless the staged npm tree or disturb the valid install. +mkdir -p "$work/wrong-node" +printf '%s\n' '#!/usr/bin/env bash' 'echo 0.0.0' >"$work/wrong-node/node" +chmod 0755 "$work/wrong-node/node" +set +e +env "${common[@]}" PATH="$work/wrong-node:$work/blockers:$PATH" \ + OLIPHAUNT_NPM_PUBLISHER_CURL=false bash "$installer" >/dev/null 2>&1 +wrong_runtime_status=$? +set -e +[ "$wrong_runtime_status" -ne 0 ] +[ "$(PATH="$publisher_bin:$work/blockers:$PATH" npm --version)" = 11.18.0 ] + +# Interrupted promotion restores the previous directory and exposes no partial stage. +chmod u+w "$cli" +printf 'interrupted prior cli\n' >"$cli" +set +e +env "${common[@]}" OLIPHAUNT_NPM_PUBLISHER_CURL=false \ + OLIPHAUNT_NPM_PUBLISHER_TESTING=1 \ + OLIPHAUNT_NPM_PUBLISHER_TEST_INTERRUPT_AFTER_BACKUP=1 \ + bash "$installer" >/dev/null 2>&1 +interrupt_status=$? +set -e +[ "$interrupt_status" -eq 143 ] +grep -Fxq 'interrupted prior cli' "$cli" +[ -z "$(find "$work/cache/installations/npm-11.18.0" -maxdepth 1 -name '.*.stage.*' -print -quit)" ] +env "${common[@]}" OLIPHAUNT_NPM_PUBLISHER_CURL=false bash "$installer" >/dev/null + +# A manifest/archive identity mismatch fails before promotion and preserves the valid runtime. +sed "s/^sha256 = \"$archive_sha256\"/sha256 = \"$(printf '0%.0s' {1..64})\"/" \ + "$work/npm.toml" >"$work/bad-digest.toml" +set +e +env "${common[@]}" OLIPHAUNT_NPM_PUBLISHER_MANIFEST="$work/bad-digest.toml" \ + bash "$installer" >/dev/null 2>&1 +bad_digest_status=$? +set -e +[ "$bad_digest_status" -ne 0 ] +[ "$(PATH="$publisher_bin:$work/blockers:$PATH" npm --version)" = 11.18.0 ] +[ ! -e "$work/ambient.log" ] + +echo "Pinned npm publisher bootstrap fault tests passed." diff --git a/.github/actions/setup-rust-tools/action.yml b/.github/actions/setup-rust-tools/action.yml index 51c709c3..694be21e 100644 --- a/.github/actions/setup-rust-tools/action.yml +++ b/.github/actions/setup-rust-tools/action.yml @@ -5,7 +5,7 @@ inputs: toolchain: description: Rust toolchain version. required: false - default: "1.92" + default: "1.93.1" components: description: Comma-separated Rust components. required: false @@ -43,8 +43,19 @@ runs: workspaces: ${{ inputs.cache-workspaces }} save-if: ${{ inputs.cache-save-if }} + - name: Install ripgrep + shell: bash + run: | + set -euo pipefail + if command -v rg >/dev/null 2>&1; then + rg --version + exit 0 + fi + cargo install ripgrep --version 15.1.0 --locked + rg --version + - name: Install Cargo tools if: ${{ inputs.tools != '' }} - uses: taiki-e/install-action@1f2425cdb59f8fffb99ee16a5968edf6f57a2b93 + uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 with: tool: ${{ inputs.tools }} diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 00000000..5c6b616f --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,31 @@ +name: Set up Rust +description: Install the pinned Rust toolchain and optional Rust tools. + +inputs: + components: + description: Comma-separated Rust components. + required: false + default: "" + tools: + description: Comma-separated Rust tools. + required: false + default: "" + cache-save-if: + description: Expression string passed to Swatinem/rust-cache save-if. + required: false + default: "false" + cache: + description: Whether to enable the Cargo cache. + required: false + default: "true" + +runs: + using: composite + steps: + - name: Set up Rust tooling + uses: ./.github/actions/setup-rust-tools + with: + components: ${{ inputs.components }} + cache: ${{ inputs.cache }} + cache-save-if: ${{ inputs.cache-save-if }} + tools: ${{ inputs.tools }} diff --git a/.github/actions/setup-wasmer-llvm/action.yml b/.github/actions/setup-wasmer-llvm/action.yml index 8d5fb2d0..28e72b04 100644 --- a/.github/actions/setup-wasmer-llvm/action.yml +++ b/.github/actions/setup-wasmer-llvm/action.yml @@ -5,6 +5,12 @@ inputs: url: description: Wasmer LLVM archive URL. required: true + sha256: + description: Expected SHA-256 digest of the Wasmer LLVM archive. + required: true + bytes: + description: Expected exact byte size of the Wasmer LLVM archive. + required: true version: description: Expected LLVM major.minor version. required: false @@ -26,19 +32,42 @@ runs: shell: bash env: LLVM_URL: ${{ inputs.url }} + LLVM_SHA256: ${{ inputs.sha256 }} + LLVM_BYTES: ${{ inputs.bytes }} LLVM_VERSION: ${{ inputs.version }} run: | + if [[ ! "$LLVM_URL" =~ ^https://[^[:space:]]+$ ]]; then + echo "Wasmer LLVM URL must be a single HTTPS URL" >&2 + exit 2 + fi + if [[ ! "$LLVM_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then + echo "Wasmer LLVM version must contain only numeric components" >&2 + exit 2 + fi + if [[ ! "$LLVM_SHA256" =~ ^[[:xdigit:]]{64}$ ]]; then + echo "Wasmer LLVM SHA-256 must contain exactly 64 hexadecimal characters" >&2 + exit 2 + fi + if [[ ! "$LLVM_BYTES" =~ ^[1-9][0-9]*$ ]] || (( LLVM_BYTES > 2147483648 )); then + echo "Wasmer LLVM byte size must be an integer between 1 and 2147483648" >&2 + exit 2 + fi + normalized_sha="$(printf '%s' "$LLVM_SHA256" | tr 'A-F' 'a-f')" if command -v shasum >/dev/null 2>&1; then url_hash="$(printf '%s' "$LLVM_URL" | shasum -a 256 | cut -c1-16)" - else + elif command -v sha256sum >/dev/null 2>&1; then url_hash="$(printf '%s' "$LLVM_URL" | sha256sum | cut -c1-16)" + else + echo "Wasmer LLVM setup requires shasum or sha256sum" >&2 + exit 127 fi - echo "key=wasmer-llvm-${RUNNER_OS}-${RUNNER_ARCH}-${LLVM_VERSION}-${url_hash}" >> "$GITHUB_OUTPUT" + echo "sha256=$normalized_sha" >> "$GITHUB_OUTPUT" + echo "key=wasmer-llvm-${RUNNER_OS}-${RUNNER_ARCH}-${LLVM_VERSION}-${LLVM_BYTES}-${normalized_sha}-${url_hash}" >> "$GITHUB_OUTPUT" - name: Restore Wasmer LLVM cache id: cache if: ${{ inputs.cache == 'true' }} - uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: ${{ runner.temp }}/wasmer-llvm/${{ steps.cache-key.outputs.key }} key: ${{ steps.cache-key.outputs.key }} @@ -46,64 +75,19 @@ runs: - name: Install Wasmer LLVM shell: bash env: + ACTION_PATH: ${{ github.action_path }} LLVM_URL: ${{ inputs.url }} + LLVM_SHA256: ${{ steps.cache-key.outputs.sha256 }} + LLVM_BYTES: ${{ inputs.bytes }} LLVM_VERSION: ${{ inputs.version }} CACHE_KEY: ${{ steps.cache-key.outputs.key }} run: | # zizmor: ignore[github-env] repo-owned LLVM URLs and validated tool paths are exported for later workflow steps. - runner_temp="$RUNNER_TEMP" - if [ "$RUNNER_OS" = "Windows" ]; then - runner_temp="$(cygpath -u "$RUNNER_TEMP")" - fi - - cache_root="$runner_temp/wasmer-llvm/$CACHE_KEY" - install_dir="$cache_root/llvm" - if [ "$RUNNER_OS" = "Windows" ]; then - llvm_config="$install_dir/bin/llvm-config.exe" - else - llvm_config="$install_dir/bin/llvm-config" - fi - - if [ ! -x "$llvm_config" ]; then - archive="$runner_temp/llvm-${LLVM_VERSION}.tar.xz" - rm -rf "$install_dir" - mkdir -p "$install_dir" - curl -L --fail --retry 3 --output "$archive" "$LLVM_URL" - tar -xJf "$archive" -C "$install_dir" - fi - - if [ ! -x "$llvm_config" ]; then - echo "LLVM extraction did not produce bin/llvm-config" >&2 - find "$install_dir" -maxdepth 3 -type f -name 'llvm-config*' -print >&2 - exit 1 - fi - - if [ "$RUNNER_OS" = "Windows" ]; then - env_prefix="$(cygpath -w "$install_dir")" - path_entry="$(cygpath -w "$install_dir/bin")" - else - env_prefix="$install_dir" - path_entry="$install_dir/bin" - fi - - echo "LLVM_PATH=$env_prefix" >> "$GITHUB_ENV" - echo "LLVM_SYS_221_PREFIX=$env_prefix" >> "$GITHUB_ENV" - echo "$path_entry" >> "$GITHUB_PATH" - - version="$("$llvm_config" --version)" - case "$version" in - "$LLVM_VERSION".*) ;; - *) echo "expected LLVM $LLVM_VERSION.x, got $version" >&2; exit 1 ;; - esac - - targets="$("$llvm_config" --targets-built)" - case "$targets" in - *LoongArch*WebAssembly*|*WebAssembly*LoongArch*) ;; - *) echo "expected Wasmer LLVM build with LoongArch and WebAssembly targets; got $targets" >&2; exit 1 ;; - esac + bash "$ACTION_PATH/install.sh" - name: Save Wasmer LLVM cache if: ${{ inputs.cache == 'true' && inputs.cache-save-if == 'true' && steps.cache.outputs.cache-hit != 'true' }} - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 with: path: ${{ runner.temp }}/wasmer-llvm/${{ steps.cache-key.outputs.key }} key: ${{ steps.cache-key.outputs.key }} diff --git a/.github/actions/setup-wasmer-llvm/install.sh b/.github/actions/setup-wasmer-llvm/install.sh new file mode 100755 index 00000000..a32bc6e0 --- /dev/null +++ b/.github/actions/setup-wasmer-llvm/install.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${LLVM_URL:?LLVM_URL is required}" +: "${LLVM_SHA256:?LLVM_SHA256 is required}" +: "${LLVM_BYTES:?LLVM_BYTES is required}" +: "${LLVM_VERSION:?LLVM_VERSION is required}" +: "${CACHE_KEY:?CACHE_KEY is required}" +: "${ACTION_PATH:?ACTION_PATH is required}" +: "${RUNNER_TEMP:?RUNNER_TEMP is required}" +: "${RUNNER_OS:?RUNNER_OS is required}" +: "${GITHUB_ENV:?GITHUB_ENV is required}" +: "${GITHUB_PATH:?GITHUB_PATH is required}" + +curl_platform_flags="$ACTION_PATH/../../../tools/dev/curl-platform-flags.sh" +if [ ! -f "$curl_platform_flags" ] || [ -L "$curl_platform_flags" ]; then + echo "Wasmer LLVM curl platform policy is missing: $curl_platform_flags" >&2 + exit 127 +fi +# shellcheck source=tools/dev/curl-platform-flags.sh +. "$curl_platform_flags" + +if [[ ! "$LLVM_URL" =~ ^https://[^[:space:]]+$ ]]; then + echo "Wasmer LLVM URL must be a single HTTPS URL" >&2 + exit 2 +fi +if [[ ! "$LLVM_VERSION" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then + echo "Wasmer LLVM version must contain only numeric components" >&2 + exit 2 +fi +if [[ ! "$LLVM_SHA256" =~ ^[[:xdigit:]]{64}$ ]]; then + echo "Wasmer LLVM SHA-256 must contain exactly 64 hexadecimal characters" >&2 + exit 2 +fi +if [[ ! "$LLVM_BYTES" =~ ^[1-9][0-9]*$ ]] || [ "$LLVM_BYTES" -gt 2147483648 ]; then + echo "Wasmer LLVM byte size must be an integer between 1 and 2147483648" >&2 + exit 2 +fi +if [[ ! "$CACHE_KEY" =~ ^wasmer-llvm-[A-Za-z0-9._-]+$ ]]; then + echo "Wasmer LLVM cache key contains unsupported characters" >&2 + exit 2 +fi +case "$RUNNER_OS" in + Linux|macOS|Windows) ;; + *) + echo "unsupported GitHub runner OS for Wasmer LLVM: $RUNNER_OS" >&2 + exit 2 + ;; +esac + +LLVM_SHA256="$(printf '%s' "$LLVM_SHA256" | tr 'A-F' 'a-f')" + +sha256_file() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + else + echo "Wasmer LLVM installation requires shasum or sha256sum" >&2 + return 127 + fi +} + +llvm_config_path() { + if [ "$RUNNER_OS" = "Windows" ]; then + printf '%s/bin/llvm-config.exe\n' "$1" + else + printf '%s/bin/llvm-config\n' "$1" + fi +} + +cache_identity() { + printf '%s\n' \ + 'schema=1' \ + "url=$LLVM_URL" \ + "sha256=$LLVM_SHA256" \ + "bytes=$LLVM_BYTES" \ + "version=$LLVM_VERSION" \ + "cache_key=$CACHE_KEY" +} + +validate_cache_identity() { + local root="$1" + local label="$2" + local identity_file="$root/.oliphaunt-wasmer-llvm" + local actual expected + if [ ! -f "$identity_file" ]; then + echo "$label lacks its pinned archive identity marker" >&2 + return 1 + fi + actual="$(< "$identity_file")" + expected="$(cache_identity)" + if [ "$actual" != "$expected" ]; then + echo "$label archive identity does not match the requested URL, digest, size, version, and cache key" >&2 + return 1 + fi +} + +validate_llvm_install() { + local root="$1" + local label="$2" + local llvm_config version targets + llvm_config="$(llvm_config_path "$root")" + if [ ! -x "$llvm_config" ]; then + echo "$label did not produce executable bin/$(basename "$llvm_config")" >&2 + return 1 + fi + if ! version="$("$llvm_config" --version)"; then + echo "$label llvm-config --version failed" >&2 + return 1 + fi + case "$version" in + "$LLVM_VERSION"|"$LLVM_VERSION".*) ;; + *) + echo "$label has LLVM version $version; expected $LLVM_VERSION.x" >&2 + return 1 + ;; + esac + if ! targets="$("$llvm_config" --targets-built)"; then + echo "$label llvm-config --targets-built failed" >&2 + return 1 + fi + case "$targets" in + *LoongArch*WebAssembly*|*WebAssembly*LoongArch*) ;; + *) + echo "$label lacks required LoongArch and WebAssembly targets: $targets" >&2 + return 1 + ;; + esac +} + +write_github_environment() { + local install_dir="$1" + local env_prefix path_entry + if [ "$RUNNER_OS" = "Windows" ]; then + env_prefix="$(cygpath -w "$install_dir")" + path_entry="$(cygpath -w "$install_dir/bin")" + else + env_prefix="$install_dir" + path_entry="$install_dir/bin" + fi + printf 'LLVM_PATH=%s\n' "$env_prefix" >> "$GITHUB_ENV" + printf 'LLVM_SYS_221_PREFIX=%s\n' "$env_prefix" >> "$GITHUB_ENV" + printf '%s\n' "$path_entry" >> "$GITHUB_PATH" +} + +runner_temp="$RUNNER_TEMP" +if [ "$RUNNER_OS" = "Windows" ]; then + if ! command -v cygpath >/dev/null 2>&1; then + echo "Wasmer LLVM installation on Windows requires cygpath" >&2 + exit 127 + fi + runner_temp="$(cygpath -u "$RUNNER_TEMP")" +fi + +cache_root="$runner_temp/wasmer-llvm/$CACHE_KEY" +install_dir="$cache_root/llvm" +if [ -d "$install_dir" ]; then + if validate_cache_identity "$install_dir" "cached Wasmer LLVM" \ + && validate_llvm_install "$install_dir" "cached Wasmer LLVM"; then + write_github_environment "$install_dir" + exit 0 + fi + echo "discarding invalid cached Wasmer LLVM installation" >&2 + rm -rf "$install_dir" +fi + +for command in curl tar mktemp python3; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "Wasmer LLVM installation requires $command" >&2 + exit 127 + fi +done +if [ ! -f "$ACTION_PATH/validate-archive.py" ]; then + echo "Wasmer LLVM archive validator is missing from $ACTION_PATH" >&2 + exit 127 +fi +if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then + echo "Wasmer LLVM installation requires shasum or sha256sum" >&2 + exit 127 +fi + +mkdir -p "$runner_temp" "$cache_root" +archive="$(mktemp "$runner_temp/wasmer-llvm-${LLVM_VERSION}.archive.XXXXXX")" +staging_dir="" +cleanup() { + if [ -n "$archive" ]; then + rm -f "$archive" || true + fi + if [ -n "$staging_dir" ]; then + rm -rf "$staging_dir" || true + fi +} +trap cleanup EXIT +trap 'exit 129' HUP +trap 'exit 130' INT +trap 'exit 143' TERM + +curl_args=( + --location + --fail + --show-error + --retry 4 + --retry-all-errors + --retry-delay 10 + --retry-max-time 3600 + --connect-timeout 30 + --max-time 1800 + --max-filesize "$LLVM_BYTES" + --proto '=https' + --proto-redir '=https' + --tlsv1.2 +) +curl_platform_tls_flag="$(oliphaunt_curl_platform_tls_flag)" +if [ -n "$curl_platform_tls_flag" ]; then + curl_args+=("$curl_platform_tls_flag") +fi +curl_args+=(--output "$archive" "$LLVM_URL") +curl "${curl_args[@]}" + +actual_bytes="$(wc -c < "$archive" | tr -d '[:space:]')" +if [ "$actual_bytes" != "$LLVM_BYTES" ]; then + echo "Wasmer LLVM archive byte-size mismatch: expected $LLVM_BYTES, got $actual_bytes" >&2 + exit 1 +fi +actual_sha256="$(sha256_file "$archive")" +actual_sha256="$(printf '%s' "$actual_sha256" | tr 'A-F' 'a-f')" +if [ "$actual_sha256" != "$LLVM_SHA256" ]; then + echo "Wasmer LLVM archive checksum mismatch" >&2 + echo " expected: $LLVM_SHA256" >&2 + echo " actual: $actual_sha256" >&2 + exit 1 +fi + +python3 "$ACTION_PATH/validate-archive.py" "$archive" "$LLVM_BYTES" + +staging_dir="$(mktemp -d "$cache_root/.llvm-stage.XXXXXX")" +if ! tar -xJf "$archive" -C "$staging_dir"; then + echo "Wasmer LLVM archive extraction failed" >&2 + exit 1 +fi +rm -f "$archive" +archive="" + +if ! validate_llvm_install "$staging_dir" "staged Wasmer LLVM"; then + exit 1 +fi +cache_identity > "$staging_dir/.oliphaunt-wasmer-llvm" + +rm -rf "$install_dir" +mv "$staging_dir" "$install_dir" +staging_dir="" +write_github_environment "$install_dir" diff --git a/.github/actions/setup-wasmer-llvm/validate-archive.py b/.github/actions/setup-wasmer-llvm/validate-archive.py new file mode 100644 index 00000000..ee9c07e0 --- /dev/null +++ b/.github/actions/setup-wasmer-llvm/validate-archive.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Fail-closed structural validation for a pinned Wasmer LLVM .tar.xz.""" + +from __future__ import annotations + +import argparse +import posixpath +import stat +import tarfile +import unicodedata +from pathlib import Path + + +MAX_MEMBERS = 500_000 +MAX_MEMBER_BYTES = 4 * 1024 * 1024 * 1024 +MAX_EXPANDED_BYTES = 12 * 1024 * 1024 * 1024 +MIN_EXPANDED_ALLOWANCE = 1024 * 1024 * 1024 +MAX_EXPANSION_RATIO = 20 +MAX_PATH_BYTES = 4096 +MAX_COMPONENT_BYTES = 255 +WINDOWS_RESERVED_NAMES = { + "aux", + "con", + "nul", + "prn", + *(f"com{number}" for number in range(1, 10)), + *(f"lpt{number}" for number in range(1, 10)), +} + + +class UnsafeArchive(ValueError): + pass + + +def normalized_path(value: str, label: str, *, allow_root: bool = False) -> str: + if not value or any(ord(character) < 32 or ord(character) == 127 for character in value): + raise UnsafeArchive(f"{label} is empty or contains control characters") + if "\\" in value or ":" in value or value.startswith("/"): + raise UnsafeArchive(f"{label} is absolute or non-portable: {value!r}") + while value.startswith("./"): + value = value[2:] + value = value.rstrip("/") + if value in {"", "."}: + if allow_root: + return "" + raise UnsafeArchive(f"{label} resolves to the archive root") + parts = value.split("/") + if any(part in {"", ".", ".."} for part in parts): + raise UnsafeArchive(f"{label} contains an empty, dot, or traversal component: {value!r}") + for part in parts: + encoded = part.encode("utf-8") + if len(encoded) > MAX_COMPONENT_BYTES: + raise UnsafeArchive(f"{label} contains a component longer than {MAX_COMPONENT_BYTES} UTF-8 bytes") + if part.endswith((" ", ".")): + raise UnsafeArchive(f"{label} contains a Windows-ambiguous trailing space or dot: {value!r}") + windows_stem = part.split(".", 1)[0].casefold() + if windows_stem in WINDOWS_RESERVED_NAMES: + raise UnsafeArchive(f"{label} contains reserved Windows name {part!r}") + normalized = "/".join(parts) + if len(normalized.encode("utf-8")) > MAX_PATH_BYTES: + raise UnsafeArchive(f"{label} is longer than {MAX_PATH_BYTES} UTF-8 bytes") + return normalized + + +def portable_key(value: str) -> str: + return unicodedata.normalize("NFC", value).casefold() + + +def link_target(member: tarfile.TarInfo, member_path: str) -> str: + target = member.linkname + if not target or "\\" in target or target.startswith("/") or (len(target) > 1 and target[1] == ":"): + raise UnsafeArchive(f"archive link {member.name!r} has an unsafe target {target!r}") + if any(ord(character) < 32 or ord(character) == 127 for character in target): + raise UnsafeArchive(f"archive link {member.name!r} has control characters in its target") + combined = posixpath.join(posixpath.dirname(member_path), target) if member.issym() else target + normalized = posixpath.normpath(combined) + if normalized in {"", ".", ".."} or normalized.startswith("../"): + raise UnsafeArchive(f"archive link {member.name!r} escapes the extraction root") + return normalized_path(normalized, f"archive link target for {member.name!r}") + + +def validate(archive: Path, expected_bytes: int) -> None: + if not archive.is_file() or archive.stat().st_size != expected_bytes: + actual = archive.stat().st_size if archive.exists() else "missing" + raise UnsafeArchive(f"archive byte size is {actual}; expected exactly {expected_bytes}") + expanded_limit = min( + MAX_EXPANDED_BYTES, + max(MIN_EXPANDED_ALLOWANCE, expected_bytes * MAX_EXPANSION_RATIO), + ) + members: dict[str, tarfile.TarInfo] = {} + portable_paths: dict[str, str] = {} + links: list[tuple[tarfile.TarInfo, str, str]] = [] + link_targets: dict[str, str] = {} + expanded_bytes = 0 + root_seen = False + try: + stream = tarfile.open(archive, mode="r:xz") + except (OSError, tarfile.TarError) as error: + raise UnsafeArchive(f"cannot open xz tar archive: {error}") from error + with stream: + try: + for index, member in enumerate(stream, start=1): + if index > MAX_MEMBERS: + raise UnsafeArchive(f"archive contains more than {MAX_MEMBERS} members") + path = normalized_path(member.name, f"archive member {member.name!r}", allow_root=True) + if path == "": + if not member.isdir(): + raise UnsafeArchive("the archive root entry must be a directory") + if root_seen: + raise UnsafeArchive("archive contains duplicate root directory entries") + root_seen = True + continue + if path in members: + raise UnsafeArchive(f"archive contains duplicate path {path!r}") + parts = path.split("/") + for depth in range(1, len(parts) + 1): + prefix = "/".join(parts[:depth]) + key = portable_key(prefix) + prior = portable_paths.get(key) + if prior is not None and prior != prefix: + raise UnsafeArchive( + f"archive contains Unicode/case-colliding paths {prior!r} and {prefix!r}" + ) + portable_paths[key] = prefix + if member.mode & (stat.S_ISUID | stat.S_ISGID): + raise UnsafeArchive(f"archive member {path!r} has set-id mode bits") + if not (member.isdir() or member.isreg() or member.issym() or member.islnk()): + raise UnsafeArchive(f"archive member {path!r} has unsupported type {member.type!r}") + if member.isreg(): + if member.size < 0 or member.size > MAX_MEMBER_BYTES: + raise UnsafeArchive(f"archive member {path!r} exceeds the per-file size limit") + expanded_bytes += member.size + if expanded_bytes > expanded_limit: + raise UnsafeArchive(f"archive expands beyond the {expanded_limit}-byte allowance") + members[path] = member + if member.issym() or member.islnk(): + target = link_target(member, path) + links.append((member, path, target)) + link_targets[path] = target + except (OSError, tarfile.TarError) as error: + raise UnsafeArchive(f"cannot read xz tar archive: {error}") from error + if not members: + raise UnsafeArchive("archive contains no installable members") + for path, member in members.items(): + parts = path.split("/") + for depth in range(1, len(parts)): + ancestor = members.get("/".join(parts[:depth])) + if ancestor is not None and not ancestor.isdir(): + raise UnsafeArchive(f"archive member {path!r} descends through a non-directory") + for member, path, target in links: + target_member = members.get(target) + if target_member is None: + raise UnsafeArchive(f"archive link {path!r} targets missing member {target!r}") + if member.islnk() and not target_member.isreg(): + raise UnsafeArchive(f"archive hard link {path!r} does not target a regular file") + for _, path, _ in links: + current = path + visited: set[str] = set() + while current in link_targets: + if current in visited: + raise UnsafeArchive(f"archive link {path!r} participates in a link cycle") + visited.add(current) + current = link_targets[current] + resolved = members.get(current) + if resolved is None or not (resolved.isreg() or resolved.isdir()): + raise UnsafeArchive(f"archive link {path!r} does not resolve to a regular file or directory") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("archive", type=Path) + parser.add_argument("expected_bytes", type=int) + arguments = parser.parse_args() + if arguments.expected_bytes < 1 or arguments.expected_bytes > 2 * 1024 * 1024 * 1024: + raise UnsafeArchive("expected archive bytes must be between 1 and 2 GiB") + validate(arguments.archive, arguments.expected_bytes) + + +if __name__ == "__main__": + try: + main() + except (OSError, UnsafeArchive, tarfile.TarError) as error: + raise SystemExit(f"unsafe Wasmer LLVM archive: {error}") from error diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 162d77a4..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,20 +0,0 @@ -version: 2 -updates: - - package-ecosystem: cargo - directory: / - schedule: - interval: weekly - groups: - cargo-patch: - update-types: [patch] - cargo-minor: - update-types: [minor] - cargo-major: - update-types: [major] - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - groups: - actions: - patterns: ["*"] diff --git a/.github/moon.yml b/.github/moon.yml new file mode 100644 index 00000000..5d7ffaac --- /dev/null +++ b/.github/moon.yml @@ -0,0 +1,57 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "ci-workflows" +language: "yaml" +layer: "configuration" +stack: "infrastructure" +tags: ["ci", "github-actions", "workflows"] + +project: + title: "GitHub Actions" + description: "GitHub workflow, action, and repository automation checks." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/core" + paths: + "**/*": ["@oliphaunt/core"] + +tasks: + check: + tags: ["policy", "assertion", "quality", "static", "ci-maintainer-tools"] + command: "bash tools/policy/check-workflows.sh" + inputs: + - "/.moon/toolchains.yml" + - "/.prototools" + - "/.github/actions/**/*" + - "/.github/scripts/**/*" + - "/.github/workflows/**/*" + - "/.github/zizmor.yml" + - "/src/sources/toolchains/**/*" + - "/tools/dev/bun.sh" + - "/tools/dev/curl-platform-flags.sh" + - "/tools/dev/extract-pinned-zip.sh" + - "/tools/dev/extract-pinned-zip.test.sh" + - "/tools/dev/install-pinned-js-runtime.sh" + - "/tools/dev/install-pinned-js-runtime.test.sh" + - "/tools/dev/install-pinned-winflexbison.sh" + - "/tools/dev/install-pinned-winflexbison.test.sh" + - "/tools/dev/setup-android-sdk.sh" + - "/tools/dev/setup-android-sdk.test.sh" + - "/tools/policy/check-workflows.sh" + - "/tools/release/install-verdaccio-runtime.sh" + - "/tools/release/install-verdaccio-runtime.test.sh" + - "/tools/release/toolchain-bootstrap.test.mjs" + - "/tools/release/verdaccio-runtime/package.json" + - "/tools/release/verdaccio-runtime/pnpm-lock.yaml" + options: + cache: true + runFromWorkspaceRoot: true + release-check: + tags: ["release", "package"] + command: "true" + deps: + - "ci-workflows:check" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 82e297ff..1c5842d8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,13 +4,13 @@ - [ ] Package/API/runtime change: PR title uses `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, or a breaking `!`. - [ ] Docs/CI/repository-only change: no release intended. -- [ ] Asset/source-spine change: source pins/fingerprints are current and the Assets workflow will generate/test release artifacts. +- [ ] Source/input/runtime asset change: source pins, generated metadata, and release metadata affectedness are current. ## Verification -- [ ] `scripts/validate.sh repo` -- [ ] `scripts/validate.sh artifacts` -- [ ] `scripts/validate.sh lint` -- [ ] `scripts/validate.sh test` -- [ ] `scripts/validate.sh package` when published package contents changed -- [ ] `cargo deny check` +- [ ] `pnpm doctor` +- [ ] `pnpm fmt:check` +- [ ] `pnpm check` +- [ ] `pnpm test` +- [ ] `pnpm release-check` +- [ ] Product-specific smoke/package/perf checks when product behavior or artifacts changed. diff --git a/.github/scripts/bootstrap-registry-credential-env.mjs b/.github/scripts/bootstrap-registry-credential-env.mjs new file mode 100644 index 00000000..6cec89e5 --- /dev/null +++ b/.github/scripts/bootstrap-registry-credential-env.mjs @@ -0,0 +1,30 @@ +const CARGO_CREDENTIAL_ENV = Object.freeze([ + "CARGO_REGISTRIES_CRATES_IO_TOKEN", + "CARGO_REGISTRY_TOKEN", + "CRATES_IO_BOOTSTRAP_TOKEN", + "CRATES_IO_TRUST_CONFIG_TOKEN", +]); + +const NPM_CREDENTIAL_ENV = Object.freeze([ + "NODE_AUTH_TOKEN", + "NPM_BOOTSTRAP_TOKEN", + "NPM_CONFIG__AUTH", + "NPM_CONFIG__AUTHTOKEN", + "NPM_CONFIG_USERCONFIG", + "NPM_TOKEN", +]); + +/** + * Give a bootstrap publisher only the credential family for its immutable + * registry lane. The parent orchestrator needs both families so it can run the + * lanes concurrently; a child publisher never does. + */ +export function bootstrapCarrierEnvironment(ecosystem, parentEnvironment = process.env) { + if (!new Set(["cargo", "npm"]).has(ecosystem)) { + throw new Error(`unsupported bootstrap credential ecosystem ${JSON.stringify(ecosystem)}`); + } + const environment = { ...parentEnvironment }; + const remove = ecosystem === "cargo" ? NPM_CREDENTIAL_ENV : CARGO_CREDENTIAL_ENV; + for (const name of remove) delete environment[name]; + return environment; +} diff --git a/.github/scripts/bootstrap-registry-identities.mjs b/.github/scripts/bootstrap-registry-identities.mjs new file mode 100644 index 00000000..a40328d5 --- /dev/null +++ b/.github/scripts/bootstrap-registry-identities.mjs @@ -0,0 +1,347 @@ +#!/usr/bin/env bun +import { spawn } from "node:child_process"; +import { + appendFileSync, + mkdirSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { bootstrapCarrierEnvironment } from "./bootstrap-registry-credential-env.mjs"; +import { bootstrapPublicationPlan } from "../../tools/release/bootstrap-publication-plan.mjs"; +import { executeBootstrapPublicationPlan } from "../../tools/release/bootstrap-publication-executor.mjs"; +import { + appendBootstrapCheckpoint, + loadBootstrapLedger, +} from "../../tools/release/bootstrap-ledger.mjs"; +import { reconcileBootstrapRegistryState } from "../../tools/release/bootstrap-registry-reconciliation.mjs"; +import { + assessCratesIoBootstrapCapacity, + assertCratesIoBootstrapCapacity, + cratesIoCapacitySummary, + inspectCratesIoVersionState, + parseRegistryMutationDeadline, + CRATES_IO_NEW_CRATE_REFILL_SECONDS, + REGISTRY_BOOTSTRAP_INTEGRITY_CONCURRENCY, +} from "../../tools/release/crates-io-bootstrap-capacity.mjs"; +import { + decodeRegistryPublicationDeferral, + REGISTRY_PUBLICATION_DEFERRAL_EXIT_CODE, +} from "../../tools/release/registry-publication-deferral.mjs"; +import { validateReleaseExecutionResult } from "../../tools/release/release-continuation-contract.mjs"; +import { inspectNpmVersionState } from "../../tools/release/frozen-npm-publish.mjs"; +import { loadPublicationLock } from "../../tools/release/publication-lock.mjs"; +import { verifyLockedRegistryIntegrity } from "../../tools/release/registry-integrity.mjs"; + +function fail(message) { + console.error(`bootstrap-registry-identities: ${message}`); + process.exit(1); +} + +function requiredEnv(name) { + const value = process.env[name]?.trim(); + if (!value) { + fail(`${name} is required`); + } + return value; +} + +const EXECUTION_RESULT_PATH = path.resolve( + process.env.OLIPHAUNT_BOOTSTRAP_EXECUTION_RESULT?.trim() + || "target/release/bootstrap-execution-result.json", +); +const CHILD_STDERR_TAIL_BYTES = 128 * 1024; + +function writeExecutionResult(value) { + const normalized = validateReleaseExecutionResult(value, { + operation: "publish-bootstrap", + releaseCommit: value.source.commit, + releaseTree: value.source.tree, + lock: value.lock, + products: value.products, + }); + mkdirSync(path.dirname(EXECUTION_RESULT_PATH), { recursive: true }); + const temporary = `${EXECUTION_RESULT_PATH}.tmp-${process.pid}`; + rmSync(temporary, { force: true }); + writeFileSync(temporary, `${JSON.stringify(normalized, null, 2)}\n`, { flag: "wx", mode: 0o600 }); + renameSync(temporary, EXECUTION_RESULT_PATH); + if (process.env.GITHUB_OUTPUT?.trim()) { + appendFileSync( + process.env.GITHUB_OUTPUT, + `complete=${normalized.decision === "complete" ? "true" : "false"}\n` + + `deferred=${normalized.decision === "deferred" ? "true" : "false"}\n` + + `deferral_mode=${normalized.deferralMode ?? ""}\n` + + `progress_count=${normalized.newlyCompletedIds.length}\n` + + `completed_count=${normalized.completedIds.length}\n` + + `remaining_count=${normalized.remainingIds.length}\n` + + `not_before_epoch=${normalized.notBeforeEpochSeconds ?? 0}\n`, + ); + } + return normalized; +} + +let products; +try { + products = JSON.parse(requiredEnv("PRODUCTS_JSON")); +} catch (error) { + fail(`invalid PRODUCTS_JSON: ${error.message}`); +} +if (!Array.isArray(products) || products.length === 0 || products.some((product) => typeof product !== "string")) { + fail("PRODUCTS_JSON must be a non-empty product string list"); +} + +const headRef = requiredEnv("RELEASE_HEAD_SHA"); +const publicationLock = requiredEnv("PUBLICATION_LOCK_PATH"); +const bootstrapLedger = requiredEnv("BOOTSTRAP_LEDGER_PATH"); +let lock; +let plan; +try { + lock = loadPublicationLock(publicationLock); + plan = bootstrapPublicationPlan(lock, products); + rmSync(EXECUTION_RESULT_PATH, { force: true }); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +// This read-only inventory and official token-bucket admission must complete +// before the genesis ledger is initialized and, critically, before npm or +// crates.io receives any publication request. Oversized first-release graphs +// are split into dependency-closed batches and resumed from immutable receipts. +let cargoInventory; +let npmInventory; +let capacityAssessment; +try { + const deadlineEpochSeconds = parseRegistryMutationDeadline( + requiredEnv("REGISTRY_MUTATION_DEADLINE_EPOCH"), + ); + [cargoInventory, npmInventory] = await Promise.all([ + inspectCratesIoVersionState({ plan, deadlineEpochSeconds }), + inspectNpmVersionState({ plan, deadlineEpochSeconds }), + ]); + capacityAssessment = assessCratesIoBootstrapCapacity({ + inventory: cargoInventory, + npmInventory, + bootstrapPlan: plan, + cargoSecondsPerCarrier: process.env.REGISTRY_BOOTSTRAP_CARGO_SECONDS_PER_CARRIER, + npmSecondsPerCarrier: process.env.REGISTRY_BOOTSTRAP_NPM_SECONDS_PER_CARRIER, + reconciliationSecondsPerCarrier: process.env.REGISTRY_BOOTSTRAP_RECONCILIATION_SECONDS_PER_CARRIER, + reserveSeconds: process.env.REGISTRY_BOOTSTRAP_RESERVE_SECONDS, + deadlineEpochSeconds, + }); + const summary = cratesIoCapacitySummary(capacityAssessment); + console.log(summary); + if (process.env.GITHUB_STEP_SUMMARY?.trim()) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`); + } + assertCratesIoBootstrapCapacity(capacityAssessment); + if (capacityAssessment.decision !== "execute") { + throw new Error( + `bounded bootstrap invocation cannot make dependency-closed progress before its deadline; ` + + `${capacityAssessment.remainingMutationCount} carrier(s) remain and continuation requires nonzero progress`, + ); + } +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +// Validate a restored immutable chain before using its receipts. Inventory is +// authoritative for current public visibility; a receipt whose exact version +// disappeared is a hard pre-mutation failure. Existing names lacking the +// locked exact version were already rejected by the admission check above. +let checkpoint; +let reconciliation; +let startingCompletedIds; +try { + checkpoint = loadBootstrapLedger(bootstrapLedger, lock, products, { allowEmpty: true }); + startingCompletedIds = new Set(checkpoint?.receipts.map(({ id }) => id) ?? []); + reconciliation = reconcileBootstrapRegistryState({ + plan, + cargoInventory, + npmInventory, + checkpoint, + }); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +// Prove every matching public version against the frozen bytes in one bounded, +// concurrent preflight. This both recovers publications accepted before an +// interrupted checkpoint and ensures public recovery skips cannot conceal an +// immutable checksum/SRI conflict. No registry mutation has happened yet. +let publicReceipts = []; +try { + if (reconciliation.publicCarrierIds.length > 0) { + publicReceipts = await verifyLockedRegistryIntegrity(lock, { + carrierIds: reconciliation.publicCarrierIds, + concurrency: REGISTRY_BOOTSTRAP_INTEGRITY_CONCURRENCY, + }); + } +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +// A genesis checkpoint is written only after every pre-mutation conflict and +// public-byte proof has passed. Every later file is append-only and +// content-addressed, so `if: always()` can upload a useful resume chain. +try { + if (checkpoint === null) { + checkpoint = appendBootstrapCheckpoint(bootstrapLedger, lock, products, []); + } + if (publicReceipts.length > 0) { + checkpoint = appendBootstrapCheckpoint(bootstrapLedger, lock, products, publicReceipts); + } +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +// Only completely absent names reach a publisher subprocess. Cargo and npm +// each retain one sequential mutation lane, while independent lanes overlap +// and explicit cross-registry dependency edges remain barriers. Node's async +// spawn is required here: spawnSync would silently serialize both lanes. +function publishCarrier(carrier) { + console.log(`reconciling pending ${carrier.ecosystem} identity ${carrier.id} (${carrier.product})`); + return new Promise((resolve, reject) => { + let stderrTail = Buffer.alloc(0); + let settled = false; + const child = spawn( + process.execPath, + [ + "tools/release/release-publish.mjs", + "publish", + "--bootstrap-identities", + "--carrier-id", + carrier.id, + "--head-ref", + headRef, + "--publication-lock", + publicationLock, + "--bootstrap-ledger", + bootstrapLedger, + ], + { + stdio: ["inherit", "inherit", "pipe"], + env: bootstrapCarrierEnvironment(carrier.ecosystem, process.env), + }, + ); + child.stderr.on("data", (chunk) => { + process.stderr.write(chunk); + stderrTail = Buffer.concat([stderrTail, Buffer.from(chunk)]).subarray(-CHILD_STDERR_TAIL_BYTES); + }); + child.once("error", (cause) => { + if (!settled) { + settled = true; + reject(cause); + } + }); + child.once("close", (code, signal) => { + if (settled) return; + settled = true; + if (code === 0) { + resolve(); + } else if (signal === null && code === REGISTRY_PUBLICATION_DEFERRAL_EXIT_CODE) { + try { + reject(decodeRegistryPublicationDeferral(stderrTail.toString("utf8"))); + } catch (cause) { + reject(cause); + } + } else { + reject(new Error(`${carrier.id} failed with ${signal === null ? `status ${String(code)}` : `signal ${signal}`}`)); + } + }); + }); +} + +let execution; +try { + const admitted = new Set(capacityAssessment.admittedCarrierIds); + const admittedPlan = reconciliation.missingCarriers.filter(({ id }) => admitted.has(id)); + if (admittedPlan.length !== admitted.size) { + throw new Error("token-bucket admission contains a carrier outside the exact missing-identity inventory"); + } + execution = await executeBootstrapPublicationPlan({ + plan: admittedPlan, + satisfiedCarrierIds: reconciliation.publicCarrierIds, + publishCarrier, + checkpointCarrierIds: async (carrierIds) => { + const receipts = await verifyLockedRegistryIntegrity(lock, { + carrierIds, + concurrency: REGISTRY_BOOTSTRAP_INTEGRITY_CONCURRENCY, + }); + checkpoint = appendBootstrapCheckpoint(bootstrapLedger, lock, products, receipts); + }, + }); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} + +let result; +try { + checkpoint = loadBootstrapLedger(bootstrapLedger, lock, products, { allowEmpty: true }); + const completedSet = new Set(checkpoint?.receipts.map(({ id }) => id) ?? []); + const completedIds = plan.filter(({ id }) => completedSet.has(id)).map(({ id }) => id); + if (completedIds.length !== completedSet.size) { + throw new Error("bootstrap ledger contains a receipt outside the exact canonical plan"); + } + const remainingIds = plan.filter(({ id }) => !completedSet.has(id)).map(({ id }) => id); + const newlyCompletedIds = completedIds.filter((id) => !startingCompletedIds.has(id)); + const decision = remainingIds.length === 0 ? "complete" : "deferred"; + if (decision === "complete") { + checkpoint = loadBootstrapLedger(bootstrapLedger, lock, products, { requireComplete: true }); + } + const remainingHasCargo = plan.some(({ id, ecosystem }) => ecosystem === "cargo" && remainingIds.includes(id)); + const notBeforeEpochSeconds = decision === "complete" + ? null + : execution.notBeforeEpochSeconds + ?? Math.floor(Date.now() / 1000) + (remainingHasCargo ? CRATES_IO_NEW_CRATE_REFILL_SECONDS : 1); + const deferralMode = decision === "complete" + ? null + : newlyCompletedIds.length > 0 + ? "progress" + : execution.deferReason === "rate-limit" + ? "rate-limit" + : execution.deferReason === "deadline" + ? "pre-mutation-deadline" + : (() => { + throw new Error( + `zero-progress bootstrap deferral requires an explicit rate-limit or deadline reason; got ` + + `${execution.deferReason ?? "none"}`, + ); + })(); + result = writeExecutionResult({ + schema: "oliphaunt-bootstrap-execution-result-v1", + operation: "publish-bootstrap", + decision, + deferralMode, + source: { commit: lock.source.commit, tree: lock.source.tree }, + lock: { + lockDigest: lock.lockDigest, + catalogDigest: lock.catalogDigest, + packageEnvelopeDigest: lock.packageEnvelopeDigest, + }, + products: [...products].sort(), + admittedIds: plan + .filter(({ id }) => capacityAssessment.admittedCarrierIds.includes(id)) + .map(({ id }) => id), + completedIds, + newlyCompletedIds, + remainingIds, + notBeforeEpochSeconds, + }); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} +if (result.decision === "complete") { + console.log( + `completed ${execution.completedCarrierIds.length} dependency-ordered first-version Cargo/npm bootstrap mutation(s), ` + + `reconciled ${reconciliation.publicCarrierIds.length} lock-matching public version(s), ` + + `and sealed immutable checkpoint ${checkpoint.sequence} (${checkpoint.receipts.length}/${checkpoint.publications.length})`, + ); +} else { + console.log( + `checkpointed ${result.newlyCompletedIds.length} new exact bootstrap receipt(s); ` + + `${result.remainingIds.length} carrier(s) remain for a continuation no earlier than ${result.notBeforeEpochSeconds}`, + ); +} diff --git a/.github/scripts/check-ci-gate.mjs b/.github/scripts/check-ci-gate.mjs new file mode 100644 index 00000000..f99c14a3 --- /dev/null +++ b/.github/scripts/check-ci-gate.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env bun +import { env, exit } from 'node:process'; + +function fail(message) { + console.error(message); + exit(1); +} + +function parseJsonEnv(name, fallback) { + const raw = env[name]; + if (!raw) { + return fallback; + } + try { + return JSON.parse(raw); + } catch (error) { + fail(`invalid ${name} JSON: ${error.message}`); + } +} + +function selectedJobs() { + const jobs = parseJsonEnv('SELECTED_JOBS_JSON', []); + if (!Array.isArray(jobs) || jobs.some((job) => typeof job !== 'string')) { + fail('SELECTED_JOBS_JSON must be a JSON string array'); + } + return [...new Set(jobs)].sort(); +} + +function requiredJobs() { + const fromJson = parseJsonEnv('REQUIRED_JOBS_JSON', null); + if (fromJson !== null) { + if (!Array.isArray(fromJson) || fromJson.some((job) => typeof job !== 'string')) { + fail('REQUIRED_JOBS_JSON must be a JSON string array'); + } + return [...new Set(fromJson)].sort(); + } + return (env.REQUIRED_JOBS ?? '') + .split(',') + .map((job) => job.trim()) + .filter(Boolean) + .sort(); +} + +function needs() { + const parsed = parseJsonEnv('NEEDS_JSON', {}); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + fail('NEEDS_JSON must be a JSON object'); + } + return parsed; +} + +function resultFor(needsByJob, job) { + const result = needsByJob[job]?.result; + return typeof result === 'string' && result.length > 0 ? result : 'missing'; +} + +function checkJobs(jobs, { label }) { + const needsByJob = needs(); + const failures = jobs + .map((job) => [job, resultFor(needsByJob, job)]) + .filter(([, result]) => result !== 'success') + .map(([job, result]) => `${job}=${result}`); + + if (failures.length > 0) { + fail(`${label} failures: ${failures.join(', ')}`); + } + + console.log(`${label} passed: ${jobs.length > 0 ? jobs.join(', ') : '(none selected)'}`); +} + +const mode = process.argv[2] ?? ''; +switch (mode) { + case 'selected': + checkJobs(selectedJobs(), { label: env.GATE_LABEL || 'selected jobs' }); + break; + case 'required': + checkJobs(requiredJobs(), { label: env.GATE_LABEL || 'required jobs' }); + break; + default: + fail('usage: check-ci-gate.mjs [selected|required]'); +} diff --git a/.github/scripts/check-crates-io-publish-capacity.mjs b/.github/scripts/check-crates-io-publish-capacity.mjs new file mode 100644 index 00000000..0e707f77 --- /dev/null +++ b/.github/scripts/check-crates-io-publish-capacity.mjs @@ -0,0 +1,155 @@ +#!/usr/bin/env bun +import { appendFileSync, rmSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { + assessCratesIoVersionCapacity, + assertCratesIoVersionCapacity, + cratesIoVersionCapacitySummary, + inspectCratesIoVersionState, + parseRegistryMutationDeadline, +} from "../../tools/release/crates-io-bootstrap-capacity.mjs"; +import { loadBootstrapLedger } from "../../tools/release/bootstrap-ledger.mjs"; +import { inspectNpmVersionState } from "../../tools/release/frozen-npm-publish.mjs"; +import { + DEFAULT_NORMAL_PUBLICATION_ADMISSION, + writeNormalPublicationAdmission, +} from "../../tools/release/normal-publication-admission.mjs"; +import { + DEFAULT_NORMAL_PUBLICATION_CHECKPOINT, + openNormalPublicationCheckpoint, +} from "../../tools/release/normal-publication-checkpoint.mjs"; +import { normalPublicationPlan } from "../../tools/release/normal-publication-plan.mjs"; +import { loadPublicationLock, lockedCarriers } from "../../tools/release/publication-lock.mjs"; + +function fail(message) { + console.error(`check-crates-io-publish-capacity: ${message}`); + process.exit(1); +} + +function requiredEnv(name) { + const value = process.env[name]?.trim(); + if (!value) fail(`${name} is required`); + return value; +} + +function positiveIntegerEnv(name) { + const raw = requiredEnv(name); + if (!/^[1-9][0-9]*$/u.test(raw) || !Number.isSafeInteger(Number(raw))) { + throw new Error(`${name} must be a positive safe integer`); + } + return Number(raw); +} + +let products; +try { + products = JSON.parse(requiredEnv("PRODUCTS_JSON")); +} catch (error) { + fail(`invalid PRODUCTS_JSON: ${error.message}`); +} +if (!Array.isArray(products) || products.length === 0 || products.some((product) => typeof product !== "string")) { + fail("PRODUCTS_JSON must be a non-empty product string list"); +} + +try { + const deadlineEpochSeconds = parseRegistryMutationDeadline( + requiredEnv("REGISTRY_MUTATION_DEADLINE_EPOCH"), + ); + const lock = loadPublicationLock(requiredEnv("PUBLICATION_LOCK_PATH")); + const normalPlan = normalPublicationPlan(lock, products); + const cargoCarrierIds = new Set(normalPlan.operations + .filter(({ ecosystem }) => ecosystem === "cargo") + .map(({ carrierId }) => carrierId)); + const npmCarrierIds = new Set(normalPlan.operations + .filter(({ ecosystem }) => ecosystem === "npm") + .map(({ carrierId }) => carrierId)); + const carriers = lockedCarriers(lock); + const cargoPlan = carriers.filter(({ id }) => cargoCarrierIds.has(id)); + const npmPlan = carriers.filter(({ id }) => npmCarrierIds.has(id)); + if (cargoPlan.length !== cargoCarrierIds.size) { + throw new Error("normal publication plan and exact-lock Cargo carriers disagree"); + } + if (npmPlan.length !== npmCarrierIds.size) { + throw new Error("normal publication plan and exact-lock npm carriers disagree"); + } + const [inventory, npmInventory] = await Promise.all([ + inspectCratesIoVersionState({ plan: cargoPlan, deadlineEpochSeconds }), + inspectNpmVersionState({ plan: npmPlan, deadlineEpochSeconds }), + ]); + const ledger = loadBootstrapLedger( + process.env.BOOTSTRAP_LEDGER_PATH?.trim() || "target/release/bootstrap-ledger", + lock, + products, + { allowEmpty: true, requireComplete: true }, + ); + const receiptIds = new Set((ledger?.receipts ?? []).map(({ id }) => id)); + const checkpoint = openNormalPublicationCheckpoint({ + file: process.env.NORMAL_PUBLICATION_CHECKPOINT_PATH?.trim() + || DEFAULT_NORMAL_PUBLICATION_CHECKPOINT, + initialReceipts: ledger?.receipts ?? [], + lock, + plan: normalPlan, + products, + }); + const authoritativeWindowSeconds = positiveIntegerEnv( + "NORMAL_REGISTRY_MUTATION_WINDOW_SECONDS", + ); + const assessment = assessCratesIoVersionCapacity({ + inventory, + npmInventory, + normalPlan, + reconciledCarrierIds: [...receiptIds], + completedOperationIds: checkpoint.checkpoint.completedOperations, + authoritativeWindowSeconds, + deadlineEpochSeconds, + }); + const summary = cratesIoVersionCapacitySummary(assessment); + console.log(summary); + if (process.env.GITHUB_STEP_SUMMARY?.trim()) { + appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`); + } + assertCratesIoVersionCapacity(assessment); + const admissionFile = path.resolve( + process.env.NORMAL_PUBLICATION_ADMISSION_PATH?.trim() + || DEFAULT_NORMAL_PUBLICATION_ADMISSION, + ); + rmSync(admissionFile, { force: true }); + const admission = assessment.decision === "execute" + ? writeNormalPublicationAdmission(admissionFile, { + assessment, + checkpoint: checkpoint.checkpoint, + lock, + plan: normalPlan, + products, + }) + : null; + if ( + assessment.decision === "defer" + && (!assessment.namesSatisfied + || !Number.isSafeInteger(assessment.notBeforeEpochSeconds) + || assessment.notBeforeEpochSeconds < 1) + ) { + throw new Error( + "pre-mutation capacity deferral requires existing identities and a positive authoritative not-before time", + ); + } + if (process.env.GITHUB_OUTPUT?.trim()) { + appendFileSync( + process.env.GITHUB_OUTPUT, + `admission=${assessment.decision}\n` + + `deferred=${assessment.decision === "defer" ? "true" : "false"}\n` + + `names_satisfied=${assessment.namesSatisfied ? "true" : "false"}\n` + + `not_before_epoch=${assessment.notBeforeEpochSeconds ?? 0}\n` + + `required_window_seconds=${assessment.minimumMutationWindowSeconds}\n` + + `authoritative_window_seconds=${authoritativeWindowSeconds}\n` + + `admitted_operation_ids_json=${JSON.stringify(assessment.admittedOperationIds)}\n` + + `unadmitted_operation_ids_json=${JSON.stringify(assessment.unadmittedOperationIds)}\n` + + `complete_after_admission=${assessment.publicationCompleteAfterAdmission ? "true" : "false"}\n` + + `admission_file=${admission === null ? "" : admissionFile}\n` + + `admission_digest=${admission?.admissionDigest ?? ""}\n`, + ); + } +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} diff --git a/.github/scripts/check-release-changelog.sh b/.github/scripts/check-release-changelog.sh deleted file mode 100755 index 33b9a50b..00000000 --- a/.github/scripts/check-release-changelog.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$root" - -package_version="$( - awk ' - /^\[package\][[:space:]]*$/ { - in_package = 1 - next - } - /^\[/ && in_package { - exit - } - in_package && $0 ~ /^[[:space:]]*version[[:space:]]*=/ { - line = $0 - sub(/^[^=]*=[[:space:]]*"/, "", line) - sub(/".*$/, "", line) - print line - exit - } - ' Cargo.toml -)" - -if [[ -z "${package_version}" ]]; then - echo "could not read package version from Cargo.toml" >&2 - exit 1 -fi - -is_version_heading_awk=' - function is_version_heading(line) { - return index(line, "## [" version "] - ") == 1 || - (index(line, "## [" version "](") == 1 && - line ~ /\) - [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) - } -' - -top_release_heading="$( - awk ' - /^## \[Unreleased\]/ { - seen_unreleased = 1 - next - } - seen_unreleased && /^## \[/ { - print - exit - } - ' CHANGELOG.md -)" - -if [[ -z "${top_release_heading}" ]]; then - echo "CHANGELOG.md does not contain a release section after [Unreleased]" >&2 - exit 1 -fi - -if ! awk -v version="${package_version}" -v heading="${top_release_heading}" "${is_version_heading_awk}"' - BEGIN { - exit is_version_heading(heading) ? 0 : 1 - } -'; then - cat >&2 <&2 <&2 <&2 exit 1 fi -release_pattern='^((feat|fix|perf|refactor|revert)(\([a-z0-9][a-z0-9._/-]*\))?(!)?|[a-z]+(\([a-z0-9][a-z0-9._/-]*\))?!): .+' -release_pr_pattern='^chore\(release\): .+' +if ! git rev-parse --verify "${head_ref}^{commit}" >/dev/null 2>&1; then + echo "could not resolve release-intent head ref: ${head_ref}" >&2 + exit 1 +fi + +# The final authorized protected-main rewrite reports the already-qualified +# introduction tip as `github.event.before`. This is intentionally distinct +# from the older displaced-main release-metadata baseline. Its immutable +# before/ref/event tuple and the exact unreleased introduction shape make this +# exception non-replayable. Every other non-fast-forward comparison remains +# strict. +if ! git rev-parse --verify "${base_ref}^{commit}" >/dev/null 2>&1 || + ! git merge-base --is-ancestor "${base_ref}^{commit}" "${head_ref}^{commit}"; then + if ! repair_contract="$( + bun -e ' +import { + RELEASE_PLEASE_BOOTSTRAP_SHA, + RELEASE_PLEASE_HISTORY_REPAIR_BEFORE_SHA, + RELEASE_PLEASE_INTRODUCTION_SUBJECT, +} from "./tools/release/release-please-bootstrap.mjs"; +console.log([ + RELEASE_PLEASE_BOOTSTRAP_SHA, + RELEASE_PLEASE_HISTORY_REPAIR_BEFORE_SHA, + RELEASE_PLEASE_INTRODUCTION_SUBJECT, +].join("\t")); +' + )"; then + echo "could not load the protected-main history-repair contract" >&2 + exit 1 + fi + IFS=$'\t' read -r canonical_bootstrap_sha repair_before_sha introduction_subject <<< "${repair_contract}" + + rewrite_parents="$(git rev-list --parents -n 1 "${head_ref}^{commit}")" + read -r -a rewrite_commit_and_parents <<< "${rewrite_parents}" + if [[ "${#rewrite_commit_and_parents[@]}" -ne 2 ]]; then + echo "protected-main history repair requires an exact one-parent introduction commit" >&2 + exit 1 + fi + rewrite_parent="${rewrite_commit_and_parents[1]}" + + candidate_bootstrap_sha="$( + git show "${head_ref}:release-please-config.json" | + bun -e ' +const config = JSON.parse(await Bun.stdin.text()); +const value = config?.["bootstrap-sha"]; +if (typeof value === "string") process.stdout.write(value); +' + )" + candidate_manifest_unreleased="$( + git show "${head_ref}:.release-please-manifest.json" | + bun -e ' +const manifest = JSON.parse(await Bun.stdin.text()); +const versions = manifest && !Array.isArray(manifest) && typeof manifest === "object" + ? Object.values(manifest) + : []; +process.stdout.write(String(versions.length > 0 && versions.every((version) => version === "0.0.0"))); +' + )" -affected_files=() + if [[ "${event_name}" != "push" ]] || + [[ "${full_ref}" != "refs/heads/main" ]] || + [[ "${head_branch}" != "main" ]] || + [[ "${base_ref}" != "${repair_before_sha}" ]] || + [[ "${subject}" != "${introduction_subject}" ]] || + [[ "${rewrite_parent}" != "${canonical_bootstrap_sha}" ]] || + [[ "${candidate_bootstrap_sha}" != "${canonical_bootstrap_sha}" ]] || + [[ "${candidate_manifest_unreleased}" != "true" ]]; then + echo "release-intent base ${base_ref} is not an ancestor of ${head_ref}" >&2 + echo "non-fast-forward main updates are allowed only for the exact final introduction repair" >&2 + exit 1 + fi + echo "authorized final main history repair; comparing ${head_ref} to its exact introduction parent" >&2 + base_ref="${head_ref}^{commit}^" +fi + +release_types="$({ + git show "${head_ref}:release-please-config.json" | + bun -e ' +const config = JSON.parse(await Bun.stdin.text()); +const sections = config["changelog-sections"]; +if (!Array.isArray(sections) || sections.length === 0) { + console.error("release-please-config.json must define changelog-sections"); + process.exit(1); +} +const types = [...new Set(sections.map((section) => section?.type))]; +if (types.some((type) => typeof type !== "string" || !/^[a-z][a-z0-9-]*$/.test(type))) { + console.error("release-please changelog section types must be conventional lowercase identifiers"); + process.exit(1); +} +console.log(types.join("|")); +' +})" +if [[ -z "${release_types}" ]]; then + echo "could not derive release-impact types from release-please-config.json" >&2 + exit 1 +fi +release_pattern="^((${release_types})(\\([a-z0-9][a-z0-9._/-]*\\))?(!)?|[a-z]+(\\([a-z0-9][a-z0-9._/-]*\\))?!): .+" +release_pr_pattern='^chore\(release\): .+' is_release_pr=false -if [[ "${subject}" =~ ${release_pr_pattern} && "${head_branch}" == release-plz-* ]]; then - is_release_pr=true +is_generated_release_branch=false +if [[ "${head_branch}" == "release-please--branches--main" ]]; then + is_generated_release_branch=true +fi +if [[ "${subject}" =~ ${release_pr_pattern} ]]; then + if [[ "${is_generated_release_branch}" == true || "${head_branch}" == "main" ]]; then + is_release_pr=true + fi fi package_versions_from_ref() { @@ -27,7 +129,7 @@ package_versions_from_ref() { files="$( git ls-tree -r --name-only "${ref}" | - grep -E '(^Cargo.toml$|^crates/.*/Cargo.toml$)' || true + grep -E '(^Cargo.toml$|^src/.*/Cargo.toml$|^tools/xtask/Cargo.toml$)' || true )" while IFS= read -r file; do @@ -61,9 +163,33 @@ package_versions_from_ref() { base_versions="$(package_versions_from_ref "${base_ref}")" head_versions="$(package_versions_from_ref "${head_ref}")" +release_manifest_versions_from_ref() { + local ref="${1:?release_manifest_versions_from_ref requires a git ref}" + local manifest + if ! manifest="$(git show "${ref}:.release-please-manifest.json" 2>/dev/null)"; then + return 0 + fi + # shellcheck disable=SC2016 + printf '%s\n' "${manifest}" | + bun -e ' +let data; +try { + data = JSON.parse(await Bun.stdin.text()); +} catch { + process.exit(0); +} +for (const [path, version] of Object.entries(data).sort(([left], [right]) => + left < right ? -1 : left > right ? 1 : 0)) { + console.log(`${path}=${version}`); +} +' +} -if [[ -z "${base_versions}" || -z "${head_versions}" ]]; then - echo "could not read package versions from Cargo.toml files" >&2 +base_release_manifest_versions="$(release_manifest_versions_from_ref "${base_ref}")" +head_release_manifest_versions="$(release_manifest_versions_from_ref "${head_ref}")" + +if [[ -z "${base_versions}" || -z "${head_versions}" || -z "${head_release_manifest_versions}" ]]; then + echo "could not read package versions or release-please manifest versions" >&2 exit 1 fi @@ -73,17 +199,28 @@ changed_existing_versions="$( <(printf '%s\n' "${head_versions}" | sed 's/=/\t/' | sort -t $'\t' -k1,1) | awk -F '\t' '$2 != $3 { print $1 "=" $2 " -> " $3 }' )" - -if [[ -n "${changed_existing_versions}" && "${is_release_pr}" != true ]]; then +if [[ -n "${base_release_manifest_versions}" ]]; then + changed_existing_release_manifest_versions="$( + join -t $'\t' \ + <(printf '%s\n' "${base_release_manifest_versions}" | sed 's/=/\t/' | sort -t $'\t' -k1,1) \ + <(printf '%s\n' "${head_release_manifest_versions}" | sed 's/=/\t/' | sort -t $'\t' -k1,1) | + awk -F '\t' '$2 != $3 { print $1 "=" $2 " -> " $3 }' + )" +else + changed_existing_release_manifest_versions="" +fi +if [[ -n "${changed_existing_versions}${changed_existing_release_manifest_versions}" && "${is_release_pr}" != true ]]; then cat >&2 <&2 + echo "release PRs must contain one normalized release-bump commit directly on the current base" >&2 + exit 1 + fi + release_products_json="$( + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --derive-products \ + --head-ref "${head_ref}" + )" + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --products-json "${release_products_json}" \ + --head-ref "${head_ref}" +fi - case "${file}" in - Cargo.toml | build.rs | src/* | crates/*) - affected_files+=("${file}") - ;; - esac -done < <(git diff --name-only "${base_ref}...${head_ref}" --) +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}" +)" -if (( ${#affected_files[@]} == 0 )); then +if [[ -z "${release_products}" ]]; then exit 0 fi @@ -123,27 +282,29 @@ if [[ "${is_release_pr}" == true ]]; then fi cat >&2 <&2 +printf '%s\n' "${release_products}" | sed 's/^/ /' >&2 exit 1 diff --git a/.github/scripts/dispatch-release-continuation.mjs b/.github/scripts/dispatch-release-continuation.mjs new file mode 100644 index 00000000..02bf5139 --- /dev/null +++ b/.github/scripts/dispatch-release-continuation.mjs @@ -0,0 +1,332 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import { + createReleaseContinuationPointer, + normalizeArtifactIdentity, + RELEASE_CONTINUATION_MAX_DISPATCH_DELAY_SECONDS, + sha256Bytes, + validateReleaseContinuationContract, +} from "../../tools/release/release-continuation-contract.mjs"; +import { + continuationAuthorizationArtifactName, + createReleaseContinuationAuthorization, + serializeReleaseContinuationAuthorization, +} from "../../tools/release/release-continuation-authorization.mjs"; +import { openContinuationEnvelope } from "./release-continuation-artifact.mjs"; +import { + retryReadOperationSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; +import { + RELEASE_CONTINUATION_ARTIFACT_DOWNLOAD_DEADLINE_MS, + RELEASE_CONTINUATION_DISPATCH_REQUEST_DEADLINE_MS, + RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, +} from "../../tools/release/release-continuation-read-budget.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024; +export const MAX_CONTINUATION_DISPATCH_DELAY_SECONDS = + RELEASE_CONTINUATION_MAX_DISPATCH_DELAY_SECONDS; +export const CONTINUATION_DISPATCH_REQUEST_TIMEOUT_MS = + RELEASE_CONTINUATION_DISPATCH_REQUEST_DEADLINE_MS; + +function error(message) { + return new Error(`dispatch-release-continuation: ${message}`); +} + +function required(name, environment = process.env) { + const value = environment[name]?.trim(); + if (!value) throw error(`${name} is required`); + return value; +} + +function positiveInteger(value, context) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered) || !Number.isSafeInteger(Number(rendered))) { + throw error(`${context} must be a positive safe integer`); + } + return Number(rendered); +} + +function json(raw, context) { + try { return JSON.parse(raw); } catch (cause) { throw error(`${context} must be strict JSON: ${cause.message}`); } +} + +function ghJson(repo, endpoint, label) { + return json(runGitHubReadSync( + ["api", "-H", "X-GitHub-Api-Version: 2022-11-28", `repos/${repo}/${endpoint}`], + { + cwd: ROOT, + deadlineMs: RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, + label, + maxBuffer: 4 * 1024 * 1024, + }, + ), label); +} + +function exactArtifactBytes(repo, artifact) { + return retryReadOperationSync( + `download sealed continuation artifact ${artifact.id}`, + ({ attemptTimeoutMs }) => runGitHubReadSync( + [ + "api", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + `repos/${repo}/actions/artifacts/${artifact.id}/zip`, + ], + { + binary: true, + cwd: ROOT, + label: `download sealed continuation artifact ${artifact.id}`, + maxBuffer: MAX_ARTIFACT_BYTES, + attemptTimeoutMs, + deadlineMs: attemptTimeoutMs, + maxAttempts: 1, + baseDelayMs: 0, + maxDelayMs: 0, + }, + ), + { + attemptTimeoutMs: 5 * 60_000, + deadlineMs: RELEASE_CONTINUATION_ARTIFACT_DOWNLOAD_DEADLINE_MS, + maxAttempts: 4, + }, + ); +} + +function command(commandName, args, options = {}) { + const result = spawnSync(commandName, args, { + cwd: ROOT, + encoding: "utf8", + maxBuffer: MAX_ARTIFACT_BYTES, + stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"], + input: options.input, + env: process.env, + timeout: options.timeoutMs, + }); + if (result.error !== undefined || result.status !== 0) { + throw error( + `${commandName} ${args.join(" ")} failed: ` + + `${(result.stderr || result.stdout || result.error?.message || "").trim()}`, + ); + } + return result.stdout; +} + +function contractFromZip(bytes) { + if (!Buffer.isBuffer(bytes) || bytes.length < 4 || bytes[0] !== 0x50 || bytes[1] !== 0x4b) { + throw error("sealed continuation artifact did not return a ZIP archive"); + } + const directory = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-continuation-dispatch-")); + try { + const archive = path.join(directory, "artifact.zip"); + writeFileSync(archive, bytes, { flag: "wx", mode: 0o600 }); + const members = command("unzip", ["-Z1", archive]).split(/\r?\n/u).filter(Boolean); + if (members.filter((member) => member === "release-continuation-contract.json").length !== 1) { + throw error("sealed continuation artifact must contain one root continuation contract"); + } + return json(command("unzip", ["-p", archive, "release-continuation-contract.json"]), "continuation contract"); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +export function continuationDelaySeconds(notBeforeEpochSeconds, nowEpochSeconds) { + if (!Number.isSafeInteger(notBeforeEpochSeconds) || notBeforeEpochSeconds < 1) { + throw error("continuation not-before time must be a positive Unix timestamp"); + } + if (!Number.isSafeInteger(nowEpochSeconds) || nowEpochSeconds < 1) { + throw error("current time must be a positive Unix timestamp"); + } + const delay = Math.max(0, notBeforeEpochSeconds - nowEpochSeconds); + if (delay > MAX_CONTINUATION_DISPATCH_DELAY_SECONDS) { + throw error( + `continuation requests a ${delay}s delay; maximum automatic dispatch delay is ` + + `${MAX_CONTINUATION_DISPATCH_DELAY_SECONDS}s`, + ); + } + return delay; +} + +export function validateDispatchResponse(value, { repo, parentRunId }) { + if (value === null || Array.isArray(value) || typeof value !== "object") { + throw error("workflow dispatch response must be an object"); + } + const childRunId = positiveInteger(value.workflow_run_id, "workflow dispatch child run id"); + if (childRunId === parentRunId) throw error("workflow dispatch returned the parent run id as its child"); + const api = `https://api.github.com/repos/${repo}/actions/runs/${childRunId}`; + const html = `https://github.com/${repo}/actions/runs/${childRunId}`; + if (value.run_url !== api || value.html_url !== html) { + throw error("workflow dispatch response URLs do not bind the returned child run id and repository"); + } + return { childRunId, htmlUrl: html, runUrl: api }; +} + +export function parseDispatchResponse(raw) { + if (typeof raw !== "string") { + throw error("workflow dispatch response must include an HTTP status and JSON body"); + } + const boundary = /\r?\n\r?\n/u.exec(raw); + if (boundary === null || boundary.index === 0) { + throw error("workflow dispatch response did not include one HTTP header block"); + } + const statusLine = raw.slice(0, boundary.index).split(/\r?\n/u)[0] ?? ""; + if (!/^HTTP\/(?:1[.][01]|2(?:[.]0)?|3(?:[.]0)?) 200(?:\s|$)/u.test(statusLine)) { + throw error(`workflow dispatch response must be HTTP 200; got ${JSON.stringify(statusLine)}`); + } + const body = raw.slice(boundary.index + boundary[0].length); + return json(body, "workflow dispatch response"); +} + +function requireCurrentMain(repo, releaseCommit) { + const ref = ghJson(repo, "git/ref/heads/main", "current main ref"); + if (ref?.ref !== "refs/heads/main" || ref.object?.type !== "commit" || ref.object?.sha !== releaseCommit) { + throw error("main moved away from the exact release commit before continuation dispatch"); + } +} + +export function buildDispatchRequest(repo, pointer) { + const payload = JSON.stringify({ + ref: "main", + inputs: { + operation: pointer.operation, + release_commit: pointer.releaseCommit, + continuation_pointer: JSON.stringify(pointer), + }, + }); + return { + args: [ + "api", + "--include", + "--method", + "POST", + "-H", + "Accept: application/vnd.github+json", + "-H", + "X-GitHub-Api-Version: 2026-03-10", + `repos/${repo}/actions/workflows/release.yml/dispatches`, + "--input", + "-", + ], + payload, + }; +} + +function dispatch(repo, pointer) { + const request = buildDispatchRequest(repo, pointer); + const raw = command( + "gh", + request.args, + { + input: request.payload, + timeoutMs: CONTINUATION_DISPATCH_REQUEST_TIMEOUT_MS, + }, + ); + return parseDispatchResponse(raw); +} + +export async function main(environment = process.env) { + const repo = required("GH_REPO", environment); + const operation = required("RELEASE_OPERATION", environment); + const releaseCommit = required("RELEASE_HEAD_SHA", environment); + const parentRunId = positiveInteger(required("GITHUB_RUN_ID", environment), "GITHUB_RUN_ID"); + const parentRunAttempt = positiveInteger(required("GITHUB_RUN_ATTEMPT", environment), "GITHUB_RUN_ATTEMPT"); + const artifactId = positiveInteger(required("CONTINUATION_ARTIFACT_ID", environment), "CONTINUATION_ARTIFACT_ID"); + const expectedArtifactDigest = required("CONTINUATION_ARTIFACT_DIGEST", environment); + const expectedContractDigest = required("CONTINUATION_CONTRACT_DIGEST", environment); + const metadata = ghJson(repo, `actions/artifacts/${artifactId}`, `continuation artifact ${artifactId}`); + const artifact = normalizeArtifactIdentity({ + digest: metadata.digest, + id: metadata.id, + name: metadata.name, + size: metadata.size_in_bytes, + }, "continuation artifact metadata"); + if ( + artifact.id !== artifactId + || artifact.digest !== expectedArtifactDigest + || metadata.expired !== false + || (metadata.workflow_run?.id !== undefined && Number(metadata.workflow_run.id) !== parentRunId) + ) { + throw error("uploaded continuation artifact metadata does not match the completed parent job outputs"); + } + const run = ghJson(repo, `actions/runs/${parentRunId}`, `parent Release run ${parentRunId}`); + if ( + Number(run.id) !== parentRunId + || Number(run.run_attempt) !== parentRunAttempt + || run.head_sha !== releaseCommit + || run.event !== "workflow_dispatch" + ) { + throw error("dispatcher parent run does not match its exact Release workflow identity"); + } + const bytes = exactArtifactBytes(repo, artifact); + if (!Buffer.isBuffer(bytes) || bytes.length !== artifact.size || `sha256:${sha256Bytes(bytes)}` !== artifact.digest) { + throw error("uploaded continuation artifact bytes do not match immutable GitHub metadata"); + } + const contract = validateReleaseContinuationContract(contractFromZip(bytes), { + contractDigest: expectedContractDigest, + operation, + parentRunAttempt, + parentRunId, + releaseCommit, + }); + const pointer = createReleaseContinuationPointer({ artifact, contract }); + openContinuationEnvelope(bytes, pointer); + if (JSON.stringify(pointer).length > 32 * 1024) { + throw error("canonical continuation pointer exceeds the bounded workflow input size"); + } + requireCurrentMain(repo, releaseCommit); + const delay = continuationDelaySeconds(contract.outcome.notBeforeEpochSeconds, Math.floor(Date.now() / 1000)); + if (delay > 0) { + console.log(`waiting ${delay}s for the registry-authoritative continuation not-before time`); + await new Promise((resolve) => setTimeout(resolve, delay * 1000)); + } + requireCurrentMain(repo, releaseCommit); + const response = validateDispatchResponse(dispatch(repo, pointer), { repo, parentRunId }); + const authorizationPath = required("CONTINUATION_AUTHORIZATION_PATH", environment); + const authorization = createReleaseContinuationAuthorization({ + childRunId: response.childRunId, + pointer, + repo, + }); + writeFileSync( + authorizationPath, + serializeReleaseContinuationAuthorization(authorization), + { flag: "wx", mode: 0o600 }, + ); + const authorizationArtifactName = continuationAuthorizationArtifactName(pointer); + if (environment.GITHUB_OUTPUT) { + writeFileSync( + environment.GITHUB_OUTPUT, + `authorization_artifact_name=${authorizationArtifactName}\n` + + `child_run_id=${response.childRunId}\nchild_run_url=${response.htmlUrl}\n`, + { flag: "a" }, + ); + } + if (environment.GITHUB_STEP_SUMMARY) { + writeFileSync( + environment.GITHUB_STEP_SUMMARY, + `## Release continuation dispatched\n\n- Child run: [${response.childRunId}](${response.htmlUrl})\n` + + `- Authorization artifact: \`${authorizationArtifactName}\`\n` + + `- Generation: ${contract.lineage.generation}/${contract.lineage.maxGenerations}\n` + + `- Remaining immutable operations: ${contract.outcome.remainingCount}\n`, + { flag: "a" }, + ); + } + console.log( + `dispatched ${operation} continuation generation ${contract.lineage.generation} as child Release run ` + + `${response.childRunId}: ${response.htmlUrl}`, + ); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((cause) => { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + }); +} diff --git a/.github/scripts/download-aot-artifacts.sh b/.github/scripts/download-aot-artifacts.sh deleted file mode 100755 index a1d1e23f..00000000 --- a/.github/scripts/download-aot-artifacts.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" - -cargo run -p xtask -- assets download --latest-compatible --all-targets diff --git a/.github/scripts/download-bootstrap-ledger.mjs b/.github/scripts/download-bootstrap-ledger.mjs new file mode 100644 index 00000000..26fc5755 --- /dev/null +++ b/.github/scripts/download-bootstrap-ledger.mjs @@ -0,0 +1,553 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + appendFileSync, + copyFileSync, + existsSync, + lstatSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import { + createSiblingStage, + promoteDirectory, + removeTemporaryPath, + stageExistingDirectory, +} from "../../tools/release/atomic-directory.mjs"; +import { + retryReadOperationSync, + runGitHubPaginatedJsonSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; +import { + parseContinuationJson, + sha256Bytes, + validateReleaseContinuationPointer, +} from "../../tools/release/release-continuation-contract.mjs"; +import { openContinuationEnvelope } from "./release-continuation-artifact.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const ARTIFACT = "oliphaunt-bootstrap-ledger"; +const CHECKPOINT = /^checkpoint-[0-9]{6}-[0-9a-f]{64}[.]json$/u; +const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024; + +function fail(message) { + throw new Error(`download-bootstrap-ledger: ${message}`); +} + +function required(name) { + const value = process.env[name]?.trim(); + if (!value) fail(`${name} is required`); + return value; +} + +function command(commandName, args, { binary = false } = {}) { + const result = spawnSync(commandName, args, { + cwd: ROOT, + env: process.env, + encoding: binary ? null : "utf8", + maxBuffer: MAX_ARTIFACT_BYTES, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : result.stdout; + fail( + `${commandName} ${args.join(" ")} failed: ` + + `${(stderr || stdout || result.error?.message || "").trim()}`, + ); + } + return result.stdout; +} + +function gh(args) { + return runGitHubReadSync(args, { + cwd: ROOT, + label: `bootstrap ledger GitHub ${args[0]} ${args[1]} read`, + maxBuffer: MAX_ARTIFACT_BYTES, + }).trim(); +} + +function ghBinary(args, options = {}) { + const result = runGitHubReadSync(args, { + binary: true, + cwd: ROOT, + label: options.label ?? `bootstrap ledger GitHub ${args[0]} ${args[1]} read`, + maxBuffer: MAX_ARTIFACT_BYTES, + ...options, + }); + if (!Buffer.isBuffer(result)) fail("gh artifact download did not return binary data"); + return result; +} + +function json(text, label) { + try { + return JSON.parse(text); + } catch (error) { + fail(`${label} returned invalid JSON: ${error.message}`); + } +} + +function timestamp(value, label) { + if (typeof value !== "string" || !value.endsWith("Z")) fail(`${label} must be a UTC timestamp`); + const milliseconds = Date.parse(value); + if (!Number.isFinite(milliseconds)) fail(`${label} must be a valid UTC timestamp`); + return milliseconds; +} + +function positiveIntegerString(value, label) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered)) fail(`${label} must be a positive integer`); + return rendered; +} + +function attemptNumber(value) { + const rendered = positiveIntegerString(value, "GITHUB_RUN_ATTEMPT"); + const parsed = Number(rendered); + if (!Number.isSafeInteger(parsed)) fail("GITHUB_RUN_ATTEMPT must be a safe integer"); + return parsed; +} + +function ledgerArtifactInventory(repo, sha) { + const artifacts = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/artifacts?name=${encodeURIComponent(ARTIFACT)}`, + { + cwd: ROOT, + itemsField: "artifacts", + label: `repository ${ARTIFACT} inventory`, + maxBuffer: MAX_ARTIFACT_BYTES, + }, + ); + const byRun = new Map(); + for (const entry of artifacts) { + if ( + entry === null + || Array.isArray(entry) + || typeof entry !== "object" + || entry.name !== ARTIFACT + || entry.workflow_run === null + || Array.isArray(entry.workflow_run) + || typeof entry.workflow_run !== "object" + || !/^[1-9][0-9]*$/u.test(String(entry.workflow_run.id ?? "")) + || typeof entry.workflow_run.head_sha !== "string" + ) { + fail(`repository ${ARTIFACT} inventory contains malformed workflow binding`); + } + if (entry.workflow_run.head_sha !== sha) continue; + const runId = String(entry.workflow_run.id); + const runArtifacts = byRun.get(runId) ?? []; + runArtifacts.push(entry); + byRun.set(runId, runArtifacts); + } + return byRun; +} + +function ledgerArtifacts(artifacts, runId) { + if (!Array.isArray(artifacts)) fail(`artifact inventory for Release run ${runId} must be a list`); + const result = []; + for (const entry of artifacts) { + if (entry?.name !== ARTIFACT || entry.expired === true) continue; + if (entry === null || Array.isArray(entry) || typeof entry !== "object") { + fail(`Release run ${runId} contains malformed ${ARTIFACT} metadata`); + } + if (entry.expired !== false) fail(`${ARTIFACT} in Release run ${runId} has ambiguous expiry metadata`); + const id = positiveIntegerString(entry.id, `${ARTIFACT} artifact id in Release run ${runId}`); + if (!Number.isSafeInteger(entry.size_in_bytes) || entry.size_in_bytes <= 0) { + fail(`${ARTIFACT} artifact ${id} has an invalid immutable byte size`); + } + if (typeof entry.digest !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(entry.digest)) { + fail(`${ARTIFACT} artifact ${id} has an invalid immutable SHA-256 digest`); + } + const createdAt = timestamp(entry.created_at, `${ARTIFACT} artifact ${id} created_at`); + const updatedAt = timestamp(entry.updated_at, `${ARTIFACT} artifact ${id} updated_at`); + if (updatedAt < createdAt) fail(`${ARTIFACT} artifact ${id} was updated before it was created`); + if (entry.workflow_run?.id !== undefined && String(entry.workflow_run.id) !== runId) { + fail(`${ARTIFACT} artifact ${id} is not bound to Release run ${runId}`); + } + result.push({ createdAt, id, raw: entry, updatedAt }); + } + return result; +} + +function newest(left, right) { + if (left.updatedAt !== right.updatedAt) return right.updatedAt - left.updatedAt; + if (left.createdAt !== right.createdAt) return right.createdAt - left.createdAt; + const leftId = BigInt(left.id); + const rightId = BigInt(right.id); + return leftId < rightId ? 1 : leftId > rightId ? -1 : 0; +} + +export function selectEarlierAttemptArtifact(artifacts, { runId, currentAttemptStartedAt }) { + const normalizedRunId = positiveIntegerString(runId, "current Release run id"); + const boundary = timestamp(currentAttemptStartedAt, "current Release run attempt start"); + const candidates = ledgerArtifacts(artifacts, normalizedRunId); + const earlier = candidates + .filter(({ createdAt, updatedAt }) => createdAt < boundary && updatedAt < boundary) + .sort(newest); + const excludedCurrentAttemptIds = candidates + .filter(({ createdAt, updatedAt }) => createdAt >= boundary || updatedAt >= boundary) + .map(({ id }) => id) + .sort((left, right) => (BigInt(left) < BigInt(right) ? -1 : 1)); + return { + artifact: earlier[0]?.raw ?? null, + excludedCurrentAttemptIds, + }; +} + +export function selectLatestArtifact(artifacts, { runId }) { + const normalizedRunId = positiveIntegerString(runId, "prior Release run id"); + return ledgerArtifacts(artifacts, normalizedRunId).sort(newest)[0]?.raw ?? null; +} + +export function validateAttemptMetadata(metadata, { runId, attempt, sha }) { + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail("current Release run attempt metadata must be an object"); + } + const normalizedRunId = positiveIntegerString(runId, "current Release run id"); + if (String(metadata.id ?? "") !== normalizedRunId) fail("current attempt metadata has the wrong run id"); + if (metadata.run_attempt !== attempt) fail("current attempt metadata has the wrong attempt number"); + if (metadata.head_sha !== sha) fail("current attempt metadata has the wrong release SHA"); + if (metadata.event !== "workflow_dispatch") fail("current attempt metadata is not a workflow_dispatch run"); + timestamp(metadata.run_started_at, "current Release run attempt run_started_at"); + return metadata.run_started_at; +} + +export function validateCurrentRunMetadata(metadata, { runId, sha }) { + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail("current Release run metadata must be an object"); + } + const normalizedRunId = positiveIntegerString(runId, "current Release run id"); + if (String(metadata.id ?? "") !== normalizedRunId) fail("current Release run metadata has the wrong run id"); + if (metadata.head_sha !== sha) fail("current Release run metadata has the wrong release SHA"); + if (metadata.event !== "workflow_dispatch") fail("current Release run metadata is not a workflow_dispatch run"); + const workflowId = positiveIntegerString(metadata.workflow_id, "current Release workflow id"); + timestamp(metadata.created_at, "current Release run created_at"); + return { workflowId }; +} + +export function validatePriorRunMetadata(metadata, { runId, sha, workflowId }) { + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail(`prior Release run ${runId} metadata must be an object`); + } + const normalizedRunId = positiveIntegerString(runId, "prior Release run id"); + if (String(metadata.id ?? "") !== normalizedRunId) { + fail(`prior Release run ${normalizedRunId} metadata has the wrong run id`); + } + if (metadata.head_sha !== sha) { + fail(`prior Release run ${normalizedRunId} metadata disagrees with its exact-SHA artifact binding`); + } + const candidateWorkflowId = positiveIntegerString( + metadata.workflow_id, + `prior Release run ${normalizedRunId} workflow id`, + ); + const createdAt = timestamp(metadata.created_at, `prior Release run ${normalizedRunId} created_at`); + if (candidateWorkflowId !== workflowId || metadata.event !== "workflow_dispatch") return null; + if (typeof metadata.status !== "string" || metadata.status.length === 0) { + fail(`prior Release run ${normalizedRunId} has malformed status metadata`); + } + if (metadata.status !== "completed") return null; + return { createdAt, runId: normalizedRunId }; +} + +function files(directory) { + const result = []; + const visit = (current) => { + for (const entry of readdirSync(current, { withFileTypes: true })) { + const file = path.join(current, entry.name); + if (entry.isDirectory()) visit(file); + else if (entry.isFile()) result.push(file); + else fail(`artifact contains unsupported entry ${file}`); + } + }; + visit(directory); + return result.sort(); +} + +function sha256(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function same(left, right) { + return statSync(left).size === statSync(right).size && sha256(left) === sha256(right); +} + +function validateZipMembers(archive) { + const members = command("unzip", ["-Z1", archive]) + .split(/\r?\n/u) + .filter(Boolean); + if (members.length === 0) fail("bootstrap ledger artifact archive is empty"); + const seen = new Set(); + for (const member of members) { + if ( + member.includes("\\") || + member.includes("/") || + member === "." || + member === ".." || + /[\u0000-\u001f\u007f]/u.test(member) || + !CHECKPOINT.test(member) + ) { + fail(`bootstrap ledger artifact contains unexpected archive member ${JSON.stringify(member)}`); + } + if (seen.has(member)) fail(`bootstrap ledger artifact repeats archive member ${member}`); + seen.add(member); + } +} + +function downloadArtifactById(repo, artifact, destination) { + const id = positiveIntegerString(artifact.id, "bootstrap ledger artifact id"); + return retryReadOperationSync( + `download bootstrap ledger artifact ${id}`, + ({ attemptTimeoutMs }) => { + const directory = createSiblingStage(destination, `artifact-${id}`); + const archive = path.join(directory, ".artifact.zip"); + try { + const bytes = ghBinary( + [ + "api", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + `repos/${repo}/actions/artifacts/${id}/zip`, + ], + { + attemptTimeoutMs, + baseDelayMs: 0, + deadlineMs: attemptTimeoutMs, + label: `download bootstrap ledger artifact ${id}`, + maxAttempts: 1, + maxDelayMs: 0, + }, + ); + if (bytes.length < 4 || bytes[0] !== 0x50 || bytes[1] !== 0x4b) { + fail(`bootstrap ledger artifact ${id} did not return a ZIP archive`); + } + const digest = createHash("sha256").update(bytes).digest("hex"); + if (bytes.length !== artifact.size_in_bytes || digest !== artifact.digest.slice("sha256:".length)) { + fail( + `bootstrap ledger artifact ${id} transport identity mismatch: expected ` + + `${artifact.size_in_bytes}/${artifact.digest}, got ${bytes.length}/sha256:${digest}`, + ); + } + writeFileSync(archive, bytes, { flag: "wx" }); + validateZipMembers(archive); + command("unzip", ["-q", archive, "-d", directory]); + rmSync(archive, { force: true }); + if (files(directory).length === 0) { + fail(`bootstrap ledger artifact ${id} contains no checkpoints`); + } + return directory; + } catch (error) { + removeTemporaryPath(directory); + throw error; + } + }, + { + attemptTimeoutMs: 5 * 60_000, + deadlineMs: 15 * 60_000, + maxAttempts: 4, + }, + ); +} + +function restoreArtifact(repo, runId, artifact, destination, sourceDescription) { + const artifactId = positiveIntegerString(artifact.id, "bootstrap ledger artifact id"); + const temporary = downloadArtifactById(repo, artifact, destination); + const stage = stageExistingDirectory(destination, "restore"); + try { + const sources = files(temporary); + if (sources.length === 0) fail(`bootstrap ledger artifact ${artifactId} contains no checkpoints`); + for (const source of sources) { + const name = path.basename(source); + const relative = path.relative(temporary, source).split(path.sep).join("/"); + if (relative !== name || !CHECKPOINT.test(name)) { + fail(`bootstrap ledger artifact contains unexpected file ${relative}`); + } + const target = path.join(stage, name); + if (statSync(target, { throwIfNoEntry: false })?.isFile()) { + if (!same(source, target)) fail(`prior checkpoint conflicts with local ${name}`); + } else { + copyFileSync(source, target); + } + } + promoteDirectory(stage, destination); + } finally { + removeTemporaryPath(temporary); + if (existsSync(stage)) removeTemporaryPath(stage); + } + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `found=true\nrun_id=${runId}\n`); + } + console.log( + `restored immutable bootstrap checkpoint chain from ${sourceDescription} ` + + `(Release run ${runId}, artifact ${artifactId})`, + ); +} + +export function restoreExactContinuationLedger(envelope, destination) { + if (!Array.isArray(envelope?.checkpointEntries) || envelope.checkpointEntries.length === 0) { + fail("exact continuation envelope contains no bootstrap checkpoints"); + } + const stage = stageExistingDirectory(destination, "exact-continuation"); + try { + for (const entry of envelope.checkpointEntries) { + if ( + entry === null + || typeof entry !== "object" + || !CHECKPOINT.test(entry.name) + || !Buffer.isBuffer(entry.bytes) + || entry.bytes.length === 0 + ) { + fail("exact continuation envelope contains a malformed bootstrap checkpoint"); + } + const target = path.join(stage, entry.name); + if (statSync(target, { throwIfNoEntry: false })?.isFile()) { + const existing = readFileSync(target); + if (existing.length !== entry.bytes.length || sha256Bytes(existing) !== sha256Bytes(entry.bytes)) { + fail(`exact continuation checkpoint conflicts with local ${entry.name}`); + } + } else { + writeFileSync(target, entry.bytes, { flag: "wx", mode: 0o600 }); + } + } + promoteDirectory(stage, destination); + } finally { + if (existsSync(stage)) removeTemporaryPath(stage); + } +} + +function priorRuns(repo, sha, currentRun, workflowId, artifactsByRun) { + const document = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/workflows/${workflowId}/runs?head_sha=${encodeURIComponent(sha)}&event=workflow_dispatch`, + { + cwd: ROOT, + itemsField: "workflow_runs", + label: `exact-SHA Release workflow ${workflowId} run inventory`, + maxBuffer: MAX_ARTIFACT_BYTES, + }, + ); + const result = []; + const seen = new Set(); + for (const metadata of document) { + const runId = positiveIntegerString(metadata?.id, "exact-SHA Release run id"); + if (seen.has(runId)) fail(`exact-SHA Release run inventory repeats run ${runId}`); + seen.add(runId); + if (runId === currentRun) continue; + const candidate = validatePriorRunMetadata(metadata, { runId, sha, workflowId }); + if (candidate !== null && artifactsByRun.has(runId)) result.push(candidate); + } + return result.sort((left, right) => + right.createdAt - left.createdAt + || (BigInt(left.runId) < BigInt(right.runId) ? 1 : -1)); +} + +export async function main() { + const repo = required("GH_REPO"); + const sha = required("RELEASE_HEAD_SHA"); + if (!/^[0-9a-f]{40}$/u.test(sha)) fail("RELEASE_HEAD_SHA must be a full lowercase commit SHA"); + const destination = path.resolve( + ROOT, + process.env.BOOTSTRAP_LEDGER_PATH || "target/release/bootstrap-ledger", + ); + const currentRun = positiveIntegerString(required("GITHUB_RUN_ID"), "GITHUB_RUN_ID"); + const rawPointer = process.env.RELEASE_CONTINUATION_POINTER?.trim() ?? ""; + if (rawPointer !== "") { + if (rawPointer.length > 32 * 1024) fail("RELEASE_CONTINUATION_POINTER exceeds 32 KiB"); + const pointer = validateReleaseContinuationPointer( + parseContinuationJson(rawPointer, "RELEASE_CONTINUATION_POINTER"), + { operation: "publish-bootstrap", releaseCommit: sha }, + ); + const archive = path.resolve(required("RELEASE_CONTINUATION_ARCHIVE")); + const metadata = lstatSync(archive, { throwIfNoEntry: false }); + if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== pointer.artifact.size) { + fail("cached exact continuation archive has the wrong file identity"); + } + const bytes = readFileSync(archive); + if (`sha256:${sha256Bytes(bytes)}` !== pointer.artifact.digest) { + fail("cached exact continuation archive digest differs from its pointer"); + } + const envelope = openContinuationEnvelope(bytes, pointer); + restoreExactContinuationLedger(envelope, destination); + if (process.env.GITHUB_OUTPUT) { + appendFileSync( + process.env.GITHUB_OUTPUT, + `found=true\nrun_id=${pointer.parentRunId}\nartifact_id=${pointer.artifact.id}\n` + + `generation=${pointer.generation}\n`, + ); + } + console.log( + `restored exact bootstrap continuation generation ${pointer.generation}/${pointer.maxGenerations} ` + + `from parent run ${pointer.parentRunId}, artifact ${pointer.artifact.id}`, + ); + return; + } + const currentAttempt = attemptNumber(required("GITHUB_RUN_ATTEMPT")); + const currentRunMetadata = json( + gh(["api", `repos/${repo}/actions/runs/${currentRun}`]), + `current Release run ${currentRun}`, + ); + const { workflowId } = validateCurrentRunMetadata(currentRunMetadata, { + runId: currentRun, + sha, + }); + const artifactsByRun = ledgerArtifactInventory(repo, sha); + + let excludedCurrentAttemptIds = []; + if (currentAttempt > 1) { + const metadata = json( + gh(["api", `repos/${repo}/actions/runs/${currentRun}/attempts/${currentAttempt}`]), + `Release run ${currentRun} attempt ${currentAttempt}`, + ); + const currentAttemptStartedAt = validateAttemptMetadata(metadata, { + attempt: currentAttempt, + runId: currentRun, + sha, + }); + const selected = selectEarlierAttemptArtifact(artifactsByRun.get(currentRun) ?? [], { + currentAttemptStartedAt, + runId: currentRun, + }); + excludedCurrentAttemptIds = selected.excludedCurrentAttemptIds; + if (selected.artifact !== null) { + restoreArtifact( + repo, + currentRun, + selected.artifact, + destination, + "an earlier attempt of the current run", + ); + return; + } + } + + for (const run of priorRuns(repo, sha, currentRun, workflowId, artifactsByRun)) { + const { runId } = run; + const artifact = selectLatestArtifact(artifactsByRun.get(runId) ?? [], { runId }); + if (artifact === null) continue; + restoreArtifact(repo, runId, artifact, destination, "a prior completed dispatch"); + return; + } + + if (excludedCurrentAttemptIds.length > 0) { + fail( + "the current rerun attempt has bootstrap ledger artifact(s), but no artifact can be proven to " + + `predate the attempt and no prior dispatch can recover the chain; refusing genesis ` + + `(excluded artifact ids: ${excludedCurrentAttemptIds.join(", ")})`, + ); + } + if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, "found=false\n"); + console.log("no prior bootstrap checkpoint artifact exists for this release SHA; starting a genesis chain"); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((cause) => { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + }); +} diff --git a/.github/scripts/download-bootstrap-ledger.test.mjs b/.github/scripts/download-bootstrap-ledger.test.mjs new file mode 100644 index 00000000..c9fad71d --- /dev/null +++ b/.github/scripts/download-bootstrap-ledger.test.mjs @@ -0,0 +1,585 @@ +#!/usr/bin/env bun + +import assert from "node:assert/strict"; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; + +import { + restoreExactContinuationLedger, + selectEarlierAttemptArtifact, + validateAttemptMetadata, +} from "./download-bootstrap-ledger.mjs"; + +const SCRIPT = path.join(import.meta.dirname, "download-bootstrap-ledger.mjs"); +const SHA = "a".repeat(40); +const ARTIFACT_NAME = "oliphaunt-bootstrap-ledger"; +const BOOTSTRAP_LEDGER_PROCESS_TIMEOUT_MS = 20_000; + +function artifact(id, runId, createdAt, updatedAt = createdAt, extra = {}) { + return { + digest: `sha256:${"a".repeat(64)}`, + id, + name: ARTIFACT_NAME, + expired: false, + size_in_bytes: 1, + created_at: createdAt, + updated_at: updatedAt, + workflow_run: { id: runId }, + ...extra, + }; +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + timeout: BOOTSTRAP_LEDGER_PROCESS_TIMEOUT_MS, + ...options, + }); + assert.equal( + result.status, + 0, + `${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout}`, + ); + return result; +} + +function checkpointName(sequence, fill) { + return `checkpoint-${String(sequence).padStart(6, "0")}-${fill.repeat(64)}.json`; +} + +function ledgerZip(root, label, sequence, fill) { + const file = path.join(root, checkpointName(sequence, fill)); + const archive = path.join(root, `${label}.zip`); + writeFileSync(file, `${JSON.stringify({ label, sequence })}\n`); + run("zip", ["-q", "-j", archive, file]); + rmSync(file); + return archive; +} + +function fakeGh(root) { + const directory = path.join(root, "bin"); + const file = path.join(directory, "gh"); + run("mkdir", ["-p", directory]); + writeFileSync( + file, + `#!/usr/bin/env node +const fs = require("node:fs"); +const crypto = require("node:crypto"); +const args = process.argv.slice(2); +fs.appendFileSync(process.env.FAKE_GH_LOG, JSON.stringify(args) + "\\n"); +if (args[0] !== "api") throw new Error("unexpected gh command " + JSON.stringify(args)); +const endpoint = args.at(-1); +const attempt = /\\/runs\\/([0-9]+)\\/attempts\\/([0-9]+)$/.exec(endpoint); +if (attempt) { + process.stdout.write(process.env.FAKE_ATTEMPT_METADATA); + process.exit(0); +} +const run = /\\/actions\\/runs\\/([0-9]+)$/.exec(endpoint); +if (run) { + if (run[1] === "900") { + process.stdout.write(process.env.FAKE_CURRENT_RUN); + process.exit(0); + } + const row = JSON.parse(process.env.FAKE_RUNS) + .find((candidate) => String(candidate.databaseId) === run[1]); + if (!row) throw new Error("missing fake run metadata " + run[1]); + process.stdout.write(JSON.stringify({ + id: Number(run[1]), + workflow_id: row.workflowId ?? 42, + head_sha: row.headSha ?? "${SHA}", + event: row.event ?? "workflow_dispatch", + created_at: row.createdAt, + status: row.status, + conclusion: row.conclusion ?? null, + })); + process.exit(0); +} +if (/\\/actions\\/workflows\\/42\\/runs[?]/.test(endpoint)) { + const url = new URL("https://api.github.com/" + endpoint); + const page = Number(url.searchParams.get("page")); + const all = JSON.parse(process.env.FAKE_RUNS).map((row) => ({ + id: row.databaseId, + workflow_id: row.workflowId ?? 42, + head_sha: row.headSha ?? "${SHA}", + event: row.event ?? "workflow_dispatch", + created_at: row.createdAt, + status: row.status, + conclusion: row.conclusion ?? null, + })); + const workflow_runs = all.slice((page - 1) * 100, page * 100); + let link = ""; + if (page * 100 < all.length) { + const next = new URL(url); + next.searchParams.set("page", String(page + 1)); + const last = new URL(url); + last.searchParams.set("page", String(Math.ceil(all.length / 100))); + link = \`Link: <\${next}>; rel="next", <\${last}>; rel="last"\\n\`; + } + process.stdout.write("HTTP/2.0 200 OK\\n" + link + "\\n" + JSON.stringify({ workflow_runs })); + process.exit(0); +} +if (/\\/actions\\/artifacts[?]name=/.test(endpoint)) { + const url = new URL("https://api.github.com/" + endpoint); + const page = Number(url.searchParams.get("page")); + const zips = JSON.parse(process.env.FAKE_ZIPS_BY_ARTIFACT); + const all = Object.values(JSON.parse(process.env.FAKE_ARTIFACTS_BY_RUN)) + .flat() + .map((artifact) => { + const archive = zips[String(artifact.id)]; + const bytes = archive ? fs.readFileSync(archive) : null; + return { + ...artifact, + ...(bytes === null ? {} : { + size_in_bytes: bytes.length, + digest: "sha256:" + crypto.createHash("sha256").update(bytes).digest("hex"), + }), + workflow_run: { ...artifact.workflow_run, head_sha: "${SHA}" }, + }; + }); + const artifacts = all.slice((page - 1) * 100, page * 100); + let link = ""; + if (page * 100 < all.length) { + const next = new URL(url); + next.searchParams.set("page", String(page + 1)); + const last = new URL(url); + last.searchParams.set("page", String(Math.ceil(all.length / 100))); + link = \`Link: <\${next}>; rel="next", <\${last}>; rel="last"\\n\`; + } + process.stdout.write("HTTP/2.0 200 OK\\n" + link + "\\n" + JSON.stringify({ + total_count: all.length, + artifacts, + })); + process.exit(0); +} +const download = /\\/artifacts\\/([0-9]+)\\/zip$/.exec(endpoint); +if (download) { + const archive = JSON.parse(process.env.FAKE_ZIPS_BY_ARTIFACT)[download[1]]; + if (!archive) throw new Error("missing fake artifact ZIP " + download[1]); + const state = process.env.FAKE_DOWNLOAD_STATE; + const count = fs.existsSync(state) ? Number(fs.readFileSync(state, "utf8")) : 0; + fs.writeFileSync(state, String(count + 1)); + if (process.env.FAKE_DOWNLOAD_MODE === "transient" && count === 0) { + process.stderr.write("HTTP 503 unexpected EOF\\n"); + process.exit(1); + } + const bytes = fs.readFileSync(archive); + if (process.env.FAKE_DOWNLOAD_MODE === "identity-mismatch") { + const altered = Buffer.from(bytes); + altered[Math.min(10, altered.length - 1)] ^= 0x01; + process.stdout.write(altered); + } else { + process.stdout.write(process.env.FAKE_DOWNLOAD_MODE === "truncated" ? bytes.subarray(0, 3) : bytes); + } + process.exit(0); +} +throw new Error("unexpected gh api endpoint " + endpoint); +`, + ); + chmodSync(file, 0o755); + return directory; +} + +async function fixture(t, label) { + const root = mkdtempSync(path.join(os.tmpdir(), `oliphaunt-ledger-download-${label}-`)); + t.after(() => rmSync(root, { force: true, recursive: true })); + const bin = fakeGh(root); + const output = path.join(root, "github-output"); + writeFileSync(output, ""); + return { + bin, + destination: path.join(root, "destination"), + downloadState: path.join(root, "download-state"), + log: path.join(root, "gh.log"), + output, + root, + }; +} + +function invoke(fixture, { + artifactsByRun = {}, + attempt = 1, + attemptMetadata = {}, + currentRunMetadata = { + id: 900, + workflow_id: 42, + head_sha: SHA, + event: "workflow_dispatch", + created_at: "2026-07-15T10:00:00Z", + status: "in_progress", + }, + runs = [], + zipsByArtifact = {}, + downloadMode = "success", +} = {}) { + return spawnSync(process.execPath, [SCRIPT], { + encoding: "utf8", + timeout: BOOTSTRAP_LEDGER_PROCESS_TIMEOUT_MS, + env: { + ...process.env, + PATH: `${fixture.bin}${path.delimiter}${process.env.PATH}`, + BOOTSTRAP_LEDGER_PATH: fixture.destination, + FAKE_ARTIFACTS_BY_RUN: JSON.stringify(artifactsByRun), + FAKE_ATTEMPT_METADATA: JSON.stringify(attemptMetadata), + FAKE_CURRENT_RUN: JSON.stringify(currentRunMetadata), + FAKE_DOWNLOAD_MODE: downloadMode, + FAKE_DOWNLOAD_STATE: fixture.downloadState, + FAKE_GH_LOG: fixture.log, + FAKE_RUNS: JSON.stringify(runs), + FAKE_ZIPS_BY_ARTIFACT: JSON.stringify(zipsByArtifact), + GH_REPO: "f0rr0/oliphaunt", + GITHUB_OUTPUT: fixture.output, + GITHUB_RUN_ATTEMPT: String(attempt), + GITHUB_RUN_ID: "900", + RELEASE_HEAD_SHA: SHA, + OLIPHAUNT_GITHUB_READ_BASE_DELAY_MS: "0", + OLIPHAUNT_GITHUB_READ_MAX_DELAY_MS: "0", + }, + }); +} + +function calls(fixture) { + return readFileSync(fixture.log, "utf8") + .trim() + .split(/\r?\n/u) + .filter(Boolean) + .map((line) => JSON.parse(line)); +} + +function downloadedArtifactIds(fixture) { + return calls(fixture) + .map((args) => /\/artifacts\/([0-9]+)\/zip$/u.exec(args.at(-1))?.[1]) + .filter(Boolean); +} + +function restoredNames(fixture) { + return readdirSync(fixture.destination).sort(); +} + +test("rerun restores an earlier-attempt artifact by immutable artifact id and excludes current-attempt bytes", async (t) => { + const f = await fixture(t, "earlier-attempt"); + const priorZip = ledgerZip(f.root, "prior-attempt", 0, "a"); + const currentZip = ledgerZip(f.root, "current-attempt", 1, "b"); + const result = invoke(f, { + attempt: 2, + attemptMetadata: { + id: 900, + run_attempt: 2, + run_started_at: "2026-07-15T10:00:00Z", + head_sha: SHA, + event: "workflow_dispatch", + }, + artifactsByRun: { + 900: [ + artifact(202, 900, "2026-07-15T10:00:01Z"), + artifact(101, 900, "2026-07-15T09:50:00Z", "2026-07-15T09:51:00Z"), + ], + }, + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 101: priorZip, 202: currentZip }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(downloadedArtifactIds(f), ["101"]); + assert.deepEqual(restoredNames(f), [checkpointName(0, "a")]); + assert.match(readFileSync(f.output, "utf8"), /^found=true\nrun_id=900\n$/u); + assert.equal(calls(f).some((args) => args[0] === "run" && args[1] === "list"), false); +}); + +test("fresh attempt preserves recovery from the newest completed distinct dispatch", async (t) => { + const f = await fixture(t, "fresh-dispatch"); + const priorZip = ledgerZip(f.root, "prior-dispatch", 3, "c"); + const result = invoke(f, { + artifactsByRun: { + 800: [artifact(303, 800, "2026-07-14T10:05:00Z")], + 700: [artifact(404, 700, "2026-07-14T09:05:00Z")], + }, + runs: [ + { databaseId: 900, createdAt: "2026-07-15T10:00:00Z", status: "in_progress", conclusion: null }, + { databaseId: 700, createdAt: "2026-07-14T09:00:00Z", status: "in_progress", conclusion: null }, + { databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }, + ], + zipsByArtifact: { 303: priorZip }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(downloadedArtifactIds(f), ["303"]); + assert.deepEqual(restoredNames(f), [checkpointName(3, "c")]); + assert.match(readFileSync(f.output, "utf8"), /^found=true\nrun_id=800\n$/u); + assert.equal( + calls(f).filter((args) => args.at(-1)?.includes("/actions/artifacts?name=oliphaunt-bootstrap-ledger")).length, + 1, + "all candidate dispatches must share one repository-level named artifact inventory", + ); + assert.equal( + calls(f).some((args) => args.at(-1)?.includes("/runs/900/artifacts")), + false, + "attempt 1 must never inspect or download its own run artifact", + ); +}); + +test("paginated artifact/run recovery finds an eligible dispatch beyond one hundred newer retries", async (t) => { + const f = await fixture(t, "beyond-one-hundred"); + const priorZip = ledgerZip(f.root, "beyond-one-hundred-prior", 13, "a"); + const artifactsByRun = {}; + const runs = []; + for (let index = 0; index < 101; index += 1) { + const runId = 1_000 + index; + const timestamp = `2026-07-15T${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}:00Z`; + artifactsByRun[runId] = [artifact(2_000 + index, runId, timestamp)]; + runs.push({ databaseId: runId, createdAt: timestamp, status: "in_progress", conclusion: null }); + } + artifactsByRun[800] = [artifact(3_000, 800, "2026-07-14T10:05:00Z")]; + runs.push({ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }); + const result = invoke(f, { + artifactsByRun, + runs, + zipsByArtifact: { 3_000: priorZip }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(downloadedArtifactIds(f), ["3000"]); + assert.deepEqual(restoredNames(f), [checkpointName(13, "a")]); + const inventory = calls(f); + assert.equal( + inventory.filter((args) => /\/actions\/runs\/[0-9]+$/u.test(args.at(-1))).length, + 1, + "only the current run needs an individual metadata read", + ); + assert.equal( + inventory.filter((args) => args.at(-1)?.includes("/actions/workflows/42/runs?")).length, + 2, + "the exact-SHA workflow run inventory must traverse both pages", + ); + assert.equal( + inventory.filter((args) => args.at(-1)?.includes("/actions/artifacts?name=")).length, + 2, + "the immutable named-artifact inventory must traverse both pages", + ); + assert.equal(inventory.some((args) => args[0] === "run" && args[1] === "list"), false); + assert.equal(inventory.flat().includes("--limit"), false); +}); + +test("a newer same-name artifact from another workflow cannot shadow the Release ledger", async (t) => { + const f = await fixture(t, "wrong-workflow-shadow"); + const correctZip = ledgerZip(f.root, "correct-workflow", 14, "b"); + const result = invoke(f, { + artifactsByRun: { + 850: [artifact(4_000, 850, "2026-07-15T09:05:00Z")], + 800: [artifact(4_001, 800, "2026-07-14T10:05:00Z")], + }, + runs: [ + { + databaseId: 850, + createdAt: "2026-07-15T09:00:00Z", + status: "completed", + conclusion: "success", + workflowId: 99, + }, + { + databaseId: 800, + createdAt: "2026-07-14T10:00:00Z", + status: "completed", + conclusion: "failure", + }, + ], + zipsByArtifact: { 4_001: correctZip }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(downloadedArtifactIds(f), ["4001"]); + assert.match(readFileSync(f.output, "utf8"), /^found=true\nrun_id=800\n$/u); +}); + +test("an artifact/run exact-SHA identity disagreement fails closed", async (t) => { + const f = await fixture(t, "sha-disagreement"); + const result = invoke(f, { + artifactsByRun: { 800: [artifact(5_000, 800, "2026-07-14T10:05:00Z")] }, + runs: [{ + databaseId: 800, + createdAt: "2026-07-14T10:00:00Z", + status: "completed", + conclusion: "failure", + headSha: "b".repeat(40), + }], + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /disagrees with its exact-SHA artifact binding/u); + assert.equal(readFileSync(f.output, "utf8"), ""); +}); + +test("rerun falls back to a completed distinct dispatch without consuming a current-attempt artifact", async (t) => { + const f = await fixture(t, "rerun-fallback"); + const currentZip = ledgerZip(f.root, "current", 4, "d"); + const priorZip = ledgerZip(f.root, "prior", 5, "e"); + const result = invoke(f, { + attempt: 3, + attemptMetadata: { + id: 900, + run_attempt: 3, + run_started_at: "2026-07-15T11:00:00Z", + head_sha: SHA, + event: "workflow_dispatch", + }, + artifactsByRun: { + 900: [artifact(202, 900, "2026-07-15T11:00:01Z")], + 800: [artifact(303, 800, "2026-07-14T10:05:00Z")], + }, + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 202: currentZip, 303: priorZip }, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(downloadedArtifactIds(f), ["303"]); + assert.deepEqual(restoredNames(f), [checkpointName(5, "e")]); +}); + +test("rerun refuses genesis when only an artifact from the current attempt is visible", async (t) => { + const f = await fixture(t, "refuse-current"); + const currentZip = ledgerZip(f.root, "current-only", 6, "f"); + const result = invoke(f, { + attempt: 2, + attemptMetadata: { + id: 900, + run_attempt: 2, + run_started_at: "2026-07-15T12:00:00Z", + head_sha: SHA, + event: "workflow_dispatch", + }, + artifactsByRun: { 900: [artifact(202, 900, "2026-07-15T12:00:00Z")] }, + runs: [], + zipsByArtifact: { 202: currentZip }, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /no artifact can be proven to predate the attempt.*refusing genesis/u); + assert.deepEqual(downloadedArtifactIds(f), []); + assert.equal(readFileSync(f.output, "utf8"), ""); +}); + +test("attempt boundary uses both creation and update time and metadata validation fails closed", () => { + const selected = selectEarlierAttemptArtifact( + [ + artifact(101, 900, "2026-07-15T09:00:00Z", "2026-07-15T09:30:00Z"), + artifact(102, 900, "2026-07-15T09:10:00Z", "2026-07-15T10:00:00Z"), + artifact(103, 900, "2026-07-15T09:20:00Z", "2026-07-15T09:40:00Z"), + ], + { runId: "900", currentAttemptStartedAt: "2026-07-15T10:00:00Z" }, + ); + assert.equal(selected.artifact.id, 103); + assert.deepEqual(selected.excludedCurrentAttemptIds, ["102"]); + assert.throws( + () => selectEarlierAttemptArtifact( + [artifact(104, 900, "not-a-timestamp")], + { runId: "900", currentAttemptStartedAt: "2026-07-15T10:00:00Z" }, + ), + /created_at must be a UTC timestamp/u, + ); + assert.throws( + () => validateAttemptMetadata( + { + id: 900, + run_attempt: 2, + run_started_at: "2026-07-15T10:00:00Z", + head_sha: "b".repeat(40), + event: "workflow_dispatch", + }, + { runId: "900", attempt: 2, sha: SHA }, + ), + /wrong release SHA/u, + ); +}); + +test("artifact transport retries transient failure and restores only a complete checkpoint envelope", async (t) => { + const f = await fixture(t, "transient-download"); + const archive = ledgerZip(f.root, "transient", 7, "a"); + const result = invoke(f, { + artifactsByRun: { 800: [artifact(707, 800, "2026-07-14T10:05:00Z")] }, + downloadMode: "transient", + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 707: archive }, + }); + assert.equal(result.status, 0, result.stderr); + assert.equal(readFileSync(f.downloadState, "utf8"), "2"); + assert.deepEqual(restoredNames(f), [checkpointName(7, "a")]); + assert.equal(readdirSync(f.root).some((name) => name.startsWith(".destination.")), false); +}); + +test("repeated truncated ZIP responses preserve the durable checkpoint cache and clean all stages", async (t) => { + const f = await fixture(t, "truncated-download"); + mkdirSync(f.destination, { recursive: true }); + const existing = checkpointName(8, "b"); + writeFileSync(path.join(f.destination, existing), "durable\n"); + const archive = ledgerZip(f.root, "truncated", 9, "c"); + const result = invoke(f, { + artifactsByRun: { 800: [artifact(808, 800, "2026-07-14T10:05:00Z")] }, + downloadMode: "truncated", + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 808: archive }, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /retry budget exhausted/u); + assert.equal(readFileSync(f.downloadState, "utf8"), "4"); + assert.deepEqual(restoredNames(f), [existing]); + assert.equal(readFileSync(path.join(f.destination, existing), "utf8"), "durable\n"); + assert.equal(readdirSync(f.root).some((name) => name.startsWith(".destination.")), false); +}); + +test("a valid-looking ZIP with the wrong immutable digest is never restored", async (t) => { + const f = await fixture(t, "identity-mismatch"); + mkdirSync(f.destination, { recursive: true }); + const existing = checkpointName(11, "e"); + writeFileSync(path.join(f.destination, existing), "durable\n"); + const archive = ledgerZip(f.root, "identity-mismatch", 12, "f"); + const result = invoke(f, { + artifactsByRun: { 800: [artifact(1001, 800, "2026-07-14T10:05:00Z")] }, + downloadMode: "identity-mismatch", + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 1001: archive }, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /transport identity mismatch.*retry budget exhausted|retry budget exhausted.*transport identity mismatch/su); + assert.equal(readFileSync(f.downloadState, "utf8"), "4"); + assert.deepEqual(restoredNames(f), [existing]); + assert.equal(readdirSync(f.root).some((name) => name.startsWith(".destination.")), false); +}); + +test("checkpoint collision is validated before atomic promotion and preserves prior bytes", async (t) => { + const f = await fixture(t, "collision"); + mkdirSync(f.destination, { recursive: true }); + const name = checkpointName(10, "d"); + writeFileSync(path.join(f.destination, name), "local-different-bytes\n"); + const archive = ledgerZip(f.root, "collision", 10, "d"); + const result = invoke(f, { + artifactsByRun: { 800: [artifact(909, 800, "2026-07-14T10:05:00Z")] }, + runs: [{ databaseId: 800, createdAt: "2026-07-14T10:00:00Z", status: "completed", conclusion: "failure" }], + zipsByArtifact: { 909: archive }, + }); + assert.equal(result.status, 1); + assert.match(result.stderr, /prior checkpoint conflicts/u); + assert.equal(readFileSync(path.join(f.destination, name), "utf8"), "local-different-bytes\n"); + assert.equal(existsSync(path.join(f.destination, ".artifact.zip")), false); + assert.equal(readdirSync(f.root).some((entry) => entry.startsWith(".destination.")), false); +}); + +test("exact continuation restore installs only immutable checkpoint entries and rejects collisions", async (t) => { + const f = await fixture(t, "exact-continuation"); + const name = checkpointName(13, "a"); + restoreExactContinuationLedger({ + checkpointEntries: [{ name, bytes: Buffer.from("exact\n") }], + }, f.destination); + assert.deepEqual(restoredNames(f), [name]); + assert.equal(readFileSync(path.join(f.destination, name), "utf8"), "exact\n"); + assert.throws( + () => restoreExactContinuationLedger({ + checkpointEntries: [{ name, bytes: Buffer.from("changed\n") }], + }, f.destination), + /conflicts with local/u, + ); + assert.equal(readFileSync(path.join(f.destination, name), "utf8"), "exact\n"); +}); diff --git a/.github/scripts/download-build-artifacts.mjs b/.github/scripts/download-build-artifacts.mjs new file mode 100644 index 00000000..f22a432f --- /dev/null +++ b/.github/scripts/download-build-artifacts.mjs @@ -0,0 +1,817 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + chmodSync, + closeSync, + copyFileSync, + createReadStream, + existsSync, + lstatSync, + mkdirSync, + openSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + statSync, + utimesSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { + createSiblingStage, + promoteDirectory, + removeTemporaryPath, + stageExistingDirectory, +} from "../../tools/release/atomic-directory.mjs"; +import { + RetryableReadError, + isRetryableGitHubReadError, + retryReadOperationSync, + runGitHubPaginatedJsonSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; +import { reserveGitHubCoreRequestSync } from "../../tools/release/github-core-request-journal.mjs"; + +const USAGE = + "usage: download-build-artifacts.mjs [--run-id ] [--job ] " + + "[--artifact-metadata-json ] --artifact [--artifact ...]"; +const SNAPSHOT_SCHEMA = "oliphaunt-github-actions-run-snapshot-v1"; +const FULL_SHA = /^[0-9a-f]{40}$/u; +const MAX_CAPTURE_BYTES = 128 * 1024 * 1024; +const ARTIFACT_DOWNLOAD_TOTAL_DEADLINE_MS = 65 * 60_000; +const ARTIFACT_DOWNLOAD_ATTEMPT_TIMEOUT_MS = 40 * 60_000; + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +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: "", + artifactMetadataJson: "", + }; + 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 if (arg === "--artifact-metadata-json") { + args.artifactMetadataJson = valueAfter(rest, index, "--artifact-metadata-json requires JSON"); + index += 2; + } else { + fail(`unknown argument: ${arg}`, 2); + } + } + if (args.artifacts.length === 0) { + fail("at least one --artifact is required", 2); + } + if (new Set(args.artifacts).size !== args.artifacts.length) { + fail("each --artifact identity must be requested exactly once", 2); + } + args.expectedArtifactMetadata = parseExpectedArtifactMetadata(args.artifactMetadataJson, args.artifacts); + if (args.expectedArtifactMetadata !== null && args.selectedRunId === "") { + fail("--artifact-metadata-json requires --run-id so immutable artifact IDs cannot drift between runs", 2); + } + return args; +} + +function parseExpectedArtifactMetadata(raw, requested) { + if (raw === "") return null; + let rows; + try { + rows = JSON.parse(raw); + } catch (cause) { + fail(`--artifact-metadata-json must be strict JSON: ${cause.message}`, 2); + } + if (!Array.isArray(rows) || rows.length < requested.length) { + fail("--artifact-metadata-json must contain every requested artifact", 2); + } + const expectedNames = [...requested].sort(); + const normalized = rows.map((row) => { + if ( + row === null + || Array.isArray(row) + || typeof row !== "object" + || JSON.stringify(Object.keys(row).sort()) !== JSON.stringify(["digest", "id", "name", "size"]) + || typeof row.name !== "string" + || !Number.isSafeInteger(row.id) + || row.id < 1 + || !Number.isSafeInteger(row.size) + || row.size < 1 + || typeof row.digest !== "string" + || !/^sha256:[0-9a-f]{64}$/u.test(row.digest) + ) { + fail("--artifact-metadata-json contains a malformed immutable artifact identity", 2); + } + return { digest: row.digest, id: row.id, name: row.name, size: row.size }; + }).sort((left, right) => compareText(left.name, right.name)); + if (new Set(normalized.map(({ name }) => name)).size !== normalized.length) { + fail("--artifact-metadata-json must contain unique artifact names", 2); + } + const all = new Map(normalized.map((row) => [row.name, row])); + if (expectedNames.some((name) => !all.has(name))) { + fail("--artifact-metadata-json is missing a requested artifact name", 2); + } + // A gate can authorize a larger immutable artifact set than one consumer + // needs. Keep every row schema-validated while returning only the explicitly + // requested subset; this avoids re-downloading a large capsule merely to + // install its companion lock. + return new Map(expectedNames.map((name) => [name, all.get(name)])); +} + +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 gh(args, label, options = {}) { + return runGitHubReadSync(args, { label, ...options }); +} + +function parsedJson(text, label) { + try { + return JSON.parse(text); + } catch (error) { + throw new Error(`${label} returned invalid JSON: ${error.message}`); + } +} + +function safeRunId(value) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered)) throw new Error("workflow run id must be a positive integer"); + return rendered; +} + +function snapshotFile(runId) { + const directory = process.env.OLIPHAUNT_GITHUB_RUN_SNAPSHOT_DIR?.trim() ?? ""; + if (directory === "") return null; + return path.join(path.resolve(directory), `run-${safeRunId(runId)}.json`); +} + +function assertRegularSnapshotFile(file) { + const metadata = lstatSync(file, { throwIfNoEntry: false }); + if (metadata !== undefined && (!metadata.isFile() || metadata.isSymbolicLink())) { + throw new Error(`GitHub run snapshot must be an absent or regular non-symlink file: ${file}`); + } +} + +function normalizeArtifact(entry, runId) { + if ( + entry === null + || Array.isArray(entry) + || typeof entry !== "object" + || typeof entry.name !== "string" + || entry.name.length === 0 + || !Number.isSafeInteger(entry.id) + || entry.id <= 0 + || !Number.isSafeInteger(entry.size_in_bytes) + || entry.size_in_bytes < 0 + || typeof entry.expired !== "boolean" + || typeof entry.digest !== "string" + || !/^sha256:[0-9a-f]{64}$/u.test(entry.digest) + ) { + throw new Error(`artifact inventory for run ${runId} contains malformed immutable metadata`); + } + if (entry.workflow_run?.id !== undefined && String(entry.workflow_run.id) !== String(runId)) { + throw new Error(`artifact ${entry.id} is not bound to workflow run ${runId}`); + } + return { + digest: entry.digest, + expired: entry.expired, + id: entry.id, + name: entry.name, + size_in_bytes: entry.size_in_bytes, + }; +} + +function artifactRecords(repo, runId, options = {}) { + const records = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/runs/${runId}/artifacts`, + { + ...options, + itemsField: "artifacts", + label: `artifact inventory for run ${runId}`, + }, + ); + return records.map((entry) => normalizeArtifact(entry, runId)); +} + +function exactArtifact(records, runId, name) { + const matches = records.filter((entry) => entry.name === name && entry.expired === false); + if (matches.length !== 1) { + throw new Error( + `${runId} must contain exactly one non-expired artifact named ${name}; found ${matches.length}`, + ); + } + return matches[0]; +} + +function exactExpectedArtifact(records, runId, name, expectedMetadata = null) { + const observed = exactArtifact(records, runId, name); + const expected = expectedMetadata?.get(name); + if ( + expected !== undefined + && ( + observed.id !== expected.id + || observed.digest !== expected.digest + || observed.size_in_bytes !== expected.size + ) + ) { + throw new Error( + `${runId}/${name} immutable identity drifted: expected id=${expected.id} size=${expected.size} digest=${expected.digest}; ` + + `observed id=${observed.id} size=${observed.size_in_bytes} digest=${observed.digest}`, + ); + } + return observed; +} + +function jobRecords(repo, runId) { + const records = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/runs/${runId}/jobs?filter=latest`, + { + itemsField: "jobs", + label: `jobs for run ${runId}`, + }, + ); + const jobs = []; + for (const job of records) { + if ( + job === null + || Array.isArray(job) + || typeof job !== "object" + || !Number.isSafeInteger(job.id) + || job.id <= 0 + || typeof job.name !== "string" + || job.name.length === 0 + || typeof job.status !== "string" + || (job.conclusion !== null && typeof job.conclusion !== "string") + || !Number.isSafeInteger(job.run_attempt) + || job.run_attempt <= 0 + ) { + throw new Error(`job inventory for run ${runId} contains malformed metadata`); + } + jobs.push({ + conclusion: job.conclusion, + id: job.id, + name: job.name, + run_attempt: job.run_attempt, + status: job.status, + }); + } + return jobs; +} + +function validateSnapshot(snapshot) { + if ( + snapshot === null + || Array.isArray(snapshot) + || typeof snapshot !== "object" + || snapshot.schema !== SNAPSHOT_SCHEMA + || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(snapshot.repo) + || !/^[1-9][0-9]*$/u.test(snapshot.runId) + || !FULL_SHA.test(snapshot.headSha) + || !Number.isSafeInteger(snapshot.workflowId) + || snapshot.workflowId <= 0 + || typeof snapshot.workflowName !== "string" + || snapshot.workflowName.length === 0 + || snapshot.status !== "completed" + || snapshot.conclusion !== "success" + || !Number.isSafeInteger(snapshot.runAttempt) + || snapshot.runAttempt <= 0 + || !Array.isArray(snapshot.jobs) + || !Array.isArray(snapshot.artifacts) + ) { + throw new Error("cached GitHub workflow run snapshot is malformed or not completed/success"); + } + const artifacts = snapshot.artifacts.map((entry) => normalizeArtifact(entry, snapshot.runId)); + const artifactIds = new Set(); + for (const artifact of artifacts) { + if (artifactIds.has(artifact.id)) throw new Error("cached GitHub workflow run snapshot repeats an artifact id"); + artifactIds.add(artifact.id); + } + for (const job of snapshot.jobs) { + if ( + job === null + || Array.isArray(job) + || typeof job !== "object" + || !Number.isSafeInteger(job.id) + || job.id <= 0 + || typeof job.name !== "string" + || typeof job.status !== "string" + || (job.conclusion !== null && typeof job.conclusion !== "string") + || job.run_attempt !== snapshot.runAttempt + ) { + throw new Error("cached GitHub workflow run snapshot contains malformed or cross-attempt jobs"); + } + } + return { ...snapshot, artifacts }; +} + +function createRunSnapshot(repo, runId) { + const data = parsedJson( + gh(["api", `repos/${repo}/actions/runs/${runId}`], `metadata for run ${runId}`), + `metadata for run ${runId}`, + ); + if ( + String(data?.id ?? "") !== String(runId) + || !FULL_SHA.test(data?.head_sha ?? "") + || !Number.isSafeInteger(data?.workflow_id) + || data.workflow_id <= 0 + || data.status !== "completed" + || data.conclusion !== "success" + || !Number.isSafeInteger(data.run_attempt) + || data.run_attempt <= 0 + ) { + throw new Error(`workflow run ${runId} is malformed or not completed/success`); + } + const workflow = parsedJson( + gh(["api", `repos/${repo}/actions/workflows/${data.workflow_id}`], `workflow ${data.workflow_id} metadata`), + `workflow ${data.workflow_id} metadata`, + ); + if (workflow?.id !== data.workflow_id || typeof workflow?.name !== "string" || workflow.name.length === 0) { + throw new Error(`workflow ${data.workflow_id} metadata is malformed`); + } + return validateSnapshot({ + artifacts: artifactRecords(repo, runId), + conclusion: data.conclusion, + headSha: data.head_sha.toLowerCase(), + jobs: jobRecords(repo, runId), + repo, + runAttempt: data.run_attempt, + runId: String(runId), + schema: SNAPSHOT_SCHEMA, + status: data.status, + workflowId: data.workflow_id, + workflowName: workflow.name, + }); +} + +function runSnapshot(repo, runId) { + const file = snapshotFile(runId); + if (file !== null && existsSync(file)) { + assertRegularSnapshotFile(file); + const snapshot = validateSnapshot(parsedJson(readFileSync(file, "utf8"), `cached run ${runId} snapshot`)); + if (snapshot.repo !== repo || snapshot.runId !== String(runId)) { + throw new Error(`cached run ${runId} snapshot belongs to a different repository or run`); + } + return snapshot; + } + const snapshot = createRunSnapshot(repo, runId); + if (file !== null) { + mkdirSync(path.dirname(file), { recursive: true }); + assertRegularSnapshotFile(file); + const temporary = `${file}.tmp-${process.pid}`; + assertRegularSnapshotFile(temporary); + try { + writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { flag: "wx", mode: 0o600 }); + renameSync(temporary, file); + } finally { + rmSync(temporary, { force: true }); + } + } + return snapshot; +} + +function requiredJobSuccess(snapshot, requiredJob) { + if (requiredJob === "") { + return true; + } + const matches = snapshot.jobs.filter((candidate) => candidate.name === requiredJob); + return matches.length === 1 && matches[0]?.conclusion === "success"; +} + +function runMatchesRequest(snapshot, workflow, sha) { + return snapshot.headSha === sha.toLowerCase() && snapshot.workflowName === workflow; +} + +function candidateRunIds(repo, workflow, sha) { + const workflows = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/workflows`, + { + itemsField: "workflows", + label: `${workflow} workflow identity inventory`, + maxBuffer: MAX_CAPTURE_BYTES, + }, + ); + const matches = workflows.filter((row) => row?.name === workflow); + if (matches.length !== 1 || !Number.isSafeInteger(matches[0]?.id) || matches[0].id < 1) { + throw new Error(`expected exactly one workflow named ${workflow}; found ${matches.length}`); + } + const runs = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/workflows/${matches[0].id}/runs?head_sha=${encodeURIComponent(sha)}`, + { + itemsField: "workflow_runs", + label: `exact-SHA workflow runs for ${workflow} at ${sha}`, + maxBuffer: MAX_CAPTURE_BYTES, + }, + ); + const ids = new Set(); + const result = []; + for (const run of runs) { + const id = safeRunId(run?.id); + if ( + ids.has(id) + || run.head_sha !== sha + || run.workflow_id !== matches[0].id + || typeof run.status !== "string" + || ![null, "success", "failure", "cancelled", "timed_out", "action_required", "neutral", "skipped", "stale", "startup_failure"].includes(run.conclusion) + ) { + throw new Error(`workflow run inventory for ${workflow} at ${sha} contains malformed, duplicate, or inexact metadata`); + } + ids.add(id); + if (run.status === "completed" && run.conclusion === "success") result.push(id); + } + return result; +} + +function sortedFiles(root) { + const files = []; + function visit(directory) { + const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => + compareText(left.name, right.name), + ); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + const entryStat = lstatSync(entryPath); + if (entryStat.isSymbolicLink()) { + throw new Error(`downloaded artifact contains symbolic link ${path.relative(root, entryPath)}`); + } + if (entryStat.isDirectory()) { + visit(entryPath); + } else if (entryStat.isFile()) { + files.push(entryPath); + } else { + throw new Error(`downloaded artifact contains special file ${path.relative(root, 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(process.execPath, [".github/scripts/merge-checksum-manifest.mjs", existing, incoming], { + stdio: "inherit", + env: process.env, + }); + return !result.error && result.status === 0; +} + +function validateDownloadedArtifact(artifact, directory) { + const files = sortedFiles(directory); + if (files.length === 0) { + throw new RetryableReadError(`artifact ${artifact} downloaded with an empty file envelope`); + } + for (const file of files) { + const relative = path.relative(directory, file); + if (relative.startsWith("..") || path.isAbsolute(relative)) { + throw new Error(`artifact ${artifact} escaped its download directory`); + } + const stat = lstatSync(file); + if (!stat.isFile() || stat.isSymbolicLink()) { + throw new Error(`artifact ${artifact} contains unsupported entry ${relative}`); + } + } +} + +function validateZipMembers(archive, artifact) { + const result = spawnSync("unzip", ["-Z1", archive], { + encoding: "utf8", + env: process.env, + maxBuffer: MAX_CAPTURE_BYTES, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0) { + throw new RetryableReadError(`artifact ${artifact} is not a readable ZIP archive`, { + cause: result.error ?? new Error(result.stderr.trim()), + }); + } + const members = result.stdout.split(/\r?\n/u).filter(Boolean); + if (members.length === 0) throw new RetryableReadError(`artifact ${artifact} ZIP archive is empty`); + const seen = new Set(); + for (const member of members) { + const normalized = member.endsWith("/") ? member.slice(0, -1) : member; + if ( + normalized === "" + || member.includes("\\") + || member.startsWith("/") + || normalized.split("/").some((segment) => segment === "" || segment === "." || segment === "..") + || /[\u0000-\u001f\u007f]/u.test(member) + || seen.has(member) + ) { + throw new Error(`artifact ${artifact} ZIP contains unsafe or duplicate member ${JSON.stringify(member)}`); + } + seen.add(member); + } + const typeCheck = spawnSync("python3", ["-c", ` +import stat +import sys +import zipfile + +with zipfile.ZipFile(sys.argv[1], "r") as archive: + for entry in archive.infolist(): + mode = (entry.external_attr >> 16) & 0xFFFF + kind = stat.S_IFMT(mode) + if entry.flag_bits & 0x1: + raise SystemExit("encrypted ZIP member") + if kind not in (0, stat.S_IFREG, stat.S_IFDIR): + raise SystemExit("symbolic-link or special ZIP member") +`, archive], { + encoding: "utf8", + env: process.env, + maxBuffer: 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + if (typeCheck.error !== undefined || typeCheck.status !== 0) { + throw new Error( + `artifact ${artifact} ZIP contains an encrypted, symbolic-link, or special member: ` + + (typeCheck.error?.message ?? typeCheck.stderr.trim()), + ); + } +} + +function extractZip(archive, directory, artifact) { + validateZipMembers(archive, artifact); + const result = spawnSync("unzip", ["-q", archive, "-d", directory], { + encoding: "utf8", + env: process.env, + maxBuffer: MAX_CAPTURE_BYTES, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0) { + throw new RetryableReadError(`artifact ${artifact} ZIP extraction failed`, { + cause: result.error ?? new Error(result.stderr.trim()), + }); + } +} + +function downloadArchiveOnce(repo, identity, archive, attemptTimeoutMs) { + let descriptor; + try { + descriptor = openSync(archive, "wx", 0o600); + reserveGitHubCoreRequestSync({ + label: `download Actions artifact ${identity.id}`, + }); + const result = spawnSync( + "gh", + [ + "api", + "-H", + "Accept: application/vnd.github+json", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + `repos/${repo}/actions/artifacts/${identity.id}/zip`, + ], + { + encoding: "utf8", + env: process.env, + maxBuffer: 4 * 1024 * 1024, + stdio: ["ignore", descriptor, "pipe"], + timeout: attemptTimeoutMs, + windowsHide: true, + }, + ); + closeSync(descriptor); + descriptor = undefined; + if (result.error !== undefined || result.status !== 0) { + const error = new Error(`exact-ID artifact download failed: ${result.stderr?.trim() ?? ""}`); + error.code = result.error?.code; + error.status = result.status; + error.retryable = isRetryableGitHubReadError(error); + throw error; + } + } catch (error) { + if (descriptor !== undefined) closeSync(descriptor); + rmSync(archive, { force: true }); + throw error; + } +} + +function downloadArtifact(repo, runId, artifact, expectedIdentity, destination, sharedDeadlineMs) { + let lastError; + for (let attempt = 1; attempt <= 2; attempt += 1) { + const remainingMs = sharedDeadlineMs - Date.now(); + if (remainingMs <= 0) { + throw new RetryableReadError( + `shared exact-artifact download deadline exhausted after ${attempt - 1} attempt(s)`, + { cause: lastError }, + ); + } + const temporary = createSiblingStage(destination, `download-${artifact.replace(/[^A-Za-z0-9_.-]/gu, "-")}`); + const archive = path.join(temporary, ".artifact.zip"); + try { + downloadArchiveOnce( + repo, + expectedIdentity, + archive, + Math.max(1, Math.min(ARTIFACT_DOWNLOAD_ATTEMPT_TIMEOUT_MS, remainingMs)), + ); + const digestResult = spawnSync(process.execPath, ["-e", ` + const { createHash } = require("node:crypto"); + const { createReadStream } = require("node:fs"); + const stream = createReadStream(process.argv[1]); + const hash = createHash("sha256"); + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("end", () => process.stdout.write(hash.digest("hex"))); + `, archive], { encoding: "utf8", maxBuffer: 1024, stdio: ["ignore", "pipe", "pipe"] }); + if (digestResult.error !== undefined || digestResult.status !== 0) { + throw new RetryableReadError(`could not hash artifact ${artifact} ZIP`); + } + const actualSize = statSync(archive).size; + const actualDigest = digestResult.stdout.trim(); + const expectedDigest = expectedIdentity.digest.slice("sha256:".length); + if (actualSize !== expectedIdentity.size_in_bytes || actualDigest !== expectedDigest) { + throw new RetryableReadError( + `artifact ${artifact} ZIP identity mismatch: expected ${expectedIdentity.size_in_bytes}/${expectedDigest}, ` + + `got ${actualSize}/${actualDigest}`, + ); + } + extractZip(archive, temporary, artifact); + rmSync(archive, { force: true }); + validateDownloadedArtifact(artifact, temporary); + return temporary; + } catch (error) { + removeTemporaryPath(temporary); + lastError = error; + if (!isRetryableGitHubReadError(error)) { + throw new Error(`permanent read failure for exact-ID artifact ${artifact}: ${error.message}`, { cause: error }); + } + if (attempt === 2) { + throw new RetryableReadError( + `exact-ID artifact download retry budget exhausted after ${attempt} attempts: ${error.message}`, + { cause: error }, + ); + } + } + } + throw lastError; +} + +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; + const snapshot = runSnapshot(repo, runId); + if (!runMatchesRequest(snapshot, args.workflow, args.sha)) { + fail(`${args.workflow} run ${runId} does not belong to commit ${args.sha}`); + } + if (!requiredJobSuccess(snapshot, args.requiredJob)) { + fail(`${args.workflow} run ${runId} does not satisfy required job ${args.requiredJob || ""}`); + } + for (const artifact of args.artifacts) { + exactExpectedArtifact(snapshot.artifacts, runId, artifact, args.expectedArtifactMetadata); + } + return snapshot; + } + + for (const candidate of candidateRunIds(repo, args.workflow, args.sha)) { + const snapshot = runSnapshot(repo, candidate); + if (!runMatchesRequest(snapshot, args.workflow, args.sha)) { + continue; + } + if (!requiredJobSuccess(snapshot, args.requiredJob)) { + continue; + } + try { + for (const artifact of args.artifacts) exactExpectedArtifact(snapshot.artifacts, candidate, artifact); + return snapshot; + } catch { + // A successfully read inventory that lacks the exact envelope is not a matching 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(process.argv.slice(2)); + requireEnv("GH_TOKEN"); + const repo = requireEnv("GH_REPO"); + const snapshot = selectRunId(repo, args); + const runId = snapshot.runId; + const identities = new Map(); + for (const artifact of args.artifacts) { + identities.set( + artifact, + exactExpectedArtifact(snapshot.artifacts, runId, artifact, args.expectedArtifactMetadata), + ); + } + const sharedDownloadDeadlineMs = Date.now() + ARTIFACT_DOWNLOAD_TOTAL_DEADLINE_MS; + + const stage = stageExistingDirectory(args.destination, "merge"); + try { + for (const artifact of args.artifacts) { + console.log(`Downloading ${args.workflow} artifact ${artifact} from run ${runId}`); + const artifactDir = downloadArtifact( + repo, + runId, + artifact, + identities.get(artifact), + args.destination, + sharedDownloadDeadlineMs, + ); + try { + if (!(await mergeDownloadedArtifact(artifact, artifactDir, stage))) { + throw new Error(`artifact ${artifact} conflicts with the durable destination`); + } + } finally { + removeTemporaryPath(artifactDir); + } + } + promoteDirectory(stage, args.destination); + } catch (error) { + removeTemporaryPath(stage); + throw error; + } +} + +try { + await main(); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} diff --git a/.github/scripts/download-normal-publication-checkpoint.mjs b/.github/scripts/download-normal-publication-checkpoint.mjs new file mode 100644 index 00000000..8919a807 --- /dev/null +++ b/.github/scripts/download-normal-publication-checkpoint.mjs @@ -0,0 +1,487 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + appendFileSync, + closeSync, + existsSync, + fsyncSync, + lstatSync, + mkdirSync, + mkdtempSync, + openSync, + readFileSync, + renameSync, + rmSync, + statSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { + DEFAULT_NORMAL_PUBLICATION_CHECKPOINT, + openNormalPublicationCheckpoint, +} from "../../tools/release/normal-publication-checkpoint.mjs"; +import { normalPublicationPlan } from "../../tools/release/normal-publication-plan.mjs"; +import { + NORMAL_PUBLICATION_RECOVERY_DOWNLOAD_DEADLINE_MS, + NORMAL_PUBLICATION_RECOVERY_MAX_ATTEMPTS, + NORMAL_PUBLICATION_RECOVERY_MAX_CANDIDATES, + NORMAL_PUBLICATION_RECOVERY_MAX_PAGES_PER_INVENTORY, + NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, +} from "../../tools/release/normal-publication-recovery-contract.mjs"; +import { + assertPublicationLockSource, + loadPublicationLock, +} from "../../tools/release/publication-lock.mjs"; +import { + retryReadOperationSync, + runGitHubPaginatedJsonSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; +import { + parseContinuationJson, + sha256Bytes, + stableJson, + validateReleaseContinuationPointer, +} from "../../tools/release/release-continuation-contract.mjs"; +import { openContinuationEnvelope } from "./release-continuation-artifact.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024; +const ALLOWED_ZIP_MEMBERS = new Set([ + "normal-publication-checkpoint.json", + "normal-publication-plan.json", + "registry-integrity-receipts.json", +]); + +function fail(message) { + throw new Error(`download-normal-publication-checkpoint: ${message}`); +} + +function required(name) { + const value = process.env[name]?.trim(); + if (!value) fail(`${name} is required`); + return value; +} + +function positiveInteger(value, context) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered) || !Number.isSafeInteger(Number(rendered))) { + fail(`${context} must be a positive safe integer`); + } + return rendered; +} + +function json(value, context) { + try { return JSON.parse(value); } catch (cause) { fail(`${context} is not strict JSON: ${cause.message}`); } +} + +function command(commandName, args, { binary = false, maxBuffer = MAX_ARTIFACT_BYTES } = {}) { + const result = spawnSync(commandName, args, { + cwd: ROOT, + env: process.env, + encoding: binary ? null : "utf8", + maxBuffer, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : result.stdout; + fail(`${commandName} ${args.join(" ")} failed: ${(stderr || stdout || result.error?.message || "").trim()}`); + } + return result.stdout; +} + +function ghJson(repo, endpoint, label) { + return json(runGitHubReadSync(["api", `repos/${repo}/${endpoint}`], { + cwd: ROOT, + label, + maxBuffer: MAX_ARTIFACT_BYTES, + attemptTimeoutMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + deadlineMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + maxAttempts: NORMAL_PUBLICATION_RECOVERY_MAX_ATTEMPTS, + }), label); +} + +function artifactName(sha) { + return `normal-publication-recovery-${sha}`; +} + +function validateCurrentRun(metadata, { currentRunId, sha }) { + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail("current Release run metadata must be an object"); + } + if (String(metadata.id ?? "") !== currentRunId) fail("current Release run metadata has the wrong run id"); + if (metadata.head_sha !== sha) fail("current Release run metadata has the wrong exact release SHA"); + if (metadata.event !== "workflow_dispatch") fail("current Release run is not a workflow_dispatch"); + return positiveInteger(metadata.workflow_id, "current Release workflow id"); +} + +export function eligibleRecoveryArtifacts({ + runs, + artifacts, + currentRunId, + workflowId, + sha, + name = artifactName(sha), +}) { + if (!Array.isArray(runs) || !Array.isArray(artifacts)) fail("run and artifact inventories must be lists"); + const eligibleRuns = new Set(); + const seenRuns = new Set(); + for (const run of runs) { + const runId = positiveInteger(run?.id, "exact-SHA Release run id"); + if (seenRuns.has(runId)) fail(`exact-SHA Release run inventory repeats run ${runId}`); + seenRuns.add(runId); + if ( + String(run.workflow_id ?? "") !== workflowId + || run.head_sha !== sha + || run.event !== "workflow_dispatch" + ) { + fail(`exact-SHA Release run inventory contains a run outside workflow ${workflowId} and SHA ${sha}`); + } + if (runId !== currentRunId && run.status === "completed") eligibleRuns.add(runId); + } + const seenArtifacts = new Set(); + const byRun = new Map(); + for (const artifact of artifacts) { + if (artifact === null || Array.isArray(artifact) || typeof artifact !== "object" || artifact.name !== name) { + fail(`named recovery artifact inventory contains malformed artifact metadata`); + } + const id = positiveInteger(artifact.id, "recovery artifact id"); + if (seenArtifacts.has(id)) fail(`recovery artifact inventory repeats immutable artifact id ${id}`); + seenArtifacts.add(id); + if ( + artifact.workflow_run === null + || Array.isArray(artifact.workflow_run) + || typeof artifact.workflow_run !== "object" + ) { + fail(`recovery artifact ${id} has no immutable workflow-run binding`); + } + const runId = positiveInteger(artifact.workflow_run.id, `recovery artifact ${id} run id`); + if (artifact.workflow_run.head_sha !== sha) continue; + if (!eligibleRuns.has(runId)) continue; + if (artifact.expired !== false) fail(`recovery artifact ${id} has expired or ambiguous expiry metadata`); + if (!Number.isSafeInteger(artifact.size_in_bytes) || artifact.size_in_bytes < 4 || artifact.size_in_bytes > MAX_ARTIFACT_BYTES) { + fail(`recovery artifact ${id} has an invalid bounded byte size`); + } + if (typeof artifact.digest !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(artifact.digest)) { + fail(`recovery artifact ${id} has an invalid immutable SHA-256 digest`); + } + const candidates = byRun.get(runId) ?? []; + candidates.push({ ...artifact, id, runId }); + byRun.set(runId, candidates); + } + for (const [runId, candidates] of byRun) { + if (candidates.length !== 1) { + fail(`completed Release run ${runId} exposes duplicate same-name recovery artifacts`); + } + } + const result = [...byRun.values()].flat(); + if (result.length > NORMAL_PUBLICATION_RECOVERY_MAX_CANDIDATES) { + fail( + `exact-SHA recovery has ${result.length} candidates; maximum audited inventory is ` + + `${NORMAL_PUBLICATION_RECOVERY_MAX_CANDIDATES}`, + ); + } + return result.sort((left, right) => BigInt(left.id) < BigInt(right.id) ? -1 : 1); +} + +function strictSuperset(left, right) { + return left.size > right.size && [...right].every((value) => left.has(value)); +} + +export function selectMaximalCheckpointCandidate(candidates) { + if (!Array.isArray(candidates) || candidates.length === 0) return null; + const byDigest = new Map(); + const completedSetDigests = new Map(); + for (const candidate of candidates) { + const checkpoint = candidate?.checkpoint; + if ( + checkpoint === null + || Array.isArray(checkpoint) + || typeof checkpoint !== "object" + || typeof checkpoint.checkpointDigest !== "string" + || !Array.isArray(checkpoint.completedOperations) + ) { + fail("validated recovery candidates must contain checkpoint identity and completed operations"); + } + const completed = new Set(checkpoint.completedOperations); + if (completed.size !== checkpoint.completedOperations.length) fail("recovery checkpoint repeats a completed operation"); + const completedKey = [...completed].sort().join("\0"); + const previousDigest = completedSetDigests.get(completedKey); + if (previousDigest !== undefined && previousDigest !== checkpoint.checkpointDigest) { + fail("recovery candidates conflict for the same completed operation set"); + } + completedSetDigests.set(completedKey, checkpoint.checkpointDigest); + const group = byDigest.get(checkpoint.checkpointDigest) ?? { candidates: [], completed }; + if (group.completed.size !== completed.size || [...completed].some((id) => !group.completed.has(id))) { + fail("one checkpoint digest is reused for conflicting completed operation sets"); + } + group.candidates.push(candidate); + byDigest.set(checkpoint.checkpointDigest, group); + } + const groups = [...byDigest.values()]; + const maximal = groups.filter((candidate) => !groups.some( + (other) => other !== candidate && strictSuperset(other.completed, candidate.completed), + )); + if (maximal.length !== 1) { + fail(`recovery candidates have ${maximal.length} incomparable maximal checkpoints`); + } + return maximal[0].candidates.sort( + (left, right) => BigInt(left.artifact.id) < BigInt(right.artifact.id) ? -1 : 1, + )[0]; +} + +function validateZipMembers(archive) { + const members = command("unzip", ["-Z1", archive]) + .split(/\r?\n/u) + .filter(Boolean); + if (members.length === 0) fail("recovery artifact ZIP is empty"); + const seen = new Set(); + for (const member of members) { + if ( + member.includes("/") + || member.includes("\\") + || /[\u0000-\u001f\u007f]/u.test(member) + || !ALLOWED_ZIP_MEMBERS.has(member) + || seen.has(member) + ) { + fail(`recovery artifact ZIP contains unexpected or repeated member ${JSON.stringify(member)}`); + } + seen.add(member); + } + if (!seen.has("normal-publication-checkpoint.json")) { + fail("recovery artifact ZIP is missing normal-publication-checkpoint.json"); + } +} + +function downloadArtifact(repo, artifact) { + return retryReadOperationSync( + `download normal publication recovery artifact ${artifact.id}`, + ({ attemptTimeoutMs }) => { + const result = runGitHubReadSync( + ["api", "-H", "X-GitHub-Api-Version: 2022-11-28", `repos/${repo}/actions/artifacts/${artifact.id}/zip`], + { + binary: true, + cwd: ROOT, + label: `download normal publication recovery artifact ${artifact.id}`, + maxBuffer: MAX_ARTIFACT_BYTES, + attemptTimeoutMs, + maxAttempts: 1, + baseDelayMs: 0, + maxDelayMs: 0, + deadlineMs: attemptTimeoutMs, + }, + ); + if (!Buffer.isBuffer(result) || result.length < 4 || result[0] !== 0x50 || result[1] !== 0x4b) { + fail(`recovery artifact ${artifact.id} did not return a ZIP archive`); + } + const observed = createHash("sha256").update(result).digest("hex"); + if (result.length !== artifact.size_in_bytes || `sha256:${observed}` !== artifact.digest) { + fail(`recovery artifact ${artifact.id} transport size/digest does not match immutable metadata`); + } + return result; + }, + { + attemptTimeoutMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + deadlineMs: NORMAL_PUBLICATION_RECOVERY_DOWNLOAD_DEADLINE_MS, + maxAttempts: NORMAL_PUBLICATION_RECOVERY_MAX_ATTEMPTS, + baseDelayMs: 500, + maxDelayMs: 2_000, + }, + ); +} + +function checkpointCandidate(artifact, bytes, { lock, products, plan }) { + const directory = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-normal-publication-recovery-")); + try { + const archive = path.join(directory, "artifact.zip"); + writeFileSync(archive, bytes, { flag: "wx", mode: 0o600 }); + validateZipMembers(archive); + const checkpointBytes = command( + "unzip", + ["-p", archive, "normal-publication-checkpoint.json"], + { binary: true, maxBuffer: MAX_ARTIFACT_BYTES }, + ); + if (!Buffer.isBuffer(checkpointBytes) || checkpointBytes.length === 0 || checkpointBytes.length > MAX_ARTIFACT_BYTES) { + fail(`recovery artifact ${artifact.id} contains an invalid checkpoint size`); + } + const checkpointFile = path.join(directory, "normal-publication-checkpoint.json"); + writeFileSync(checkpointFile, checkpointBytes, { flag: "wx", mode: 0o600 }); + const manager = openNormalPublicationCheckpoint({ file: checkpointFile, lock, products, plan }); + return { artifact, checkpoint: manager.checkpoint, checkpointBytes }; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +function atomicRestore(file, bytes) { + const absolute = path.resolve(file); + if (existsSync(absolute)) fail(`refusing to replace pre-existing local checkpoint ${file}`); + mkdirSync(path.dirname(absolute), { recursive: true }); + const temporary = `${absolute}.restore-${process.pid}`; + let descriptor; + try { + descriptor = openSync(temporary, "wx", 0o600); + writeFileSync(descriptor, bytes); + fsyncSync(descriptor); + closeSync(descriptor); + descriptor = undefined; + renameSync(temporary, absolute); + } finally { + if (descriptor !== undefined) closeSync(descriptor); + try { unlinkSync(temporary); } catch {} + } + if (!statSync(absolute).isFile()) fail(`atomic checkpoint restore did not create ${file}`); +} + +function productsFromEnvironment() { + const value = json(required("PRODUCTS_JSON"), "PRODUCTS_JSON"); + if ( + !Array.isArray(value) + || value.length === 0 + || value.some((product) => typeof product !== "string" || product.length === 0) + || new Set(value).size !== value.length + ) { + fail("PRODUCTS_JSON must be a nonempty unique string list"); + } + return value; +} + +export async function main() { + const repo = required("GH_REPO"); + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repo)) fail("GH_REPO must be owner/repository"); + const sha = required("RELEASE_HEAD_SHA"); + if (!/^[0-9a-f]{40}$/u.test(sha)) fail("RELEASE_HEAD_SHA must be a full lowercase commit SHA"); + const currentRunId = positiveInteger(required("GITHUB_RUN_ID"), "GITHUB_RUN_ID"); + const destination = path.resolve( + ROOT, + process.env.NORMAL_PUBLICATION_CHECKPOINT_PATH || DEFAULT_NORMAL_PUBLICATION_CHECKPOINT, + ); + const lockFile = path.resolve(ROOT, required("PUBLICATION_LOCK_PATH")); + const products = productsFromEnvironment(); + const lock = loadPublicationLock(lockFile); + assertPublicationLockSource(lock, sha); + const plan = normalPublicationPlan(lock, products); + const rawPointer = process.env.RELEASE_CONTINUATION_POINTER?.trim() ?? ""; + if (rawPointer !== "") { + if (rawPointer.length > 32 * 1024) fail("RELEASE_CONTINUATION_POINTER exceeds 32 KiB"); + const pointer = validateReleaseContinuationPointer( + parseContinuationJson(rawPointer, "RELEASE_CONTINUATION_POINTER"), + { operation: "publish", releaseCommit: sha }, + ); + const archive = path.resolve(required("RELEASE_CONTINUATION_ARCHIVE")); + const archiveMetadata = lstatSync(archive, { throwIfNoEntry: false }); + if ( + !archiveMetadata?.isFile() + || archiveMetadata.isSymbolicLink() + || archiveMetadata.size !== pointer.artifact.size + ) { + fail("cached exact continuation archive has the wrong file identity"); + } + const bytes = readFileSync(archive); + if (`sha256:${sha256Bytes(bytes)}` !== pointer.artifact.digest) { + fail("cached exact continuation archive digest differs from its pointer"); + } + const envelope = openContinuationEnvelope(bytes, pointer); + const expectedLock = { + catalogDigest: lock.catalogDigest, + lockDigest: lock.lockDigest, + packageEnvelopeDigest: lock.packageEnvelopeDigest, + }; + if ( + stableJson(envelope.contract.lock) !== stableJson(expectedLock) + || stableJson(envelope.contract.products) !== stableJson( + [...products].sort((left, right) => left < right ? -1 : left > right ? 1 : 0), + ) + || envelope.contract.source.tree !== lock.source.tree + ) { + fail("exact continuation contract disagrees with the installed publication lock and release plan"); + } + const temporary = path.join(os.tmpdir(), `oliphaunt-exact-normal-checkpoint-${process.pid}.json`); + try { + writeFileSync(temporary, envelope.stateBytes, { flag: "wx", mode: 0o600 }); + openNormalPublicationCheckpoint({ file: temporary, lock, products, plan }); + } finally { + rmSync(temporary, { force: true }); + } + atomicRestore(destination, envelope.stateBytes); + if (process.env.GITHUB_OUTPUT) { + appendFileSync( + process.env.GITHUB_OUTPUT, + `found=true\nrun_id=${pointer.parentRunId}\nartifact_id=${pointer.artifact.id}\n` + + `completed_operations=${envelope.executionResult.completedIds.length}\n` + + `generation=${pointer.generation}\n`, + ); + } + console.log( + `restored exact normal-publication continuation generation ${pointer.generation}/${pointer.maxGenerations} ` + + `from parent run ${pointer.parentRunId}, artifact ${pointer.artifact.id}`, + ); + return; + } + const current = ghJson(repo, `actions/runs/${currentRunId}`, `current Release run ${currentRunId}`); + const workflowId = validateCurrentRun(current, { currentRunId, sha }); + const runs = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/workflows/${workflowId}/runs?head_sha=${encodeURIComponent(sha)}&event=workflow_dispatch`, + { + cwd: ROOT, + itemsField: "workflow_runs", + label: `exact-SHA Release workflow ${workflowId} run inventory`, + maxBuffer: MAX_ARTIFACT_BYTES, + maxPages: NORMAL_PUBLICATION_RECOVERY_MAX_PAGES_PER_INVENTORY, + attemptTimeoutMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + deadlineMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + maxAttempts: NORMAL_PUBLICATION_RECOVERY_MAX_ATTEMPTS, + }, + ); + const name = artifactName(sha); + const artifacts = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/artifacts?name=${encodeURIComponent(name)}`, + { + cwd: ROOT, + itemsField: "artifacts", + label: `repository ${name} artifact inventory`, + maxBuffer: MAX_ARTIFACT_BYTES, + maxPages: NORMAL_PUBLICATION_RECOVERY_MAX_PAGES_PER_INVENTORY, + attemptTimeoutMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + deadlineMs: NORMAL_PUBLICATION_RECOVERY_READ_DEADLINE_MS, + maxAttempts: NORMAL_PUBLICATION_RECOVERY_MAX_ATTEMPTS, + }, + ); + const inventory = eligibleRecoveryArtifacts({ runs, artifacts, currentRunId, workflowId, sha, name }); + if (inventory.length === 0) { + if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, "found=false\n"); + console.log("no earlier exact-SHA normal-publication checkpoint exists; starting a new checkpoint"); + return; + } + const candidates = inventory.map((artifact) => checkpointCandidate( + artifact, + downloadArtifact(repo, artifact), + { lock, products, plan }, + )); + const selected = selectMaximalCheckpointCandidate(candidates); + atomicRestore(destination, selected.checkpointBytes); + if (process.env.GITHUB_OUTPUT) { + appendFileSync( + process.env.GITHUB_OUTPUT, + `found=true\nrun_id=${selected.artifact.runId}\nartifact_id=${selected.artifact.id}\n` + + `completed_operations=${selected.checkpoint.completedOperations.length}\n`, + ); + } + console.log( + `restored ${selected.checkpoint.completedOperations.length} completed normal-publication operations ` + + `from exact-SHA Release run ${selected.artifact.runId}, artifact ${selected.artifact.id}`, + ); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((cause) => { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + }); +} 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/inspect-release-continuation.mjs b/.github/scripts/inspect-release-continuation.mjs new file mode 100644 index 00000000..5b510f3f --- /dev/null +++ b/.github/scripts/inspect-release-continuation.mjs @@ -0,0 +1,182 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; +import { + appendFileSync, + closeSync, + mkdirSync, + openSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import { + parseContinuationJson, + stableJson, + validateReleaseContinuationPointer, +} from "../../tools/release/release-continuation-contract.mjs"; +import { + currentRunMetadata, + openContinuationEnvelope, + readContinuationAuthorization, + readExactContinuationArtifact, + validateContinuationRunLineage, +} from "./release-continuation-artifact.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +function error(message) { + return new Error(`inspect-release-continuation: ${message}`); +} + +function required(name, environment = process.env) { + const value = environment[name]?.trim(); + if (!value) throw error(`${name} is required`); + return value; +} + +function positiveInteger(value, context) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered) || !Number.isSafeInteger(Number(rendered))) { + throw error(`${context} must be a positive safe integer`); + } + return Number(rendered); +} + +function json(raw, context) { + try { return JSON.parse(raw); } catch (cause) { throw error(`${context} must be strict JSON: ${cause.message}`); } +} + +function products(raw) { + const value = json(raw, "PRODUCTS_JSON"); + if ( + !Array.isArray(value) + || value.length === 0 + || value.some((item) => typeof item !== "string" || item.length === 0) + || new Set(value).size !== value.length + ) { + throw error("PRODUCTS_JSON must be a nonempty unique string list"); + } + return [...value].sort((left, right) => left < right ? -1 : left > right ? 1 : 0); +} + +function gitTree(releaseCommit) { + const result = spawnSync("git", ["rev-parse", `${releaseCommit}^{tree}`], { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0 || !/^[0-9a-f]{40,64}\s*$/u.test(result.stdout)) { + throw error(`cannot resolve exact release tree: ${(result.stderr || result.error?.message || "").trim()}`); + } + return result.stdout.trim(); +} + +function atomicBytes(file, bytes) { + const absolute = path.resolve(file); + mkdirSync(path.dirname(absolute), { recursive: true }); + const temporary = `${absolute}.tmp-${process.pid}`; + let descriptor; + try { + descriptor = openSync(temporary, "wx", 0o600); + writeFileSync(descriptor, bytes); + closeSync(descriptor); + descriptor = undefined; + renameSync(temporary, absolute); + } finally { + if (descriptor !== undefined) closeSync(descriptor); + try { unlinkSync(temporary); } catch {} + } +} + +function emit(output, key, value) { + appendFileSync(output, `${key}=${typeof value === "string" ? value : JSON.stringify(value)}\n`); +} + +export async function main(environment = process.env) { + const output = environment.GITHUB_OUTPUT; + const rawPointer = environment.RELEASE_CONTINUATION_POINTER?.trim() ?? ""; + if (rawPointer === "") { + if (output) emit(output, "found", "false"); + console.log("no automatic release continuation pointer was supplied"); + return null; + } + if (rawPointer.length > 32 * 1024) throw error("RELEASE_CONTINUATION_POINTER exceeds 32 KiB"); + const repo = required("GH_REPO", environment); + const operation = required("RELEASE_OPERATION", environment); + const releaseCommit = required("RELEASE_HEAD_SHA", environment); + const currentRunId = positiveInteger(required("GITHUB_RUN_ID", environment), "GITHUB_RUN_ID"); + const currentProducts = products(required("PRODUCTS_JSON", environment)); + const pointer = validateReleaseContinuationPointer( + parseContinuationJson(rawPointer, "RELEASE_CONTINUATION_POINTER"), + { operation, releaseCommit }, + ); + const exact = readExactContinuationArtifact(repo, pointer); + const current = currentRunMetadata(repo, currentRunId); + validateContinuationRunLineage({ + current: { id: currentRunId, metadata: current }, + parent: exact.parent, + root: exact.root, + }, pointer); + readContinuationAuthorization(repo, pointer, currentRunId); + const envelope = openContinuationEnvelope(exact.bytes, pointer); + const tree = gitTree(releaseCommit); + if (envelope.contract.source.tree !== tree) { + throw error("continuation contract release tree does not match the exact checked-out commit"); + } + if (stableJson(envelope.contract.products) !== stableJson(currentProducts)) { + throw error("continuation products do not match the current exact release plan"); + } + const cache = required("RELEASE_CONTINUATION_ARCHIVE", environment); + atomicBytes(cache, exact.bytes); + let continuedPacerPath = null; + let continuedJournalPath = null; + if (operation === "publish") { + continuedPacerPath = required("RELEASE_CONTINUATION_GITHUB_PACER_PATH", environment); + continuedJournalPath = required( + "RELEASE_CONTINUATION_GITHUB_CORE_JOURNAL_PATH", + environment, + ); + atomicBytes(continuedPacerPath, envelope.githubPacerBytes); + atomicBytes(continuedJournalPath, envelope.githubCoreJournalBytes); + } + if (output) { + emit(output, "found", "true"); + emit(output, "products_json", envelope.contract.products); + emit(output, "approved_run_id", envelope.contract.approvedPublication.runId); + emit(output, "approved_artifact_metadata_json", envelope.contract.approvedPublication.artifacts); + emit(output, "root_run_id", envelope.contract.lineage.rootRunId); + emit(output, "generation", envelope.contract.lineage.generation); + emit(output, "max_generations", envelope.contract.lineage.maxGenerations); + emit(output, "contract_digest", envelope.contract.contractDigest); + if (operation === "publish") { + emit(output, "continued_github_pacer_path", path.resolve(continuedPacerPath)); + emit(output, "continued_github_core_journal_path", path.resolve(continuedJournalPath)); + emit(output, "continued_github_state_json", envelope.contract.githubState); + } + if (envelope.contract.stageHandoff !== null) { + emit(output, "stage_handoff_run_id", envelope.contract.stageHandoff.runId); + emit(output, "stage_handoff_artifact_id", envelope.contract.stageHandoff.artifact.id); + emit(output, "stage_handoff_artifact_name", envelope.contract.stageHandoff.artifact.name); + emit(output, "stage_handoff_artifact_digest", envelope.contract.stageHandoff.artifact.digest); + emit(output, "stage_handoff_artifact_size", envelope.contract.stageHandoff.artifact.size); + emit(output, "stage_handoff_artifact_metadata_json", [envelope.contract.stageHandoff.artifact]); + } + } + console.log( + `verified ${operation} continuation generation ${envelope.contract.lineage.generation}/` + + `${envelope.contract.lineage.maxGenerations} from exact parent run ${pointer.parentRunId}, ` + + `artifact ${pointer.artifact.id}`, + ); + return envelope; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((cause) => { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + }); +} diff --git a/.github/scripts/install-release-continuation-github-state.mjs b/.github/scripts/install-release-continuation-github-state.mjs new file mode 100644 index 00000000..083765bd --- /dev/null +++ b/.github/scripts/install-release-continuation-github-state.mjs @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +import { appendFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import { installContinuationGitHubState } from "../../tools/release/github-release-continuation-state.mjs"; + +function error(message) { + return new Error(`install-release-continuation-github-state: ${message}`); +} + +function required(name, environment) { + const value = environment[name]?.trim(); + if (!value) throw error(`${name} is required`); + return value; +} + +function positiveInteger(name, environment) { + const raw = required(name, environment); + if (!/^[1-9][0-9]*$/u.test(raw) || !Number.isSafeInteger(Number(raw))) { + throw error(`${name} must be a positive safe integer`); + } + return Number(raw); +} + +function optionalPair(environment, leftName, rightName) { + const left = environment[leftName]?.trim() ?? ""; + const right = environment[rightName]?.trim() ?? ""; + if ((left === "") !== (right === "")) { + throw error(`${leftName} and ${rightName} must be supplied together`); + } + return left === "" ? null : { left, right }; +} + +function expectedState(environment, continued) { + const raw = environment.RELEASE_CONTINUATION_GITHUB_STATE_JSON?.trim() ?? ""; + if (continued === null) { + if (raw !== "") throw error("root registry state must not claim a parent continuation identity"); + return undefined; + } + if (raw === "") throw error("continued registry state requires RELEASE_CONTINUATION_GITHUB_STATE_JSON"); + try { + return JSON.parse(raw); + } catch (cause) { + throw error(`RELEASE_CONTINUATION_GITHUB_STATE_JSON must be strict JSON: ${cause.message}`); + } +} + +export function main(environment = process.env) { + const continued = optionalPair( + environment, + "RELEASE_CONTINUATION_GITHUB_PACER_PATH", + "RELEASE_CONTINUATION_GITHUB_CORE_JOURNAL_PATH", + ); + const result = installContinuationGitHubState({ + destinationJournal: required("OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH", environment), + destinationPacer: required("OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH", environment), + earlyJournal: required("PREINSTALL_GITHUB_CORE_JOURNAL_PATH", environment), + expectedLatestState: expectedState(environment, continued), + latestJournal: continued?.right, + latestPacer: continued?.left, + lineage: { + headSha: required("RELEASE_HEAD_SHA", environment), + repository: required("GITHUB_REPOSITORY", environment), + rootRunId: required("OLIPHAUNT_RELEASE_ROOT_RUN_ID", environment), + }, + originalJournal: required("ORIGINAL_GITHUB_CORE_JOURNAL_PATH", environment), + originalPacer: required("ORIGINAL_GITHUB_PACER_PATH", environment), + source: { + runAttempt: positiveInteger("GITHUB_RUN_ATTEMPT", environment), + runId: positiveInteger("GITHUB_RUN_ID", environment), + }, + }); + if (environment.GITHUB_OUTPUT) { + appendFileSync(environment.GITHUB_OUTPUT, `github_state_json=${JSON.stringify(result)}\n`); + } + console.log( + `installed continuation-safe GitHub state: pacer sequence ${result.pacer.sequence}, ` + + `core-request sequence ${result.coreRequestJournal.sequence}`, + ); + return result; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + main(); + } catch (cause) { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + } +} diff --git a/.github/scripts/manage-release-drafts.mjs b/.github/scripts/manage-release-drafts.mjs new file mode 100644 index 00000000..79eb10d0 --- /dev/null +++ b/.github/scripts/manage-release-drafts.mjs @@ -0,0 +1,688 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { redactGitHubReadDetail } from "../../tools/release/github-read.mjs"; +import { + assertResumableReleaseMetadata, + createGitHubOperationBudget, + exactReleaseMetadata, + exactTagRefPayload, + readReleaseByTagSync, + readReleaseMapSync, + readTagRefSync, + reconcileGitHubMutationSync, + releaseNotesForVersion, + remainingGitHubReadOptions, + runGitHubMutationSync, +} from "../../tools/release/github-release-mutations.mjs"; +import { loadGraph } from "../../tools/release/release-graph.mjs"; +import { + DEFAULT_PUBLICATION_LOCK, + loadPublicationLock, +} from "../../tools/release/publication-lock.mjs"; + +const FULL_SHA = /^[0-9a-f]{40}$/u; +const DEFAULT_GIT_SNAPSHOT_TIMEOUT_MS = 60_000; +const DEFAULT_FAST_MUTATION_TIMEOUT_MS = 60_000; + +export { + assertResumableReleaseMetadata, + exactReleaseMetadata, + exactTagRefPayload, + releaseNotesForVersion, +}; + +function error(message, options = {}) { + return new Error(`release-drafts: ${message}`, options); +} + +function usageError() { + return error( + "usage: manage-release-drafts.mjs " + + "--products-json JSON --head-ref SHA [--state draft|public|staged]", + ); +} + +function parseArgs(argv) { + const command = argv.shift(); + const values = new Map(); + for (let index = 0; index < argv.length; index += 2) { + const key = argv[index]; + const value = argv[index + 1]; + if (!key?.startsWith("--") || value === undefined || values.has(key.slice(2))) { + throw usageError(); + } + values.set(key.slice(2), value); + } + return { command, values }; +} + +function selectedPublicationLock(command, products, headRef, environment) { + const file = path.resolve( + environment.PUBLICATION_LOCK_PATH + ?? environment.OLIPHAUNT_PUBLICATION_LOCK + ?? DEFAULT_PUBLICATION_LOCK, + ); + if (!existsSync(file)) { + if (command === "preflight") return null; + throw error(`${command} requires the frozen publication lock: ${file}`); + } + const lock = loadPublicationLock(file); + if (lock.source.commit !== headRef) { + throw error(`publication lock targets ${lock.source.commit}, not ${headRef}`); + } + const lockedProducts = lock.products.map(({ id }) => id).sort(); + const requestedProducts = [...products].sort(); + if (JSON.stringify(lockedProducts) !== JSON.stringify(requestedProducts)) { + throw error( + `publication lock products ${JSON.stringify(lockedProducts)} do not match selected products ${JSON.stringify(requestedProducts)}`, + ); + } + return lock; +} + +function selectedReleases(command, products, headRef, environment) { + const graph = loadGraph("release-drafts"); + const publicationLock = selectedPublicationLock(command, products, headRef, environment); + const lockedProducts = publicationLock === null + ? new Map() + : new Map(publicationLock.products.map((product) => [product.id, product])); + return products.map((product) => { + const config = graph.products[product]; + if (!config) throw error(`unknown release product ${product}`); + const locked = lockedProducts.get(product); + const version = locked?.version ?? config.version; + if (config.version !== version) { + throw error(`${product} graph version ${config.version} does not match publication lock version ${version}`); + } + let body; + try { + body = releaseNotesForVersion(readFileSync(config.changelog_path, "utf8"), version); + } catch (cause) { + throw error(`${product} release notes are invalid: ${cause.message}`, { cause }); + } + const tag = `${config.tag_prefix}${version}`; + return { + metadata: exactReleaseMetadata({ body, headRef, product, tag, version }), + product, + tag, + version, + }; + }); +} + +function tagReconciliationState(ref, tag, headRef) { + if (ref === null) return { kind: "absent" }; + if (ref.type !== "commit" || ref.sha !== headRef || ref.ref !== `refs/tags/${tag}`) { + return { + detail: `${tag} targets ${ref.type}:${ref.sha}, not commit:${headRef}`, + kind: "conflict", + }; + } + return { kind: "desired" }; +} + +function releaseReconciliationState(release, expected, { allowPublic, expectedId } = {}) { + if (release === null) { + return expectedId === undefined + ? { kind: "absent" } + : { detail: `${expected.tag_name} release ${expectedId} disappeared`, kind: "conflict" }; + } + try { + assertResumableReleaseMetadata(release, expected); + } catch (cause) { + return { detail: cause.message, kind: "conflict" }; + } + if (expectedId !== undefined && release.id !== expectedId) { + return { + detail: `${expected.tag_name} release id changed from ${expectedId} to ${release.id}`, + kind: "conflict", + }; + } + if (allowPublic === true) return { kind: "desired" }; + return release.draft ? { kind: "unchanged" } : { kind: "desired" }; +} + +function mutationOptions(budget, environment, overrides) { + return { budget, environment, ...overrides }; +} + +export function stageExactTagSync({ budget, environment, headRef, repo, tag }, dependencies = {}) { + const readTag = dependencies.readTagRef ?? (() => + readTagRefSync(repo, tag, remainingGitHubReadOptions(budget))); + const createTag = dependencies.createTag ?? (({ deadlineMs, now, timeoutMs }) => + runGitHubMutationSync( + ["api", `repos/${repo}/git/refs`, "-X", "POST", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify(exactTagRefPayload(tag, headRef))}\n`, + now, + timeoutMs, + }, + )); + return reconcileGitHubMutationSync({ + inspect: () => tagReconciliationState(readTag(), tag, headRef), + label: `create exact tag ${tag}`, + mutate: createTag, + options: mutationOptions(budget, environment, dependencies.mutationOptions), + }); +} + +export function stageExactDraftReleaseSync( + { budget, environment, metadata, repo, tag }, + dependencies = {}, +) { + const readRelease = dependencies.readRelease ?? (() => + readReleaseByTagSync(repo, tag, remainingGitHubReadOptions(budget))); + const createRelease = dependencies.createRelease ?? (({ deadlineMs, now, timeoutMs }) => + runGitHubMutationSync( + ["api", `repos/${repo}/releases`, "-X", "POST", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify({ ...metadata, draft: true })}\n`, + now, + timeoutMs, + }, + )); + return reconcileGitHubMutationSync({ + inspect: () => releaseReconciliationState(readRelease(), metadata, { allowPublic: true }), + label: `create exact draft release ${tag}`, + mutate: createRelease, + options: mutationOptions(budget, environment, dependencies.mutationOptions), + }); +} + +export function promoteExactReleaseSync( + { budget, environment, expectedId, metadata, repo, tag }, + dependencies = {}, +) { + const readRelease = dependencies.readRelease ?? (() => + readReleaseByTagSync(repo, tag, remainingGitHubReadOptions(budget))); + const promoteRelease = dependencies.promoteRelease ?? (({ deadlineMs, now, timeoutMs }) => + runGitHubMutationSync( + ["api", `repos/${repo}/releases/${expectedId}`, "-X", "PATCH", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify({ draft: false })}\n`, + now, + timeoutMs, + }, + )); + return reconcileGitHubMutationSync({ + inspect: () => releaseReconciliationState(readRelease(), metadata, { expectedId }), + label: `promote exact release ${tag} (${expectedId})`, + mutate: promoteRelease, + options: mutationOptions(budget, environment, dependencies.mutationOptions), + }); +} + +function validateExistingReleases(selected, releasesByTag) { + for (const { metadata, tag } of selected) { + const release = releasesByTag.get(tag); + if (release === undefined) continue; + try { + assertResumableReleaseMetadata(release, metadata); + } catch (cause) { + throw error(cause.message, { cause }); + } + } +} + +function requireExactTags(selected, repo, headRef, budget) { + for (const { product, tag } of selected) { + const ref = readTagRefSync(repo, tag, remainingGitHubReadOptions(budget)); + const state = tagReconciliationState(ref, tag, headRef); + if (state.kind !== "desired") { + throw error( + state.kind === "absent" + ? `${product} tag ${tag} does not exist` + : state.detail, + ); + } + } +} + +function finalReleaseState(selected, releasesByTag, command, expectedState) { + const wantDraft = command === "promote" ? false : expectedState === "draft"; + for (const { tag } of selected) { + const release = releasesByTag.get(tag); + if (release === undefined) { + throw error(`GitHub release for ${tag} does not exist after ${command}`); + } + if (expectedState !== "staged" && release.draft !== wantDraft) { + throw error(`${tag} is ${release.draft ? "draft" : "public"}; expected ${wantDraft ? "draft" : "public"}`); + } + } + return wantDraft; +} + +function parseMutationJson(output, label) { + if (typeof output !== "string" || Buffer.byteLength(output, "utf8") > 4 * 1024 * 1024) { + throw error(`${label} returned an invalid bounded response`); + } + try { + return JSON.parse(output); + } catch (cause) { + throw error(`${label} returned malformed JSON`, { cause }); + } +} + +function exactTagFromMutation(output, tag, headRef) { + const value = parseMutationJson(output, `create exact tag ${tag}`); + if ( + value === null + || Array.isArray(value) + || typeof value !== "object" + || value.ref !== `refs/tags/${tag}` + || value.object === null + || Array.isArray(value.object) + || typeof value.object !== "object" + || value.object.sha !== headRef + || value.object.type !== "commit" + ) { + throw error(`create exact tag ${tag} returned a response that does not bind commit:${headRef}`); + } + return { ref: value.ref, sha: value.object.sha, type: value.object.type }; +} + +function exactReleaseFromMutation(output, metadata, { draft, expectedId } = {}) { + const value = parseMutationJson(output, `mutate exact release ${metadata.tag_name}`); + assertResumableReleaseMetadata(value, metadata); + if (value.draft !== draft) { + throw error( + `${metadata.tag_name} mutation response is ${value.draft ? "draft" : "public"}; ` + + `expected ${draft ? "draft" : "public"}`, + ); + } + if (expectedId !== undefined && value.id !== expectedId) { + throw error(`${metadata.tag_name} mutation response id changed from ${expectedId} to ${value.id}`); + } + return value; +} + +function selectedTagNames(selected) { + const tags = selected.map(({ tag }) => tag); + if ( + tags.length === 0 + || new Set(tags).size !== tags.length + || tags.some((tag) => typeof tag !== "string" || tag.length === 0 || /[\s\u0000-\u001f\u007f]/u.test(tag)) + ) { + throw error("selected release tags must be a non-empty unique printable string list"); + } + return tags; +} + +/** + * Read every selected tag in one Git protocol advertisement rather than one + * REST request per product. The canonical release repository is public, so + * this snapshot intentionally carries no credential and consumes no + * GITHUB_TOKEN REST quota. + */ +export function readSelectedRemoteTagMapSync(repo, selected, options = {}) { + if (typeof repo !== "string" || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repo)) { + throw error("GitHub repository must be OWNER/NAME"); + } + const tags = selectedTagNames(selected); + const remainingMs = options.budget === undefined + ? DEFAULT_GIT_SNAPSHOT_TIMEOUT_MS + : options.budget.deadlineMs - options.budget.now(); + if (!Number.isSafeInteger(remainingMs) || remainingMs <= 0) { + throw error("GitHub operation deadline has been reached before the remote tag snapshot"); + } + const spawn = options.spawn ?? spawnSync; + const result = spawn("git", [ + "-c", + "credential.helper=", + "ls-remote", + "--refs", + "--tags", + `https://github.com/${repo}.git`, + ...tags.map((tag) => `refs/tags/${tag}`), + ], { + cwd: options.cwd, + encoding: "utf8", + env: { + ...(options.environment ?? process.env), + GIT_ASKPASS: "", + GIT_TERMINAL_PROMPT: "0", + SSH_ASKPASS: "", + }, + maxBuffer: 4 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + timeout: Math.max(1, Math.min(DEFAULT_GIT_SNAPSHOT_TIMEOUT_MS, remainingMs)), + }); + if (result.error !== undefined || result.status !== 0) { + const detail = redactGitHubReadDetail(result.error?.message ?? result.stderr ?? ""); + throw error(`could not read the exact selected remote tag snapshot${detail ? `: ${detail}` : ""}`); + } + const wanted = new Set(tags.map((tag) => `refs/tags/${tag}`)); + const refs = new Map(); + for (const line of String(result.stdout ?? "").split(/\r?\n/u).filter(Boolean)) { + const match = /^([0-9a-f]{40})\t(refs\/tags\/[^\s\u0000-\u001f\u007f]+)$/u.exec(line); + if (match === null || !wanted.has(match[2]) || refs.has(match[2])) { + throw error("remote tag snapshot contained malformed, unexpected, or duplicate output"); + } + refs.set(match[2], { ref: match[2], sha: match[1], type: "commit" }); + } + return new Map(tags.map((tag) => [tag, refs.get(`refs/tags/${tag}`) ?? null])); +} + +function requireExactTagSnapshot(selected, tagsByName, headRef) { + if (!(tagsByName instanceof Map)) throw error("remote tag snapshot must be a Map"); + for (const { product, tag } of selected) { + const state = tagReconciliationState(tagsByName.get(tag) ?? null, tag, headRef); + if (state.kind !== "desired") { + throw error(state.kind === "absent" ? `${product} tag ${tag} does not exist` : state.detail); + } + } +} + +function requireCollisionFreeTagSnapshot(selected, tagsByName, headRef) { + if (!(tagsByName instanceof Map)) throw error("remote tag snapshot must be a Map"); + for (const { tag } of selected) { + const state = tagReconciliationState(tagsByName.get(tag) ?? null, tag, headRef); + if (state.kind === "conflict") throw error(state.detail); + } +} + +function fastMutationTimeout(budget) { + const remainingMs = budget.deadlineMs - budget.now(); + if (remainingMs < DEFAULT_FAST_MUTATION_TIMEOUT_MS) { + throw error( + `GitHub operation requires a complete ${DEFAULT_FAST_MUTATION_TIMEOUT_MS}ms mutation timeout; ` + + `${Math.max(0, remainingMs)}ms remains`, + ); + } + return DEFAULT_FAST_MUTATION_TIMEOUT_MS; +} + +function defaultTagMutation({ deadlineMs, environment, headRef, now, repo, tag, timeoutMs }) { + return runGitHubMutationSync( + ["api", `repos/${repo}/git/refs`, "-X", "POST", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify(exactTagRefPayload(tag, headRef))}\n`, + now, + timeoutMs, + }, + ); +} + +function defaultReleaseMutation({ deadlineMs, environment, metadata, now, repo, timeoutMs }) { + return runGitHubMutationSync( + ["api", `repos/${repo}/releases`, "-X", "POST", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify({ ...metadata, draft: true })}\n`, + now, + timeoutMs, + }, + ); +} + +function defaultPromotionMutation({ deadlineMs, environment, expectedId, now, repo, timeoutMs }) { + return runGitHubMutationSync( + ["api", `repos/${repo}/releases/${expectedId}`, "-X", "PATCH", "--input", "-"], + { + environment, + deadlineMs, + input: `${JSON.stringify({ draft: false })}\n`, + now, + timeoutMs, + }, + ); +} + +function stageMissingTagFromSnapshot(context, dependencies) { + const mutateTag = dependencies.mutateTag ?? defaultTagMutation; + try { + const output = mutateTag({ + ...context, + deadlineMs: context.budget.deadlineMs, + now: context.budget.now, + timeoutMs: fastMutationTimeout(context.budget), + }); + exactTagFromMutation(output, context.tag, context.headRef); + return { mutationAttempts: 1, recovered: false }; + } catch (cause) { + const result = stageExactTagSync(context, { + createTag: ({ deadlineMs, now, timeoutMs }) => mutateTag({ + ...context, + deadlineMs, + now, + timeoutMs, + }), + mutationOptions: dependencies.mutationOptions, + readTagRef: dependencies.readTagRef, + }); + return { ...result, fastMutationError: cause }; + } +} + +function stageMissingReleaseFromSnapshot(context, dependencies) { + const mutateRelease = dependencies.mutateRelease ?? defaultReleaseMutation; + try { + const output = mutateRelease({ + ...context, + deadlineMs: context.budget.deadlineMs, + now: context.budget.now, + timeoutMs: fastMutationTimeout(context.budget), + }); + exactReleaseFromMutation(output, context.metadata, { draft: true }); + return { mutationAttempts: 1, recovered: false }; + } catch (cause) { + const result = stageExactDraftReleaseSync(context, { + createRelease: ({ deadlineMs, now, timeoutMs }) => mutateRelease({ + ...context, + deadlineMs, + now, + timeoutMs, + }), + mutationOptions: dependencies.mutationOptions, + readRelease: dependencies.readRelease, + }); + return { ...result, fastMutationError: cause }; + } +} + +function promoteReleaseFromSnapshot(context, dependencies) { + const mutatePromotion = dependencies.mutatePromotion ?? defaultPromotionMutation; + try { + const output = mutatePromotion({ + ...context, + deadlineMs: context.budget.deadlineMs, + now: context.budget.now, + timeoutMs: fastMutationTimeout(context.budget), + }); + exactReleaseFromMutation(output, context.metadata, { draft: false, expectedId: context.expectedId }); + return { mutationAttempts: 1, recovered: false }; + } catch (cause) { + const result = promoteExactReleaseSync(context, { + mutationOptions: dependencies.mutationOptions, + promoteRelease: ({ deadlineMs, now, timeoutMs }) => mutatePromotion({ + ...context, + deadlineMs, + now, + timeoutMs, + }), + readRelease: dependencies.readRelease, + }); + return { ...result, fastMutationError: cause }; + } +} + +export function reconcileSelectedReleasesSync( + { budget, command, environment, expectedState, headRef, repo, selected }, + dependencies = {}, +) { + const readReleaseMap = dependencies.readReleaseMap ?? ((targetRepo) => + readReleaseMapSync(targetRepo, remainingGitHubReadOptions(budget))); + const readTagMap = dependencies.readTagMap ?? ((targetRepo, targetSelected) => + readSelectedRemoteTagMapSync(targetRepo, targetSelected, { budget, environment })); + const perTagDependencies = { + ...dependencies, + readTagRef: dependencies.readTagRef ?? ((tag) => + readTagRefSync(repo, tag, remainingGitHubReadOptions(budget))), + }; + const perReleaseDependencies = { + ...dependencies, + readRelease: dependencies.readRelease ?? ((tag) => + readReleaseByTagSync(repo, tag, remainingGitHubReadOptions(budget))), + }; + + let releasesByTag = readReleaseMap(repo); + validateExistingReleases(selected, releasesByTag); + let tagsByName = readTagMap(repo, selected); + requireCollisionFreeTagSnapshot(selected, tagsByName, headRef); + + if (command === "preflight") { + console.log(`${selected.length} selected product tag/release names are absent or exact-SHA resumable`); + return; + } + + if (command === "stage") { + for (const { product, tag } of selected) { + if (tagsByName.get(tag) !== null) continue; + const result = stageMissingTagFromSnapshot( + { budget, environment, headRef, repo, tag }, + { + ...perTagDependencies, + readTagRef: () => perTagDependencies.readTagRef(tag), + }, + ); + if (result.mutationAttempts > 0) console.log(`reconciled exact-SHA tag ${tag} for ${product}`); + } + tagsByName = readTagMap(repo, selected); + requireExactTagSnapshot(selected, tagsByName, headRef); + for (const { metadata, tag } of selected) { + if (releasesByTag.has(tag)) continue; + const result = stageMissingReleaseFromSnapshot( + { budget, environment, metadata, repo, tag }, + { + ...perReleaseDependencies, + readRelease: () => perReleaseDependencies.readRelease(tag), + }, + ); + if (result.mutationAttempts > 0) console.log(`reconciled draft GitHub release ${tag}`); + } + releasesByTag = readReleaseMap(repo); + validateExistingReleases(selected, releasesByTag); + tagsByName = readTagMap(repo, selected); + requireExactTagSnapshot(selected, tagsByName, headRef); + } else { + requireExactTagSnapshot(selected, tagsByName, headRef); + } + + for (const { tag } of selected) { + if (!releasesByTag.has(tag)) throw error(`GitHub release for ${tag} does not exist`); + } + + if (command === "promote") { + for (const { metadata, tag } of selected) { + const release = releasesByTag.get(tag); + if (!release.draft) continue; + const result = promoteReleaseFromSnapshot( + { + budget, + environment, + expectedId: release.id, + metadata, + repo, + tag, + }, + { + ...perReleaseDependencies, + readRelease: () => perReleaseDependencies.readRelease(tag), + }, + ); + if (result.mutationAttempts > 0) console.log(`reconciled promotion of ${tag}`); + } + releasesByTag = readReleaseMap(repo); + validateExistingReleases(selected, releasesByTag); + tagsByName = readTagMap(repo, selected); + requireExactTagSnapshot(selected, tagsByName, headRef); + } + + const wantDraft = finalReleaseState(selected, releasesByTag, command, expectedState); + if (expectedState === "staged" && command !== "promote") { + console.log(`${selected.length} exact-SHA releases are staged (draft or already promoted by a resumable prior run)`); + } else { + console.log(`${selected.length} exact-SHA releases are ${wantDraft ? "draft" : "public"}`); + } +} + +function defaultWindowForCommand(command) { + if (command === "stage") return 30 * 60_000; + // Promotion count is release-plan-derived. Keep the command inside the + // mandatory finalization reserve while leaving a bounded contingency margin. + if (command === "promote") return 12 * 60_000; + return 5 * 60_000; +} + +export function main(argv, { environment = process.env, now = Date.now } = {}) { + const { command, values } = parseArgs([...argv]); + if (!["preflight", "stage", "verify", "promote"].includes(command)) { + throw error("command must be preflight, stage, verify, or promote"); + } + const repo = environment.GITHUB_REPOSITORY?.trim(); + if (!repo || !environment.GH_TOKEN) { + throw error("GITHUB_REPOSITORY and GH_TOKEN are required"); + } + + let products; + try { + products = JSON.parse(values.get("products-json") ?? ""); + } catch (cause) { + throw error(`invalid --products-json: ${cause.message}`, { cause }); + } + if ( + !Array.isArray(products) + || products.length === 0 + || products.some((product) => typeof product !== "string" || product.length === 0) + || new Set(products).size !== products.length + ) { + throw error("--products-json must be a non-empty unique product string list"); + } + + const headRef = values.get("head-ref"); + if (!headRef || !FULL_SHA.test(headRef)) { + throw error("--head-ref must be a full lowercase commit SHA"); + } + const expectedState = values.get("state") ?? "draft"; + if (!new Set(["draft", "public", "staged"]).has(expectedState)) { + throw error("--state must be draft, public, or staged"); + } + + const selected = selectedReleases(command, products, headRef, environment); + const budget = createGitHubOperationBudget({ + defaultWindowMs: defaultWindowForCommand(command), + environment, + now, + }); + reconcileSelectedReleasesSync({ + budget, + command, + environment, + expectedState, + headRef, + repo, + selected, + }); +} + +if (import.meta.main) { + try { + main(process.argv.slice(2)); + } catch (cause) { + console.error(redactGitHubReadDetail(cause instanceof Error ? cause.message : String(cause))); + process.exit(1); + } +} diff --git a/.github/scripts/merge-checksum-manifest.mjs b/.github/scripts/merge-checksum-manifest.mjs new file mode 100644 index 00000000..08de5702 --- /dev/null +++ b/.github/scripts/merge-checksum-manifest.mjs @@ -0,0 +1,61 @@ +#!/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 compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +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]) => compareText(left, 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/.github/scripts/moon-task-capabilities.mjs b/.github/scripts/moon-task-capabilities.mjs new file mode 100644 index 00000000..bfae985d --- /dev/null +++ b/.github/scripts/moon-task-capabilities.mjs @@ -0,0 +1,128 @@ +const RUST_CAPABILITY_TAG = "ci-rust"; +const MAINTAINER_TOOLS_CAPABILITY_TAG = "ci-maintainer-tools"; +const ANDROID_SDK_CAPABILITY_TAG = "ci-android-sdk"; + +export const CHECK_SHARD_MAX_TARGETS = 4; + +function taskTags(task) { + return new Set(Array.isArray(task?.tags) ? task.tags : []); +} + +export function taskDependencies(task) { + return (Array.isArray(task?.deps) ? task.deps : []) + .map((dependency) => { + if (typeof dependency === "string") return dependency; + if (dependency && typeof dependency === "object" && typeof dependency.target === "string") { + return dependency.target; + } + return ""; + }) + .filter(Boolean); +} + +function taskTarget(task) { + if (typeof task?.target !== "string" || task.target.length === 0) { + throw new Error("Moon task capability resolution requires a task target"); + } + return task.target; +} + +export function taskCapabilities(task, taskMap, state = {}) { + const target = taskTarget(task); + const memo = state.memo ?? new Map(); + const visiting = state.visiting ?? new Set(); + const cached = memo.get(target); + if (cached !== undefined) return cached; + if (visiting.has(target)) { + throw new Error(`Moon task capability dependency cycle through ${target}`); + } + + visiting.add(target); + const tags = taskTags(task); + let requiresMaintainerTools = tags.has(MAINTAINER_TOOLS_CAPABILITY_TAG); + let requiresRust = tags.has(RUST_CAPABILITY_TAG) || requiresMaintainerTools; + let requiresAndroidSdk = tags.has(ANDROID_SDK_CAPABILITY_TAG); + + for (const dependency of taskDependencies(task)) { + const dependencyTask = taskMap.get(dependency); + if (dependencyTask === undefined) { + throw new Error(`${target} capability dependency ${dependency} is missing from Moon task metadata`); + } + const capabilities = taskCapabilities(dependencyTask, taskMap, { memo, visiting }); + requiresMaintainerTools ||= capabilities.requires_maintainer_tools; + requiresRust ||= capabilities.requires_rust; + requiresAndroidSdk ||= capabilities.requires_android_sdk; + } + + visiting.delete(target); + const capabilities = Object.freeze({ + requires_rust: requiresRust, + requires_maintainer_tools: requiresMaintainerTools, + requires_android_sdk: requiresAndroidSdk, + }); + memo.set(target, capabilities); + return capabilities; +} + +export function matrixTarget(task, upstream, taskMap) { + return { + target: taskTarget(task), + upstream, + ...taskCapabilities(task, taskMap), + }; +} + +function compareTargets(left, right) { + return left.target < right.target ? -1 : left.target > right.target ? 1 : 0; +} + +function shardRow(targets, { index, total }) { + const first = targets[0]; + const label = targets.length === 1 + ? first.target + : `static ${index + 1}/${total} (${targets.length} targets)`; + return { + label, + target_count: targets.length, + requires_rust: first.requires_rust, + requires_maintainer_tools: first.requires_maintainer_tools, + requires_android_sdk: first.requires_android_sdk, + targets_json: JSON.stringify({ + include: targets.map(({ target, upstream }) => ({ target, upstream })), + }), + }; +} + +export function shardCheckTargets(targets, { maxTargets = CHECK_SHARD_MAX_TARGETS } = {}) { + if (!Number.isInteger(maxTargets) || maxTargets < 1) { + throw new Error("check shard size must be a positive integer"); + } + const ordered = [...targets].sort(compareTargets); + const unique = new Set(ordered.map(({ target }) => target)); + if (unique.size !== ordered.length) { + throw new Error("check shard input contains duplicate Moon targets"); + } + + const shardable = ordered.filter((target) => + !target.requires_rust + && !target.requires_maintainer_tools + && !target.requires_android_sdk); + const dedicated = ordered.filter((target) => !shardable.includes(target)); + const groups = []; + for (let index = 0; index < shardable.length; index += maxTargets) { + groups.push(shardable.slice(index, index + maxTargets)); + } + groups.push(...dedicated.map((target) => [target])); + + const staticGroupCount = groups.filter((group) => group.length > 1).length; + let staticIndex = 0; + return groups.map((group) => { + const isStaticShard = group.length > 1; + const row = shardRow(group, { + index: isStaticShard ? staticIndex : 0, + total: isStaticShard ? staticGroupCount : 1, + }); + if (isStaticShard) staticIndex += 1; + return row; + }); +} diff --git a/.github/scripts/moon-task-capabilities.test.mjs b/.github/scripts/moon-task-capabilities.test.mjs new file mode 100644 index 00000000..98a9879a --- /dev/null +++ b/.github/scripts/moon-task-capabilities.test.mjs @@ -0,0 +1,75 @@ +import { strict as assert } from "node:assert"; +import { describe, test } from "node:test"; + +import { + matrixTarget, + shardCheckTargets, + taskCapabilities, +} from "./moon-task-capabilities.mjs"; + +function tasks(...entries) { + return new Map(entries.map((task) => [task.target, task])); +} + +describe("Moon task capabilities", () => { + test("propagates capabilities through dependencies and makes maintainer tools imply Rust", () => { + const taskMap = tasks( + { target: "repo:leaf", tags: ["ci-maintainer-tools"] }, + { target: "repo:middle", tags: ["ci-android-sdk"], deps: [{ target: "repo:leaf" }] }, + { target: "repo:root", tags: [], deps: ["repo:middle"] }, + ); + + assert.deepEqual(taskCapabilities(taskMap.get("repo:root"), taskMap), { + requires_rust: true, + requires_maintainer_tools: true, + requires_android_sdk: true, + }); + }); + + test("fails closed for incomplete or cyclic dependency metadata", () => { + const incomplete = tasks({ target: "repo:root", deps: ["repo:missing"] }); + assert.throws( + () => taskCapabilities(incomplete.get("repo:root"), incomplete), + /repo:missing/u, + ); + + const cyclic = tasks( + { target: "repo:first", deps: ["repo:second"] }, + { target: "repo:second", deps: ["repo:first"] }, + ); + assert.throws( + () => taskCapabilities(cyclic.get("repo:first"), cyclic), + /dependency cycle/u, + ); + }); + + test("creates bounded static shards while keeping capability-bearing work dedicated", () => { + const taskMap = tasks( + ...Array.from({ length: 9 }, (_, index) => ({ target: `plain:${index}`, tags: [] })), + { target: "rust:first", tags: ["ci-rust"] }, + { target: "rust:second", tags: ["ci-rust"] }, + { target: "android:check", tags: ["ci-android-sdk"] }, + ); + const targets = [...taskMap.values()].map((task) => matrixTarget(task, "deep", taskMap)); + const shards = shardCheckTargets(targets, { maxTargets: 4 }); + + assert.deepEqual(shards.map(({ target_count }) => target_count), [4, 4, 1, 1, 1, 1]); + assert.equal(shards.filter(({ requires_rust }) => requires_rust).length, 2); + assert.equal(shards.filter(({ requires_android_sdk }) => requires_android_sdk).length, 1); + const selected = shards.flatMap(({ targets_json }) => + JSON.parse(targets_json).include.map(({ target }) => target)); + assert.deepEqual(selected.sort(), [...taskMap.keys()].sort()); + }); + + test("rejects duplicate targets and invalid shard limits", () => { + const row = { + target: "repo:check", + upstream: "deep", + requires_rust: false, + requires_maintainer_tools: false, + requires_android_sdk: false, + }; + assert.throws(() => shardCheckTargets([row, row]), /duplicate/u); + assert.throws(() => shardCheckTargets([row], { maxTargets: 0 }), /positive integer/u); + }); +}); diff --git a/.github/scripts/normalize-release-please-pr.mjs b/.github/scripts/normalize-release-please-pr.mjs new file mode 100644 index 00000000..c43d1086 --- /dev/null +++ b/.github/scripts/normalize-release-please-pr.mjs @@ -0,0 +1,237 @@ +#!/usr/bin/env bun + +import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +const TOOL = "normalize-release-please-pr.mjs"; +const CANONICAL_REPOSITORY = "f0rr0/oliphaunt"; +const MAIN_BRANCH = "main"; +const RELEASE_BRANCH = "release-please--branches--main"; +const FULL_SHA = /^[0-9a-f]{40}$/u; +const POSITIVE_INTEGER = /^[1-9][0-9]*$/u; +const SAFE_REMOTE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u; + +function fail(message) { + throw new Error(`${TOOL}: ${message}`); +} + +function run(command, args, { cwd = process.cwd(), check = true } = {}) { + const result = spawnSync(command, args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) fail(`${command} failed: ${result.error.message}`); + if (check && result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + fail(`${command} ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`); + } + return result; +} + +function git(args, options = {}) { + return run("git", args, options); +} + +function gitText(args, options = {}) { + return git(args, options).stdout.trimEnd(); +} + +function parseArgs(argv) { + const command = argv[0]; + if (!new Set(["normalize", "push"]).has(command)) { + fail("usage: normalize-release-please-pr.mjs --pr-number N --observed-pr-number N --base main --head release-please--branches--main --head-sha SHA --head-repository f0rr0/oliphaunt --cross-repository false --state OPEN --title TITLE --main-sha SHA [--remote origin]"); + } + const values = { command, remote: "origin" }; + const known = new Set([ + "--pr-number", + "--observed-pr-number", + "--base", + "--head", + "--head-sha", + "--head-repository", + "--cross-repository", + "--state", + "--title", + "--main-sha", + "--remote", + ]); + for (let index = 1; index < argv.length; index += 2) { + const flag = argv[index]; + const value = argv[index + 1]; + if (!known.has(flag) || value === undefined) fail(`unknown or incomplete argument ${flag ?? ""}`); + const key = flag.slice(2).replaceAll(/-([a-z])/gu, (_match, letter) => letter.toUpperCase()); + if (Object.hasOwn(values, key) && key !== "remote") fail(`${flag} must be supplied exactly once`); + values[key] = value; + } + for (const key of [ + "prNumber", + "observedPrNumber", + "base", + "head", + "headSha", + "headRepository", + "crossRepository", + "state", + "title", + "mainSha", + ]) { + if (typeof values[key] !== "string" || values[key].length === 0) fail(`--${key.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)} is required`); + } + return values; +} + +function expectedTitle(repo, base) { + let config; + try { + config = JSON.parse(readFileSync(`${repo}/release-please-config.json`, "utf8")); + } catch (cause) { + fail(`release-please-config.json is unreadable: ${cause.message}`); + } + const pattern = config?.["group-pull-request-title-pattern"]; + if (typeof pattern !== "string" || pattern.length === 0) { + fail("release-please-config.json must define group-pull-request-title-pattern"); + } + const title = pattern.replaceAll("${branch}", base); + if (title.includes("${") || !/^chore\(release\): .+/u.test(title) || /[\r\n]/u.test(title)) { + fail("group-pull-request-title-pattern must render one conventional release title using only ${branch}"); + } + return title; +} + +function validateIdentity(args, repo) { + if (!POSITIVE_INTEGER.test(args.prNumber) || !POSITIVE_INTEGER.test(args.observedPrNumber)) { + fail("release PR numbers must be positive integers"); + } + if (args.prNumber !== args.observedPrNumber) { + fail(`release PR identity changed: requested #${args.prNumber}, observed #${args.observedPrNumber}`); + } + if (args.base !== MAIN_BRANCH) fail(`release PR base must be ${MAIN_BRANCH}, got ${args.base}`); + if (args.head !== RELEASE_BRANCH) fail(`release PR head must be ${RELEASE_BRANCH}, got ${args.head}`); + if (args.headRepository !== CANONICAL_REPOSITORY) { + fail(`release PR head repository must be ${CANONICAL_REPOSITORY}, got ${args.headRepository}`); + } + if (args.crossRepository !== "false") fail("release PR must not be cross-repository"); + if (args.state !== "OPEN") fail(`release PR must be OPEN, got ${args.state}`); + if (!FULL_SHA.test(args.headSha) || !FULL_SHA.test(args.mainSha)) { + fail("release PR head and main identities must be lowercase full commit SHAs"); + } + if (!SAFE_REMOTE.test(args.remote)) fail(`unsafe Git remote name ${JSON.stringify(args.remote)}`); + const title = expectedTitle(repo, args.base); + if (args.title !== title) fail(`release PR title must be ${JSON.stringify(title)}, got ${JSON.stringify(args.title)}`); + return title; +} + +function requireClean(repo) { + const status = gitText(["status", "--porcelain", "--untracked-files=all"], { cwd: repo }); + if (status !== "") fail(`working tree must be clean before release PR normalization or push: ${status}`); +} + +function requireCommit(repo, ref, expected, context) { + const actual = gitText(["rev-parse", "--verify", `${ref}^{commit}`], { cwd: repo }); + if (actual !== expected) fail(`${context} is ${actual}, expected ${expected}`); + return actual; +} + +function releaseRangeShape(repo, mainSha, headRef, title) { + if (git(["merge-base", "--is-ancestor", mainSha, headRef], { cwd: repo, check: false }).status !== 0) { + fail(`release PR head is not descended from exact main ${mainSha}`); + } + const countText = gitText(["rev-list", "--count", `${mainSha}..${headRef}`], { cwd: repo }); + const count = Number(countText); + if (!Number.isSafeInteger(count) || count < 1) fail("release PR must contain at least one commit above exact main"); + const merges = gitText(["rev-list", "--merges", `${mainSha}..${headRef}`], { cwd: repo }); + if (merges !== "") fail("release PR history must be linear and contain no merge commits"); + const subjects = gitText(["log", "--format=%s", `${mainSha}..${headRef}`], { cwd: repo }).split(/\r?\n/u); + if (subjects.length !== count || subjects.some((subject) => subject !== title)) { + fail(`every generated release PR chunk must use exact title ${JSON.stringify(title)}`); + } + const mainTree = gitText(["rev-parse", `${mainSha}^{tree}`], { cwd: repo }); + const headTree = gitText(["rev-parse", `${headRef}^{tree}`], { cwd: repo }); + if (mainTree === headTree) fail("release PR tree must differ from exact main"); + return { count, headTree }; +} + +function normalize(args, repo) { + const title = validateIdentity(args, repo); + requireClean(repo); + const mainRemoteRef = `refs/remotes/${args.remote}/${MAIN_BRANCH}`; + const headRemoteRef = `refs/remotes/${args.remote}/${RELEASE_BRANCH}`; + git(["fetch", "--no-tags", args.remote, `+refs/heads/${MAIN_BRANCH}:${mainRemoteRef}`], { cwd: repo }); + git(["fetch", "--no-tags", args.remote, `+refs/heads/${RELEASE_BRANCH}:${headRemoteRef}`], { cwd: repo }); + requireCommit(repo, mainRemoteRef, args.mainSha, "current remote main"); + requireCommit(repo, headRemoteRef, args.headSha, "inspected release PR head"); + const shape = releaseRangeShape(repo, args.mainSha, headRemoteRef, title); + + git(["switch", "-C", RELEASE_BRANCH, headRemoteRef], { cwd: repo }); + const directParent = gitText(["rev-parse", "HEAD^"], { cwd: repo, check: false }); + let normalized = false; + if (shape.count !== 1 || directParent !== args.mainSha) { + git(["reset", "--soft", args.mainSha], { cwd: repo }); + if (git(["diff", "--cached", "--quiet", "--exit-code"], { cwd: repo, check: false }).status === 0) { + fail("release PR normalization produced no staged tree change"); + } + git([ + "-c", "user.name=oliphaunt-release-bot", + "-c", "user.email=oliphaunt-release-bot@users.noreply.github.com", + "commit", "-m", title, + ], { cwd: repo }); + normalized = true; + } + + const localShape = releaseRangeShape(repo, args.mainSha, "HEAD", title); + if (localShape.count !== 1 || gitText(["rev-parse", "HEAD^"], { cwd: repo }) !== args.mainSha) { + fail("normalized release PR must be exactly one commit above exact main"); + } + if (localShape.headTree !== shape.headTree) fail("normalization changed the generated release PR tree"); + requireClean(repo); + console.log(`release PR #${args.prNumber} checked out at ${gitText(["rev-parse", "HEAD"], { cwd: repo })}; normalized=${normalized}`); +} + +function remoteRefSha(repo, remote, ref) { + const output = gitText(["ls-remote", "--heads", remote, ref], { cwd: repo }); + const rows = output.split(/\r?\n/u).filter(Boolean); + if (rows.length !== 1) fail(`expected exactly one remote ref ${ref}; found ${rows.length}`); + const [sha, observedRef, ...extra] = rows[0].split(/\s+/u); + if (!FULL_SHA.test(sha) || observedRef !== ref || extra.length !== 0) fail(`remote ref ${ref} returned malformed metadata`); + return sha; +} + +function push(args, repo) { + const title = validateIdentity(args, repo); + requireClean(repo); + const branch = gitText(["branch", "--show-current"], { cwd: repo }); + if (branch !== RELEASE_BRANCH) fail(`local branch must be ${RELEASE_BRANCH}, got ${branch || ""}`); + const localShape = releaseRangeShape(repo, args.mainSha, "HEAD", title); + if (localShape.count !== 1 || gitText(["rev-parse", "HEAD^"], { cwd: repo }) !== args.mainSha) { + fail("release PR push requires exactly one local commit above exact main"); + } + const remoteMain = remoteRefSha(repo, args.remote, `refs/heads/${MAIN_BRANCH}`); + if (remoteMain !== args.mainSha) fail(`main moved before release PR push: ${remoteMain}, expected ${args.mainSha}`); + const localHead = gitText(["rev-parse", "HEAD"], { cwd: repo }); + git([ + "push", + `--force-with-lease=refs/heads/${RELEASE_BRANCH}:${args.headSha}`, + args.remote, + `HEAD:refs/heads/${RELEASE_BRANCH}`, + ], { cwd: repo }); + const remoteHead = remoteRefSha(repo, args.remote, `refs/heads/${RELEASE_BRANCH}`); + if (remoteHead !== localHead) fail(`release PR push produced ${remoteHead}, expected ${localHead}`); + console.log(`release PR #${args.prNumber} pushed as one exact release commit ${localHead}`); +} + +export function main(argv, { repo = process.cwd() } = {}) { + const args = parseArgs(argv); + if (args.command === "normalize") normalize(args, repo); + else push(args, repo); +} + +if (import.meta.main) { + try { + main(Bun.argv.slice(2)); + } catch (cause) { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + } +} diff --git a/.github/scripts/prepare-linux-apt.sh b/.github/scripts/prepare-linux-apt.sh new file mode 100755 index 00000000..ab1e04e4 --- /dev/null +++ b/.github/scripts/prepare-linux-apt.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(uname -s)" != "Linux" ]; then + exit 0 +fi + +disabled_any=0 +for file in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do + [ -f "$file" ] || continue + if ! grep -q "packages.microsoft.com" "$file"; then + continue + fi + + disabled_any=1 + if [ "$file" = "/etc/apt/sources.list" ]; then + sudo sed -i.bak '/packages\.microsoft\.com/s/^/# disabled by oliphaunt CI: /' "$file" + else + sudo mv "$file" "$file.disabled" + fi +done + +if [ "$disabled_any" = "1" ]; then + echo "Disabled preinstalled packages.microsoft.com apt sources before apt-get update" +fi diff --git a/.github/scripts/prepare-macos-homebrew.sh b/.github/scripts/prepare-macos-homebrew.sh new file mode 100755 index 00000000..578a3883 --- /dev/null +++ b/.github/scripts/prepare-macos-homebrew.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$(uname -s)" != "Darwin" ]; then + exit 0 +fi + +command -v brew >/dev/null 2>&1 || { + echo "prepare-macos-homebrew.sh: Homebrew is not installed" >&2 + exit 1 +} + +# GitHub's macOS image currently carries this third-party tap even though +# Oliphaunt does not consume it. Homebrew's tap-trust policy reports every +# formula lookup as a workflow warning while the untrusted tap remains. +# Removing an unused runner-owned tap preserves the trust policy instead of +# disabling it or broadly trusting current and future tap contents. +if brew tap | grep -Fxq 'aws/tap'; then + HOMEBREW_NO_AUTO_UPDATE=1 brew untap aws/tap +fi diff --git a/.github/scripts/prepare-release-continuation.mjs b/.github/scripts/prepare-release-continuation.mjs new file mode 100644 index 00000000..e8d02eb4 --- /dev/null +++ b/.github/scripts/prepare-release-continuation.mjs @@ -0,0 +1,473 @@ +#!/usr/bin/env node +import { + appendFileSync, + closeSync, + mkdirSync, + openSync, + readFileSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +import { + RELEASE_CONTINUATION_GENERATION_CEILING, + RELEASE_CONTINUATION_DEADLINE_DEFERRAL_BUDGET, + RELEASE_CONTINUATION_MAX_DISPATCH_DELAY_SECONDS, + RELEASE_CONTINUATION_RATE_LIMIT_DEFERRAL_BUDGET, + continuationStateIdentity, + createReleaseContinuationContract, + normalizeArtifactIdentity, + parseContinuationJson, + sha256File, + stableJson, + validateReleaseContinuationPointer, + validateReleaseExecutionResult, +} from "../../tools/release/release-continuation-contract.mjs"; +import { bootstrapPublicationPlan } from "../../tools/release/bootstrap-publication-plan.mjs"; +import { runGitHubReadSync } from "../../tools/release/github-read.mjs"; +import { + CONTINUATION_CORE_JOURNAL_MEMBER, + CONTINUATION_PACER_MEMBER, + continuationGitHubStateIdentity, +} from "../../tools/release/github-release-continuation-state.mjs"; +import { normalPublicationPlan } from "../../tools/release/normal-publication-plan.mjs"; +import { + RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, +} from "../../tools/release/release-continuation-read-budget.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const DEFAULT_CONTRACT = "target/release/release-continuation-contract.json"; + +function error(message) { + return new Error(`prepare-release-continuation: ${message}`); +} + +function required(name, environment = process.env) { + const value = environment[name]?.trim(); + if (!value) throw error(`${name} is required`); + return value; +} + +function safeInteger(value, context) { + const rendered = String(value ?? ""); + if (!/^[1-9][0-9]*$/u.test(rendered) || !Number.isSafeInteger(Number(rendered))) { + throw error(`${context} must be a positive safe integer`); + } + return Number(rendered); +} + +function json(raw, context) { + try { return JSON.parse(raw); } catch (cause) { throw error(`${context} must be strict JSON: ${cause.message}`); } +} + +function sortedProducts(raw) { + const value = json(raw, "PRODUCTS_JSON"); + if ( + !Array.isArray(value) + || value.length === 0 + || value.some((item) => typeof item !== "string" || item.length === 0) + || new Set(value).size !== value.length + ) { + throw error("PRODUCTS_JSON must be a nonempty unique string list"); + } + return [...value].sort((left, right) => left < right ? -1 : left > right ? 1 : 0); +} + +export function continuationPointerFromEnvironment(environment = process.env) { + const raw = environment.RELEASE_CONTINUATION_POINTER?.trim() ?? ""; + if (raw.length > 32 * 1024) { + throw error("RELEASE_CONTINUATION_POINTER exceeds 32 KiB"); + } + return parseContinuationJson(raw, "RELEASE_CONTINUATION_POINTER"); +} + +function lockBinding(lock) { + return { + catalogDigest: lock.catalogDigest, + lockDigest: lock.lockDigest, + packageEnvelopeDigest: lock.packageEnvelopeDigest, + }; +} + +function githubJson(environment, repo, endpoint, label, githubReadOptions = {}) { + const raw = runGitHubReadSync(["api", "-H", "X-GitHub-Api-Version: 2022-11-28", `repos/${repo}/${endpoint}`], { + ...githubReadOptions, + cwd: ROOT, + deadlineMs: RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, + environment, + label, + maxBuffer: 4 * 1024 * 1024, + }); + return json(raw, label); +} + +function exactStageArtifact(environment, { operation, releaseCommit }, githubReadOptions = {}) { + if (operation === "publish-bootstrap") return null; + const repo = required("GH_REPO", environment); + const id = safeInteger(required("STAGE_HANDOFF_ARTIFACT_ID", environment), "STAGE_HANDOFF_ARTIFACT_ID"); + const expectedDigest = required("STAGE_HANDOFF_ARTIFACT_DIGEST", environment); + const expectedName = required("STAGE_HANDOFF_ARTIFACT_NAME", environment); + const runId = safeInteger(required("STAGE_HANDOFF_RUN_ID", environment), "STAGE_HANDOFF_RUN_ID"); + const metadata = githubJson( + environment, + repo, + `actions/artifacts/${id}`, + `GitHub-stage artifact ${id}`, + githubReadOptions, + ); + const artifact = normalizeArtifactIdentity({ + digest: metadata.digest, + id: metadata.id, + name: metadata.name, + size: metadata.size_in_bytes, + }, "GitHub-stage artifact metadata"); + if ( + artifact.id !== id + || artifact.digest !== expectedDigest + || artifact.name !== expectedName + || metadata.expired !== false + ) { + throw error("GitHub-stage artifact metadata does not match the sealed stage handoff"); + } + if (metadata.workflow_run?.id !== undefined && Number(metadata.workflow_run.id) !== runId) { + throw error("GitHub-stage artifact is bound to the wrong root Release run"); + } + const run = githubJson( + environment, + repo, + `actions/runs/${runId}`, + `GitHub-stage Release run ${runId}`, + githubReadOptions, + ); + if ( + Number(run.id) !== runId + || run.head_sha !== releaseCommit + || run.event !== "workflow_dispatch" + ) { + throw error("GitHub-stage handoff run is not bound to the exact release workflow dispatch"); + } + return { artifact, runId }; +} + +function atomicJson(file, value) { + atomicBytes(file, Buffer.from(`${JSON.stringify(value, null, 2)}\n`)); +} + +function atomicBytes(file, bytes) { + const absolute = path.resolve(ROOT, file); + mkdirSync(path.dirname(absolute), { recursive: true }); + const temporary = `${absolute}.tmp-${process.pid}`; + let descriptor; + try { + descriptor = openSync(temporary, "wx", 0o600); + writeFileSync(descriptor, bytes); + closeSync(descriptor); + descriptor = undefined; + renameSync(temporary, absolute); + } finally { + if (descriptor !== undefined) closeSync(descriptor); + try { unlinkSync(temporary); } catch {} + } +} + +function snapshotGitHubState(environment, { currentPointer, currentRunId, operation, releaseCommit }) { + if (operation === "publish-bootstrap") return null; + const repository = required("GITHUB_REPOSITORY", environment); + const rootRunId = safeInteger( + required("OLIPHAUNT_RELEASE_ROOT_RUN_ID", environment), + "OLIPHAUNT_RELEASE_ROOT_RUN_ID", + ); + const expectedRootRunId = currentPointer === null ? currentRunId : currentPointer.rootRunId; + if (rootRunId !== expectedRootRunId) { + throw error("installed GitHub state does not belong to the exact continuation root run"); + } + const pacerBytes = readFileSync( + path.resolve(required("OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH", environment)), + ); + const journalBytes = readFileSync( + path.resolve(required("OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH", environment)), + ); + const githubState = continuationGitHubStateIdentity({ + journalBytes, + lineage: { headSha: releaseCommit, repository, rootRunId: String(rootRunId) }, + pacerBytes, + }); + const pacerSnapshot = environment.RELEASE_CONTINUATION_GITHUB_PACER_SNAPSHOT_PATH + || path.join("target/release", CONTINUATION_PACER_MEMBER); + const journalSnapshot = environment.RELEASE_CONTINUATION_GITHUB_CORE_JOURNAL_SNAPSHOT_PATH + || path.join("target/release", CONTINUATION_CORE_JOURNAL_MEMBER); + atomicBytes(pacerSnapshot, pacerBytes); + atomicBytes(journalSnapshot, journalBytes); + return githubState; +} + +export function captureContinuationGitHubState( + environment, + { currentPointer, currentRunId, operation, releaseCommit }, + { githubReadOptions = {} } = {}, +) { + const normalizedPointer = currentPointer === null + ? null + : validateReleaseContinuationPointer(currentPointer, { operation, releaseCommit }); + // The stage-handoff identity costs two journaled GitHub reads. Resolve it + // before freezing the state so a child inherits every charged request. + const stageHandoff = exactStageArtifact( + environment, + { operation, releaseCommit }, + githubReadOptions, + ); + const githubState = snapshotGitHubState(environment, { + currentPointer: normalizedPointer, + currentRunId, + operation, + releaseCommit, + }); + return { githubState, stageHandoff }; +} + +export function buildContinuationContract({ + operation, + releaseCommit, + releaseTree, + lock, + products, + result, + executionResultFile, + githubState, + state, + approvedPublication, + stageHandoff, + currentPointer, + currentRunId, + currentRunAttempt, + exactPlanIds, + nowEpochSeconds = Math.floor(Date.now() / 1000), +}) { + const normalizedResult = validateReleaseExecutionResult(result, { + lock: lockBinding(lock), + operation, + products, + releaseCommit, + releaseTree, + }); + if (normalizedResult.decision !== "deferred") { + throw error("continuation may be prepared only from a typed deferred execution result"); + } + if ( + !Array.isArray(exactPlanIds) + || exactPlanIds.length === 0 + || exactPlanIds.some((id) => typeof id !== "string" || id.length === 0) + || new Set(exactPlanIds).size !== exactPlanIds.length + ) { + throw error("independently derived exact publication plan IDs must be a nonempty unique list"); + } + const exactPlanSet = new Set(exactPlanIds); + for (const [key, ids] of [ + ["admittedIds", normalizedResult.admittedIds], + ["completedIds", normalizedResult.completedIds], + ["remainingIds", normalizedResult.remainingIds], + ]) { + const selected = new Set(ids); + const projection = exactPlanIds.filter((id) => selected.has(id)); + if (stableJson(ids) !== stableJson(projection)) { + throw error(`execution result ${key} is not an exact ordered projection of the lock-derived plan`); + } + } + if ( + normalizedResult.completedIds.length + normalizedResult.remainingIds.length !== exactPlanIds.length + || normalizedResult.completedIds.some((id) => !exactPlanSet.has(id)) + || normalizedResult.remainingIds.some((id) => !exactPlanSet.has(id)) + ) { + throw error("execution result completed/remaining IDs do not exactly partition the lock-derived plan"); + } + const exactPlanCount = exactPlanIds.length; + let generation = 1; + let capacityDeferralAllowance = normalizedResult.deferralMode === "pre-mutation-capacity"; + let maxGenerations = exactPlanCount + + RELEASE_CONTINUATION_RATE_LIMIT_DEFERRAL_BUDGET + + RELEASE_CONTINUATION_DEADLINE_DEFERRAL_BUDGET; + let deadlineDeferralsUsed = normalizedResult.deferralMode === "pre-mutation-deadline" ? 1 : 0; + let rateLimitDeferralsUsed = normalizedResult.deferralMode === "rate-limit" ? 1 : 0; + let rootRunId = currentRunId; + let parentPointer = null; + if ( + normalizedResult.notBeforeEpochSeconds - nowEpochSeconds + > RELEASE_CONTINUATION_MAX_DISPATCH_DELAY_SECONDS + ) { + throw error( + `typed deferral exceeds the ${RELEASE_CONTINUATION_MAX_DISPATCH_DELAY_SECONDS}s ` + + "automatic dispatch ceiling", + ); + } + if (currentPointer !== null) { + const pointer = validateReleaseContinuationPointer(currentPointer, { + operation, + releaseCommit, + }); + parentPointer = pointer; + if (normalizedResult.deferralMode === "pre-mutation-capacity") { + throw error("pre-mutation capacity deferral cannot recur in a continuation generation"); + } + if (pointer.generation >= pointer.maxGenerations) { + throw error("deferred execution exhausted its exact-plan-derived continuation bound"); + } + capacityDeferralAllowance = pointer.capacityDeferralAllowance; + if ( + pointer.deadlineDeferralBudget !== RELEASE_CONTINUATION_DEADLINE_DEFERRAL_BUDGET + || pointer.rateLimitDeferralBudget !== RELEASE_CONTINUATION_RATE_LIMIT_DEFERRAL_BUDGET + || exactPlanCount + pointer.rateLimitDeferralBudget + pointer.deadlineDeferralBudget + !== pointer.maxGenerations + ) { + throw error("execution result plan size drifted from the root continuation bound"); + } + deadlineDeferralsUsed = pointer.deadlineDeferralsUsed; + if (normalizedResult.deferralMode === "pre-mutation-deadline") { + if (deadlineDeferralsUsed >= pointer.deadlineDeferralBudget) { + throw error("typed pre-mutation deadline deferral exhausted its frozen one-shot lineage budget"); + } + deadlineDeferralsUsed += 1; + } + rateLimitDeferralsUsed = pointer.rateLimitDeferralsUsed; + if (normalizedResult.deferralMode === "rate-limit") { + if (rateLimitDeferralsUsed >= pointer.rateLimitDeferralBudget) { + throw error("typed rate-limit deferral exhausted its frozen finite lineage budget"); + } + rateLimitDeferralsUsed += 1; + } + generation = pointer.generation + 1; + maxGenerations = pointer.maxGenerations; + rootRunId = pointer.rootRunId; + } + if (operation === "publish" && parentPointer !== null) { + for (const key of ["pacer", "coreRequestJournal"]) { + const previous = parentPointer.githubState[key]; + const current = githubState?.[key]; + if ( + current === undefined + || current.sequence < previous.sequence + || (current.sequence === previous.sequence && current.digest !== previous.digest) + || (current.sequence > previous.sequence && current.digest === previous.digest) + || ( + previous.lastReservedAtMs !== null + && (current.lastReservedAtMs === null || current.lastReservedAtMs < previous.lastReservedAtMs) + ) + ) { + throw error(`GitHub ${key} state did not monotonically extend the exact parent continuation`); + } + } + } + if (maxGenerations > RELEASE_CONTINUATION_GENERATION_CEILING) { + throw error( + `exact publication plan plus its frozen zero-progress allowances exceeds the ` + + `${RELEASE_CONTINUATION_GENERATION_CEILING}-generation transport ceiling`, + ); + } + return createReleaseContinuationContract({ + approvedPublication, + githubState, + lineage: { + capacityDeferralAllowance, + deadlineDeferralBudget: RELEASE_CONTINUATION_DEADLINE_DEFERRAL_BUDGET, + deadlineDeferralsUsed, + generation, + maxGenerations, + parentRunAttempt: currentRunAttempt, + parentRunId: currentRunId, + rateLimitDeferralBudget: RELEASE_CONTINUATION_RATE_LIMIT_DEFERRAL_BUDGET, + rateLimitDeferralsUsed, + rootRunId, + }, + lock: lockBinding(lock), + operation, + outcome: { + completedCount: normalizedResult.completedIds.length, + decision: "deferred", + deferralMode: normalizedResult.deferralMode, + executionResultDigest: sha256File(executionResultFile), + notBeforeEpochSeconds: normalizedResult.notBeforeEpochSeconds, + progressCount: normalizedResult.newlyCompletedIds.length, + remainingCount: normalizedResult.remainingIds.length, + stateDigest: state.digest, + }, + products, + source: { commit: releaseCommit, tree: releaseTree }, + stageHandoff, + state, + }); +} + +export async function main(environment = process.env) { + const { + assertPublicationLockSource, + loadPublicationLock, + } = await import("../../tools/release/publication-lock.mjs"); + const operation = required("RELEASE_OPERATION", environment); + const releaseCommit = required("RELEASE_HEAD_SHA", environment); + const publicationLockFile = path.resolve(ROOT, required("PUBLICATION_LOCK_PATH", environment)); + const executionResultFile = path.resolve(ROOT, required("RELEASE_EXECUTION_RESULT_PATH", environment)); + const statePath = path.resolve(ROOT, required("RELEASE_CONTINUATION_STATE_PATH", environment)); + const products = sortedProducts(required("PRODUCTS_JSON", environment)); + const lock = loadPublicationLock(publicationLockFile); + assertPublicationLockSource(lock, releaseCommit); + const releaseTree = lock.source.tree; + const exactPlanIds = operation === "publish-bootstrap" + ? bootstrapPublicationPlan(lock, products).map(({ id }) => id) + : normalPublicationPlan(lock, products).operations.map(({ id }) => id); + const result = json(readFileSync(executionResultFile, "utf8"), "release execution result"); + const state = continuationStateIdentity(operation, statePath); + const approvedPublication = { + artifacts: json(required("APPROVED_ARTIFACT_METADATA_JSON", environment), "APPROVED_ARTIFACT_METADATA_JSON"), + runId: safeInteger(required("APPROVED_RUN_ID", environment), "APPROVED_RUN_ID"), + }; + const currentPointer = continuationPointerFromEnvironment(environment); + const currentRunId = safeInteger(required("GITHUB_RUN_ID", environment), "GITHUB_RUN_ID"); + const currentRunAttempt = safeInteger(required("GITHUB_RUN_ATTEMPT", environment), "GITHUB_RUN_ATTEMPT"); + const { githubState, stageHandoff } = captureContinuationGitHubState(environment, { + currentPointer, + currentRunId, + operation, + releaseCommit, + }); + const contract = buildContinuationContract({ + approvedPublication, + currentPointer, + currentRunAttempt, + currentRunId, + exactPlanIds, + lock, + operation, + products, + releaseCommit, + releaseTree, + result, + executionResultFile, + githubState, + stageHandoff, + state, + }); + const output = environment.RELEASE_CONTINUATION_CONTRACT_PATH || DEFAULT_CONTRACT; + atomicJson(output, contract); + if (environment.GITHUB_OUTPUT) { + appendFileSync( + environment.GITHUB_OUTPUT, + `contract_digest=${contract.contractDigest}\nnext_generation=${contract.lineage.generation}\n` + + `not_before_epoch=${contract.outcome.notBeforeEpochSeconds}\n`, + ); + } + console.log( + `sealed ${operation} continuation generation ${contract.lineage.generation}/${contract.lineage.maxGenerations} ` + + `for ${contract.outcome.remainingCount} remaining immutable operation(s)`, + ); + return contract; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((cause) => { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + }); +} diff --git a/.github/scripts/reclaim-android-mobile-build-disk.mjs b/.github/scripts/reclaim-android-mobile-build-disk.mjs new file mode 100644 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/registry-bootstrap-ledger-state.mjs b/.github/scripts/registry-bootstrap-ledger-state.mjs new file mode 100644 index 00000000..1bf258e4 --- /dev/null +++ b/.github/scripts/registry-bootstrap-ledger-state.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { appendFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +import { loadPublicationLock, lockedCarriers } from "../../tools/release/publication-lock.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function fail(message) { + throw new Error(`registry-bootstrap-ledger-state: ${message}`); +} + +function run(command, args, { check = true } = {}) { + const result = spawnSync(command, args, { + cwd: ROOT, + env: process.env, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || (check && result.status !== 0)) { + fail(`${command} ${args.join(" ")} failed: ${(result.stderr || result.stdout || result.error?.message || "").trim()}`); + } + return { status: result.status, stdout: result.stdout.trim() }; +} + +function parseProducts(raw) { + let value; + try { + value = JSON.parse(raw); + } catch (cause) { + fail(`PRODUCTS_JSON is invalid: ${cause.message}`); + } + if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) { + fail("PRODUCTS_JSON must be a non-empty product string list"); + } + return [...new Set(value)].sort(); +} + +export function classifyLedgerRequirement(rows) { + const requiring = rows + .filter((row) => row.published > 0 && row.tagState === "missing") + .map(({ product, ecosystem, published }) => ({ product, ecosystem, published })) + .sort((left, right) => compareText(`${left.product}:${left.ecosystem}`, `${right.product}:${right.ecosystem}`)); + const conflicting = rows.filter((row) => row.tagState === "wrong"); + if (conflicting.length > 0) { + fail(`current product tag points at another commit: ${conflicting.map(({ product }) => product).join(", ")}`); + } + return { needsLedger: requiring.length > 0, requiring }; +} + +function query(lockFile, product, ecosystem) { + const registryKind = ecosystem === "cargo" ? "crates" : "npm"; + const result = run(process.execPath, [ + "tools/release/check_registry_publication.mjs", + "query-product-publication", + "--product", product, + "--registry-kind", registryKind, + "--publication-lock", lockFile, + ]); + try { + return JSON.parse(result.stdout); + } catch (cause) { + fail(`registry query returned invalid JSON for ${product}/${ecosystem}: ${cause.message}`); + } +} + +function tagState(product, version, headCommit) { + const result = run("git", ["rev-parse", "--verify", "--quiet", `refs/tags/${product}-v${version}^{commit}`], { check: false }); + if (result.status !== 0) return "missing"; + return result.stdout === headCommit ? "exact" : "wrong"; +} + +function main() { + const lockFile = path.resolve(ROOT, process.env.PUBLICATION_LOCK_PATH || "target/release/publication-lock.json"); + const products = parseProducts(process.env.PRODUCTS_JSON || ""); + const headCommit = run("git", ["rev-parse", `${process.env.RELEASE_HEAD_SHA || "HEAD"}^{commit}`]).stdout; + const lock = loadPublicationLock(lockFile); + if (lock.source.commit !== headCommit) { + fail(`publication lock source ${lock.source.commit} does not match ${headCommit}`); + } + const rows = []; + for (const product of products) { + const productRow = lock.products.find((entry) => entry.id === product); + if (productRow === undefined) fail(`publication lock omits selected product ${product}`); + for (const ecosystem of ["cargo", "npm"]) { + const carriers = lockedCarriers(lock, { product, ecosystem }); + if (carriers.length === 0) continue; + const result = query(lockFile, product, ecosystem); + rows.push({ + product, + ecosystem, + published: result.published?.length ?? 0, + missing: result.missing?.length ?? 0, + tagState: tagState(product, productRow.version, headCommit), + }); + } + } + const state = classifyLedgerRequirement(rows); + const output = process.env.GITHUB_OUTPUT; + if (output) { + appendFileSync(output, `needs_ledger=${String(state.needsLedger)}\n`); + appendFileSync(output, `state_json=${JSON.stringify(rows)}\n`); + } + console.log(JSON.stringify({ ...state, rows }, null, 2)); +} + +if (import.meta.main) { + try { + main(); + } catch (cause) { + console.error(cause instanceof Error ? cause.message : String(cause)); + process.exit(1); + } +} diff --git a/.github/scripts/release-candidate-lib.mjs b/.github/scripts/release-candidate-lib.mjs new file mode 100644 index 00000000..1a6c6f2c --- /dev/null +++ b/.github/scripts/release-candidate-lib.mjs @@ -0,0 +1,255 @@ +import { createHash } from "node:crypto"; +import { + existsSync, + readFileSync, + readdirSync, +} from "node:fs"; +import path from "node:path"; + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function canonicalValue(value) { + if (Array.isArray(value)) { + return value.map(canonicalValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value).sort().map((key) => [key, canonicalValue(value[key])]), + ); + } + return value; +} + +function sha256(value) { + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} + +function strictJson(file, context) { + let bytes; + try { + bytes = readFileSync(file); + } catch (error) { + throw new Error(`${context} cannot be read at ${file}: ${error.message}`); + } + let value; + try { + value = JSON.parse(bytes.toString("utf8")); + } catch (error) { + throw new Error(`${context} is not strict JSON at ${file}: ${error.message}`); + } + assert(value !== null && !Array.isArray(value) && typeof value === "object", `${context} must be a JSON object`); + return { bytes, value }; +} + +function sortedUniqueStrings(value, context) { + assert(Array.isArray(value), `${context} must be a string list`); + assert(value.every((item) => typeof item === "string" && item.length > 0), `${context} must contain non-empty strings`); + const sorted = [...new Set(value)].sort(); + assert(sorted.length === value.length, `${context} must not contain duplicates`); + assert(JSON.stringify(sorted) === JSON.stringify(value), `${context} must be canonically sorted`); + return sorted; +} + +function uniqueStrings(value, context) { + assert(Array.isArray(value), `${context} must be a string list`); + assert(value.every((item) => typeof item === "string" && item.length > 0), `${context} must contain non-empty strings`); + assert(new Set(value).size === value.length, `${context} must not contain duplicates`); + return value; +} + +export function affectedPlanBinding(planPath, wasixReleaseRegressionRequired) { + assert(typeof wasixReleaseRegressionRequired === "boolean", "WASIX release regression requirement must be boolean"); + const { value: plan } = strictJson(planPath, "affected CI plan"); + const jobs = sortedUniqueStrings(plan.jobs, "affected CI plan jobs"); + const projects = sortedUniqueStrings(plan.projects, "affected CI plan projects"); + const extensionPackageProducts = sortedUniqueStrings( + plan.extension_package_products ?? [], + "affected CI plan extension package products", + ); + const expectedRequirement = jobs.includes("liboliphaunt-wasix-runtime"); + assert( + wasixReleaseRegressionRequired === expectedRequirement, + `affected CI plan WASIX requirement mismatch: jobs imply ${expectedRequirement}, workflow reported ${wasixReleaseRegressionRequired}`, + ); + const canonical = JSON.stringify(canonicalValue(plan)); + return { + digest: sha256(canonical), + jobs, + projects, + extensionPackageProducts, + wasixReleaseRegressionRequired, + }; +} + +function jsonFiles(root) { + assert(existsSync(root), `WASIX evidence artifact root does not exist: ${root}`); + const files = []; + const visit = (directory) => { + for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => compareText(left.name, right.name))) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(file); + } else if (entry.isFile() && entry.name.endsWith(".json")) { + files.push(file); + } + } + }; + visit(root); + return files; +} + +function expectedPublicExtensions(catalogPath) { + const { value: catalog } = strictJson(catalogPath, "extension catalog"); + assert(Array.isArray(catalog.extensions), "extension catalog extensions must be a list"); + return catalog.extensions + .filter((extension) => extension?.promotion?.promoted === true) + .map((extension) => extension.id) + .sort(); +} + +function same(actual, expected, context) { + assert(actual === expected, `${context} mismatch: expected ${expected}, got ${actual}`); +} + +function positiveInteger(value, context) { + assert(Number.isSafeInteger(value) && value > 0, `${context} must be a positive safe integer`); +} + +function findEvidenceRun(root) { + const matches = []; + for (const file of jsonFiles(root)) { + let value; + try { + value = JSON.parse(readFileSync(file, "utf8")); + } catch { + continue; + } + if (value?.schema === "oliphaunt-extension-evidence-v1" && value?.evidenceTier === "wasix-full-lifecycle-v1") { + matches.push({ file, value, bytes: readFileSync(file) }); + } + } + assert(matches.length === 1, `WASIX evidence artifact must contain exactly one full-lifecycle run, found ${matches.length}`); + return matches[0]; +} + +export function wasixEvidenceBinding( + evidenceRoot, + { + repository, + workflow, + runId, + runAttempt, + sha, + tree, + catalogPath = "src/extensions/generated/extensions.catalog.json", + }, +) { + const expectedRunId = Number.parseInt(String(runId), 10); + positiveInteger(expectedRunId, "expected evidence runId"); + positiveInteger(runAttempt, "expected evidence runAttempt"); + const root = path.resolve(evidenceRoot); + const { file, value: evidence, bytes } = findEvidenceRun(root); + same(evidence.status, "passed", "WASIX evidence status"); + same(evidence.sourceCommit, sha, "WASIX evidence sourceCommit"); + same(evidence.sourceTree, tree, "WASIX evidence sourceTree"); + assert(/^sha256:[0-9a-f]{64}$/u.test(evidence.sourceDigest), "WASIX evidence sourceDigest must be SHA-256"); + uniqueStrings(evidence.sourceDigestInputs, "WASIX evidence sourceDigestInputs"); + same(evidence.github?.repository, repository, "WASIX evidence GitHub repository"); + same(evidence.github?.workflow, workflow, "WASIX evidence GitHub workflow"); + same(evidence.github?.runId, expectedRunId, "WASIX evidence GitHub runId"); + same(evidence.github?.runAttempt, runAttempt, "WASIX evidence GitHub runAttempt"); + same(evidence.github?.job, "wasix-release-regression", "WASIX evidence GitHub job"); + + assert(Array.isArray(evidence.results) && evidence.results.length > 0, "WASIX evidence results must be non-empty"); + const extensions = []; + for (const result of evidence.results) { + assert(typeof result?.extension === "string" && result.extension.length > 0, "WASIX evidence result extension is invalid"); + same(result.postgresMajor, 18, `${result.extension} PostgreSQL major`); + same(result.artifactFamily, "wasix-runtime", `${result.extension} artifact family`); + same(result.platformTarget, "portable", `${result.extension} platform target`); + for (const mode of ["direct", "server", "restart", "dump-restore"]) { + same(result.runtimeModeStatuses?.[mode], "passed", `${result.extension} ${mode} status`); + } + extensions.push(result.extension); + } + extensions.sort(); + assert(new Set(extensions).size === extensions.length, "WASIX evidence results must not repeat extensions"); + const expectedExtensions = expectedPublicExtensions(catalogPath); + assert( + JSON.stringify(extensions) === JSON.stringify(expectedExtensions), + "WASIX evidence results must cover every and only promoted public extension", + ); + + return { + artifact: "wasix-release-regression-evidence", + file: path.relative(root, file).split(path.sep).join("/"), + digest: sha256(bytes), + id: evidence.id, + sourceDigest: evidence.sourceDigest, + sourceCommit: evidence.sourceCommit, + sourceTree: evidence.sourceTree, + github: { + repository: evidence.github.repository, + workflow: evidence.github.workflow, + runId: evidence.github.runId, + runAttempt: evidence.github.runAttempt, + job: evidence.github.job, + }, + resultCount: extensions.length, + extensionsDigest: sha256(JSON.stringify(extensions)), + }; +} + +export function assertCandidateBindingShape(candidate) { + assert(candidate?.schemaVersion === 2, `release candidate schemaVersion must be 2, got ${candidate?.schemaVersion}`); + assert(candidate.affectedPlan !== null && typeof candidate.affectedPlan === "object", "release candidate affectedPlan is missing"); + assert(/^sha256:[0-9a-f]{64}$/u.test(candidate.affectedPlan.digest), "release candidate plan digest is invalid"); + const jobs = sortedUniqueStrings(candidate.affectedPlan.jobs, "release candidate affectedPlan.jobs"); + sortedUniqueStrings(candidate.affectedPlan.projects, "release candidate affectedPlan.projects"); + sortedUniqueStrings( + candidate.affectedPlan.extensionPackageProducts, + "release candidate affectedPlan.extensionPackageProducts", + ); + assert( + typeof candidate.affectedPlan.wasixReleaseRegressionRequired === "boolean", + "release candidate affectedPlan WASIX requirement must be boolean", + ); + assert( + candidate.affectedPlan.wasixReleaseRegressionRequired === jobs.includes("liboliphaunt-wasix-runtime"), + "release candidate affectedPlan WASIX requirement is inconsistent with selected jobs", + ); + const requirements = candidate.evidenceRequirements; + assert(requirements !== null && typeof requirements === "object", "release candidate evidenceRequirements is missing"); + assert( + requirements.wasixReleaseRegression === candidate.affectedPlan.wasixReleaseRegressionRequired, + "release candidate WASIX evidence requirement is inconsistent with affected plan", + ); + const expectedArtifacts = requirements.wasixReleaseRegression ? ["wasix-release-regression-evidence"] : []; + assert( + JSON.stringify(requirements.artifacts) === JSON.stringify(expectedArtifacts), + "release candidate evidence artifact requirements are inconsistent", + ); + if (requirements.wasixReleaseRegression) { + assert(candidate.evidence?.wasixReleaseRegression !== null, "release candidate is missing required WASIX evidence binding"); + assert(/^sha256:[0-9a-f]{64}$/u.test(candidate.evidence.wasixReleaseRegression.digest), "release candidate WASIX evidence digest is invalid"); + positiveInteger(candidate.evidence.wasixReleaseRegression.github?.runId, "release candidate WASIX evidence runId"); + positiveInteger(candidate.evidence.wasixReleaseRegression.github?.runAttempt, "release candidate WASIX evidence runAttempt"); + } else { + assert(candidate.evidence?.wasixReleaseRegression === null, "release candidate carries WASIX evidence that its plan did not require"); + } +} + +export function assertBindingMatches(actual, expected, context) { + assert( + JSON.stringify(actual) === JSON.stringify(expected), + `${context} binding does not match the recomputed same-run content`, + ); +} diff --git a/.github/scripts/release-continuation-artifact.mjs b/.github/scripts/release-continuation-artifact.mjs new file mode 100644 index 00000000..f8a8a750 --- /dev/null +++ b/.github/scripts/release-continuation-artifact.mjs @@ -0,0 +1,509 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + normalizeArtifactIdentity, + sha256Bytes, + stableJson, + validateReleaseContinuationContract, + validateReleaseContinuationPointer, + validateReleaseExecutionResult, +} from "../../tools/release/release-continuation-contract.mjs"; +import { + RELEASE_CONTINUATION_ARTIFACT_DOWNLOAD_DEADLINE_MS, + RELEASE_CONTINUATION_AUTHORIZATION_DISCOVERY_DEADLINE_MS, + RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, + RELEASE_CONTINUATION_PARENT_COMPLETION_DEADLINE_MS, +} from "../../tools/release/release-continuation-read-budget.mjs"; +import { + continuationAuthorizationArtifactName, + serializeReleaseContinuationAuthorization, + validateReleaseContinuationAuthorization, +} from "../../tools/release/release-continuation-authorization.mjs"; +import { + RetryableReadError, + retryReadOperationSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; +import { + CONTINUATION_CORE_JOURNAL_MEMBER, + CONTINUATION_PACER_MEMBER, + continuationGitHubStateIdentity, +} from "../../tools/release/github-release-continuation-state.mjs"; + +const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024; +const CHECKPOINT = /^checkpoint-[0-9]{6}-[0-9a-f]{64}[.]json$/u; +const AUTHORIZATION_MEMBER = "release-continuation-authorization.json"; + +function error(message) { + return new Error(`release-continuation-artifact: ${message}`); +} + +function permanentError(message) { + const cause = error(message); + cause.retryable = false; + return cause; +} + +function json(raw, context) { + try { return JSON.parse(raw); } catch (cause) { throw error(`${context} must be strict JSON: ${cause.message}`); } +} + +function command(commandName, args, { binary = false } = {}) { + const result = spawnSync(commandName, args, { + encoding: binary ? null : "utf8", + maxBuffer: MAX_ARTIFACT_BYTES, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined || result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : result.stdout; + throw error(`${commandName} ${args.join(" ")} failed: ${(stderr || stdout || result.error?.message || "").trim()}`); + } + return result.stdout; +} + +function ghJson(repo, endpoint, label) { + return json(runGitHubReadSync( + ["api", "-H", "X-GitHub-Api-Version: 2022-11-28", `repos/${repo}/${endpoint}`], + { + deadlineMs: RELEASE_CONTINUATION_METADATA_READ_DEADLINE_MS, + label, + maxBuffer: 4 * 1024 * 1024, + }, + ), label); +} + +export function validateContinuationArtifactMetadata(metadata, pointer) { + const normalizedPointer = validateReleaseContinuationPointer(pointer); + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + throw error("continuation artifact metadata must be an object"); + } + const observed = normalizeArtifactIdentity({ + digest: metadata.digest, + id: metadata.id, + name: metadata.name, + size: metadata.size_in_bytes, + }, "continuation artifact metadata"); + if (stableJson(observed) !== stableJson(normalizedPointer.artifact)) { + throw error("continuation artifact metadata does not match the exact pointer identity"); + } + if (metadata.expired !== false) throw error("continuation artifact is expired or has ambiguous expiry metadata"); + if ( + metadata.workflow_run?.id !== undefined + && Number(metadata.workflow_run.id) !== normalizedPointer.parentRunId + ) { + throw error("continuation artifact is bound to the wrong parent run"); + } + return observed; +} + +export function validateContinuationRunLineage({ parent, root, current }, pointer) { + const normalized = validateReleaseContinuationPointer(pointer); + const validate = (run, id, context, { requireComplete = false } = {}) => { + if (run === null || Array.isArray(run) || typeof run !== "object") throw error(`${context} metadata must be an object`); + if ( + Number(run.id) !== id + || run.head_sha !== normalized.releaseCommit + || run.event !== "workflow_dispatch" + || !Number.isSafeInteger(Number(run.workflow_id)) + ) { + throw error(`${context} is not bound to the exact Release workflow and release commit`); + } + if (requireComplete && (run.status !== "completed" || run.conclusion !== "success")) { + throw error(`${context} must be a successful completed continuation parent`); + } + return Number(run.workflow_id); + }; + const parentWorkflow = validate(parent, normalized.parentRunId, "parent Release run", { requireComplete: true }); + if (Number(parent.run_attempt) !== normalized.parentRunAttempt) { + throw error("parent Release run attempt does not match the continuation pointer"); + } + const rootWorkflow = validate(root, normalized.rootRunId, "root Release run", { requireComplete: true }); + const currentWorkflow = validate(current.metadata, current.id, "current Release run"); + if (parentWorkflow !== rootWorkflow || parentWorkflow !== currentWorkflow) { + throw error("continuation root, parent, and current run use different workflows"); + } + if (current.id === normalized.parentRunId) throw error("continuation cannot consume its own parent artifact"); + return parentWorkflow; +} + +export function requireCompletedContinuationParent(run, pointer) { + const normalized = validateReleaseContinuationPointer(pointer); + if (run === null || Array.isArray(run) || typeof run !== "object") { + throw permanentError("parent Release run metadata must be an object"); + } + if ( + Number(run.id) !== normalized.parentRunId + || Number(run.run_attempt) !== normalized.parentRunAttempt + || run.head_sha !== normalized.releaseCommit + || run.event !== "workflow_dispatch" + ) { + throw permanentError("parent Release run metadata does not match the continuation pointer"); + } + if (run.status !== "completed") { + throw new RetryableReadError("exact continuation parent is still running"); + } + if (run.conclusion !== "success") { + throw permanentError("exact continuation parent completed without success"); + } + return run; +} + +function completedContinuationParent(repo, pointer) { + return retryReadOperationSync( + `wait for exact continuation parent ${pointer.parentRunId} attempt ${pointer.parentRunAttempt}`, + () => requireCompletedContinuationParent( + ghJson(repo, `actions/runs/${pointer.parentRunId}`, `parent Release run ${pointer.parentRunId}`), + pointer, + ), + { + attemptTimeoutMs: 60_000, + baseDelayMs: 2_000, + deadlineMs: RELEASE_CONTINUATION_PARENT_COMPLETION_DEADLINE_MS, + maxAttempts: 10, + maxDelayMs: 60_000, + }, + ); +} + +export function selectContinuationAuthorizationArtifact(listing, pointer) { + const normalized = validateReleaseContinuationPointer(pointer); + if ( + listing === null + || Array.isArray(listing) + || typeof listing !== "object" + || !Array.isArray(listing.artifacts) + ) { + throw permanentError("continuation authorization artifact listing must be an object"); + } + const expectedName = continuationAuthorizationArtifactName(normalized); + const matches = listing.artifacts.filter((candidate) => candidate?.name === expectedName); + if (matches.length === 0) { + throw new RetryableReadError("exact dispatched-child authorization artifact is not visible yet"); + } + if (matches.length !== 1) { + throw permanentError("exact dispatched-child authorization artifact identity is ambiguous"); + } + const [metadata] = matches; + const artifact = normalizeArtifactIdentity({ + digest: metadata.digest, + id: metadata.id, + name: metadata.name, + size: metadata.size_in_bytes, + }, "continuation authorization artifact metadata"); + if ( + metadata.expired !== false + || Number(metadata.workflow_run?.id) !== normalized.parentRunId + ) { + throw permanentError("continuation authorization artifact is expired or bound to the wrong parent run"); + } + return artifact; +} + +function exactAuthorizationArtifact(repo, pointer) { + const name = continuationAuthorizationArtifactName(pointer); + return retryReadOperationSync( + `wait for dispatched-child authorization ${name}`, + () => selectContinuationAuthorizationArtifact( + ghJson( + repo, + `actions/runs/${pointer.parentRunId}/artifacts?name=${encodeURIComponent(name)}&per_page=100`, + `continuation authorization artifacts for parent ${pointer.parentRunId}`, + ), + pointer, + ), + { + attemptTimeoutMs: 60_000, + baseDelayMs: 2_000, + deadlineMs: RELEASE_CONTINUATION_AUTHORIZATION_DISCOVERY_DEADLINE_MS, + maxAttempts: 10, + maxDelayMs: 60_000, + }, + ); +} + +function exactArtifactZip(repo, artifact, label) { + return retryReadOperationSync( + label, + ({ attemptTimeoutMs }) => runGitHubReadSync( + [ + "api", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + `repos/${repo}/actions/artifacts/${artifact.id}/zip`, + ], + { + binary: true, + label, + maxBuffer: MAX_ARTIFACT_BYTES, + attemptTimeoutMs, + deadlineMs: attemptTimeoutMs, + maxAttempts: 1, + baseDelayMs: 0, + maxDelayMs: 0, + }, + ), + { + attemptTimeoutMs: 5 * 60_000, + deadlineMs: RELEASE_CONTINUATION_ARTIFACT_DOWNLOAD_DEADLINE_MS, + maxAttempts: 4, + }, + ); +} + +export function openContinuationAuthorization(bytes, artifact, pointer, { currentRunId, repo }) { + if ( + !Buffer.isBuffer(bytes) + || bytes.length !== artifact.size + || `sha256:${sha256Bytes(bytes)}` !== artifact.digest + ) { + throw error("continuation authorization ZIP does not match immutable GitHub metadata"); + } + const directory = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-continuation-authorization-")); + try { + const archive = path.join(directory, "authorization.zip"); + writeFileSync(archive, bytes, { flag: "wx", mode: 0o600 }); + const members = command("unzip", ["-Z1", archive]).split(/\r?\n/u).filter(Boolean); + if (members.length !== 1 || members[0] !== AUTHORIZATION_MEMBER) { + throw error(`continuation authorization artifact must contain only ${AUTHORIZATION_MEMBER}`); + } + const receiptBytes = command("unzip", ["-p", archive, AUTHORIZATION_MEMBER], { binary: true }); + if (!Buffer.isBuffer(receiptBytes) || receiptBytes.length === 0 || receiptBytes.length > 32 * 1024) { + throw error("continuation authorization receipt has an invalid size"); + } + const receipt = validateReleaseContinuationAuthorization( + json(receiptBytes.toString("utf8"), "continuation authorization receipt"), + { currentRunId, pointer, repo }, + ); + if (receiptBytes.toString("utf8") !== serializeReleaseContinuationAuthorization(receipt)) { + throw error("continuation authorization receipt is not canonical JSON"); + } + return receipt; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +export function readContinuationAuthorization(repo, pointer, currentRunId) { + const artifact = exactAuthorizationArtifact(repo, pointer); + const bytes = exactArtifactZip( + repo, + artifact, + `download dispatched-child authorization artifact ${artifact.id}`, + ); + return openContinuationAuthorization(bytes, artifact, pointer, { currentRunId, repo }); +} + +export function readExactContinuationArtifact(repo, pointer) { + const normalized = validateReleaseContinuationPointer(pointer); + const metadata = ghJson(repo, `actions/artifacts/${normalized.artifact.id}`, `continuation artifact ${normalized.artifact.id}`); + validateContinuationArtifactMetadata(metadata, normalized); + const bytes = exactArtifactZip( + repo, + normalized.artifact, + `download continuation artifact ${normalized.artifact.id}`, + ); + if (!Buffer.isBuffer(bytes) || bytes.length < 4 || bytes[0] !== 0x50 || bytes[1] !== 0x4b) { + throw error("continuation artifact transport did not return a ZIP archive"); + } + const observedDigest = `sha256:${sha256Bytes(bytes)}`; + if (bytes.length !== normalized.artifact.size || observedDigest !== normalized.artifact.digest) { + throw error("continuation artifact ZIP size/digest does not match the exact pointer"); + } + const parent = completedContinuationParent(repo, normalized); + const root = normalized.rootRunId === normalized.parentRunId + ? parent + : ghJson(repo, `actions/runs/${normalized.rootRunId}`, `root Release run ${normalized.rootRunId}`); + return { bytes, metadata, parent, root }; +} + +function memberContract(operation) { + const common = new Set(["release-continuation-contract.json"]); + if (operation === "publish-bootstrap") { + common.add("bootstrap-execution-result.json"); + common.add("bootstrap-ledger/"); + } else { + common.add("normal-publication-execution-result.json"); + common.add("normal-publication-checkpoint.json"); + common.add(CONTINUATION_PACER_MEMBER); + common.add(CONTINUATION_CORE_JOURNAL_MEMBER); + } + return common; +} + +function memberBytes(archive, member) { + const bytes = command("unzip", ["-p", archive, member], { binary: true }); + if (!Buffer.isBuffer(bytes) || bytes.length === 0 || bytes.length > MAX_ARTIFACT_BYTES) { + throw error(`continuation artifact member ${member} has an invalid size`); + } + return bytes; +} + +export function openContinuationEnvelope(bytes, pointer) { + const normalizedPointer = validateReleaseContinuationPointer(pointer); + if (!Buffer.isBuffer(bytes)) throw error("continuation envelope must be opened from exact ZIP bytes"); + const directory = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-release-continuation-")); + try { + const archive = path.join(directory, "continuation.zip"); + writeFileSync(archive, bytes, { flag: "wx", mode: 0o600 }); + const members = command("unzip", ["-Z1", archive]).split(/\r?\n/u).filter(Boolean); + const allowed = memberContract(normalizedPointer.operation); + const seen = new Set(); + const checkpoints = []; + for (const member of members) { + if ( + member.startsWith("/") + || member.includes("\\") + || member.split("/").includes("..") + || /[\u0000-\u001f\u007f]/u.test(member) + || seen.has(member) + ) { + throw error(`continuation artifact contains unsafe or repeated member ${JSON.stringify(member)}`); + } + seen.add(member); + const checkpointName = member.startsWith("bootstrap-ledger/") + ? member.slice("bootstrap-ledger/".length) + : member; + if (normalizedPointer.operation === "publish-bootstrap" && CHECKPOINT.test(checkpointName)) { + if (member !== checkpointName && member !== `bootstrap-ledger/${checkpointName}`) { + throw error(`bootstrap checkpoint has unexpected archive path ${member}`); + } + checkpoints.push({ member, name: checkpointName }); + } else if (!allowed.has(member)) { + throw error(`continuation artifact contains unexpected member ${JSON.stringify(member)}`); + } + } + const resultName = normalizedPointer.operation === "publish-bootstrap" + ? "bootstrap-execution-result.json" + : "normal-publication-execution-result.json"; + const stateName = "normal-publication-checkpoint.json"; + const requiredMembers = ["release-continuation-contract.json", resultName]; + if (normalizedPointer.operation === "publish") { + requiredMembers.push(CONTINUATION_PACER_MEMBER, CONTINUATION_CORE_JOURNAL_MEMBER); + } + for (const required of requiredMembers) { + if (!seen.has(required)) throw error(`continuation artifact is missing ${required}`); + } + if (normalizedPointer.operation === "publish" && !seen.has(stateName)) { + throw error(`continuation artifact is missing ${stateName}`); + } + if (normalizedPointer.operation === "publish-bootstrap" && checkpoints.length === 0) { + throw error("bootstrap continuation artifact contains no immutable checkpoints"); + } + const contractBytes = memberBytes(archive, "release-continuation-contract.json"); + const contract = validateReleaseContinuationContract( + json(contractBytes.toString("utf8"), "continuation contract"), + { + contractDigest: normalizedPointer.contractDigest, + generation: normalizedPointer.generation, + operation: normalizedPointer.operation, + parentRunAttempt: normalizedPointer.parentRunAttempt, + parentRunId: normalizedPointer.parentRunId, + releaseCommit: normalizedPointer.releaseCommit, + rootRunId: normalizedPointer.rootRunId, + }, + ); + if ( + contract.lineage.maxGenerations !== normalizedPointer.maxGenerations + || contract.lineage.capacityDeferralAllowance + !== normalizedPointer.capacityDeferralAllowance + || contract.lineage.deadlineDeferralBudget + !== normalizedPointer.deadlineDeferralBudget + || contract.lineage.deadlineDeferralsUsed + !== normalizedPointer.deadlineDeferralsUsed + || contract.lineage.rateLimitDeferralBudget + !== normalizedPointer.rateLimitDeferralBudget + || contract.lineage.rateLimitDeferralsUsed + !== normalizedPointer.rateLimitDeferralsUsed + || stableJson(contract.githubState) !== stableJson(normalizedPointer.githubState) + ) { + throw error("continuation contract generation policy differs from its pointer"); + } + const executionResultBytes = memberBytes(archive, resultName); + if (sha256Bytes(executionResultBytes) !== contract.outcome.executionResultDigest) { + throw error("continuation execution-result bytes do not match the contract"); + } + const executionResult = validateReleaseExecutionResult( + json(executionResultBytes.toString("utf8"), "continuation execution result"), + { + lock: contract.lock, + operation: contract.operation, + products: contract.products, + releaseCommit: contract.source.commit, + releaseTree: contract.source.tree, + }, + ); + if ( + executionResult.decision !== "deferred" + || executionResult.deferralMode !== contract.outcome.deferralMode + || executionResult.completedIds.length !== contract.outcome.completedCount + || executionResult.newlyCompletedIds.length !== contract.outcome.progressCount + || executionResult.remainingIds.length !== contract.outcome.remainingCount + || executionResult.notBeforeEpochSeconds !== contract.outcome.notBeforeEpochSeconds + ) { + throw error("continuation execution result disagrees with the sealed outcome envelope"); + } + let stateBytes = null; + let githubPacerBytes = null; + let githubCoreJournalBytes = null; + let checkpointEntries = []; + if (normalizedPointer.operation === "publish") { + stateBytes = memberBytes(archive, stateName); + if (sha256Bytes(stateBytes) !== contract.state.digest || contract.state.entryCount !== 1) { + throw error("normal-publication checkpoint bytes do not match the continuation contract"); + } + githubPacerBytes = memberBytes(archive, CONTINUATION_PACER_MEMBER); + githubCoreJournalBytes = memberBytes(archive, CONTINUATION_CORE_JOURNAL_MEMBER); + const observedGitHubState = continuationGitHubStateIdentity({ + journalBytes: githubCoreJournalBytes, + lineage: { + headSha: contract.source.commit, + repository: contract.githubState.repository, + rootRunId: String(contract.lineage.rootRunId), + }, + pacerBytes: githubPacerBytes, + }); + if (stableJson(observedGitHubState) !== stableJson(contract.githubState)) { + throw error("continuation GitHub pacer/journal bytes do not match the contract"); + } + } else { + checkpointEntries = checkpoints + .map(({ member, name }) => ({ bytes: memberBytes(archive, member), name })) + .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0); + if (new Set(checkpointEntries.map(({ name }) => name)).size !== checkpointEntries.length) { + throw error("bootstrap continuation artifact repeats a checkpoint identity"); + } + const identity = checkpointEntries.map(({ bytes: entryBytes, name }) => ({ + name, + sha256: sha256Bytes(entryBytes), + size: entryBytes.length, + })); + if ( + sha256Bytes(stableJson(identity)) !== contract.state.digest + || checkpointEntries.length !== contract.state.entryCount + ) { + throw error("bootstrap checkpoint set does not match the continuation contract"); + } + } + return { + checkpointEntries, + contract, + contractBytes, + executionResult, + executionResultBytes, + githubCoreJournalBytes, + githubPacerBytes, + stateBytes, + }; + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +export function currentRunMetadata(repo, currentRunId) { + return ghJson(repo, `actions/runs/${currentRunId}`, `current Release run ${currentRunId}`); +} diff --git a/.github/scripts/require-current-main.sh b/.github/scripts/require-current-main.sh new file mode 100644 index 00000000..37e89e27 --- /dev/null +++ b/.github/scripts/require-current-main.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +expected_sha="${1:-${GITHUB_SHA:-}}" + +if [[ -z "$expected_sha" ]]; then + echo "usage: require-current-main.sh " >&2 + exit 2 +fi +if [[ ! "$expected_sha" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "expected a full 40-character commit SHA, got: $expected_sha" >&2 + exit 2 +fi +if [[ "${GITHUB_REF:-}" != "refs/heads/main" ]]; then + echo "release operations must be dispatched from main; got ${GITHUB_REF:-}" >&2 + exit 2 +fi + +expected_sha="$(git rev-parse "${expected_sha}^{commit}")" +git fetch --no-tags origin "+refs/heads/main:refs/remotes/origin/main" +remote_main_sha="$(git rev-parse "refs/remotes/origin/main^{commit}")" + +if [[ "$expected_sha" != "$remote_main_sha" ]]; then + echo "main moved after this workflow was dispatched." >&2 + echo "workflow commit: $expected_sha" >&2 + echo "current main: $remote_main_sha" >&2 + echo "dispatch a new release workflow for the current main commit." >&2 + exit 1 +fi + +echo "validated current main commit: $expected_sha" diff --git a/.github/scripts/require-workflow-success.sh b/.github/scripts/require-workflow-success.sh index be359444..26b6f9df 100644 --- a/.github/scripts/require-workflow-success.sh +++ b/.github/scripts/require-workflow-success.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -workflow="${1:?usage: require-workflow-success.sh [timeout-seconds] [--artifact ...]}" -sha="${2:?usage: require-workflow-success.sh [timeout-seconds] [--artifact ...]}" +workflow="${1:?usage: require-workflow-success.sh [timeout-seconds] [--job ...] [--artifact ...] [--event ...]}" +sha="${2:?usage: require-workflow-success.sh [timeout-seconds] [--job ...] [--artifact ...] [--event ...]}" timeout="${3:-7200}" if [[ $# -ge 3 ]]; then shift 3 @@ -11,12 +11,28 @@ else fi required_artifacts=() +required_jobs=() +required_events=() +expected_run_id="" +selected_artifacts_json='[]' while [[ $# -gt 0 ]]; do case "$1" in + --run-id) + expected_run_id="${2:?--run-id requires a run id}" + shift 2 + ;; + --job) + required_jobs+=("${2:?--job requires a name}") + shift 2 + ;; --artifact) required_artifacts+=("${2:?--artifact requires a name}") shift 2 ;; + --event) + required_events+=("${2:?--event requires an event name}") + shift 2 + ;; *) echo "unknown argument: $1" >&2 exit 2 @@ -26,42 +42,379 @@ done : "${GH_TOKEN:?GH_TOKEN is required}" : "${GH_REPO:?GH_REPO is required}" +if [[ ! "$sha" =~ ^[0-9A-Fa-f]{40}$ ]]; then + echo "workflow gate SHA must be a full hexadecimal commit SHA" >&2 + exit 2 +fi + +github_read() { + local label="${1:?GitHub read label is required}" + shift + node tools/release/github-read.mjs --label "$label" -- "$@" +} + +github_paginated_json() { + local label="${1:?GitHub paginated read label is required}" + local field="${2:?GitHub paginated read field is required}" + local endpoint="${3:?GitHub paginated read endpoint is required}" + node tools/release/github-read.mjs \ + --label "$label" \ + --paginate-field "$field" \ + -- "$endpoint" +} + +normalize_sha() { + LC_ALL=C tr '[:upper:]' '[:lower:]' +} + +emit_run_id() { + local run_id="$1" + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + { + echo "run_id=$run_id" + echo "artifact_metadata_json=$selected_artifacts_json" + } >> "$GITHUB_OUTPUT" + fi + echo "selected $workflow run $run_id" +} + +run_matches_request() { + local run_id="$1" + local row status + row="$( + github_read "$workflow run $run_id metadata" \ + api "repos/$GH_REPO/actions/runs/$run_id" \ + --jq '[.head_sha, .workflow_id, .event, .status, (.conclusion // "")] | @tsv' + )" || { + status=$? + echo "failed to inspect $workflow run $run_id" >&2 + return "$status" + } + + local run_sha workflow_id run_event run_status run_conclusion workflow_name + IFS=$'\t' read -r run_sha workflow_id run_event run_status run_conclusion <<< "$row" + if [[ "$(printf '%s' "$run_sha" | normalize_sha)" != "$(printf '%s' "$sha" | normalize_sha)" ]]; then + echo "$workflow run $run_id belongs to $run_sha, not $sha" >&2 + return 1 + fi + workflow_name="$( + github_read "workflow $workflow_id metadata" \ + api "repos/$GH_REPO/actions/workflows/$workflow_id" --jq .name + )" || { + status=$? + return "$status" + } + if [[ "$workflow_name" != "$workflow" ]]; then + echo "run $run_id is workflow $workflow_name, not $workflow" >&2 + return 1 + fi + if [[ "${#required_events[@]}" -gt 0 ]]; then + local expected_event matched_event=false + for expected_event in "${required_events[@]}"; do + if [[ "$run_event" == "$expected_event" ]]; then + matched_event=true + break + fi + done + if [[ "$matched_event" != true ]]; then + echo "$workflow run $run_id has event $run_event; expected one of: ${required_events[*]}" >&2 + return 1 + fi + fi + # A successful named job is not sufficient release evidence while the + # enclosing run is still mutable or has an unsuccessful final conclusion. + # This also keeps the waiter aligned with the documented non-cancelled, + # exact-SHA qualification contract. + if [[ "$run_status" != "completed" || "$run_conclusion" != "success" ]]; then + echo "$workflow run $run_id is $run_status/${run_conclusion:-}, not completed/success" >&2 + return 1 + fi +} required_artifacts_present() { - run_id="$1" + local run_id="$1" if [[ "${#required_artifacts[@]}" -eq 0 ]]; then + selected_artifacts_json='[]' return 0 fi - artifacts="$(gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts" \ - --paginate \ - --jq '.artifacts[].name')" || return 1 - for expected in "${required_artifacts[@]}"; do - if ! printf '%s\n' "$artifacts" | grep -Fxq "$expected"; then - return 1 - fi - done + local artifacts_json + artifacts_json="$( + github_paginated_json \ + "$workflow run $run_id artifact inventory" \ + artifacts \ + "repos/$GH_REPO/actions/runs/$run_id/artifacts" + )" || { + local status=$? + echo "failed to list artifacts for $workflow run $run_id" >&2 + return "$status" + } + local required_json + required_json="$(printf '%s\n' "${required_artifacts[@]}" | bun -e ' +const names = (await Bun.stdin.text()).split(/\r?\n/u).filter(Boolean); +process.stdout.write(JSON.stringify(names)); +')" + local selected status + # shellcheck disable=SC2016 + if selected="$(REQUIRED_ARTIFACTS_JSON="$required_json" bun -e ' +const expected = JSON.parse(process.env.REQUIRED_ARTIFACTS_JSON); +let records; +try { + records = JSON.parse(await Bun.stdin.text()); +} catch (cause) { + console.error(`artifact inventory is not valid JSON: ${cause.message}`); + process.exit(64); +} +if (!Array.isArray(expected) || expected.length === 0 || new Set(expected).size !== expected.length) { + console.error("required artifact identity list is malformed"); + process.exit(64); +} +if (!Array.isArray(records) || records.some((entry) => + entry === null || Array.isArray(entry) || typeof entry !== "object" || + typeof entry.name !== "string" || typeof entry.expired !== "boolean" || + !Number.isSafeInteger(entry.id) || entry.id < 1 || + !Number.isSafeInteger(entry.size_in_bytes) || entry.size_in_bytes < 1 || + typeof entry.digest !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(entry.digest) +)) { + console.error("artifact inventory contains malformed metadata"); + process.exit(64); +} +const selected = []; +for (const name of expected) { + const matches = records.filter((entry) => entry.name === name && entry.expired === false); + if (matches.length !== 1) { + console.error(`expected exactly one non-expired artifact named ${name}; found ${matches.length}`); + process.exit(1); + } + const [entry] = matches; + selected.push({ digest: entry.digest, id: entry.id, name: entry.name, size: entry.size_in_bytes }); +} +selected.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0); +process.stdout.write(JSON.stringify(selected)); +' <<< "$artifacts_json")"; then + selected_artifacts_json="$selected" + else + status=$? + selected_artifacts_json='[]' + return "$status" + fi +} + +required_jobs_success() { + local run_id="$1" + if [[ "${#required_jobs[@]}" -eq 0 ]]; then + return 0 + fi + + local jobs_file + jobs_file="$(mktemp)" + local status + if github_read "$workflow run $run_id jobs" \ + run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"; then + : + else + status=$? + rm -f "$jobs_file" + return "$status" + fi + + local conclusion + # shellcheck disable=SC2016 + if ! conclusion="$( + bun -e ' +const fs = require("node:fs"); +const data = JSON.parse(fs.readFileSync(Bun.argv[1], "utf8")); +const required = Bun.argv.slice(2); +if (!Array.isArray(data.jobs)) { + console.error("workflow job inventory must be a list"); + process.exit(1); +} +const failures = required + .map((name) => { + const matches = data.jobs.filter((job) => job?.name === name); + if (matches.length !== 1) return [name, `count-${matches.length}`]; + return [name, matches[0]?.conclusion ?? "missing"]; + }) + .filter(([, conclusion]) => conclusion !== "success"); +if (failures.length > 0) { + console.error(failures.map(([name, conclusion]) => `${name}=${conclusion}`).join(", ")); + process.exit(1); +} +' "$jobs_file" "${required_jobs[@]}" + )"; then + rm -f "$jobs_file" + return 1 + fi + rm -f "$jobs_file" + [[ -z "$conclusion" ]] +} + +candidate_satisfies_gate() { + local run_id="$1" + local status + if run_matches_request "$run_id"; then + : + else + status=$? + return "$status" + fi + if required_jobs_success "$run_id"; then + : + else + status=$? + return "$status" + fi + if required_artifacts_present "$run_id"; then + : + else + status=$? + return "$status" + fi +} + +resolve_workflow_id() { + local workflows_json + workflows_json="$( + github_paginated_json \ + "$workflow workflow inventory" \ + workflows \ + "repos/$GH_REPO/actions/workflows" + )" || return $? + # Resolve the immutable workflow id from the exact display name. This avoids + # gh run list's arbitrary latest-N truncation and refuses ambiguous names. + # shellcheck disable=SC2016 + WORKFLOW_NAME="$workflow" bun -e ' +const expected = process.env.WORKFLOW_NAME; +let rows; +try { + rows = JSON.parse(await Bun.stdin.text()); +} catch (cause) { + console.error(`workflow inventory is not valid JSON: ${cause.message}`); + process.exit(1); +} +if (!Array.isArray(rows)) { + console.error("workflow inventory must be a list"); + process.exit(1); +} +const matches = rows.filter((row) => row?.name === expected); +if (matches.length !== 1 || !Number.isSafeInteger(matches[0]?.id) || matches[0].id < 1) { + console.error(`expected exactly one workflow named ${expected}; found ${matches.length}`); + process.exit(1); +} +process.stdout.write(String(matches[0].id)); +' <<< "$workflows_json" || return 64 } +exact_sha_workflow_runs() { + local workflow_id="$1" + local runs_json + runs_json="$( + github_paginated_json \ + "$workflow exact-SHA run inventory for $sha" \ + workflow_runs \ + "repos/$GH_REPO/actions/workflows/$workflow_id/runs?head_sha=$sha" + )" || return $? + # shellcheck disable=SC2016 + EXPECTED_SHA="$(printf '%s' "$sha" | normalize_sha)" bun -e ' +const expectedSha = process.env.EXPECTED_SHA; +let rows; +try { + rows = JSON.parse(await Bun.stdin.text()); +} catch (cause) { + console.error(`workflow run inventory is not valid JSON: ${cause.message}`); + process.exit(1); +} +if (!Array.isArray(rows)) { + console.error("workflow run inventory must be a list"); + process.exit(1); +} +const ids = new Set(); +const rendered = []; +for (const row of rows) { + const conclusion = row?.conclusion ?? ""; + if ( + row === null || Array.isArray(row) || typeof row !== "object" || + !Number.isSafeInteger(row.id) || row.id < 1 || ids.has(row.id) || + typeof row.head_sha !== "string" || row.head_sha.toLowerCase() !== expectedSha || + typeof row.status !== "string" || typeof conclusion !== "string" || + typeof row.html_url !== "string" || /[\t\r\n]/u.test(row.html_url) || + typeof row.event !== "string" || /[\t\r\n]/u.test(row.event) + ) { + console.error("workflow run inventory contains malformed, duplicate, or non-exact-SHA metadata"); + process.exit(1); + } + ids.add(row.id); + rendered.push([row.id, row.status, conclusion, row.html_url, row.event].join("\t")); +} +process.stdout.write(rendered.join("\n")); +' <<< "$runs_json" || return 64 +} + +if [[ -n "$expected_run_id" ]]; then + if candidate_satisfies_gate "$expected_run_id"; then + emit_run_id "$expected_run_id" + exit 0 + else + status=$? + fi + if [[ "$status" -eq 64 || "$status" -eq 75 ]]; then + echo "$workflow run $expected_run_id could not be read within the bounded GitHub read policy" >&2 + exit "$status" + fi + echo "$workflow run $expected_run_id does not satisfy the required job/artifact gate" >&2 + exit 1 +fi + deadline=$((SECONDS + timeout)) +workflow_id="" while true; do - runs="$(gh run list \ - --workflow "$workflow" \ - --commit "$sha" \ - --limit 10 \ - --json databaseId,status,conclusion,url,event \ - --jq '.[] | [.databaseId, .status, (.conclusion // ""), .url, .event] | @tsv')" + if [[ -z "$workflow_id" ]]; then + if workflow_id="$(resolve_workflow_id)"; then + : + else + status=$? + if [[ "$status" -eq 64 ]]; then + echo "permanent GitHub read failure while resolving $workflow" >&2 + exit 64 + fi + echo "transient GitHub read budget exhausted while resolving $workflow; the waiter remains active" >&2 + workflow_id="" + fi + fi + if [[ -n "$workflow_id" ]] && runs="$(exact_sha_workflow_runs "$workflow_id")"; then + : + else + status=$? + if [[ "$status" -eq 64 ]]; then + echo "permanent GitHub read failure while searching for $workflow at $sha" >&2 + exit 64 + fi + echo "transient GitHub read budget exhausted while searching for $workflow at $sha; the waiter remains active" >&2 + runs="" + fi if [ -n "$runs" ]; then echo "$runs" - for run_id in $(echo "$runs" | awk -F '\t' '$2 == "completed" && $3 == "success" { print $1 }'); do - if required_artifacts_present "$run_id"; then + candidate_run_ids="$(echo "$runs" | awk -F '\t' '$2 == "completed" && $3 == "success" { print $1 }')" + for run_id in $candidate_run_ids; do + if candidate_satisfies_gate "$run_id"; then + emit_run_id "$run_id" exit 0 + else + status=$? + fi + if [[ "$status" -eq 64 ]]; then + echo "permanent GitHub read failure while inspecting $workflow run $run_id" >&2 + exit 64 + fi + if [[ "$status" -eq 75 ]]; then + echo "transient GitHub read budget exhausted while inspecting $workflow run $run_id; the waiter remains active" + continue fi - echo "$workflow run $run_id is successful but is missing one or more required artifacts" + echo "$workflow run $run_id does not satisfy the required job/artifact gate" done if echo "$runs" | awk -F '\t' '$2 != "completed" { active=1 } END { exit active ? 0 : 1 }'; then echo "$workflow is still running for $sha" - elif echo "$runs" | awk -F '\t' '$2 == "completed" && $3 != "success" && $5 != "workflow_dispatch" { failed=1 } END { exit failed ? 0 : 1 }'; then + elif [[ "${#required_jobs[@]}" -eq 0 ]] && echo "$runs" | awk -F '\t' '$2 == "completed" && $3 != "success" { failed=1 } END { exit failed ? 0 : 1 }'; then echo "$workflow failed for $sha" >&2 exit 1 else diff --git a/.github/scripts/resolve-mobile-e2e.mjs b/.github/scripts/resolve-mobile-e2e.mjs new file mode 100644 index 00000000..3f0d1f46 --- /dev/null +++ b/.github/scripts/resolve-mobile-e2e.mjs @@ -0,0 +1,300 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { appendFileSync } from "node:fs"; +import { env, exit } from "node:process"; + +import { + retryReadOperationSync, + runGitHubPaginatedJsonSync, + runGitHubReadSync, +} from "../../tools/release/github-read.mjs"; + +const FULL_SHA = /^[0-9a-f]{40}$/u; +const FULL_SHA_INPUT = /^[0-9a-f]{40}$/iu; +const RUN_ID = /^[1-9][0-9]*$/u; +const MOBILE_ARTIFACTS = Object.freeze({ + android: "react-native-mobile-android-app-android-x86_64", + ios: "react-native-mobile-ios-app", +}); + +function run(command, args, { environment = process.env } = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + env: environment, + }); + if (result.error) { + throw new Error(result.error.message); + } + if (result.status !== 0) { + const detail = [result.stdout, result.stderr] + .map((value) => value?.trimEnd()) + .filter(Boolean) + .join("\n"); + throw new Error( + `${command} ${args.join(" ")} failed with exit code ${result.status}` + + (detail ? `:\n${detail}` : ""), + ); + } + return result.stdout; +} + +function parsedJson(output, label) { + const text = output.trim(); + if (!text) return null; + try { + return JSON.parse(text); + } catch (error) { + throw new Error(`${label} returned invalid JSON: ${error.message}`); + } +} + +function outputWriter(environment) { + return (name, value) => { + const rendered = `${name}=${value}\n`; + if (environment.GITHUB_OUTPUT) { + appendFileSync(environment.GITHUB_OUTPUT, rendered, "utf8"); + } else { + process.stdout.write(rendered); + } + }; +} + +export function mobilePlatformSelection(value = "all") { + if (!new Set(["all", "android", "ios"]).has(value)) { + throw new Error( + `unsupported mobile E2E platform ${JSON.stringify(value)}; expected all, android, or ios`, + ); + } + return { + android: value === "all" || value === "android", + ios: value === "all" || value === "ios", + }; +} + +export function githubResolverDependencies( + repo, + { execute = undefined, retryOptions = {} } = {}, +) { + const executeLocal = execute ?? ((command, args) => run(command, args)); + const ghRead = (args, label) => { + if (execute === undefined) { + return runGitHubReadSync(args, { label, ...retryOptions }); + } + return retryReadOperationSync( + label, + () => execute("gh", args), + retryOptions, + ); + }; + const paginationSpawn = execute === undefined + ? undefined + : (command, args) => { + try { + return { + status: 0, + stderr: "", + stdout: execute(command, args), + }; + } catch (error) { + return { + status: 1, + stderr: error instanceof Error ? error.message : String(error), + stdout: "", + }; + } + }; + const ghJson = (args, label) => parsedJson(ghRead(args, label), label); + return { + checkoutSha() { + return executeLocal("git", ["rev-parse", "HEAD^{commit}"]).trim(); + }, + listRuns(sha) { + return ghJson( + [ + "run", + "list", + "--repo", + repo, + "--workflow", + "ci.yml", + "--commit", + sha, + "--limit", + "100", + "--json", + "databaseId,status,conclusion,headSha", + ], + "gh run list", + ); + }, + gateSucceeded(runId, gateJobName) { + const data = ghJson( + ["run", "view", String(runId), "--repo", repo, "--json", "jobs"], + "gh run view", + ); + if (!Array.isArray(data?.jobs)) { + throw new Error(`CI run ${runId} jobs must be a list`); + } + const matches = data.jobs.filter((job) => job?.name === gateJobName); + return matches.length === 1 && matches[0]?.conclusion === "success"; + }, + artifactNames(runId) { + const label = `CI run ${runId} mobile artifact inventory`; + const artifacts = runGitHubPaginatedJsonSync( + `repos/${repo}/actions/runs/${runId}/artifacts`, + { + ...retryOptions, + itemsField: "artifacts", + label, + ...(paginationSpawn === undefined ? {} : { spawn: paginationSpawn }), + }, + ); + return artifacts + .map((artifact) => { + if ( + artifact === null + || Array.isArray(artifact) + || typeof artifact !== "object" + || typeof artifact.name !== "string" + || artifact.name.length === 0 + || typeof artifact.expired !== "boolean" + ) { + throw new Error(`${label} contains malformed artifact metadata`); + } + return artifact; + }) + .filter((artifact) => artifact.expired === false) + .map((artifact) => artifact.name); + }, + }; +} + +function stringCounts(value, label) { + if (value === null || value === undefined || typeof value[Symbol.iterator] !== "function") { + throw new Error(`${label} must be an iterable of artifact names`); + } + const result = new Map(); + for (const item of value) { + if (typeof item !== "string" || item.length === 0) { + throw new Error(`${label} contains an invalid artifact name`); + } + result.set(item, (result.get(item) ?? 0) + 1); + } + return result; +} + +export function resolveMobileE2e( + { + repo, + requestedPlatform = "all", + requestedSha, + defaultSha, + gateJobName = "Builds", + }, + dependencies, +) { + if (typeof repo !== "string" || repo.length === 0) { + throw new Error("GH_REPO is required"); + } + if (!dependencies || typeof dependencies !== "object") { + throw new Error("mobile E2E resolver dependencies are required"); + } + const requested = mobilePlatformSelection(requestedPlatform); + const inputSha = requestedSha || defaultSha; + if (!inputSha) { + throw new Error("an input SHA or default SHA is required"); + } + if (!FULL_SHA_INPUT.test(inputSha)) { + throw new Error(`mobile E2E input must be a full commit SHA: ${inputSha}`); + } + + const sha = String(dependencies.checkoutSha()).trim(); + if (!FULL_SHA.test(sha)) { + throw new Error(`checked-out mobile E2E commit is not a full SHA: ${sha}`); + } + if (sha !== inputSha.toLowerCase()) { + throw new Error(`checked-out mobile E2E commit ${sha} does not match requested SHA ${inputSha}`); + } + + const runs = dependencies.listRuns(sha); + if (runs !== null && !Array.isArray(runs)) { + throw new Error("gh run list must return a JSON array"); + } + const candidateIds = []; + const seenRunIds = new Set(); + for (const candidate of runs ?? []) { + if (candidate?.status !== "completed" || candidate?.conclusion !== "success") continue; + if (!FULL_SHA.test(candidate.headSha ?? "")) { + throw new Error("successful CI run metadata is missing a full lowercase headSha"); + } + if (candidate.headSha !== sha) continue; + const runId = String(candidate.databaseId ?? ""); + if (!RUN_ID.test(runId)) { + throw new Error(`successful CI run for ${sha} has invalid databaseId ${JSON.stringify(candidate.databaseId)}`); + } + if (!seenRunIds.has(runId)) { + seenRunIds.add(runId); + candidateIds.push(runId); + } + } + + for (const runId of candidateIds) { + if (dependencies.gateSucceeded(runId, gateJobName) !== true) continue; + const names = stringCounts(dependencies.artifactNames(runId), `CI run ${runId} artifacts`); + const selected = { + android: requested.android && names.get(MOBILE_ARTIFACTS.android) === 1, + ios: requested.ios && names.get(MOBILE_ARTIFACTS.ios) === 1, + }; + if (Object.entries(requested).every(([platform, wanted]) => !wanted || selected[platform])) { + return { + android: selected.android, + ios: selected.ios, + platformJobs: [ + ...(selected.android ? ["android"] : []), + ...(selected.ios ? ["ios"] : []), + ], + runId, + sha, + }; + } + } + + throw new Error(`No successful CI run for ${sha} contains requested mobile app artifacts.`); +} + +export function runMobileE2eResolver({ + environment = env, + dependencies = undefined, + writeOutput = undefined, +} = {}) { + const repo = environment.GH_REPO; + const resolved = resolveMobileE2e( + { + defaultSha: environment.DEFAULT_SHA, + gateJobName: environment.BUILD_GATE_JOB || "Builds", + repo, + requestedPlatform: environment.INPUT_PLATFORM || "all", + requestedSha: environment.INPUT_SHA, + }, + dependencies ?? + githubResolverDependencies(repo, { + execute: (command, args) => run(command, args, { environment }), + }), + ); + const emit = writeOutput ?? outputWriter(environment); + emit("sha", resolved.sha); + emit("run_id", resolved.runId); + emit("android", String(resolved.android)); + emit("ios", String(resolved.ios)); + emit("platform_jobs", JSON.stringify(resolved.platformJobs)); + return resolved; +} + +if (import.meta.main) { + try { + runMobileE2eResolver(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + exit(1); + } +} diff --git a/.github/scripts/resolve-mobile-e2e.test.mjs b/.github/scripts/resolve-mobile-e2e.test.mjs new file mode 100644 index 00000000..220a14af --- /dev/null +++ b/.github/scripts/resolve-mobile-e2e.test.mjs @@ -0,0 +1,295 @@ +#!/usr/bin/env node + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + githubResolverDependencies, + resolveMobileE2e, + runMobileE2eResolver, +} from "./resolve-mobile-e2e.mjs"; + +const SHA = "a".repeat(40); +const OTHER_SHA = "b".repeat(40); +const ANDROID_ARTIFACT = "react-native-mobile-android-app-android-x86_64"; +const IOS_ARTIFACT = "react-native-mobile-ios-app"; + +function successfulRun(databaseId, headSha = SHA) { + return { conclusion: "success", databaseId, headSha, status: "completed" }; +} + +function includedArtifactInventory(...names) { + return `HTTP/2.0 200 OK\n\n${JSON.stringify({ + artifacts: names.map((name) => ({ expired: false, name })), + })}`; +} + +function dependencies({ + sha = SHA, + runs = [successfulRun(101)], + gates = new Map([["101", true]]), + artifacts = new Map([["101", new Set([ANDROID_ARTIFACT, IOS_ARTIFACT])]]), +} = {}) { + const calls = { artifacts: [], gates: [], listRuns: [] }; + return { + calls, + checkoutSha: () => sha, + listRuns(value) { + calls.listRuns.push(value); + return runs; + }, + gateSucceeded(runId, gateJobName) { + calls.gates.push([runId, gateJobName]); + return gates.get(runId) ?? false; + }, + artifactNames(runId) { + calls.artifacts.push(runId); + return artifacts.get(runId) ?? new Set(); + }, + }; +} + +function resolve(options = {}, injected = dependencies()) { + return resolveMobileE2e( + { + repo: "f0rr0/oliphaunt", + requestedPlatform: "all", + requestedSha: SHA, + ...options, + }, + injected, + ); +} + +test("resolves only an exact-SHA successful run with the successful aggregate build gate", () => { + const injected = dependencies({ + runs: [ + successfulRun(1, OTHER_SHA), + { ...successfulRun(2), status: "in_progress", conclusion: null }, + { ...successfulRun(3), conclusion: "failure" }, + successfulRun(4), + successfulRun(5), + successfulRun(6), + ], + gates: new Map([ + ["4", false], + ["5", true], + ["6", true], + ]), + artifacts: new Map([ + ["5", new Set([ANDROID_ARTIFACT])], + ["6", new Set([ANDROID_ARTIFACT, IOS_ARTIFACT])], + ]), + }); + assert.deepEqual(resolve({}, injected), { + android: true, + ios: true, + platformJobs: ["android", "ios"], + runId: "6", + sha: SHA, + }); + assert.deepEqual(injected.calls.listRuns, [SHA]); + assert.deepEqual(injected.calls.gates, [ + ["4", "Builds"], + ["5", "Builds"], + ["6", "Builds"], + ]); + assert.deepEqual(injected.calls.artifacts, ["5", "6"]); +}); + +test("platform selection requires only the exact requested artifact identity", () => { + const androidOnly = dependencies({ + artifacts: new Map([["101", new Set([ANDROID_ARTIFACT, `${IOS_ARTIFACT}-near-match`])]]), + }); + assert.deepEqual(resolve({ requestedPlatform: "android" }, androidOnly).platformJobs, ["android"]); + + const iosOnly = dependencies({ + artifacts: new Map([["101", new Set([IOS_ARTIFACT, `${ANDROID_ARTIFACT}-near-match`])]]), + }); + assert.deepEqual(resolve({ requestedPlatform: "ios" }, iosOnly).platformJobs, ["ios"]); + + assert.throws( + () => resolve({}, androidOnly), + /contains requested mobile app artifacts/u, + ); + assert.throws( + () => resolve({}, iosOnly), + /contains requested mobile app artifacts/u, + ); +}); + +test("rejects abbreviated, malformed, mismatched, or non-canonical checkout SHAs", () => { + for (const requestedSha of ["a".repeat(39), `${"a".repeat(40)}0`, "not-a-sha"]) { + assert.throws( + () => resolve({ requestedSha }, dependencies()), + /input must be a full commit SHA/u, + ); + } + assert.throws( + () => resolve({ requestedSha: OTHER_SHA }, dependencies()), + /does not match requested SHA/u, + ); + assert.throws( + () => resolve({}, dependencies({ sha: SHA.toUpperCase() })), + /checked-out mobile E2E commit is not a full SHA/u, + ); + assert.equal(resolve({ requestedSha: SHA.toUpperCase() }).sha, SHA); +}); + +test("fails closed on malformed successful-run metadata and artifact responses", () => { + assert.throws( + () => resolve({}, dependencies({ runs: [successfulRun(undefined)] })), + /invalid databaseId/u, + ); + assert.throws( + () => resolve({}, dependencies({ runs: [{ ...successfulRun(1), headSha: "abc" }] })), + /missing a full lowercase headSha/u, + ); + assert.throws( + () => resolve({}, dependencies({ runs: {} })), + /gh run list must return a JSON array/u, + ); + assert.throws( + () => resolve({}, dependencies({ artifacts: new Map([["101", [ANDROID_ARTIFACT, null]]]) })), + /invalid artifact name/u, + ); +}); + +test("deduplicates repeated attempts and preserves GitHub run ordering", () => { + const injected = dependencies({ + runs: [successfulRun(201), successfulRun(201), successfulRun(202)], + gates: new Map([ + ["201", true], + ["202", true], + ]), + artifacts: new Map([ + ["201", new Set([ANDROID_ARTIFACT])], + ["202", new Set([ANDROID_ARTIFACT, IOS_ARTIFACT])], + ]), + }); + assert.equal(resolve({}, injected).runId, "202"); + assert.deepEqual(injected.calls.gates, [ + ["201", "Builds"], + ["202", "Builds"], + ]); +}); + +test("skips ambiguous duplicate gate and artifact identities", () => { + const duplicateArtifacts = dependencies({ + runs: [successfulRun(301), successfulRun(302)], + gates: new Map([ + ["301", true], + ["302", true], + ]), + artifacts: new Map([ + ["301", [ANDROID_ARTIFACT, ANDROID_ARTIFACT, IOS_ARTIFACT]], + ["302", [ANDROID_ARTIFACT, IOS_ARTIFACT]], + ]), + }); + assert.equal(resolve({}, duplicateArtifacts).runId, "302"); + + const commands = []; + const adapter = githubResolverDependencies("f0rr0/oliphaunt", { + execute(command, args) { + commands.push([command, args]); + if (command === "git") return `${SHA}\n`; + if (args[0] === "run" && args[1] === "list") return JSON.stringify([successfulRun(303)]); + if (args[0] === "run" && args[1] === "view") { + return JSON.stringify({ jobs: [ + { conclusion: "success", name: "Builds" }, + { conclusion: "success", name: "Builds" }, + ] }); + } + return `${ANDROID_ARTIFACT}\n${IOS_ARTIFACT}\n`; + }, + }); + assert.throws(() => resolve({}, adapter), /contains requested mobile app artifacts/u); + assert.equal(commands.some(([, args]) => args[0] === "api"), false); +}); + +test("environment entry point emits the complete exact resolver contract", () => { + const emitted = []; + const result = runMobileE2eResolver({ + environment: { + BUILD_GATE_JOB: "Builds", + GH_REPO: "f0rr0/oliphaunt", + INPUT_PLATFORM: "ios", + INPUT_SHA: SHA, + }, + dependencies: dependencies({ artifacts: new Map([["101", new Set([IOS_ARTIFACT])]]) }), + writeOutput: (name, value) => emitted.push([name, value]), + }); + assert.deepEqual(result.platformJobs, ["ios"]); + assert.deepEqual(emitted, [ + ["sha", SHA], + ["run_id", "101"], + ["android", "false"], + ["ios", "true"], + ["platform_jobs", '["ios"]'], + ]); +}); + +test("production adapter pins repository, exact commit, gate run, and non-expired artifacts", () => { + const commands = []; + const execute = (command, args) => { + commands.push([command, args]); + if (command === "git") return `${SHA}\n`; + if (args[0] === "run" && args[1] === "list") { + return JSON.stringify([successfulRun(901)]); + } + if (args[0] === "run" && args[1] === "view") { + return JSON.stringify({ jobs: [{ conclusion: "success", name: "Builds" }] }); + } + return includedArtifactInventory(ANDROID_ARTIFACT, IOS_ARTIFACT); + }; + const result = resolve({}, githubResolverDependencies("f0rr0/oliphaunt", { execute })); + assert.equal(result.runId, "901"); + + const list = commands.find(([command, args]) => command === "gh" && args[1] === "list")[1]; + assert.deepEqual(list.slice(0, 6), ["run", "list", "--repo", "f0rr0/oliphaunt", "--workflow", "ci.yml"]); + assert.equal(list[list.indexOf("--commit") + 1], SHA); + assert.equal(list[list.indexOf("--limit") + 1], "100"); + assert.equal(list[list.indexOf("--json") + 1], "databaseId,status,conclusion,headSha"); + + const view = commands.find(([command, args]) => command === "gh" && args[1] === "view")[1]; + assert.deepEqual(view.slice(0, 5), ["run", "view", "901", "--repo", "f0rr0/oliphaunt"]); + assert.equal(view[view.indexOf("--json") + 1], "jobs"); + + const artifacts = commands.find(([command, args]) => command === "gh" && args[0] === "api")[1]; + assert.equal(artifacts[1], "--include"); + assert.equal( + artifacts[2], + "repos/f0rr0/oliphaunt/actions/runs/901/artifacts?per_page=100&page=1", + ); +}); + +test("production adapter survives a transient GitHub read without weakening exact identity", () => { + let listAttempts = 0; + const execute = (command, args) => { + if (command === "git") return `${SHA}\n`; + if (args[0] === "run" && args[1] === "list") { + listAttempts += 1; + if (listAttempts === 1) throw new Error("HTTP 503 temporary failure"); + return JSON.stringify([successfulRun(902)]); + } + if (args[0] === "run" && args[1] === "view") { + return JSON.stringify({ jobs: [{ conclusion: "success", name: "Builds" }] }); + } + return includedArtifactInventory(ANDROID_ARTIFACT, IOS_ARTIFACT); + }; + const resolved = resolve( + {}, + githubResolverDependencies("f0rr0/oliphaunt", { + execute, + retryOptions: { + baseDelayMs: 0, + deadlineMs: 100, + maxAttempts: 2, + maxDelayMs: 0, + }, + }), + ); + assert.equal(resolved.runId, "902"); + assert.equal(resolved.sha, SHA); + assert.equal(listAttempts, 2); +}); diff --git a/.github/scripts/resolve-release-head.sh b/.github/scripts/resolve-release-head.sh new file mode 100755 index 00000000..916b15e7 --- /dev/null +++ b/.github/scripts/resolve-release-head.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_SHA:?GITHUB_SHA is required}" +: "${GITHUB_REF:?GITHUB_REF is required}" +: "${GITHUB_OUTPUT:?GITHUB_OUTPUT is required}" +: "${GITHUB_ENV:?GITHUB_ENV is required}" + +input="${INPUT_RELEASE_COMMIT:-}" +workflow_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" + +if [[ -z "$input" ]]; then + release_sha="$workflow_sha" +else + if [[ ! "$input" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "release_commit must be a full 40-character commit SHA, got: $input" >&2 + exit 2 + fi + release_sha="$(git rev-parse "${input}^{commit}")" + release_sha_lower="$(printf '%s' "$release_sha" | tr '[:upper:]' '[:lower:]')" + input_lower="$(printf '%s' "$input" | tr '[:upper:]' '[:lower:]')" + if [[ "$release_sha_lower" != "$input_lower" ]]; then + echo "release_commit resolved to $release_sha, not $input" >&2 + exit 2 + fi +fi + +if [[ "$release_sha" != "$workflow_sha" ]]; then + echo "release_commit must equal the workflow commit exactly." >&2 + echo "workflow commit: $workflow_sha" >&2 + echo "release commit: $release_sha" >&2 + echo "Tooling-lag publication is intentionally unsupported; dispatch from the release commit on main." >&2 + exit 2 +fi + +bash .github/scripts/require-current-main.sh "$workflow_sha" + +{ + echo "sha=$release_sha" + echo "workflow_sha=$workflow_sha" +} >> "$GITHUB_OUTPUT" +echo "RELEASE_HEAD_SHA=$release_sha" >> "$GITHUB_ENV" + +echo "workflow commit: $workflow_sha" +echo "release commit: $release_sha" 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/scripts/run-moon-target-matrix.mjs b/.github/scripts/run-moon-target-matrix.mjs new file mode 100644 index 00000000..361bc922 --- /dev/null +++ b/.github/scripts/run-moon-target-matrix.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +function fail(message) { + console.error(message); + process.exit(1); +} + +let matrix; +try { + matrix = JSON.parse(process.env.MOON_TARGET_MATRIX_JSON ?? ""); +} catch (error) { + fail(`MOON_TARGET_MATRIX_JSON is invalid JSON: ${error.message}`); +} + +if (!matrix || !Array.isArray(matrix.include)) { + fail("MOON_TARGET_MATRIX_JSON must contain an include array"); +} + +const groups = new Map(); +for (const row of matrix.include) { + if (!row || typeof row.target !== "string" || !/^[A-Za-z0-9_:@./-]+$/u.test(row.target)) { + fail(`invalid Moon target matrix row: ${JSON.stringify(row)}`); + } + const upstream = row.upstream ?? "deep"; + if (!["none", "deep"].includes(upstream)) { + fail(`unsupported Moon upstream mode ${upstream} for ${row.target}`); + } + const targets = groups.get(upstream) ?? new Set(); + targets.add(row.target); + groups.set(upstream, targets); +} + +if (groups.size === 0) { + fail("Moon target matrix must not be empty"); +} + +for (const [upstream, targets] of groups) { + const ordered = [...targets].sort(); + console.log(`running ${ordered.length} Moon targets with --upstream ${upstream}`); + const result = spawnSync( + ".github/scripts/run-moon-targets.sh", + ["--upstream", upstream, ...ordered], + { stdio: "inherit", env: process.env }, + ); + if (result.error || result.status !== 0) { + fail(result.error?.message || `Moon target group failed with status ${result.status}`); + } +} diff --git a/.github/scripts/run-moon-targets.sh b/.github/scripts/run-moon-targets.sh new file mode 100755 index 00000000..8e71645d --- /dev/null +++ b/.github/scripts/run-moon-targets.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +unset MOON_BASE +unset MOON_HEAD + +moon_bin="${MOON_BIN:-moon}" + +exec "$moon_bin" run "$@" diff --git a/.github/scripts/run-planned-moon-job.sh b/.github/scripts/run-planned-moon-job.sh new file mode 100755 index 00000000..3bd4d543 --- /dev/null +++ b/.github/scripts/run-planned-moon-job.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +job="${1:-}" +if [[ -z "$job" ]]; then + echo "usage: .github/scripts/run-planned-moon-job.sh " >&2 + exit 2 +fi + +targets_file="$(mktemp)" +trap 'rm -f "$targets_file"' EXIT + +bun .github/scripts/select-planned-moon-targets.mjs "$job" >"$targets_file" + +targets=() +while IFS= read -r target; do + target="${target%$'\r'}" + targets+=("$target") +done <"$targets_file" + +if [[ "${#targets[@]}" -eq 0 ]]; then + echo "CI job '$job' has no planned Moon targets" >&2 + exit 2 +fi + +moon_args=() +if [[ -n "${OLIPHAUNT_MOON_UPSTREAM:-}" ]]; then + moon_args+=(--upstream "$OLIPHAUNT_MOON_UPSTREAM") +fi + +if [[ "${#moon_args[@]}" -gt 0 ]]; then + exec .github/scripts/run-moon-targets.sh "${moon_args[@]}" "${targets[@]}" +fi + +exec .github/scripts/run-moon-targets.sh "${targets[@]}" diff --git a/.github/scripts/select-affected-moon-targets.mjs b/.github/scripts/select-affected-moon-targets.mjs new file mode 100644 index 00000000..393bd3fa --- /dev/null +++ b/.github/scripts/select-affected-moon-targets.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +import {spawnSync} from 'node:child_process'; +import process from 'node:process'; + +import {moonCommand} from '../../tools/dev/moon-command.mjs'; + +function fail(message) { + console.error(message); + process.exit(1); +} + +const taskId = process.argv[2] ?? ''; +if (!/^[A-Za-z0-9_-]+$/.test(taskId)) { + fail('usage: select-affected-moon-targets.mjs '); +} + +function useAffectedQuery() { + return Boolean(process.env.MOON_BASE?.trim() && process.env.MOON_HEAD?.trim()); +} + +function moonQueryTaskArgs() { + const args = ['query', 'tasks']; + if (useAffectedQuery()) { + args.push('--affected'); + } + args.push('--id', taskId); + if (useAffectedQuery()) { + args.push('--upstream', 'none', '--downstream', 'deep'); + } + return args; +} + +function moonQueryTasks() { + const result = spawnSync( + moonCommand(), + moonQueryTaskArgs(), + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }, + ); + if (result.status !== 0) { + fail(`moon query tasks failed for task id ${taskId}`); + } + try { + return JSON.parse(result.stdout); + } catch (error) { + fail(`moon query tasks returned invalid JSON: ${error.message}`); + } +} + +function runsInCI(task) { + const value = task?.options?.runInCI; + return value !== false && value !== 'skip'; +} + +const query = moonQueryTasks(); +const tasksByProject = query.tasks; +if (!tasksByProject || typeof tasksByProject !== 'object' || Array.isArray(tasksByProject)) { + fail('moon query tasks did not return a tasks object'); +} + +const targets = []; +for (const projectTasks of Object.values(tasksByProject)) { + if (!projectTasks || typeof projectTasks !== 'object' || Array.isArray(projectTasks)) { + continue; + } + for (const task of Object.values(projectTasks)) { + if (!task || typeof task !== 'object') { + continue; + } + if (task.id === taskId && typeof task.target === 'string' && runsInCI(task)) { + targets.push(task.target); + } + } +} + +for (const target of [...new Set(targets)].sort()) { + console.log(target); +} diff --git a/.github/scripts/select-planned-moon-targets.mjs b/.github/scripts/select-planned-moon-targets.mjs new file mode 100644 index 00000000..3091c1ed --- /dev/null +++ b/.github/scripts/select-planned-moon-targets.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env bun +import {existsSync, readFileSync} from 'node:fs'; +import process from 'node:process'; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function parseJson(raw, source) { + try { + return JSON.parse(raw); + } catch (error) { + fail(`invalid ${source} JSON: ${error.message}`); + } +} + +function plannedTargetsJson() { + const envJson = process.env.OLIPHAUNT_CI_JOB_TARGETS_JSON; + if (envJson) { + return parseJson(envJson, 'OLIPHAUNT_CI_JOB_TARGETS_JSON'); + } + + const planPath = 'target/graph/ci-plan.json'; + if (!existsSync(planPath)) { + fail('missing OLIPHAUNT_CI_JOB_TARGETS_JSON or target/graph/ci-plan.json'); + } + + const plan = parseJson(readFileSync(planPath, 'utf8'), planPath); + return plan.job_targets ?? {}; +} + +const job = process.argv[2] ?? ''; +if (!job) { + fail('usage: select-planned-moon-targets.mjs '); +} + +const mapping = plannedTargetsJson(); +const targets = mapping?.[job] ?? []; +if (!Array.isArray(targets) || targets.some((target) => typeof target !== 'string')) { + fail(`CI job ${JSON.stringify(job)} has invalid target list`); +} +for (const target of targets) { + console.log(target); +} diff --git a/.github/scripts/selected-registry-needs.mjs b/.github/scripts/selected-registry-needs.mjs new file mode 100644 index 00000000..d8c4fe27 --- /dev/null +++ b/.github/scripts/selected-registry-needs.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env bun +import { appendFileSync } from "node:fs"; +import process from "node:process"; + +import { loadPublicationCatalog } from "../../tools/release/publication-catalog.mjs"; + +function fail(message) { + console.error(`selected-registry-needs: ${message}`); + process.exit(1); +} + +let products; +try { + products = JSON.parse(process.env.PRODUCTS_JSON ?? ""); +} catch (error) { + fail(`invalid PRODUCTS_JSON: ${error.message}`); +} +if (!Array.isArray(products) || products.length === 0 || products.some((product) => typeof product !== "string")) { + fail("PRODUCTS_JSON must be a non-empty product string list"); +} + +const catalog = loadPublicationCatalog("selected-registry-needs", { products }); +const ecosystems = new Set(catalog.carriers.map((carrier) => carrier.ecosystem)); +for (const ecosystem of ["cargo", "npm", "maven", "jsr"]) { + const line = `needs_${ecosystem}=${String(ecosystems.has(ecosystem))}`; + console.log(line); + if (process.env.GITHUB_OUTPUT) { + appendFileSync(process.env.GITHUB_OUTPUT, `${line}\n`, "utf8"); + } +} diff --git a/.github/scripts/setup-apple.test.mjs b/.github/scripts/setup-apple.test.mjs new file mode 100644 index 00000000..3eda12bb --- /dev/null +++ b/.github/scripts/setup-apple.test.mjs @@ -0,0 +1,50 @@ +import { readFileSync } from "node:fs"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +const action = readFileSync(".github/actions/setup-apple/action.yml", "utf8"); + +const requiredTokens = [ + 'default: "26.5"', + 'SUPPORTED_XCODE_MINOR: "26.5"', + '[[ "$REQUESTED_XCODE_MINOR" != "$SUPPORTED_XCODE_MINOR" ]]', + '/Applications/Xcode_26.5.app/Contents/Developer', + 'export DEVELOPER_DIR="$developer_dir"', + '[[ "$observed_xcode_version" != "$SUPPORTED_XCODE_MINOR" ]]', + "Build version", + "xcrun --sdk macosx --show-sdk-version", + "xcrun --sdk iphoneos --show-sdk-version", + "xcrun --sdk iphonesimulator --show-sdk-version", + "ImageOS", + "ImageVersion", + "GITHUB_ENV", + "GITHUB_STEP_SUMMARY", +]; + +function assertAppleSetupContract(source) { + for (const token of requiredTokens) { + assert.ok(source.includes(token), `Apple setup must preserve ${token}`); + } + assert.doesNotMatch(source, /\bxcode-select\s+(?:--switch|-s)\b/u); + assert.doesNotMatch(source, /\/Applications\/Xcode\.app\/Contents\/Developer/u); +} + +test("Apple setup selects and verifies exact Xcode 26.5 with observed provenance", () => { + assertAppleSetupContract(action); +}); + +for (const token of requiredTokens) { + test(`Apple contract rejects mutation of ${token}`, () => { + const mutated = action.replaceAll(token, ""); + assert.notEqual(mutated, action, `test mutation must alter ${token}`); + assert.throws(() => assertAppleSetupContract(mutated)); + }); +} + +test("Apple contract rejects mutable default Xcode selection", () => { + const mutated = action.replace( + "/Applications/Xcode_26.5.app/Contents/Developer", + "/Applications/Xcode.app/Contents/Developer", + ); + assert.throws(() => assertAppleSetupContract(mutated)); +}); diff --git a/.github/scripts/setup-msvc.ps1 b/.github/scripts/setup-msvc.ps1 new file mode 100644 index 00000000..6de664eb --- /dev/null +++ b/.github/scripts/setup-msvc.ps1 @@ -0,0 +1,225 @@ +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +function Fail([string] $Message) { + throw "setup-msvc.ps1: $Message" +} + +function Write-GitHubEnvironment([string] $Name, [string] $Value) { + if (-not $env:GITHUB_ENV) { + Fail "GITHUB_ENV is not set" + } + if ($Name -notmatch '^[A-Za-z_][A-Za-z0-9_()]*$') { + Fail "refusing to export invalid environment name: $Name" + } + if ($Name -match '^(?:GITHUB|RUNNER|ACTIONS)_') { + Fail "refusing to overwrite GitHub-owned environment variable: $Name" + } + + $delimiter = "OLIPHAUNT_MSVC_$([Guid]::NewGuid().ToString('N'))" + @( + "$Name<<$delimiter" + $Value + $delimiter + ) | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 +} + +function Require-MajorVersion([string] $Name, [string] $Value, [int] $ExpectedMajor) { + if ([string]::IsNullOrWhiteSpace($Value)) { + Fail "$Name is empty" + } + try { + $version = [Version]::Parse($Value.Trim()) + } + catch { + Fail "$Name '$Value' is not a numeric dotted version" + } + if ($version.Major -ne $ExpectedMajor) { + Fail "$Name '$Value' has major $($version.Major); expected $ExpectedMajor" + } + return $version +} + +function Require-RunnerProvenance([string] $Name, [string] $Value) { + if ([string]::IsNullOrWhiteSpace($Value)) { + Fail "hosted runner provenance $Name is empty" + } + if ($Value -match '[\r\n|]') { + Fail "hosted runner provenance $Name must be a single safe summary value" + } + return $Value +} + +function Resolve-CommandPath([string] $Name, [string] $ExpectedPath) { + $command = Get-Command $Name -CommandType Application -ErrorAction Stop | Select-Object -First 1 + $observedPath = [IO.Path]::GetFullPath($command.Source) + $normalizedExpectedPath = [IO.Path]::GetFullPath($ExpectedPath) + if (-not [StringComparer]::OrdinalIgnoreCase.Equals($observedPath, $normalizedExpectedPath)) { + Fail "$Name resolved to $observedPath; expected $normalizedExpectedPath" + } + return $observedPath +} + +function Require-FileVersion([string] $Name, [string] $Path) { + $versionInfo = (Get-Item -LiteralPath $Path).VersionInfo + $version = $versionInfo.FileVersion + if ([string]::IsNullOrWhiteSpace($version)) { + $version = $versionInfo.ProductVersion + } + if ([string]::IsNullOrWhiteSpace($version) -or $version -match '[\r\n|]') { + Fail "$Name at $Path did not expose a safe file version" + } + return $version.Trim() +} + +$programFilesX86 = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFilesX86) +$vswhere = Join-Path $programFilesX86 "Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path -LiteralPath $vswhere -PathType Leaf)) { + Fail "vswhere.exe was not found at $vswhere" +} + +$vswhereOutput = & $vswhere ` + -latest ` + -products '*' ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -version '[18.0,19.0)' ` + -format json ` + -utf8 +if ($LASTEXITCODE -ne 0) { + Fail "vswhere failed with exit code $LASTEXITCODE" +} +$vswhereJson = $vswhereOutput -join [Environment]::NewLine +if ([string]::IsNullOrWhiteSpace($vswhereJson)) { + Fail "Visual Studio 18 with the x64 C++ toolchain was not found" +} +try { + $installations = @($vswhereJson | ConvertFrom-Json) +} +catch { + Fail "vswhere returned invalid JSON: $($_.Exception.Message)" +} +if ($installations.Count -ne 1) { + Fail "vswhere returned $($installations.Count) installations; expected exactly one latest Visual Studio 18 installation" +} +$installation = $installations[0] +$installPathProperty = $installation.PSObject.Properties['installationPath'] +$installationVersionProperty = $installation.PSObject.Properties['installationVersion'] +if ($null -eq $installPathProperty -or $null -eq $installationVersionProperty) { + Fail "vswhere JSON omitted installationPath or installationVersion" +} +$installPath = ([string] $installPathProperty.Value).Trim() +$installationVersion = ([string] $installationVersionProperty.Value).Trim() +if (-not $installPath -or -not (Test-Path -LiteralPath $installPath -PathType Container)) { + Fail "Visual Studio 18 installation path was not found at $installPath" +} +$null = Require-MajorVersion "installationVersion" $installationVersion 18 + +$devShellLauncher = Join-Path $installPath "Common7\Tools\Launch-VsDevShell.ps1" +if (-not (Test-Path -LiteralPath $devShellLauncher -PathType Leaf)) { + Fail "Visual Studio developer shell launcher was not found at $devShellLauncher" +} + +$before = @{} +Get-ChildItem Env: | ForEach-Object { $before[$_.Name] = $_.Value } + +& $devShellLauncher ` + -SkipAutomaticLocation ` + -Arch x64 ` + -HostArch x64 +if (-not $?) { + Fail "Visual Studio developer shell initialization failed" +} + +if (-not $env:VCToolsInstallDir) { + Fail "VCToolsInstallDir was not set by the Visual Studio developer shell" +} +if (-not $env:VCToolsRedistDir) { + Fail "VCToolsRedistDir was not set by the Visual Studio developer shell" +} +if (-not $env:VSCMD_VER) { + Fail "VSCMD_VER was not set by the Visual Studio developer shell" +} +$null = Require-MajorVersion "VSCMD_VER" $env:VSCMD_VER 18 +if ($env:VSCMD_ARG_HOST_ARCH -ne "x64" -or $env:VSCMD_ARG_TGT_ARCH -ne "x64") { + Fail "developer shell architecture must be HostX64/x64; observed host '$($env:VSCMD_ARG_HOST_ARCH)' target '$($env:VSCMD_ARG_TGT_ARCH)'" +} +if (-not $env:VCToolsVersion) { + Fail "VCToolsVersion was not set by the Visual Studio developer shell" +} +$vcToolsVersion = $env:VCToolsVersion.Trim().TrimEnd('\') +$vcToolsVersionInfo = Require-MajorVersion "VCToolsVersion" $vcToolsVersion 14 +if ($vcToolsVersionInfo.Minor -lt 50 -or $vcToolsVersionInfo.Minor -ge 60) { + Fail "VCToolsVersion '$vcToolsVersion' is not the Visual Studio 2026 VC145 toolset family" +} +if (-not $env:WindowsSDKVersion) { + Fail "WindowsSDKVersion was not set by the Visual Studio developer shell" +} +$windowsSdkVersion = $env:WindowsSDKVersion.Trim().TrimEnd('\') +$null = Require-MajorVersion "WindowsSDKVersion" $windowsSdkVersion 10 + +$vcRuntimeDir = Join-Path $env:VCToolsRedistDir "x64\Microsoft.VC145.CRT" +if (-not (Test-Path -LiteralPath $vcRuntimeDir -PathType Container)) { + Fail "the exact x64 Microsoft.VC145.CRT redistributable directory was not found at $vcRuntimeDir" +} +foreach ($runtime in @("msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll")) { + $runtimePath = Join-Path $vcRuntimeDir $runtime + if (-not (Test-Path -LiteralPath $runtimePath -PathType Leaf)) { + Fail "required x64 VC redistributable was not found at $runtimePath" + } +} + +$msvcBin = Join-Path $env:VCToolsInstallDir "bin\HostX64\x64" +$requiredTools = @("cl.exe", "link.exe", "lib.exe", "dumpbin.exe") +foreach ($tool in $requiredTools) { + $toolPath = Join-Path $msvcBin $tool + if (-not (Test-Path -LiteralPath $toolPath -PathType Leaf)) { + Fail "required MSVC tool was not found at $toolPath" + } +} +$clPath = Resolve-CommandPath "cl.exe" (Join-Path $msvcBin "cl.exe") +$linkPath = Resolve-CommandPath "link.exe" (Join-Path $msvcBin "link.exe") +$clVersion = Require-FileVersion "cl.exe" $clPath +$linkVersion = Require-FileVersion "link.exe" $linkPath +$imageOS = Require-RunnerProvenance "ImageOS" $env:ImageOS +$imageVersion = Require-RunnerProvenance "ImageVersion" $env:ImageVersion + +$changed = 0 +Get-ChildItem Env: | Sort-Object Name | ForEach-Object { + $name = $_.Name + $value = $_.Value + $previous = $before[$name] + if ($previous -ne $value -and $name -notmatch '^(?:GITHUB|RUNNER|ACTIONS)_') { + Write-GitHubEnvironment $name $value + $changed += 1 + } +} +if ($changed -eq 0) { + Fail "Visual Studio developer shell did not change any environment variables" +} + +$linker = $linkPath +Write-GitHubEnvironment "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER" $linker +Write-Host "Configured Visual Studio at $installPath (installationVersion=$installationVersion, VSCMD_VER=$($env:VSCMD_VER))" +Write-Host "Configured MSVC tools at $msvcBin" +Write-Host "Configured x64 VC redistributables at $vcRuntimeDir" +Write-Host "Configured Rust MSVC linker at $linker" +Write-Host "Windows toolchain: VCToolsVersion=$vcToolsVersion WindowsSDKVersion=$windowsSdkVersion" +Write-Host "Windows tools: cl=$clPath fileVersion=$clVersion; link=$linkPath fileVersion=$linkVersion" +Write-Host "Windows runner image: ImageOS=$imageOS ImageVersion=$imageVersion" + +if ($env:GITHUB_STEP_SUMMARY) { + @( + "### Windows toolchain" + "" + "| Property | Observed value |" + "| --- | --- |" + "| ImageOS | ``$imageOS`` |" + "| ImageVersion | ``$imageVersion`` |" + "| Visual Studio installationVersion | ``$installationVersion`` |" + "| VSCMD_VER | ``$($env:VSCMD_VER)`` |" + "| VCToolsVersion | ``$vcToolsVersion`` |" + "| WindowsSDKVersion | ``$windowsSdkVersion`` |" + "| cl.exe | ``$clPath`` (``$clVersion``) |" + "| link.exe | ``$linkPath`` (``$linkVersion``) |" + ) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 +} diff --git a/.github/scripts/setup-msvc.test.mjs b/.github/scripts/setup-msvc.test.mjs new file mode 100644 index 00000000..1e0f953c --- /dev/null +++ b/.github/scripts/setup-msvc.test.mjs @@ -0,0 +1,73 @@ +import { readFileSync } from "node:fs"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +const action = readFileSync(".github/actions/setup-msvc/action.yml", "utf8"); +const script = readFileSync(".github/scripts/setup-msvc.ps1", "utf8"); + +const requiredActionTokens = [ + "run: .github/scripts/setup-msvc.ps1", +]; + +const requiredScriptTokens = [ + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "-version '[18.0,19.0)'", + "-format json", + "installationVersion", + 'Require-MajorVersion "installationVersion" $installationVersion 18', + "Launch-VsDevShell.ps1", + "-Arch x64", + "-HostArch x64", + 'Require-MajorVersion "VSCMD_VER" $env:VSCMD_VER 18', + "VSCMD_ARG_HOST_ARCH", + "VSCMD_ARG_TGT_ARCH", + "VCToolsInstallDir", + '$vcToolsVersionInfo = Require-MajorVersion "VCToolsVersion" $vcToolsVersion 14', + "$vcToolsVersionInfo.Minor -lt 50 -or $vcToolsVersionInfo.Minor -ge 60", + 'Require-MajorVersion "WindowsSDKVersion" $windowsSdkVersion 10', + "x64\\Microsoft.VC145.CRT", + "msvcp140.dll", + "vcruntime140.dll", + "vcruntime140_1.dll", + "bin\\HostX64\\x64", + 'Resolve-CommandPath "cl.exe"', + 'Resolve-CommandPath "link.exe"', + 'Require-FileVersion "cl.exe"', + 'Require-FileVersion "link.exe"', + 'Require-RunnerProvenance "ImageOS"', + 'Require-RunnerProvenance "ImageVersion"', + "CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER", + "GITHUB_STEP_SUMMARY", +]; + +function assertMsvcSetupContract(actionSource, scriptSource) { + for (const token of requiredActionTokens) { + assert.ok(actionSource.includes(token), `MSVC action must preserve ${token}`); + } + for (const token of requiredScriptTokens) { + assert.ok(scriptSource.includes(token), `MSVC setup must preserve ${token}`); + } + assert.doesNotMatch(actionSource, /ilammy\/msvc-dev-cmd/u); + assert.doesNotMatch(scriptSource, /Invoke-Expression|\biex\b/u); + assert.doesNotMatch(scriptSource, /installationVersion\s+-ne\s+['"]18\.[0-9.]+/u); +} + +test("MSVC setup selects and verifies the supported VS18 x64 toolchain", () => { + assertMsvcSetupContract(action, script); +}); + +for (const token of requiredScriptTokens) { + test(`MSVC contract rejects mutation of ${token}`, () => { + const mutated = script.replaceAll(token, ""); + assert.notEqual(mutated, script, `test mutation must alter ${token}`); + assert.throws(() => assertMsvcSetupContract(action, mutated)); + }); +} + +test("MSVC contract rejects replacement with the third-party setup action", () => { + const mutated = action.replace( + "run: .github/scripts/setup-msvc.ps1", + "uses: ilammy/msvc-dev-cmd@latest", + ); + assert.throws(() => assertMsvcSetupContract(mutated, script)); +}); diff --git a/.github/scripts/setup-native-build-tools.sh b/.github/scripts/setup-native-build-tools.sh new file mode 100755 index 00000000..d376c1f7 --- /dev/null +++ b/.github/scripts/setup-native-build-tools.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)" +ccache_max_size="${1:-${OLIPHAUNT_CCACHE_MAX_SIZE:-2G}}" +enable_ccache=1 + +install_macos_tools() { + local missing_packages=() + + .github/scripts/prepare-macos-homebrew.sh + + require_brew_tool() { + local command_name="$1" + local package_name="$2" + if ! command -v "$command_name" >/dev/null 2>&1; then + missing_packages+=("$package_name") + fi + } + + require_brew_tool ccache ccache + require_brew_tool autoconf autoconf + require_brew_tool aclocal automake + require_brew_tool glibtoolize libtool + + if ((${#missing_packages[@]} > 0)); then + local attempt + for attempt in 1 2 3; do + if HOMEBREW_NO_AUTO_UPDATE=1 brew install "${missing_packages[@]}"; then + return 0 + fi + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 15)) + fi + done + echo "setup-native-build-tools.sh: Homebrew failed after 3 attempts" >&2 + return 1 + fi +} + +install_linux_tools() { + .github/scripts/prepare-linux-apt.sh + sudo apt-get \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + update + sudo apt-get \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + ccache \ + cmake \ + curl \ + g++-12 \ + gcc-12 \ + git \ + make \ + perl \ + pkg-config \ + ripgrep \ + rsync \ + sqlite3 \ + xz-utils + + local linux_cc linux_cxx cc_major cxx_major + linux_cc="$(readlink -f "$(command -v gcc-12)")" + linux_cxx="$(readlink -f "$(command -v g++-12)")" + cc_major="$("$linux_cc" -dumpfullversion -dumpversion | cut -d. -f1)" + cxx_major="$("$linux_cxx" -dumpfullversion -dumpversion | cut -d. -f1)" + if [ "$cc_major" != "12" ] || [ "$cxx_major" != "12" ]; then + echo "setup-native-build-tools.sh: expected GCC/G++ major 12, got $cc_major/$cxx_major" >&2 + return 1 + fi + + export CC="$linux_cc" + export CXX="$linux_cxx" + export OLIPHAUNT_CC="$linux_cc" + export OLIPHAUNT_CXX="$linux_cxx" + if [ -n "${GITHUB_ENV:-}" ]; then + { + printf 'CC=%s\n' "$linux_cc" + printf 'CXX=%s\n' "$linux_cxx" + printf 'OLIPHAUNT_CC=%s\n' "$linux_cc" + printf 'OLIPHAUNT_CXX=%s\n' "$linux_cxx" + } >> "$GITHUB_ENV" + fi + printf 'Pinned Linux native compiler contract: %s / %s\n' \ + "$("$linux_cc" -dumpfullversion -dumpversion)" \ + "$("$linux_cxx" -dumpfullversion -dumpversion)" +} + +install_choco_package() { + local package="$1" + local expected_executable="$2" + local attempt + for attempt in 1 2 3; do + if choco install -y "$package" --no-progress --limit-output && + [ -x "$expected_executable" ]; then + return 0 + fi + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 15)) + fi + done + echo "setup-native-build-tools.sh: Chocolatey did not install $expected_executable after 3 attempts" >&2 + return 1 +} + +install_windows_tools() { + python -m pip install \ + --disable-pip-version-check \ + --retries 8 \ + --timeout 60 \ + --user \ + meson==1.10.0 \ + ninja==1.13.0 + if [ ! -x /c/Strawberry/perl/bin/perl.exe ]; then + install_choco_package strawberryperl /c/Strawberry/perl/bin/perl.exe + fi + [ -x /c/Strawberry/perl/bin/perl.exe ] || { + echo "setup-native-build-tools.sh: missing Strawberry Perl after verified installation" >&2 + return 1 + } + local winflex_dir cache_root + cache_root="${RUNNER_TEMP:-$repo_root/target}/oliphaunt-native-tools" + winflex_dir="$( + OLIPHAUNT_PINNED_NATIVE_TOOL_CACHE_ROOT="$cache_root" \ + bash "$repo_root/tools/dev/install-pinned-winflexbison.sh" + )" + [ -x "$winflex_dir/win_flex.exe" ] && [ -x "$winflex_dir/win_bison.exe" ] || { + echo "setup-native-build-tools.sh: pinned winflexbison payload is incomplete" >&2 + return 1 + } + export PATH="$winflex_dir:$PATH" + if [ -n "${GITHUB_PATH:-}" ]; then + if command -v cygpath >/dev/null 2>&1; then + cygpath -w "$winflex_dir" >>"$GITHUB_PATH" + else + printf '%s\n' "$winflex_dir" >>"$GITHUB_PATH" + fi + fi + enable_ccache=0 +} + +case "$(uname -s)" in + Darwin) + install_macos_tools + ;; + Linux) + install_linux_tools + ;; + MINGW* | MSYS* | CYGWIN*) + install_windows_tools + ;; + *) + ;; +esac + +if [ "$enable_ccache" = "1" ] && command -v ccache >/dev/null 2>&1; then + if [ -n "${CCACHE_DIR:-}" ]; then + mkdir -p "$CCACHE_DIR" + fi + ccache --max-size="$ccache_max_size" + if [ "${OLIPHAUNT_CCACHE_ZERO_STATS:-0}" = "1" ]; then + ccache --zero-stats + fi +fi diff --git a/.github/scripts/validate-release-workflow-inputs.sh b/.github/scripts/validate-release-workflow-inputs.sh new file mode 100644 index 00000000..ddde703e --- /dev/null +++ b/.github/scripts/validate-release-workflow-inputs.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${GITHUB_SHA:?GITHUB_SHA is required}" +: "${RELEASE_OPERATION:?RELEASE_OPERATION is required}" + +release_commit="${RELEASE_COMMIT:-}" +continuation_pointer="${RELEASE_CONTINUATION_POINTER:-}" + +if [[ ! "${GITHUB_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "GITHUB_SHA must be a full 40-character commit SHA, got: ${GITHUB_SHA}" >&2 + exit 2 +fi + +case "${RELEASE_OPERATION}" in + prepare-release-pr|publish-dry-run|publish-bootstrap|publish) ;; + *) + echo "Unsupported release operation: ${RELEASE_OPERATION}" >&2 + exit 2 + ;; +esac + +# release_commit is an assertion about this workflow run, never a selector for +# historical code. Validate it before operation-specific jobs are evaluated so +# every operation, including prepare-release-pr, fails closed on a stale input. +if [[ -n "${release_commit}" ]]; then + if [[ ! "${release_commit}" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "release_commit must be a full 40-character commit SHA, got: ${release_commit}" >&2 + exit 2 + fi + if [[ "${release_commit,,}" != "${GITHUB_SHA,,}" ]]; then + echo "release_commit must equal the exact workflow SHA" >&2 + echo "workflow commit: ${GITHUB_SHA}" >&2 + echo "release commit: ${release_commit}" >&2 + exit 2 + fi +fi + +if (( ${#continuation_pointer} > 32768 )); then + echo "continuation_pointer exceeds the 32 KiB transport bound" >&2 + exit 1 +fi +if [[ -n "${continuation_pointer}" ]]; then + if [[ "${RELEASE_OPERATION}" != "publish" && "${RELEASE_OPERATION}" != "publish-bootstrap" ]]; then + echo "continuation_pointer is not valid for ${RELEASE_OPERATION}" >&2 + exit 1 + fi + if [[ -z "${release_commit}" ]]; then + echo "automatic continuation requires release_commit to equal the exact workflow SHA" >&2 + exit 1 + fi +fi diff --git a/.github/scripts/verify-external-publish-readiness.mjs b/.github/scripts/verify-external-publish-readiness.mjs new file mode 100644 index 00000000..6937ce60 --- /dev/null +++ b/.github/scripts/verify-external-publish-readiness.mjs @@ -0,0 +1,215 @@ +#!/usr/bin/env bun +import process from "node:process"; + +import { loadPublicationCatalog } from "../../tools/release/publication-catalog.mjs"; +import { mavenCentralAuthorization } from "../../tools/release/maven-central-auth.mjs"; + +const JSR_API_BASE = "https://api.jsr.io"; +const MAVEN_CENTRAL_API_BASE = "https://central.sonatype.com"; +const MAX_READINESS_RESPONSE_BYTES = 1024 * 1024; + +function fail(message) { + console.error(`verify-external-publish-readiness: ${message}`); + process.exit(1); +} + +function requiredEnv(name) { + const value = process.env[name]?.trim(); + if (!value) { + fail(`${name} is required`); + } + return value; +} + +function productsFromEnvironment() { + let products; + try { + products = JSON.parse(requiredEnv("PRODUCTS_JSON")); + } catch (error) { + fail(`PRODUCTS_JSON must be strict JSON: ${error.message}`); + } + if ( + !Array.isArray(products) + || products.length === 0 + || products.some((product) => typeof product !== "string" || product.length === 0) + ) { + fail("PRODUCTS_JSON must be a non-empty product string list"); + } + return products; +} + +function safeResponseMessage(body) { + return body.replace(/[\r\n\t]+/gu, " ").trim().slice(0, 300); +} + +async function boundedResponseText(response, context) { + const contentLength = response.headers.get("content-length"); + if (contentLength !== null) { + const declared = Number(contentLength); + if (!Number.isSafeInteger(declared) || declared < 0) { + await response.body?.cancel?.().catch(() => {}); + throw new Error(`${context} returned an invalid Content-Length`); + } + if (declared > MAX_READINESS_RESPONSE_BYTES) { + await response.body?.cancel?.().catch(() => {}); + throw new Error(`${context} response exceeded ${MAX_READINESS_RESPONSE_BYTES} bytes`); + } + } + const reader = response.body?.getReader?.(); + if (reader === undefined) { + const text = await response.text(); + if (Buffer.byteLength(text) > MAX_READINESS_RESPONSE_BYTES) { + throw new Error(`${context} response exceeded ${MAX_READINESS_RESPONSE_BYTES} bytes`); + } + return text; + } + const chunks = []; + let size = 0; + try { + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > MAX_READINESS_RESPONSE_BYTES) { + await reader.cancel().catch(() => {}); + throw new Error(`${context} response exceeded ${MAX_READINESS_RESPONSE_BYTES} bytes`); + } + chunks.push(Buffer.from(value)); + } + } finally { + reader.releaseLock(); + } + return Buffer.concat(chunks, size).toString("utf8"); +} + +async function requestJson(url, { authorization = undefined, context }) { + let lastFailure; + for (let attempt = 1; attempt <= 3; attempt += 1) { + try { + const headers = { + Accept: "application/json", + "User-Agent": "oliphaunt-release-readiness/1; https://github.com/f0rr0/oliphaunt", + }; + if (authorization !== undefined) { + headers.Authorization = authorization; + } + const response = await fetch(url, { + headers, + redirect: "error", + signal: AbortSignal.timeout(15_000), + }); + const body = await boundedResponseText(response, context); + if (!response.ok) { + const detail = safeResponseMessage(body); + const failure = `${context} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`; + if (response.status !== 429 && response.status < 500) { + fail(failure); + } + lastFailure = failure; + } else { + try { + return JSON.parse(body); + } catch (error) { + fail(`${context} returned invalid JSON: ${error.message}`); + } + } + } catch (error) { + lastFailure = `${context} request failed: ${error.message}`; + } + if (attempt < 3) { + await Bun.sleep(attempt * 500); + } + } + fail(lastFailure ?? `${context} request failed`); +} + +function parseJsrIdentity(identity) { + const match = identity.match(/^@([a-z][a-z0-9-]*)\/([a-z][a-z0-9-]*)$/u); + if (match === null) { + fail(`invalid JSR identity in publication catalog: ${identity}`); + } + return { scope: match[1], packageName: match[2] }; +} + +async function verifyJsrIdentity(identity, expectedRepository) { + const { scope, packageName } = parseJsrIdentity(identity); + const url = `${JSR_API_BASE}/scopes/${encodeURIComponent(scope)}/packages/${encodeURIComponent(packageName)}`; + const pkg = await requestJson(url, { context: `JSR package ${identity}` }); + if (pkg?.scope !== scope || pkg?.name !== packageName) { + fail(`JSR package ${identity} returned mismatched identity metadata`); + } + const linkedRepository = pkg?.githubRepository; + const actualRepository = linkedRepository === null || linkedRepository === undefined + ? null + : `${linkedRepository.owner}/${linkedRepository.name}`; + if (actualRepository !== expectedRepository) { + fail( + `JSR package ${identity} must exist and link to ${expectedRepository}; got ${actualRepository ?? "no linked repository"}. ` + + "Create/link it in JSR package settings before normal publication.", + ); + } + console.log(`JSR readiness passed: ${identity} is linked to ${expectedRepository}`); +} + +function mavenNamespaces(catalog, expectedNamespace) { + const groups = new Set(); + for (const carrier of catalog.carriers.filter(({ ecosystem }) => ecosystem === "maven")) { + const separator = carrier.name.indexOf(":"); + if (separator <= 0 || separator === carrier.name.length - 1) { + fail(`invalid Maven identity in publication catalog: ${carrier.name}`); + } + const group = carrier.name.slice(0, separator); + if (group !== expectedNamespace && !group.startsWith(`${expectedNamespace}.`)) { + fail(`Maven group ${group} is outside verified namespace ${expectedNamespace}`); + } + groups.add(group); + } + return [...groups].sort(); +} + +async function verifyMavenNamespace(catalog) { + const expectedNamespace = requiredEnv("MAVEN_CENTRAL_NAMESPACE"); + const groups = mavenNamespaces(catalog, expectedNamespace); + if (groups.length === 0) { + return; + } + const username = requiredEnv("ORG_GRADLE_PROJECT_mavenCentralUsername"); + const password = requiredEnv("ORG_GRADLE_PROJECT_mavenCentralPassword"); + const authorization = mavenCentralAuthorization(username, password); + const url = new URL("/api/v1/publisher/deployments", MAVEN_CENTRAL_API_BASE); + url.searchParams.set("namespace", expectedNamespace); + url.searchParams.set("page", "0"); + url.searchParams.set("size", "1"); + const result = await requestJson(url, { + authorization, + context: `Maven Central namespace ${expectedNamespace}`, + }); + if ( + !Array.isArray(result?.deployments) + || !Number.isInteger(result?.page) + || !Number.isInteger(result?.pageSize) + || !Number.isInteger(result?.pageCount) + || !Number.isInteger(result?.totalResultCount) + ) { + fail(`Maven Central namespace ${expectedNamespace} returned an unexpected response shape`); + } + console.log( + `Maven Central readiness passed: credentials can access ${expectedNamespace}; selected groups: ${groups.join(", ")}`, + ); +} + +const products = productsFromEnvironment(); +const catalog = loadPublicationCatalog("verify-external-publish-readiness", { products }); +const expectedRepository = requiredEnv("CANONICAL_RELEASE_REPOSITORY"); +const jsrIdentities = [ + ...new Set(catalog.carriers.filter(({ ecosystem }) => ecosystem === "jsr").map(({ name }) => name)), +].sort(); + +for (const identity of jsrIdentities) { + await verifyJsrIdentity(identity, expectedRepository); +} +await verifyMavenNamespace(catalog); + +if (jsrIdentities.length === 0 && !catalog.carriers.some(({ ecosystem }) => ecosystem === "maven")) { + console.log("selected products do not require JSR or Maven Central external readiness checks"); +} diff --git a/.github/scripts/verify-github-oidc-identity.mjs b/.github/scripts/verify-github-oidc-identity.mjs new file mode 100644 index 00000000..a46936c9 --- /dev/null +++ b/.github/scripts/verify-github-oidc-identity.mjs @@ -0,0 +1,205 @@ +#!/usr/bin/env bun + +import process from "node:process"; + +const TOOL = "verify-github-oidc-identity"; +const OIDC_ISSUER = "https://token.actions.githubusercontent.com"; +const OIDC_AUDIENCE = "oliphaunt-release-identity-preflight"; +const MAX_OIDC_RESPONSE_BYTES = 128 * 1024; +const CALLER_WORKFLOW = "release.yml"; +const CALLED_WORKFLOW = "release-execute.yml"; +const ENVIRONMENT_BY_OPERATION = Object.freeze({ + "publish-bootstrap": "release-bootstrap", + publish: "release-publish", +}); + +function required(environment, name) { + const value = environment[name]?.trim(); + if (!value) { + throw new Error(`${name} is required`); + } + return value; +} + +function requireFullSha(value, name) { + if (!/^[0-9a-f]{40}$/u.test(value)) { + throw new Error(`${name} must be a lowercase full commit SHA; got ${value}`); + } + return value; +} + +export function expectedOidcIdentity(environment = process.env) { + const operation = required(environment, "RELEASE_OPERATION"); + const releaseEnvironment = ENVIRONMENT_BY_OPERATION[operation]; + if (releaseEnvironment === undefined) { + throw new Error(`RELEASE_OPERATION must be publish-bootstrap or publish; got ${operation}`); + } + + const repository = required(environment, "CANONICAL_RELEASE_REPOSITORY"); + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)) { + throw new Error(`CANONICAL_RELEASE_REPOSITORY must be owner/repository; got ${repository}`); + } + const ref = required(environment, "GITHUB_REF"); + if (ref !== "refs/heads/main") { + throw new Error(`trusted publication must run from refs/heads/main; got ${ref}`); + } + const sha = requireFullSha(required(environment, "GITHUB_SHA"), "GITHUB_SHA"); + const eventName = required(environment, "GITHUB_EVENT_NAME"); + if (eventName !== "workflow_dispatch") { + throw new Error(`trusted publication must originate from workflow_dispatch; got ${eventName}`); + } + + return Object.freeze({ + aud: OIDC_AUDIENCE, + environment: releaseEnvironment, + event_name: eventName, + iss: OIDC_ISSUER, + job_workflow_ref: `${repository}/.github/workflows/${CALLED_WORKFLOW}@${ref}`, + job_workflow_sha: sha, + ref, + ref_type: "branch", + repository, + runner_environment: "github-hosted", + sha, + workflow_ref: `${repository}/.github/workflows/${CALLER_WORKFLOW}@${ref}`, + workflow_sha: sha, + }); +} + +function printable(value) { + return typeof value === "string" ? JSON.stringify(value) : JSON.stringify(value ?? null); +} + +export function verifyOidcClaims(claims, expected) { + if (claims === null || typeof claims !== "object" || Array.isArray(claims)) { + throw new Error("GitHub OIDC token payload must be an object"); + } + for (const [claim, value] of Object.entries(expected)) { + if (claims[claim] !== value) { + throw new Error( + `GitHub OIDC claim ${claim} mismatch: expected ${printable(value)}, got ${printable(claims[claim])}`, + ); + } + } + return claims; +} + +export function decodeJwtPayload(token) { + if (typeof token !== "string" || token.length === 0 || token.length > 100_000) { + throw new Error("GitHub OIDC response did not contain a bounded JWT"); + } + const parts = token.split("."); + if (parts.length !== 3 || parts.some((part) => part.length === 0)) { + throw new Error("GitHub OIDC response was not a three-part JWT"); + } + let payload; + try { + payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); + } catch (error) { + throw new Error(`GitHub OIDC JWT payload is invalid: ${error.message}`); + } + if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { + throw new Error("GitHub OIDC JWT payload must be an object"); + } + return payload; +} + +export function oidcRequestUrl(rawUrl) { + let url; + try { + url = new URL(rawUrl); + } catch (error) { + throw new Error(`ACTIONS_ID_TOKEN_REQUEST_URL is invalid: ${error.message}`); + } + if (url.protocol !== "https:") { + throw new Error("ACTIONS_ID_TOKEN_REQUEST_URL must use HTTPS"); + } + url.searchParams.set("audience", OIDC_AUDIENCE); + return url; +} + +export async function readBoundedOidcResponse(response) { + const contentLength = response.headers.get("content-length"); + if (contentLength !== null) { + const declaredLength = Number(contentLength); + if (!Number.isSafeInteger(declaredLength) || declaredLength < 0) { + throw new Error("GitHub OIDC endpoint returned an invalid Content-Length"); + } + if (declaredLength > MAX_OIDC_RESPONSE_BYTES) { + throw new Error("GitHub OIDC endpoint response exceeded the byte limit"); + } + } + + if (response.body === null) { + return ""; + } + const reader = response.body.getReader(); + const chunks = []; + let total = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > MAX_OIDC_RESPONSE_BYTES) { + await reader.cancel(); + throw new Error("GitHub OIDC endpoint response exceeded the byte limit"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + return Buffer.concat(chunks, total).toString("utf8"); +} + +async function requestOidcToken(environment = process.env, fetchImpl = fetch) { + const url = oidcRequestUrl(required(environment, "ACTIONS_ID_TOKEN_REQUEST_URL")); + const requestToken = required(environment, "ACTIONS_ID_TOKEN_REQUEST_TOKEN"); + const response = await fetchImpl(url, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${requestToken}`, + }, + redirect: "error", + signal: AbortSignal.timeout(15_000), + }); + const body = await readBoundedOidcResponse(response); + if (!response.ok) { + throw new Error(`GitHub OIDC endpoint returned HTTP ${response.status}`); + } + let result; + try { + result = JSON.parse(body); + } catch (error) { + throw new Error(`GitHub OIDC endpoint returned invalid JSON: ${error.message}`); + } + if (typeof result?.value !== "string" || result.value.length === 0) { + throw new Error("GitHub OIDC endpoint response did not contain a token value"); + } + return result.value; +} + +export async function verifyGithubOidcIdentity(environment = process.env, fetchImpl = fetch) { + const expected = expectedOidcIdentity(environment); + const token = await requestOidcToken(environment, fetchImpl); + const claims = decodeJwtPayload(token); + verifyOidcClaims(claims, expected); + return expected; +} + +async function main() { + try { + const expected = await verifyGithubOidcIdentity(); + console.log( + `GitHub OIDC identity passed: caller=${expected.workflow_ref}, reusable=${expected.job_workflow_ref}, environment=${expected.environment}`, + ); + } catch (error) { + console.error(`${TOOL}: ${error.message}`); + process.exit(1); + } +} + +if (import.meta.main) { + await main(); +} diff --git a/.github/scripts/verify-release-candidate.mjs b/.github/scripts/verify-release-candidate.mjs new file mode 100644 index 00000000..e7603c6d --- /dev/null +++ b/.github/scripts/verify-release-candidate.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env bun +import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +import { + affectedPlanBinding, + assertBindingMatches, + assertCandidateBindingShape, + wasixEvidenceBinding, +} from "./release-candidate-lib.mjs"; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function requiredEnv(name) { + const value = process.env[name]?.trim(); + if (!value) { + fail(`${name} is required`); + } + return value; +} + +function git(args) { + const result = spawnSync("git", args, { encoding: "utf8" }); + if (result.error || result.status !== 0) { + fail(result.stderr?.trim() || result.error?.message || `git ${args.join(" ")} failed`); + } + return result.stdout.trim(); +} + +function parseArgs(argv) { + const candidatePath = argv[0]; + const values = new Map(); + for (let index = 1; index < argv.length; index += 1) { + const name = argv[index]; + if (!["--plan", "--wasix-evidence-required", "--wasix-evidence-root"].includes(name)) { + fail(`unknown argument: ${name}`); + } + if (index + 1 >= argv.length) { + fail(`${name} requires a value`); + } + values.set(name.slice(2), argv[index + 1]); + index += 1; + } + if (!candidatePath || !values.has("plan") || !values.has("wasix-evidence-required")) { + fail( + "usage: verify-release-candidate.mjs --plan " + + "--wasix-evidence-required true|false [--wasix-evidence-root ]", + ); + } + const required = values.get("wasix-evidence-required"); + if (!["true", "false"].includes(required)) { + fail("--wasix-evidence-required must be true or false"); + } + if (required === "true" && !values.has("wasix-evidence-root")) { + fail("--wasix-evidence-root is required when WASIX evidence is required"); + } + return { + candidatePath, + planPath: values.get("plan"), + wasixEvidenceRequired: required === "true", + wasixEvidenceRoot: values.get("wasix-evidence-root"), + }; +} + +const args = parseArgs(process.argv.slice(2)); + +let candidate; +try { + candidate = JSON.parse(readFileSync(args.candidatePath, "utf8")); +} catch (error) { + fail(`invalid release candidate ${args.candidatePath}: ${error.message}`); +} + +try { + assertCandidateBindingShape(candidate); +} catch (error) { + fail(error.message); +} + +const expected = { + repository: requiredEnv("GITHUB_REPOSITORY"), + runId: requiredEnv("CI_RUN_ID"), + sha: requiredEnv("RELEASE_HEAD_SHA").toLowerCase(), +}; +const expectedTree = git(["rev-parse", `${expected.sha}^{tree}`]).toLowerCase(); + +for (const [field, value] of Object.entries({ + schemaVersion: 2, + repository: expected.repository, + workflow: "CI", + runId: expected.runId, + ref: "refs/heads/main", + sha: expected.sha, + tree: expectedTree, +})) { + if (candidate?.[field] !== value) { + fail(`release candidate ${field} mismatch: expected ${value}, got ${candidate?.[field]}`); + } +} + +if (!["push", "workflow_dispatch"].includes(candidate.eventName)) { + fail(`release candidate event must be push or workflow_dispatch, got ${candidate.eventName}`); +} +if (!Number.isSafeInteger(candidate.runAttempt) || candidate.runAttempt < 1) { + fail(`release candidate has invalid runAttempt: ${candidate.runAttempt}`); +} +if (typeof candidate.workflowRef !== "string" || !candidate.workflowRef.includes("/.github/workflows/ci.yml@")) { + fail(`release candidate has invalid workflowRef: ${candidate.workflowRef}`); +} + +let expectedPlan; +try { + expectedPlan = affectedPlanBinding( + args.planPath, + candidate.affectedPlan.wasixReleaseRegressionRequired, + ); +} catch (error) { + fail(error.message); +} +try { + assertBindingMatches(candidate.affectedPlan, expectedPlan, "release candidate affected plan"); +} catch (error) { + fail(error.message); +} + +if (args.wasixEvidenceRequired) { + if (!candidate.evidenceRequirements.wasixReleaseRegression) { + fail("selected release products require WASIX evidence, but the qualified CI plan did not require it"); + } + let evidence; + try { + evidence = wasixEvidenceBinding(args.wasixEvidenceRoot, { + repository: expected.repository, + workflow: "CI", + runId: expected.runId, + runAttempt: candidate.runAttempt, + sha: expected.sha, + tree: expectedTree, + }); + } catch (error) { + fail(error.message); + } + try { + assertBindingMatches( + candidate.evidence.wasixReleaseRegression, + evidence, + "release candidate WASIX evidence", + ); + } catch (error) { + fail(error.message); + } +} + +console.log(`verified qualified CI run ${candidate.runId} for ${candidate.sha}`); diff --git a/.github/scripts/write-affected-moon-target-matrices.mjs b/.github/scripts/write-affected-moon-target-matrices.mjs new file mode 100644 index 00000000..ba450a18 --- /dev/null +++ b/.github/scripts/write-affected-moon-target-matrices.mjs @@ -0,0 +1,335 @@ +#!/usr/bin/env bun +import {appendFileSync} from 'node:fs'; +import {spawnSync} from 'node:child_process'; +import process from 'node:process'; + +import {moonCommand} from '../../tools/dev/moon-command.mjs'; +import { + matrixTarget, + shardCheckTargets, + taskDependencies, +} from './moon-task-capabilities.mjs'; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function output(name, value) { + const rendered = typeof value === 'string' ? value : JSON.stringify(value); + console.log(`${name}=${rendered}`); + const outputPath = process.env.GITHUB_OUTPUT; + if (outputPath) { + appendFileSync(outputPath, `${name}=${rendered}\n`, 'utf8'); + } +} + +function useAffectedQuery() { + return Boolean(process.env.MOON_BASE?.trim() && process.env.MOON_HEAD?.trim()); +} + +function moonQueryTaskArgs(taskId = '', {affected = useAffectedQuery()} = {}) { + const args = ['query', 'tasks']; + if (affected) { + args.push('--affected'); + } + if (taskId) { + args.push('--id', taskId); + } + if (affected) { + args.push('--upstream', 'none', '--downstream', 'deep'); + } + return args; +} + +function targetsForTask(taskId) { + const result = spawnSync('bun', ['.github/scripts/select-affected-moon-targets.mjs', taskId], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + env: process.env, + }); + if (result.status !== 0) { + fail(`failed to select affected Moon ${taskId} targets`); + } + return result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); +} + +function taskMapForTask(taskId) { + const result = spawnSync( + moonCommand(), + moonQueryTaskArgs(taskId), + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }, + ); + if (result.status !== 0) { + fail(`moon query tasks failed for ${taskId}`); + } + let query; + try { + query = JSON.parse(result.stdout); + } catch (error) { + fail(`moon query tasks returned invalid JSON for ${taskId}: ${error.message}`); + } + const tasksByProject = query.tasks; + if (!tasksByProject || typeof tasksByProject !== 'object' || Array.isArray(tasksByProject)) { + fail(`moon query tasks did not return a tasks object for ${taskId}`); + } + const tasks = new Map(); + for (const projectTasks of Object.values(tasksByProject)) { + if (!projectTasks || typeof projectTasks !== 'object' || Array.isArray(projectTasks)) { + continue; + } + for (const task of Object.values(projectTasks)) { + if (task?.id === taskId && typeof task.target === 'string') { + tasks.set(task.target, task); + } + } + } + return tasks; +} + +function selectedScopeTaskMap() { + const result = spawnSync( + moonCommand(), + moonQueryTaskArgs(), + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }, + ); + if (result.status !== 0) { + fail('moon query tasks failed for selected-scope tasks'); + } + let query; + try { + query = JSON.parse(result.stdout); + } catch (error) { + fail(`moon query tasks returned invalid JSON for selected-scope tasks: ${error.message}`); + } + const tasksByProject = query.tasks; + if (!tasksByProject || typeof tasksByProject !== 'object' || Array.isArray(tasksByProject)) { + fail('moon query tasks did not return a tasks object for selected-scope tasks'); + } + const tasks = new Map(); + for (const projectTasks of Object.values(tasksByProject)) { + if (!projectTasks || typeof projectTasks !== 'object' || Array.isArray(projectTasks)) { + continue; + } + for (const task of Object.values(projectTasks)) { + if (task && typeof task === 'object' && typeof task.target === 'string') { + tasks.set(task.target, task); + } + } + } + return tasks; +} + +function allTaskMap() { + const result = spawnSync( + moonCommand(), + moonQueryTaskArgs('', {affected: false}), + { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }, + ); + if (result.status !== 0) { + fail('moon query tasks failed for complete task capability metadata'); + } + let query; + try { + query = JSON.parse(result.stdout); + } catch (error) { + fail(`moon query tasks returned invalid JSON for complete task capability metadata: ${error.message}`); + } + const tasksByProject = query.tasks; + if (!tasksByProject || typeof tasksByProject !== 'object' || Array.isArray(tasksByProject)) { + fail('moon query tasks did not return a tasks object for complete task capability metadata'); + } + const tasks = new Map(); + for (const projectTasks of Object.values(tasksByProject)) { + if (!projectTasks || typeof projectTasks !== 'object' || Array.isArray(projectTasks)) { + continue; + } + for (const task of Object.values(projectTasks)) { + if (task && typeof task === 'object' && typeof task.target === 'string') { + tasks.set(task.target, task); + } + } + } + return tasks; +} + +function commandText(task) { + const parts = []; + if (typeof task?.command === 'string') { + parts.push(task.command); + } + if (Array.isArray(task?.args)) { + parts.push(...task.args.filter((arg) => typeof arg === 'string')); + } + return parts.join(' ').trim(); +} + +function tags(task) { + return new Set(Array.isArray(task?.tags) ? task.tags : []); +} + +const policyProjectIds = new Set([ + 'dev-tools', + 'graph-tools', + 'perf-tools', + 'policy-tools', + 'release-tools', +]); +function projectId(target) { + return target.split(':', 1)[0] ?? ''; +} + +function isPolicyTarget(task) { + const taskTags = tags(task); + const command = commandText(task); + return ( + taskTags.has('policy') || + taskTags.has('assertion') || + command.includes('tools/policy/assertions/assert-') || + command.includes('src/extensions/tools/check-extension-') || + policyProjectIds.has(projectId(task.target)) + ); +} + +function isNoopTask(task) { + return commandText(task) === 'true'; +} + +function runsInCI(task) { + const value = task?.options?.runInCI; + return value !== false && value !== 'skip'; +} + +function addMatrixTarget(targets, task, upstream, allTasks) { + const target = task.target; + const existing = targets.get(target); + if (!existing || existing.upstream !== 'none') { + targets.set(target, matrixTarget(task, upstream, allTasks)); + } +} + +function classifyTarget(task, targets, allTasks) { + if (!runsInCI(task)) return; + if (isPolicyTarget(task)) { + addMatrixTarget(targets.policy, task, 'none', allTasks); + } else if (!isNoopTask(task)) { + addMatrixTarget(targets.check, task, 'deep', allTasks); + } +} + +function classifySelectedTask(task, targets, {selectedScopeTasks, allTasks, visiting = new Set()}) { + if (!runsInCI(task)) return; + if (!isNoopTask(task)) { + classifyTarget(task, targets, allTasks); + return; + } + if (visiting.has(task.target)) { + fail(`Moon aggregate task cycle through ${task.target}`); + } + visiting.add(task.target); + for (const dependency of taskDependencies(task)) { + const dependencyTask = selectedScopeTasks.get(dependency); + if (dependencyTask !== undefined) { + classifySelectedTask(dependencyTask, targets, {selectedScopeTasks, allTasks, visiting}); + } + } + visiting.delete(task.target); +} + +function matrix(targets) { + return { + include: targets.map((target) => { + if (typeof target === 'string') { + return {target, upstream: 'deep'}; + } + return target; + }), + }; +} + +const taskIds = process.argv.slice(2); +if (taskIds.length === 0 || taskIds.some((taskId) => !/^[A-Za-z0-9_-]+$/.test(taskId))) { + fail('usage: write-affected-moon-target-matrices.mjs [ ...]'); +} + +const completeTasks = allTaskMap(); +let wroteCheckOutputs = false; +let wrotePolicyOutputs = false; +for (const taskId of taskIds) { + const targets = targetsForTask(taskId); + if (taskId === 'check') { + const taskMap = taskMapForTask(taskId); + const selectedScopeTasks = selectedScopeTaskMap(); + const checkTargets = new Map(); + const policyTargets = new Map(); + for (const target of targets) { + const task = taskMap.get(target); + if (!task) { + fail(`Moon metadata did not include selected target ${target}`); + } + classifySelectedTask(task, {check: checkTargets, policy: policyTargets}, { + selectedScopeTasks, + allTasks: completeTasks, + }); + } + const checkShards = shardCheckTargets([...checkTargets.values()]); + output('check_count', String(checkTargets.size)); + output('check_job_count', String(checkShards.length)); + output('check_matrix', matrix(checkShards)); + output('policy_count', String(policyTargets.size)); + output('policy_matrix', matrix([...policyTargets.values()])); + output( + 'policy_requires_android_sdk', + String([...policyTargets.values()].some((target) => target.requires_android_sdk)), + ); + output( + 'policy_requires_maintainer_tools', + String([...policyTargets.values()].some((target) => target.requires_maintainer_tools)), + ); + output('check_jobs', [ + ...(checkTargets.size > 0 ? ['check-targets'] : []), + ...(policyTargets.size > 0 ? ['policy-targets'] : []), + ]); + wroteCheckOutputs = true; + wrotePolicyOutputs = true; + continue; + } + const taskMap = taskMapForTask(taskId); + const matrixTargets = targets.flatMap((target) => { + const task = taskMap.get(target); + if (!task) { + fail(`Moon metadata did not include selected target ${target}`); + } + return runsInCI(task) ? [matrixTarget(task, 'deep', completeTasks)] : []; + }); + output(`${taskId}_count`, String(matrixTargets.length)); + output(`${taskId}_matrix`, matrix(matrixTargets)); + if (taskId === 'test') { + output('test_jobs', matrixTargets.length > 0 ? ['test-targets'] : []); + } +} + +if (!wroteCheckOutputs) { + output('check_count', '0'); + output('check_job_count', '0'); + output('check_matrix', matrix([])); +} +if (!wrotePolicyOutputs) { + output('policy_count', '0'); + output('policy_matrix', matrix([])); + output('policy_requires_android_sdk', 'false'); + output('policy_requires_maintainer_tools', 'false'); + output('check_jobs', []); +} diff --git a/.github/scripts/write-release-candidate.mjs b/.github/scripts/write-release-candidate.mjs new file mode 100644 index 00000000..bb49d629 --- /dev/null +++ b/.github/scripts/write-release-candidate.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env bun +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +import { + affectedPlanBinding, + assertCandidateBindingShape, + wasixEvidenceBinding, +} from "./release-candidate-lib.mjs"; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function requiredEnv(name) { + const value = process.env[name]?.trim(); + if (!value) { + fail(`${name} is required`); + } + return value; +} + +function git(args) { + const result = spawnSync("git", args, { encoding: "utf8" }); + if (result.error || result.status !== 0) { + fail(result.stderr?.trim() || result.error?.message || `git ${args.join(" ")} failed`); + } + return result.stdout.trim(); +} + +const outputPath = process.argv[2] ?? "target/qualification/oliphaunt-release-candidate.json"; +const expectedSha = requiredEnv("CI_HEAD_SHA").toLowerCase(); +if (!/^[0-9a-f]{40}$/u.test(expectedSha)) { + fail(`CI_HEAD_SHA must be a full commit SHA, got ${expectedSha}`); +} + +const checkedOutSha = git(["rev-parse", "HEAD^{commit}"]).toLowerCase(); +if (checkedOutSha !== expectedSha) { + fail(`checked-out commit ${checkedOutSha} does not match CI_HEAD_SHA ${expectedSha}`); +} + +const tree = git(["rev-parse", "HEAD^{tree}"]).toLowerCase(); +const wasixRequiredRaw = requiredEnv("WASIX_RELEASE_REGRESSION_REQUIRED"); +if (!["true", "false"].includes(wasixRequiredRaw)) { + fail(`WASIX_RELEASE_REGRESSION_REQUIRED must be true or false, got ${wasixRequiredRaw}`); +} +const wasixRequired = wasixRequiredRaw === "true"; +let affectedPlan; +try { + affectedPlan = affectedPlanBinding(requiredEnv("CI_PLAN_PATH"), wasixRequired); +} catch (error) { + fail(error.message); +} + +const runAttempt = Number.parseInt(requiredEnv("GITHUB_RUN_ATTEMPT"), 10); +if (!Number.isSafeInteger(runAttempt) || runAttempt < 1) { + fail(`invalid GITHUB_RUN_ATTEMPT: ${process.env.GITHUB_RUN_ATTEMPT}`); +} + +let wasixEvidence = null; +if (wasixRequired) { + try { + wasixEvidence = wasixEvidenceBinding(requiredEnv("WASIX_EVIDENCE_ROOT"), { + repository: requiredEnv("GITHUB_REPOSITORY"), + workflow: requiredEnv("GITHUB_WORKFLOW"), + runId: requiredEnv("GITHUB_RUN_ID"), + runAttempt, + sha: checkedOutSha, + tree, + }); + } catch (error) { + fail(error.message); + } +} + +const candidate = { + schemaVersion: 2, + repository: requiredEnv("GITHUB_REPOSITORY"), + workflow: requiredEnv("GITHUB_WORKFLOW"), + workflowRef: requiredEnv("GITHUB_WORKFLOW_REF"), + runId: requiredEnv("GITHUB_RUN_ID"), + runAttempt, + eventName: requiredEnv("GITHUB_EVENT_NAME"), + ref: requiredEnv("GITHUB_REF"), + sha: checkedOutSha, + tree, + affectedPlan, + evidenceRequirements: { + wasixReleaseRegression: wasixRequired, + artifacts: wasixRequired ? ["wasix-release-regression-evidence"] : [], + }, + evidence: { + wasixReleaseRegression: wasixEvidence, + }, +}; + +try { + assertCandidateBindingShape(candidate); +} catch (error) { + fail(error.message); +} + +mkdirSync(dirname(outputPath), { recursive: true }); +writeFileSync(outputPath, `${JSON.stringify(candidate, null, 2)}\n`, "utf8"); +console.log(`wrote release qualification record for ${candidate.sha} to ${outputPath}`); diff --git a/.github/workflows/assets.yml b/.github/workflows/assets.yml deleted file mode 100644 index 14a65ae8..00000000 --- a/.github/workflows/assets.yml +++ /dev/null @@ -1,225 +0,0 @@ -name: Assets -run-name: Assets / ${{ github.event_name == 'workflow_dispatch' && inputs.target || (github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name) }} - -on: - pull_request: - paths: - - ".github/workflows/assets.yml" - - ".github/actions/setup-wasmer-llvm/**" - - "Cargo.lock" - - "Cargo.toml" - - "assets/**" - - "crates/aot/**" - - "crates/assets/**" - - "xtask/**" - push: - branches: [main] - paths: - - ".github/workflows/assets.yml" - - ".github/actions/setup-wasmer-llvm/**" - - "Cargo.lock" - - "Cargo.toml" - - "assets/**" - - "crates/aot/**" - - "crates/assets/**" - - "xtask/**" - workflow_dispatch: - inputs: - target: - description: Native AOT target to build - required: true - default: all - type: choice - options: - - all - - aarch64-apple-darwin - - x86_64-unknown-linux-gnu - - aarch64-unknown-linux-gnu - - x86_64-pc-windows-msvc - schedule: - - cron: "17 3 * * 1" - -permissions: - contents: read - -concurrency: - group: assets-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.target || 'all' }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - ASSET_PROFILE: release-o3 - RUST_CACHE_SAVE_IF: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - WASMER_LLVM_VERSION: "22.1" - WASMER_LLVM_LINUX_X64_URL: https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-amd64.tar.xz - -defaults: - run: - shell: bash - -jobs: - portable-wasix: - name: Build portable WASIX assets - runs-on: ubuntu-latest - timeout-minutes: 360 - permissions: - contents: read - actions: write - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools - with: - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - - name: Verify source-controlled asset inputs - run: cargo run -p xtask -- assets verify-committed - - - name: Fetch pinned asset sources - run: cargo run -p xtask -- assets fetch - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - - - name: Build WASIX builder image and save cache - if: ${{ github.ref == 'refs/heads/main' }} - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f - with: - context: assets/wasix-build/docker - file: assets/wasix-build/docker/Dockerfile - tags: pglite-oxide-wasix-build:ci - load: true - cache-from: type=gha,scope=wasix-builder - cache-to: type=gha,mode=max,scope=wasix-builder - - - name: Build WASIX builder image - if: ${{ github.ref != 'refs/heads/main' }} - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f - with: - context: assets/wasix-build/docker - file: assets/wasix-build/docker/Dockerfile - tags: pglite-oxide-wasix-build:ci - load: true - cache-from: type=gha,scope=wasix-builder - - - name: Install Wasmer LLVM 22.1 for WASIX template generation - uses: ./.github/actions/setup-wasmer-llvm - with: - url: ${{ env.WASMER_LLVM_LINUX_X64_URL }} - version: ${{ env.WASMER_LLVM_VERSION }} - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - - name: Build portable WASIX modules and package runtime assets - env: - IMAGE: pglite-oxide-wasix-build:ci - run: | - cargo run -p xtask --features template-runner -- assets release-build \ - --profile "$ASSET_PROFILE" \ - --target-triple x86_64-unknown-linux-gnu \ - --skip-aot \ - --skip-package-size - - - name: Validate generated portable assets - run: cargo run -p xtask -- assets check --strict-generated - - - name: Upload portable WASIX build outputs - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: pglite-oxide-portable-wasix - path: | - assets/wasix-build/build/** - target/pglite-oxide/assets/** - assets/generated/** - if-no-files-found: error - - native-targets: - name: Select native AOT targets - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - matrix: ${{ steps.targets.outputs.matrix }} - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - persist-credentials: false - - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools - with: - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - - name: Build target matrix - id: targets - env: - REQUESTED_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.target || 'all' }} - run: cargo run --quiet -p xtask -- assets ci-matrix --target "$REQUESTED_TARGET" --github-output >> "$GITHUB_OUTPUT" - - native-aot: - name: Native AOT / ${{ matrix.target }} - needs: - - portable-wasix - - native-targets - runs-on: ${{ matrix.os }} - timeout-minutes: 180 - permissions: - contents: read - actions: write - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.native-targets.outputs.matrix) }} - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - fetch-depth: 0 - persist-credentials: false - - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools - with: - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - - name: Download portable WASIX build outputs - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: pglite-oxide-portable-wasix - path: . - - - name: Install Wasmer LLVM 22.1 for AOT generation - uses: ./.github/actions/setup-wasmer-llvm - with: - url: ${{ matrix.llvm_url }} - version: ${{ env.WASMER_LLVM_VERSION }} - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - - name: Generate and package target AOT artifacts - env: - AOT_TARGET: ${{ matrix.target }} - run: | - cargo run -p xtask -- assets aot --target-triple "$AOT_TARGET" - cargo run -p xtask -- assets package-aot --target-triple "$AOT_TARGET" - cargo run -p xtask -- assets check-aot --target-triple "$AOT_TARGET" - - - name: Check target AOT crate - env: - AOT_PACKAGE: ${{ matrix.package }} - run: cargo check -p "$AOT_PACKAGE" --locked - - - name: Run asset smoke tests - run: cargo run -p xtask -- assets smoke - - - name: Upload target artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: ${{ matrix.artifact }} - path: | - target/pglite-oxide/aot/${{ matrix.target }}/** - if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72091ffa..ed2881e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,392 +1,3043 @@ name: CI -run-name: CI / ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name }} +run-name: CI / ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.number) || github.ref_name }} on: pull_request: + types: [opened, synchronize, reopened, closed] + merge_group: + types: [checks_requested] push: branches: [main] - + workflow_dispatch: + inputs: + wasm_target: + description: WASM AOT target to build when WASM runtime inputs are affected + required: true + default: all + type: choice + options: + - all + - macos-arm64 + - linux-x64-gnu + - linux-arm64-gnu + - windows-x64-msvc + native_target: + description: Native runtime target to run for focused manual debugging + required: true + default: all + type: choice + options: + - all + - macos-arm64 + - linux-x64-gnu + - linux-arm64-gnu + - windows-x64-msvc + - android-arm64-v8a + - android-x86_64 + - ios-xcframework + mobile_target: + description: Mobile lane to run for focused manual debugging + required: true + default: all + type: choice + options: + - all + - android + - ios + - both permissions: contents: read concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || format('sha-{0}', github.sha) }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - PREK_VERSION: 0.3.10 - CARGO_HACK_VERSION: 0.6.44 - RUST_CACHE_SAVE_IF: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + NODE_VERSION: 22.22.3 + PNPM_VERSION: 11.5.0 + NPM_VERSION: 11.18.0 + BUN_VERSION: 1.3.14 + DENO_VERSION: v2.8.1 + ACTIONLINT_VERSION: 1.7.12 + ASSET_PROFILE: release + WASMER_LLVM_VERSION: "22.1" + WASMER_LLVM_LINUX_X64_URL: https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-amd64.tar.xz + WASMER_LLVM_LINUX_X64_SHA256: 5fb1c687c5e895d517a23e7aabea9ec3557e3a3e33f8a8d3a8d21395157b3906 + WASMER_LLVM_LINUX_X64_BYTES: 741670068 + RUST_CACHE_SAVE_IF: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }} + MOON_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }} + MOON_HEAD: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + +defaults: + run: + shell: bash jobs: - scope: - name: Determine changed surfaces - runs-on: ubuntu-latest - timeout-minutes: 5 + affected: + name: Plan + needs: + - release-intent + if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 outputs: - repo: ${{ steps.scope.outputs.repo }} - rust: ${{ steps.scope.outputs.rust }} - examples: ${{ steps.scope.outputs.examples }} - package: ${{ steps.scope.outputs.package }} - assets: ${{ steps.scope.outputs.assets }} - ci: ${{ steps.scope.outputs.ci }} - docs: ${{ steps.scope.outputs.docs }} - docs_only: ${{ steps.scope.outputs.docs_only }} + builder_jobs: ${{ steps.plan.outputs.builder_jobs }} + check_count: ${{ steps.target-matrices.outputs.check_count }} + check_job_count: ${{ steps.target-matrices.outputs.check_job_count }} + check_jobs: ${{ steps.target-matrices.outputs.check_jobs }} + check_matrix: ${{ steps.target-matrices.outputs.check_matrix }} + policy_count: ${{ steps.target-matrices.outputs.policy_count }} + policy_matrix: ${{ steps.target-matrices.outputs.policy_matrix }} + policy_requires_android_sdk: ${{ steps.target-matrices.outputs.policy_requires_android_sdk }} + policy_requires_maintainer_tools: ${{ steps.target-matrices.outputs.policy_requires_maintainer_tools }} + jobs: ${{ steps.plan.outputs.jobs }} + e2e_jobs: ${{ steps.plan.outputs.e2e_jobs }} + job_targets: ${{ steps.plan.outputs.job_targets }} + broker_runtime_matrix: ${{ steps.plan.outputs.broker_runtime_matrix }} + extension_artifacts_native_matrix: ${{ steps.plan.outputs.extension_artifacts_native_matrix }} + extension_artifacts_wasix_matrix: ${{ steps.plan.outputs.extension_artifacts_wasix_matrix }} + extension_package_products: ${{ steps.plan.outputs.extension_package_products }} + extension_package_products_csv: ${{ steps.plan.outputs.extension_package_products_csv }} + extension_package_sql_names: ${{ steps.plan.outputs.extension_package_sql_names }} + extension_package_sql_names_csv: ${{ steps.plan.outputs.extension_package_sql_names_csv }} + native_extension_lifecycle_sql_names: ${{ steps.plan.outputs.native_extension_lifecycle_sql_names }} + native_extension_lifecycle_sql_names_csv: ${{ steps.plan.outputs.native_extension_lifecycle_sql_names_csv }} + native_extension_lifecycle_matrix: ${{ steps.plan.outputs.native_extension_lifecycle_matrix }} + native_extension_lifecycle_shard_count: ${{ steps.plan.outputs.native_extension_lifecycle_shard_count }} + liboliphaunt_wasix_aot_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_wasix_aot_runtime_matrix }} + liboliphaunt_native_android_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_native_android_runtime_matrix }} + liboliphaunt_native_desktop_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_native_desktop_runtime_matrix }} + liboliphaunt_native_ios_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_native_ios_runtime_matrix }} + mobile_extension_package_native_targets: ${{ steps.plan.outputs.mobile_extension_package_native_targets }} + mobile_extension_package_native_targets_csv: ${{ steps.plan.outputs.mobile_extension_package_native_targets_csv }} + react_native_android_mobile_app_matrix: ${{ steps.plan.outputs.react_native_android_mobile_app_matrix }} + js_exact_candidate_consumer_matrix: ${{ steps.plan.outputs.js_exact_candidate_consumer_matrix }} + node_direct_runtime_matrix: ${{ steps.plan.outputs.node_direct_runtime_matrix }} + projects: ${{ steps.plan.outputs.projects }} + tasks: ${{ steps.plan.outputs.tasks }} + test_count: ${{ steps.target-matrices.outputs.test_count }} + test_jobs: ${{ steps.target-matrices.outputs.test_jobs }} + test_matrix: ${{ steps.target-matrices.outputs.test_matrix }} + reason: ${{ steps.plan.outputs.reason }} + wasix_release_regression_required: ${{ contains(fromJson(steps.plan.outputs.jobs), 'liboliphaunt-wasix-runtime') && (github.event_name != 'workflow_dispatch' || inputs.wasm_target == 'all' || inputs.wasm_target == 'linux-x64-gnu') }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Classify changed paths - id: scope + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "false" + + - name: Plan artifact builder jobs + id: plan env: - BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} - HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: scripts/ci-scope.sh "$BASE_SHA" "$HEAD_SHA" + GITHUB_EVENT_NAME: ${{ github.event_name }} + MOON_BASE: ${{ github.event.pull_request.base.sha }} + MOON_HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + 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: tools/dev/bun.sh tools/graph/ci_plan.mjs + + - name: Plan check and test jobs + id: target-matrices + run: bun .github/scripts/write-affected-moon-target-matrices.mjs check test - repo-hygiene: - name: Repository hygiene - needs: scope - runs-on: ubuntu-latest - timeout-minutes: 15 + - name: Upload build plan + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: artifact-build-plan + path: target/graph/ci-plan.json + if-no-files-found: error + + release-intent: + name: Validate commit and PR title + if: ${{ github.event_name != 'pull_request' || github.event.action != 'closed' }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Bun + uses: ./.github/actions/setup-bun with: - components: rustfmt - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - tools: prek@${{ env.PREK_VERSION }} - - - name: Validate repository hygiene - run: scripts/validate.sh repo + bun-version: ${{ env.BUN_VERSION }} - - name: Verify asset inputs - if: ${{ github.event_name == 'push' || needs.scope.outputs.assets == 'true' || needs.scope.outputs.package == 'true' || needs.scope.outputs.ci == 'true' }} - run: scripts/validate.sh artifacts + - name: Check release intent + id: release_intent + env: + PR_TITLE: ${{ github.event.pull_request.title }} + BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || github.event.merge_group.base_sha || 'origin/main' }} + HEAD_REF: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }} + HEAD_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + CI_EVENT_NAME: ${{ github.event_name }} + CI_FULL_REF: ${{ github.ref }} + run: | + subject="${PR_TITLE:-}" + if [ -z "$subject" ]; then + subject="$(git log -1 --pretty=%s "$HEAD_REF")" + fi + .github/scripts/check-release-intent.sh "$subject" "$BASE_REF" "$HEAD_REF" "$HEAD_BRANCH" "$CI_EVENT_NAME" "$CI_FULL_REF" - workflow-lint: - name: Workflow lint - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.ci == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - actions: read - contents: read + check-targets: + name: Checks / ${{ matrix.label }} + needs: + - affected + if: ${{ needs.affected.outputs.check_count != '0' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.check_matrix) }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Lint GitHub Actions workflows - uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" - - name: Audit GitHub Actions workflows - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e + - name: Set up Rust + if: ${{ matrix.requires_rust }} + uses: ./.github/actions/setup-rust with: - advanced-security: false - config: .github/zizmor.yml - inputs: .github/workflows .github/actions - min-severity: medium - persona: auditor - version: 1.24.1 + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - rust-lint: - name: Rust lint - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 45 + - name: Set up Android + if: ${{ matrix.requires_android_sdk }} + uses: ./.github/actions/setup-android + + - name: Install pinned maintainer tools + if: ${{ matrix.requires_maintainer_tools }} + run: tools/dev/bootstrap-tools.sh + + - name: Run check shard + env: + MOON_TARGET_MATRIX_JSON: ${{ matrix.targets_json }} + run: bun .github/scripts/run-moon-target-matrix.mjs + + policy-targets: + name: Policy + needs: + - affected + if: ${{ needs.affected.outputs.policy_count != '0' }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: - components: clippy cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Validate lint gates - run: scripts/validate.sh lint + - name: Set up Android + if: ${{ needs.affected.outputs.policy_requires_android_sdk == 'true' }} + uses: ./.github/actions/setup-android - rust-tests: - name: Rust tests - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} - runs-on: ubuntu-latest + - name: Install pinned maintainer tools + if: ${{ needs.affected.outputs.policy_requires_maintainer_tools == 'true' }} + run: tools/dev/bootstrap-tools.sh + + - name: Run selected policy targets + env: + MOON_TARGET_MATRIX_JSON: ${{ needs.affected.outputs.policy_matrix }} + run: bun .github/scripts/run-moon-target-matrix.mjs + + checks: + name: Checks + if: ${{ always() }} + needs: + - affected + - check-targets + - policy-targets + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Check selected check and policy jobs + id: selected_checks_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.affected.outputs.check_jobs }} + GATE_LABEL: selected check and policy jobs + run: bun .github/scripts/check-ci-gate.mjs selected + + test-targets: + name: Tests / ${{ matrix.target }} + needs: + - affected + if: ${{ needs.affected.outputs.test_count != '0' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.test_matrix) }} + runs-on: ubuntu-24.04 timeout-minutes: 90 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Rust + if: ${{ matrix.requires_rust }} + uses: ./.github/actions/setup-rust with: + tools: cargo-nextest@0.9.137,cargo-llvm-cov@0.8.7 cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Validate test gates - run: scripts/validate.sh test + - name: Set up Android + if: ${{ matrix.requires_android_sdk }} + uses: ./.github/actions/setup-android + with: + gradle-cache-scope-file: ${{ matrix.target == 'oliphaunt-kotlin:test' && 'src/sdks/kotlin/gradle/cache-scopes/linux-native-tests.txt' || '' }} + + - name: Install pinned maintainer tools + if: ${{ matrix.requires_maintainer_tools }} + run: tools/dev/bootstrap-tools.sh + + - name: Run test target + env: + MOON_TARGET: ${{ matrix.target }} + MOON_UPSTREAM: ${{ matrix.upstream }} + run: .github/scripts/run-moon-targets.sh --upstream "$MOON_UPSTREAM" "$MOON_TARGET" - runtime-targets: - name: Select runtime AOT targets - needs: scope - if: ${{ needs.scope.outputs.rust == 'true' && needs.scope.outputs.assets != 'true' }} - runs-on: ubuntu-latest + tests: + name: Tests + if: ${{ always() }} + needs: + - affected + - test-targets + runs-on: ubuntu-24.04 timeout-minutes: 5 - outputs: - matrix: ${{ steps.targets.outputs.matrix }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Bun + uses: ./.github/actions/setup-bun with: - cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + bun-version: ${{ env.BUN_VERSION }} - - name: Build target matrix - id: targets - run: cargo run --quiet -p xtask -- assets ci-matrix --github-output >> "$GITHUB_OUTPUT" + - name: Check selected test jobs + id: selected_tests_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.affected.outputs.test_jobs }} + GATE_LABEL: selected test jobs + run: bun .github/scripts/check-ci-gate.mjs selected - runtime-aot-tests: - name: Runtime AOT smoke / ${{ matrix.target }} + extension-artifacts-native: + name: Builds / extension-native (${{ matrix.target }}) needs: - - scope - - runtime-targets - if: ${{ needs.scope.outputs.rust == 'true' && needs.scope.outputs.assets != 'true' }} - runs-on: ${{ matrix.os }} - timeout-minutes: 180 - permissions: - contents: read - actions: read - defaults: - run: - shell: bash + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'extension-artifacts-native') }} strategy: fail-fast: false - matrix: ${{ fromJson(needs.runtime-targets.outputs.matrix) }} + matrix: ${{ fromJson(needs.affected.outputs.extension_artifacts_native_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + env: + CCACHE_DIR: ${{ github.workspace }}/.ci-cache/ccache/native-extension/${{ matrix.target }} + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_COMPRESS: "true" + OLIPHAUNT_CCACHE_MAX_SIZE: ${{ matrix.target == 'ios-xcframework' && '512M' || '2G' }} + OLIPHAUNT_CCACHE_ZERO_STATS: "1" steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Apple + if: ${{ runner.os == 'macOS' }} + uses: ./.github/actions/setup-apple + + - name: Set up Android + if: ${{ startsWith(matrix.target, 'android-') }} + uses: ./.github/actions/setup-android + with: + gradle-cache: "false" + + - name: Set up MSVC + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-msvc + + - name: Verify Windows VC runtime atomic staging + if: ${{ runner.os == 'Windows' }} + run: tools/dev/bun.sh test tools/release/windows-vc-runtime-closure.test.mjs + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Download compatible runtime artifacts + - name: Prepare native compiler cache path + if: ${{ matrix.target == 'ios-xcframework' }} + run: mkdir -p "$CCACHE_DIR" + + - name: Restore native compiler cache + if: ${{ matrix.target == 'ios-xcframework' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ env.CCACHE_DIR }} + key: liboliphaunt-native-extension-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/extensions/**', 'src/runtimes/liboliphaunt/native/**', 'tools/xtask/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + liboliphaunt-native-extension-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Configure native compiler cache + run: .github/scripts/setup-native-build-tools.sh + + - name: Build native exact-extension artifacts env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AOT_TARGET: ${{ matrix.target }} + OLIPHAUNT_EXTENSION_PRODUCTS: ${{ matrix.extensions_csv }} + OLIPHAUNT_EXTENSION_TARGET: ${{ matrix.target }} + OLIPHAUNT_BISON: /opt/homebrew/opt/bison/bin/bison + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-native + + - name: Show native compiler cache stats + if: ${{ always() && runner.os != 'Windows' }} run: | - cargo run -p xtask -- assets download \ - --latest-compatible \ - --target-triple "$AOT_TARGET" + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats + else + echo "ccache was not installed before the build stopped" + fi + + - name: Upload native exact-extension artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-extension-artifacts-${{ matrix.target }} + path: target/extensions/native/release-assets + if-no-files-found: error + + - name: Upload native exact-extension build logs + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-extension-logs-${{ matrix.target }} + path: | + target/liboliphaunt-mobile-extension-ci/**/*.log + target/liboliphaunt-mobile-extension-release/**/*.log + target/liboliphaunt-mobile-extension-release/ios-xcframework/ios-simulator/*.log + target/liboliphaunt-mobile-extension-release/ios-xcframework/ios-device/*.log + target/liboliphaunt-pg18-extension-release/**/*.log + target/liboliphaunt-pg18-*-extension-release/**/*.log + /tmp/liboliphaunt-ci-*-extensions.log + /tmp/liboliphaunt-release-*-extensions.log + /tmp/liboliphaunt-ci-extension-assets-fetch.log + /tmp/liboliphaunt-release-extension-assets-fetch.log + if-no-files-found: ignore + + extension-artifacts-wasix: + name: Builds / extension-wasix (${{ matrix.target }}) + needs: + - affected + - liboliphaunt-wasix-runtime + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'extension-artifacts-wasix') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.extension_artifacts_wasix_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download portable WASIX runtime outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-runtime-portable + path: . - - name: Check target AOT crate + - name: Build WASIX exact-extension artifacts env: - AOT_PACKAGE: ${{ matrix.package }} - run: cargo check -p "$AOT_PACKAGE" --locked + OLIPHAUNT_EXTENSION_PRODUCTS: ${{ matrix.extensions_csv }} + OLIPHAUNT_EXTENSION_TARGET: ${{ matrix.target }} + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-wasix - - name: Run runtime tests against target AOT - run: scripts/validate.sh runtime-smoke + - name: Upload WASIX exact-extension artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-wasix-extension-artifacts-${{ matrix.target }} + path: target/extensions/wasix/release-assets + if-no-files-found: error - asset-status: - name: Wait for same-SHA Assets - needs: scope - if: ${{ needs.scope.outputs.assets == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 360 - permissions: - actions: read - contents: read + extension-packages: + name: Builds / extension-packages + needs: + - affected + - extension-artifacts-native + - extension-artifacts-wasix + - liboliphaunt-wasix-aot + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'extension-packages') }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Wait for successful same-SHA Assets workflow + - name: Download native exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-native-extension-artifacts-* + path: target/extensions/native/release-assets + merge-multiple: true + + - name: Download WASIX exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-wasix-extension-artifacts-* + 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: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - ASSET_SHA: ${{ github.event.pull_request.head.sha || github.sha }} - run: | - args=() - while IFS= read -r artifact; do - args+=(--artifact "$artifact") - done < <(cargo run --quiet -p xtask -- assets ci-artifacts) - bash .github/scripts/require-workflow-success.sh Assets "$ASSET_SHA" 21000 "${args[@]}" + OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS: ${{ needs.affected.outputs.extension_package_products_csv }} + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-packages - examples: - name: Examples - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.examples == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 45 + - name: Upload exact-extension package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-extension-package-artifacts + path: target/extension-artifacts + if-no-files-found: error + + mobile-extension-packages: + name: Builds / mobile-extension-packages + needs: + - affected + - extension-artifacts-native + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.extension-artifacts-native.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'mobile-extension-packages') }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Install Tauri Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y \ - libayatana-appindicator3-dev \ - libssl-dev \ - libwebkit2gtk-4.1-dev \ - librsvg2-dev \ - patchelf \ - pkg-config + - name: Set up Moon + uses: ./.github/actions/setup-moon - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - cache-workspaces: | - . -> target - examples/tauri-sqlx-vanilla/src-tauri -> target - - name: Install Node.js - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 + - name: Download native exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: - node-version: 22 - cache: npm - cache-dependency-path: examples/tauri-sqlx-vanilla/package-lock.json + pattern: liboliphaunt-native-extension-artifacts-* + path: target/extensions/native/release-assets + merge-multiple: true - - name: Validate examples - run: scripts/validate.sh examples + - name: Build mobile exact-extension package artifacts + env: + OLIPHAUNT_EXTENSION_PACKAGE_NATIVE_TARGETS: ${{ needs.affected.outputs.mobile_extension_package_native_targets_csv }} + OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS: ${{ needs.affected.outputs.extension_package_products_csv }} + run: 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-extension-packages - package: - name: Package checks - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.package == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 45 + - name: Upload mobile exact-extension package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-mobile-extension-package-artifacts + path: target/extension-artifacts + if-no-files-found: error + + liboliphaunt-native-desktop: + name: Builds / native-runtime-desktop (${{ matrix.target }}) + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-native-desktop') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_native_desktop_runtime_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + env: + CCACHE_DIR: ${{ github.workspace }}/.ci-cache/ccache/native-runtime/${{ matrix.target }} + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_COMPRESS: "true" + OLIPHAUNT_CCACHE_ZERO_STATS: "1" steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Validate package checks - run: scripts/validate.sh package + - name: Set up MSVC + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-msvc - feature-powerset: - name: Feature powerset - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 45 + - name: Prepare native compiler cache paths + env: + NATIVE_BUILD_ROOT: ${{ matrix.build-root }} + run: | + mkdir -p "$NATIVE_BUILD_ROOT" + if [[ "$RUNNER_OS" != "Windows" ]]; then + mkdir -p "$CCACHE_DIR" + fi + + - name: Restore native compiler cache + if: ${{ runner.os != 'Windows' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: | + ${{ env.CCACHE_DIR }} + ${{ matrix.build-root }} + key: liboliphaunt-native-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/runtimes/liboliphaunt/native/**', 'tools/xtask/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + liboliphaunt-native-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Restore native Windows build cache + if: ${{ runner.os == 'Windows' }} + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ matrix.build-root }} + key: liboliphaunt-native-build-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/runtimes/liboliphaunt/native/**', 'tools/xtask/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + liboliphaunt-native-build-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Configure native compiler cache + run: .github/scripts/setup-native-build-tools.sh 2G + + - name: Build liboliphaunt native runtime + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-native-desktop + + - name: Package liboliphaunt macOS release asset + if: ${{ startsWith(matrix.target, 'macos-') }} + env: + OLIPHAUNT_RELEASE_FETCH_ASSETS: "0" + run: tools/release/package-liboliphaunt-macos-assets.sh + + - name: Package portable ICU data for exact consumers + if: ${{ matrix.target == 'macos-arm64' }} + env: + NATIVE_BUILD_ROOT: ${{ matrix.build-root }} + run: | + tools/release/package-liboliphaunt-icu-data.sh \ + "$NATIVE_BUILD_ROOT/icu/share/icu" \ + target/liboliphaunt/portable-icu-assets + + - name: Package liboliphaunt Linux release asset + if: ${{ startsWith(matrix.target, 'linux-') }} + env: + OLIPHAUNT_RELEASE_FETCH_ASSETS: "0" + run: tools/release/package-liboliphaunt-linux-assets.sh + + - name: Package liboliphaunt Windows release asset + if: ${{ startsWith(matrix.target, 'windows-') }} + shell: pwsh + env: + OLIPHAUNT_RELEASE_FETCH_ASSETS: "0" + run: ./tools/release/package-liboliphaunt-windows-assets.ps1 + + - name: Upload liboliphaunt release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-release-assets-${{ matrix.target }} + path: target/liboliphaunt/release-assets + if-no-files-found: error + + - name: Upload portable ICU data for exact consumers + if: ${{ matrix.target == 'macos-arm64' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-icu-data + path: target/liboliphaunt/portable-icu-assets + if-no-files-found: error + + - name: Show native compiler cache stats + if: ${{ always() }} + env: + NATIVE_TARGET: ${{ matrix.target }} + run: | + if [[ "$NATIVE_TARGET" != windows-* ]] && command -v ccache >/dev/null 2>&1; then + ccache --show-stats + fi + + - name: Upload liboliphaunt build logs + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-logs-${{ matrix.target }} + path: | + ${{ matrix.build-root }}/*.log + target/liboliphaunt/**/*.log + if-no-files-found: ignore + + liboliphaunt-native-android: + name: Builds / native-runtime-android (${{ matrix.target }}) + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-native-android') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_native_android_runtime_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Set up Android + uses: ./.github/actions/setup-android + with: + gradle-cache: "false" + native-ccache: "true" + + - name: Prepare native build cache path + env: + NATIVE_BUILD_ROOT: ${{ matrix.build-root }} + run: mkdir -p "$NATIVE_BUILD_ROOT" + + - name: Restore native compiler cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: ${{ matrix.build-root }} + key: liboliphaunt-native-ccache-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/runtimes/liboliphaunt/native/**', 'tools/xtask/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + liboliphaunt-native-ccache-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Configure native compiler cache + run: .github/scripts/setup-native-build-tools.sh 2G + + - name: Build liboliphaunt Android target + env: + OLIPHAUNT_CI_TARGET: ${{ matrix.target }} + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-native-android + + - name: Package liboliphaunt Android release asset + env: + OLIPHAUNT_CI_TARGET: ${{ matrix.target }} + run: tools/release/package-liboliphaunt-mobile-assets.sh "$OLIPHAUNT_CI_TARGET" + + - name: Upload liboliphaunt release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-release-assets-${{ matrix.target }} + path: target/liboliphaunt/release-assets + if-no-files-found: error + + - name: Upload liboliphaunt Android target artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-target-${{ matrix.target }} + path: ${{ matrix.ci-artifact-root }} + if-no-files-found: error + + - name: Show native compiler cache stats + if: ${{ always() }} + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats + fi + + - name: Upload liboliphaunt Android build logs + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-logs-${{ matrix.target }} + path: | + ${{ matrix.build-root }}/*.log + target/liboliphaunt/**/*.log + if-no-files-found: ignore + + liboliphaunt-native-ios: + name: Builds / native-runtime-ios (${{ matrix.target }}) + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-native-ios') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_native_ios_runtime_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + env: + CCACHE_DIR: ${{ github.workspace }}/.ci-cache/ccache/native-runtime/${{ matrix.target }} + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + CCACHE_COMPRESS: "true" + OLIPHAUNT_CCACHE_ZERO_STATS: "1" steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Apple + uses: ./.github/actions/setup-apple + + - name: Test iOS app artifact transport + id: ios_app_transport_test + run: node src/sdks/react-native/tools/ios-app-transport.test.mjs + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - tools: cargo-hack@${{ env.CARGO_HACK_VERSION }} - - name: Check feature combinations - run: scripts/validate.sh feature-powerset + - name: Prepare native compiler cache paths + env: + NATIVE_BUILD_ROOT: ${{ matrix.build-root }} + run: mkdir -p "$CCACHE_DIR" "$NATIVE_BUILD_ROOT" + + - name: Restore native compiler cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: | + ${{ env.CCACHE_DIR }} + ${{ matrix.build-root }} + key: liboliphaunt-native-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('src/postgres/versions/18/**', 'src/sources/third-party/shared/**', 'src/sources/third-party/native/**', 'src/shared/extension-runtime-contract/**', 'src/runtimes/liboliphaunt/native/**', 'tools/xtask/**', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml') }} + restore-keys: | + liboliphaunt-native-ccache-v2-${{ matrix.target }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Configure native compiler cache + run: .github/scripts/setup-native-build-tools.sh 2G - semver: - name: Public API compatibility - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.package == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 20 + - name: Build liboliphaunt iOS target + env: + OLIPHAUNT_CI_TARGET: ${{ matrix.target }} + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-native-ios + + - name: Package liboliphaunt iOS release asset + env: + OLIPHAUNT_CI_TARGET: ${{ matrix.target }} + run: tools/release/package-liboliphaunt-mobile-assets.sh "$OLIPHAUNT_CI_TARGET" + + - name: Upload liboliphaunt release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-release-assets-${{ matrix.target }} + path: target/liboliphaunt/release-assets + if-no-files-found: error + + - name: Upload liboliphaunt iOS target artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-target-${{ matrix.target }} + path: ${{ matrix.ci-artifact-root }} + if-no-files-found: error + + - name: Show native compiler cache stats + if: ${{ always() }} + run: | + if command -v ccache >/dev/null 2>&1; then + ccache --show-stats + fi + + - name: Upload liboliphaunt iOS build logs + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-logs-${{ matrix.target }} + path: | + ${{ matrix.build-root }}/*.log + ${{ matrix.build-root }}/logs/*.log + ${{ matrix.build-root }}/**/*.log + target/liboliphaunt-ios-simulator/*.log + target/liboliphaunt-ios-simulator/**/*.log + target/liboliphaunt-ios-device/*.log + target/liboliphaunt-ios-device/**/*.log + target/liboliphaunt/**/*.log + if-no-files-found: ignore + + liboliphaunt-native-release-assets: + name: Builds / native-runtime-release-assets + needs: + - affected + - liboliphaunt-native-android + - liboliphaunt-native-desktop + - liboliphaunt-native-ios + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-native-release-assets') }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust with: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - - name: Check semver compatibility - uses: obi1kenobi/cargo-semver-checks-action@6b69fcf40e9b5fb17adeb57e4b6ecd020649a239 + - name: Download liboliphaunt target release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-native-release-assets-* + path: target/liboliphaunt/release-assets + merge-multiple: true + + - name: Package aggregate liboliphaunt release assets + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-native-release-assets - supply-chain: - name: Supply chain - needs: scope - if: ${{ github.event_name == 'push' || needs.scope.outputs.rust == 'true' || needs.scope.outputs.ci == 'true' }} - runs-on: ubuntu-latest - timeout-minutes: 10 + - name: Upload aggregate liboliphaunt release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-native-release-assets + path: target/liboliphaunt/release-assets + if-no-files-found: error + + rust-sdk-package: + name: Builds / rust-sdk + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'rust-sdk-package') }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - name: Check dependency policy - uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb - required: - name: Required checks - if: always() - needs: - - scope - - repo-hygiene - - workflow-lint - - rust-lint - - rust-tests - - runtime-targets - - runtime-aot-tests - - asset-status - - examples - - package - - feature-powerset - - semver - - supply-chain - runs-on: ubuntu-latest - timeout-minutes: 5 + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + components: clippy,llvm-tools-preview + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + tools: cargo-nextest@0.9.137,cargo-llvm-cov@0.8.7 + + - name: Build Rust SDK package artifacts + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh rust-sdk-package + + - name: Upload Rust SDK package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-rust-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-rust + if-no-files-found: error + + - name: Build native extension lifecycle proof runner + run: cargo build --offline --locked --release -p oliphaunt-native-extension-proof + + - name: Stage native extension lifecycle proof runner + run: | + install -D -m 0755 \ + target/release/oliphaunt-native-extension-proof \ + target/native-extension-proof/oliphaunt-native-extension-proof + + - name: Upload native extension lifecycle proof runner + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-native-extension-proof-linux-x64-gnu + path: target/native-extension-proof/oliphaunt-native-extension-proof + if-no-files-found: error + + broker-runtime: + name: Builds / broker-runtime (${{ matrix.target }}) + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'broker-runtime') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.broker_runtime_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 steps: - - name: Fail if any required job failed - if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') - run: exit 1 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false - - name: All required jobs passed - run: echo "All required CI jobs passed or were intentionally skipped." + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Set up MSVC + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-msvc + + - name: Build broker runtime release asset + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh broker-runtime + + - name: Upload broker release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-broker-release-assets-${{ matrix.target }} + path: target/oliphaunt-broker/release-assets + if-no-files-found: error + + node-direct: + name: Builds / node-direct (${{ matrix.target }}) + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'node-direct') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.node_direct_runtime_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up exact Node and pnpm for Windows standard-user proof + id: windows_exact_node_pnpm + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-node-pnpm + + - name: Set up exact Bun for Windows standard-user proof + id: windows_exact_bun + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up exact npm runtime for Windows standard-user proof + id: windows_exact_npm + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Set up exact Deno for Windows standard-user proof + id: windows_exact_deno + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-deno + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Verify Windows package-manager shim argument transport + if: ${{ runner.os == 'Windows' }} + env: + OLIPHAUNT_WINDOWS_STANDARD_USER_EXACT_NPM_PROOF: "1" + run: tools/dev/bun.sh test tools/release/windows-exact-candidate-command.test.mjs + + - name: Verify Windows exact-consumer tools under a standard-user token early + if: ${{ runner.os == 'Windows' }} + env: + BUN_EXECUTION_ENVELOPE: ${{ steps.windows_exact_bun.outputs.execution-envelope }} + DENO_EXECUTION_ENVELOPE: ${{ steps.windows_exact_deno.outputs.execution-envelope }} + NPM_EXECUTION_ENVELOPE: ${{ steps.windows_exact_npm.outputs.execution-envelope }} + NODE_EXECUTION_ENVELOPE: ${{ steps.windows_exact_node_pnpm.outputs.node-execution-envelope }} + PNPM_EXECUTION_ENVELOPE: ${{ steps.windows_exact_node_pnpm.outputs.pnpm-execution-envelope }} + run: | + command -v cygpath >/dev/null 2>&1 + bun_path="$(cygpath -w "$(command -v bun)")" + launcher="$(cygpath -w tools/release/run-windows-standard-user-exact-candidate.ps1)" + repository_root="$(cygpath -w "$PWD")" + MSYS2_ARG_CONV_EXCL='*' pwsh.exe \ + -NoLogo \ + -NoProfile \ + -NonInteractive \ + -ExecutionPolicy Bypass \ + -File "$launcher" \ + -SelfTest \ + -RepositoryRoot "$repository_root" \ + -BunPath "$bun_path" \ + -BunEnvelope "$BUN_EXECUTION_ENVELOPE" \ + -DenoEnvelope "$DENO_EXECUTION_ENVELOPE" \ + -NpmEnvelope "$NPM_EXECUTION_ENVELOPE" \ + -NodeEnvelope "$NODE_EXECUTION_ENVELOPE" \ + -PnpmEnvelope "$PNPM_EXECUTION_ENVELOPE" + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Set up MSVC + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-msvc + + - name: Build Node direct release asset + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh node-direct + + - name: Upload Node direct release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-node-direct-release-assets-${{ matrix.target }} + path: target/oliphaunt-node-direct/release-assets + if-no-files-found: error + + - name: Upload Node direct optional npm package + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-node-direct-npm-package-${{ matrix.target }} + path: target/oliphaunt-node-direct/npm-packages/*.tgz + if-no-files-found: error + + swift-sdk-package: + name: Builds / swift-sdk + needs: + - affected + - liboliphaunt-native-ios + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'swift-sdk-package') }} + runs-on: macos-26 + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Apple + uses: ./.github/actions/setup-apple + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download Apple liboliphaunt release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-ios-xcframework + path: target/liboliphaunt/release-assets + + - name: Build Swift SDK package artifacts + env: + OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR: ${{ github.workspace }}/target/liboliphaunt/release-assets + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh swift-sdk-package + + - name: Run exact-candidate Swift package consumer + id: swift_exact_candidate_consumer + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_SWIFT_NATIVE_ASSET_DIR: ${{ github.workspace }}/target/liboliphaunt/release-assets + OLIPHAUNT_SWIFT_SDK_ARTIFACT_DIR: ${{ github.workspace }}/target/sdk-artifacts/oliphaunt-swift + run: | + mkdir -p target/exact-candidate-consumer/swift + src/sdks/swift/tools/check-release-consumer.sh 2>&1 \ + | tee target/exact-candidate-consumer/swift/proof.log + + - name: Write exact-candidate Swift consumer receipt + id: swift_exact_candidate_receipt + if: ${{ always() && steps.swift_exact_candidate_consumer.outcome != 'skipped' }} + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + PROOF_OUTCOME: ${{ steps.swift_exact_candidate_consumer.outcome }} + run: | + candidate_tree="$(git rev-parse 'HEAD^{tree}')" + tools/dev/bun.sh tools/release/write-sdk-exact-candidate-receipt.mjs \ + --sdk swift \ + --candidate-sha "$CI_HEAD_SHA" \ + --candidate-tree "$candidate_tree" \ + --workflow-sha "$GITHUB_SHA" \ + --proof-outcome "$PROOF_OUTCOME" \ + --log target/exact-candidate-consumer/swift/proof.log \ + --input native-assets=target/liboliphaunt/release-assets \ + --input swift-sdk=target/sdk-artifacts/oliphaunt-swift \ + --output target/exact-candidate-consumer/swift/receipt.json + + - name: Upload exact-candidate Swift consumer evidence + id: swift_exact_candidate_evidence + if: ${{ always() && steps.swift_exact_candidate_consumer.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-swift-exact-candidate-consumer-evidence + path: target/exact-candidate-consumer/swift + if-no-files-found: error + retention-days: 30 + + - name: Upload Swift SDK package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-swift-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-swift + if-no-files-found: error + + kotlin-sdk-package: + name: Builds / kotlin-sdk + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'kotlin-sdk-package') }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Android + uses: ./.github/actions/setup-android + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build Kotlin SDK package artifacts + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh kotlin-sdk-package + + - name: Upload Kotlin SDK package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-kotlin-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-kotlin + if-no-files-found: error + + react-native-sdk-package: + name: Builds / react-native-sdk + needs: + - affected + - liboliphaunt-native-ios + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'react-native-sdk-package') }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download Apple liboliphaunt release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-ios-xcframework + path: target/liboliphaunt/release-assets + + - name: Build React Native SDK package artifacts + env: + OLIPHAUNT_REACT_NATIVE_IOS_RELEASE_ASSET_DIR: ${{ github.workspace }}/target/liboliphaunt/release-assets + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh react-native-sdk-package + + - name: Upload React Native SDK package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-react-native-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-react-native + if-no-files-found: error + + js-sdk-package: + name: Builds / js-sdk + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'js-sdk-package') }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Deno + uses: ./.github/actions/setup-deno + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Build TypeScript SDK package artifacts + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh js-sdk-package + + - name: Upload JS SDK package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-js-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-js + if-no-files-found: error + + js-sdk-exact-candidate-consumer: + name: Builds / js-sdk-exact-candidate (${{ matrix.target }}) + needs: + - affected + - extension-artifacts-native + - js-sdk-package + - liboliphaunt-native-desktop + - liboliphaunt-native-ios + - broker-runtime + - node-direct + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.extension-artifacts-native.result == 'success' && needs.js-sdk-package.result == 'success' && needs.liboliphaunt-native-desktop.result == 'success' && needs.liboliphaunt-native-ios.result == 'success' && needs.broker-runtime.result == 'success' && needs.node-direct.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'js-sdk-exact-candidate-consumer') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.js_exact_candidate_consumer_matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Node and pnpm + id: exact_node_pnpm + uses: ./.github/actions/setup-node-pnpm + + - name: Set up Bun + id: exact_bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up exact npm runtime + id: exact_npm + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Set up Deno + id: exact_deno + uses: ./.github/actions/setup-deno + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Download same-run target native runtime and tools + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: ${{ matrix.native_artifact }} + path: target/js-exact-candidate-input/native + + - name: Download same-run portable ICU data + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-icu-data + path: target/js-exact-candidate-input/native + + - name: Download same-run Apple base carrier + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-ios-xcframework + path: target/js-exact-candidate-input/ios + + - name: Download same-run target broker + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: ${{ matrix.broker_artifact }} + path: target/js-exact-candidate-input/broker + + - name: Download same-run target Node direct package + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: ${{ matrix.node_artifact }} + path: target/js-exact-candidate-input/node + + - name: Download same-run target exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: ${{ matrix.extension_artifact }} + path: target/js-exact-candidate-input/extensions + + - name: Download same-run Apple exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-extension-artifacts-ios-xcframework + path: target/js-exact-candidate-input/ios-extensions + + - name: Download same-run TypeScript package candidates + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-js-sdk-package-artifacts + path: target/js-exact-candidate-input/js + + - name: Consume exact TypeScript candidate + id: js_exact_candidate_consumer + timeout-minutes: 70 + env: + CANDIDATE_OUTPUT_ROOT: target/js-exact-candidate-consumer/${{ matrix.target }} + CANDIDATE_TARGET: ${{ matrix.target }} + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + BUN_EXECUTION_ENVELOPE: ${{ steps.exact_bun.outputs.execution-envelope }} + DENO_EXECUTION_ENVELOPE: ${{ steps.exact_deno.outputs.execution-envelope }} + NPM_EXECUTION_ENVELOPE: ${{ steps.exact_npm.outputs.execution-envelope }} + NODE_EXECUTION_ENVELOPE: ${{ steps.exact_node_pnpm.outputs.node-execution-envelope }} + PNPM_EXECUTION_ENVELOPE: ${{ steps.exact_node_pnpm.outputs.pnpm-execution-envelope }} + run: | + if [[ "${RUNNER_OS:-}" == "Windows" ]]; then + command -v cygpath >/dev/null 2>&1 + bun_path="$(cygpath -w "$(command -v bun)")" + repository_root="$(cygpath -w "$PWD")" + output_root="$(cygpath -w "$CANDIDATE_OUTPUT_ROOT")" + launcher="$(cygpath -w tools/release/run-windows-standard-user-exact-candidate.ps1)" + MSYS2_ARG_CONV_EXCL='*' pwsh.exe \ + -NoLogo \ + -NoProfile \ + -NonInteractive \ + -ExecutionPolicy Bypass \ + -File "$launcher" \ + -RepositoryRoot "$repository_root" \ + -OutputRoot "$output_root" \ + -BunPath "$bun_path" \ + -BunEnvelope "$BUN_EXECUTION_ENVELOPE" \ + -DenoEnvelope "$DENO_EXECUTION_ENVELOPE" \ + -NpmEnvelope "$NPM_EXECUTION_ENVELOPE" \ + -NodeEnvelope "$NODE_EXECUTION_ENVELOPE" \ + -PnpmEnvelope "$PNPM_EXECUTION_ENVELOPE" \ + -CandidateSha "$CI_HEAD_SHA" \ + -Target "$CANDIDATE_TARGET" + else + bun tools/release/js-exact-candidate-consumer.mjs \ + --candidate-sha "$CI_HEAD_SHA" \ + --target "$CANDIDATE_TARGET" \ + --artifact-root target/js-exact-candidate-input/native \ + --artifact-root target/js-exact-candidate-input/broker \ + --artifact-root target/js-exact-candidate-input/node \ + --artifact-root target/js-exact-candidate-input/extensions \ + --ios-extension-artifact-root target/js-exact-candidate-input/ios-extensions \ + --artifact-root target/js-exact-candidate-input/js \ + --artifact-root target/js-exact-candidate-input/ios \ + --output-root "$CANDIDATE_OUTPUT_ROOT" + fi + + - name: Upload exact TypeScript candidate evidence + id: js_exact_candidate_evidence + if: ${{ always() && steps.js_exact_candidate_consumer.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-js-exact-candidate-consumer-${{ matrix.target }} + path: target/js-exact-candidate-consumer/${{ matrix.target }}/evidence + if-no-files-found: error + retention-days: 30 + + wasix-rust-package: + name: Builds / wasix-rust-binding + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'wasix-rust-package') }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + components: llvm-tools-preview + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + tools: cargo-nextest@0.9.137,cargo-llvm-cov@0.8.7 + + - name: Install Tauri Linux dependencies + run: | + .github/scripts/prepare-linux-apt.sh + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libayatana-appindicator3-dev \ + libglib2.0-dev \ + libgtk-3-dev \ + librsvg2-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + pkg-config + + - name: Build WASIX Rust binding package artifacts + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh wasix-rust-package + + - name: Upload WASIX Rust binding package artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-wasix-rust-package-artifacts + path: target/sdk-artifacts/oliphaunt-wasix-rust + if-no-files-found: error + + liboliphaunt-wasix-runtime: + name: Builds / liboliphaunt-wasix-runtime + needs: + - affected + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-runtime') }} + runs-on: ubuntu-24.04 + timeout-minutes: 360 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Verify source-controlled asset inputs + run: cargo run -p xtask -- assets verify-committed + + - name: Restore WASIX compilation cache + id: wasix-build-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: | + target/liboliphaunt-pg18/source + target/oliphaunt-wasix/wasix-build/source-cache + target/oliphaunt-wasix/wasix-build/work/docker-oliphaunt + target/oliphaunt-wasix/wasix-build/work/icu-native + target/oliphaunt-wasix/wasix-build/work/icu-wasix + target/oliphaunt-wasix/wasix-build/work/icu-wasix-build + target/oliphaunt-wasix/wasix-build/work/json-c-wasix + target/oliphaunt-wasix/wasix-build/work/json-c-wasix-build + target/oliphaunt-wasix/wasix-build/work/libiconv-wasix + target/oliphaunt-wasix/wasix-build/work/libiconv-wasix-build + target/oliphaunt-wasix/wasix-build/work/libxml2-wasix + target/oliphaunt-wasix/wasix-build/work/libxml2-wasix-build + target/oliphaunt-wasix/wasix-build/work/openssl-wasix + target/oliphaunt-wasix/wasix-build/work/openssl-wasix-build + target/oliphaunt-wasix/wasix-build/work/proj-wasix + target/oliphaunt-wasix/wasix-build/work/proj-wasix-build + target/oliphaunt-wasix/wasix-build/work/sqlite-wasix + target/oliphaunt-wasix/wasix-build/work/sqlite-wasix-build + target/oliphaunt-wasix/wasix-build/work/geos-wasix + target/oliphaunt-wasix/wasix-build/work/geos-wasix-build + target/oliphaunt-wasix/wasix-build/build + key: wasix-build-${{ runner.os }}-${{ env.ASSET_PROFILE }}-${{ env.WASMER_LLVM_VERSION }}-${{ env.WASMER_LLVM_LINUX_X64_BYTES }}-${{ env.WASMER_LLVM_LINUX_X64_SHA256 }}-${{ hashFiles('src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256') }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + + - name: Build WASIX builder image and save cache + if: ${{ env.RUST_CACHE_SAVE_IF == 'true' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + with: + context: src/runtimes/liboliphaunt/wasix/assets/build/docker + file: src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile + tags: oliphaunt-wasix-wasix-build:ci + load: true + cache-from: type=gha,scope=wasix-builder + cache-to: type=gha,mode=max,scope=wasix-builder,ignore-error=true + + - name: Build WASIX builder image + if: ${{ env.RUST_CACHE_SAVE_IF != 'true' }} + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f + with: + context: src/runtimes/liboliphaunt/wasix/assets/build/docker + file: src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile + tags: oliphaunt-wasix-wasix-build:ci + load: true + cache-from: type=gha,scope=wasix-builder + + - name: Install Wasmer LLVM 22.1 for WASIX template generation + uses: ./.github/actions/setup-wasmer-llvm + with: + url: ${{ env.WASMER_LLVM_LINUX_X64_URL }} + sha256: ${{ env.WASMER_LLVM_LINUX_X64_SHA256 }} + bytes: ${{ env.WASMER_LLVM_LINUX_X64_BYTES }} + version: ${{ env.WASMER_LLVM_VERSION }} + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build and validate portable WASIX runtime + env: + IMAGE: oliphaunt-wasix-wasix-build:ci + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-wasix-runtime + + - name: Save WASIX compilation cache + if: ${{ env.RUST_CACHE_SAVE_IF == 'true' && steps.wasix-build-cache.outputs.cache-hit != 'true' }} + continue-on-error: true + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 + with: + path: | + target/liboliphaunt-pg18/source + target/oliphaunt-wasix/wasix-build/source-cache + target/oliphaunt-wasix/wasix-build/work/docker-oliphaunt + target/oliphaunt-wasix/wasix-build/work/icu-native + target/oliphaunt-wasix/wasix-build/work/icu-wasix + target/oliphaunt-wasix/wasix-build/work/icu-wasix-build + target/oliphaunt-wasix/wasix-build/work/json-c-wasix + target/oliphaunt-wasix/wasix-build/work/json-c-wasix-build + target/oliphaunt-wasix/wasix-build/work/libiconv-wasix + target/oliphaunt-wasix/wasix-build/work/libiconv-wasix-build + target/oliphaunt-wasix/wasix-build/work/libxml2-wasix + target/oliphaunt-wasix/wasix-build/work/libxml2-wasix-build + target/oliphaunt-wasix/wasix-build/work/openssl-wasix + target/oliphaunt-wasix/wasix-build/work/openssl-wasix-build + target/oliphaunt-wasix/wasix-build/work/proj-wasix + target/oliphaunt-wasix/wasix-build/work/proj-wasix-build + target/oliphaunt-wasix/wasix-build/work/sqlite-wasix + target/oliphaunt-wasix/wasix-build/work/sqlite-wasix-build + target/oliphaunt-wasix/wasix-build/work/geos-wasix + target/oliphaunt-wasix/wasix-build/work/geos-wasix-build + target/oliphaunt-wasix/wasix-build/build + key: wasix-build-${{ runner.os }}-${{ env.ASSET_PROFILE }}-${{ env.WASMER_LLVM_VERSION }}-${{ env.WASMER_LLVM_LINUX_X64_BYTES }}-${{ env.WASMER_LLVM_LINUX_X64_SHA256 }}-${{ hashFiles('src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256') }} + + - name: Upload portable WASIX build outputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-wasix-runtime-portable + path: | + target/oliphaunt-wasix/wasix-build/build/** + target/oliphaunt-wasix/wasix-build/work/icu-wasix/share/icu/** + target/oliphaunt-wasix/assets/** + src/extensions/generated/** + src/runtimes/liboliphaunt/wasix/assets/generated/** + if-no-files-found: error + + liboliphaunt-wasix-aot: + name: Builds / liboliphaunt-wasix-aot (${{ matrix.target_id }}) + needs: + - affected + - liboliphaunt-wasix-runtime + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-aot') }} + runs-on: ${{ matrix.os }} + timeout-minutes: 180 + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_wasix_aot_runtime_matrix || '{"include":[]}') }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Set up MSVC + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-msvc + + - name: Download portable WASIX build outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-runtime-portable + path: . + + - name: Install Wasmer LLVM 22.1 for AOT generation + uses: ./.github/actions/setup-wasmer-llvm + with: + url: ${{ matrix.llvm_url }} + sha256: ${{ matrix.llvm_sha256 }} + bytes: ${{ matrix.llvm_bytes }} + version: ${{ env.WASMER_LLVM_VERSION }} + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Build, validate, and smoke target AOT artifacts + env: + AOT_TARGET: ${{ matrix.target }} + AOT_PACKAGE: ${{ matrix.package }} + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-wasix-aot + + - name: Stage target AOT artifact envelope + env: + AOT_TARGET: ${{ matrix.target }} + run: | + target="${AOT_TARGET:?AOT_TARGET is required}" + source="target/oliphaunt-wasix/aot/$target" + if [ ! -d "$source" ]; then + echo "missing AOT output directory: $source" >&2 + exit 1 + fi + upload="target/oliphaunt-wasix/aot-upload" + rm -rf "$upload" + mkdir -p "$upload/files" + cp -R "$source/." "$upload/files/" + printf '%s\n' "$target" >"$upload/target-triple.txt" + + - name: Upload target artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-wasix-runtime-aot-${{ matrix.target_id }} + path: | + 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: + - affected + - liboliphaunt-wasix-runtime + - liboliphaunt-wasix-aot + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-release-assets') && (github.event_name != 'workflow_dispatch' || inputs.wasm_target == 'all') }} + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download portable WASIX runtime outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-runtime-portable + path: . + + - name: Download WASIX AOT runtime outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-wasix-runtime-aot-* + path: target/oliphaunt-wasix/aot-downloads + + - name: Restore WASIX AOT target layout + run: | + shopt -s nullglob + for artifact_dir in target/oliphaunt-wasix/aot-downloads/liboliphaunt-wasix-runtime-aot-*; do + marker="$artifact_dir/target-triple.txt" + raw_target_dir="$artifact_dir/files" + if [ ! -f "$marker" ] || [ ! -d "$raw_target_dir" ]; then + echo "Invalid WASIX AOT artifact envelope in $artifact_dir" >&2 + exit 1 + fi + target="$(tr -d '\r\n' < "$marker")" + if [ -z "$target" ]; then + echo "Empty WASIX AOT target marker in $marker" >&2 + exit 1 + fi + destination="target/oliphaunt-wasix/aot/$target" + mkdir -p "$destination" + rsync -a "$raw_target_dir/" "$destination/" + done + + - name: Package WASIX release assets + run: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh liboliphaunt-wasix-release-assets + + - name: Upload WASIX release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-wasix-release-assets + path: target/oliphaunt-wasix/release-assets + if-no-files-found: error + + native-extension-lifecycle: + name: E2E / native extension lifecycle (shard ${{ matrix.shard }}) + needs: + - affected + - extension-artifacts-native + - liboliphaunt-native-desktop + - broker-runtime + - rust-sdk-package + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.extension-artifacts-native.result == 'success' && needs.liboliphaunt-native-desktop.result == 'success' && needs.broker-runtime.result == 'success' && needs.rust-sdk-package.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'native-extension-lifecycle') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.native_extension_lifecycle_matrix) }} + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Download same-run Linux native runtime + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-linux-x64-gnu + path: target/native-extension-lifecycle/input/runtime + + - name: Download same-run Linux exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-extension-artifacts-linux-x64-gnu + path: target/native-extension-lifecycle/input/extensions + + - name: Download same-run Linux broker + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-broker-release-assets-linux-x64-gnu + path: target/native-extension-lifecycle/input/broker + + - name: Download same-run native lifecycle proof runner + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-native-extension-proof-linux-x64-gnu + path: target/native-extension-lifecycle/input/proof-runner + + - name: Run planned native extension lifecycle proof + id: native_extension_lifecycle + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_NATIVE_EXTENSION_PROOF_SQL_NAMES: ${{ needs.affected.outputs.native_extension_lifecycle_sql_names_csv }} + SHARD_INDEX: ${{ matrix.shard }} + SHARD_COUNT: ${{ matrix.shard_count }} + run: | + mkdir -p target/native-extension-lifecycle/evidence + tools/release/run-native-extension-lifecycle-proof.sh 2>&1 \ + | tee "target/native-extension-lifecycle/evidence/job-shard-${SHARD_INDEX}.log" + + - name: Upload native extension lifecycle evidence + id: native_extension_lifecycle_evidence + if: ${{ always() && steps.native_extension_lifecycle.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: native-extension-lifecycle-evidence-${{ matrix.shard }} + path: target/native-extension-lifecycle/evidence + if-no-files-found: error + retention-days: 30 + + native-extension-lifecycle-aggregate: + name: E2E / native extension lifecycle (aggregate) + needs: + - affected + - native-extension-lifecycle + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.native-extension-lifecycle.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'native-extension-lifecycle') }} + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Download all same-run native lifecycle shard receipts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: native-extension-lifecycle-evidence-* + path: target/native-extension-lifecycle/aggregate/input + merge-multiple: false + + - name: Verify exact aggregate native extension lifecycle proof + id: native_extension_lifecycle_aggregate + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_NATIVE_EXTENSION_PROOF_SQL_NAMES: ${{ needs.affected.outputs.native_extension_lifecycle_sql_names_csv }} + OLIPHAUNT_NATIVE_EXTENSION_PROOF_SHARD_COUNT: ${{ needs.affected.outputs.native_extension_lifecycle_shard_count }} + run: | + actual_sha="$(git rev-parse HEAD)" + if [[ "$actual_sha" != "$CI_HEAD_SHA" ]]; then + echo "native lifecycle aggregate candidate mismatch: expected $CI_HEAD_SHA, got $actual_sha" >&2 + exit 1 + fi + candidate_tree="$(git rev-parse 'HEAD^{tree}')" + mkdir -p target/native-extension-lifecycle/aggregate/output + tools/dev/bun.sh tools/release/verify-native-extension-lifecycle-receipts.mjs \ + --receipts target/native-extension-lifecycle/aggregate/input \ + --candidate-sha "$CI_HEAD_SHA" \ + --candidate-tree "$candidate_tree" \ + --expected-extensions-csv "$OLIPHAUNT_NATIVE_EXTENSION_PROOF_SQL_NAMES" \ + --expected-shard-count "$OLIPHAUNT_NATIVE_EXTENSION_PROOF_SHARD_COUNT" \ + --output target/native-extension-lifecycle/aggregate/output/aggregate-receipt.json + + - name: Upload aggregate native extension lifecycle evidence + id: native_extension_lifecycle_aggregate_evidence + if: ${{ always() && steps.native_extension_lifecycle_aggregate.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: native-extension-lifecycle-evidence + path: target/native-extension-lifecycle/aggregate + if-no-files-found: error + retention-days: 30 + + rust-sdk-exact-candidate-consumer: + name: E2E / Rust exact-candidate consumer + needs: + - affected + - extension-artifacts-native + - liboliphaunt-native-desktop + - broker-runtime + - rust-sdk-package + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.extension-artifacts-native.result == 'success' && needs.liboliphaunt-native-desktop.result == 'success' && needs.broker-runtime.result == 'success' && needs.rust-sdk-package.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'rust-sdk-exact-candidate-consumer') }} + runs-on: ubuntu-24.04 + timeout-minutes: 90 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download same-run Rust SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-rust-sdk-package-artifacts + path: target/exact-candidate-consumer/rust/input/sdk + + - name: Download same-run Linux native runtime + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-linux-x64-gnu + path: target/exact-candidate-consumer/rust/input/native + + - name: Download same-run Linux broker + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-broker-release-assets-linux-x64-gnu + path: target/exact-candidate-consumer/rust/input/broker + + - name: Download same-run Linux exact-extension artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-extension-artifacts-linux-x64-gnu + path: target/exact-candidate-consumer/rust/input/extensions + + - name: Run exact-candidate Rust package consumer + id: rust_exact_candidate_consumer + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_RUST_BROKER_ASSET_DIR: ${{ github.workspace }}/target/exact-candidate-consumer/rust/input/broker + OLIPHAUNT_RUST_EXTENSION_ASSET_DIR: ${{ github.workspace }}/target/exact-candidate-consumer/rust/input/extensions + OLIPHAUNT_RUST_NATIVE_ASSET_DIR: ${{ github.workspace }}/target/exact-candidate-consumer/rust/input/native + OLIPHAUNT_RUST_SDK_ARTIFACT_DIR: ${{ github.workspace }}/target/exact-candidate-consumer/rust/input/sdk + run: | + mkdir -p target/exact-candidate-consumer/rust/evidence + src/sdks/rust/tools/check-release-consumer.sh 2>&1 \ + | tee target/exact-candidate-consumer/rust/evidence/proof.log + + - name: Write exact-candidate Rust consumer receipt + id: rust_exact_candidate_receipt + if: ${{ always() && steps.rust_exact_candidate_consumer.outcome != 'skipped' }} + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + PROOF_OUTCOME: ${{ steps.rust_exact_candidate_consumer.outcome }} + run: | + candidate_tree="$(git rev-parse 'HEAD^{tree}')" + tools/dev/bun.sh tools/release/write-sdk-exact-candidate-receipt.mjs \ + --sdk rust \ + --candidate-sha "$CI_HEAD_SHA" \ + --candidate-tree "$candidate_tree" \ + --workflow-sha "$GITHUB_SHA" \ + --proof-outcome "$PROOF_OUTCOME" \ + --log target/exact-candidate-consumer/rust/evidence/proof.log \ + --input broker=target/exact-candidate-consumer/rust/input/broker \ + --input extensions=target/exact-candidate-consumer/rust/input/extensions \ + --input native=target/exact-candidate-consumer/rust/input/native \ + --input rust-sdk=target/exact-candidate-consumer/rust/input/sdk \ + --output target/exact-candidate-consumer/rust/evidence/receipt.json + + - name: Upload exact-candidate Rust consumer evidence + id: rust_exact_candidate_evidence + if: ${{ always() && steps.rust_exact_candidate_consumer.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-rust-exact-candidate-consumer-evidence + path: target/exact-candidate-consumer/rust/evidence + if-no-files-found: error + retention-days: 30 + + wasix-rust-exact-candidate-consumer: + name: E2E / WASIX Rust exact-candidate consumer + needs: + - affected + - wasix-rust-package + - liboliphaunt-wasix-release-assets + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.wasix-rust-package.result == 'success' && needs.liboliphaunt-wasix-release-assets.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'wasix-rust-exact-candidate-consumer') }} + runs-on: ubuntu-24.04 + timeout-minutes: 120 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download same-run WASIX Rust SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-wasix-rust-package-artifacts + path: target/exact-candidate-consumer/wasix-rust/input/sdk + + - name: Download same-run WASIX runtime release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-release-assets + path: target/exact-candidate-consumer/wasix-rust/input/runtime + + - name: Run exact-candidate WASIX Rust package consumer + id: wasix_rust_exact_candidate_consumer + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + evidence=target/exact-candidate-consumer/wasix-rust/evidence + work=target/exact-candidate-consumer/wasix-rust/work + mkdir -p "$evidence" + tools/dev/bun.sh tools/release/wasix-rust-exact-candidate-consumer.mjs \ + --candidate-sha "$CI_HEAD_SHA" \ + --sdk-root target/exact-candidate-consumer/wasix-rust/input/sdk \ + --runtime-asset-root target/exact-candidate-consumer/wasix-rust/input/runtime \ + --output-root "$work" 2>&1 \ + | tee "$evidence/proof.log" + cp "$work/evidence/exact-candidate.json" "$evidence/exact-candidate.json" + cp "$work/evidence/local-registry-report.json" "$evidence/local-registry-report.json" + cp "$work/cargo-consumer/evidence/Cargo.lock" "$evidence/Cargo.lock" + cp "$work/cargo-consumer/evidence/exact-cargo-consumer.json" "$evidence/exact-cargo-consumer.json" + + - name: Write exact-candidate WASIX Rust consumer receipt + id: wasix_rust_exact_candidate_receipt + if: ${{ always() && steps.wasix_rust_exact_candidate_consumer.outcome != 'skipped' }} + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + PROOF_OUTCOME: ${{ steps.wasix_rust_exact_candidate_consumer.outcome }} + run: | + candidate_tree="$(git rev-parse 'HEAD^{tree}')" + tools/dev/bun.sh tools/release/write-sdk-exact-candidate-receipt.mjs \ + --sdk wasix-rust \ + --candidate-sha "$CI_HEAD_SHA" \ + --candidate-tree "$candidate_tree" \ + --workflow-sha "$GITHUB_SHA" \ + --proof-outcome "$PROOF_OUTCOME" \ + --log target/exact-candidate-consumer/wasix-rust/evidence/proof.log \ + --input wasix-runtime=target/exact-candidate-consumer/wasix-rust/input/runtime \ + --input wasix-sdk=target/exact-candidate-consumer/wasix-rust/input/sdk \ + --output target/exact-candidate-consumer/wasix-rust/evidence/receipt.json + + - name: Upload exact-candidate WASIX Rust consumer evidence + id: wasix_rust_exact_candidate_evidence + if: ${{ always() && steps.wasix_rust_exact_candidate_consumer.outcome != 'skipped' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-wasix-rust-exact-candidate-consumer-evidence + path: target/exact-candidate-consumer/wasix-rust/evidence + if-no-files-found: error + retention-days: 30 + + wasix-release-regression: + name: E2E / WASIX release regression + needs: + - affected + - extension-artifacts-wasix + - liboliphaunt-wasix-runtime + - liboliphaunt-wasix-aot + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.extension-artifacts-wasix.result == 'success' && needs.liboliphaunt-wasix-runtime.result == 'success' && needs.liboliphaunt-wasix-aot.result == 'success' && needs.affected.outputs.wasix_release_regression_required == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 240 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Node and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Warm pinned extension metadata formatter + run: | + for attempt in 1 2 3; do + if printf 'export const oliphauntFormatterProbe={ready:true};\n' \ + | pnpm --package=@biomejs/biome@2.4.16 dlx biome format \ + --stdin-file-path src/extensions/generated/sdk/react-native.ts \ + >/dev/null; then + break + fi + if (( attempt == 3 )); then + echo 'Pinned Biome formatter preflight failed after three attempts' >&2 + exit 1 + fi + sleep $((attempt * 5)) + done + printf 'export const oliphauntFormatterProbe={offline:true};\n' \ + | PNPM_CONFIG_OFFLINE=true \ + pnpm --package=@biomejs/biome@2.4.16 dlx biome format \ + --stdin-file-path src/extensions/generated/sdk/react-native.ts \ + >/dev/null + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + components: rustfmt + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download same-run portable WASIX outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-runtime-portable + path: . + + - name: Download same-run WASIX exact-extension outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-wasix-extension-artifacts-* + path: target/extensions/wasix/release-assets + merge-multiple: true + + - name: Download same-run Linux extension AOT outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-extension-aot-linux-x64-gnu + path: target/extensions/wasix/aot-artifacts + + - name: Download same-run Linux host AOT outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-wasix-runtime-aot-linux-x64-gnu + path: target/oliphaunt-wasix/host-aot-download + + - name: Restore Linux host AOT target layout + run: | + artifact_dir=target/oliphaunt-wasix/host-aot-download + marker="$artifact_dir/target-triple.txt" + raw_target_dir="$artifact_dir/files" + if [[ ! -f "$marker" || ! -d "$raw_target_dir" ]]; then + echo "invalid Linux host AOT artifact envelope in $artifact_dir" >&2 + exit 1 + fi + target="$(tr -d '\r\n' < "$marker")" + if [[ "$target" != x86_64-unknown-linux-gnu ]]; then + echo "Linux host AOT artifact targets $target, expected x86_64-unknown-linux-gnu" >&2 + exit 1 + fi + destination="target/oliphaunt-wasix/aot/$target" + mkdir -p "$destination" + rsync -a "$raw_target_dir/" "$destination/" + + - name: Stage exact-extension WASIX evidence inputs + run: tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs --all --require-wasix + + - name: Collect exact-candidate WASIX extension evidence + id: regression + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_WASIX_EXTENSION_ARTIFACT_ROOT: ${{ github.workspace }}/target/extension-artifacts + # The pinned Biome package was fetched and exercised above. Keep the + # evidence mutation independent of a second registry lookup. + PNPM_CONFIG_OFFLINE: "true" + run: | + actual_sha="$(git rev-parse HEAD)" + if [[ "$actual_sha" != "$CI_HEAD_SHA" ]]; then + echo "checked-out candidate $actual_sha does not match requested SHA $CI_HEAD_SHA" >&2 + exit 1 + fi + run_id="$(date -u +%Y-%m-%dT%H%M%SZ)-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${CI_HEAD_SHA}" + run_path="src/extensions/evidence/runs/$run_id.json" + echo "run_path=$run_path" >> "$GITHUB_OUTPUT" + mkdir -p target/qualification/wasix-release-regression + src/extensions/tools/collect-wasix-evidence.sh "$run_id" 2>&1 \ + | tee target/qualification/wasix-release-regression/runtime-smoke.log + + - name: Upload exact-candidate WASIX evidence + if: ${{ steps.regression.outcome == 'success' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: wasix-release-regression-evidence + path: | + ${{ steps.regression.outputs.run_path }} + src/extensions/generated/docs/extension-evidence.json + target/qualification/wasix-release-regression/runtime-smoke.log + if-no-files-found: error + overwrite: true + retention-days: 30 + + - name: Upload WASIX regression diagnostics + if: ${{ always() && steps.regression.outcome != 'success' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: wasix-release-regression-diagnostics + path: target/qualification/wasix-release-regression/runtime-smoke.log + if-no-files-found: ignore + overwrite: true + retention-days: 14 + + mobile-build-android: + name: Builds / mobile-android (${{ matrix.target }}) + needs: + - affected + - mobile-extension-packages + - liboliphaunt-native-android + - kotlin-sdk-package + - react-native-sdk-package + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.mobile-extension-packages.result == 'success' && needs.liboliphaunt-native-android.result == 'success' && needs.kotlin-sdk-package.result == 'success' && needs.react-native-sdk-package.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'mobile-build-android') }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.affected.outputs.react_native_android_mobile_app_matrix) }} + runs-on: ubuntu-24.04 + timeout-minutes: 180 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Android + uses: ./.github/actions/setup-android + with: + native-ccache: "true" + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Reclaim Android mobile build disk + run: bun .github/scripts/reclaim-android-mobile-build-disk.mjs + + - name: Download Android liboliphaunt target + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-target-${{ matrix.target }} + path: . + + - name: Download Kotlin SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-kotlin-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-kotlin + + - name: Download React Native SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-react-native-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-react-native + + - name: Download exact-extension package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-mobile-extension-package-artifacts + path: target/extension-artifacts + + - name: Build Android mobile app + env: + OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS: "0" + OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS: "1" + OLIPHAUNT_EXPO_SDK_ARTIFACT_ROOT: ${{ github.workspace }}/target/sdk-artifacts + OLIPHAUNT_EXPO_ANDROID_ABI: ${{ matrix.abi }} + OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release + OLIPHAUNT_EXPO_ANDROID_EXTENSIONS: ${{ needs.affected.outputs.extension_package_sql_names_csv }} + OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS: "1" + OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT: ${{ github.workspace }}/target/extension-artifacts + OLIPHAUNT_EXPO_ANDROID_OLIPHAUNT_SO: ${{ github.workspace }}/${{ matrix.build-root }}/out/liboliphaunt.so + OLIPHAUNT_EXPO_ANDROID_RUNTIME_DIR: ${{ github.workspace }}/target/liboliphaunt-pg18-linux-x64-gnu/install + OLIPHAUNT_EXPO_ANDROID_INITDB: ${{ github.workspace }}/target/liboliphaunt-pg18-linux-x64-gnu/install/bin/initdb + run: | + 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: 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() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-android-build-logs-${{ matrix.target }} + path: | + target/mobile/react-native/android-build/logs + target/oliphaunt-expo-android-*/logs + if-no-files-found: ignore + + - name: Upload Android mobile app + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-android-app-${{ matrix.target }} + path: target/mobile-build/react-native/android + if-no-files-found: error + + mobile-build-ios: + name: Builds / mobile-ios + needs: + - affected + - mobile-extension-packages + - liboliphaunt-native-ios + - react-native-sdk-package + - swift-sdk-package + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'mobile-build-ios') }} + runs-on: macos-26 + timeout-minutes: 180 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Moon + uses: ./.github/actions/setup-moon + with: + install-workspace: "true" + + - name: Set up Apple + uses: ./.github/actions/setup-apple + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} + + - name: Download iOS liboliphaunt target + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-target-ios-xcframework + path: . + + - name: Download iOS liboliphaunt release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: liboliphaunt-native-release-assets-ios-xcframework + path: target/liboliphaunt/release-assets + + - name: Download Swift SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-swift-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-swift + + - name: Download React Native SDK package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-react-native-sdk-package-artifacts + path: target/sdk-artifacts/oliphaunt-react-native + + - name: Download exact-extension package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: oliphaunt-mobile-extension-package-artifacts + path: target/extension-artifacts + + - name: Render exact-SHA cache-warm iOS carrier manifest + run: | + tools/dev/bun.sh tools/release/ios-carrier-manifest.mjs \ + --base-asset-dir target/liboliphaunt/release-assets \ + --extension-root target/extension-artifacts \ + --output target/release/ios-carriers/oliphaunt-react-native-ios-carriers.json \ + --local-urls + + - name: Qualify every exact iOS carrier + env: + IOS_CARRIER_MANIFEST: target/release/ios-carriers/oliphaunt-react-native-ios-carriers.json + IOS_CARRIER_STAGE: target/qualification/react-native-ios-all-carriers + IOS_CARRIER_CACHE: target/qualification/react-native-ios-carrier-cache + PLANNED_EXTENSION_SQL_NAMES: ${{ needs.affected.outputs.extension_package_sql_names_csv }} + run: | + extensions="$(tools/dev/bun.sh -e ' + const manifest = JSON.parse(await Bun.file(process.env.IOS_CARRIER_MANIFEST).text()); + const planned = String(process.env.PLANNED_EXTENSION_SQL_NAMES ?? "") + .split(",").filter(Boolean).sort(); + if (planned.length === 0) { + throw new Error("planner selected no exact iOS extension carriers"); + } + if (!Array.isArray(manifest.extensions) || manifest.extensions.length === 0) { + throw new Error("exact iOS carrier manifest contains no extensions"); + } + const names = manifest.extensions.map((row) => row.sqlName).sort(); + if (names.some((name) => typeof name !== "string") || new Set(names).size !== names.length) { + throw new Error("exact iOS carrier manifest has invalid or duplicate extension identities"); + } + if (JSON.stringify(names) !== JSON.stringify(planned)) { + throw new Error("exact iOS carrier manifest does not match the planner-selected extension set"); + } + process.stdout.write(names.join(",")); + ')" + node src/sdks/react-native/tools/stage-ios-app.mjs \ + --carrier "$IOS_CARRIER_MANIFEST" \ + --output-dir "$IOS_CARRIER_STAGE" \ + --extensions "$extensions" \ + --icu \ + --cache-dir "$IOS_CARRIER_CACHE" \ + --allow-file-urls + tools/dev/bun.sh -e ' + const manifest = JSON.parse(await Bun.file(process.env.IOS_CARRIER_MANIFEST).text()); + const selection = JSON.parse(await Bun.file(process.env.IOS_CARRIER_STAGE + "/selection.json").text()); + const expected = String(process.env.PLANNED_EXTENSION_SQL_NAMES ?? "") + .split(",").filter(Boolean).sort(); + const manifested = manifest.extensions.map((row) => row.sqlName).sort(); + const requested = [...selection.requestedExtensions].sort(); + const resolved = selection.extensions.map((row) => row.sqlName).sort(); + if (JSON.stringify(manifested) !== JSON.stringify(expected) || !selection.icu || JSON.stringify(requested) !== JSON.stringify(expected) || JSON.stringify(resolved) !== JSON.stringify(expected)) { + throw new Error("staged iOS carrier selection does not exactly cover every manifest extension plus ICU"); + } + ' + mkdir -p target/release/ios-carriers/qualification + cp "$IOS_CARRIER_STAGE/selection.json" \ + target/release/ios-carriers/qualification/all-extensions-selection.json + cp "$IOS_CARRIER_STAGE/resources/OliphauntReactNativeResources.bundle/oliphaunt/package-size.tsv" \ + target/release/ios-carriers/qualification/all-extensions-package-size.tsv + rm -rf "$IOS_CARRIER_STAGE" "$IOS_CARRIER_CACHE" + + - name: Run exact-extension Swift release consumer + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_SWIFT_NATIVE_ASSET_DIR: ${{ github.workspace }}/target/liboliphaunt/release-assets + OLIPHAUNT_SWIFT_SDK_ARTIFACT_DIR: ${{ github.workspace }}/target/sdk-artifacts/oliphaunt-swift + PLANNED_EXTENSION_PRODUCTS: ${{ needs.affected.outputs.extension_package_products_csv }} + run: | + source_carrier=target/sdk-artifacts/oliphaunt-swift/release-tree/src/sdks/swift/Carriers/oliphaunt-react-native-ios-carriers.json + react_native_source_carrier=target/sdk-artifacts/oliphaunt-react-native/ios-carriers/oliphaunt-react-native-ios-carriers.json + cache_warm_carrier=target/release/ios-carriers/oliphaunt-react-native-ios-carriers.json + if ! cmp -s "$source_carrier" "$react_native_source_carrier"; then + echo 'Swift and React Native package artifacts disagree on the selection-neutral source carrier.' >&2 + diff -u "$source_carrier" "$react_native_source_carrier" || true + exit 1 + fi + consumer_args=( + --source-carrier "$source_carrier" + --cache-warm-carrier "$cache_warm_carrier" + ) + evidence_dir=target/release/ios-carriers/qualification/swift-independent-carriers + mkdir -p "$evidence_dir" + cp "$source_carrier" "$evidence_dir/selection-neutral-source-carrier.json" + cp "$react_native_source_carrier" "$evidence_dir/react-native-selection-neutral-source-carrier.json" + + product_count=0 + while IFS= read -r product; do + [[ -n "$product" ]] || continue + if [[ ! "$product" =~ ^oliphaunt-extension-[A-Za-z0-9._-]+$ ]]; then + echo "Planner returned an invalid exact-extension product id: $product" >&2 + exit 1 + fi + release_assets="target/extension-artifacts/$product/release-assets" + product_carriers=() + while IFS= read -r carrier; do + product_carriers+=("$carrier") + done < <(find "$release_assets" -maxdepth 1 -type f -name '*-swift-extension-carrier.json' | LC_ALL=C sort) + if (( ${#product_carriers[@]} != 1 )); then + echo "Planner-selected $product must provide exactly one independent Swift carrier; found ${#product_carriers[@]}" >&2 + exit 1 + fi + consumer_args+=(--extension-carrier "${product_carriers[0]}") + cp "${product_carriers[0]}" "$evidence_dir/$(basename "${product_carriers[0]}")" + product_count=$((product_count + 1)) + done < <(printf '%s' "$PLANNED_EXTENSION_PRODUCTS" | tr ',' '\n') + if (( product_count == 0 )); then + echo 'Exact-extension Swift release qualification resolved no independent extension carriers.' >&2 + exit 1 + fi + + printf '%s\n' "${consumer_args[@]}" > "$evidence_dir/consumer-arguments.txt" + src/sdks/swift/tools/check-extension-release-consumer.sh "${consumer_args[@]}" + + - name: Build iOS mobile app + env: + OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS: "0" + OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS: "1" + OLIPHAUNT_EXPO_SDK_ARTIFACT_ROOT: ${{ github.workspace }}/target/sdk-artifacts + OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release + OLIPHAUNT_EXPO_IOS_EXTENSIONS: ${{ needs.affected.outputs.extension_package_sql_names_csv }} + OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator + OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS: "1" + OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT: ${{ github.workspace }}/target/extension-artifacts + OLIPHAUNT_EXPO_IOS_OLIPHAUNT_XCFRAMEWORK: ${{ github.workspace }}/target/liboliphaunt-ios-xcframework/out/liboliphaunt.xcframework + OLIPHAUNT_EXPO_IOS_RUNTIME_DIR: ${{ github.workspace }}/target/liboliphaunt-pg18/install + OLIPHAUNT_EXPO_IOS_INITDB: ${{ github.workspace }}/target/liboliphaunt-pg18/install/bin/initdb + OLIPHAUNT_REACT_NATIVE_IOS_BASE_CARRIER: ${{ github.workspace }}/target/release/ios-carriers/oliphaunt-react-native-ios-carriers.json + run: | + 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: tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions + + - name: Pack fidelity-preserving iOS app transport + run: | + node src/sdks/react-native/tools/ios-app-transport.mjs pack \ + --app-dir target/mobile-build/react-native/ios \ + --transport-dir target/mobile-build/react-native/ios-transport + + - name: Upload iOS mobile build logs + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-ios-build-logs + path: | + target/mobile/react-native/ios-build/logs + target/mobile/react-native/ios-build/xcodebuild.log + target/oliphaunt-expo-ios-*/logs + target/oliphaunt-expo-ios-*/xcodebuild.log + if-no-files-found: ignore + + - name: Upload iOS mobile app + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-ios-app + path: target/mobile-build/react-native/ios-transport + if-no-files-found: error + + - name: Upload exact-SHA iOS carrier evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-react-native-ios-carriers + path: target/release/ios-carriers + if-no-files-found: error + overwrite: true + + builds: + name: Builds + if: ${{ always() }} + needs: + - affected + - extension-artifacts-native + - extension-artifacts-wasix + - extension-packages + - mobile-extension-packages + - liboliphaunt-native-android + - liboliphaunt-native-desktop + - liboliphaunt-native-ios + - liboliphaunt-native-release-assets + - rust-sdk-package + - broker-runtime + - node-direct + - swift-sdk-package + - kotlin-sdk-package + - react-native-sdk-package + - js-sdk-package + - js-sdk-exact-candidate-consumer + - wasix-rust-package + - liboliphaunt-wasix-runtime + - liboliphaunt-wasix-aot + - liboliphaunt-wasix-release-assets + - wasix-release-regression + - mobile-build-android + - mobile-build-ios + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Check selected build jobs + id: selected_builds_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.affected.outputs.builder_jobs }} + GATE_LABEL: selected build jobs + run: bun .github/scripts/check-ci-gate.mjs selected + + - name: Check exact-candidate WASIX regression + id: wasix_regression_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.affected.outputs.wasix_release_regression_required == 'true' && '["wasix-release-regression"]' || '[]' }} + GATE_LABEL: exact-candidate WASIX regression + run: bun .github/scripts/check-ci-gate.mjs selected + + mobile-e2e-android: + name: E2E / mobile-android + needs: + - affected + - mobile-build-android + if: ${{ always() && !cancelled() && needs.affected.result == 'success' && needs.mobile-build-android.result == 'success' && contains(fromJson(needs.affected.outputs.jobs), 'mobile-build-android') }} + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Node and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Set up Android + uses: ./.github/actions/setup-android + + - name: Set up Maestro + uses: ./.github/actions/setup-maestro + + - name: Download Android app artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: react-native-mobile-android-app-android-x86_64 + path: target/mobile-build/react-native/android + + - name: List Android app artifact + run: find target/mobile-build/react-native/android -maxdepth 2 -type f -print + + - name: Start Android emulator + env: + OLIPHAUNT_ANDROID_EMULATOR_API: "35" + run: tools/dev/start-android-emulator-ci.sh + + - name: Run Android installed-app E2E + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_EXPO_ANDROID_BUILD_ARTIFACT_DIR: ${{ github.workspace }}/target/mobile-build/react-native/android + OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release + OLIPHAUNT_EXPO_ANDROID_LIFECYCLE_SMOKE: "0" + OLIPHAUNT_MOBILE_E2E_ASSERTION_RUNNER: maestro + run: bash src/sdks/react-native/tools/mobile-e2e.sh android + + - name: Upload Android E2E reports + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-android-e2e-reports + path: | + target/mobile/react-native/android-e2e/reports + target/mobile/react-native/android-e2e/logs + ${{ runner.temp }}/oliphaunt-android-emulator.log + if-no-files-found: ignore + + mobile-e2e-ios: + name: E2E / mobile-ios + needs: + - affected + - mobile-build-ios + if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'mobile-build-ios') }} + runs-on: macos-26 + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Node and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Set up Apple + uses: ./.github/actions/setup-apple + + - name: Set up Maestro + uses: ./.github/actions/setup-maestro + + - name: Download iOS app artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: react-native-mobile-ios-app + path: target/mobile-build/react-native/ios-transport + + - name: Verify and extract iOS app artifact + id: ios_app_transport_verify + run: | + rm -rf target/mobile-build/react-native/ios + node src/sdks/react-native/tools/ios-app-transport.mjs verify-extract \ + --transport-dir target/mobile-build/react-native/ios-transport \ + --output-dir target/mobile-build/react-native/ios + + - name: List iOS app artifact + run: find target/mobile-build/react-native/ios -maxdepth 2 -print + + - name: Run iOS installed-app E2E + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + OLIPHAUNT_EXPO_IOS_BUILD_ARTIFACT_DIR: ${{ github.workspace }}/target/mobile-build/react-native/ios + OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release + OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator + OLIPHAUNT_EXPO_IOS_LIFECYCLE_SMOKE: "0" + OLIPHAUNT_MOBILE_E2E_ASSERTION_RUNNER: maestro + MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000" + run: bash src/sdks/react-native/tools/mobile-e2e.sh ios + + - name: Upload iOS E2E reports + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-ios-e2e-reports + path: | + target/mobile/react-native/ios-e2e/reports + target/mobile/react-native/ios-e2e/logs + target/mobile/react-native/ios-e2e/*.log + if-no-files-found: ignore + + e2e: + name: E2E + if: ${{ always() }} + needs: + - affected + - native-extension-lifecycle-aggregate + - rust-sdk-exact-candidate-consumer + - wasix-rust-exact-candidate-consumer + - mobile-e2e-android + - mobile-e2e-ios + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Check E2E jobs + id: selected_e2e_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.affected.outputs.e2e_jobs }} + GATE_LABEL: E2E jobs + run: bun .github/scripts/check-ci-gate.mjs selected + + required: + name: Required + if: ${{ always() }} + needs: + - affected + - release-intent + - checks + - tests + - builds + - e2e + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Check required jobs + id: required_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + REQUIRED_JOBS_JSON: '["affected","release-intent","checks","tests","builds","e2e"]' + GATE_LABEL: required CI phases + run: bun .github/scripts/check-ci-gate.mjs required + + qualified: + name: Qualified + if: ${{ always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' }} + needs: + - affected + - required + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require full CI success + id: require_full_ci + env: + CI_EVENT_NAME: ${{ github.event_name }} + CI_MOBILE_TARGET: ${{ inputs.mobile_target }} + CI_NATIVE_TARGET: ${{ inputs.native_target }} + CI_WASM_TARGET: ${{ inputs.wasm_target }} + run: | + if [[ '${{ needs.affected.result }}' != success || '${{ needs.required.result }}' != success ]]; then + echo 'release qualification requires successful Plan and Required jobs' >&2 + exit 1 + fi + if [[ "$CI_EVENT_NAME" == workflow_dispatch ]] && \ + [[ "$CI_WASM_TARGET" != all || "$CI_NATIVE_TARGET" != all || "$CI_MOBILE_TARGET" != all ]]; then + echo 'focused workflow_dispatch runs are diagnostic only and cannot qualify a release candidate' >&2 + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Node.js + uses: ./.github/actions/setup-node-runtime + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Download same-run affected plan + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: artifact-build-plan + path: target/qualification/affected-plan + + - name: Download required same-run WASIX evidence + if: ${{ needs.affected.outputs.wasix_release_regression_required == 'true' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: wasix-release-regression-evidence + path: target/qualification/wasix-release-regression-evidence + + - name: Write exact-SHA qualification record + id: qualification_record + env: + CI_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CI_PLAN_PATH: target/qualification/affected-plan/ci-plan.json + WASIX_RELEASE_REGRESSION_REQUIRED: ${{ needs.affected.outputs.wasix_release_regression_required }} + WASIX_EVIDENCE_ROOT: target/qualification/wasix-release-regression-evidence + run: node .github/scripts/write-release-candidate.mjs + + - name: Upload exact-SHA qualification record + id: qualification_evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-release-candidate + path: target/qualification/oliphaunt-release-candidate.json + if-no-files-found: error + overwrite: true + retention-days: 14 diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml deleted file mode 100644 index d5d53f52..00000000 --- a/.github/workflows/conventional-commits.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Conventional Commits -run-name: Commit policy / ${{ github.event_name == 'pull_request' && format('PR {0}', github.event.pull_request.number) || github.ref_name }} - -on: - pull_request: - types: [opened, edited, synchronize, reopened, ready_for_review] - push: - branches: [main] - -permissions: - contents: read - pull-requests: read - -concurrency: - group: conventional-commits-${{ github.ref }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - PREK_VERSION: 0.3.10 - -jobs: - conventional: - name: Validate commit and PR title - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha || github.sha }} - persist-credentials: false - - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools - with: - tools: prek@${{ env.PREK_VERSION }} - - - name: Check PR title - if: github.event_name == 'pull_request' - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - printf '%s\n' "${PR_TITLE}" > "${RUNNER_TEMP}/pr-title.txt" - scripts/validate.sh commit-msg "${RUNNER_TEMP}/pr-title.txt" - - name: Check release intent for package changes - if: github.event_name == 'pull_request' - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_BRANCH: ${{ github.event.pull_request.head.ref }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_TITLE: ${{ github.event.pull_request.title }} - run: ./.github/scripts/check-release-intent.sh "$PR_TITLE" "$BASE_SHA" "$HEAD_SHA" "$HEAD_BRANCH" - - name: Check HEAD commit subject - run: | - git log -1 --pretty=%s > "${RUNNER_TEMP}/head-commit.txt" - scripts/validate.sh commit-msg "${RUNNER_TEMP}/head-commit.txt" diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml new file mode 100644 index 00000000..e06d31d3 --- /dev/null +++ b/.github/workflows/mobile-e2e.yml @@ -0,0 +1,261 @@ +name: E2E +run-name: E2E / ${{ inputs.sha || github.sha }} / ${{ inputs.platform || 'all' }} + +on: + workflow_call: + inputs: + sha: + description: Commit SHA whose qualified CI build artifacts should be tested. Defaults to the caller SHA. + required: false + type: string + platform: + description: Mobile platform to test. + required: false + default: all + type: string + workflow_dispatch: + inputs: + sha: + description: Commit SHA whose CI build artifacts should be tested. Defaults to the current ref SHA. + required: false + type: string + platform: + description: Mobile platform to test. + required: true + default: all + type: choice + options: + - all + - android + - ios + +permissions: + actions: read + contents: read + +concurrency: + group: mobile-e2e-${{ inputs.sha || github.sha }}-${{ inputs.platform || 'all' }} + cancel-in-progress: false + +env: + NODE_VERSION: 22.22.3 + PNPM_VERSION: 11.5.0 + BUN_VERSION: 1.3.14 + +jobs: + resolve: + name: resolve-build-artifacts + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + android: ${{ steps.plan.outputs.android }} + ios: ${{ steps.plan.outputs.ios }} + platform_jobs: ${{ steps.plan.outputs.platform_jobs }} + run_id: ${{ steps.plan.outputs.run_id }} + sha: ${{ steps.plan.outputs.sha }} + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Resolve mobile app artifacts + id: plan + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + INPUT_SHA: ${{ inputs.sha || '' }} + INPUT_PLATFORM: ${{ inputs.platform || 'all' }} + DEFAULT_SHA: ${{ github.sha }} + BUILD_GATE_JOB: Builds + run: bun .github/scripts/resolve-mobile-e2e.mjs + + android: + name: android-installed-app + needs: + - resolve + if: ${{ needs.resolve.outputs.android == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false + + - name: Set up Node and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Set up Android + uses: ./.github/actions/setup-android + + - name: Set up Maestro + uses: ./.github/actions/setup-maestro + + - name: Download Android app artifact + id: download_android_app + env: + CI_HEAD_SHA: ${{ needs.resolve.outputs.sha }} + CI_RUN_ID: ${{ needs.resolve.outputs.run_id }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + node .github/scripts/download-build-artifacts.mjs \ + CI \ + "$CI_HEAD_SHA" \ + target/mobile-build/react-native/android \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + --artifact react-native-mobile-android-app-android-x86_64 + find target/mobile-build/react-native/android -maxdepth 2 -type f -print + + - name: Start Android emulator + env: + OLIPHAUNT_ANDROID_EMULATOR_API: "35" + run: tools/dev/start-android-emulator-ci.sh + + - name: Run Android installed-app E2E + id: android_app_e2e + env: + CI_HEAD_SHA: ${{ needs.resolve.outputs.sha }} + OLIPHAUNT_EXPO_ANDROID_BUILD_ARTIFACT_DIR: ${{ github.workspace }}/target/mobile-build/react-native/android + OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release + OLIPHAUNT_EXPO_ANDROID_LIFECYCLE_SMOKE: "0" + OLIPHAUNT_MOBILE_E2E_ASSERTION_RUNNER: maestro + run: bash src/sdks/react-native/tools/mobile-e2e.sh android + + - name: Upload Android E2E reports + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-android-e2e-reports + path: | + target/mobile/react-native/android-e2e/reports + target/mobile/react-native/android-e2e/logs + ${{ runner.temp }}/oliphaunt-android-emulator.log + if-no-files-found: ignore + + ios: + name: ios-installed-app + needs: + - resolve + if: ${{ needs.resolve.outputs.ios == 'true' }} + runs-on: macos-26 + timeout-minutes: 45 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ needs.resolve.outputs.sha }} + persist-credentials: false + + - name: Set up Node and pnpm + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Set up Apple + uses: ./.github/actions/setup-apple + + - name: Set up Maestro + uses: ./.github/actions/setup-maestro + + - name: Download iOS app artifact + id: download_ios_app + env: + CI_HEAD_SHA: ${{ needs.resolve.outputs.sha }} + CI_RUN_ID: ${{ needs.resolve.outputs.run_id }} + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + node .github/scripts/download-build-artifacts.mjs \ + CI \ + "$CI_HEAD_SHA" \ + target/mobile-build/react-native/ios-transport \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + --artifact react-native-mobile-ios-app + + - name: Verify and extract iOS app artifact + id: verify_ios_app_transport + run: | + rm -rf target/mobile-build/react-native/ios + node src/sdks/react-native/tools/ios-app-transport.mjs verify-extract \ + --transport-dir target/mobile-build/react-native/ios-transport \ + --output-dir target/mobile-build/react-native/ios + find target/mobile-build/react-native/ios -maxdepth 2 -print + + - name: Run iOS installed-app E2E + id: ios_app_e2e + env: + CI_HEAD_SHA: ${{ needs.resolve.outputs.sha }} + OLIPHAUNT_EXPO_IOS_BUILD_ARTIFACT_DIR: ${{ github.workspace }}/target/mobile-build/react-native/ios + OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release + OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator + OLIPHAUNT_EXPO_IOS_LIFECYCLE_SMOKE: "0" + OLIPHAUNT_MOBILE_E2E_ASSERTION_RUNNER: maestro + MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000" + run: bash src/sdks/react-native/tools/mobile-e2e.sh ios + + - name: Upload iOS E2E reports + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: react-native-mobile-ios-e2e-reports + path: | + target/mobile/react-native/ios-e2e/reports + target/mobile/react-native/ios-e2e/logs + target/mobile/react-native/ios-e2e/*.log + if-no-files-found: ignore + + required: + name: E2E + if: ${{ always() }} + needs: + - resolve + - android + - ios + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 1 + ref: ${{ inputs.sha || github.sha }} + persist-credentials: false + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Check resolver + id: resolver_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + REQUIRED_JOBS_JSON: '["resolve"]' + GATE_LABEL: E2E resolver + run: bun .github/scripts/check-ci-gate.mjs required + + - name: Check selected E2E platform jobs + id: selected_platforms_gate + env: + NEEDS_JSON: ${{ toJson(needs) }} + SELECTED_JOBS_JSON: ${{ needs.resolve.outputs.platform_jobs }} + GATE_LABEL: selected E2E platform jobs + run: bun .github/scripts/check-ci-gate.mjs selected diff --git a/.github/workflows/release-execute.yml b/.github/workflows/release-execute.yml new file mode 100644 index 00000000..2b973d61 --- /dev/null +++ b/.github/workflows/release-execute.yml @@ -0,0 +1,2720 @@ +name: Release execution +run-name: Release execution / ${{ inputs.operation }} / ${{ github.ref_name }} + +on: + workflow_call: + inputs: + operation: + required: true + type: string + release_commit: + required: false + type: string + default: "" + continuation_pointer: + required: false + type: string + default: "" + outputs: + continuation_required: + description: Whether a verified immutable continuation artifact requires a child dispatch + value: ${{ jobs.publish-bootstrap.outputs.continuation_required || jobs.publish-registry.outputs.continuation_required }} + continuation_artifact_id: + description: Immutable parent continuation artifact ID + value: ${{ jobs.publish-bootstrap.outputs.continuation_artifact_id || jobs.publish-registry.outputs.continuation_artifact_id }} + continuation_artifact_digest: + description: Immutable parent continuation artifact digest + value: ${{ jobs.publish-bootstrap.outputs.continuation_artifact_digest || jobs.publish-registry.outputs.continuation_artifact_digest }} + continuation_contract_digest: + description: Canonical continuation contract digest + value: ${{ jobs.publish-bootstrap.outputs.continuation_contract_digest || jobs.publish-registry.outputs.continuation_contract_digest }} + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + CANONICAL_RELEASE_REPOSITORY: f0rr0/oliphaunt + NODE_VERSION: 22.22.3 + NPM_VERSION: 11.18.0 + PNPM_VERSION: 11.5.0 + DENO_VERSION: v2.8.1 + BUN_VERSION: 1.3.14 + PUBLICATION_LOCK_PATH: target/release/publication-lock.json + BOOTSTRAP_LEDGER_PATH: target/release/bootstrap-ledger + # Bootstrap retains its independent six-hour recovery envelope. Normal + # publication is split into three sequential jobs, each with a fresh hard + # deadline and an explicit runner/action cleanup reserve. + RELEASE_JOB_HARD_WINDOW_SECONDS: 21180 + REGISTRY_MUTATION_WINDOW_SECONDS: 19800 + GITHUB_STAGE_JOB_HARD_WINDOW_SECONDS: 21000 + REGISTRY_JOB_HARD_WINDOW_SECONDS: 21000 + FINALIZE_JOB_HARD_WINDOW_SECONDS: 6840 + NORMAL_REGISTRY_MUTATION_WINDOW_SECONDS: 11400 + REGISTRY_EVIDENCE_HANDOFF_RESERVE_SECONDS: 900 + # Finalization has its own two-hour job. Its entry gate requires 48 intact + # minutes: every finalization step's 45-minute hard bound plus a separate + # three-minute runner/action transition margin. + RELEASE_FINALIZATION_RESERVE_SECONDS: 3000 + RELEASE_MINIMUM_FINALIZATION_SECONDS: 2880 + RELEASE_FINALIZATION_CLEANUP_MARGIN_SECONDS: 180 + # Public consumer lanes run concurrently under one 13-minute internal + # deadline and stop with ten minutes preserved for evidence, lock proof, and + # draft promotion. The enclosing workflow step has a 15-minute hard bound. + PUBLIC_CONSUMER_SMOKE_TIMEOUT_SECONDS: 780 + PUBLIC_CONSUMER_FINALIZATION_RESERVE_SECONDS: 600 + MAVEN_CENTRAL_NAMESPACE: dev.oliphaunt + +defaults: + run: + shell: bash + +jobs: + release-identity: + name: Validate release identity + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require canonical release repository + run: | + if [[ "${GITHUB_REPOSITORY}" != "${CANONICAL_RELEASE_REPOSITORY}" ]]; then + echo "Release workflow is pinned to ${CANONICAL_RELEASE_REPOSITORY}; got ${GITHUB_REPOSITORY}" >&2 + exit 1 + fi + + - name: Checkout exact workflow commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Validate release workflow inputs + id: validate_release_inputs + env: + RELEASE_OPERATION: ${{ inputs.operation }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + run: bash .github/scripts/validate-release-workflow-inputs.sh + + prepare-release-pr: + name: Prepare release PR + needs: release-identity + runs-on: ubuntu-24.04 + timeout-minutes: 20 + if: ${{ inputs.operation == 'prepare-release-pr' }} + environment: release-pr + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Require current main + id: require_current_main + timeout-minutes: 1 + run: bash .github/scripts/require-current-main.sh "$GITHUB_SHA" + + - name: Set up Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: "true" + + - name: Validate release metadata + run: | + tools/dev/bun.sh tools/release/release-check.mjs + + - name: Require release PR token + env: + RELEASE_PR_TOKEN: ${{ secrets.RELEASE_PR_TOKEN }} + run: | + if [[ -z "${RELEASE_PR_TOKEN}" ]]; then + echo "RELEASE_PR_TOKEN is required so generated release PRs trigger normal PR CI." >&2 + echo "Configure a GitHub App or maintainer bot token in the release-pr environment." >&2 + exit 1 + fi + + - name: Create or update release-please PR + id: release_please + uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 + with: + token: ${{ secrets.RELEASE_PR_TOKEN }} + target-branch: main + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + skip-github-release: true + + - name: Sync derived release PR files + id: sync_release_pr + env: + GH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN }} + RELEASE_PLEASE_PR: ${{ steps.release_please.outputs.pr }} + RELEASE_PLEASE_PRS: ${{ steps.release_please.outputs.prs }} + RELEASE_PLEASE_PRS_CREATED: ${{ steps.release_please.outputs.prs_created }} + run: | + set -euo pipefail + + release_pr_number="$(bun .github/scripts/resolve-release-please-pr.mjs)" + if [[ -z "${release_pr_number}" ]]; then + release_pr_number="$( + gh pr list \ + --base main \ + --head release-please--branches--main \ + --state open \ + --json number \ + --jq '.[0].number // empty' + )" + fi + if [[ -z "${release_pr_number}" ]]; then + if [[ "${RELEASE_PLEASE_PRS_CREATED}" == "true" ]]; then + echo "release-please reported PR changes, but no release PR number could be resolved." >&2 + exit 1 + fi + echo "No open release-please PR found; derived release file sync skipped." + exit 0 + fi + + IFS=$'\t' read -r \ + release_pr_observed_number \ + release_pr_base \ + release_pr_head \ + release_pr_old_sha \ + release_pr_head_repository \ + release_pr_is_cross_repository \ + release_pr_state \ + release_pr_title \ + <<<"$( + gh pr view "${release_pr_number}" \ + --json number,baseRefName,headRefName,headRefOid,headRepository,isCrossRepository,state,title \ + --jq '[.number, .baseRefName, .headRefName, .headRefOid, .headRepository.nameWithOwner, (.isCrossRepository | tostring), .state, .title] | @tsv' + )" + + gh auth setup-git + bash .github/scripts/require-current-main.sh "$GITHUB_SHA" + + release_pr_identity_args=( + --pr-number "${release_pr_number}" + --observed-pr-number "${release_pr_observed_number}" + --base "${release_pr_base}" + --head "${release_pr_head}" + --head-sha "${release_pr_old_sha}" + --head-repository "${release_pr_head_repository}" + --cross-repository "${release_pr_is_cross_repository}" + --state "${release_pr_state}" + --title "${release_pr_title}" + --main-sha "$GITHUB_SHA" + ) + bun .github/scripts/normalize-release-please-pr.mjs \ + normalize \ + "${release_pr_identity_args[@]}" + + tools/dev/bun.sh tools/release/sync-release-pr.mjs + tools/dev/bun.sh tools/release/sync-release-pr.mjs --check + + if [[ -n "$(git ls-files --others --exclude-standard)" ]]; then + echo "Derived release synchronization created untracked files; refusing an incomplete amend." >&2 + git status --short >&2 + exit 1 + fi + + git config user.name "oliphaunt-release-bot" + git config user.email "oliphaunt-release-bot@users.noreply.github.com" + if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then + git add -u + git commit --amend --no-edit + else + echo "Derived release files already match the normalized Release Please tree." + fi + + release_products_json="$( + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --derive-products \ + --head-ref HEAD + )" + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --products-json "${release_products_json}" \ + --head-ref HEAD + tools/dev/bun.sh tools/release/release-metadata-check.mjs + bun .github/scripts/normalize-release-please-pr.mjs \ + push \ + "${release_pr_identity_args[@]}" + + - name: Report release-please PR result + env: + RELEASE_PLEASE_PRS_CREATED: ${{ steps.release_please.outputs.prs_created }} + run: | + if [[ "${RELEASE_PLEASE_PRS_CREATED}" == "true" ]]; then + echo "release-please created or updated a release PR." + else + echo "release-please found no releasable changes." + fi + + publish-dry-run: + name: Prepare release dry run + needs: + - release-identity + runs-on: macos-26 + timeout-minutes: 360 + if: ${{ inputs.operation == 'publish-dry-run' }} + environment: release-dry-run + permissions: + actions: read + contents: read + steps: &release_candidate_steps + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Record bounded GitHub staging job deadline + id: github_stage_job_deadline + if: ${{ inputs.operation == 'publish' }} + run: | + if [[ ! "$GITHUB_STAGE_JOB_HARD_WINDOW_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'GITHUB_STAGE_JOB_HARD_WINDOW_SECONDS must be a positive integer' >&2 + exit 1 + fi + hard_deadline=$(( $(date +%s) + GITHUB_STAGE_JOB_HARD_WINDOW_SECONDS )) + { + echo "GITHUB_STAGE_JOB_HARD_DEADLINE_EPOCH=$hard_deadline" + echo "OLIPHAUNT_GITHUB_CONTENT_WRITE_COLD_START_EPOCH=$(date +%s)" + echo "OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH=$RUNNER_TEMP/oliphaunt-github-content-write-pacer.json" + echo "OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH=$RUNNER_TEMP/oliphaunt-github-core-request-journal.json" + echo "OLIPHAUNT_REQUIRE_GITHUB_CORE_REQUEST_JOURNAL=true" + echo "OLIPHAUNT_RELEASE_ROOT_RUN_ID=$GITHUB_RUN_ID" + echo "OLIPHAUNT_GITHUB_RUN_SNAPSHOT_DIR=$RUNNER_TEMP/oliphaunt-github-run-snapshots" + } >> "$GITHUB_ENV" + echo "The GitHub staging job must seal its immutable handoff before Unix time $hard_deadline." + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Resolve release commit + id: release_head + timeout-minutes: 1 + env: + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit }} + run: .github/scripts/resolve-release-head.sh + + - name: Set up pinned Node.js + id: setup_github_stage_node + timeout-minutes: 3 + uses: ./.github/actions/setup-node-runtime + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Set up Moon + uses: ./.github/actions/setup-moon + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + cache-save-if: "true" + + - name: Configure macOS release toolchains + run: | + java_home_17="${JAVA_HOME_17_arm64:-${JAVA_HOME_17_X64:-}}" + if [[ -z "$java_home_17" || ! -x "$java_home_17/bin/java" ]]; then + echo 'macOS release runner does not expose a usable Java 17 toolchain' >&2 + exit 1 + fi + echo "JAVA_HOME=$java_home_17" >> "$GITHUB_ENV" + echo "$java_home_17/bin" >> "$GITHUB_PATH" + if [[ -z "${ANDROID_HOME:-}" && -d "$HOME/Library/Android/sdk" ]]; then + echo "ANDROID_HOME=$HOME/Library/Android/sdk" >> "$GITHUB_ENV" + echo "ANDROID_SDK_ROOT=$HOME/Library/Android/sdk" >> "$GITHUB_ENV" + fi + + - name: Validate release metadata + run: | + tools/dev/bun.sh tools/release/release-check.mjs + + - name: Plan product releases + id: release_plan + run: | + release_plan_args=( + --from-product-tags + --include-current-tags + --head-ref "$RELEASE_HEAD_SHA" + --format github-output + ) + tools/dev/bun.sh tools/release/release_plan.mjs "${release_plan_args[@]}" >> "$GITHUB_OUTPUT" + + - name: No package release planned + if: ${{ steps.release_plan.outputs.has_release_changes != 'true' }} + run: echo "No release-affecting product changes were found since the last product tag." + + - name: Resolve selected registry authentication needs + id: registry_needs + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: bun .github/scripts/selected-registry-needs.mjs + + - name: Verify reusable-workflow OIDC identity + id: verify_oidc_identity + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + RELEASE_OPERATION: ${{ inputs.operation }} + run: bun .github/scripts/verify-github-oidc-identity.mjs + + - name: Prove workflow HEAD is the release-bump commit + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Preflight selected product tag and release collisions + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + gh auth setup-git + tools/dev/bun.sh tools/release/verify_product_tags.mjs \ + --products-json "$PRODUCTS_JSON" \ + --target "$RELEASE_HEAD_SHA" \ + --allow-missing + bun .github/scripts/manage-release-drafts.mjs preflight \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Check publish environment + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + 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.mjs --products-json "${PRODUCTS_JSON}" + + - name: Verify external registry ownership and trust links + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: bun .github/scripts/verify-external-publish-readiness.mjs + + - name: Import, sign, and verify Maven credentials before mutation + id: verify_maven_signing + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.registry_needs.outputs.needs_maven == 'true' }} + timeout-minutes: 2 + env: + 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/dev/bun.sh tools/release/verify-maven-signing-readiness.mjs + + - name: Require qualified release-commit CI run + id: ci_qualification + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_OPERATION: ${{ inputs.operation }} + REQUIRES_WASIX_EVIDENCE: ${{ steps.release_plan.outputs.requires_wasix_release_regression_evidence }} + run: | + qualification_timeout=7200 + if [[ "$RELEASE_OPERATION" != publish-dry-run ]]; then + qualification_timeout=0 + fi + qualification_args=( + CI + "$RELEASE_HEAD_SHA" + "$qualification_timeout" + --event push + --event workflow_dispatch + --job Builds + --job Required + --job Qualified + --artifact artifact-build-plan + --artifact oliphaunt-release-candidate + ) + if [[ "$REQUIRES_WASIX_EVIDENCE" == true ]]; then + qualification_args+=(--artifact wasix-release-regression-evidence) + fi + if [[ "${{ steps.release_plan.outputs.has_extension_products }}" == true ]]; then + qualification_args+=(--artifact oliphaunt-extension-package-artifacts) + fi + bash .github/scripts/require-workflow-success.sh "${qualification_args[@]}" + + - name: Download exact-SHA qualification record + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/release-candidate \ + --run-id "$CI_RUN_ID" \ + --job Qualified \ + --artifact oliphaunt-release-candidate + + - name: Download exact-SHA affected plan + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/release-candidate/affected-plan \ + --run-id "$CI_RUN_ID" \ + --job Plan \ + --artifact artifact-build-plan + + - name: Download required exact-SHA WASIX evidence + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.requires_wasix_release_regression_evidence == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/release-candidate/wasix-evidence \ + --run-id "$CI_RUN_ID" \ + --job "E2E / WASIX release regression" \ + --artifact wasix-release-regression-evidence + + - name: Verify exact-SHA qualification record + id: verify_qualification + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + WASIX_EVIDENCE_REQUIRED: ${{ steps.release_plan.outputs.requires_wasix_release_regression_evidence }} + run: | + node .github/scripts/verify-release-candidate.mjs \ + target/release-candidate/oliphaunt-release-candidate.json \ + --plan target/release-candidate/affected-plan/ci-plan.json \ + --wasix-evidence-required "$WASIX_EVIDENCE_REQUIRED" \ + --wasix-evidence-root target/release-candidate/wasix-evidence + + - name: Require one approved dry-run lock and capsule + id: approved_publication_lock + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + bash .github/scripts/require-workflow-success.sh \ + Release \ + "$RELEASE_HEAD_SHA" \ + 300 \ + --event workflow_dispatch \ + --artifact oliphaunt-publication-lock \ + --artifact oliphaunt-bootstrap-capsule + + - name: Download prior approved publication lock + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_LOCK_RUN_ID: ${{ steps.approved_publication_lock.outputs.run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.approved_publication_lock.outputs.artifact_metadata_json }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + Release \ + "$RELEASE_HEAD_SHA" \ + target/approved-publication-lock \ + --run-id "$RELEASE_LOCK_RUN_ID" \ + --artifact-metadata-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --artifact oliphaunt-publication-lock + + - name: Validate product versions and registry state + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + 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' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + 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' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/oliphaunt-wasix/release-assets \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + --artifact liboliphaunt-wasix-release-assets + + - name: Download exact-extension package artifacts + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.has_extension_products == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/extension-artifacts \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + --artifact oliphaunt-extension-package-artifacts + + - name: Download SDK package artifacts + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + download_sdk_artifact() { + local product="$1" + local artifact_args=() + 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) + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + "target/sdk-artifacts/$product" \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + "${artifact_args[@]}" + } + while IFS= read -r product; do + download_sdk_artifact "$product" + 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' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/liboliphaunt/release-assets \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + --artifact liboliphaunt-native-release-assets + + - name: Set up Deno for TypeScript JSR consumer checks + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} + uses: ./.github/actions/setup-deno + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Set up Bun for TypeScript npm consumer checks + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Install TypeScript release tooling + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} + run: pnpm install --frozen-lockfile + + - name: Download native helper release assets + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && (steps.release_plan.outputs.product_oliphaunt_broker == 'true' || steps.release_plan.outputs.product_oliphaunt_node_direct == 'true') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRODUCT_OLIPHAUNT_BROKER: ${{ steps.release_plan.outputs.product_oliphaunt_broker }} + PRODUCT_OLIPHAUNT_NODE_DIRECT: ${{ steps.release_plan.outputs.product_oliphaunt_node_direct }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + download_helper_artifacts() { + local product="$1" + local kind="$2" + local destination="$3" + local artifact_args=() + 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) + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + "$destination" \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + "${artifact_args[@]}" + } + if [ "$PRODUCT_OLIPHAUNT_BROKER" = "true" ]; then + download_helper_artifacts \ + oliphaunt-broker \ + broker-helper \ + target/oliphaunt-broker/release-assets + fi + if [ "$PRODUCT_OLIPHAUNT_NODE_DIRECT" = "true" ]; then + download_helper_artifacts \ + oliphaunt-node-direct \ + node-direct-addon \ + target/oliphaunt-node-direct/release-assets + fi + + - name: Download Node direct optional npm packages + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + artifact_args=() + 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) + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + target/oliphaunt-node-direct/npm-packages \ + --run-id "$CI_RUN_ID" \ + --job Builds \ + "${artifact_args[@]}" + + - name: Freeze canonical Apple extension carrier input + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && (steps.release_plan.outputs.product_oliphaunt_swift == 'true' || steps.release_plan.outputs.product_oliphaunt_react_native == 'true') }} + env: + HAS_EXTENSION_PRODUCTS: ${{ steps.release_plan.outputs.has_extension_products }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + includes_swift="$(bun -e 'console.log(JSON.parse(process.argv[1]).includes("oliphaunt-swift"))' "$PRODUCTS_JSON")" + includes_react_native="$(bun -e 'console.log(JSON.parse(process.argv[1]).includes("oliphaunt-react-native"))' "$PRODUCTS_JSON")" + swift_source_carrier=target/sdk-artifacts/oliphaunt-swift/release-tree/src/sdks/swift/Carriers/oliphaunt-react-native-ios-carriers.json + react_native_source_carrier=target/sdk-artifacts/oliphaunt-react-native/ios-carriers/oliphaunt-react-native-ios-carriers.json + extension_manifest_args=() + extension_carrier_args=() + while IFS= read -r product; do + product_root="target/extension-artifacts/$product" + manifest="$product_root/extension-artifacts.json" + if [[ ! -f "$manifest" ]]; then + echo "Selected extension product $product is missing $manifest" >&2 + exit 1 + fi + extension_manifest_args+=(--extension-manifest "$manifest") + product_carriers=() + while IFS= read -r carrier; do + product_carriers+=("$carrier") + done < <(find "$product_root/release-assets" -maxdepth 1 -type f -name '*-swift-extension-carrier.json' | LC_ALL=C sort) + if (( ${#product_carriers[@]} != 1 )); then + echo "Selected extension product $product must provide exactly one independent Swift carrier; found ${#product_carriers[@]}" >&2 + exit 1 + fi + extension_carrier_args+=(--extension-carrier "${product_carriers[0]}") + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs \ + ci-products \ + --family extension-artifacts \ + --products-json "$PRODUCTS_JSON" \ + --format lines) + if [[ "$HAS_EXTENSION_PRODUCTS" == true && ${#extension_manifest_args[@]} == 0 ]]; then + echo 'The release plan selected extension products but resolved no selected extension artifacts.' >&2 + exit 1 + fi + + rm -rf target/release/ios-carriers target/release-work/ios-carriers + if [[ "$includes_swift" == true && "$includes_react_native" == true ]] && ! cmp -s "$swift_source_carrier" "$react_native_source_carrier"; then + echo 'Swift and React Native source carriers disagree for the same release plan.' >&2 + diff --unified "$swift_source_carrier" "$react_native_source_carrier" || true + exit 1 + fi + + if [[ "$includes_react_native" == true ]]; then + mkdir -p target/release/ios-carriers + public_args=( + --base-carrier "$react_native_source_carrier" + "${extension_manifest_args[@]}" + --output target/release/ios-carriers/oliphaunt-react-native-ios-carriers.json + ) + tools/dev/bun.sh tools/release/ios-carrier-manifest.mjs "${public_args[@]}" + fi + + if [[ "$includes_swift" == true && "$HAS_EXTENSION_PRODUCTS" == true ]]; then + mkdir -p target/release-work/ios-carriers + local_aggregate_carrier=target/release-work/ios-carriers/oliphaunt-react-native-ios-carriers.json + local_args=( + --base-carrier "$swift_source_carrier" + "${extension_manifest_args[@]}" + --output "$local_aggregate_carrier" + --local-urls + ) + tools/dev/bun.sh tools/release/ios-carrier-manifest.mjs "${local_args[@]}" + extensions_csv="$(bun -e ' + const manifest = JSON.parse(await Bun.file(process.argv[1]).text()); + console.log(manifest.extensions.map((row) => row.sqlName).sort().join(",")); + ' "$local_aggregate_carrier")" + if [[ -z "$extensions_csv" || ${#extension_carrier_args[@]} == 0 ]]; then + echo 'Swift extension validation requires selected independent extension carrier assets.' >&2 + exit 1 + fi + swift_version="$(tools/dev/bun.sh tools/release/product-version.mjs version oliphaunt-swift)" + cache=target/release-work/swiftpm-extension-cache + tools/dev/bun.sh src/sdks/swift/tools/render-extension-products.mjs \ + --carrier "$local_aggregate_carrier" \ + --extensions "$extensions_csv" \ + --cache-dir "$cache" \ + --allow-file-urls \ + --base-package-version "$swift_version" \ + --output-dir target/release-work/swiftpm-extension-cache-warm + tools/dev/bun.sh src/sdks/swift/tools/render-extension-products.mjs \ + --carrier "$swift_source_carrier" \ + "${extension_carrier_args[@]}" \ + --extensions "$extensions_csv" \ + --cache-dir "$cache" \ + --offline \ + --base-package-version "$swift_version" \ + --output-dir target/release/swiftpm-extension-consumer-fixture + fi + + - name: Set up pinned npm publisher for product dry-runs + id: setup_github_stage_npm + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.registry_needs.outputs.needs_npm == 'true' }} + timeout-minutes: 3 + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Validate selected release product dry-runs + id: validate_product_dry_runs + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + 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 + OLIPHAUNT_VERIFIED_NODE_EXECUTABLE: ${{ steps.setup_github_stage_npm.outputs.node-executable }} + OLIPHAUNT_VERIFIED_NPM_CLI: ${{ steps.setup_github_stage_npm.outputs.npm-cli }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + WASIX_EVIDENCE_REQUIRED: ${{ steps.release_plan.outputs.requires_wasix_release_regression_evidence }} + run: tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --qualified-ci --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + + - name: Assemble exact candidate Cargo registry + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.registry_needs.outputs.needs_cargo == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + candidate_registry_args=( + publish + --surface cargo + --strict + --exact-artifacts + --products-json "$PRODUCTS_JSON" + --registry-root target/release-work/candidate-registries + ) + for artifact_root in \ + target/release \ + target/sdk-artifacts \ + target/liboliphaunt/release-assets \ + target/liboliphaunt/cargo-artifacts \ + target/oliphaunt-wasix/release-assets \ + target/oliphaunt-wasix/cargo-artifacts \ + target/oliphaunt-broker/release-assets \ + target/oliphaunt-broker/cargo-artifacts \ + target/oliphaunt-node-direct/release-assets \ + target/oliphaunt-node-direct/npm-packages; do + if [[ -e "$artifact_root" ]]; then + candidate_registry_args+=(--artifact-root "$artifact_root") + fi + done + while IFS= read -r product; do + artifact_root="target/extension-artifacts/$product" + if [[ ! -d "$artifact_root" ]]; then + echo "Selected extension product $product is missing $artifact_root" >&2 + exit 1 + fi + candidate_registry_args+=(--artifact-root "$artifact_root") + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs \ + ci-products \ + --family extension-artifacts \ + --products-json "$PRODUCTS_JSON" \ + --format lines) + tools/dev/bun.sh tools/release/local-registry-publish.mjs "${candidate_registry_args[@]}" + + - name: Resolve Cargo examples against exact candidates + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.registry_needs.outputs.needs_cargo == 'true' }} + run: | + tools/dev/bun.sh tools/release/validate-example-cargo-candidates.mjs \ + --index target/release-work/candidate-registries/cargo/index \ + --output-root target/release-work/example-cargo-candidates + tools/dev/bun.sh tools/release/validate-example-cargo-candidates.mjs \ + --index target/release-work/candidate-registries/cargo/index \ + --output-root target/release-work/example-cargo-candidate-build \ + --policy wasix-electron-sidecar \ + --build + + - name: Freeze exhaustive publication lock + id: freeze_publication_lock + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + lock_args=( + create + --products-json "$PRODUCTS_JSON" + --head-ref "$RELEASE_HEAD_SHA" + --output target/release/publication-lock.json + ) + for artifact_root in \ + target/release \ + target/sdk-artifacts \ + target/liboliphaunt/release-assets \ + target/liboliphaunt/cargo-artifacts \ + target/oliphaunt-wasix/release-assets \ + target/oliphaunt-broker/release-assets \ + target/oliphaunt-broker/cargo-artifacts \ + target/oliphaunt-wasix/cargo-artifacts \ + target/oliphaunt-node-direct/release-assets \ + target/oliphaunt-node-direct/npm-packages; do + if [[ -e "$artifact_root" ]]; then + lock_args+=(--artifact-root "$artifact_root") + fi + done + while IFS= read -r product; do + artifact_root="target/extension-artifacts/$product" + if [[ ! -d "$artifact_root" ]]; then + echo "Selected extension product $product is missing $artifact_root" >&2 + exit 1 + fi + lock_args+=(--artifact-root "$artifact_root") + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs \ + ci-products \ + --family extension-artifacts \ + --products-json "$PRODUCTS_JSON" \ + --format lines) + tools/dev/bun.sh tools/release/publication-lock.mjs "${lock_args[@]}" + tools/dev/bun.sh tools/release/publication-lock.mjs \ + verify \ + --lock target/release/publication-lock.json \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Match prior approved publication lock + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + run: | + approved_lock=target/approved-publication-lock/publication-lock.json + if ! cmp -s "$approved_lock" "$PUBLICATION_LOCK_PATH"; then + echo 'publication lock differs from the prior approved dry-run lock' >&2 + diff --unified "$approved_lock" "$PUBLICATION_LOCK_PATH" || true + exit 1 + fi + echo 'publication lock exactly matches the prior approved dry-run lock' + + - name: Assemble and sign the exact Maven Central bundle before release mutation + id: preflight_maven_bundle + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.registry_needs.outputs.needs_maven == 'true' }} + timeout-minutes: 15 + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + 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/dev/bun.sh tools/release/preflight-maven-central-bundle.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --release-commit "$RELEASE_HEAD_SHA" + + - name: Prove the exact SwiftPM source tag is remotely collision-free + id: preflight_swift_source_tag + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_swift == 'true' }} + timeout-minutes: 2 + run: | + tools/dev/bun.sh tools/release/preflight-swiftpm-source-tag.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --release-commit "$RELEASE_HEAD_SHA" + + - name: Admit the complete paced GitHub release request envelope + id: github_request_budget + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/github-release-request-budget.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --github-output "$GITHUB_OUTPUT" + + - name: Preflight normal all-registry publication capacity + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + SIMULATED_REGISTRY_DEADLINE_EPOCH: ${{ steps.github_request_budget.outputs.simulated_registry_deadline_epoch }} + run: | + REGISTRY_MUTATION_DEADLINE_EPOCH="$SIMULATED_REGISTRY_DEADLINE_EPOCH" \ + bun .github/scripts/check-crates-io-publish-capacity.mjs + + - name: Prove exact-lock normal registry topology + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/normal-publication-plan.mjs \ + --lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + > target/release/normal-publication-plan.json + + - name: Audit exact cross-job registry input topology + id: audit_registry_input_transfer + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 5 + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.approved_publication_lock.outputs.run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.approved_publication_lock.outputs.artifact_metadata_json }} + run: | + tools/dev/bun.sh tools/release/release-phase-handoff.mjs audit \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --approved-run-id "$APPROVED_RUN_ID" \ + --approved-artifacts-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --github-output "$GITHUB_OUTPUT" + + - name: Prove bounded GitHub stage phase margin + id: github_stage_phase_budget + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 1 + env: + STAGE_OPERATION_SECONDS: ${{ steps.github_request_budget.outputs.pre_registry_operation_seconds }} + run: | + tools/dev/bun.sh tools/release/release-phase-budget.mjs \ + --phase github-staged \ + --stage-operation-seconds "$STAGE_OPERATION_SECONDS" \ + --github-output "$GITHUB_OUTPUT" + + - name: Classify pre-tag registry publication state + id: bootstrap_ledger_state + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && (steps.registry_needs.outputs.needs_cargo == 'true' || steps.registry_needs.outputs.needs_npm == 'true') }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: bun .github/scripts/registry-bootstrap-ledger-state.mjs + + - name: Download immutable registry bootstrap ledger + if: ${{ steps.bootstrap_ledger_state.outputs.needs_ledger == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + Release \ + "$RELEASE_HEAD_SHA" \ + "$BOOTSTRAP_LEDGER_PATH" \ + --artifact oliphaunt-bootstrap-ledger + + - name: Verify immutable bootstrap ledger and registry existence + if: ${{ steps.bootstrap_ledger_state.outputs.needs_ledger == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/bootstrap-ledger.mjs verify \ + --lock "$PUBLICATION_LOCK_PATH" \ + --ledger "$BOOTSTRAP_LEDGER_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --verify-registries + + - name: Freeze bootstrap publication capsule + id: freeze_bootstrap_capsule + if: ${{ inputs.operation == 'publish-dry-run' && steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/bootstrap-publication-capsule.mjs pack \ + --lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --output target/release/oliphaunt-bootstrap-capsule.tar + + - name: Upload frozen publication lock + id: preserve_publication_lock + if: ${{ inputs.operation == 'publish-dry-run' && steps.release_plan.outputs.has_release_changes == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-publication-lock + path: target/release/publication-lock.json + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Upload frozen bootstrap publication capsule + id: preserve_bootstrap_capsule + if: ${{ inputs.operation == 'publish-dry-run' && steps.release_plan.outputs.has_release_changes == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-bootstrap-capsule + path: target/release/oliphaunt-bootstrap-capsule.tar + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Upload publication lock audit evidence + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-publication-lock-${{ inputs.operation }} + path: target/release/publication-lock.json + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Re-admit the live GitHub request envelope immediately before mutation + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/github-release-request-budget.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --github-output "$RUNNER_TEMP/oliphaunt-final-github-admission.out" + + - name: Cool down and open the shared GitHub content-write journal + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + timeout-minutes: 65 + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve \ + --label "release mutation cold-start admission" + + - name: Revalidate current main immediately before release mutation + id: revalidate_release_mutation + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + timeout-minutes: 1 + run: bash .github/scripts/require-current-main.sh "$RELEASE_HEAD_SHA" + + - name: Stage exact-SHA product tags and draft releases + id: stage_github_releases + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + timeout-minutes: 31 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + bun .github/scripts/manage-release-drafts.mjs stage \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --state staged + + - name: Verify exact product tags + id: verify_product_tags + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: tools/dev/bun.sh tools/release/verify_product_tags.mjs --products-json "${PRODUCTS_JSON}" --target "$RELEASE_HEAD_SHA" + + - name: Verify exact-SHA GitHub release staging + id: verify_github_staging + if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: bun .github/scripts/manage-release-drafts.mjs verify --products-json "$PRODUCTS_JSON" --head-ref "$RELEASE_HEAD_SHA" --state staged + + - name: Publish all selected GitHub release asset sets concurrently + id: publish_github_assets + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 90 + env: + GITHUB_RELEASE_ASSET_UPLOAD_REPORT_PATH: ${{ runner.temp }}/oliphaunt-github-release-asset-upload-report.json + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --step github-release-assets --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" --publication-lock "$PUBLICATION_LOCK_PATH" + + - name: Resolve exact selected extension attestation subjects + id: extension_attestation_subjects + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} + env: + EXTENSION_PRODUCTS_JSON: ${{ steps.release_plan.outputs.extension_products_json }} + run: | + tools/dev/bun.sh tools/release/locked-attestation-subjects.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$EXTENSION_PRODUCTS_JSON" \ + --github-output "$GITHUB_OUTPUT" + + - name: Reserve extension attestation content write (shard 1) + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' && steps.extension_attestation_subjects.outputs.nonempty_1 == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "extension attestation shard 1" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "extension attestation shard 1 API attempt" + + - name: Attest selected extension release assets (shard 1) + id: attest_extensions_1 + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' && steps.extension_attestation_subjects.outputs.nonempty_1 == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: ${{ steps.extension_attestation_subjects.outputs.paths_1 }} + + - name: Reserve extension attestation content write (shard 2) + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' && steps.extension_attestation_subjects.outputs.nonempty_2 == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "extension attestation shard 2" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "extension attestation shard 2 API attempt" + + - name: Attest selected extension release assets (shard 2) + id: attest_extensions_2 + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' && steps.extension_attestation_subjects.outputs.nonempty_2 == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: ${{ steps.extension_attestation_subjects.outputs.paths_2 }} + + - name: Reserve liboliphaunt attestation content write + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "liboliphaunt native attestation" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "liboliphaunt native attestation API attempt" + + - name: Attest liboliphaunt release assets + id: attest_liboliphaunt_native + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: | + 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 + + - name: Publish Swift SDK GitHub release and SwiftPM tags + id: publish_swift_source_tag + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_swift == 'true' }} + timeout-minutes: 6 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-swift --step github-release --head-ref "$RELEASE_HEAD_SHA" --publication-lock "$PUBLICATION_LOCK_PATH" + + - name: Reserve broker attestation content write + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "broker attestation" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "broker attestation API attempt" + + - name: Attest broker release assets + id: attest_broker + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: | + target/oliphaunt-broker/release-assets/*.tar.gz + target/oliphaunt-broker/release-assets/*.zip + target/oliphaunt-broker/release-assets/*.sha256 + + - name: Reserve Node direct attestation content write + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "Node direct attestation" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "Node direct attestation API attempt" + + - name: Attest Node direct release assets + id: attest_node_direct + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: | + target/oliphaunt-node-direct/release-assets/*.tar.gz + target/oliphaunt-node-direct/release-assets/*.zip + target/oliphaunt-node-direct/release-assets/*.sha256 + + - name: Reserve WASIX attestation content write + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} + run: | + tools/dev/bun.sh tools/release/github-content-write-pacer.mjs reserve --label "WASIX attestation" + tools/dev/bun.sh tools/release/github-core-request-journal.mjs reserve --label "WASIX attestation API attempt" + + - name: Attest WASIX release assets + id: attest_wasix + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} + timeout-minutes: 5 + uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 + with: + subject-path: | + target/oliphaunt-wasix/release-assets/*.tar.zst + target/oliphaunt-wasix/release-assets/*.sha256 + + - name: Freeze exact GitHub release asset and attestation evidence + id: freeze_github_evidence + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 10 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + EXTENSIONS_ATTESTATION_BUNDLE_1: ${{ steps.attest_extensions_1.outputs.bundle-path }} + EXTENSIONS_ATTESTATION_BUNDLE_2: ${{ steps.attest_extensions_2.outputs.bundle-path }} + LIBOLIPHAUNT_NATIVE_ATTESTATION_BUNDLE: ${{ steps.attest_liboliphaunt_native.outputs.bundle-path }} + BROKER_ATTESTATION_BUNDLE: ${{ steps.attest_broker.outputs.bundle-path }} + NODE_DIRECT_ATTESTATION_BUNDLE: ${{ steps.attest_node_direct.outputs.bundle-path }} + WASIX_ATTESTATION_BUNDLE: ${{ steps.attest_wasix.outputs.bundle-path }} + run: | + bundle_args=() + for bundle in \ + "$EXTENSIONS_ATTESTATION_BUNDLE_1" \ + "$EXTENSIONS_ATTESTATION_BUNDLE_2" \ + "$LIBOLIPHAUNT_NATIVE_ATTESTATION_BUNDLE" \ + "$BROKER_ATTESTATION_BUNDLE" \ + "$NODE_DIRECT_ATTESTATION_BUNDLE" \ + "$WASIX_ATTESTATION_BUNDLE" + do + if [[ -n "$bundle" ]]; then + bundle_args+=(--attestation-bundle "$bundle") + fi + done + tools/dev/bun.sh tools/release/verify_github_release_attestations.mjs pre-mutation \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --output target/release/github-release-attestation-receipt.json \ + "${bundle_args[@]}" + + - name: Seal immutable GitHub-stage handoff + id: seal_github_stage_handoff + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 5 + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.approved_publication_lock.outputs.run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.approved_publication_lock.outputs.artifact_metadata_json }} + run: | + tools/dev/bun.sh tools/release/release-phase-handoff.mjs seal \ + --phase github-staged \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --runner-root "$RUNNER_TEMP" \ + --approved-run-id "$APPROVED_RUN_ID" \ + --approved-artifacts-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --output "$RUNNER_TEMP/github-stage-handoff" + + - name: Preserve immutable GitHub-stage handoff + id: preserve_github_stage_handoff + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} + timeout-minutes: 25 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: github-stage-handoff-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/github-stage-handoff + if-no-files-found: error + include-hidden-files: true + compression-level: 0 + retention-days: 90 + + - name: Preserve failed GitHub staging evidence + id: preserve_failed_github_stage_evidence + if: ${{ failure() && inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' }} + continue-on-error: true + timeout-minutes: 5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: github-staging-recovery-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + path: | + target/release/publication-lock.json + target/release/normal-publication-plan.json + target/release/github-release-attestation-receipt.json + target/release/bootstrap-ledger + ${{ runner.temp }}/github-stage-handoff + ${{ runner.temp }}/oliphaunt-github-content-write-pacer.json + ${{ runner.temp }}/oliphaunt-github-core-request-journal.json + ${{ runner.temp }}/oliphaunt-github-release-asset-upload-report.json + if-no-files-found: warn + include-hidden-files: true + retention-days: 90 + + publish: + name: Prepare and stage release + needs: + - release-identity + runs-on: macos-26 + timeout-minutes: 360 + if: ${{ inputs.operation == 'publish' && inputs.continuation_pointer == '' }} + environment: release-publish + permissions: + actions: read + attestations: write + contents: write + id-token: write + outputs: + has_release_changes: ${{ steps.release_plan.outputs.has_release_changes }} + products_json: ${{ steps.release_plan.outputs.products_json }} + release_head_sha: ${{ steps.release_head.outputs.sha }} + approved_run_id: ${{ steps.approved_publication_lock.outputs.run_id }} + approved_artifact_metadata_json: ${{ steps.approved_publication_lock.outputs.artifact_metadata_json }} + stage_handoff_artifact_id: ${{ steps.preserve_github_stage_handoff.outputs.artifact-id }} + stage_handoff_artifact_digest: ${{ steps.preserve_github_stage_handoff.outputs.artifact-digest }} + steps: *release_candidate_steps + + publish-registry: + name: Publish exact registry topology + needs: + - release-identity + - publish + runs-on: macos-26 + timeout-minutes: 360 + if: ${{ always() && inputs.operation == 'publish' && ((inputs.continuation_pointer == '' && needs.publish.result == 'success' && needs.publish.outputs.has_release_changes == 'true') || (inputs.continuation_pointer != '' && needs.release-identity.result == 'success')) }} + environment: release-publish + permissions: + actions: read + contents: read + id-token: write + outputs: + publication_complete: ${{ steps.require_registry_execution_decision.outputs.complete }} + release_head_sha: ${{ steps.registry_release_head.outputs.sha }} + products_json: ${{ steps.registry_inputs.outputs.products_json }} + approved_run_id: ${{ steps.registry_inputs.outputs.approved_run_id }} + approved_artifact_metadata_json: ${{ steps.registry_inputs.outputs.approved_artifact_metadata_json }} + root_run_id: ${{ steps.registry_inputs.outputs.root_run_id }} + registry_handoff_artifact_id: ${{ steps.preserve_publication_receipts.outputs.artifact-id }} + registry_handoff_artifact_digest: ${{ steps.preserve_publication_receipts.outputs.artifact-digest }} + continuation_required: ${{ steps.require_registry_execution_decision.outputs.deferred }} + continuation_artifact_id: ${{ steps.preserve_deferred_registry_recovery.outputs.artifact-id }} + continuation_artifact_digest: ${{ steps.preserve_deferred_registry_recovery.outputs.artifact-digest }} + continuation_contract_digest: ${{ steps.prepare_registry_continuation.outputs.contract_digest }} + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Record bounded registry job deadline + id: registry_job_deadline + run: | + if [[ ! "$REGISTRY_JOB_HARD_WINDOW_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'REGISTRY_JOB_HARD_WINDOW_SECONDS must be a positive integer' >&2 + exit 1 + fi + hard_deadline=$(( $(date +%s) + REGISTRY_JOB_HARD_WINDOW_SECONDS )) + { + echo "REGISTRY_JOB_HARD_DEADLINE_EPOCH=$hard_deadline" + echo "OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH=$RUNNER_TEMP/registry-input-download-core-journal.json" + echo "OLIPHAUNT_REQUIRE_GITHUB_CORE_REQUEST_JOURNAL=true" + echo "OLIPHAUNT_GITHUB_RUN_SNAPSHOT_DIR=$RUNNER_TEMP/oliphaunt-github-run-snapshots" + } >> "$GITHUB_ENV" + echo "Registry publication and immutable handoff must complete before Unix time $hard_deadline." + + - name: Checkout exact release commit + timeout-minutes: 5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ inputs.release_commit || needs.publish.outputs.release_head_sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Resolve exact release commit + id: registry_release_head + timeout-minutes: 1 + env: + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit || needs.publish.outputs.release_head_sha }} + run: .github/scripts/resolve-release-head.sh + + - name: Set up pinned Node.js + id: setup_registry_node + timeout-minutes: 3 + uses: ./.github/actions/setup-node-pnpm + with: + node-version: ${{ env.NODE_VERSION }} + pnpm-version: ${{ env.PNPM_VERSION }} + + - name: Set up pinned Bun + timeout-minutes: 3 + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Recompute exact continued release plan + id: continued_release_plan + if: ${{ inputs.continuation_pointer != '' }} + timeout-minutes: 2 + run: | + 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: Inspect exact parent continuation + id: inspect_registry_continuation + if: ${{ inputs.continuation_pointer != '' }} + # Covers the 53-minute sequential GitHub-read envelope plus local verification margin. + timeout-minutes: 55 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRODUCTS_JSON: ${{ steps.continued_release_plan.outputs.products_json }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_CONTINUATION_ARCHIVE: ${{ runner.temp }}/release-continuation.zip + RELEASE_CONTINUATION_GITHUB_PACER_PATH: ${{ runner.temp }}/continued-oliphaunt-github-content-write-pacer.json + RELEASE_CONTINUATION_GITHUB_CORE_JOURNAL_PATH: ${{ runner.temp }}/continued-oliphaunt-github-core-request-journal.json + RELEASE_OPERATION: publish + run: node .github/scripts/inspect-release-continuation.mjs + + - name: Resolve exact registry continuation inputs + id: registry_inputs + timeout-minutes: 1 + env: + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + ROOT_PRODUCTS_JSON: ${{ needs.publish.outputs.products_json }} + ROOT_APPROVED_RUN_ID: ${{ needs.publish.outputs.approved_run_id }} + ROOT_APPROVED_ARTIFACT_METADATA_JSON: ${{ needs.publish.outputs.approved_artifact_metadata_json }} + ROOT_STAGE_HANDOFF_ARTIFACT_ID: ${{ needs.publish.outputs.stage_handoff_artifact_id }} + ROOT_STAGE_HANDOFF_ARTIFACT_DIGEST: ${{ needs.publish.outputs.stage_handoff_artifact_digest }} + CONTINUED_PRODUCTS_JSON: ${{ steps.inspect_registry_continuation.outputs.products_json }} + CONTINUED_APPROVED_RUN_ID: ${{ steps.inspect_registry_continuation.outputs.approved_run_id }} + CONTINUED_APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.inspect_registry_continuation.outputs.approved_artifact_metadata_json }} + CONTINUED_STAGE_HANDOFF_RUN_ID: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_run_id }} + CONTINUED_STAGE_HANDOFF_ARTIFACT_ID: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_artifact_id }} + CONTINUED_STAGE_HANDOFF_ARTIFACT_NAME: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_artifact_name }} + CONTINUED_STAGE_HANDOFF_ARTIFACT_DIGEST: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_artifact_digest }} + CONTINUED_STAGE_HANDOFF_ARTIFACT_SIZE: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_artifact_size }} + CONTINUED_ROOT_RUN_ID: ${{ steps.inspect_registry_continuation.outputs.root_run_id }} + run: | + if [[ -n "$RELEASE_CONTINUATION_POINTER" ]]; then + products_json="$CONTINUED_PRODUCTS_JSON" + approved_run_id="$CONTINUED_APPROVED_RUN_ID" + approved_artifacts="$CONTINUED_APPROVED_ARTIFACT_METADATA_JSON" + stage_run_id="$CONTINUED_STAGE_HANDOFF_RUN_ID" + stage_artifact_id="$CONTINUED_STAGE_HANDOFF_ARTIFACT_ID" + stage_artifact_name="$CONTINUED_STAGE_HANDOFF_ARTIFACT_NAME" + stage_artifact_digest="$CONTINUED_STAGE_HANDOFF_ARTIFACT_DIGEST" + stage_artifact_size="$CONTINUED_STAGE_HANDOFF_ARTIFACT_SIZE" + root_run_id="$CONTINUED_ROOT_RUN_ID" + else + products_json="$ROOT_PRODUCTS_JSON" + approved_run_id="$ROOT_APPROVED_RUN_ID" + approved_artifacts="$ROOT_APPROVED_ARTIFACT_METADATA_JSON" + stage_run_id="$GITHUB_RUN_ID" + stage_artifact_id="$ROOT_STAGE_HANDOFF_ARTIFACT_ID" + stage_artifact_name="github-stage-handoff-${GITHUB_SHA}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + stage_artifact_digest="$ROOT_STAGE_HANDOFF_ARTIFACT_DIGEST" + stage_artifact_size='' + root_run_id="$GITHUB_RUN_ID" + fi + for name in products_json approved_run_id approved_artifacts stage_run_id stage_artifact_id stage_artifact_name stage_artifact_digest root_run_id; do + if [[ -z "${!name}" ]]; then + echo "resolved registry continuation input $name is empty" >&2 + exit 1 + fi + done + { + printf 'products_json=%s\n' "$products_json" + printf 'approved_run_id=%s\n' "$approved_run_id" + printf 'approved_artifact_metadata_json=%s\n' "$approved_artifacts" + printf 'stage_handoff_run_id=%s\n' "$stage_run_id" + printf 'stage_handoff_artifact_id=%s\n' "$stage_artifact_id" + printf 'stage_handoff_artifact_name=%s\n' "$stage_artifact_name" + printf 'stage_handoff_artifact_digest=%s\n' "$stage_artifact_digest" + printf 'stage_handoff_artifact_size=%s\n' "$stage_artifact_size" + printf 'root_run_id=%s\n' "$root_run_id" + } >> "$GITHUB_OUTPUT" + + - name: Prove bounded registry phase margin + id: registry_phase_budget + timeout-minutes: 1 + run: | + node tools/release/release-phase-budget.mjs \ + --phase registry-published \ + --registry-transfer-seconds 4200 + + - name: Download exact approved lock and capsule + id: download_approved_publication_inputs + timeout-minutes: 50 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + APPROVED_RUN_ID: ${{ steps.registry_inputs.outputs.approved_run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.registry_inputs.outputs.approved_artifact_metadata_json }} + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + run: | + node .github/scripts/download-build-artifacts.mjs \ + Release \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/approved-dry-run" \ + --run-id "$APPROVED_RUN_ID" \ + --artifact-metadata-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --artifact oliphaunt-publication-lock \ + --artifact oliphaunt-bootstrap-capsule + + - name: Download exact GitHub-stage handoff + id: download_github_stage_handoff + if: ${{ inputs.continuation_pointer == '' }} + timeout-minutes: 20 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + artifact-ids: ${{ needs.publish.outputs.stage_handoff_artifact_id }} + path: ${{ runner.temp }}/github-stage-handoff + + - name: Download original exact GitHub-stage handoff for continuation + id: download_continued_github_stage_handoff + if: ${{ inputs.continuation_pointer != '' }} + timeout-minutes: 20 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + STAGE_HANDOFF_RUN_ID: ${{ steps.registry_inputs.outputs.stage_handoff_run_id }} + STAGE_HANDOFF_ARTIFACT_NAME: ${{ steps.registry_inputs.outputs.stage_handoff_artifact_name }} + STAGE_HANDOFF_ARTIFACT_METADATA_JSON: ${{ steps.inspect_registry_continuation.outputs.stage_handoff_artifact_metadata_json }} + run: | + node .github/scripts/download-build-artifacts.mjs \ + Release \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/github-stage-handoff" \ + --run-id "$STAGE_HANDOFF_RUN_ID" \ + --artifact-metadata-json "$STAGE_HANDOFF_ARTIFACT_METADATA_JSON" \ + --artifact "$STAGE_HANDOFF_ARTIFACT_NAME" + + - name: Verify and install exact registry inputs + id: install_github_stage_handoff + timeout-minutes: 8 + env: + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.registry_inputs.outputs.approved_run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.registry_inputs.outputs.approved_artifact_metadata_json }} + ORIGINAL_GITHUB_PACER_PATH: ${{ runner.temp }}/oliphaunt-github-content-write-pacer.json + ORIGINAL_GITHUB_CORE_JOURNAL_PATH: ${{ runner.temp }}/oliphaunt-github-core-request-journal.json + PREINSTALL_GITHUB_CORE_JOURNAL_PATH: ${{ runner.temp }}/registry-input-download-core-journal.json + OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH: ${{ runner.temp }}/oliphaunt-github-content-write-pacer.json + OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH: ${{ runner.temp }}/oliphaunt-github-core-request-journal.json + OLIPHAUNT_RELEASE_ROOT_RUN_ID: ${{ steps.registry_inputs.outputs.root_run_id }} + RELEASE_CONTINUATION_GITHUB_PACER_PATH: ${{ steps.inspect_registry_continuation.outputs.continued_github_pacer_path }} + RELEASE_CONTINUATION_GITHUB_CORE_JOURNAL_PATH: ${{ steps.inspect_registry_continuation.outputs.continued_github_core_journal_path }} + RELEASE_CONTINUATION_GITHUB_STATE_JSON: ${{ steps.inspect_registry_continuation.outputs.continued_github_state_json }} + run: | + bun tools/release/bootstrap-publication-capsule.mjs verify-extract \ + --transport "$RUNNER_TEMP/approved-dry-run/oliphaunt-bootstrap-capsule.tar" \ + --approved-lock "$RUNNER_TEMP/approved-dry-run/publication-lock.json" \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --workspace-root "$GITHUB_WORKSPACE" + tools/dev/bun.sh tools/release/release-phase-handoff.mjs install \ + --phase github-staged \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --runner-root "$RUNNER_TEMP" \ + --approved-run-id "$APPROVED_RUN_ID" \ + --approved-artifacts-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --merge-existing-target true \ + --input "$RUNNER_TEMP/github-stage-handoff" + cmp "$RUNNER_TEMP/approved-dry-run/publication-lock.json" "$PUBLICATION_LOCK_PATH" + node .github/scripts/install-release-continuation-github-state.mjs + { + echo "OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH=$RUNNER_TEMP/oliphaunt-github-content-write-pacer.json" + echo "OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH=$RUNNER_TEMP/oliphaunt-github-core-request-journal.json" + echo "OLIPHAUNT_REQUIRE_GITHUB_CORE_REQUEST_JOURNAL=true" + echo "OLIPHAUNT_RELEASE_ROOT_RUN_ID=${{ steps.registry_inputs.outputs.root_run_id }}" + } >> "$GITHUB_ENV" + + - name: Set up Rust + timeout-minutes: 10 + uses: ./.github/actions/setup-rust + with: + cache-save-if: "true" + + - name: Configure macOS registry toolchains + timeout-minutes: 1 + run: | + java_home_17="${JAVA_HOME_17_arm64:-${JAVA_HOME_17_X64:-}}" + if [[ -z "$java_home_17" || ! -x "$java_home_17/bin/java" ]]; then + echo 'macOS release runner does not expose a usable Java 17 toolchain' >&2 + exit 1 + fi + echo "JAVA_HOME=$java_home_17" >> "$GITHUB_ENV" + echo "$java_home_17/bin" >> "$GITHUB_PATH" + + - name: Set up pinned Deno + timeout-minutes: 3 + uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Resolve selected registry authentication needs + id: registry_publish_needs + timeout-minutes: 1 + env: + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + run: bun .github/scripts/selected-registry-needs.mjs + + - name: Install exact JSR publisher dependencies + id: install_registry_jsr_tooling + if: ${{ steps.registry_publish_needs.outputs.needs_jsr == 'true' }} + timeout-minutes: 2 + run: pnpm install --frozen-lockfile --ignore-scripts --filter @oliphaunt/ts + + - name: Set up pinned npm publisher + id: setup_registry_npm + if: ${{ steps.registry_publish_needs.outputs.needs_npm == 'true' }} + timeout-minutes: 3 + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Verify reusable-workflow OIDC identity + id: verify_registry_oidc_identity + timeout-minutes: 2 + env: + RELEASE_OPERATION: publish + run: bun .github/scripts/verify-github-oidc-identity.mjs + + - name: Recheck registry credentials and trust links + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + 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.mjs --products-json "$PRODUCTS_JSON" + bun .github/scripts/verify-external-publish-readiness.mjs + + - name: Reverify Maven signing credentials before registry mutation + id: verify_registry_maven_signing + if: ${{ steps.registry_publish_needs.outputs.needs_maven == 'true' }} + timeout-minutes: 2 + env: + 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/dev/bun.sh tools/release/verify-maven-signing-readiness.mjs + + - name: Verify exact GitHub staging before registry mutation + id: verify_registry_github_staging + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/verify_product_tags.mjs \ + --products-json "$PRODUCTS_JSON" \ + --target "$RELEASE_HEAD_SHA" + bun .github/scripts/manage-release-drafts.mjs verify \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --state staged + + - name: Restore exact-SHA normal-publication checkpoint + id: restore_normal_publication_checkpoint + timeout-minutes: 7 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PUBLICATION_LOCK_PATH: target/release/publication-lock.json + NORMAL_PUBLICATION_CHECKPOINT_PATH: target/release/normal-publication-checkpoint.json + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_CONTINUATION_ARCHIVE: ${{ runner.temp }}/release-continuation.zip + run: tools/dev/bun.sh .github/scripts/download-normal-publication-checkpoint.mjs + + - name: Revalidate current main immediately before registry mutation + id: revalidate_registry_mutation + timeout-minutes: 1 + run: bash .github/scripts/require-current-main.sh "$RELEASE_HEAD_SHA" + + - name: Establish bounded capacity-inspection deadline + id: registry_capacity_deadline + timeout-minutes: 1 + run: | + for name in NORMAL_REGISTRY_MUTATION_WINDOW_SECONDS REGISTRY_JOB_HARD_DEADLINE_EPOCH REGISTRY_EVIDENCE_HANDOFF_RESERVE_SECONDS; do + value="${!name:-}" + if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then + echo "$name must be a positive integer" >&2 + exit 1 + fi + done + now=$(date +%s) + handoff_deadline=$(( REGISTRY_JOB_HARD_DEADLINE_EPOCH - REGISTRY_EVIDENCE_HANDOFF_RESERVE_SECONDS )) + available=$(( handoff_deadline - now )) + if (( available <= 0 )); then + echo 'No positive capacity-inspection window remains before protected recovery handoff' >&2 + exit 1 + fi + echo "REGISTRY_MUTATION_DEADLINE_EPOCH=$handoff_deadline" >> "$GITHUB_ENV" + echo "Capacity inspection and any admitted mutation must stop before Unix time $handoff_deadline." + + - name: Reprove all-registry publication capacity immediately before registry mutation + id: reprove_registry_capacity + timeout-minutes: 3 + env: + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + run: bun .github/scripts/check-crates-io-publish-capacity.mjs + + - name: Start authoritative bounded normal registry mutation window + id: registry_mutation_deadline + if: ${{ steps.reprove_registry_capacity.outputs.admission == 'execute' }} + timeout-minutes: 1 + env: + REQUIRED_WINDOW_SECONDS: ${{ steps.reprove_registry_capacity.outputs.required_window_seconds }} + run: node tools/release/normal-registry-mutation-start.mjs + + - name: Seal typed zero-mutation capacity or deadline deferral + id: record_registry_capacity_deferral + if: ${{ steps.reprove_registry_capacity.outputs.admission == 'defer' || steps.registry_mutation_deadline.outputs.admission == 'defer' }} + timeout-minutes: 10 + env: + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + CAPACITY_NAMES_SATISFIED: ${{ steps.reprove_registry_capacity.outputs.names_satisfied }} + CAPACITY_NOT_BEFORE_EPOCH: ${{ steps.registry_mutation_deadline.outputs.not_before_epoch || steps.reprove_registry_capacity.outputs.not_before_epoch }} + CAPACITY_REQUIRED_WINDOW_SECONDS: ${{ steps.reprove_registry_capacity.outputs.required_window_seconds }} + PRE_MUTATION_DEFERRAL_MODE: ${{ steps.registry_mutation_deadline.outputs.admission == 'defer' && 'pre-mutation-deadline' || '' }} + run: tools/dev/bun.sh tools/release/record-normal-publication-capacity-deferral.mjs + + - name: Publish exact-lock registry topology + id: exact_registry_publish + if: ${{ steps.reprove_registry_capacity.outputs.admission == 'execute' && steps.registry_mutation_deadline.outputs.admission == 'execute' }} + timeout-minutes: 190 + env: + ADMISSION_FILE: ${{ steps.reprove_registry_capacity.outputs.admission_file }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NORMAL_PUBLICATION_ADMISSION_DIGEST: ${{ steps.reprove_registry_capacity.outputs.admission_digest }} + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + 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/dev/bun.sh tools/release/release-publish.mjs publish \ + --registry-plan \ + --registry-admission "$ADMISSION_FILE" \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --publication-lock "$PUBLICATION_LOCK_PATH" + + - name: Require a typed registry execution decision + id: require_registry_execution_decision + if: ${{ steps.exact_registry_publish.outcome == 'success' || steps.record_registry_capacity_deferral.outcome == 'success' }} + timeout-minutes: 1 + env: + ADMISSION: ${{ steps.reprove_registry_capacity.outputs.admission }} + COMPLETE: ${{ steps.exact_registry_publish.outputs.complete || steps.record_registry_capacity_deferral.outputs.complete }} + CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + DEFERRED: ${{ steps.exact_registry_publish.outputs.deferred || steps.record_registry_capacity_deferral.outputs.deferred }} + DEFERRAL_MODE: ${{ steps.exact_registry_publish.outputs.deferral_mode || steps.record_registry_capacity_deferral.outputs.deferral_mode }} + PROGRESS_COUNT: ${{ steps.exact_registry_publish.outputs.progress_count || steps.record_registry_capacity_deferral.outputs.progress_count }} + REMAINING_COUNT: ${{ steps.exact_registry_publish.outputs.remaining_count || steps.record_registry_capacity_deferral.outputs.remaining_count }} + NOT_BEFORE_EPOCH: ${{ steps.exact_registry_publish.outputs.not_before_epoch || steps.record_registry_capacity_deferral.outputs.not_before_epoch }} + run: | + if [[ "$COMPLETE" == true && "$DEFERRED" == false ]]; then + if [[ "$ADMISSION" != execute || -n "$DEFERRAL_MODE" || "$REMAINING_COUNT" != 0 ]]; then + echo 'complete registry result disagrees with its execute admission or retains remaining operations' >&2 + exit 1 + fi + elif [[ "$COMPLETE" == false && "$DEFERRED" == true ]]; then + if [[ ! "$REMAINING_COUNT" =~ ^[1-9][0-9]*$ || ! "$NOT_BEFORE_EPOCH" =~ ^[1-9][0-9]*$ ]]; then + echo 'deferred registry result requires remaining work and a positive not-before time' >&2 + exit 1 + fi + if [[ "$DEFERRAL_MODE" == progress ]]; then + if [[ "$ADMISSION" != execute || ! "$PROGRESS_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo 'progress deferral requires execute admission and nonzero durable progress' >&2 + exit 1 + fi + elif [[ "$DEFERRAL_MODE" == rate-limit ]]; then + if [[ "$ADMISSION" != execute || "$PROGRESS_COUNT" != 0 ]]; then + echo 'rate-limit deferral requires execute admission and zero claimed progress' >&2 + exit 1 + fi + elif [[ "$DEFERRAL_MODE" == pre-mutation-deadline ]]; then + if [[ "$PROGRESS_COUNT" != 0 ]] || ! { [[ "$ADMISSION" == execute ]] || [[ "$ADMISSION" == defer && -n "$CONTINUATION_POINTER" ]]; }; then + echo 'pre-mutation deadline deferral requires zero progress and either execute admission or a continued capacity admission' >&2 + exit 1 + fi + elif [[ "$DEFERRAL_MODE" == pre-mutation-capacity ]]; then + if [[ "$ADMISSION" != defer || "$PROGRESS_COUNT" != 0 || -n "$CONTINUATION_POINTER" ]]; then + echo 'zero-mutation capacity deferral is permitted only on the root deferred admission' >&2 + exit 1 + fi + else + echo 'deferred registry result has an unsupported deferral mode' >&2 + exit 1 + fi + else + echo 'registry publisher must emit exactly one of complete or deferred' >&2 + exit 1 + fi + { + echo "complete=$COMPLETE" + echo "deferred=$DEFERRED" + echo "deferral_mode=$DEFERRAL_MODE" + echo "progress_count=$PROGRESS_COUNT" + echo "remaining_count=$REMAINING_COUNT" + echo "not_before_epoch=$NOT_BEFORE_EPOCH" + } >> "$GITHUB_OUTPUT" + + - name: Seal exact normal-publication continuation contract + id: prepare_registry_continuation + if: ${{ steps.require_registry_execution_decision.outputs.deferred == 'true' }} + timeout-minutes: 3 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.registry_inputs.outputs.approved_run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.registry_inputs.outputs.approved_artifact_metadata_json }} + RELEASE_OPERATION: publish + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_EXECUTION_RESULT_PATH: target/release/normal-publication-execution-result.json + RELEASE_CONTINUATION_STATE_PATH: target/release/normal-publication-checkpoint.json + RELEASE_CONTINUATION_CONTRACT_PATH: target/release/release-continuation-contract.json + STAGE_HANDOFF_RUN_ID: ${{ steps.registry_inputs.outputs.stage_handoff_run_id }} + STAGE_HANDOFF_ARTIFACT_ID: ${{ steps.registry_inputs.outputs.stage_handoff_artifact_id }} + STAGE_HANDOFF_ARTIFACT_NAME: ${{ steps.registry_inputs.outputs.stage_handoff_artifact_name }} + STAGE_HANDOFF_ARTIFACT_DIGEST: ${{ steps.registry_inputs.outputs.stage_handoff_artifact_digest }} + run: bun .github/scripts/prepare-release-continuation.mjs + + - name: Preserve immutable deferred normal-publication continuation + id: preserve_deferred_registry_recovery + if: ${{ steps.require_registry_execution_decision.outputs.deferred == 'true' }} + timeout-minutes: 10 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: normal-publication-continuation-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.prepare_registry_continuation.outputs.next_generation }} + path: | + target/release/normal-publication-checkpoint.json + target/release/normal-publication-execution-result.json + target/release/release-continuation-contract.json + target/release/oliphaunt-github-content-write-pacer.json + target/release/oliphaunt-github-core-request-journal.json + if-no-files-found: error + include-hidden-files: true + compression-level: 0 + retention-days: 90 + + - name: Preserve failed normal-publication recovery checkpoint + id: preserve_failed_registry_recovery + if: ${{ always() && ((steps.exact_registry_publish.outcome != 'skipped' && steps.exact_registry_publish.outcome != 'success') || (steps.record_registry_capacity_deferral.outcome != 'skipped' && steps.record_registry_capacity_deferral.outcome != 'success') || ((steps.exact_registry_publish.outcome == 'success' || steps.record_registry_capacity_deferral.outcome == 'success') && steps.require_registry_execution_decision.outcome != 'success') || (steps.require_registry_execution_decision.outputs.deferred == 'true' && (steps.prepare_registry_continuation.outcome != 'success' || steps.preserve_deferred_registry_recovery.outcome != 'success'))) }} + continue-on-error: true + timeout-minutes: 3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: normal-publication-recovery-${{ github.sha }} + path: | + target/release/normal-publication-plan.json + target/release/normal-publication-checkpoint.json + target/release/registry-integrity-receipts.json + if-no-files-found: warn + include-hidden-files: true + overwrite: true + retention-days: 90 + + - name: Preserve complete normal-publication recovery checkpoint + id: preserve_complete_registry_recovery + if: ${{ steps.require_registry_execution_decision.outputs.complete == 'true' }} + timeout-minutes: 3 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: normal-publication-recovery-${{ github.sha }} + path: | + target/release/normal-publication-plan.json + target/release/normal-publication-checkpoint.json + target/release/registry-integrity-receipts.json + target/release/github-release-attestation-receipt.json + if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 90 + + - name: Seal immutable registry-published handoff + id: seal_registry_handoff + if: ${{ steps.require_registry_execution_decision.outputs.complete == 'true' }} + timeout-minutes: 2 + env: + PRODUCTS_JSON: ${{ steps.registry_inputs.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.registry_inputs.outputs.approved_run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.registry_inputs.outputs.approved_artifact_metadata_json }} + run: | + tools/dev/bun.sh tools/release/release-phase-handoff.mjs seal \ + --phase registry-published \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --runner-root "$RUNNER_TEMP" \ + --approved-run-id "$APPROVED_RUN_ID" \ + --approved-artifacts-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --output "$RUNNER_TEMP/registry-published-handoff" + + - name: Preserve immutable publication receipts + id: preserve_publication_receipts + if: ${{ steps.require_registry_execution_decision.outputs.complete == 'true' }} + timeout-minutes: 10 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: registry-published-handoff-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/registry-published-handoff + if-no-files-found: error + include-hidden-files: true + compression-level: 0 + retention-days: 90 + + publish-finalize: + name: Verify consumers and publish GitHub releases + needs: + - release-identity + - publish-registry + runs-on: macos-26 + timeout-minutes: 120 + if: ${{ always() && inputs.operation == 'publish' && needs.release-identity.result == 'success' && needs.publish-registry.result == 'success' && needs.publish-registry.outputs.publication_complete == 'true' }} + environment: release-publish + permissions: + actions: read + contents: write + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Record bounded finalization job deadline + id: finalize_job_deadline + run: | + if [[ ! "$FINALIZE_JOB_HARD_WINDOW_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'FINALIZE_JOB_HARD_WINDOW_SECONDS must be a positive integer' >&2 + exit 1 + fi + hard_deadline=$(( $(date +%s) + FINALIZE_JOB_HARD_WINDOW_SECONDS )) + { + echo "FINALIZE_JOB_HARD_DEADLINE_EPOCH=$hard_deadline" + echo "REGISTRY_JOB_HARD_DEADLINE_EPOCH=$hard_deadline" + } >> "$GITHUB_ENV" + echo "Final verification and draft promotion must complete before Unix time $hard_deadline." + + - name: Checkout exact release commit + timeout-minutes: 5 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ needs.publish-registry.outputs.release_head_sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Resolve exact release commit + id: finalize_release_head + timeout-minutes: 1 + env: + INPUT_RELEASE_COMMIT: ${{ needs.publish-registry.outputs.release_head_sha }} + run: .github/scripts/resolve-release-head.sh + + - name: Set up pinned Node.js + id: setup_finalize_node + timeout-minutes: 5 + uses: ./.github/actions/setup-node-runtime + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Prove bounded finalization phase margin + id: finalize_phase_budget + timeout-minutes: 1 + run: node tools/release/release-phase-budget.mjs --phase github-finalized + + - name: Download exact registry-published handoff + id: download_registry_handoff + timeout-minutes: 5 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + artifact-ids: ${{ needs.publish-registry.outputs.registry_handoff_artifact_id }} + path: ${{ runner.temp }}/registry-published-handoff + + - name: Set up pinned Bun + timeout-minutes: 5 + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Install and verify exact registry-published handoff + id: install_registry_handoff + timeout-minutes: 5 + env: + PRODUCTS_JSON: ${{ needs.publish-registry.outputs.products_json }} + APPROVED_RUN_ID: ${{ needs.publish-registry.outputs.approved_run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ needs.publish-registry.outputs.approved_artifact_metadata_json }} + RELEASE_ROOT_RUN_ID: ${{ needs.publish-registry.outputs.root_run_id }} + run: | + tools/dev/bun.sh tools/release/release-phase-handoff.mjs install \ + --phase registry-published \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --runner-root "$RUNNER_TEMP" \ + --approved-run-id "$APPROVED_RUN_ID" \ + --approved-artifacts-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --input "$RUNNER_TEMP/registry-published-handoff" + { + echo "OLIPHAUNT_GITHUB_CONTENT_WRITE_PACER_PATH=$RUNNER_TEMP/oliphaunt-github-content-write-pacer.json" + echo "OLIPHAUNT_GITHUB_CORE_REQUEST_JOURNAL_PATH=$RUNNER_TEMP/oliphaunt-github-core-request-journal.json" + echo "OLIPHAUNT_REQUIRE_GITHUB_CORE_REQUEST_JOURNAL=true" + echo "OLIPHAUNT_RELEASE_ROOT_RUN_ID=$RELEASE_ROOT_RUN_ID" + } >> "$GITHUB_ENV" + + - name: Set up Rust + timeout-minutes: 15 + uses: ./.github/actions/setup-rust + with: + cache-save-if: "true" + + - name: Set up pinned npm + id: setup_finalize_npm + timeout-minutes: 5 + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Set up pinned Deno + timeout-minutes: 5 + uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb + with: + deno-version: ${{ env.DENO_VERSION }} + + - name: Configure macOS finalization toolchains + timeout-minutes: 1 + run: | + java_home_17="${JAVA_HOME_17_arm64:-${JAVA_HOME_17_X64:-}}" + if [[ -z "$java_home_17" || ! -x "$java_home_17/bin/java" ]]; then + echo 'macOS release runner does not expose a usable Java 17 toolchain' >&2 + exit 1 + fi + echo "JAVA_HOME=$java_home_17" >> "$GITHUB_ENV" + echo "$java_home_17/bin" >> "$GITHUB_PATH" + + - name: Verify final GitHub staging + id: verify_final_github_staging + timeout-minutes: 6 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ needs.publish-registry.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/verify_product_tags.mjs \ + --products-json "$PRODUCTS_JSON" \ + --target "$RELEASE_HEAD_SHA" + bun .github/scripts/manage-release-drafts.mjs verify \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --state staged + + - name: Enter bounded release finalization + id: enter_finalization + timeout-minutes: 1 + run: | + if [[ ! "$RELEASE_MINIMUM_FINALIZATION_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'RELEASE_MINIMUM_FINALIZATION_SECONDS must be a positive integer' >&2 + exit 1 + fi + now=$(date +%s) + remaining=$(( FINALIZE_JOB_HARD_DEADLINE_EPOCH - now )) + if (( remaining < RELEASE_MINIMUM_FINALIZATION_SECONDS )); then + echo "Only ${remaining}s remain before the finalization hard deadline; at least ${RELEASE_MINIMUM_FINALIZATION_SECONDS}s are required" >&2 + exit 1 + fi + echo "Mandatory finalization started with ${remaining}s before its fresh hard deadline." + + - name: Verify published release + id: verify_published_release + timeout-minutes: 8 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ needs.publish-registry.outputs.products_json }} + run: | + gh auth setup-git + git fetch --force --tags origin + tools/dev/bun.sh tools/release/release-verify.mjs \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --registry-receipts target/release/registry-integrity-receipts.json \ + --github-release-receipt target/release/github-release-attestation-receipt.json + + - name: Resolve and install exact public consumer surfaces + id: public_consumer_smoke + timeout-minutes: 15 + env: + PRODUCTS_JSON: ${{ needs.publish-registry.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/public-consumer-smoke.mjs \ + --publication-lock "$PUBLICATION_LOCK_PATH" \ + --products-json "$PRODUCTS_JSON" \ + --registry-receipts target/release/registry-integrity-receipts.json \ + --github-release-receipt target/release/github-release-attestation-receipt.json \ + --output target/release/public-consumer-smoke.json + + - name: Preserve immutable public consumer evidence + id: preserve_consumer_evidence + timeout-minutes: 2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: public-consumer-evidence-${{ github.sha }} + path: target/release/public-consumer-smoke.json + if-no-files-found: error + overwrite: true + retention-days: 90 + + - name: Reverify exact main and publication lock before promotion + id: reverify_publication_lock + timeout-minutes: 2 + run: | + bash .github/scripts/require-current-main.sh "$RELEASE_HEAD_SHA" + tools/dev/bun.sh tools/release/publication-lock.mjs \ + verify \ + --lock "$PUBLICATION_LOCK_PATH" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Preserve pre-promotion GitHub mutation evidence + id: preserve_pre_promotion_evidence + if: ${{ always() }} + timeout-minutes: 2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: github-pre-promotion-mutation-evidence-${{ github.sha }} + path: | + target/release/publication-lock.json + target/release/normal-publication-checkpoint.json + target/release/registry-integrity-receipts.json + target/release/public-consumer-smoke.json + ${{ runner.temp }}/oliphaunt-github-content-write-pacer.json + ${{ runner.temp }}/oliphaunt-github-core-request-journal.json + ${{ runner.temp }}/oliphaunt-github-release-asset-upload-report.json + if-no-files-found: error + include-hidden-files: true + overwrite: true + retention-days: 90 + + - name: Promote verified GitHub release drafts + id: promote_github_releases + timeout-minutes: 12 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ needs.publish-registry.outputs.products_json }} + run: bun .github/scripts/manage-release-drafts.mjs promote --products-json "$PRODUCTS_JSON" --head-ref "$RELEASE_HEAD_SHA" + + publish-bootstrap: + name: Bootstrap registry identities + needs: + - release-identity + runs-on: ubuntu-24.04 + timeout-minutes: 360 + if: ${{ inputs.operation == 'publish-bootstrap' }} + environment: release-bootstrap + permissions: + actions: read + contents: read + id-token: write + outputs: + continuation_required: ${{ steps.require_bootstrap_execution_decision.outputs.deferred }} + continuation_artifact_id: ${{ steps.preserve_deferred_bootstrap_ledger.outputs.artifact-id }} + continuation_artifact_digest: ${{ steps.preserve_deferred_bootstrap_ledger.outputs.artifact-digest }} + continuation_contract_digest: ${{ steps.prepare_bootstrap_continuation.outputs.contract_digest }} + steps: + - name: Require main + run: | + if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then + echo "Releases must be run from main; got ${GITHUB_REF}" >&2 + exit 1 + fi + + - name: Record bounded bootstrap job deadline + id: bootstrap_job_deadline + run: | + if [[ ! "$RELEASE_JOB_HARD_WINDOW_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'RELEASE_JOB_HARD_WINDOW_SECONDS must be a positive integer' >&2 + exit 1 + fi + hard_deadline=$(( $(date +%s) + RELEASE_JOB_HARD_WINDOW_SECONDS )) + { + echo "REGISTRY_JOB_HARD_DEADLINE_EPOCH=$hard_deadline" + echo "OLIPHAUNT_GITHUB_RUN_SNAPSHOT_DIR=$RUNNER_TEMP/oliphaunt-github-run-snapshots" + } >> "$GITHUB_ENV" + echo "The bootstrap job must stop registry work before Unix time $hard_deadline." + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + + - name: Resolve exact release commit + id: release_head + timeout-minutes: 1 + env: + INPUT_RELEASE_COMMIT: ${{ inputs.release_commit }} + run: .github/scripts/resolve-release-head.sh + + - name: Set up pinned Node.js + id: setup_bootstrap_node + timeout-minutes: 3 + uses: ./.github/actions/setup-node-runtime + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Set up pinned Bun + uses: ./.github/actions/setup-bun + with: + bun-version: ${{ env.BUN_VERSION }} + + - name: Plan bootstrap releases + id: release_plan + run: | + 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' }} + run: echo "No release-affecting product changes were found since the last product tag." + + - name: Resolve selected bootstrap authentication needs + id: registry_needs + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: bun .github/scripts/selected-registry-needs.mjs + + - name: Resolve registry identity bootstrap scope + id: bootstrap_scope + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} + env: + NEEDS_CARGO: ${{ steps.registry_needs.outputs.needs_cargo }} + NEEDS_NPM: ${{ steps.registry_needs.outputs.needs_npm }} + run: | + required=false + if [[ "$NEEDS_CARGO" == true || "$NEEDS_NPM" == true ]]; then + required=true + fi + echo "required=$required" >> "$GITHUB_OUTPUT" + + - name: No registry identities require bootstrap + if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.bootstrap_scope.outputs.required != 'true' }} + run: echo 'The selected release has no Cargo or npm identities; bootstrap is a no-op.' + + - name: Set up pinned npm publisher + if: ${{ steps.bootstrap_scope.outputs.required == 'true' && steps.registry_needs.outputs.needs_npm == 'true' }} + uses: ./.github/actions/setup-npm-publisher + with: + npm-version: ${{ env.NPM_VERSION }} + + - name: Verify reusable-workflow OIDC identity + id: verify_bootstrap_oidc_identity + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + RELEASE_OPERATION: publish-bootstrap + run: bun .github/scripts/verify-github-oidc-identity.mjs + + - name: Prove workflow HEAD is the release-bump commit + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/verify-release-commit.mjs \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Preflight selected product tag and release collisions + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + tools/dev/bun.sh tools/release/verify_product_tags.mjs \ + --products-json "$PRODUCTS_JSON" \ + --target "$RELEASE_HEAD_SHA" \ + --allow-missing + bun .github/scripts/manage-release-drafts.mjs preflight \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Require existing qualified release-commit CI run + id: ci_qualification + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + REQUIRES_WASIX_EVIDENCE: ${{ steps.release_plan.outputs.requires_wasix_release_regression_evidence }} + run: | + qualification_args=( + CI + "$RELEASE_HEAD_SHA" + 0 + --event push + --event workflow_dispatch + --job Builds + --job Required + --job Qualified + --artifact artifact-build-plan + --artifact oliphaunt-release-candidate + ) + if [[ "$REQUIRES_WASIX_EVIDENCE" == true ]]; then + qualification_args+=(--artifact wasix-release-regression-evidence) + fi + bash .github/scripts/require-workflow-success.sh "${qualification_args[@]}" + + - name: Download exact-SHA qualification record + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/release-candidate" \ + --run-id "$CI_RUN_ID" \ + --job Qualified \ + --artifact oliphaunt-release-candidate + + - name: Download exact-SHA affected plan + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/release-candidate/affected-plan" \ + --run-id "$CI_RUN_ID" \ + --job Plan \ + --artifact artifact-build-plan + + - name: Download required exact-SHA WASIX evidence + if: ${{ steps.bootstrap_scope.outputs.required == 'true' && steps.release_plan.outputs.requires_wasix_release_regression_evidence == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + CI \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/release-candidate/wasix-evidence" \ + --run-id "$CI_RUN_ID" \ + --job "E2E / WASIX release regression" \ + --artifact wasix-release-regression-evidence + + - name: Verify exact-SHA qualification record + id: verify_bootstrap_qualification + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + CI_RUN_ID: ${{ steps.ci_qualification.outputs.run_id }} + WASIX_EVIDENCE_REQUIRED: ${{ steps.release_plan.outputs.requires_wasix_release_regression_evidence }} + run: | + node .github/scripts/verify-release-candidate.mjs \ + "$RUNNER_TEMP/release-candidate/oliphaunt-release-candidate.json" \ + --plan "$RUNNER_TEMP/release-candidate/affected-plan/ci-plan.json" \ + --wasix-evidence-required "$WASIX_EVIDENCE_REQUIRED" \ + --wasix-evidence-root "$RUNNER_TEMP/release-candidate/wasix-evidence" + + - name: Inspect exact parent bootstrap continuation + id: inspect_bootstrap_continuation + if: ${{ steps.bootstrap_scope.outputs.required == 'true' && inputs.continuation_pointer != '' }} + # Covers the 53-minute sequential GitHub-read envelope plus local verification margin. + timeout-minutes: 55 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_CONTINUATION_ARCHIVE: ${{ runner.temp }}/release-continuation.zip + RELEASE_OPERATION: publish-bootstrap + run: node .github/scripts/inspect-release-continuation.mjs + + - name: Select one approved dry-run capsule + id: approved_bootstrap_capsule + if: ${{ steps.bootstrap_scope.outputs.required == 'true' && inputs.continuation_pointer == '' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + bash .github/scripts/require-workflow-success.sh \ + Release \ + "$RELEASE_HEAD_SHA" \ + 0 \ + --event workflow_dispatch \ + --artifact oliphaunt-publication-lock \ + --artifact oliphaunt-bootstrap-capsule + + - name: Download approved lock and capsule from one dry-run + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + DRY_RUN_ID: ${{ steps.inspect_bootstrap_continuation.outputs.approved_run_id || steps.approved_bootstrap_capsule.outputs.run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.inspect_bootstrap_continuation.outputs.approved_artifact_metadata_json || steps.approved_bootstrap_capsule.outputs.artifact_metadata_json }} + run: | + bun .github/scripts/download-build-artifacts.mjs \ + Release \ + "$RELEASE_HEAD_SHA" \ + "$RUNNER_TEMP/approved-bootstrap" \ + --run-id "$DRY_RUN_ID" \ + --artifact-metadata-json "$APPROVED_ARTIFACT_METADATA_JSON" \ + --artifact oliphaunt-publication-lock \ + --artifact oliphaunt-bootstrap-capsule + + - name: Verify and install approved bootstrap capsule + id: verify_bootstrap_capsule + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + run: | + if [[ -e "$GITHUB_WORKSPACE/target" ]]; then + echo 'bootstrap capsule installation requires an absent workspace target directory' >&2 + exit 1 + fi + tools/dev/bun.sh tools/release/bootstrap-publication-capsule.mjs verify-extract \ + --transport "$RUNNER_TEMP/approved-bootstrap/oliphaunt-bootstrap-capsule.tar" \ + --approved-lock "$RUNNER_TEMP/approved-bootstrap/publication-lock.json" \ + --products-json "$PRODUCTS_JSON" \ + --head-ref "$RELEASE_HEAD_SHA" \ + --workspace-root "$GITHUB_WORKSPACE" + + - name: Verify external lock equals installed capsule lock + id: verify_bootstrap_lock + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + run: | + if ! cmp -s "$RUNNER_TEMP/approved-bootstrap/publication-lock.json" "$PUBLICATION_LOCK_PATH"; then + echo 'installed capsule lock differs from the separately downloaded approved publication lock' >&2 + exit 1 + fi + tools/dev/bun.sh tools/release/publication-lock.mjs verify \ + --lock "$PUBLICATION_LOCK_PATH" \ + --head-ref "$RELEASE_HEAD_SHA" + + - name: Restore prior bootstrap checkpoint chain + id: restore_bootstrap_checkpoint + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_CONTINUATION_ARCHIVE: ${{ runner.temp }}/release-continuation.zip + run: bun .github/scripts/download-bootstrap-ledger.mjs + + - name: Revalidate current main immediately before bootstrap mutation + id: revalidate_bootstrap_mutation + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + timeout-minutes: 1 + run: bash .github/scripts/require-current-main.sh "$RELEASE_HEAD_SHA" + + - name: Require bootstrap credentials before mutation + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + CRATES_IO_BOOTSTRAP_TOKEN: ${{ secrets.CRATES_IO_BOOTSTRAP_TOKEN }} + NPM_BOOTSTRAP_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }} + NEEDS_CARGO: ${{ steps.registry_needs.outputs.needs_cargo }} + NEEDS_NPM: ${{ steps.registry_needs.outputs.needs_npm }} + run: | + if [[ "$NEEDS_CARGO" == true && -z "$CRATES_IO_BOOTSTRAP_TOKEN" ]]; then + echo 'selected products require CRATES_IO_BOOTSTRAP_TOKEN in the release-bootstrap environment' >&2 + exit 1 + fi + if [[ "$NEEDS_NPM" == true && -z "$NPM_BOOTSTRAP_TOKEN" ]]; then + echo 'selected products require NPM_BOOTSTRAP_TOKEN in the release-bootstrap environment' >&2 + exit 1 + fi + + - name: Start bounded bootstrap mutation window + id: bootstrap_mutation_deadline + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + run: | + if [[ ! "$REGISTRY_MUTATION_WINDOW_SECONDS" =~ ^[1-9][0-9]*$ ]]; then + echo 'REGISTRY_MUTATION_WINDOW_SECONDS must be a positive integer' >&2 + exit 1 + fi + if [[ ! "$REGISTRY_JOB_HARD_DEADLINE_EPOCH" =~ ^[1-9][0-9]*$ ]]; then + echo 'REGISTRY_JOB_HARD_DEADLINE_EPOCH must be a positive Unix timestamp' >&2 + exit 1 + fi + now=$(date +%s) + window_deadline=$(( now + REGISTRY_MUTATION_WINDOW_SECONDS )) + deadline=$window_deadline + if (( REGISTRY_JOB_HARD_DEADLINE_EPOCH < deadline )); then + deadline=$REGISTRY_JOB_HARD_DEADLINE_EPOCH + fi + echo "REGISTRY_MUTATION_DEADLINE_EPOCH=$deadline" >> "$GITHUB_ENV" + echo "Bootstrap registry mutation must stop before Unix time $deadline." + + - name: Configure npm identity-bootstrap authentication + if: ${{ steps.bootstrap_scope.outputs.required == 'true' && steps.registry_needs.outputs.needs_npm == 'true' }} + env: + NPM_BOOTSTRAP_TOKEN: ${{ secrets.NPM_BOOTSTRAP_TOKEN }} + run: | + umask 077 + npmrc="$RUNNER_TEMP/oliphaunt-bootstrap.npmrc" + printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_BOOTSTRAP_TOKEN" > "$npmrc" + + - name: Bootstrap missing Cargo and npm identities + id: bootstrap_registry_identities + if: ${{ steps.bootstrap_scope.outputs.required == 'true' }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_BOOTSTRAP_TOKEN }} + NPM_CONFIG_USERCONFIG: ${{ runner.temp }}/oliphaunt-bootstrap.npmrc + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + REGISTRY_BOOTSTRAP_CARGO_SECONDS_PER_CARRIER: ${{ vars.REGISTRY_BOOTSTRAP_CARGO_SECONDS_PER_CARRIER || '30' }} + REGISTRY_BOOTSTRAP_NPM_SECONDS_PER_CARRIER: ${{ vars.REGISTRY_BOOTSTRAP_NPM_SECONDS_PER_CARRIER || '30' }} + REGISTRY_BOOTSTRAP_RECONCILIATION_SECONDS_PER_CARRIER: ${{ vars.REGISTRY_BOOTSTRAP_RECONCILIATION_SECONDS_PER_CARRIER || '6' }} + REGISTRY_BOOTSTRAP_RESERVE_SECONDS: ${{ vars.REGISTRY_BOOTSTRAP_RESERVE_SECONDS || '600' }} + run: bun .github/scripts/bootstrap-registry-identities.mjs + + - name: Require a typed bootstrap execution decision + id: require_bootstrap_execution_decision + if: ${{ steps.bootstrap_registry_identities.outcome == 'success' }} + timeout-minutes: 1 + env: + COMPLETE: ${{ steps.bootstrap_registry_identities.outputs.complete }} + DEFERRED: ${{ steps.bootstrap_registry_identities.outputs.deferred }} + DEFERRAL_MODE: ${{ steps.bootstrap_registry_identities.outputs.deferral_mode }} + PROGRESS_COUNT: ${{ steps.bootstrap_registry_identities.outputs.progress_count }} + REMAINING_COUNT: ${{ steps.bootstrap_registry_identities.outputs.remaining_count }} + NOT_BEFORE_EPOCH: ${{ steps.bootstrap_registry_identities.outputs.not_before_epoch }} + run: | + if [[ "$COMPLETE" == true && "$DEFERRED" == false ]]; then + if [[ -n "$DEFERRAL_MODE" || "$REMAINING_COUNT" != 0 ]]; then + echo 'complete bootstrap result retains a deferral mode or remaining carriers' >&2 + exit 1 + fi + elif [[ "$COMPLETE" == false && "$DEFERRED" == true ]]; then + if [[ ! "$REMAINING_COUNT" =~ ^[1-9][0-9]*$ || ! "$NOT_BEFORE_EPOCH" =~ ^[1-9][0-9]*$ ]]; then + echo 'deferred bootstrap result requires remaining work and a positive not-before time' >&2 + exit 1 + fi + if [[ "$DEFERRAL_MODE" == progress ]]; then + if [[ ! "$PROGRESS_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo 'bootstrap progress deferral requires nonzero durable progress' >&2 + exit 1 + fi + elif [[ "$DEFERRAL_MODE" == rate-limit ]]; then + if [[ "$PROGRESS_COUNT" != 0 ]]; then + echo 'bootstrap rate-limit deferral cannot claim durable progress' >&2 + exit 1 + fi + elif [[ "$DEFERRAL_MODE" == pre-mutation-deadline ]]; then + if [[ "$PROGRESS_COUNT" != 0 ]]; then + echo 'bootstrap pre-mutation deadline deferral cannot claim durable progress' >&2 + exit 1 + fi + else + echo 'deferred bootstrap result has an unsupported deferral mode' >&2 + exit 1 + fi + else + echo 'bootstrap publisher must emit exactly one of complete or deferred' >&2 + exit 1 + fi + { + echo "complete=$COMPLETE" + echo "deferred=$DEFERRED" + echo "deferral_mode=$DEFERRAL_MODE" + echo "progress_count=$PROGRESS_COUNT" + echo "remaining_count=$REMAINING_COUNT" + echo "not_before_epoch=$NOT_BEFORE_EPOCH" + } >> "$GITHUB_OUTPUT" + + - name: Seal exact bootstrap continuation contract + id: prepare_bootstrap_continuation + if: ${{ steps.require_bootstrap_execution_decision.outputs.deferred == 'true' }} + timeout-minutes: 3 + env: + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} + APPROVED_RUN_ID: ${{ steps.inspect_bootstrap_continuation.outputs.approved_run_id || steps.approved_bootstrap_capsule.outputs.run_id }} + APPROVED_ARTIFACT_METADATA_JSON: ${{ steps.inspect_bootstrap_continuation.outputs.approved_artifact_metadata_json || steps.approved_bootstrap_capsule.outputs.artifact_metadata_json }} + RELEASE_OPERATION: publish-bootstrap + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + RELEASE_EXECUTION_RESULT_PATH: target/release/bootstrap-execution-result.json + RELEASE_CONTINUATION_STATE_PATH: target/release/bootstrap-ledger + RELEASE_CONTINUATION_CONTRACT_PATH: target/release/release-continuation-contract.json + run: bun .github/scripts/prepare-release-continuation.mjs + + - name: Record bootstrap identity result + if: ${{ steps.require_bootstrap_execution_decision.outputs.complete == 'true' }} + run: | + lock_sha256="$(sha256sum "$PUBLICATION_LOCK_PATH" | awk '{print $1}')" + { + echo '## Registry identity bootstrap complete' + echo + echo "- Release commit: \`$RELEASE_HEAD_SHA\`" + echo "- Publication lock SHA-256: \`$lock_sha256\`" + echo '- Scope: selected Cargo and npm identities only' + echo '- Next: configure trusted publishers, revoke bootstrap tokens, then run the normal publish operation' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Remove bootstrap npm credentials + id: remove_bootstrap_credentials + if: ${{ always() }} + run: rm -f "$RUNNER_TEMP/oliphaunt-bootstrap.npmrc" + + - name: Upload immutable deferred bootstrap continuation + id: preserve_deferred_bootstrap_ledger + if: ${{ steps.require_bootstrap_execution_decision.outputs.deferred == 'true' }} + timeout-minutes: 10 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: bootstrap-continuation-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.prepare_bootstrap_continuation.outputs.next_generation }} + path: | + target/release/bootstrap-ledger + target/release/bootstrap-execution-result.json + target/release/release-continuation-contract.json + if-no-files-found: error + include-hidden-files: true + compression-level: 0 + retention-days: 90 + + - name: Upload bootstrap identity ledger + id: preserve_bootstrap_ledger + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: oliphaunt-bootstrap-ledger + path: target/release/bootstrap-ledger + if-no-files-found: warn + overwrite: true + retention-days: 90 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4432219..f05cdc82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: inputs: operation: - description: Prepare a release PR, dry-run publish, or publish from main + description: Prepare, dry-run, publish with trusted publishers, or bootstrap first registry versions required: true type: choice default: prepare-release-pr @@ -13,183 +13,218 @@ on: - prepare-release-pr - publish-dry-run - publish + - publish-bootstrap + release_commit: + description: Optional full commit SHA assertion; when set it must equal the workflow commit + required: false + type: string + default: "" + continuation_pointer: + description: Internal exact-parent continuation pointer; leave empty for a maintainer dispatch + required: false + type: string + default: "" permissions: contents: read +env: + NODE_VERSION: 22.22.3 + concurrency: - group: release-${{ github.ref }} + # Every registry-writing or release-branch-writing operation shares one + # non-cancelling lock. GitHub permits one pending run while the active run + # completes, so maintainers must not stack mutation dispatches. Dry-runs are + # read-only and may run once per exact SHA. + group: release-${{ inputs.operation == 'publish-dry-run' && github.sha || 'mutation' }} cancel-in-progress: false -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - jobs: + validate-inputs: + name: Validate release inputs + runs-on: ubuntu-24.04 + timeout-minutes: 2 + permissions: + contents: read + steps: + - name: Checkout exact workflow commit + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Reject unsupported or stale release inputs + id: validate_release_inputs + env: + RELEASE_OPERATION: ${{ inputs.operation }} + RELEASE_COMMIT: ${{ inputs.release_commit }} + RELEASE_CONTINUATION_POINTER: ${{ inputs.continuation_pointer }} + run: bash .github/scripts/validate-release-workflow-inputs.sh + + # GitHub validates the permission requests of every nested job in a reusable + # workflow before evaluating job-level `if` conditions. Each caller must + # therefore expose the complete reusable-workflow ceiling. The explicit + # permissions on the called jobs in release-execute.yml still reduce the + # token used by the one operation that actually runs. prepare-release-pr: name: Prepare release PR - runs-on: ubuntu-latest - timeout-minutes: 20 - if: ${{ github.repository == 'f0rr0/oliphaunt' && inputs.operation == 'prepare-release-pr' }} - environment: release-pr + needs: validate-inputs + if: ${{ inputs.operation == 'prepare-release-pr' }} + permissions: + actions: read + attestations: write + contents: write + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/release-execute.yml + with: + operation: prepare-release-pr + release_commit: ${{ inputs.release_commit }} + continuation_pointer: ${{ inputs.continuation_pointer }} + + publish-dry-run: + name: Publish dry run + needs: validate-inputs + if: ${{ inputs.operation == 'publish-dry-run' }} + permissions: + actions: read + attestations: write + contents: write + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/release-execute.yml + with: + operation: publish-dry-run + release_commit: ${{ inputs.release_commit }} + continuation_pointer: ${{ inputs.continuation_pointer }} + + publish-bootstrap: + name: Bootstrap registry identities + needs: validate-inputs + if: ${{ inputs.operation == 'publish-bootstrap' }} permissions: + actions: read + attestations: write contents: write + id-token: write + issues: write pull-requests: write + uses: ./.github/workflows/release-execute.yml + with: + operation: publish-bootstrap + release_commit: ${{ inputs.release_commit }} + continuation_pointer: ${{ inputs.continuation_pointer }} + + dispatch-bootstrap-continuation: + name: Dispatch verified bootstrap continuation + needs: publish-bootstrap + if: ${{ needs.publish-bootstrap.outputs.continuation_required == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + permissions: + actions: write + contents: read steps: - - name: Require main - run: | - if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then - echo "Releases must be run from main; got ${GITHUB_REF}" >&2 - exit 1 - fi - - - name: Checkout repository + - name: Checkout exact release transport + timeout-minutes: 3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - fetch-depth: 0 persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools - with: - cache: "false" - - - name: Create or update release PR - id: release_plz_pr - uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 + - name: Set up pinned Node.js + id: setup_dispatch_node + timeout-minutes: 3 + uses: ./.github/actions/setup-node-runtime with: - command: release-pr - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }} + node-version: ${{ env.NODE_VERSION }} - - name: Refresh release PR example lockfiles - if: ${{ steps.release_plz_pr.outputs.pr != '' && steps.release_plz_pr.outputs.pr != 'null' }} + - name: Dispatch exact verified child run + id: dispatch_continuation + timeout-minutes: 40 env: - GH_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN || secrets.GITHUB_TOKEN }} - PR: ${{ steps.release_plz_pr.outputs.pr }} - run: | - set -euo pipefail - - pr_number="$(jq -r '.number // empty' <<< "${PR}")" - if [[ -z "${pr_number}" ]]; then - echo "release-plz did not return a release PR; skipping example lockfile refresh" - exit 0 - fi - - gh auth setup-git - gh pr checkout "${pr_number}" - scripts/sync-example-lockfiles.py - - if git diff --quiet -- examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock; then - echo "example lockfiles already current" - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock - git commit -m "chore(release): refresh example lockfiles" - git push + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_OPERATION: publish-bootstrap + RELEASE_HEAD_SHA: ${{ github.sha }} + CONTINUATION_ARTIFACT_ID: ${{ needs.publish-bootstrap.outputs.continuation_artifact_id }} + CONTINUATION_ARTIFACT_DIGEST: ${{ needs.publish-bootstrap.outputs.continuation_artifact_digest }} + CONTINUATION_CONTRACT_DIGEST: ${{ needs.publish-bootstrap.outputs.continuation_contract_digest }} + CONTINUATION_AUTHORIZATION_PATH: ${{ runner.temp }}/release-continuation-authorization.json + run: node .github/scripts/dispatch-release-continuation.mjs + + - name: Publish exact dispatched-child authorization + id: preserve_continuation_authorization + timeout-minutes: 5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: ${{ steps.dispatch_continuation.outputs.authorization_artifact_name }} + path: ${{ runner.temp }}/release-continuation-authorization.json + if-no-files-found: error + compression-level: 0 + retention-days: 90 publish: name: Publish release - runs-on: ubuntu-latest - timeout-minutes: 120 - if: ${{ github.repository == 'f0rr0/oliphaunt' && inputs.operation != 'prepare-release-pr' }} - environment: ${{ inputs.operation == 'publish' && 'crates-io' || 'release-dry-run' }} + needs: validate-inputs + if: ${{ inputs.operation == 'publish' }} permissions: actions: read + attestations: write contents: write id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/release-execute.yml + with: + operation: publish + release_commit: ${{ inputs.release_commit }} + continuation_pointer: ${{ inputs.continuation_pointer }} + + dispatch-publish-continuation: + name: Dispatch verified registry continuation + needs: publish + if: ${{ needs.publish.outputs.continuation_required == 'true' }} + runs-on: ubuntu-24.04 + timeout-minutes: 60 + permissions: + actions: write + contents: read steps: - - name: Require main - run: | - if [[ "${GITHUB_REF}" != "refs/heads/main" ]]; then - echo "Releases must be run from main; got ${GITHUB_REF}" >&2 - exit 1 - fi - - - name: Checkout repository + - name: Checkout exact release transport + timeout-minutes: 3 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - fetch-depth: 0 persist-credentials: false - - name: Set up Rust tooling - uses: ./.github/actions/setup-rust-tools + - name: Set up pinned Node.js + id: setup_dispatch_node + timeout-minutes: 3 + uses: ./.github/actions/setup-node-runtime with: - cache-save-if: "true" + node-version: ${{ env.NODE_VERSION }} - - name: Require successful same-SHA CI workflow + - name: Dispatch exact verified child run + id: dispatch_continuation + timeout-minutes: 40 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: bash .github/scripts/require-workflow-success.sh CI "$GITHUB_SHA" 7200 - - - name: Validate release changelog - run: .github/scripts/check-release-changelog.sh - - - name: Download release asset and AOT artifacts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: .github/scripts/download-aot-artifacts.sh - - - name: Validate staged release packages and dry-runs - run: scripts/validate.sh release - - - name: Confirm release dry-run coverage - if: ${{ inputs.operation == 'publish-dry-run' }} - run: | - echo "scripts/validate.sh release staged the generated release workspace," - echo "dry-ran every internal asset/AOT crate, enforced package sizes," - echo "and attempted the root crate dry-run." - echo "The real publish step uses the same staged Cargo.toml so" - echo "generated payloads are included in the published crates." - echo "Skipping release-plz dry_run because same-release internal crates" - echo "are not present in crates.io until the real publish step." - - - name: Publish with release-plz - if: ${{ inputs.operation == 'publish' }} - id: release_plz_publish - uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 + RELEASE_OPERATION: publish + RELEASE_HEAD_SHA: ${{ github.sha }} + CONTINUATION_ARTIFACT_ID: ${{ needs.publish.outputs.continuation_artifact_id }} + CONTINUATION_ARTIFACT_DIGEST: ${{ needs.publish.outputs.continuation_artifact_digest }} + CONTINUATION_CONTRACT_DIGEST: ${{ needs.publish.outputs.continuation_contract_digest }} + CONTINUATION_AUTHORIZATION_PATH: ${{ runner.temp }}/release-continuation-authorization.json + run: node .github/scripts/dispatch-release-continuation.mjs + + - name: Publish exact dispatched-child authorization + id: preserve_continuation_authorization + timeout-minutes: 5 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: - command: release - manifest_path: target/pglite-oxide/release/workspace/Cargo.toml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Require release output - if: ${{ inputs.operation == 'publish' && steps.release_plz_publish.outputs.releases_created != 'true' }} - run: | - echo "release-plz completed without creating a release." >&2 - echo "Check that Cargo.toml contains an unpublished version and that release-plz was run without dry_run." >&2 - exit 1 - - - name: Resolve release tag - if: ${{ inputs.operation == 'publish' }} - id: release_tag - run: | - version="$(cargo metadata --no-deps --format-version 1 \ - --manifest-path target/pglite-oxide/release/workspace/Cargo.toml \ - | jq -r '.packages[] | select(.name == "pglite-oxide") | .version')" - if [[ -z "${version}" || "${version}" == "null" ]]; then - echo "could not resolve pglite-oxide package version" >&2 - exit 1 - fi - echo "tag=${version}" >> "$GITHUB_OUTPUT" - - - name: Package public release assets - if: ${{ inputs.operation == 'publish' }} - run: cargo run -p xtask -- release package-assets - - - name: Upload public release assets - if: ${{ inputs.operation == 'publish' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ steps.release_tag.outputs.tag }} - run: | - gh release upload "$RELEASE_TAG" \ - target/pglite-oxide/release-assets/*.tar.zst \ - target/pglite-oxide/release-assets/*.sha256 \ - --clobber \ - --repo "$GITHUB_REPOSITORY" + name: ${{ steps.dispatch_continuation.outputs.authorization_artifact_name }} + path: ${{ runner.temp }}/release-continuation-authorization.json + if-no-files-found: error + compression-level: 0 + retention-days: 90 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 14d9f6e3..e48df653 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,2 +1,2 @@ -# Shared by `scripts/validate.sh workflows` and the workflow-lint CI job. +# Shared by `moon run ci-workflows:check` and the workflow-lint CI job. rules: {} diff --git a/.gitignore b/.gitignore index eb5f90ff..acbc667c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,27 @@ /target/ +/.local/ +/.moon/cache/ node_modules/ -/scripts/perf/node-bench/node_modules/ /tmp/ -/assets/checkouts/ -/assets/wasix-build/build/ -/assets/wasix-build/work/ -/target/pglite-oxide/ -/crates/assets/assets/ -/crates/aot/*/artifacts/ +/target/oliphaunt-sources/checkouts/ +/target/oliphaunt-wasix/ +/src/target/ +/src/docs/.docusaurus/ +/src/docs/.next/ +/src/docs/.source/ +/src/docs/build/ +/src/docs/out/ +/src/runtimes/liboliphaunt/wasix/crates/assets/assets/ +/src/runtimes/liboliphaunt/wasix/crates/aot/*/artifacts/ +/src/sdks/react-native/ios/vendor/oliphaunt-swift/ +/src/sdks/react-native/ios/vendor/liboliphaunt.xcframework/ **/.DS_Store __pycache__/ # Build artifacts from cargo package -pglite_oxide-*.crates.tar.gz +oliphaunt_wasix-*.crates.tar.gz + +# Exact candidate locks are generated from the qualified local Cargo registry. +/examples/tauri/src-tauri/Cargo.lock +/examples/tauri-wasix/src-tauri/Cargo.lock +/examples/electron-wasix/src-wasix/Cargo.lock +/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 00000000..fafbf350 --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,15 @@ +accept = [200, 206, 429] +exclude = [ + "http://localhost", + "https://localhost", + "http://127.0.0.1", + "https://127.0.0.1", +] +exclude_path = [ + "target", + "node_modules", + "src/runtimes/liboliphaunt/wasix/assets/build", +] +max_retries = 2 +timeout = 20 +user_agent = "oliphaunt-link-check" diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 00000000..d169c907 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,19 @@ +{ + "globs": [ + "README.md", + "docs/**/*.md", + "src/**/README.md", + "src/sdks/react-native/examples/expo/README.md", + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md" + ], + "ignores": ["target/**", "**/node_modules/**", "src/runtimes/liboliphaunt/wasix/assets/build/**"], + "config": { + "default": true, + "MD013": false, + "MD024": { + "siblings_only": true + }, + "MD033": false, + "MD041": false + } +} diff --git a/.moon/tasks/inputs.yml b/.moon/tasks/inputs.yml new file mode 100644 index 00000000..071ce5c3 --- /dev/null +++ b/.moon/tasks/inputs.yml @@ -0,0 +1,21 @@ +$schema: "https://moonrepo.dev/schemas/tasks.json" + +# Installed dependency trees are derived state. Task invalidation comes from +# tracked package manifests, workspace configuration, and lockfiles instead of +# pnpm's directory symlinks, which Moon cannot hash as files. +implicitInputs: + - "/.moon/tasks/**/*" + # Checkout normalization affects every source byte seen by every task. + - "/.gitattributes" + # Command discovery participates in Moon graph, affected-plan, policy, and + # release computation, so changing it must invalidate every consumer. + - "/tools/dev/bun.sh" + - "/tools/dev/moon-command.mjs" + - "!/node_modules" + - "!/node_modules/**" + - "!/examples/**/node_modules" + - "!/examples/**/node_modules/**" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/tools/**/node_modules" + - "!/tools/**/node_modules/**" diff --git a/.moon/toolchains.yml b/.moon/toolchains.yml new file mode 100644 index 00000000..95676b25 --- /dev/null +++ b/.moon/toolchains.yml @@ -0,0 +1,22 @@ +$schema: "https://moonrepo.dev/schemas/toolchains.json" + +proto: + version: "0.57.4" + +javascript: + plugin: "registry://ghcr.io/moonrepo/javascript_toolchain@sha256:cc86bce23fb5d02228da3dd29396da21ff5e63ef97b74e6d7b4e2db78c69c762" + packageManager: "pnpm" + installDependencies: false + syncProjectWorkspaceDependencies: false + +node: + plugin: "registry://ghcr.io/moonrepo/node_toolchain@sha256:f3d7fb17637b543ea4916caeedbfde6827b94b4b70ab1c30651846d96c8583b1" + versionFromPrototools: true + +pnpm: + plugin: "registry://ghcr.io/moonrepo/node_depman_toolchain@sha256:c56f6dd596fbbf9bc628b9fcf145fd6cd5c5d842d5a65537571e775f328ddce6" + versionFromPrototools: true + +rust: + plugin: "registry://ghcr.io/moonrepo/rust_toolchain@sha256:46387ad173cc254f83b4bd392618c8fa530bca667bdafe4c63c5114c4a59f4a5" + versionFromPrototools: false diff --git a/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 00000000..d697fd16 --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,44 @@ +$schema: "https://moonrepo.dev/schemas/workspace.json" + +projects: + globFormat: "source-path" + globs: + - "moon.yml" + - "benchmarks/moon.yml" + - "examples/moon.yml" + - "src/*/moon.yml" + - "src/bindings/*/moon.yml" + - "src/extensions/artifacts/*/moon.yml" + - "src/extensions/contrib/moon.yml" + - "src/extensions/contrib/*/moon.yml" + - "src/extensions/external/*/moon.yml" + - "src/extensions/model/moon.yml" + - "src/postgres/versions/*/moon.yml" + - "src/runtimes/*/moon.yml" + - "src/runtimes/liboliphaunt/*/moon.yml" + - "src/sdks/*/moon.yml" + - "src/sources/*/moon.yml" + - "src/sources/third-party/*/moon.yml" + - "src/shared/*/moon.yml" + - "tools/*/moon.yml" + sources: + ci-workflows: ".github" + +defaultProject: "oliphaunt-rust" + +pipeline: + installDependencies: false + cacheLifetime: "7 days" + logRunningCommand: true + +vcs: + provider: "github" + defaultBranch: "main" + +telemetry: false + +experiments: + asyncAffectedTracking: true + asyncGraphBuilding: true + casOutputsCache: true + nativeFileHashing: true diff --git a/.prototools b/.prototools new file mode 100644 index 00000000..e04ca5b7 --- /dev/null +++ b/.prototools @@ -0,0 +1,5 @@ +moon = "2.3.2" +node = "22.22.3" +pnpm = "11.5.0" +bun = "1.3.14" +deno = "2.8.1" diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..47bbf033 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,20 @@ +{ + "src/runtimes/liboliphaunt/native": "0.0.0", + "src/sdks/rust": "0.0.0", + "src/runtimes/broker": "0.0.0", + "src/runtimes/node-direct": "0.0.0", + "src/sdks/swift": "0.0.0", + "src/sdks/kotlin": "0.0.0", + "src/sdks/react-native": "0.0.0", + "src/sdks/js": "0.0.0", + "src/extensions/contrib": "0.0.0", + "src/extensions/external/pg_hashids": "0.0.0", + "src/extensions/external/pg_ivm": "0.0.0", + "src/extensions/external/pg_textsearch": "0.0.0", + "src/extensions/external/pg_uuidv7": "0.0.0", + "src/extensions/external/pgtap": "0.0.0", + "src/extensions/external/postgis": "0.0.0", + "src/extensions/external/vector": "0.0.0", + "src/runtimes/liboliphaunt/wasix": "0.0.0", + "src/bindings/wasix-rust/crates/oliphaunt-wasix": "0.0.0" +} diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..af430982 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,19 @@ +[files] +extend-exclude = [ + "target/**", + "**/node_modules/**", + "src/runtimes/liboliphaunt/wasix/assets/build/postgres/patches/**", + "src/runtimes/liboliphaunt/native/patches/**", + "src/runtimes/liboliphaunt/native/postgres18/**", +] + +[default] +extend-ignore-re = [ + "PNPM", + "WASIX", +] + +[default.extend-words] +oliphaunt = "oliphaunt" +wasix = "wasix" +ser = "ser" diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2d807225..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,123 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) -and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.5.1](https://github.com/f0rr0/oliphaunt/compare/0.5.0...0.5.1) - 2026-06-04 - -### Fixed - -- webc dependency conflict ([#39](https://github.com/f0rr0/oliphaunt/pull/39)) - -## [0.5.0](https://github.com/f0rr0/oliphaunt/compare/0.4.1...0.5.0) - 2026-05-08 - -### Fixed - -- publish runtime assets on GitHub releases ([#27](https://github.com/f0rr0/oliphaunt/pull/27)) -- mount WASIX urandom as virtual device ([#31](https://github.com/f0rr0/oliphaunt/pull/31)) -- quiet expected postgres error recovery ([#28](https://github.com/f0rr0/oliphaunt/pull/28)) - -## [0.4.1](https://github.com/f0rr0/oliphaunt/compare/0.4.0...0.4.1) - 2026-05-07 - -### Fixed - -- Publish release crates from the validated staged workspace so the - `pglite-oxide-assets` and target-specific AOT crates include their generated - payloads ([#25](https://github.com/f0rr0/oliphaunt/pull/25), - [#24](https://github.com/f0rr0/oliphaunt/issues/24)). -- Release all internal asset/AOT crates at `0.4.1` alongside the root crate so - the fixed release cannot resolve to broken `0.4.0` payload packages. -- Guard release validation against root/internal package version drift and - Cargo's same-release internal dependency dry-run wording. - -## [0.4.0](https://github.com/f0rr0/oliphaunt/compare/0.3.0...0.4.0) - 2026-05-07 - -> **Known broken release:** The crates.io packages for `0.4.0` were published -> without the generated runtime/AOT payloads, causing default `bundled` builds -> to fail at startup with missing embedded runtime assets. Use `0.4.1` or newer. - -### Breaking - -- Pivoted `pglite-oxide` to a new runtime architecture built around - reproducible Wasmer WASIX artifacts, generated asset manifests, and - target-specific AOT crates instead of checked-in runtime blobs - ([#13](https://github.com/f0rr0/oliphaunt/pull/13)). -- Release packages now rely on CI-generated portable WASIX and native AOT - artifacts for the exact release SHA. Applications should use the crate APIs - instead of depending on repository asset paths. - -### Added - -- Added extension catalog/build metadata, smoke/promoted extension manifests, - and generated APIs for discovering bundled PostgreSQL extensions. -- Added `pg_dump` support and native AOT packages for the supported macOS, - Linux, and Windows target triples. - -### Changed - -- Reworked runtime startup, asset loading, protocol recovery, proxy behavior, - and test coverage around the new backend. - -### Fixed - -- Refreshed example lockfiles automatically after release-plz version bumps so - release PRs keep examples in sync with package versions. -- Isolated downloaded AOT artifacts by target and preserved portable assets - after cache restore during asset/release jobs. -- Kept release validation inside the generated staged workspace so package - checks run against the publishable crate contents. -- Allowed release dry-runs to reuse the latest compatible Assets run when - package-only release commits do not change runtime inputs. - -## [0.3.0](https://github.com/f0rr0/oliphaunt/compare/0.2.0...0.3.0) - 2026-04-26 - -### Breaking - -- optimize startup and add Tauri SQLx profiler ([#9](https://github.com/f0rr0/oliphaunt/pull/9)) - -- `PgliteRuntimeOptions::default` now selects the optimized embedded-template - startup path. -- `ensure_cluster` now requires runtime options. -- Runtime packaging now uses a bundled optimized runtime archive. - -### Added - -- Reusable embedded runtime caching and on-disk compiled-module cache support - for faster startup. -- Embedded prepopulated PGDATA template with manifest validation. -- Vanilla Tauri v2 SQLx profiler example with release-mode workload reporting. -- Repo hooks for Conventional Commit validation, formatting, and pre-push checks. - -### Changed - -- Quieted WASI stdio by default and prefer Unix sockets where available. -- Streamlined runtime, release, contributing, and usage docs around the optimized - default path. - -### Fixed - -- Hardened proxy frontend framing for SSL requests, cancel requests, - split/coalesced packets, and extended-query batching. - -## [0.2.0](https://github.com/f0rr0/oliphaunt/compare/0.1.0...0.2.0) - 2026-04-24 - -### Added - -- modernize embedded PGlite API and OSS tooling ([#3](https://github.com/f0rr0/oliphaunt/pull/3)) - -- Added the high-level `Pglite` and `PgliteServer` APIs for direct embedded use - and PostgreSQL client compatibility. -- Added process-local template cluster reuse for fast temporary databases. -- Added SQLx and `tokio-postgres` compatibility coverage, runtime/proxy smoke - tests, CI, cargo-deny policy checks, Conventional Commit validation, and - documented runtime asset provenance. -- Improved the blocking proxy/server path for extended-protocol clients, - readiness handling, and socket mode behavior. - -## [0.1.0](https://github.com/f0rr0/oliphaunt/releases/tag/0.1.0) - 2025-09-27 - -- Initial repository release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51693f81..67ca1555 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,42 +2,54 @@ ## Local Checks -Run the same gates as CI before opening a PR: +Install the pinned tools once, then run the local qualification gates before +opening a PR: ```sh -scripts/validate.sh ci -scripts/validate.sh release -cargo deny check +tools/dev/bootstrap-tools.sh +moon run dev-tools:doctor +moon run policy-tools:fmt-check +moon run :check +moon run :test ``` The runtime smoke starts embedded Postgres and is intentionally slower than unit tests. +The protected `publish-dry-run` operation is a release-candidate check: run it +from the GitHub `Release` workflow after the exact release-bump commit has a +successful `Qualified` CI record. It is not a routine source-PR check. Install local hooks with: ```sh -scripts/install-hooks.sh +tools/dev/bun.sh tools/dev/install-hooks.mjs ``` Hooks stay deliberately smaller than CI: pre-commit handles file hygiene and -formatting, while pre-push runs whitespace diff checking, clippy, and -`scripts/validate.sh test`. That test gate compiles runtime tests when host AOT -artifacts are unavailable and runs them when artifacts have been materialized. -CI repeats those hook checks and remains the source of truth for generated AOT -runtime matrices, packaging, Tauri, frontend, feature combinations, public API +formatting, while commit-msg validates Conventional Commit messages. Run +`moon run repo:release-check` in addition to the normal checks for a +release-sensitive PR. CI remains the source of truth for generated AOT runtime +matrices, packaging, Tauri, frontend, feature combinations, public API compatibility, and supply-chain checks. -In GitHub branch protection, require the aggregate `Required checks` status and -the Conventional Commit status before merging. Local hooks are convenience -checks and can be skipped; CI is authoritative. +In GitHub branch protection, require the aggregate `Required` status before +merging; it already includes release intent, checks, tests, builds, and selected +E2E. Require linear history and squash merges, and keep force-push and deletion +disabled. Local hooks are convenience checks and can be skipped; CI is +authoritative. ## Assets -Bundled runtime assets must stay aligned with `docs/ASSETS.md`. If the WASI runtime -changes, update the asset metadata in `Cargo.toml` and run the full local checks. +Bundled runtime assets must stay aligned with product-local runtime metadata +under `src/runtimes/` and extension metadata under `src/extensions/`. If a +runtime or extension artifact target changes, update the owning product +metadata and run the affected Moon checks. ## Releases Releases are manual and must be dispatched from `main` through the GitHub -Actions `Release` workflow. release-plz owns version bumps, changelog updates, -tags, GitHub releases, and crates.io publishing. See `docs/RELEASE.md` for the -release-intent, Trusted Publishing, and manual workflow details. +Actions `Release` workflow. Release Please manifest mode owns version bumps, +changelog updates, and the generated release PR. The protected publish workflow +owns exact-SHA product tags and draft GitHub releases. Product-local release +metadata owns publish targets and artifact shape; Moon dependency scopes +provide release coupling. See `docs/maintainers/release.md` for release intent, +trusted publishing, and workflow details. diff --git a/Cargo.lock b/Cargo.lock index c858cd8d..a4cdf5b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,9 +90,18 @@ 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 = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arrayref" @@ -153,6 +162,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "2.0.1" @@ -477,6 +492,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -589,9 +610,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -645,6 +666,33 @@ dependencies = [ "cmov", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.20.11" @@ -778,6 +826,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -787,6 +845,17 @@ dependencies = [ "powerfmt", ] +[[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.117", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -859,7 +928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.0", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] @@ -923,6 +992,30 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.16.0" @@ -1029,12 +1122,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.29" @@ -1094,6 +1205,16 @@ 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 = "fs_extra" version = "1.3.0" @@ -1655,9 +1776,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -1733,6 +1854,47 @@ dependencies = [ "windows-link", ] +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-portable" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "libredox" version = "0.1.17" @@ -1745,6 +1907,17 @@ dependencies = [ "redox_syscall 0.8.1", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libtest-mimic" version = "0.8.2" @@ -1821,9 +1994,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lz4_flex" @@ -2091,6 +2264,165 @@ dependencies = [ "ruzstd", ] +[[package]] +name = "oliphaunt" +version = "0.0.0" +dependencies = [ + "crossbeam-channel", + "ed25519-dalek", + "flate2", + "fs2", + "futures-util", + "getrandom 0.3.4", + "libloading", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx", + "tar", + "tokio", + "tokio-postgres", + "toml 0.9.12+spec-1.1.0", + "ureq", + "zip", + "zstd", +] + +[[package]] +name = "oliphaunt-broker" +version = "0.0.0" +dependencies = [ + "oliphaunt", +] + +[[package]] +name = "oliphaunt-build" +version = "0.0.0" +dependencies = [ + "serde", + "sha2 0.10.9", + "tempfile", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "oliphaunt-icu" +version = "0.0.0" +dependencies = [ + "sha2 0.10.9", + "tar", + "zstd", +] + +[[package]] +name = "oliphaunt-native-extension-proof" +version = "0.0.0" +dependencies = [ + "oliphaunt", + "tar", +] + +[[package]] +name = "oliphaunt-perf" +version = "0.0.0" +dependencies = [ + "anyhow", + "directories", + "futures-util", + "oliphaunt", + "oliphaunt-wasix", + "rusqlite", + "serde", + "serde_json", + "sqlx", + "tar", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "oliphaunt-tools" +version = "0.0.0" + +[[package]] +name = "oliphaunt-wasix" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "directories", + "dunce", + "filetime", + "flate2", + "hex", + "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", + "sha2 0.10.9", + "sqlx", + "tar", + "tempfile", + "tokio", + "tokio-postgres", + "tracing", + "wasmer", + "wasmer-config", + "wasmer-types", + "wasmer-wasix", + "webc", + "zstd", +] + +[[package]] +name = "oliphaunt-wasix-tools" +version = "0.0.0" +dependencies = [ + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" +version = "0.0.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2168,64 +2500,6 @@ dependencies = [ "serde", ] -[[package]] -name = "pglite-oxide" -version = "0.5.1" -dependencies = [ - "anyhow", - "async-trait", - "directories", - "dunce", - "filetime", - "flate2", - "hex", - "pglite-oxide-aot-aarch64-apple-darwin", - "pglite-oxide-aot-aarch64-unknown-linux-gnu", - "pglite-oxide-aot-x86_64-pc-windows-msvc", - "pglite-oxide-aot-x86_64-unknown-linux-gnu", - "pglite-oxide-assets", - "regex", - "serde", - "serde_json", - "sha2 0.10.9", - "sqlx", - "tar", - "tempfile", - "tokio", - "tokio-postgres", - "tracing", - "wasmer", - "wasmer-config", - "wasmer-types", - "wasmer-wasix", - "webc", - "zstd", -] - -[[package]] -name = "pglite-oxide-aot-aarch64-apple-darwin" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-x86_64-pc-windows-msvc" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-assets" -version = "0.5.1" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "phf" version = "0.13.1" @@ -2301,6 +2575,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2315,14 +2599,14 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "postgres-protocol" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ "base64", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "hmac 0.13.0", "md-5 0.11.0", "memchr", @@ -2333,12 +2617,12 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" dependencies = [ "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "postgres-protocol", ] @@ -2690,6 +2974,20 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" +[[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" @@ -2720,6 +3018,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.12.1", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -2754,6 +3066,41 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "log", + "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" @@ -2987,6 +3334,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3050,6 +3403,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlx" version = "0.8.6" @@ -3407,14 +3770,14 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" dependencies = [ "async-trait", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-channel", "futures-util", "log", @@ -3626,12 +3989,33 @@ 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 = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.8" @@ -3673,6 +4057,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3681,9 +4071,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "virtual-fs" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e66c1686d8c304c6136cb1a553cbc16c92261af8f34be365af8400b0ce82f94" +checksum = "a1a93b14f49c6ab20c3d4bbdbf58043629474d9b8dedf98a131bb80655938950" dependencies = [ "anyhow", "async-trait", @@ -3711,9 +4101,9 @@ dependencies = [ [[package]] name = "virtual-mio" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f86b519f58e30beca3845b5da865ebb7ea29c59b8d6b625ef8982ef1af93337" +checksum = "617451273a35a957615025a19e3cbc5150182eb6763170957b1f218f2a0a4796" dependencies = [ "async-trait", "bytes", @@ -3728,9 +4118,9 @@ dependencies = [ [[package]] name = "virtual-net" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac308570c4756033af92f1b8680f0f84b82df526d25575c2136cde7bbbd838d6" +checksum = "59442152699e0dd4e8c48df76dc98a9212e10c70550329ef43a38bdf8e221743" dependencies = [ "anyhow", "async-trait", @@ -3948,12 +4338,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.250.0" +version = "0.252.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" dependencies = [ "leb128fmt", - "wasmparser 0.250.0", + "wasmparser 0.252.0", ] [[package]] @@ -3970,9 +4360,9 @@ dependencies = [ [[package]] name = "wasmer" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "596add954aa5e3937e889839c63250fc72340ccdb0cb9adcb89f026535300f73" +checksum = "d431becbbc16f47b18ee6a2f9990748bec2016aadc00fc8dec5d9c413bf1c2b8" dependencies = [ "bindgen", "bytes", @@ -3983,7 +4373,7 @@ dependencies = [ "derive_more", "futures", "indexmap", - "itertools 0.14.0", + "itertools 0.15.0", "js-sys", "more-asserts", "paste", @@ -4007,9 +4397,9 @@ dependencies = [ [[package]] name = "wasmer-compiler" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c15b69f6d74316e1a8366911bd04d9bab1115a8712c1fb4323d37624382d84c" +checksum = "50eb9d7da57f78359118ad77fc396a5b43c24f0bd33cb06274403a0931540761" dependencies = [ "backtrace", "bytes", @@ -4017,7 +4407,7 @@ dependencies = [ "crossbeam-channel", "enum-iterator", "enumset", - "itertools 0.14.0", + "itertools 0.15.0", "leb128", "libc", "macho-unwind-info", @@ -4036,16 +4426,16 @@ dependencies = [ "thiserror", "wasmer-types", "wasmer-vm", - "wasmparser 0.250.0", + "wasmparser 0.252.0", "which", "windows-sys 0.61.2", ] [[package]] name = "wasmer-compiler-llvm" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e26a612f49bccca19aff456e55c0ee3dc1e0437d054f577af1dbfc253a3988e" +checksum = "1cccf59f46038c05f0e9dd1b9e30395381fa34170f743bd88844722138c3b698" dependencies = [ "byteorder", "cc", @@ -4053,7 +4443,7 @@ dependencies = [ "enum-iterator", "enumset", "inkwell", - "itertools 0.14.0", + "itertools 0.15.0", "libc", "object 0.39.1", "phf", @@ -4071,9 +4461,9 @@ dependencies = [ [[package]] name = "wasmer-config" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcff14aae6b37c51f0bdc6e73736df7b978dd0515659e5fc6db3afb74ffe323f" +checksum = "a38646c3b2bcb22ec9850534e7cfd9fd32593a9c2688e28578f75fa01e763b94" dependencies = [ "anyhow", "bytesize", @@ -4094,9 +4484,9 @@ dependencies = [ [[package]] name = "wasmer-derive" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349030f566b3fe9ef09bf4abf4b917968a937f403a5e208740aa4c88e87928e5" +checksum = "0fb1362cd59f3bab617d6304df7146c6011a0730c831a1a34ccf38204d15a906" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -4106,9 +4496,9 @@ dependencies = [ [[package]] name = "wasmer-journal" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5863066574694ff8df6cf316416e89b7d4f0c7bca866facdfd4d8369b335fa55" +checksum = "f43d272409a4933ee1abfd5edc7182d0346f9324ce2cd294a3659a861f5e7b3e" dependencies = [ "anyhow", "async-trait", @@ -4133,9 +4523,9 @@ dependencies = [ [[package]] name = "wasmer-package" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b786ad94623fa6612d4ed85e2603590797544ecd4ac5f8d414bebe677920cd5" +checksum = "b25588b797946331c5d0595c063689a9171ee0da9aff58f49a955e6a8baec9b8" dependencies = [ "anyhow", "bytes", @@ -4162,9 +4552,9 @@ dependencies = [ [[package]] name = "wasmer-types" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aaf2baad42ce3f3ebc4508fbe8bb362fe31c08bae9048646842affd4868812d" +checksum = "02e4041b657749cb8ce4cf9410c92fe7dd521de8e05d3ee6de27076a18894351" dependencies = [ "bytecheck", "crc32fast", @@ -4173,21 +4563,21 @@ dependencies = [ "getrandom 0.4.2", "hex", "indexmap", - "itertools 0.14.0", + "itertools 0.15.0", "more-asserts", "rkyv", "serde", "sha2 0.11.0", "target-lexicon", "thiserror", - "wasmparser 0.250.0", + "wasmparser 0.252.0", ] [[package]] name = "wasmer-vm" -version = "7.2.0-alpha.3" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54214dc7f3bc7c0f19eb31ac7d10796f30314a6fb3666004f4b11798646dd6e4" +checksum = "9116fa04fc64ec2ee8f033e21e648fb2590582265fe129d2ff5f2f27a0661256" dependencies = [ "backtrace", "bytesize", @@ -4200,7 +4590,7 @@ dependencies = [ "fnv", "gimli 0.33.0", "indexmap", - "itertools 0.14.0", + "itertools 0.15.0", "libc", "libunwind", "mach2 0.6.0", @@ -4217,9 +4607,9 @@ dependencies = [ [[package]] name = "wasmer-wasix" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6cfbfb4636accd684b014841965d19674b75b8ae8446e9327ef04f7a7e9ae9" +checksum = "c63bed9300a2767cae4445556a52efa5926220018b0c8ad733c2b4c21d033f65" dependencies = [ "anyhow", "async-trait", @@ -4243,7 +4633,7 @@ dependencies = [ "heapless", "hex", "http", - "itertools 0.14.0", + "itertools 0.15.0", "libc", "libtest-mimic", "linked_hash_set", @@ -4278,14 +4668,14 @@ dependencies = [ "virtual-net", "waker-fn", "walkdir", - "wasm-encoder 0.250.0", + "wasm-encoder 0.252.0", "wasmer", "wasmer-config", "wasmer-journal", "wasmer-package", "wasmer-types", "wasmer-wasix-types", - "wasmparser 0.250.0", + "wasmparser 0.252.0", "webc", "weezl", "windows-sys 0.61.2", @@ -4295,9 +4685,9 @@ dependencies = [ [[package]] name = "wasmer-wasix-types" -version = "0.702.0-alpha.3" +version = "0.702.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e823d48c54f97a6663844c2fd52dad4894da08fc930bcb930b93799b5d9606" +checksum = "9a95a4e6064fc49b4996570cc74252afb60890af48020757a930d5b34abd8514" dependencies = [ "anyhow", "bitflags 2.12.1", @@ -4342,6 +4732,16 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags 2.12.1", + "indexmap", +] + [[package]] name = "web-sys" version = "0.3.99" @@ -4380,6 +4780,24 @@ dependencies = [ "url", ] +[[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.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weezl" version = "0.2.1" @@ -4418,6 +4836,22 @@ dependencies = [ "web-sys", ] +[[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" @@ -4427,6 +4861,12 @@ 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 = "windows-link" version = "0.2.1" @@ -4721,16 +5161,11 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", - "directories", - "futures-util", - "pglite-oxide", "serde", "serde_json", "sha2 0.10.9", - "sqlx", "tar", "tokio", - "tokio-postgres", "toml 0.9.12+spec-1.1.0", "walkdir", "wasmer", @@ -4811,6 +5246,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" @@ -4844,12 +5285,41 @@ dependencies = [ "syn 2.0.117", ] +[[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", + "memchr", + "thiserror", + "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" diff --git a/Cargo.toml b/Cargo.toml index 8a3428db..ba35f63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,123 +1,34 @@ -[package] -name = "pglite-oxide" -version = "0.5.1" -edition = "2024" -rust-version = "1.93" -description = "Embedded Postgres for Rust tests and local apps. No Docker, works with SQLx and any Postgres client." -readme = "README.md" -repository = "https://github.com/f0rr0/pglite-oxide" -homepage = "https://github.com/f0rr0/pglite-oxide" -documentation = "https://docs.rs/pglite-oxide" -keywords = ["postgres", "pglite", "wasm", "database", "embedded"] -categories = ["database-implementations", "wasm", "development-tools::testing"] -license = "MIT AND Apache-2.0 AND PostgreSQL" -exclude = [ - ".github/**", - "Cargo.toml.orig", - "assets/checkouts/**", - "assets/wasix-build/build/**", - "assets/wasix-build/work/**", - "crates/**", - "examples/tauri-sqlx-vanilla/**", - "release-plz.toml", - "xtask/**", -] - [workspace] members = [ - ".", - "crates/assets", - "crates/aot/aarch64-apple-darwin", - "crates/aot/x86_64-unknown-linux-gnu", - "crates/aot/aarch64-unknown-linux-gnu", - "crates/aot/x86_64-pc-windows-msvc", - "xtask", + "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", + "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/native-extension-proof", + "tools/xtask", ] -exclude = ["examples/tauri-sqlx-vanilla/src-tauri"] +exclude = ["src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri"] resolver = "3" -[features] -default = ["bundled", "extensions"] -bundled = [ - "dep:pglite-oxide-assets", - "dep:pglite-oxide-aot-aarch64-apple-darwin", - "dep:pglite-oxide-aot-x86_64-unknown-linux-gnu", - "dep:pglite-oxide-aot-aarch64-unknown-linux-gnu", - "dep:pglite-oxide-aot-x86_64-pc-windows-msvc", -] -extensions = ["bundled"] - -[package.metadata.pglite-oxide.assets] -postgres-version = "17.5" -postgres-pglite-branch = "REL_17_5-pglite" -pglite-build-repo = "electric-sql/pglite-build" -pglite-build-branch = "portable" -pglite-build-commit = "c195113dbaf09488f8d5eeb2db91dacd123b74d0" -pglite-npm-version-checked = "0.4.5" -runtime-archive-sha256 = "8c2271c53d4f2786f7406ec0211e679e1a13352fc14ae87215ae3569b547fc1f" -pglite-wasix-sha256 = "4ce77a543675b25a5b1fd93c62bd175576bf1fee0266b9fd96fac193bf13b811" -pgdata-template-archive-sha256 = "a0a91f4fbd0428787ce78b351ee84f0c33f9ce8578448701b0f6080f7d8b052e" -pg-dump-wasix-sha256 = "59482f1193c35147c1b50e6a5fd9bc2dfed4b15e0624102c4da93a266d8303ed" -initdb-wasix-sha256 = "" - -[dependencies] -anyhow = "1" -async-trait = "0.1" -tar = "0.4" -zstd = { version = "0.13", default-features = false } -directories = "6" -tracing = "0.1" -flate2 = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -regex = "1" -tempfile = "3" -hex = "0.4" -sha2 = "0.10" -dunce = "1" -filetime = "0.2" -pglite-oxide-assets = { version = "=0.5.1", path = "crates/assets", optional = true } -tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } -wasmer = { version = "=7.2.0-alpha.3", default-features = false, features = [ - "sys", - "headless", - "compiler", - "wasmer-artifact-load", -] } -wasmer-config = "=0.702.0-alpha.3" -wasmer-types = "=7.2.0-alpha.3" -wasmer-wasix = { version = "=0.702.0-alpha.3", default-features = false, features = [ - "sys-minimal", - "sys-poll", - "host-vnet", - "time", -] } -webc = "=12.0.0" - -[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] -pglite-oxide-aot-aarch64-apple-darwin = { version = "=0.5.1", path = "crates/aot/aarch64-apple-darwin", optional = true } - -[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] -pglite-oxide-aot-x86_64-unknown-linux-gnu = { version = "=0.5.1", path = "crates/aot/x86_64-unknown-linux-gnu", optional = true } - -[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies] -pglite-oxide-aot-aarch64-unknown-linux-gnu = { version = "=0.5.1", path = "crates/aot/aarch64-unknown-linux-gnu", optional = true } - -[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies] -pglite-oxide-aot-x86_64-pc-windows-msvc = { version = "=0.5.1", path = "crates/aot/x86_64-pc-windows-msvc", optional = true } - -[dev-dependencies] -sqlx = { version = "0.8", default-features = false, features = [ - "postgres", - "runtime-tokio", -] } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } -tokio-postgres = "0.7" - -[[bin]] -name = "pglite-dump" -path = "src/bin/pglite_dump.rs" +[workspace.package] +edition = "2024" +rust-version = "1.93" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" -[[bin]] -name = "pglite-proxy" -path = "src/bin/pglite_proxy.rs" +[profile.release] +strip = "symbols" diff --git a/LICENSE b/LICENSE index 4780985e..ac7484ea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 pglite-oxide Contributors +Copyright (c) 2024 oliphaunt-wasix Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift new file mode 100644 index 00000000..95cc0807 --- /dev/null +++ b/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +// SwiftPM is the public Apple SDK entrypoint. Release automation tags this +// root package and pairs it with checksum-covered liboliphaunt-native-v assets. +let package = Package( + name: "Oliphaunt", + platforms: [ + .iOS(.v17), + .macOS(.v14) + ], + products: [ + .library(name: "Oliphaunt", targets: ["Oliphaunt"]) + ], + targets: [ + .target( + name: "COliphaunt", + path: "src/sdks/swift/Sources/COliphaunt", + publicHeadersPath: "include" + ), + .target( + name: "Oliphaunt", + dependencies: ["COliphaunt"], + path: "src/sdks/swift/Sources/Oliphaunt" + ), + .testTarget( + name: "OliphauntTests", + dependencies: ["Oliphaunt"], + path: "src/sdks/swift/Tests/OliphauntTests" + ) + ] +) diff --git a/README.md b/README.md index 62daec96..845d13c1 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,135 @@

- pglite-oxide logo + Oliphaunt

-

pglite-oxide

+

Oliphaunt

- Embedded Postgres for Rust tests and local apps.
- Real PostgreSQL. Instant testing. Packaged runtime. Direct Rust API or a local Postgres URL. + Native-first embedded PostgreSQL 18 for desktop, mobile, and WASIX applications.

-

- Usage - · - Performance - · - Extensions - · - Dump & Upgrade - · - Testing - · - Tauri -

- -

- CI - crates.io - docs.rs - MSRV - License -

+Oliphaunt is a family of peer SDKs and runtime products over the same embedded +PostgreSQL model. Applications own their database roots, choose an honest +runtime mode for their platform, and package only the exact PostgreSQL +extensions they select. + +> **Release status:** this source tree is preparing Oliphaunt's first +> independently versioned public releases. Source versions intentionally remain +> `0.0.0` until the generated release PR advances them. Package names and +> install examples describe the release contract; they are not evidence that a +> registry package has already been published. + +## Product model + +Oliphaunt is a multi-product monorepo, not one repository-wide version: + +- `liboliphaunt-native` owns the PostgreSQL 18 C ABI runtime and native target + carriers. +- `liboliphaunt-wasix` owns portable WASIX runtime assets and host AOT + carriers. +- Rust, Swift, Kotlin/Android, React Native, TypeScript, and WASIX Rust are + separately versioned SDK products. +- Broker and Node-direct helpers are separately versioned runtime products. +- Every promoted SQL extension remains exactly selectable. PostgreSQL 18 + contrib members share one runtime-bound distribution product and its stable + carriers; each external extension is a separately tagged, independently + versioned product. + +A product owns its SemVer, changelog, source identity, product tag, and GitHub +release. Platform packages, ABI payloads, and size-split crates are carriers of +that product; they use the product version and are not extra products. + +## First-release target envelope + +The published target manifests currently declare: + +| Surface | Declared release targets | +| --- | --- | +| Desktop native | Linux x64 GNU, Linux arm64 GNU, macOS arm64, Windows x64 MSVC | +| Android | `arm64-v8a`, `x86_64` | +| Apple | iOS XCFramework carrier plus the declared macOS arm64 runtime carrier, delivered through SwiftPM and GitHub release assets | +| WASIX | portable runtime plus AOT carriers for Linux x64/arm64 GNU, macOS arm64, and Windows x64 MSVC | + +The first release intentionally does **not** claim macOS x64, Windows ARM64, +Linux musl, Android 32-bit, or undeclared Apple architectures. A compiler, +language, or runtime working on a broader platform is not a support promise; +the explicit target manifest, publication catalog, and frozen release lock are +the boundary. + +Exact-extension support is target-specific too. An extension is publishable +for a target only when its own target manifest and evidence declare that row. +The public [release reference](src/docs/content/reference/releases.mdx) +publishes the enforced OS/API/ABI floors and distinguishes built package +coverage from installed-app execution evidence, including the Android arm64 +and physical-iOS boundaries. + +## SDK entry points + +The planned public entry points are: + +| App surface | Package entry point | Distribution boundary | +| --- | --- | --- | +| Rust/Tauri desktop | `oliphaunt` | Cargo and target-specific native artifact crates | +| WASIX Rust | `oliphaunt-wasix` | Cargo portable/AOT artifact crates | +| Swift | `Oliphaunt` | SwiftPM source tag and checksum-pinned release assets | +| Android | `dev.oliphaunt:oliphaunt-android` and `dev.oliphaunt.android` | Maven Central AAR, Gradle plugin/marker, and declared ABI carriers | +| React Native | `@oliphaunt/react-native` | npm package delegating runtime work to Swift and Kotlin | +| Node.js, Bun, and Deno | `@oliphaunt/ts` | npm for native runtime support; JSR for protocol/query helpers only | +| Native bindings | `liboliphaunt` C ABI | declared native runtime carriers | + +Kotlin host-native and JVM compilations are development/parity evidence, not +public Kotlin Multiplatform or JVM artifacts. The first Swift release starts at +`0.6.0` because legacy unscoped SwiftPM tags already occupy `0.1.0` through +`0.5.1`; other new products start at `0.1.0`. + +## Exact extensions + +Extension selection uses exact PostgreSQL SQL names. There are no selection +packs, aliases, or implicit groups; the contrib distribution bundle is only a +carrier envelope. Selecting `earthdistance` may include its declared `cube` +dependency; selecting `vector` does not pull unrelated extensions into the +application. + +The `oliphaunt-extension-contrib-pg18` product is runtime-bound and moves with +the linked runtime version group. Its target carriers contain an exact, +checksummed member inventory, but consumers still stage only the requested SQL +members. External extension products own independent packaging SemVer. Their +immutable upstream version/commit and compatible Oliphaunt runtime versions are +recorded separately, so consumers must not infer compatibility from matching +version numbers. + +## Development + +Install the pinned toolchain once, then use Moon as the repository task +surface: -`pglite-oxide` brings PGlite/Postgres to Rust with a small API. Open a database -directly with `Pglite`, or hand `PgliteServer` to SQLx and any standard -Postgres client. The packaged runtime is PostgreSQL 17.5. No local Postgres -install, no Docker, no runtime build toolchain. - -## Add Postgres In One Minute ⚡ +```sh +tools/dev/bootstrap-tools.sh +moon run dev-tools:doctor +moon run policy-tools:fmt-check +moon run :check +moon run :test +``` -Already using SQLx or another Postgres client? Add the crate and point your -client at an embedded database URL: +For a release-sensitive change, also run the metadata and package contract +gate: ```sh -cargo add pglite-oxide +moon run repo:release-check ``` -```rust,no_run -use pglite_oxide::PgliteServer; -use sqlx::{Connection, Row}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let server = PgliteServer::temporary_tcp()?; - // For a persistent TCP server: - // let server = PgliteServer::builder().path("./.pglite").start()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; - - let row = sqlx::query("SELECT $1::int4 + 1 AS answer") - .bind(41_i32) - .fetch_one(&mut conn) - .await?; - assert_eq!(row.try_get::("answer")?, 42); - - conn.close().await?; - server.shutdown()?; - Ok(()) -} -``` +The protected GitHub `Release` workflow owns candidate dry-runs and all public +mutation. Local development commands do not publish packages, create tags, or +promote releases. -That's it. Real PostgreSQL, no service setup. - -## Why pglite-oxide ✨ - -Postgres should be as easy to add to a Rust project as SQLite. - -- ⚡ **No service tax**: no Docker, no local Postgres, no testcontainers. -- 🔌 **Use your real stack**: SQLx, `tokio-postgres`, CLIs, and other clients - connect through a normal local URL. -- 🌉 **Proxy included**: expose an embedded database to non-Rust tools with - `pglite-proxy`. -- 🧪 **Clean tests**: temporary databases are isolated, fast, and removed on - drop. -- 💾 **Persistent apps**: keep local app data across restarts when you want it. -- 🧩 **Extensions included**: `pgvector`, `pg_trgm`, `hstore`, `citext`, and - more. -- 📦 **Portable dumps**: use bundled `pg_dump` for logical backups and upgrade - paths. -- 🚀 **Near-native feel**: close to native Postgres, fully embedded. - -## Near-Native Performance 🚀 - -Current local snapshot on `Apple M1 Pro`, `16 GB RAM`, and `macOS 26.4.1`. -Full numbers and reproduction steps live in the -[performance guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PERFORMANCE.md). Lower is better. - -| Operation | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | -|---|---:|---:|---:| -| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | -| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | -| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | -| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | -| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | - -`pglite-oxide` stays close to native Postgres while running entirely embedded -and consistently performs better than vanilla PGlite. - -## Extensions 🧩 - -Bundled extensions are supported, including `pgvector`, `pg_trgm`, `hstore`, -`citext`, `ltree`, and more. See the -[extensions guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/EXTENSIONS.md) -for the full catalog and usage details. - -```rust,no_run -use pglite_oxide::{extensions, PgliteServer}; -use sqlx::Connection; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let server = PgliteServer::builder() - .path("./.pglite") - .extension(extensions::VECTOR) - .start()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; - - sqlx::query("CREATE TABLE IF NOT EXISTS items (embedding vector(3))") - .execute(&mut conn) - .await?; - sqlx::query("INSERT INTO items VALUES ('[1,2,3]')") - .execute(&mut conn) - .await?; - - conn.close().await?; - server.shutdown()?; - Ok(()) -} -``` +## Documentation -## Docs +- [Public SDK documentation](src/docs/content/sdk/index.mdx) +- [Capabilities](src/docs/content/reference/capabilities.mdx) +- [Exact extension model](src/docs/content/reference/extensions.mdx) +- [Source architecture](docs/architecture/final-product-source-architecture.md) +- [Maintainer documentation index](docs/maintainers/README.md) +- [Release process](docs/maintainers/release.md) +- [Contributing](CONTRIBUTING.md) -- [Usage guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/USAGE.md) -- [Extensions](https://github.com/f0rr0/pglite-oxide/blob/main/docs/EXTENSIONS.md) -- [Performance guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PERFORMANCE.md) -- [Dump and upgrade guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/PG_DUMP.md) -- [Testing guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/TESTING.md) -- [Tauri usage](https://github.com/f0rr0/pglite-oxide/blob/main/docs/TAURI.md) -- [Runtime guide](https://github.com/f0rr0/pglite-oxide/blob/main/docs/RUNTIME.md) +Oliphaunt is licensed under the terms recorded in [LICENSE](LICENSE). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 575bddf6..4ed084cb 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,13 +1,18 @@ # Third-Party Notices -`pglite-oxide` Rust code is licensed under the MIT license in `LICENSE`. +Oliphaunt source code in this repository is licensed under the MIT license in +`LICENSE`. -The bundled PGlite/PostgreSQL runtime assets derive from Electric SQL PGlite and -PostgreSQL: +This file is the repository-level notice index. Product-specific runtime and +packaging notices live next to the product that ships the relevant artifacts: -- PGlite: https://github.com/electric-sql/pglite -- PGlite PostgreSQL fork: https://github.com/electric-sql/postgres-pglite -- PostgreSQL license: https://www.postgresql.org/about/licence/ +- `src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md` +- `src/bindings/wasix-rust/THIRD_PARTY_NOTICES.md` + +Shared PostgreSQL source pins, third-party source pins, and extension metadata +are maintained in `src/postgres/versions/18/`, `src/sources/third-party/`, and +`src/extensions/`. Generated release artifacts must include the notices for +every product they ship. -Those bundled assets are covered by their upstream Apache-2.0 and PostgreSQL -license terms. +- PostgreSQL license: https://www.postgresql.org/about/licence/ +- ICU / Unicode License v3: https://github.com/unicode-org/icu/blob/main/LICENSE diff --git a/assets/extensions.smoke.toml b/assets/extensions.smoke.toml deleted file mode 100644 index d2ae5dcf..00000000 --- a/assets/extensions.smoke.toml +++ /dev/null @@ -1,260 +0,0 @@ -format-version = 1 - -[[extensions]] -id = "age" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "amcheck" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "auto_explain" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "bloom" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "btree_gin" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "btree_gist" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "citext" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "cube" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "dict_int" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "dict_xsyn" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "earthdistance" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "file_fdw" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "fuzzystrmatch" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "hstore" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "intarray" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "isn" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "lo" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "ltree" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pageinspect" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_buffercache" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_freespacemap" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_hashids" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_ivm" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_surgery" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_textsearch" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_trgm" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_uuidv7" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_visibility" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pg_walinspect" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "pgtap" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "seg" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "tablefunc" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "tcn" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "tsm_system_rows" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "tsm_system_time" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "unaccent" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "not-run" - -[[extensions]] -id = "vector" -direct = "passed" -server = "passed" -restart = "passed" -dump-restore = "passed" diff --git a/assets/generated/asset-inputs.sha256 b/assets/generated/asset-inputs.sha256 deleted file mode 100644 index b5d018cd..00000000 --- a/assets/generated/asset-inputs.sha256 +++ /dev/null @@ -1 +0,0 @@ -3b74fc87331f910141c8a76224451a6c1b1b8e6936f233b04b0b65a0d6d991d7 diff --git a/assets/generated/extensions.build-plan.json b/assets/generated/extensions.build-plan.json deleted file mode 100644 index e72df8b5..00000000 --- a/assets/generated/extensions.build-plan.json +++ /dev/null @@ -1,1320 +0,0 @@ -{ - "format-version": 1, - "generated-from": [ - { - "name": "extension-catalog", - "path": "assets/generated/extensions.catalog.json" - }, - { - "name": "promotion-config", - "path": "assets/extensions.promoted.toml" - }, - { - "name": "asset-manifest-evidence", - "path": "target/pglite-oxide/assets/manifest.json" - } - ], - "extensions": [ - { - "id": "age", - "sql-name": "age", - "display-name": "Apache AGE", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/age", - "make-args": [ - "SIZEOF_DATUM=4" - ], - "module-file": "age.so", - "archive": "extensions/age.tar.zst", - "control-file": "assets/checkouts/age/age.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "ag_catalog", - "load-sql": [ - "LOAD 'age';" - ], - "post-create-sql": [ - "SET search_path = ag_catalog, \"$user\", public;" - ], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/age.test.ts" - ] - }, - { - "id": "amcheck", - "sql-name": "amcheck", - "display-name": "amcheck", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/amcheck", - "contrib-dir": "amcheck", - "module-file": "amcheck.so", - "archive": "extensions/amcheck.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/amcheck/amcheck.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/amcheck.test.js" - ] - }, - { - "id": "auto_explain", - "sql-name": "auto_explain", - "display-name": "auto_explain", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/auto_explain", - "contrib-dir": "auto_explain", - "module-file": "auto_explain.so", - "archive": "extensions/auto_explain.tar.zst", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": false, - "load-sql": [ - "LOAD 'auto_explain';", - "SET auto_explain.log_min_duration = '0';", - "SET auto_explain.log_analyze = 'true';", - "SET auto_explain.log_level = 'NOTICE';" - ], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/auto_explain.test.js" - ] - }, - { - "id": "bloom", - "sql-name": "bloom", - "display-name": "bloom", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/bloom", - "contrib-dir": "bloom", - "module-file": "bloom.so", - "archive": "extensions/bloom.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/bloom/bloom.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/bloom.test.js" - ] - }, - { - "id": "btree_gin", - "sql-name": "btree_gin", - "display-name": "btree_gin", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/btree_gin", - "contrib-dir": "btree_gin", - "module-file": "btree_gin.so", - "archive": "extensions/btree_gin.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gin/btree_gin.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gin.test.js" - ] - }, - { - "id": "btree_gist", - "sql-name": "btree_gist", - "display-name": "btree_gist", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/btree_gist", - "contrib-dir": "btree_gist", - "module-file": "btree_gist.so", - "archive": "extensions/btree_gist.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gist/btree_gist.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gist.test.js" - ] - }, - { - "id": "citext", - "sql-name": "citext", - "display-name": "citext", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/citext", - "contrib-dir": "citext", - "module-file": "citext.so", - "archive": "extensions/citext.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/citext/citext.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/citext.test.js" - ] - }, - { - "id": "cube", - "sql-name": "cube", - "display-name": "cube", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/cube", - "contrib-dir": "cube", - "module-file": "cube.so", - "archive": "extensions/cube.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/cube/cube.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/cube.test.js" - ] - }, - { - "id": "dict_int", - "sql-name": "dict_int", - "display-name": "dict_int", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/dict_int", - "contrib-dir": "dict_int", - "module-file": "dict_int.so", - "archive": "extensions/dict_int.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/dict_int/dict_int.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_int.test.js" - ] - }, - { - "id": "dict_xsyn", - "sql-name": "dict_xsyn", - "display-name": "dict_xsyn", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/dict_xsyn", - "contrib-dir": "dict_xsyn", - "module-file": "dict_xsyn.so", - "archive": "extensions/dict_xsyn.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/dict_xsyn/dict_xsyn.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_xsyn.test.ts" - ] - }, - { - "id": "earthdistance", - "sql-name": "earthdistance", - "display-name": "earthdistance", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/earthdistance", - "contrib-dir": "earthdistance", - "module-file": "earthdistance.so", - "archive": "extensions/earthdistance.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/earthdistance/earthdistance.control", - "stable": true, - "dependencies": [ - "cube" - ], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/earthdistance.test.js" - ] - }, - { - "id": "file_fdw", - "sql-name": "file_fdw", - "display-name": "file_fdw", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/file_fdw", - "contrib-dir": "file_fdw", - "module-file": "file_fdw.so", - "archive": "extensions/file_fdw.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/file_fdw/file_fdw.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/file_fdw.test.js" - ] - }, - { - "id": "fuzzystrmatch", - "sql-name": "fuzzystrmatch", - "display-name": "fuzzystrmatch", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch", - "contrib-dir": "fuzzystrmatch", - "module-file": "fuzzystrmatch.so", - "archive": "extensions/fuzzystrmatch.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch/fuzzystrmatch.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/fuzzystrmatch.test.js" - ] - }, - { - "id": "hstore", - "sql-name": "hstore", - "display-name": "hstore", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/hstore", - "contrib-dir": "hstore", - "module-file": "hstore.so", - "archive": "extensions/hstore.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/hstore/hstore.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/hstore.test.js" - ] - }, - { - "id": "intarray", - "sql-name": "intarray", - "display-name": "intarray", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/intarray", - "contrib-dir": "intarray", - "module-file": "_int.so", - "archive": "extensions/intarray.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/intarray/intarray.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/intarray.test.js" - ] - }, - { - "id": "isn", - "sql-name": "isn", - "display-name": "isn", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/isn", - "contrib-dir": "isn", - "module-file": "isn.so", - "archive": "extensions/isn.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/isn/isn.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/isn.test.js" - ] - }, - { - "id": "lo", - "sql-name": "lo", - "display-name": "lo", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/lo", - "contrib-dir": "lo", - "module-file": "lo.so", - "archive": "extensions/lo.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/lo/lo.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/lo.test.js" - ] - }, - { - "id": "ltree", - "sql-name": "ltree", - "display-name": "ltree", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/ltree", - "contrib-dir": "ltree", - "module-file": "ltree.so", - "archive": "extensions/ltree.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/ltree/ltree.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/ltree.test.js" - ] - }, - { - "id": "pageinspect", - "sql-name": "pageinspect", - "display-name": "pageinspect", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pageinspect", - "contrib-dir": "pageinspect", - "module-file": "pageinspect.so", - "archive": "extensions/pageinspect.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pageinspect/pageinspect.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pageinspect.test.js" - ] - }, - { - "id": "pg_buffercache", - "sql-name": "pg_buffercache", - "display-name": "pg_buffercache", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_buffercache", - "contrib-dir": "pg_buffercache", - "module-file": "pg_buffercache.so", - "archive": "extensions/pg_buffercache.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_buffercache/pg_buffercache.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_buffercache.test.js" - ] - }, - { - "id": "pg_freespacemap", - "sql-name": "pg_freespacemap", - "display-name": "pg_freespacemap", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap", - "contrib-dir": "pg_freespacemap", - "module-file": "pg_freespacemap.so", - "archive": "extensions/pg_freespacemap.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap/pg_freespacemap.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_freespacemap.test.ts" - ] - }, - { - "id": "pg_hashids", - "sql-name": "pg_hashids", - "display-name": "pg_hashids", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pg_hashids", - "module-file": "pg_hashids.so", - "archive": "extensions/pg_hashids.tar.zst", - "control-file": "assets/checkouts/pg_hashids/pg_hashids.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_hashids.test.ts" - ] - }, - { - "id": "pg_ivm", - "sql-name": "pg_ivm", - "display-name": "pg_ivm", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pg_ivm", - "module-file": "pg_ivm.so", - "archive": "extensions/pg_ivm.tar.zst", - "control-file": "assets/checkouts/pg_ivm/pg_ivm.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_ivm.test.ts" - ] - }, - { - "id": "pg_surgery", - "sql-name": "pg_surgery", - "display-name": "pg_surgery", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_surgery", - "contrib-dir": "pg_surgery", - "module-file": "pg_surgery.so", - "archive": "extensions/pg_surgery.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_surgery/pg_surgery.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_surgery.test.js" - ] - }, - { - "id": "pg_textsearch", - "sql-name": "pg_textsearch", - "display-name": "pg_textsearch", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pg_textsearch", - "module-file": "pg_textsearch.so", - "archive": "extensions/pg_textsearch.tar.zst", - "control-file": "assets/checkouts/pg_textsearch/pg_textsearch.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_textsearch.test.ts" - ] - }, - { - "id": "pg_trgm", - "sql-name": "pg_trgm", - "display-name": "pg_trgm", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_trgm", - "contrib-dir": "pg_trgm", - "module-file": "pg_trgm.so", - "archive": "extensions/pg_trgm.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_trgm/pg_trgm.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_trgm.test.js" - ] - }, - { - "id": "pg_uuidv7", - "sql-name": "pg_uuidv7", - "display-name": "pg_uuidv7", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pg_uuidv7", - "module-file": "pg_uuidv7.so", - "archive": "extensions/pg_uuidv7.tar.zst", - "control-file": "assets/checkouts/pg_uuidv7/pg_uuidv7.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_uuidv7.test.ts" - ] - }, - { - "id": "pg_visibility", - "sql-name": "pg_visibility", - "display-name": "pg_visibility", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_visibility", - "contrib-dir": "pg_visibility", - "module-file": "pg_visibility.so", - "archive": "extensions/pg_visibility.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_visibility/pg_visibility.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_visibility.test.js" - ] - }, - { - "id": "pg_walinspect", - "sql-name": "pg_walinspect", - "display-name": "pg_walinspect", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/pg_walinspect", - "contrib-dir": "pg_walinspect", - "module-file": "pg_walinspect.so", - "archive": "extensions/pg_walinspect.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_walinspect/pg_walinspect.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_walinspect.test.js" - ] - }, - { - "id": "pgtap", - "sql-name": "pgtap", - "display-name": "pgtap", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pgtap", - "archive": "extensions/pgtap.tar.zst", - "control-file": "assets/checkouts/pgtap/pgtap.control", - "stable": true, - "dependencies": [ - "plpgsql" - ], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pgtap.test.ts" - ] - }, - { - "id": "seg", - "sql-name": "seg", - "display-name": "seg", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/seg", - "contrib-dir": "seg", - "module-file": "seg.so", - "archive": "extensions/seg.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/seg/seg.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/seg.test.js" - ] - }, - { - "id": "tablefunc", - "sql-name": "tablefunc", - "display-name": "tablefunc", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/tablefunc", - "contrib-dir": "tablefunc", - "module-file": "tablefunc.so", - "archive": "extensions/tablefunc.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/tablefunc/tablefunc.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tablefunc.test.js" - ] - }, - { - "id": "tcn", - "sql-name": "tcn", - "display-name": "tcn", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/tcn", - "contrib-dir": "tcn", - "module-file": "tcn.so", - "archive": "extensions/tcn.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/tcn/tcn.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tcn.test.js" - ] - }, - { - "id": "tsm_system_rows", - "sql-name": "tsm_system_rows", - "display-name": "tsm_system_rows", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows", - "contrib-dir": "tsm_system_rows", - "module-file": "tsm_system_rows.so", - "archive": "extensions/tsm_system_rows.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows/tsm_system_rows.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_rows.test.js" - ] - }, - { - "id": "tsm_system_time", - "sql-name": "tsm_system_time", - "display-name": "tsm_system_time", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/tsm_system_time", - "contrib-dir": "tsm_system_time", - "module-file": "tsm_system_time.so", - "archive": "extensions/tsm_system_time.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_time/tsm_system_time.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_time.test.js" - ] - }, - { - "id": "unaccent", - "sql-name": "unaccent", - "display-name": "unaccent", - "source-kind": "postgres-contrib", - "build-kind": "postgres-contrib", - "source-dir": "assets/checkouts/postgres-pglite/contrib/unaccent", - "contrib-dir": "unaccent", - "module-file": "unaccent.so", - "archive": "extensions/unaccent.tar.zst", - "control-file": "assets/checkouts/postgres-pglite/contrib/unaccent/unaccent.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/unaccent.test.js" - ] - }, - { - "id": "vector", - "sql-name": "vector", - "display-name": "pgvector", - "source-kind": "pglite-other-extension", - "build-kind": "pgxs-external", - "source-dir": "assets/checkouts/pgvector", - "module-file": "vector.so", - "archive": "extensions/vector.tar.zst", - "control-file": "assets/checkouts/pgvector/vector.control", - "stable": true, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "passed" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pgvector.test.ts" - ] - } - ] -} diff --git a/assets/generated/extensions.catalog.json b/assets/generated/extensions.catalog.json deleted file mode 100644 index 7f19ee16..00000000 --- a/assets/generated/extensions.catalog.json +++ /dev/null @@ -1,2262 +0,0 @@ -{ - "format-version": 1, - "generated-from": [ - { - "name": "pglite-repl-exports", - "path": "assets/checkouts/pglite/docs/repl/allExtensions.ts" - }, - { - "name": "pglite-docs-catalog", - "path": "assets/checkouts/pglite/docs/extensions/extensions.data.ts" - }, - { - "name": "pglite-package-exports", - "path": "assets/checkouts/pglite/packages/pglite/package.json" - }, - { - "name": "pglite-contrib-modules", - "path": "assets/checkouts/pglite/packages/pglite/src/contrib" - }, - { - "name": "postgres-contrib", - "path": "assets/checkouts/postgres-pglite/contrib" - }, - { - "name": "postgres-pglite-other-extensions", - "path": "assets/checkouts/postgres-pglite/pglite/other_extensions" - }, - { - "name": "extension-promotion-config", - "path": "assets/extensions.promoted.toml" - }, - { - "name": "extension-smoke-evidence", - "path": "assets/extensions.smoke.toml" - }, - { - "name": "asset-manifest-evidence", - "path": "target/pglite-oxide/assets/manifest.json" - } - ], - "extensions": [ - { - "id": "age", - "sql-name": "age", - "rust-constant": "AGE", - "display-name": "Apache AGE", - "source-kind": "pglite-other-extension", - "pglite-import-name": "age", - "pglite-import-path": "@electric-sql/pglite/age", - "package-export": "./age", - "tags": [ - "postgres extension" - ], - "bundle-size": 141551, - "control-file": "assets/checkouts/age/age.control", - "control": { - "default-version": "1.7.0", - "module-pathname": "$libdir/age", - "requires": [], - "schema": "ag_catalog" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "ag_catalog", - "load-sql": [ - "LOAD 'age';" - ], - "post-create-sql": [ - "SET search_path = ag_catalog, \"$user\", public;" - ], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/age.test.ts" - ], - "native-module-file": "age.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/age.tar.zst", - "module-sha256": "db961a6cd82c7712c1270c4fa6ccb2dccdb24d83b6a3d2781af2d62e8685de0a" - }, - "notes": [ - "postgres-pglite submodule https://github.com/apache/age.git pinned at e1467f12e0b1d15dd35d3ab93f057a7112d425b8" - ] - }, - { - "id": "amcheck", - "sql-name": "amcheck", - "rust-constant": "AMCHECK", - "display-name": "amcheck", - "source-kind": "postgres-contrib", - "pglite-import-name": "amcheck", - "pglite-import-path": "@electric-sql/pglite/contrib/amcheck", - "package-export": "./contrib/amcheck", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 18815, - "control-file": "assets/checkouts/postgres-pglite/contrib/amcheck/amcheck.control", - "control": { - "default-version": "1.4", - "module-pathname": "$libdir/amcheck", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/amcheck.test.js" - ], - "native-module-file": "amcheck.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/amcheck.tar.zst", - "module-sha256": "46f8569941bd68b3660bf0239017c5818f13d0d4b6109b25ef701a6886a17d45" - }, - "notes": [] - }, - { - "id": "auto_explain", - "sql-name": "auto_explain", - "rust-constant": "AUTO_EXPLAIN", - "display-name": "auto_explain", - "source-kind": "postgres-contrib", - "pglite-import-name": "auto_explain", - "pglite-import-path": "@electric-sql/pglite/contrib/auto_explain", - "package-export": "./contrib/auto_explain", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 3125, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": false, - "load-sql": [ - "LOAD 'auto_explain';", - "SET auto_explain.log_min_duration = '0';", - "SET auto_explain.log_analyze = 'true';", - "SET auto_explain.log_level = 'NOTICE';" - ], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/auto_explain.test.js" - ], - "native-module-file": "auto_explain.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/auto_explain.tar.zst", - "module-sha256": "cd4e51ac361ee24a9e7ce798d44be00994db625deccda7f510d57b486e072354" - }, - "notes": [] - }, - { - "id": "bloom", - "sql-name": "bloom", - "rust-constant": "BLOOM", - "display-name": "bloom", - "source-kind": "postgres-contrib", - "pglite-import-name": "bloom", - "pglite-import-path": "@electric-sql/pglite/contrib/bloom", - "package-export": "./contrib/bloom", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 6197, - "control-file": "assets/checkouts/postgres-pglite/contrib/bloom/bloom.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/bloom", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/bloom.test.js" - ], - "native-module-file": "bloom.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/bloom.tar.zst", - "module-sha256": "62b098457eca4dc5559adda558e090379949948a179b25840931d86dcd0f0461" - }, - "notes": [] - }, - { - "id": "btree_gin", - "sql-name": "btree_gin", - "rust-constant": "BTREE_GIN", - "display-name": "btree_gin", - "source-kind": "postgres-contrib", - "pglite-import-name": "btree_gin", - "pglite-import-path": "@electric-sql/pglite/contrib/btree_gin", - "package-export": "./contrib/btree_gin", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 6347, - "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gin/btree_gin.control", - "control": { - "default-version": "1.3", - "module-pathname": "$libdir/btree_gin", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gin.test.js" - ], - "native-module-file": "btree_gin.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/btree_gin.tar.zst", - "module-sha256": "3ef71d9930b3bba882c67a8981d45a72c91d7ac547cc476ffef9539bc30cb678" - }, - "notes": [] - }, - { - "id": "btree_gist", - "sql-name": "btree_gist", - "rust-constant": "BTREE_GIST", - "display-name": "btree_gist", - "source-kind": "postgres-contrib", - "pglite-import-name": "btree_gist", - "pglite-import-path": "@electric-sql/pglite/contrib/btree_gist", - "package-export": "./contrib/btree_gist", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 24181, - "control-file": "assets/checkouts/postgres-pglite/contrib/btree_gist/btree_gist.control", - "control": { - "default-version": "1.7", - "module-pathname": "$libdir/btree_gist", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/btree_gist.test.js" - ], - "native-module-file": "btree_gist.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/btree_gist.tar.zst", - "module-sha256": "1074bc00fef8fa8631e5b01bd1811b15cba12e203cc5fff6f58f43dbba97fd87" - }, - "notes": [] - }, - { - "id": "citext", - "sql-name": "citext", - "rust-constant": "CITEXT", - "display-name": "citext", - "source-kind": "postgres-contrib", - "pglite-import-name": "citext", - "pglite-import-path": "@electric-sql/pglite/contrib/citext", - "package-export": "./contrib/citext", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 4983, - "control-file": "assets/checkouts/postgres-pglite/contrib/citext/citext.control", - "control": { - "default-version": "1.6", - "module-pathname": "$libdir/citext", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/citext.test.js" - ], - "native-module-file": "citext.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/citext.tar.zst", - "module-sha256": "63f974647782a25b233ba787d7fcbab24a672234d534c5f25425878e1cdf22ec" - }, - "notes": [] - }, - { - "id": "cube", - "sql-name": "cube", - "rust-constant": "CUBE", - "display-name": "cube", - "source-kind": "postgres-contrib", - "pglite-import-name": "cube", - "pglite-import-path": "@electric-sql/pglite/contrib/cube", - "package-export": "./contrib/cube", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 15104, - "control-file": "assets/checkouts/postgres-pglite/contrib/cube/cube.control", - "control": { - "default-version": "1.5", - "module-pathname": "$libdir/cube", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/cube.test.js" - ], - "native-module-file": "cube.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/cube.tar.zst", - "module-sha256": "e63cd4629af7d6a94dd5c4443c828211162bfa0321b5f4c8b136e00d1fe4fe13" - }, - "notes": [] - }, - { - "id": "dict_int", - "sql-name": "dict_int", - "rust-constant": "DICT_INT", - "display-name": "dict_int", - "source-kind": "postgres-contrib", - "pglite-import-name": "dict_int", - "pglite-import-path": "@electric-sql/pglite/contrib/dict_int", - "package-export": "./contrib/dict_int", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1361, - "control-file": "assets/checkouts/postgres-pglite/contrib/dict_int/dict_int.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/dict_int", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_int.test.js" - ], - "native-module-file": "dict_int.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/dict_int.tar.zst", - "module-sha256": "44955defc617a97878d7ad47fb37eb2a6b4d62d6566256f3852d468afc14a009" - }, - "notes": [] - }, - { - "id": "dict_xsyn", - "sql-name": "dict_xsyn", - "rust-constant": "DICT_XSYN", - "display-name": "dict_xsyn", - "source-kind": "postgres-contrib", - "pglite-import-name": "dict_xsyn", - "pglite-import-path": "@electric-sql/pglite/contrib/dict_xsyn", - "package-export": "./contrib/dict_xsyn", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1948, - "control-file": "assets/checkouts/postgres-pglite/contrib/dict_xsyn/dict_xsyn.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/dict_xsyn", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/dict_xsyn.test.ts" - ], - "native-module-file": "dict_xsyn.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/dict_xsyn.tar.zst", - "module-sha256": "ed92af122102ebcf520801456bfb90231dcce50495aa59cba7a3464fceff77f9" - }, - "notes": [] - }, - { - "id": "earthdistance", - "sql-name": "earthdistance", - "rust-constant": "EARTHDISTANCE", - "display-name": "earthdistance", - "source-kind": "postgres-contrib", - "pglite-import-name": "earthdistance", - "pglite-import-path": "@electric-sql/pglite/contrib/earthdistance", - "package-export": "./contrib/earthdistance", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 2220, - "control-file": "assets/checkouts/postgres-pglite/contrib/earthdistance/earthdistance.control", - "control": { - "default-version": "1.2", - "module-pathname": "$libdir/earthdistance", - "requires": [ - "cube" - ], - "relocatable": "true" - }, - "dependencies": [ - "cube" - ], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/earthdistance.test.js" - ], - "native-module-file": "earthdistance.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/earthdistance.tar.zst", - "module-sha256": "a2fd0268a07e14df2b8cd0624f4c37d806aad5ce0a82f40bb8622cb1500af5c4" - }, - "notes": [] - }, - { - "id": "file_fdw", - "sql-name": "file_fdw", - "rust-constant": "FILE_FDW", - "display-name": "file_fdw", - "source-kind": "postgres-contrib", - "pglite-import-name": "file_fdw", - "pglite-import-path": "@electric-sql/pglite/contrib/file_fdw", - "package-export": "./contrib/file_fdw", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 4467, - "control-file": "assets/checkouts/postgres-pglite/contrib/file_fdw/file_fdw.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/file_fdw", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/file_fdw.test.js" - ], - "native-module-file": "file_fdw.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/file_fdw.tar.zst", - "module-sha256": "7bd1a071edb2596f389ebb8c0a486da368c7e00191b0987c536fadf061256d50" - }, - "notes": [] - }, - { - "id": "fuzzystrmatch", - "sql-name": "fuzzystrmatch", - "rust-constant": "FUZZYSTRMATCH", - "display-name": "fuzzystrmatch", - "source-kind": "postgres-contrib", - "pglite-import-name": "fuzzystrmatch", - "pglite-import-path": "@electric-sql/pglite/contrib/fuzzystrmatch", - "package-export": "./contrib/fuzzystrmatch", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 12026, - "control-file": "assets/checkouts/postgres-pglite/contrib/fuzzystrmatch/fuzzystrmatch.control", - "control": { - "default-version": "1.2", - "module-pathname": "$libdir/fuzzystrmatch", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/fuzzystrmatch.test.js" - ], - "native-module-file": "fuzzystrmatch.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/fuzzystrmatch.tar.zst", - "module-sha256": "329cbb5f46e13529987934094660b827e65552cbeaa5f2b2c0405a193ae75687" - }, - "notes": [] - }, - { - "id": "hstore", - "sql-name": "hstore", - "rust-constant": "HSTORE", - "display-name": "hstore", - "source-kind": "postgres-contrib", - "pglite-import-name": "hstore", - "pglite-import-path": "@electric-sql/pglite/contrib/hstore", - "package-export": "./contrib/hstore", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 21380, - "control-file": "assets/checkouts/postgres-pglite/contrib/hstore/hstore.control", - "control": { - "default-version": "1.8", - "module-pathname": "$libdir/hstore", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/hstore.test.js" - ], - "native-module-file": "hstore.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/hstore.tar.zst", - "module-sha256": "51359ef4a23523ccf0cedcf26ac47e3594f6b22bedc8d6d5d9a41dce066e0ba6" - }, - "notes": [] - }, - { - "id": "intarray", - "sql-name": "intarray", - "rust-constant": "INTARRAY", - "display-name": "intarray", - "source-kind": "postgres-contrib", - "pglite-import-name": "intarray", - "pglite-import-path": "@electric-sql/pglite/contrib/intarray", - "package-export": "./contrib/intarray", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 14712, - "control-file": "assets/checkouts/postgres-pglite/contrib/intarray/intarray.control", - "control": { - "default-version": "1.5", - "module-pathname": "$libdir/_int", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/intarray.test.js" - ], - "native-module-file": "_int.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/intarray.tar.zst", - "module-sha256": "77abeef93372180e39e969a955f7c8201e34b08319ea429e7b07703970c8cc4e" - }, - "notes": [] - }, - { - "id": "isn", - "sql-name": "isn", - "rust-constant": "ISN", - "display-name": "isn", - "source-kind": "postgres-contrib", - "pglite-import-name": "isn", - "pglite-import-path": "@electric-sql/pglite/contrib/isn", - "package-export": "./contrib/isn", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 31417, - "control-file": "assets/checkouts/postgres-pglite/contrib/isn/isn.control", - "control": { - "default-version": "1.2", - "module-pathname": "$libdir/isn", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/isn.test.js" - ], - "native-module-file": "isn.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/isn.tar.zst", - "module-sha256": "cc0e86b65c59209df4e7c12fd8b2fedfd52de08117f597c73d5d04ef989275e5" - }, - "notes": [] - }, - { - "id": "lo", - "sql-name": "lo", - "rust-constant": "LO", - "display-name": "lo", - "source-kind": "postgres-contrib", - "pglite-import-name": "lo", - "pglite-import-path": "@electric-sql/pglite/contrib/lo", - "package-export": "./contrib/lo", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1822, - "control-file": "assets/checkouts/postgres-pglite/contrib/lo/lo.control", - "control": { - "default-version": "1.1", - "module-pathname": "$libdir/lo", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/lo.test.js" - ], - "native-module-file": "lo.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/lo.tar.zst", - "module-sha256": "36a2862ccc365f83ad5808eaa496e79d34d006d05b353bd1121b04246ea78ce8" - }, - "notes": [] - }, - { - "id": "ltree", - "sql-name": "ltree", - "rust-constant": "LTREE", - "display-name": "ltree", - "source-kind": "postgres-contrib", - "pglite-import-name": "ltree", - "pglite-import-path": "@electric-sql/pglite/contrib/ltree", - "package-export": "./contrib/ltree", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 19553, - "control-file": "assets/checkouts/postgres-pglite/contrib/ltree/ltree.control", - "control": { - "default-version": "1.3", - "module-pathname": "$libdir/ltree", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/ltree.test.js" - ], - "native-module-file": "ltree.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/ltree.tar.zst", - "module-sha256": "56507f0debc47adda14f5d35a3265f8f00f2471c023ecc1fbaffc704779c321e" - }, - "notes": [] - }, - { - "id": "pageinspect", - "sql-name": "pageinspect", - "rust-constant": "PAGEINSPECT", - "display-name": "pageinspect", - "source-kind": "postgres-contrib", - "pglite-import-name": "pageinspect", - "pglite-import-path": "@electric-sql/pglite/contrib/pageinspect", - "package-export": "./contrib/pageinspect", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 15923, - "control-file": "assets/checkouts/postgres-pglite/contrib/pageinspect/pageinspect.control", - "control": { - "default-version": "1.12", - "module-pathname": "$libdir/pageinspect", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pageinspect.test.js" - ], - "native-module-file": "pageinspect.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pageinspect.tar.zst", - "module-sha256": "9c5fc1aa2243810a123d2868c2405227d7d25d9866e273b3a6f24797115f8322" - }, - "notes": [] - }, - { - "id": "pg_buffercache", - "sql-name": "pg_buffercache", - "rust-constant": "PG_BUFFERCACHE", - "display-name": "pg_buffercache", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_buffercache", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_buffercache", - "package-export": "./contrib/pg_buffercache", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 3133, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_buffercache/pg_buffercache.control", - "control": { - "default-version": "1.5", - "module-pathname": "$libdir/pg_buffercache", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_buffercache.test.js" - ], - "native-module-file": "pg_buffercache.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_buffercache.tar.zst", - "module-sha256": "3ac0bed8c504ed7af629f32454e323924bb28fcaec28da403dd8e4fc50e0f2e6" - }, - "notes": [] - }, - { - "id": "pg_freespacemap", - "sql-name": "pg_freespacemap", - "rust-constant": "PG_FREESPACEMAP", - "display-name": "pg_freespacemap", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_freespacemap", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_freespacemap", - "package-export": "./contrib/pg_freespacemap", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1485, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_freespacemap/pg_freespacemap.control", - "control": { - "default-version": "1.2", - "module-pathname": "$libdir/pg_freespacemap", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_freespacemap.test.ts" - ], - "native-module-file": "pg_freespacemap.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_freespacemap.tar.zst", - "module-sha256": "38809d986cd421965060d030a59102aa44dbe1707a90ad74e322ce41318b4ec7" - }, - "notes": [] - }, - { - "id": "pg_hashids", - "sql-name": "pg_hashids", - "rust-constant": "PG_HASHIDS", - "display-name": "pg_hashids", - "source-kind": "pglite-other-extension", - "pglite-import-name": "pg_hashids", - "pglite-import-path": "@electric-sql/pglite/pg_hashids", - "package-export": "./pg_hashids", - "tags": [ - "postgres extension" - ], - "bundle-size": 4212, - "control-file": "assets/checkouts/pg_hashids/pg_hashids.control", - "control": { - "default-version": "1.3", - "module-pathname": "$libdir/pg_hashids", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_hashids.test.ts" - ], - "native-module-file": "pg_hashids.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_hashids.tar.zst", - "module-sha256": "b7bdf822c03ff5fcba6ba2a89f6dcaab90d30bf1c531062165bb3d125b4b95ee" - }, - "notes": [ - "postgres-pglite submodule https://github.com/iCyberon/pg_hashids pinned at 8c404dd86408f3a987a3ff6825ac7e42bd618b98" - ] - }, - { - "id": "pg_ivm", - "sql-name": "pg_ivm", - "rust-constant": "PG_IVM", - "display-name": "pg_ivm", - "source-kind": "pglite-other-extension", - "pglite-import-name": "pg_ivm", - "pglite-import-path": "@electric-sql/pglite/pg_ivm", - "package-export": "./pg_ivm", - "tags": [ - "postgres extension" - ], - "bundle-size": 24865, - "control-file": "assets/checkouts/pg_ivm/pg_ivm.control", - "control": { - "default-version": "1.13", - "module-pathname": "$libdir/pg_ivm", - "requires": [], - "relocatable": "false", - "schema": "pg_catalog" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_ivm.test.ts" - ], - "native-module-file": "pg_ivm.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_ivm.tar.zst", - "module-sha256": "7aefdd437c0a3ee8b4f246b9fadf01734ad991c505dff8f6d004ac6e74a9154a" - }, - "notes": [ - "postgres-pglite submodule https://github.com/sraoss/pg_ivm.git pinned at b66487f7a6f8deee3998e858d773e19923e4bd4b" - ] - }, - { - "id": "pg_surgery", - "sql-name": "pg_surgery", - "rust-constant": "PG_SURGERY", - "display-name": "pg_surgery", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_surgery", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_surgery", - "package-export": "./contrib/pg_surgery", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 2635, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_surgery/pg_surgery.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/pg_surgery", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_surgery.test.js" - ], - "native-module-file": "pg_surgery.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_surgery.tar.zst", - "module-sha256": "dd1b8daebf7da458eec04b9ba5733913fd1f0d9bad41447e7ee3e0d3611f0ae2" - }, - "notes": [] - }, - { - "id": "pg_textsearch", - "sql-name": "pg_textsearch", - "rust-constant": "PG_TEXTSEARCH", - "display-name": "pg_textsearch", - "source-kind": "pglite-other-extension", - "pglite-import-name": "pg_textsearch", - "pglite-import-path": "@electric-sql/pglite/pg_textsearch", - "package-export": "./pg_textsearch", - "tags": [ - "postgres extension", - "experimental" - ], - "bundle-size": 55062, - "control-file": "assets/checkouts/pg_textsearch/pg_textsearch.control", - "control": { - "default-version": "0.5.1", - "module-pathname": "$libdir/pg_textsearch", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_textsearch.test.ts" - ], - "native-module-file": "pg_textsearch.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_textsearch.tar.zst", - "module-sha256": "70566a2191130c20f703aa20016dcc0a2c90312d6b05b0c04a5c8fc4a1e1b896" - }, - "notes": [ - "postgres-pglite submodule https://github.com/timescale/pg_textsearch.git pinned at 5c5147bf2610d786f1bd139951b9fb7fe4ac68fb" - ] - }, - { - "id": "pg_trgm", - "sql-name": "pg_trgm", - "rust-constant": "PG_TRGM", - "display-name": "pg_trgm", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_trgm", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_trgm", - "package-export": "./contrib/pg_trgm", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 16208, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_trgm/pg_trgm.control", - "control": { - "default-version": "1.6", - "module-pathname": "$libdir/pg_trgm", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_trgm.test.js" - ], - "native-module-file": "pg_trgm.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_trgm.tar.zst", - "module-sha256": "997b99a690c538d0034997c3f9224c2d6e108438e679535a5fa67ecaec476863" - }, - "notes": [] - }, - { - "id": "pg_uuidv7", - "sql-name": "pg_uuidv7", - "rust-constant": "PG_UUIDV7", - "display-name": "pg_uuidv7", - "source-kind": "pglite-other-extension", - "pglite-import-name": "pg_uuidv7", - "pglite-import-path": "@electric-sql/pglite/pg_uuidv7", - "package-export": "./pg_uuidv7", - "tags": [ - "postgres extension" - ], - "bundle-size": 1522, - "control-file": "assets/checkouts/pg_uuidv7/pg_uuidv7.control", - "control": { - "default-version": "1.7", - "module-pathname": "$libdir/pg_uuidv7", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pg_uuidv7.test.ts" - ], - "native-module-file": "pg_uuidv7.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_uuidv7.tar.zst", - "module-sha256": "96e9903d134288ca7f7f5e094dd3c09ec83bd6ce5dc2d87c3d93732c4b4040b4" - }, - "notes": [ - "postgres-pglite submodule https://github.com/fboulnois/pg_uuidv7/ pinned at c707aae2411181be4802f5fa565b44d9c0bcbc29" - ] - }, - { - "id": "pg_visibility", - "sql-name": "pg_visibility", - "rust-constant": "PG_VISIBILITY", - "display-name": "pg_visibility", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_visibility", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_visibility", - "package-export": "./contrib/pg_visibility", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 4159, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_visibility/pg_visibility.control", - "control": { - "default-version": "1.2", - "module-pathname": "$libdir/pg_visibility", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_visibility.test.js" - ], - "native-module-file": "pg_visibility.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_visibility.tar.zst", - "module-sha256": "0c5fcce9a85cb57422a7c201352fe30848ba8d67714e49283515f343c41401d6" - }, - "notes": [] - }, - { - "id": "pg_walinspect", - "sql-name": "pg_walinspect", - "rust-constant": "PG_WALINSPECT", - "display-name": "pg_walinspect", - "source-kind": "postgres-contrib", - "pglite-import-name": "pg_walinspect", - "pglite-import-path": "@electric-sql/pglite/contrib/pg_walinspect", - "package-export": "./contrib/pg_walinspect", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 4689, - "control-file": "assets/checkouts/postgres-pglite/contrib/pg_walinspect/pg_walinspect.control", - "control": { - "default-version": "1.1", - "module-pathname": "$libdir/pg_walinspect", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pg_walinspect.test.js" - ], - "native-module-file": "pg_walinspect.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pg_walinspect.tar.zst", - "module-sha256": "7a6ce46998a15d4487b51d3a0c7125f3468372c1e2ddbeb31b66a8ad106a41e8" - }, - "notes": [] - }, - { - "id": "pgcrypto", - "sql-name": "pgcrypto", - "rust-constant": "PGCRYPTO", - "display-name": "pgcrypto", - "source-kind": "postgres-contrib", - "pglite-import-name": "pgcrypto", - "pglite-import-path": "@electric-sql/pglite/contrib/pgcrypto", - "package-export": "./contrib/pgcrypto", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1148162, - "control-file": "assets/checkouts/postgres-pglite/contrib/pgcrypto/pgcrypto.control", - "control": { - "default-version": "1.3", - "module-pathname": "$libdir/pgcrypto", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "not-run", - "server": "not-run", - "restart": "not-run", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/pgcrypto.test.js" - ], - "native-module-file": "pgcrypto.so", - "promotion": { - "configured": true, - "requested": false, - "packaged": false, - "promoted": false, - "stable": false, - "archive": "extensions/pgcrypto.tar.zst", - "blocker": "Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." - }, - "notes": [ - "promotion blocker: Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." - ] - }, - { - "id": "pgtap", - "sql-name": "pgtap", - "rust-constant": "PGTAP", - "display-name": "pgtap", - "source-kind": "pglite-other-extension", - "pglite-import-name": "pgtap", - "pglite-import-path": "@electric-sql/pglite/pgtap", - "package-export": "./pgtap", - "tags": [ - "postgres extension" - ], - "bundle-size": 239428, - "control-file": "assets/checkouts/pgtap/pgtap.control", - "control": { - "default-version": "1.3.5", - "requires": [ - "plpgsql" - ], - "relocatable": "true" - }, - "dependencies": [ - "plpgsql" - ], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pgtap.test.ts" - ], - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/pgtap.tar.zst", - "module-sha256": "" - }, - "notes": [ - "postgres-pglite submodule https://github.com/theory/pgtap.git pinned at b89585a64ffef012ff0f219de9197c669aa8485b" - ] - }, - { - "id": "postgis", - "sql-name": "postgis", - "rust-constant": "POSTGIS", - "display-name": "PostGIS", - "source-kind": "postgis", - "pglite-import-name": "postgis", - "pglite-import-path": "@electric-sql/pglite-postgis", - "tags": [ - "postgres extension", - "experimental" - ], - "bundle-size": 8551161, - "dependencies": [], - "native-dependencies": [], - "load-order": [ - "lib/postgresql/postgis-3.so", - "lib/postgresql/postgis_topology-3.so", - "lib/postgresql/postgis_raster-3.so" - ], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "not-run", - "server": "not-run", - "restart": "not-run", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite-postgis/tests/postgis.test.ts" - ], - "promotion": { - "configured": true, - "requested": false, - "packaged": false, - "promoted": false, - "stable": false, - "archive": "extensions/postgis.tar.zst", - "blocker": "Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." - }, - "notes": [ - "control file unavailable in current checkout; source submodule may not be initialized", - "postgres-pglite submodule https://github.com/postgis/postgis.git pinned at 08d9b9f749fa3531591055db2a736bfb6df47006", - "promotion blocker: Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." - ] - }, - { - "id": "seg", - "sql-name": "seg", - "rust-constant": "SEG", - "display-name": "seg", - "source-kind": "postgres-contrib", - "pglite-import-name": "seg", - "pglite-import-path": "@electric-sql/pglite/contrib/seg", - "package-export": "./contrib/seg", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 10426, - "control-file": "assets/checkouts/postgres-pglite/contrib/seg/seg.control", - "control": { - "default-version": "1.4", - "module-pathname": "$libdir/seg", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/seg.test.js" - ], - "native-module-file": "seg.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/seg.tar.zst", - "module-sha256": "bf0650bbc399138f0b62c25bff3f5f57dc17e216fa0325b5b109eb31015682fe" - }, - "notes": [] - }, - { - "id": "tablefunc", - "sql-name": "tablefunc", - "rust-constant": "TABLEFUNC", - "display-name": "tablefunc", - "source-kind": "postgres-contrib", - "pglite-import-name": "tablefunc", - "pglite-import-path": "@electric-sql/pglite/contrib/tablefunc", - "package-export": "./contrib/tablefunc", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 5824, - "control-file": "assets/checkouts/postgres-pglite/contrib/tablefunc/tablefunc.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/tablefunc", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tablefunc.test.js" - ], - "native-module-file": "tablefunc.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/tablefunc.tar.zst", - "module-sha256": "c15351f6a1dad0b1ff43a20c8966be666a0d4a6dc0ef2d87b9aae8d04671aa11" - }, - "notes": [] - }, - { - "id": "tcn", - "sql-name": "tcn", - "rust-constant": "TCN", - "display-name": "tcn", - "source-kind": "postgres-contrib", - "pglite-import-name": "tcn", - "pglite-import-path": "@electric-sql/pglite/contrib/tcn", - "package-export": "./contrib/tcn", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 1914, - "control-file": "assets/checkouts/postgres-pglite/contrib/tcn/tcn.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/tcn", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tcn.test.js" - ], - "native-module-file": "tcn.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/tcn.tar.zst", - "module-sha256": "ed94b83055df5cb5e64884cc556f76e17ef663fd272adfb4fbcba80f894fb604" - }, - "notes": [] - }, - { - "id": "tsm_system_rows", - "sql-name": "tsm_system_rows", - "rust-constant": "TSM_SYSTEM_ROWS", - "display-name": "tsm_system_rows", - "source-kind": "postgres-contrib", - "pglite-import-name": "tsm_system_rows", - "pglite-import-path": "@electric-sql/pglite/contrib/tsm_system_rows", - "package-export": "./contrib/tsm_system_rows", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 2048, - "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_rows/tsm_system_rows.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/tsm_system_rows", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_rows.test.js" - ], - "native-module-file": "tsm_system_rows.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/tsm_system_rows.tar.zst", - "module-sha256": "d66231a731c3a66e16226ef90023208c8659133c1af5059ddf7d09a29f4b56a3" - }, - "notes": [] - }, - { - "id": "tsm_system_time", - "sql-name": "tsm_system_time", - "rust-constant": "TSM_SYSTEM_TIME", - "display-name": "tsm_system_time", - "source-kind": "postgres-contrib", - "pglite-import-name": "tsm_system_time", - "pglite-import-path": "@electric-sql/pglite/contrib/tsm_system_time", - "package-export": "./contrib/tsm_system_time", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 2099, - "control-file": "assets/checkouts/postgres-pglite/contrib/tsm_system_time/tsm_system_time.control", - "control": { - "default-version": "1.0", - "module-pathname": "$libdir/tsm_system_time", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/tsm_system_time.test.js" - ], - "native-module-file": "tsm_system_time.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/tsm_system_time.tar.zst", - "module-sha256": "c4b0a9770859c988e7c278d8a6518a0e892bcae96cf218c337a0aef188bcb8c8" - }, - "notes": [] - }, - { - "id": "unaccent", - "sql-name": "unaccent", - "rust-constant": "UNACCENT", - "display-name": "unaccent", - "source-kind": "postgres-contrib", - "pglite-import-name": "unaccent", - "pglite-import-path": "@electric-sql/pglite/contrib/unaccent", - "package-export": "./contrib/unaccent", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 9323, - "control-file": "assets/checkouts/postgres-pglite/contrib/unaccent/unaccent.control", - "control": { - "default-version": "1.1", - "module-pathname": "$libdir/unaccent", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/unaccent.test.js" - ], - "native-module-file": "unaccent.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/unaccent.tar.zst", - "module-sha256": "f966e59cf9431a5b880d4a5e091c6a3eb1eac0c84173466631216bc0c8c0ea67" - }, - "notes": [] - }, - { - "id": "uuid_ossp", - "sql-name": "uuid-ossp", - "rust-constant": "UUID_OSSP", - "display-name": "uuid-ossp", - "source-kind": "postgres-contrib", - "pglite-import-name": "uuid_ossp", - "pglite-import-path": "@electric-sql/pglite/contrib/uuid_ossp", - "package-export": "./contrib/uuid_ossp", - "tags": [ - "postgres extension", - "postgres/contrib" - ], - "bundle-size": 17936, - "control-file": "assets/checkouts/postgres-pglite/contrib/uuid-ossp/uuid-ossp.control", - "control": { - "default-version": "1.1", - "module-pathname": "$libdir/uuid-ossp", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "not-run", - "server": "not-run", - "restart": "not-run", - "dump-restore": "not-run" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/contrib/uuid_ossp.test.js" - ], - "native-module-file": "uuid-ossp.so", - "promotion": { - "configured": true, - "requested": false, - "packaged": false, - "promoted": false, - "stable": false, - "archive": "extensions/uuid-ossp.tar.zst", - "blocker": "Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." - }, - "notes": [ - "promotion blocker: Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." - ] - }, - { - "id": "vector", - "sql-name": "vector", - "rust-constant": "VECTOR", - "display-name": "pgvector", - "source-kind": "pglite-other-extension", - "pglite-import-name": "vector", - "pglite-import-path": "@electric-sql/pglite/vector", - "package-export": "./vector", - "tags": [ - "postgres extension" - ], - "bundle-size": 43953, - "control-file": "assets/checkouts/pgvector/vector.control", - "control": { - "default-version": "0.8.2", - "module-pathname": "$libdir/vector", - "requires": [], - "relocatable": "true" - }, - "dependencies": [], - "native-dependencies": [], - "load-order": [], - "lifecycle": { - "create-extension": true, - "create-schema": "pg_catalog", - "load-sql": [], - "post-create-sql": [], - "startup-config": [], - "preload-required": false, - "restart-required": false, - "shared-memory-required": false - }, - "smoke": { - "direct": "passed", - "server": "passed", - "restart": "passed", - "dump-restore": "passed" - }, - "tests": [ - "assets/checkouts/pglite/packages/pglite/tests/pgvector.test.ts" - ], - "native-module-file": "vector.so", - "promotion": { - "configured": true, - "requested": true, - "packaged": true, - "promoted": true, - "stable": true, - "archive": "extensions/vector.tar.zst", - "module-sha256": "860a9dda5d86bfb5008fa0281a30454b8de6b0754e6fd25be5d1cd6308a36629" - }, - "notes": [ - "postgres-pglite submodule https://github.com/pgvector/pgvector.git pinned at 35ab919bf5da677709b2ebb8be07480bb25e97cf" - ] - } - ] -} diff --git a/assets/generated/pgxs-build.tsv b/assets/generated/pgxs-build.tsv deleted file mode 100644 index 274a9c57..00000000 --- a/assets/generated/pgxs-build.tsv +++ /dev/null @@ -1,8 +0,0 @@ -# id sql_name source_dir module_file archive stable make_args -age age assets/checkouts/age age.so extensions/age.tar.zst true SIZEOF_DATUM=4 -pg_hashids pg_hashids assets/checkouts/pg_hashids pg_hashids.so extensions/pg_hashids.tar.zst true - -pg_ivm pg_ivm assets/checkouts/pg_ivm pg_ivm.so extensions/pg_ivm.tar.zst true - -pg_textsearch pg_textsearch assets/checkouts/pg_textsearch pg_textsearch.so extensions/pg_textsearch.tar.zst true - -pg_uuidv7 pg_uuidv7 assets/checkouts/pg_uuidv7 pg_uuidv7.so extensions/pg_uuidv7.tar.zst true - -pgtap pgtap assets/checkouts/pgtap - extensions/pgtap.tar.zst true - -vector vector assets/checkouts/pgvector vector.so extensions/vector.tar.zst true - diff --git a/assets/sources.toml b/assets/sources.toml deleted file mode 100644 index eabb60fd..00000000 --- a/assets/sources.toml +++ /dev/null @@ -1,88 +0,0 @@ -[toolchain] -wasmer = "7.2.0-alpha.3" -wasmer-wasix = "0.702.0-alpha.3" -wasixcc = "2026-03-02.1" -llvm = "22.1" -docker_image = "ghcr.io/f0rr0/pglite-oxide-wasix-build" -docker_image_digest = "sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b" - -[build] -postgres_prefix = "/" -postgres_pkglibdir = "/lib/postgresql" -postgres_sharedir = "/share/postgresql" -main_flags = ["-fwasm-exceptions"] -extension_flags = ["-fwasm-exceptions", "-fPIC", "-Wl,-shared"] -archive_format = "tar.zst" -deterministic_archives = true - -[[sources]] -name = "pglite" -url = "https://github.com/electric-sql/pglite.git" -branch = "main" -commit = "1337be6e33b7c294f8987c918b1e64d2421365ee" - -[[sources]] -name = "pglite-build" -url = "https://github.com/electric-sql/pglite-build" -branch = "portable" -commit = "c195113dbaf09488f8d5eeb2db91dacd123b74d0" - -[[sources]] -name = "postgres-pglite" -url = "https://github.com/electric-sql/postgres-pglite" -branch = "REL_17_5-pglite" -commit = "01792c31a62b7045eb22e93d7dad022bb64b1184" - -[[sources]] -name = "pgvector" -url = "https://github.com/pgvector/pgvector.git" -branch = "master" -commit = "d238409becebb8172fe696ffa776badfad4b631c" - -[[sources]] -name = "pgtap" -url = "https://github.com/theory/pgtap.git" -branch = "postgres-pglite-submodule" -commit = "b89585a64ffef012ff0f219de9197c669aa8485b" - -[[sources]] -name = "pg_ivm" -url = "https://github.com/sraoss/pg_ivm.git" -branch = "postgres-pglite-submodule" -commit = "b66487f7a6f8deee3998e858d773e19923e4bd4b" - -[[sources]] -name = "pg_uuidv7" -url = "https://github.com/fboulnois/pg_uuidv7/" -branch = "postgres-pglite-submodule" -commit = "c707aae2411181be4802f5fa565b44d9c0bcbc29" - -[[sources]] -name = "pg_hashids" -url = "https://github.com/iCyberon/pg_hashids" -branch = "postgres-pglite-submodule" -commit = "8c404dd86408f3a987a3ff6825ac7e42bd618b98" - -[[sources]] -name = "age" -url = "https://github.com/apache/age.git" -branch = "PG17" -commit = "e1467f12e0b1d15dd35d3ab93f057a7112d425b8" - -[[sources]] -name = "pg_textsearch" -url = "https://github.com/timescale/pg_textsearch.git" -branch = "postgres-pglite-submodule" -commit = "5c5147bf2610d786f1bd139951b9fb7fe4ac68fb" - -[[sources]] -name = "postgis" -url = "https://github.com/postgis/postgis.git" -branch = "postgres-pglite-submodule" -commit = "08d9b9f749fa3531591055db2a736bfb6df47006" - -[[sources]] -name = "pglite-bindings" -url = "https://github.com/electric-sql/pglite-bindings" -branch = "main" -commit = "0d31326a41f7251548103f73e601699b0ebae2fa" diff --git a/assets/wasix-build/.gitignore b/assets/wasix-build/.gitignore deleted file mode 100644 index 5998c97c..00000000 --- a/assets/wasix-build/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -work/ diff --git a/assets/wasix-build/analyze_pgl_stubs.sh b/assets/wasix-build/analyze_pgl_stubs.sh deleted file mode 100755 index f365f019..00000000 --- a/assets/wasix-build/analyze_pgl_stubs.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -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 -DOCKER_USER_ARGS=() -if [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$DOCKER" run --rm \ - "${DOCKER_USER_ARGS[@]}" \ - --cpus="$JOBS" \ - -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - test -f "$BUILD_DIR/src/backend/pglite" - - mkdir -p /work/assets/wasix-build/build/link-analysis - out=/work/assets/wasix-build/build/link-analysis/wasix-host-abi-used.txt - stubs="pgl_system pgl_popen pgl_pclose pgl_geteuid pgl_getuid pgl_getpwuid pgl_exit pgl_atexit pgl_longjmp pgl_siglongjmp pgl_recv pgl_send pgl_shmget pgl_shmat pgl_shmdt pgl_shmctl ProcessStartupPacket" - - runtime_inputs=( - "$BUILD_DIR/libpgcore.a" - "$BUILD_DIR/libpgcore.o" - "$BUILD_DIR/src/common/libpgcommon_srv.a" - "$BUILD_DIR/src/port/libpgport_srv.a" - "$BUILD_DIR/src/backend/snowball/libdict_snowball.a" - "$BUILD_DIR/src/pl/plpgsql/src/libplpgsql.a" - ) - frontend_tool_inputs=( - "$BUILD_DIR/src/bin/pg_dump/"*.o - "$BUILD_DIR/src/interfaces/libpq/libpq.a" - "$BUILD_DIR/src/common/libpgcommon.a" - "$BUILD_DIR/src/common/libpgcommon_shlib.a" - "$BUILD_DIR/src/port/libpgport.a" - "$BUILD_DIR/src/port/libpgport_shlib.a" - "$BUILD_DIR/src/fe_utils/libpgfeutils.a" - ) - compiled_sources=( - "$PGSRC/pglite/src/pglitec" - "$PGSRC/src/bin/initdb/initdb.c" - "$PGSRC/src/bin/initdb/findtimezone.c" - "$PGSRC/src/fe_utils/option_utils.c" - ) - - print_undefined_refs() { - for obj in "$@"; do - [ -e "$obj" ] || continue - undef=$(wasixnm -u "$obj" 2>/dev/null || true) - for sym in $stubs; do - if printf "%s\n" "$undef" | grep -Eq "(^| )U $sym$"; then - echo "$sym $obj" - fi - done - done | sort -u - } - - symbol_defined_in() { - obj="$1" - sym="$2" - wasixnm "$obj" 2>/dev/null | awk -v sym="$sym" \ - '\''$2 ~ /^[TtWw]$/ && $3 == sym { found = 1 } END { exit(found ? 0 : 1) }'\'' - } - - { - echo "# WASIX host ABI link-symbol analysis" - echo - echo "Generated from $BUILD_DIR with wasixnm." - echo "Source tree: $PGSRC" - echo - echo "## Definitions compiled into final pglite" - for sym in $stubs; do - printf "%-30s" "$sym" - if symbol_defined_in "$BUILD_DIR/src/backend/pglite" "$sym"; then - printf " final" - fi - printf "\n" - done - echo - echo "## Runtime link inputs requiring WASIX host ABI ownership" - print_undefined_refs "${runtime_inputs[@]}" - echo - echo "## Frontend tool inputs requiring frontend/common ownership" - echo "These references are reported for pg_dump and future tool packaging." - echo "They do not by themselves justify adding symbols to the production WASIX bridge." - print_undefined_refs "${frontend_tool_inputs[@]}" - echo - echo "## Runtime compiled-source call sites" - echo "This includes upstream pglitec plus initdb/frontend source files when present." - for sym in $stubs; do - matches=$(grep -R --line-number -E "\\b${sym}\\s*\\(" "${compiled_sources[@]}" 2>/dev/null || true) - if [ -n "$matches" ]; then - printf "%s\n%s\n" "$sym" "$matches" - fi - done - } > "$out" - - cat "$out" - ' diff --git a/assets/wasix-build/configure_wasix_dl.sh b/assets/wasix-build/configure_wasix_dl.sh deleted file mode 100755 index dc182668..00000000 --- a/assets/wasix-build/configure_wasix_dl.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" -DEFAULT_PGSRC="$ROOT/work/postgres-pglite-wasix-src" -if [ ! -d "$DEFAULT_PGSRC" ]; then - DEFAULT_PGSRC="$REPO_ROOT/assets/checkouts/postgres-pglite" -fi -PGSRC="${PGSRC:-$DEFAULT_PGSRC}" -BUILD="${BUILD_DIR:-$ROOT/work/configure-smoke}" - -WASIX_HOME="${WASIX_HOME:-/tmp/wasixcc-home/.wasixcc}" -export HOME="${WASIX_HOME%/.wasixcc}" -export PATH="$WASIX_HOME/bin:$PATH" - -mkdir -p "$BUILD" - -. "$ROOT/profile_flags.sh" -pglite_oxide_apply_wasix_profile configure - -COMMON_CPPFLAGS="-I$PGSRC/src/include/port/wasix-dl" -if [ "${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" = "1" ]; then - COMMON_CPPFLAGS="$COMMON_CPPFLAGS -DPGLITE_WASIX_BACKEND_TIMING" -fi -COMMON_CFLAGS="$PGLITE_OXIDE_PROFILE_CFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -Wno-unused-command-line-argument" -COMMON_LDFLAGS="$PGLITE_OXIDE_PROFILE_LDFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes" -MAIN_LDFLAGS="-sMODULE_KIND=dynamic-main -sSTACK_SIZE=8MB -sINITIAL_MEMORY=128MB" -SIDE_MODULE_LDFLAGS="-Wl,-shared" - -if [ "${PGLITE_MODE:-0}" = "1" ]; then - mkdir -p "$ROOT/build/wasix-pglite" - PGLITE_SHIM="$ROOT/build/wasix-pglite/pglite_wasix_bridge.o" - - wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ - -include stdbool.h \ - -include stdlib.h \ - -I"$PGSRC/src/include/port/wasix-dl" \ - -c "$ROOT/wasix_shim/pglite_wasix_bridge.c" \ - -o "$PGLITE_SHIM" - - PGLITE_CFLAGS="\ - -D__PGLITE__\ - -DPGLITE_WASIX_DL\ - -Dsystem=pgl_system -Dpopen=pgl_popen -Dpclose=pgl_pclose\ - -Dgeteuid=pgl_geteuid -Dgetuid=pgl_getuid -Dgetpwuid=pgl_getpwuid\ - -Dexit=pgl_exit\ - -Dmunmap=pgl_munmap\ - -Dfcntl=pgl_fcntl\ - -Datexit=pgl_atexit\ - -Dsetsockopt=pgl_setsockopt -Dgetsockopt=pgl_getsockopt -Dgetsockname=pgl_getsockname\ - -Drecv=pgl_recv -Dsend=pgl_send -Dconnect=pgl_connect\ - -Dpoll=pgl_poll\ - -Dshmget=pgl_shmget -Dshmat=pgl_shmat -Dshmdt=pgl_shmdt -Dshmctl=pgl_shmctl\ - -Dlongjmp=pgl_longjmp -Dsiglongjmp=pgl_siglongjmp\ - -Wno-declaration-after-statement\ - -Wno-macro-redefined\ - -Wno-unused-function\ - -Wno-missing-prototypes\ - -Wno-incompatible-pointer-types" - LDFLAGS_EXTRA=" $PGLITE_SHIM" -else - mkdir -p "$ROOT/build/wasix-shim" - GENERIC_SHIM="$ROOT/build/wasix-shim/pglite_wasix_shim.o" - - wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ - -I"$PGSRC/src/include/port/wasix-dl" \ - -c "$ROOT/wasix_shim/pglite_wasix_shim.c" \ - -o "$GENERIC_SHIM" - - PGLITE_CFLAGS="" - LDFLAGS_EXTRA=" $GENERIC_SHIM" -fi - -cd "$BUILD" - -CC=wasixcc \ -AR=wasixar \ -RANLIB=wasixranlib \ -NM=wasixnm \ -CPPFLAGS="$COMMON_CPPFLAGS" \ -CFLAGS="$COMMON_CFLAGS$PGLITE_CFLAGS" \ -LDFLAGS="$COMMON_LDFLAGS" \ -LDFLAGS_EX="$MAIN_LDFLAGS$LDFLAGS_EXTRA" \ -LDFLAGS_SL="$SIDE_MODULE_LDFLAGS" \ -"$PGSRC/configure" \ - --prefix=/ \ - --libdir=/lib \ - --datadir=/share/postgresql \ - --bindir=/bin \ - --host=wasm32-wasix \ - --with-template=wasix-dl \ - --without-readline \ - --without-icu \ - --without-zlib \ - --without-llvm \ - --disable-largefile \ - --without-pam \ - --with-openssl=no diff --git a/assets/wasix-build/docker/Dockerfile b/assets/wasix-build/docker/Dockerfile deleted file mode 100644 index 1e471ed3..00000000 --- a/assets/wasix-build/docker/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# syntax=docker/dockerfile:1.7 -FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b - -ENV DEBIAN_FRONTEND=noninteractive - -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ - apt-get update && apt-get install -y --no-install-recommends \ - bash \ - bison \ - build-essential \ - ca-certificates \ - curl \ - file \ - flex \ - gawk \ - git \ - jq \ - lz4 \ - make \ - patch \ - perl \ - pkg-config \ - python3 \ - rsync \ - tar \ - wget \ - xz-utils \ - zstd - -ENV HOME=/opt/wasixcc-home -ENV WASIX_HOME=/opt/wasixcc-home/.wasixcc -ENV PATH=/opt/wasixcc-home/.wasixcc/bin:$PATH - -RUN mkdir -p /opt/wasixcc-home \ - && curl -fsSL https://wasix.cc -o /tmp/wasixcc-install.sh \ - && HOME=/opt/wasixcc-home sh /tmp/wasixcc-install.sh \ - && wasixcc --version - -WORKDIR /work diff --git a/assets/wasix-build/docker_contrib_extensions.sh b/assets/wasix-build/docker_contrib_extensions.sh deleted file mode 100755 index b7802bd0..00000000 --- a/assets/wasix-build/docker_contrib_extensions.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -CONTAINER_PLAN="${CONTAINER_PLAN:-/work/assets/generated/contrib-build.tsv}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e PLAN="$CONTAINER_PLAN" \ - -e JOBS="$JOBS" \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile build - - test -f "$BUILD_DIR/config.status" - test -f "$BUILD_DIR/src/backend/pglite" - cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null - test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" - - if [ ! -f "$PLAN" ]; then - echo "generated contrib build plan missing: $PLAN" >&2 - exit 1 - fi - - while IFS=$'\''\t'\'' read -r id sql_name contrib_dir module_file archive stable; do - case "$id" in ""|"#"*) continue ;; esac - test -n "$sql_name" - test -n "$contrib_dir" - echo "building contrib extension $id from contrib/$contrib_dir" - test -d "$BUILD_DIR/contrib/$contrib_dir" - make -s -j"$JOBS" -C "$BUILD_DIR/contrib/$contrib_dir" all - if [ "$module_file" = "-" ]; then - continue - fi - if [ ! -f "$BUILD_DIR/contrib/$contrib_dir/$module_file" ]; then - echo "expected WASIX side module missing: $BUILD_DIR/contrib/$contrib_dir/$module_file" >&2 - find "$BUILD_DIR/contrib/$contrib_dir" -maxdepth 1 -type f -name "*.so" -print >&2 - exit 1 - fi - done < "$PLAN" - ' diff --git a/assets/wasix-build/docker_initdb.sh b/assets/wasix-build/docker_initdb.sh deleted file mode 100755 index 27339c0e..00000000 --- a/assets/wasix-build/docker_initdb.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e JOBS="$JOBS" \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile build - export AR=wasixar - export RANLIB=wasixranlib - export NM=wasixnm - export LLVM_NM=wasixnm - - test -f "$BUILD_DIR/config.status" - cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null - test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" - - COMMON_CPPFLAGS="-I$PGSRC/src/include/port/wasix-dl" - COMMON_CFLAGS="$PGLITE_OXIDE_PROFILE_CFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -Wno-unused-command-line-argument" - COMMON_LDFLAGS="$PGLITE_OXIDE_PROFILE_LDFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes" - MAIN_LDFLAGS="-sMODULE_KIND=dynamic-main -sSTACK_SIZE=8MB -sINITIAL_MEMORY=128MB -Wl,--wrap=system -Wl,--wrap=popen -Wl,--wrap=pclose" - - INITDB_BUILD_DIR="$CONTAINER_ROOT/build/wasix-initdb" - mkdir -p "$INITDB_BUILD_DIR" - GENERIC_SHIM="$INITDB_BUILD_DIR/pglite_wasix_shim.o" - INITDB_SHIM="$INITDB_BUILD_DIR/pglite_wasix_initdb_shim.o" - wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ - -I"$BUILD_DIR/src/include" \ - -I"$PGSRC/src/include/port/wasix-dl" \ - -c "$CONTAINER_ROOT/wasix_shim/pglite_wasix_shim.c" \ - -o "$GENERIC_SHIM" - wasixcc $COMMON_CFLAGS $COMMON_CPPFLAGS \ - -I"$BUILD_DIR/src/include" \ - -I"$PGSRC/src/include/port/wasix-dl" \ - -c "$CONTAINER_ROOT/wasix_shim/pglite_wasix_initdb_shim.c" \ - -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=pgl_initdb_system -Dpopen=pgl_initdb_popen -Dpclose=pgl_initdb_pclose -Dgeteuid=pgl_geteuid -Dgetuid=pgl_getuid -Dgetpwuid=pgl_getpwuid -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" - test -f "$BUILD_DIR/src/bin/initdb/initdb" - ' diff --git a/assets/wasix-build/docker_pgdump.sh b/assets/wasix-build/docker_pgdump.sh deleted file mode 100755 index 257aa271..00000000 --- a/assets/wasix-build/docker_pgdump.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e JOBS="$JOBS" \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile build - export AR=wasixar - export RANLIB=wasixranlib - export NM=wasixnm - export LLVM_NM=wasixnm - - test -f "$BUILD_DIR/config.status" - cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null - test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" - make -s -C "$BUILD_DIR/src/bin/pg_dump" clean - make -s -C "$BUILD_DIR/src/bin/pg_dump" pg_dump \ - libpq="$BUILD_DIR/src/interfaces/libpq/libpq.a" \ - LIBS="$BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a -lm" - test -f "$BUILD_DIR/src/bin/pg_dump/pg_dump" - if wasixnm -u "$BUILD_DIR/src/bin/pg_dump/pg_dump" | grep -E " PQ[A-Za-z0-9_]+$"; then - echo "pg_dump still imports libpq symbols; expected standalone WASIX pg_dump" >&2 - exit 1 - fi - ' diff --git a/assets/wasix-build/docker_pglite.sh b/assets/wasix-build/docker_pglite.sh deleted file mode 100755 index b06c546c..00000000 --- a/assets/wasix-build/docker_pglite.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e FORCE_RECONFIGURE="${FORCE_RECONFIGURE:-0}" \ - -e JOBS="$JOBS" \ - -e PGLITE_MODE=1 \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile configure - profile_signature="$(pglite_oxide_wasix_profile_signature)" - - needs_configure=0 - if [ "${FORCE_RECONFIGURE:-0}" = "1" ] || [ ! -f "$BUILD_DIR/config.status" ]; then - needs_configure=1 - elif ! cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head"; then - needs_configure=1 - elif ! cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256"; then - needs_configure=1 - elif [ ! -f "$BUILD_DIR/.pglite-oxide-bridge-sha256" ]; then - needs_configure=1 - elif ! sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null 2>&1; then - needs_configure=1 - elif [ ! -f "$BUILD_DIR/.pglite-oxide-build-profile" ]; then - needs_configure=1 - elif [ "$profile_signature" != "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" ]; then - needs_configure=1 - fi - - if [ "$needs_configure" = "1" ]; then - rm -rf "$BUILD_DIR" - ./assets/wasix-build/configure_wasix_dl.sh - cp "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cp "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum ./assets/wasix-build/wasix_shim/pglite_wasix_bridge.c \ - > "$BUILD_DIR/.pglite-oxide-bridge-sha256" - printf "%s\n" "$profile_signature" > "$BUILD_DIR/.pglite-oxide-build-profile" - else - echo "reusing configured PGlite build at $BUILD_DIR" - fi - pglite_oxide_apply_wasix_profile build - rm -rf "$BUILD_DIR/src/timezone/compiled" - mkdir -p "$BUILD_DIR/src/timezone/compiled" - /usr/sbin/zic \ - -d "$BUILD_DIR/src/timezone/compiled" \ - "$PGSRC/src/timezone/data/tzdata.zi" - test -f "$BUILD_DIR/src/timezone/compiled/UTC" - test -f "$BUILD_DIR/src/timezone/compiled/GMT" - test -f "$BUILD_DIR/src/timezone/compiled/Etc/UTC" - test -f "$BUILD_DIR/src/timezone/compiled/America/New_York" - make -s -C "$BUILD_DIR/src/backend" generated-headers - make -s -C "$BUILD_DIR/src/backend" submake-libpgport - make -s -j"$JOBS" -C "$BUILD_DIR/src/backend" pglite - ' diff --git a/assets/wasix-build/docker_pgxs_extensions.sh b/assets/wasix-build/docker_pgxs_extensions.sh deleted file mode 100755 index d96f7721..00000000 --- a/assets/wasix-build/docker_pgxs_extensions.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -CONTAINER_PLAN="${CONTAINER_PLAN:-/work/assets/generated/pgxs-build.tsv}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e PLAN="$CONTAINER_PLAN" \ - -e JOBS="$JOBS" \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile build - - test -f "$BUILD_DIR/config.status" - test -f "$BUILD_DIR/src/backend/pglite" - cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null - test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" - - if [ ! -f "$PLAN" ]; then - echo "generated PGXS build plan missing: $PLAN" >&2 - exit 1 - fi - - mkdir -p "$BUILD_DIR/src/makefiles" "$BUILD_DIR/install/lib" - ln -sf "$PGSRC/src/makefiles/pgxs.mk" "$BUILD_DIR/src/makefiles/pgxs.mk" - ln -sf "$PGSRC/src/Makefile.shlib" "$BUILD_DIR/src/Makefile.shlib" - - while IFS=$'\''\t'\'' read -r id sql_name source_dir module_file archive stable make_args; do - case "$id" in ""|"#"*) continue ;; esac - test -n "$sql_name" - test -n "$source_dir" - extension_source="/work/$source_dir" - test -d "$extension_source" - extension_dir="$BUILD_DIR/pgxs/$id" - rm -rf "$extension_dir" - mkdir -p "$(dirname "$extension_dir")" - cp -a "$extension_source" "$extension_dir" - rm -rf "$extension_dir/.git" - extra_make_args=() - if [ "${make_args:-"-"}" != "-" ]; then - read -r -a extra_make_args <<< "$make_args" - fi - make -s -C "$extension_dir" \ - PG_CONFIG=/work/assets/wasix-build/pg_config_wasix.sh \ - clean >/dev/null 2>&1 || true - make -s -j"$JOBS" -C "$extension_dir" \ - PG_CONFIG=/work/assets/wasix-build/pg_config_wasix.sh \ - CPPFLAGS="-I$BUILD_DIR/src/include -I$PGSRC/src/include -I$PGSRC/src/include/port/wasix-dl" \ - OPTFLAGS="" \ - "${extra_make_args[@]}" \ - all - if [ "$id" = "age" ] && grep -q "^ PASSEDBYVALUE,$" "$extension_dir/age--1.7.0.sql"; then - echo "AGE generated SQL still declares graphid PASSEDBYVALUE on wasm32" >&2 - exit 1 - fi - if [ "$module_file" = "-" ]; then - continue - fi - if [ ! -f "$extension_dir/$module_file" ]; then - echo "expected WASIX side module missing: $extension_dir/$module_file" >&2 - find "$extension_dir" -maxdepth 1 -type f -name "*.so" -print >&2 - exit 1 - fi - done < "$PLAN" - ' diff --git a/assets/wasix-build/docker_runtime_support.sh b/assets/wasix-build/docker_runtime_support.sh deleted file mode 100755 index fd33571f..00000000 --- a/assets/wasix-build/docker_runtime_support.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" - -IMAGE="${IMAGE:-pglite-oxide-wasix-build:local}" -JOBS="${JOBS:-4}" -CONTAINER_ROOT="${CONTAINER_ROOT:-/work/assets/wasix-build}" -CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$CONTAINER_ROOT/work/docker-pglite}" -CONTAINER_PGSRC="${CONTAINER_PGSRC:-$CONTAINER_ROOT/work/postgres-pglite-wasix-src}" -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 [ "${PGLITE_OXIDE_DOCKER_AS_ROOT:-0}" != "1" ]; then - DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) -fi - -"$ROOT/prepare_patched_source.sh" - -if [ "${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 BUILD_DIR="$CONTAINER_BUILD_DIR" \ - -e PGSRC="$CONTAINER_PGSRC" \ - -e JOBS="$JOBS" \ - -e PGLITE_OXIDE_BUILD_PROFILE="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" \ - -e PGLITE_OXIDE_WASIX_COPT="${PGLITE_OXIDE_WASIX_COPT:-}" \ - -e PGLITE_OXIDE_WASIX_LOPT="${PGLITE_OXIDE_WASIX_LOPT:-}" \ - -e PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" \ - -e PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" \ - -e PGLITE_OXIDE_WASM_OPT_FLAGS="${PGLITE_OXIDE_WASM_OPT_FLAGS-}" \ - -e PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT="${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT-}" \ - -e PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED="${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ - -e PGLITE_OXIDE_WASIX_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" \ - -e PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" \ - -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ - -v "$REPO_ROOT:/work" \ - -w /work \ - "$IMAGE" \ - bash -lc ' - set -euo pipefail - . ./assets/wasix-build/docker_wasix_env.sh - . ./assets/wasix-build/profile_flags.sh - pglite_oxide_apply_wasix_profile build - - test -f "$BUILD_DIR/config.status" - test -f "$BUILD_DIR/src/backend/pglite" - cmp -s "$PGSRC/.pglite-oxide-source-head" "$BUILD_DIR/.pglite-oxide-source-head" - cmp -s "$PGSRC/.pglite-oxide-patch-sha256" "$BUILD_DIR/.pglite-oxide-patch-sha256" - sha256sum -c "$BUILD_DIR/.pglite-oxide-bridge-sha256" >/dev/null - test "$(pglite_oxide_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.pglite-oxide-build-profile")" - - make -s -j"$JOBS" -C "$BUILD_DIR/src/pl/plpgsql/src" all - make -s -j"$JOBS" -C "$BUILD_DIR/src/backend/snowball" all - test -f "$BUILD_DIR/src/pl/plpgsql/src/plpgsql.so" - test -f "$BUILD_DIR/src/backend/snowball/dict_snowball.so" - test -f "$BUILD_DIR/src/backend/snowball/snowball_create.sql" - ' diff --git a/assets/wasix-build/docker_wasix_env.sh b/assets/wasix-build/docker_wasix_env.sh deleted file mode 100755 index 2f2df26b..00000000 --- a/assets/wasix-build/docker_wasix_env.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${WASIX_HOME:=/opt/wasixcc-home/.wasixcc}" - -if [ "${HOME:-}" != "${WASIX_HOME%/.wasixcc}" ] && [ ! -e "$HOME/.wasixcc" ]; then - ln -s "$WASIX_HOME" "$HOME/.wasixcc" -fi - -export PATH="$WASIX_HOME/bin:$PATH" diff --git a/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch b/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch deleted file mode 100644 index 7765655f..00000000 --- a/assets/wasix-build/patches/postgres-pglite-wasix-dl.patch +++ /dev/null @@ -1,1187 +0,0 @@ -diff --git a/src/Makefile.shlib b/src/Makefile.shlib -index 723fe2b7fa..1f99174a1f 100644 ---- a/src/Makefile.shlib -+++ b/src/Makefile.shlib -@@ -239,6 +239,17 @@ ifeq ($(PORTNAME), emscripten) - # endif - endif - -+ifeq ($(PORTNAME), wasix-dl) -+ LINK.shared = $(COMPILER) -shared -Wno-unused-function -+ ifdef soname -+ # wasm side modules use unversioned shared libraries -+ shlib = $(shlib_bare) -+ soname = $(shlib_bare) -+ endif -+ BUILD.exports = ( $(AWK) '/^[^\#]/ {printf "%s\n",$$1}' $< ) | sort -u >$@ -+ exports_file = $(SHLIB_EXPORTS:%.txt=%.list) -+endif -+ - ## - ## BUILD - ## -diff --git a/src/backend/Makefile b/src/backend/Makefile -index a215e39386..dbafda3a7e 100644 ---- a/src/backend/Makefile -+++ b/src/backend/Makefile -@@ -60,7 +60,7 @@ override LDFLAGS := $(LDFLAGS) $(LDFLAGS_EX) $(LDFLAGS_EX_BE) - - all: submake-libpgport submake-catalog-headers submake-utils-headers postgres $(POSTGRES_IMP) - --ifneq ($(PORTNAME), emscripten) -+ifeq (,$(filter emscripten wasix-dl,$(PORTNAME))) - ifneq ($(PORTNAME), cygwin) - ifneq ($(PORTNAME), win32) - -@@ -109,6 +109,30 @@ pglite-libc: - - endif - -+ifeq ($(PORTNAME), wasix-dl) -+AR ?= llvm-ar -+WASM_LD ?= $(shell $(CC) -print-prog-name=wasm-ld) -+LIBPGCORE ?= $(top_builddir)/libpgcore.a -+LIBPG = $(top_builddir)/libpostgres.a -+PGCORE = $(top_builddir)/src/common/libpgcommon_srv.a $(top_builddir)/src/port/libpgport_srv.a $(LIBPG) -+PGMAIN = main/main.o tcop/postgres.o -+PGBACKEND = $(filter-out $(PGMAIN) $(top_builddir)/src/common/libpgcommon_srv.a $(top_builddir)/src/port/libpgport_srv.a,$(call expand_subsys,$(OBJS))) -+ -+postgres: $(OBJS) -+ $(AR) rcs $(top_builddir)/libpgmain.a $(PGMAIN) -+ $(AR) rcs $(LIBPG) $(PGBACKEND) -+ $(WASM_LD) --relocatable -o $(top_builddir)/libpgcore.o --whole-archive $(PGCORE) --no-whole-archive -+ $(AR) rcs $(LIBPGCORE) $(top_builddir)/libpgcore.o -+ COPTS="$(LOPTS)" $(CC) $(MAIN_MODULE) $(CFLAGS) $(LDFLAGS) -nostartfiles -o $@ $(LIBPGCORE) $(top_builddir)/libpgmain.a $(LIBS) -+ -+pglite: $(OBJS) -+ $(AR) rcs $(top_builddir)/libpgmain.a $(PGMAIN) -+ $(AR) rcs $(LIBPG) $(PGBACKEND) -+ $(WASM_LD) --relocatable -o $(top_builddir)/libpgcore.o --whole-archive $(PGCORE) --no-whole-archive -+ $(AR) rcs $(LIBPGCORE) $(top_builddir)/libpgcore.o -+ COPTS="$(LOPTS)" $(CC) $(MAIN_MODULE) $(CFLAGS) $(LDFLAGS) -nostartfiles -o $@ $(LIBPGCORE) $(top_builddir)/libpgmain.a $(LIBS) -+endif -+ - ifeq ($(PORTNAME), cygwin) - - postgres: $(OBJS) -diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c -index 97a4c387a3..5b5a19b7f2 100644 ---- a/src/backend/commands/copyfromparse.c -+++ b/src/backend/commands/copyfromparse.c -@@ -174,6 +174,9 @@ ReceiveCopyBegin(CopyFromState cstate) - int16 format = (cstate->opts.binary ? 1 : 0); - int i; - -+#ifdef PGLITE_WASIX_DL -+ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_IN); -+#endif - pq_beginmessage(&buf, PqMsg_CopyInResponse); - pq_sendbyte(&buf, format); /* overall format */ - pq_sendint16(&buf, natts); -diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c -index 84dc465cba..63526776be 100644 ---- a/src/backend/commands/copyto.c -+++ b/src/backend/commands/copyto.c -@@ -137,6 +137,9 @@ SendCopyBegin(CopyToState cstate) - int16 format = (cstate->opts.binary ? 1 : 0); - int i; - -+#ifdef PGLITE_WASIX_DL -+ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_OUT); -+#endif - pq_beginmessage(&buf, PqMsg_CopyOutResponse); - pq_sendbyte(&buf, format); /* overall format */ - pq_sendint16(&buf, natts); -diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c -index e0257cc49b..5137013369 100644 ---- a/src/backend/replication/walsender.c -+++ b/src/backend/replication/walsender.c -@@ -701,6 +701,9 @@ UploadManifest(void) - ib = CreateIncrementalBackupInfo(mcxt); - - /* Send a CopyInResponse message */ -+#ifdef PGLITE_WASIX_DL -+ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_IN); -+#endif - pq_beginmessage(&buf, PqMsg_CopyInResponse); - pq_sendbyte(&buf, 0); - pq_sendint16(&buf, 0); -@@ -954,6 +957,9 @@ StartReplication(StartReplicationCmd *cmd) - WalSndSetState(WALSNDSTATE_CATCHUP); - - /* Send a CopyBothResponse message, and start streaming */ -+#ifdef PGLITE_WASIX_DL -+ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_BOTH); -+#endif - pq_beginmessage(&buf, PqMsg_CopyBothResponse); - pq_sendbyte(&buf, 0); - pq_sendint16(&buf, 0); -@@ -1496,6 +1502,9 @@ StartLogicalReplication(StartReplicationCmd *cmd) - WalSndSetState(WALSNDSTATE_CATCHUP); - - /* Send a CopyBothResponse message, and start streaming */ -+#ifdef PGLITE_WASIX_DL -+ pgl_protocol_report_copy_response(PGL_WASIX_PROTOCOL_COPY_BOTH); -+#endif - pq_beginmessage(&buf, PqMsg_CopyBothResponse); - pq_sendbyte(&buf, 0); - pq_sendint16(&buf, 0); -diff --git a/src/common/file_utils.c b/src/common/file_utils.c -index 35458d1844..a04ee56efa 100644 ---- a/src/common/file_utils.c -+++ b/src/common/file_utils.c -@@ -416,1 +416,1 @@ fsync_fname(const char *fname, bool isdir) -- if (returncode != 0 && !(isdir && (errno == EBADF || errno == EINVAL))) -+ if (returncode != 0 && !(isdir && (errno == EBADF || errno == EINVAL || errno == EISDIR))) -diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c -index dd3307cb76..eb14fa5c13 100644 ---- a/src/backend/tcop/backend_startup.c -+++ b/src/backend/tcop/backend_startup.c -@@ -47,7 +47,7 @@ bool Trace_connection_negotiation = false; - static void BackendInitialize(ClientSocket *client_sock, CAC_state cac); - static int ProcessSSLStartup(Port *port); - --#if defined(__EMSCRIPTEN__) -+#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) - int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done); - #else - static int ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done); -@@ -459,7 +459,9 @@ reject: - * should make no assumption here about the order in which the client may make - * requests. - */ --#if defined(__EMSCRIPTEN__) -+#if defined(PGLITE_WASIX_DL) -+__attribute__((export_name("ProcessStartupPacket"))) int -+#elif defined(__EMSCRIPTEN__) - int EMSCRIPTEN_KEEPALIVE - #else - static int -diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c -index 1b41bf4de9..2394275215 100644 ---- a/src/backend/tcop/postgres.c -+++ b/src/backend/tcop/postgres.c -@@ -81,6 +81,15 @@ - #include "utils/timestamp.h" - #include "utils/varlena.h" - -+#ifdef PGLITE_WASIX_DL -+#include "port/wasix-dl.h" -+#define PGLITE_HOST_EXPORT(name) __attribute__((export_name(name))) -+extern void pglite_wasix_process_startup_options(struct Port *port); -+extern volatile int pglite_wasix_startup_error_capture_active; -+#else -+#define PGLITE_HOST_EXPORT(name) -+#endif -+ - /* ---------------- - * global variables - * ---------------- -@@ -243,7 +251,7 @@ void initDummyPort() { - MemoryContextSwitchTo(oldcontext); - } - --void pgl_startPGlite() { -+PGLITE_HOST_EXPORT("pgl_startPGlite") void pgl_startPGlite() { - initDummyPort(); - whereToSendOutput = DestRemote; - // initdb execs postgres in single mode, which sets this to true -@@ -267,15 +275,18 @@ void pgl_startPGlite() { - - } - --void pgl_pq_flush() { -+PGLITE_HOST_EXPORT("pgl_pq_flush") void pgl_pq_flush() { - pq_flush(); - } - --struct Port* pgl_getMyProcPort() { -+PGLITE_HOST_EXPORT("pgl_getMyProcPort") struct Port* pgl_getMyProcPort() { - return MyProcPort; - } - --void pgl_sendConnData() { -+PGLITE_HOST_EXPORT("pgl_sendConnData") void pgl_sendConnData() { -+#ifdef PGLITE_WASIX_DL -+ pglite_wasix_process_startup_options(MyProcPort); -+#endif - ClientAuthInProgress = false; - - { -@@ -297,6 +308,28 @@ void pgl_sendConnData() { - ReadyForQuery(DestRemote); - } - -+#ifdef PGLITE_WASIX_DL -+static CommandDest pglite_wasix_startup_error_saved_dest = DestDebug; -+ -+static void -+pglite_wasix_begin_startup_error_capture(void) -+{ -+ if (MyProcPort == NULL) -+ initDummyPort(); -+ pglite_wasix_startup_error_saved_dest = whereToSendOutput; -+ pglite_wasix_startup_error_capture_active = 1; -+ whereToSendOutput = DestRemote; -+} -+ -+static void -+pglite_wasix_end_startup_error_capture(void) -+{ -+ pglite_wasix_startup_error_capture_active = 0; -+ if (whereToSendOutput == DestRemote) -+ whereToSendOutput = pglite_wasix_startup_error_saved_dest; -+} -+#endif -+ - #endif // ifdef __PGLITE__ - - /* ---------------------------------------------------------------- -@@ -1125,6 +1156,8 @@ exec_simple_query(const char *query_string) - bool use_implicit_block; - char msec_str[32]; - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY); -+ - /* - * Report query to various monitoring facilities. - */ -@@ -1148,7 +1181,9 @@ exec_simple_query(const char *query_string) - * one of those, else bad things will happen in xact.c. (Note that this - * will normally change current memory context.) - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_START_XACT); - start_xact_command(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_START_XACT); - - /* - * Zap any pre-existing unnamed statement. (While not strictly necessary, -@@ -1156,7 +1191,9 @@ exec_simple_query(const char *query_string) - * statement and portal; this ensures we recover any storage used by prior - * unnamed operations.) - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED); - drop_unnamed_stmt(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED); - - /* - * Switch to appropriate context for constructing parsetrees. -@@ -1167,7 +1204,9 @@ exec_simple_query(const char *query_string) - * Do basic parsing of the query or queries (this should be safe even if - * we are in aborted transaction state!) - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PARSE); - parsetree_list = pg_parse_query(query_string); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PARSE); - - /* Log immediately if dictated by log_statement */ - if (check_log_statement(parsetree_list)) -@@ -1244,7 +1283,9 @@ exec_simple_query(const char *query_string) - errdetail_abort())); - - /* Make sure we are in a transaction command */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_START_XACT); - start_xact_command(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_START_XACT); - - /* - * If using an implicit transaction block, and we're not already in a -@@ -1264,7 +1305,9 @@ exec_simple_query(const char *query_string) - */ - if (analyze_requires_snapshot(parsetree)) - { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_SNAPSHOT); - PushActiveSnapshot(GetTransactionSnapshot()); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_SNAPSHOT); - snapshot_set = true; - } - -@@ -1291,11 +1334,15 @@ exec_simple_query(const char *query_string) - else - oldcontext = MemoryContextSwitchTo(MessageContext); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE); - querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, query_string, - NULL, 0, NULL); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PLAN); - plantree_list = pg_plan_queries(querytree_list, query_string, - CURSOR_OPT_PARALLEL_OK, NULL); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PLAN); - - /* - * Done with the snapshot used for parsing/planning. -@@ -1336,7 +1383,9 @@ exec_simple_query(const char *query_string) - /* - * Start the portal. No parameters here. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PORTAL_START); - PortalStart(portal, NULL, 0, InvalidSnapshot); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PORTAL_START); - - /* - * Select the appropriate output format: text unless we are doing a -@@ -1363,9 +1412,11 @@ exec_simple_query(const char *query_string) - /* - * Now we can create the destination receiver object. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER); - receiver = CreateDestReceiver(dest); - if (dest == DestRemote) - SetRemoteDestReceiverParams(receiver, portal); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER); - - /* - * Switch back to transaction context for execution. -@@ -1375,6 +1426,7 @@ exec_simple_query(const char *query_string) - /* - * Run the portal to completion, and then drop it (and the receiver). - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_PORTAL_RUN); - (void) PortalRun(portal, - FETCH_ALL, - true, /* always top level */ -@@ -1382,6 +1434,7 @@ exec_simple_query(const char *query_string) - receiver, - receiver, - &qc); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_PORTAL_RUN); - - receiver->rDestroy(receiver); - -@@ -1400,7 +1453,9 @@ exec_simple_query(const char *query_string) - */ - if (use_implicit_block) - EndImplicitTransactionBlock(); -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - finish_xact_command(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - } - else if (IsA(parsetree->stmt, TransactionStmt)) - { -@@ -1408,7 +1463,9 @@ exec_simple_query(const char *query_string) - * If this was a transaction control statement, commit it. We will - * start a new xact command for the next command. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - finish_xact_command(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - } - else - { -@@ -1423,7 +1480,9 @@ exec_simple_query(const char *query_string) - * We need a CommandCounterIncrement after every query, except - * those that start or end a transaction block. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER); - CommandCounterIncrement(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER); - - /* - * Disable statement timeout between queries of a multi-query -@@ -1439,7 +1498,9 @@ exec_simple_query(const char *query_string) - * command the client sent, regardless of rewriting. (But a command - * aborted by error will not send an EndCommand report at all.) - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_END_COMMAND); - EndCommand(&qc, dest, false); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_END_COMMAND); - - /* Now we may drop the per-parsetree context, if one was created. */ - if (per_parsetree_context) -@@ -1451,7 +1512,9 @@ exec_simple_query(const char *query_string) - * something if the parsetree list was empty; otherwise the last loop - * iteration already did it.) - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - finish_xact_command(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_FINISH_XACT); - - /* - * If there were no parsetrees, return EmptyQueryResponse message. -@@ -1484,6 +1547,7 @@ exec_simple_query(const char *query_string) - TRACE_POSTGRESQL_QUERY_DONE(query_string); - - debug_query_string = NULL; -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY); - } - - /* -@@ -4231,20 +4295,28 @@ PostgresSingleUserMain(int argc, char *argv[], - { - const char *dbname = NULL; - -+ PGL_BACKEND_TIMING_RESET(); -+ - Assert(!IsUnderPostmaster); - - /* Initialize startup process environment. */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STANDALONE_PROCESS); - InitStandaloneProcess(argv[0]); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STANDALONE_PROCESS); - - /* - * Set default values for command-line options. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_GUC_INIT); - InitializeGUCOptions(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_GUC_INIT); - - /* - * Parse command-line options. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SWITCH_PARSE); - process_postgres_switches(argc, argv, PGC_POSTMASTER, &dbname); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SWITCH_PARSE); - - /* Must have gotten a database name, or have a default (the username) */ - if (dbname == NULL) -@@ -4258,13 +4330,16 @@ PostgresSingleUserMain(int argc, char *argv[], - } - - /* Acquire configuration parameters */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CONFIG_FILES); - if (!SelectConfigFiles(userDoption, progname)) - proc_exit(1); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CONFIG_FILES); - - /* - * Validate we have been given a reasonable-looking DataDir and change - * into it. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATA_DIR_LOCK); - checkDataDir(); - ChangeToDataDir(); - -@@ -4272,17 +4347,25 @@ PostgresSingleUserMain(int argc, char *argv[], - * Create lockfile for data directory. - */ - CreateDataDirLockFile(false); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATA_DIR_LOCK); - - /* read control file (error checking and contains config ) */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CONTROL_FILE); - LocalProcessControlFile(false); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CONTROL_FILE); - - /* - * process any libraries that should be preloaded at postmaster start - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_PRELOAD_LIBS); - process_shared_preload_libraries(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_PRELOAD_LIBS); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SHARED_MEMORY); - /* Initialize MaxBackends */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_MAX_BACKENDS); - InitializeMaxBackends(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_MAX_BACKENDS); - - /* - * Give preloaded libraries a chance to request additional shared memory. -@@ -4302,7 +4385,9 @@ PostgresSingleUserMain(int argc, char *argv[], - */ - InitializeWalConsistencyChecking(); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY); - CreateSharedMemoryAndSemaphores(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY); - - /* - * Remember stand-alone backend startup time,roughly at the same point -@@ -4314,7 +4399,10 @@ PostgresSingleUserMain(int argc, char *argv[], - * Create a per-backend PGPROC struct in shared memory. We must do this - * before we can use LWLocks. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_PROCESS); - InitProcess(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_PROCESS); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SHARED_MEMORY); - - /* - * Now that sufficient infrastructure has been initialized, PostgresMain() -@@ -4323,7 +4411,7 @@ PostgresSingleUserMain(int argc, char *argv[], - PostgresMain(dbname, username); - } - --void PostgresSendReadyForQueryIfNecessary() { -+PGLITE_HOST_EXPORT("PostgresSendReadyForQueryIfNecessary") void PostgresSendReadyForQueryIfNecessary() { - /* - * (1) If we've reached idle state, tell the frontend we're ready for - * a new query. -@@ -4431,7 +4519,7 @@ void PostgresSendReadyForQueryIfNecessary() { - } - } - --void PostgresMainLoopOnce() { -+PGLITE_HOST_EXPORT("PostgresMainLoopOnce") void PostgresMainLoopOnce() { - - int firstchar; - StringInfoData input_message; -@@ -4805 +4893 @@ void PostgresMainLoopOnce() { --void PostgresMainLongJmp() { -+PGLITE_HOST_EXPORT("PostgresMainLongJmp") void PostgresMainLongJmp() { -@@ -4894,6 +4982,9 @@ void PostgresMainLongJmp() { - if (doing_extended_query_message) - ignore_till_sync = true; - -+ if (!ignore_till_sync) -+ send_ready_for_query = true; -+ - /* We don't have a transaction command open anymore */ - xact_started = false; - -@@ -4994,7 +5085,9 @@ PostgresMain(const char *dbname, const char *username) - } - - /* Early initialization */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_BASE_INIT); - BaseInit(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_BASE_INIT); - - /* We need to allow SIGINT, etc during the initial transaction */ - sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); -@@ -5008,10 +5101,18 @@ PostgresMain(const char *dbname, const char *username) - * - * Honor session_preload_libraries if not dealing with a WAL sender. - */ -+#ifdef PGLITE_WASIX_DL -+ pglite_wasix_begin_startup_error_capture(); -+#endif -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_POSTGRES); - InitPostgres(dbname, InvalidOid, /* database to connect to */ - username, InvalidOid, /* role to connect as */ - (!am_walsender) ? INIT_PG_LOAD_SESSION_LIBS : 0, - NULL); /* no out_dbname */ -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_POSTGRES); -+#ifdef PGLITE_WASIX_DL -+ pglite_wasix_end_startup_error_capture(); -+#endif - - /* - * If the PostmasterContext is still around, recycle the space; we don't -@@ -5025,6 +5126,7 @@ PostgresMain(const char *dbname, const char *username) - - SetProcessingMode(NormalProcessing); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_POST_INIT); - /* - * Now all GUC states are fully set up. Report them to client if - * appropriate. -@@ -5061,7 +5163,9 @@ PostgresMain(const char *dbname, const char *username) - /* Welcome banner for standalone case */ - if (whereToSendOutput == DestDebug) - printf("\nPostgreSQL stand-alone backend %s\n", PG_VERSION); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_POST_INIT); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_MESSAGE_CONTEXTS); - /* - * Create the memory context we will use in the main loop. - * -@@ -5084,6 +5188,7 @@ PostgresMain(const char *dbname, const char *username) - MemoryContextSwitchTo(row_description_context); - initStringInfo(&row_description_buf); - MemoryContextSwitchTo(TopMemoryContext); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_MESSAGE_CONTEXTS); - - /* Fire any defined login event triggers, if appropriate */ - EventTriggerOnLogin(); -diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c -index 288c55a90d..0a4ba03fb2 100644 ---- a/src/backend/utils/init/postinit.c -+++ b/src/backend/utils/init/postinit.c -@@ -65,6 +65,9 @@ - #include "utils/snapmgr.h" - #include "utils/syscache.h" - #include "utils/timeout.h" -+#ifdef PGLITE_WASIX_DL -+#include "port/wasix-dl.h" -+#endif - - static HeapTuple GetDatabaseTuple(const char *dbname); - static HeapTuple GetDatabaseTupleByOid(Oid dboid); -@@ -759,6 +762,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - * - * Once I have done this, I am visible to other backends! - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INIT_PROC_PHASE2); - InitProcessPhase2(); - - /* -@@ -786,6 +790,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT, - IdleStatsUpdateTimeoutHandler); - } -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INIT_PROC_PHASE2); - - /* - * If this is either a bootstrap process or a standalone backend, start up -@@ -802,7 +807,9 @@ InitPostgres(const char *in_dbname, Oid dboid, - */ - CreateAuxProcessResourceOwner(); - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STARTUP_XLOG); - StartupXLOG(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STARTUP_XLOG); - /* Release (and warn about) any buffer pins leaked in StartupXLOG */ - ReleaseAuxProcessResources(true); - /* Reset CurrentResourceOwner to nothing for the moment */ -@@ -822,6 +829,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - * We must do this before starting a transaction because transaction abort - * would try to touch these hashtables. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT); - RelationCacheInitialize(); - InitCatalogCache(); - InitPlanCache(); -@@ -837,6 +845,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - * at least entries for pg_database and catalogs used for authentication. - */ - RelationCacheInitializePhase2(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT); - - /* - * Set up process-exit callback to do pre-shutdown cleanup. This is the -@@ -872,6 +881,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - */ - if (!bootstrap) - { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT); - /* statement_timestamp must be set for timeouts to work correctly */ - SetCurrentStatementStartTimestamp(); - StartTransactionCommand(); -@@ -885,6 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - XactIsoLevel = XACT_READ_COMMITTED; - - (void) GetTransactionSnapshot(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT); - } - - /* -@@ -895,6 +906,7 @@ InitPostgres(const char *in_dbname, Oid dboid, - * process, we use a fixed ID, otherwise we figure it out from the - * authenticated user name. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_USER); - if (bootstrap || AmAutoVacuumWorkerProcess() || AmLogicalSlotSyncWorkerProcess()) - { - InitializeSessionUserIdStandalone(); -@@ -902,12 +914,12 @@ InitPostgres(const char *in_dbname, Oid dboid, - } - else if (!IsUnderPostmaster) - { --#if defined(__EMSCRIPTEN__) -+#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) - if (!strcmp( username , WASM_USERNAME )) { - #endif - InitializeSessionUserIdStandalone(); - am_superuser = true; --#if defined(__EMSCRIPTEN__) -+#if defined(__EMSCRIPTEN__) || defined(PGLITE_WASIX_DL) - } else { - //puts("# 894: switching session id"); - InitializeSessionUserId(username, InvalidOid, false); -@@ -947,6 +959,7 @@ if (!strcmp( username , WASM_USERNAME )) { - hba_authname(MyClientConnectionInfo.auth_method)); - am_superuser = superuser(); - } -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_USER); - - /* - * Binary upgrades only allowed super-user connections -@@ -1043,7 +1056,9 @@ if (!strcmp( username , WASM_USERNAME )) { - HeapTuple tuple; - Form_pg_database dbform; - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOOKUP); - tuple = GetDatabaseTuple(in_dbname); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOOKUP); - if (!HeapTupleIsValid(tuple)) - ereport(FATAL, - (errcode(ERRCODE_UNDEFINED_DATABASE), -@@ -1089,7 +1104,11 @@ if (!strcmp( username , WASM_USERNAME )) { - * CREATE DATABASE. - */ - if (!bootstrap) -+ { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); - LockSharedObject(DatabaseRelationId, dboid, 0, RowExclusiveLock); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); -+ } - - /* - * Recheck pg_database to make sure the target database hasn't gone away. -@@ -1101,6 +1120,7 @@ if (!strcmp( username , WASM_USERNAME )) { - HeapTuple tuple; - Form_pg_database datform; - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); - tuple = GetDatabaseTupleByOid(dboid); - if (HeapTupleIsValid(tuple)) - datform = (Form_pg_database) GETSTRUCT(tuple); -@@ -1134,6 +1154,7 @@ if (!strcmp( username , WASM_USERNAME )) { - /* pass the database name back to the caller */ - if (out_dbname) - strcpy(out_dbname, dbname); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK); - } - - /* -@@ -1175,6 +1196,7 @@ if (!strcmp( username , WASM_USERNAME )) { - * Now we should be able to access the database directory safely. Verify - * it's there and looks reasonable. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_DATABASE_PATH); - fullpath = GetDatabasePath(MyDatabaseId, MyDatabaseTableSpace); - - if (!bootstrap) -@@ -1200,6 +1222,7 @@ if (!strcmp( username , WASM_USERNAME )) { - - SetDatabasePath(fullpath); - pfree(fullpath); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_DATABASE_PATH); - - /* - * It's now possible to do real access to the system catalogs. -@@ -1207,10 +1230,16 @@ if (!strcmp( username , WASM_USERNAME )) { - * Load relcache entries for the system catalogs. This must create at - * least the minimum set of "nailed-in" cache entries. - */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELCACHE_PHASE3); -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3); - RelationCacheInitializePhase3(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3); - - /* set up ACL framework (so CheckMyDatabase can check permissions) */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_INITIALIZE_ACL); - initialize_acl(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_INITIALIZE_ACL); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_RELCACHE_PHASE3); - - /* - * Re-read the pg_database row for our database, check permissions and set -@@ -1219,8 +1248,12 @@ if (!strcmp( username , WASM_USERNAME )) { - * user is a superuser, so the above stuff has to happen first.) - */ - if (!bootstrap) -+ { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_CHECK_MY_DATABASE); - CheckMyDatabase(dbname, am_superuser, - (flags & INIT_PG_OVERRIDE_ALLOW_CONNS) != 0); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_CHECK_MY_DATABASE); -+ } - - /* - * Now process any command-line switches and any additional GUC variable -@@ -1228,10 +1261,16 @@ if (!strcmp( username , WASM_USERNAME )) { - * because we didn't know if client is a superuser. - */ - if (MyProcPort != NULL) -+ { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_STARTUP_OPTIONS); - process_startup_options(MyProcPort, am_superuser); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_STARTUP_OPTIONS); -+ } - - /* Process pg_db_role_setting options */ -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_PROCESS_SETTINGS); - process_settings(MyDatabaseId, GetSessionUserId()); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_PROCESS_SETTINGS); - - /* Apply PostAuthDelay as soon as we've read all options */ - if (PostAuthDelay > 0) -@@ -1242,6 +1281,7 @@ if (!strcmp( username , WASM_USERNAME )) { - * selected the active user and gotten the right GUC settings. - */ - -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_INITIALIZATION); - /* set default namespace search path */ - InitializeSearchPath(); - -@@ -1250,6 +1290,7 @@ if (!strcmp( username , WASM_USERNAME )) { - - /* Initialize this backend's session state. */ - InitializeSession(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_INITIALIZATION); - - /* - * If this is an interactive session, load any libraries that should be -@@ -1259,7 +1300,11 @@ if (!strcmp( username , WASM_USERNAME )) { - * access needs to be done. - */ - if ((flags & INIT_PG_LOAD_SESSION_LIBS) != 0) -+ { -+ PGL_BACKEND_TIMING_START(PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS); - process_session_preload_libraries(); -+ PGL_BACKEND_TIMING_END(PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS); -+ } - - /* report this backend in the PgBackendStatus array */ - if (!bootstrap) -@@ -1333,6 +1378,15 @@ process_startup_options(Port *port, bool am_superuser) - } - } - -+#ifdef PGLITE_WASIX_DL -+void -+pglite_wasix_process_startup_options(Port *port) -+{ -+ if (port != NULL) -+ process_startup_options(port, true); -+} -+#endif -+ - /* - * Load GUC settings from pg_db_role_setting. - * -diff --git a/src/backend/utils/mmgr/portalmem.c b/src/backend/utils/mmgr/portalmem.c -index 93137820ac..4fd3eed96e 100644 ---- a/src/backend/utils/mmgr/portalmem.c -+++ b/src/backend/utils/mmgr/portalmem.c -@@ -28,6 +28,10 @@ - #include "utils/snapmgr.h" - #include "utils/timestamp.h" - -+#ifdef PGLITE_WASIX_DL -+extern int is_pglite_active; -+#endif -+ - /* - * Estimate of the maximum number of open portals a user would have, - * used in initially sizing the PortalHashTable in EnablePortalManager(). -@@ -795,6 +799,10 @@ AtAbort_Portals(void) - */ - if (portal->status == PORTAL_ACTIVE && shmem_exit_inprogress) - MarkPortalFailed(portal); -+#ifdef PGLITE_WASIX_DL -+ else if (portal->status == PORTAL_ACTIVE && is_pglite_active) -+ MarkPortalFailed(portal); -+#endif - - /* - * Do nothing else to cursors held over from a previous transaction. -diff --git a/src/include/port/wasix-dl.h b/src/include/port/wasix-dl.h -new file mode 100644 -index 0000000000..acbaf6cd8c ---- /dev/null -+++ b/src/include/port/wasix-dl.h -@@ -0,0 +1,133 @@ -+#pragma once -+ -+#ifndef I_WASIX_DL -+#define I_WASIX_DL -+ -+#undef HAVE_PTHREAD -+ -+#ifdef HAVE_GETRLIMIT -+#undef HAVE_GETRLIMIT -+#endif -+ -+#ifdef HAVE_SETSID -+#undef HAVE_SETSID -+#endif -+ -+#define PLATFORM_DEFAULT_SYNC_METHOD SYNC_METHOD_FDATASYNC -+ -+#ifndef EMSCRIPTEN_KEEPALIVE -+#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) -+#endif -+ -+#ifndef __declspec -+#define __declspec(x) __attribute__((used)) -+#endif -+ -+#define em_callback_func void -+#define emscripten_set_main_loop(...) -+#define emscripten_force_exit(...) -+#define EM_JS(...) -+ -+#if defined(PGLITE_WASIX_DL) && defined(PGLITE_WASIX_BACKEND_TIMING) -+#define PGL_BACKEND_TIMING_MAIN_PRE 1 -+#define PGL_BACKEND_TIMING_RESTART_SINGLE_USER_MAIN 2 -+#define PGL_BACKEND_TIMING_ASYNC_SINGLE_USER_MAIN 3 -+#define PGL_BACKEND_TIMING_STANDALONE_PROCESS 4 -+#define PGL_BACKEND_TIMING_GUC_INIT 5 -+#define PGL_BACKEND_TIMING_SWITCH_PARSE 6 -+#define PGL_BACKEND_TIMING_CONFIG_FILES 7 -+#define PGL_BACKEND_TIMING_DATA_DIR_LOCK 8 -+#define PGL_BACKEND_TIMING_CONTROL_FILE 9 -+#define PGL_BACKEND_TIMING_PRELOAD_LIBS 10 -+#define PGL_BACKEND_TIMING_SHARED_MEMORY 11 -+#define PGL_BACKEND_TIMING_BASE_INIT 12 -+#define PGL_BACKEND_TIMING_INIT_POSTGRES 13 -+#define PGL_BACKEND_TIMING_POST_INIT 14 -+#define PGL_BACKEND_TIMING_MESSAGE_CONTEXTS 15 -+#define PGL_BACKEND_TIMING_POSTMASTER_ENVIRONMENT 16 -+#define PGL_BACKEND_TIMING_INIT_PROC_PHASE2 17 -+#define PGL_BACKEND_TIMING_STARTUP_XLOG 18 -+#define PGL_BACKEND_TIMING_RELCACHE_CATCACHE_INIT 19 -+#define PGL_BACKEND_TIMING_TRANSACTION_SNAPSHOT 20 -+#define PGL_BACKEND_TIMING_SESSION_USER 21 -+#define PGL_BACKEND_TIMING_DATABASE_LOOKUP 22 -+#define PGL_BACKEND_TIMING_DATABASE_LOCK_RECHECK 23 -+#define PGL_BACKEND_TIMING_DATABASE_PATH 24 -+#define PGL_BACKEND_TIMING_RELCACHE_PHASE3 25 -+#define PGL_BACKEND_TIMING_CHECK_MY_DATABASE 26 -+#define PGL_BACKEND_TIMING_STARTUP_OPTIONS 27 -+#define PGL_BACKEND_TIMING_PROCESS_SETTINGS 28 -+#define PGL_BACKEND_TIMING_SESSION_INITIALIZATION 29 -+#define PGL_BACKEND_TIMING_SESSION_PRELOAD_LIBS 30 -+#define PGL_BACKEND_TIMING_INIT_MAX_BACKENDS 31 -+#define PGL_BACKEND_TIMING_CREATE_SHARED_MEMORY 32 -+#define PGL_BACKEND_TIMING_INIT_PROCESS 33 -+#define PGL_BACKEND_TIMING_RELATION_CACHE_PHASE3 34 -+#define PGL_BACKEND_TIMING_INITIALIZE_ACL 35 -+#define PGL_BACKEND_TIMING_EXEC_SIMPLE_QUERY 36 -+#define PGL_BACKEND_TIMING_EXEC_START_XACT 37 -+#define PGL_BACKEND_TIMING_EXEC_DROP_UNNAMED 38 -+#define PGL_BACKEND_TIMING_EXEC_PARSE 39 -+#define PGL_BACKEND_TIMING_EXEC_SNAPSHOT 40 -+#define PGL_BACKEND_TIMING_EXEC_ANALYZE_REWRITE 41 -+#define PGL_BACKEND_TIMING_EXEC_PLAN 42 -+#define PGL_BACKEND_TIMING_EXEC_PORTAL_START 43 -+#define PGL_BACKEND_TIMING_EXEC_DEST_RECEIVER 44 -+#define PGL_BACKEND_TIMING_EXEC_PORTAL_RUN 45 -+#define PGL_BACKEND_TIMING_EXEC_FINISH_XACT 46 -+#define PGL_BACKEND_TIMING_EXEC_COMMAND_COUNTER 47 -+#define PGL_BACKEND_TIMING_EXEC_END_COMMAND 48 -+ -+extern void pgl_backend_timing_reset(void); -+extern void pgl_backend_timing_start(int id); -+extern void pgl_backend_timing_end(int id); -+ -+#define PGL_BACKEND_TIMING_RESET() pgl_backend_timing_reset() -+#define PGL_BACKEND_TIMING_START(id) pgl_backend_timing_start(id) -+#define PGL_BACKEND_TIMING_END(id) pgl_backend_timing_end(id) -+#else -+#define PGL_BACKEND_TIMING_RESET() -+#define PGL_BACKEND_TIMING_START(id) -+#define PGL_BACKEND_TIMING_END(id) -+#endif -+ -+#define PGL_WASIX_PROTOCOL_COPY_IN 1 -+#define PGL_WASIX_PROTOCOL_COPY_OUT 2 -+#define PGL_WASIX_PROTOCOL_COPY_BOTH 3 -+extern volatile int pglite_wasix_startup_error_capture_active; -+extern void pgl_protocol_report_copy_response(int state); -+ -+#ifdef __PGLITE__ -+#include -+#define fe_utils_quote_all_identifiers quote_all_identifiers -+#define sdk_sock_flush() ((void) 0) -+#define fork() (errno = ENOSYS, -1) -+#ifndef PDEBUG -+#define PDEBUG(...) ((void) 0) -+#endif -+#ifndef WASM_USERNAME -+#define WASM_USERNAME "postgres" -+#endif -+#ifndef WASM_PREFIX -+#define WASM_PREFIX "" -+#endif -+#ifndef WASM_PGOPTS -+#define WASM_PGOPTS \ -+ "-c", "log_checkpoints=false", \ -+ "-c", "search_path=pg_catalog", \ -+ "-c", "exit_on_error=true", \ -+ "-c", "ignore_invalid_pages=on", \ -+ "-c", "temp_buffers=8MB", \ -+ "-c", "work_mem=4MB", \ -+ "-c", "fsync=on", \ -+ "-c", "synchronous_commit=on", \ -+ "-c", "wal_buffers=4MB", \ -+ "-c", "min_wal_size=80MB", \ -+ "-c", "shared_buffers=128MB" -+#endif -+extern int pgl_system(const char *command); -+#define system_wasi(command) pgl_system(command) -+#define proc_exit(arg) pg_proc_exit(arg) -+#endif -+ -+#endif /* I_WASIX_DL */ -diff --git a/src/include/port/wasix-dl/sys/ipc.h b/src/include/port/wasix-dl/sys/ipc.h -new file mode 100644 -index 0000000000..0872fdb47f ---- /dev/null -+++ b/src/include/port/wasix-dl/sys/ipc.h -@@ -0,0 +1,37 @@ -+#pragma once -+ -+#include -+ -+#ifndef IPC_PRIVATE -+#define IPC_PRIVATE ((key_t) 0) -+#endif -+#ifndef IPC_CREAT -+#define IPC_CREAT 01000 -+#endif -+#ifndef IPC_EXCL -+#define IPC_EXCL 02000 -+#endif -+#ifndef IPC_NOWAIT -+#define IPC_NOWAIT 04000 -+#endif -+ -+#ifndef IPC_RMID -+#define IPC_RMID 0 -+#endif -+#ifndef IPC_SET -+#define IPC_SET 1 -+#endif -+#ifndef IPC_STAT -+#define IPC_STAT 2 -+#endif -+ -+struct ipc_perm -+{ -+ key_t __key; -+ uid_t uid; -+ gid_t gid; -+ uid_t cuid; -+ gid_t cgid; -+ mode_t mode; -+ unsigned short __seq; -+}; -diff --git a/src/include/port/wasix-dl/sys/shm.h b/src/include/port/wasix-dl/sys/shm.h -new file mode 100644 -index 0000000000..91f59d9e39 ---- /dev/null -+++ b/src/include/port/wasix-dl/sys/shm.h -@@ -0,0 +1,30 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+#ifndef SHM_RDONLY -+#define SHM_RDONLY 010000 -+#endif -+#ifndef SHM_RND -+#define SHM_RND 020000 -+#endif -+#ifndef SHMLBA -+#define SHMLBA 4096 -+#endif -+ -+struct shmid_ds -+{ -+ struct ipc_perm shm_perm; -+ size_t shm_segsz; -+ time_t shm_atime; -+ time_t shm_dtime; -+ time_t shm_ctime; -+ unsigned long shm_nattch; -+}; -+ -+int shmget(key_t key, size_t size, int shmflg); -+void *shmat(int shmid, const void *shmaddr, int shmflg); -+int shmdt(const void *shmaddr); -+int shmctl(int shmid, int cmd, struct shmid_ds *buf); -diff --git a/src/makefiles/Makefile.wasix-dl b/src/makefiles/Makefile.wasix-dl -new file mode 100644 -index 0000000000..c0e3bac8c8 ---- /dev/null -+++ b/src/makefiles/Makefile.wasix-dl -@@ -0,0 +1,14 @@ -+# Use unversioned shared objects for WebAssembly side modules. -+rpath = -+AROPT = crs -+ -+# Rule for building a shared library from a single .o file. -+%.so: %.o -+ $(CC) $(CFLAGS) $< $(LDFLAGS) $(LDFLAGS_SL) -shared -o $@ -+ -+# WASIX side modules install import lists under the same layout contract that -+# PGlite's dynamic loader consumes, without reusing Emscripten-named variables. -+wasm_dl_include_dir := $(pkgincludedir)/wasix-dl -+wasm_dl_base_dir := $(wasm_dl_include_dir)/base -+wasm_dl_imports_dir := $(wasm_dl_base_dir)/imports -+wasm_dl_extension_dir := $(wasm_dl_include_dir)/extension -diff --git a/src/makefiles/pgxs.mk b/src/makefiles/pgxs.mk -index 79705050cf..6780d837e0 100644 ---- a/src/makefiles/pgxs.mk -+++ b/src/makefiles/pgxs.mk -@@ -249,11 +249,11 @@ ifdef MODULES - ifeq ($(with_llvm), yes) - $(foreach mod, $(MODULES), $(call install_llvm_module,$(mod),$(mod).bc)) - endif # with_llvm --ifeq ($(PORTNAME), emscripten) -+ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) - find . -name "*.o" -exec $(LLVM_NM) --undefined-only {} \; | awk '{print $$2}' | sed '/^$$/d' | sort -u > '$(MODULES).undef.txt' - find . -type f \( -name "*.o" -o -name "*.so" \) -exec $(LLVM_NM) --defined-only {} \; | awk '$$2 ~ /^[TDB]$$/ {print $$3}' | sed '/^$$/d' | sort -u > '$(MODULES).defs.txt' -- comm -23 '$(MODULES).undef.txt' '$(MODULES).defs.txt' > '$(emscripten_extension_imports_dir)/$(MODULES).imports' --endif # PORTNAME=emscripten -+ comm -23 '$(MODULES).undef.txt' '$(MODULES).defs.txt' > '$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULES).imports' -+endif # PORTNAME=emscripten wasix-dl - endif # MODULES - ifdef DOCS - ifdef docdir -@@ -276,11 +276,11 @@ ifdef MODULE_big - ifeq ($(with_llvm), yes) - $(call install_llvm_module,$(MODULE_big),$(OBJS)) - endif # with_llvm --ifeq ($(PORTNAME), emscripten) -+ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) - find . -name "*.o" -exec $(LLVM_NM) --undefined-only {} \; | awk '{print $$2}' | sed '/^$$/d' | sort -u > '$(MODULE_big).undef.txt' - find . -type f \( -name "*.o" -o -name "*.so" \) -exec $(LLVM_NM) --defined-only {} \; | awk '$$2 ~ /^[TDB]$$/ {print $$3}' | sed '/^$$/d' | sort -u > '$(MODULE_big).defs.txt' -- comm -23 '$(MODULE_big).undef.txt' '$(MODULE_big).defs.txt' > '$(emscripten_extension_imports_dir)/$(MODULE_big).imports' --endif # PORTNAME=emscripten -+ comm -23 '$(MODULE_big).undef.txt' '$(MODULE_big).defs.txt' > '$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULE_big).imports' -+endif # PORTNAME=emscripten wasix-dl - - install: install-lib - endif # MODULE_big -@@ -307,8 +307,8 @@ endif # DOCS - ifneq (,$(PROGRAM)$(SCRIPTS)$(SCRIPTS_built)) - $(MKDIR_P) '$(DESTDIR)$(bindir)' - endif --ifeq ($(PORTNAME), emscripten) -- $(MKDIR_P) '$(DESTDIR)$(emscripten_extension_imports_dir)' -+ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) -+ $(MKDIR_P) '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))' - endif - - ifdef MODULE_big -@@ -331,8 +331,8 @@ ifdef MODULES - ifeq ($(with_llvm), yes) - $(foreach mod, $(MODULES), $(call uninstall_llvm_module,$(mod))) - endif # with_llvm --ifeq ($(PORTNAME), emscripten) -- rm -f '$(DESTDIR)$(emscripten_extension_imports_dir)/$(MODULES).imports' -+ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) -+ rm -f '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULES).imports' - endif - endif # MODULES - ifdef DOCS -@@ -356,8 +356,8 @@ ifeq ($(with_llvm), yes) - $(call uninstall_llvm_module,$(MODULE_big)) - endif # with_llvm - --ifeq ($(PORTNAME), emscripten) -- rm -f '$(DESTDIR)$(emscripten_extension_imports_dir)/$(MODULE_big).imports' -+ifneq (,$(filter emscripten wasix-dl,$(PORTNAME))) -+ rm -f '$(DESTDIR)$(if $(wasm_dl_extension_imports_dir),$(wasm_dl_extension_imports_dir),$(emscripten_extension_imports_dir))/$(MODULE_big).imports' - endif - - uninstall: uninstall-lib -diff --git a/src/template/wasix-dl b/src/template/wasix-dl -new file mode 100644 -index 0000000000..f747e11735 ---- /dev/null -+++ b/src/template/wasix-dl -@@ -0,0 +1,13 @@ -+# src/template/wasix-dl -+ -+# Prefer unnamed POSIX semaphores if available, unless user overrides choice. -+if test x"$PREFERRED_SEMAPHORES" = x"" ; then -+ PREFERRED_SEMAPHORES=UNNAMED_POSIX -+fi -+ -+# Keep the same GNU feature surface as the Emscripten/WASI templates and -+# identify the dynamic-linking WASIX personality inside shared extension code. -+CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE -DPGLITE_WASIX_DL" -+ -+# Side modules must be position-independent WebAssembly objects. -+CFLAGS_SL="-fPIC" diff --git a/assets/wasix-build/pg_config_wasix.sh b/assets/wasix-build/pg_config_wasix.sh deleted file mode 100755 index 61d0141e..00000000 --- a/assets/wasix-build/pg_config_wasix.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BUILD_DIR="${BUILD_DIR:-/work/assets/wasix-build/work/docker-pglite}" -PGSRC="${PGSRC:-/work/assets/checkouts/postgres-pglite}" -PREFIX="${PGLITE_WASIX_PREFIX:-$BUILD_DIR/install}" - -case "${1:-}" in - --pgxs) - echo "$BUILD_DIR/src/makefiles/pgxs.mk" - ;; - --bindir) - echo "$PREFIX/bin" - ;; - --sharedir) - echo "$PREFIX/share" - ;; - --sysconfdir) - echo "$PREFIX/etc" - ;; - --libdir) - echo "$PREFIX/lib" - ;; - --pkglibdir) - echo "$PREFIX/lib/postgresql" - ;; - --includedir | --pkgincludedir) - echo "$PREFIX/include" - ;; - --mandir) - echo "$PREFIX/share/man" - ;; - --docdir) - echo "$PREFIX/share/doc" - ;; - --localedir) - echo "$PREFIX/share/locale" - ;; - --version) - echo "PostgreSQL 17.5-wasix-pglite" - ;; - --configure) - echo "--host=wasm32-wasix --with-template=wasix-dl" - ;; - --cc) - echo "wasixcc" - ;; - --cppflags) - echo "-I$BUILD_DIR/src/include -I$PGSRC/src/include -I$PGSRC/src/include/port/wasix-dl" - ;; - --cflags) - echo "" - ;; - --ldflags | --libs) - echo "" - ;; - *) - echo "unsupported pg_config_wasix.sh option: ${1:-}" >&2 - exit 2 - ;; -esac diff --git a/assets/wasix-build/prepare_patched_source.sh b/assets/wasix-build/prepare_patched_source.sh deleted file mode 100755 index dd69867a..00000000 --- a/assets/wasix-build/prepare_patched_source.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$ROOT/../.." && pwd)" -UPSTREAM_PGSRC="${UPSTREAM_PGSRC:-$REPO_ROOT/assets/checkouts/postgres-pglite}" -PATCHED_PGSRC="${PATCHED_PGSRC:-$ROOT/work/postgres-pglite-wasix-src}" -PATCH_PATH="${PATCH_PATH:-$ROOT/patches/postgres-pglite-wasix-dl.patch}" -POSTGRES_PGLITE_COMMIT="${POSTGRES_PGLITE_COMMIT:-$(git -C "$UPSTREAM_PGSRC" rev-parse HEAD)}" - -PATCH_SHA="$(shasum -a 256 "$PATCH_PATH" | awk '{print $1}')" -HEAD_FILE="$PATCHED_PGSRC/.pglite-oxide-source-head" -PATCH_FILE="$PATCHED_PGSRC/.pglite-oxide-patch-sha256" - -if [ -e "$PATCHED_PGSRC/.git" ] \ - && [ -f "$HEAD_FILE" ] \ - && [ -f "$PATCH_FILE" ] \ - && [ "$(cat "$HEAD_FILE")" = "$POSTGRES_PGLITE_COMMIT" ] \ - && [ "$(cat "$PATCH_FILE")" = "$PATCH_SHA" ]; then - echo "reusing patched postgres-pglite source at $PATCHED_PGSRC" - exit 0 -fi - -git -C "$UPSTREAM_PGSRC" worktree remove --force "$PATCHED_PGSRC" >/dev/null 2>&1 || true -rm -rf "$PATCHED_PGSRC" -git -C "$UPSTREAM_PGSRC" worktree prune -git -C "$UPSTREAM_PGSRC" worktree add --detach "$PATCHED_PGSRC" "$POSTGRES_PGLITE_COMMIT" -git -C "$PATCHED_PGSRC" apply --unidiff-zero --whitespace=nowarn "$PATCH_PATH" - -printf '%s' "$POSTGRES_PGLITE_COMMIT" > "$HEAD_FILE" -printf '%s' "$PATCH_SHA" > "$PATCH_FILE" -echo "prepared patched postgres-pglite source at $PATCHED_PGSRC" diff --git a/assets/wasix-build/profile_flags.sh b/assets/wasix-build/profile_flags.sh deleted file mode 100644 index ed052309..00000000 --- a/assets/wasix-build/profile_flags.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash - -pglite_oxide_wasix_profile="${PGLITE_OXIDE_BUILD_PROFILE:-release-o3}" - -case "$pglite_oxide_wasix_profile" in - debug) - PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O0 -g3}" - PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" - ;; - release) - PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O2 -g0}" - PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" - ;; - release-o3) - PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--O3 -g0 -flto=thin}" - PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:--flto=thin}" - ;; - release-os) - PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--Os -g0}" - PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" - ;; - release-oz) - PGLITE_OXIDE_PROFILE_CFLAGS="${PGLITE_OXIDE_WASIX_COPT:--Oz -g0}" - PGLITE_OXIDE_PROFILE_LDFLAGS="${PGLITE_OXIDE_WASIX_LOPT:-}" - ;; - *) - echo "unknown PGLITE_OXIDE_BUILD_PROFILE=$pglite_oxide_wasix_profile" >&2 - exit 2 - ;; -esac - -PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT="${PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT:-no}" -PGLITE_OXIDE_WASIX_BUILD_WASM_OPT="${PGLITE_OXIDE_WASIX_BUILD_WASM_OPT:-yes}" -PGLITE_OXIDE_WASIX_BACKEND_TIMING="${PGLITE_OXIDE_WASIX_BACKEND_TIMING:-0}" -if [ -z "${PGLITE_OXIDE_WASM_OPT_FLAGS:-}" ]; then - case "$pglite_oxide_wasix_profile" in - release*) - PGLITE_OXIDE_WASM_OPT_FLAGS="--converge:--strip-debug:--strip-producers" - ;; - *) - PGLITE_OXIDE_WASM_OPT_FLAGS="" - ;; - esac -elif [ "$PGLITE_OXIDE_WASM_OPT_FLAGS" = "none" ]; then - PGLITE_OXIDE_WASM_OPT_FLAGS="" -fi - -pglite_oxide_reject_asyncify_flag() { - local name="$1" - local value="${!name:-}" - - if [ -z "$value" ] || [ -n "${PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT:-}" ]; then - return - fi - - case "$value" in - *ASYNCIFY*|*asyncify*) - echo "$name contains Asyncify flags; production WASIX artifacts require WebAssembly exceptions. Set PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1 only for isolated experiments." >&2 - exit 2 - ;; - esac -} - -for pglite_oxide_flag_var in \ - PGLITE_OXIDE_PROFILE_CFLAGS \ - PGLITE_OXIDE_PROFILE_LDFLAGS \ - PGLITE_OXIDE_WASM_OPT_FLAGS \ - PGLITE_OXIDE_WASIX_COMPILER_FLAGS \ - PGLITE_OXIDE_WASIX_LINKER_FLAGS -do - pglite_oxide_reject_asyncify_flag "$pglite_oxide_flag_var" -done - -pglite_oxide_apply_wasix_profile() { - local phase="${1:-build}" - - export PGLITE_OXIDE_PROFILE_CFLAGS - export PGLITE_OXIDE_PROFILE_LDFLAGS - export WASIXCC_COMPILER_FLAGS="${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" - export WASIXCC_LINKER_FLAGS="${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" - export WASIXCC_WASM_OPT_FLAGS="$PGLITE_OXIDE_WASM_OPT_FLAGS" - if [ -n "${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT:-}" ]; then - export WASIXCC_WASM_OPT_SUPPRESS_DEFAULT="$PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT" - fi - if [ -n "${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED:-}" ]; then - export WASIXCC_WASM_OPT_PRESERVE_UNOPTIMIZED="$PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED" - fi - - if [ "$phase" = "configure" ]; then - export WASIXCC_RUN_WASM_OPT="$PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT" - else - export WASIXCC_RUN_WASM_OPT="$PGLITE_OXIDE_WASIX_BUILD_WASM_OPT" - fi -} - -pglite_oxide_wasix_profile_signature() { - printf 'profile=%s\n' "$pglite_oxide_wasix_profile" - printf 'cflags=%s\n' "$PGLITE_OXIDE_PROFILE_CFLAGS" - printf 'ldflags=%s\n' "$PGLITE_OXIDE_PROFILE_LDFLAGS" - printf 'configure_wasm_opt=%s\n' "$PGLITE_OXIDE_WASIX_CONFIGURE_WASM_OPT" - printf 'build_wasm_opt=%s\n' "$PGLITE_OXIDE_WASIX_BUILD_WASM_OPT" - printf 'wasm_opt_flags=%s\n' "$PGLITE_OXIDE_WASM_OPT_FLAGS" - printf 'wasm_opt_suppress_default=%s\n' "${PGLITE_OXIDE_WASM_OPT_SUPPRESS_DEFAULT:-}" - printf 'wasm_opt_preserve_unoptimized=%s\n' "${PGLITE_OXIDE_WASM_OPT_PRESERVE_UNOPTIMIZED:-}" - printf 'compiler_flags=%s\n' "${PGLITE_OXIDE_WASIX_COMPILER_FLAGS:-}" - printf 'linker_flags=%s\n' "${PGLITE_OXIDE_WASIX_LINKER_FLAGS:-}" - printf 'backend_timing=%s\n' "$PGLITE_OXIDE_WASIX_BACKEND_TIMING" - if [ -f ./assets/wasix-build/configure_wasix_dl.sh ]; then - printf 'configure_wasix_dl_sha256=%s\n' "$(sha256sum ./assets/wasix-build/configure_wasix_dl.sh | awk '{print $1}')" - fi -} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c b/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c deleted file mode 100644 index 0dd0114e..00000000 --- a/assets/wasix-build/wasix_shim/pglite_wasix_bridge.c +++ /dev/null @@ -1,1103 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#endif -#ifndef _DARWIN_C_SOURCE -#define _DARWIN_C_SOURCE -#endif -#ifndef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200809L -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#ifndef EMSCRIPTEN_KEEPALIVE -#define EMSCRIPTEN_KEEPALIVE __attribute__((used)) -#endif - -#define PGLITE_UID 123 -#define PGLITE_PROTOCOL_FD 1 -#define POSTGRES_MAIN_LONGJMP 100 -#define MAX_ATEXIT_FUNCS 32 -#ifdef PGLITE_WASIX_BACKEND_TIMING -#define PGL_BACKEND_TIMING_MAX 64 -#endif - -volatile int is_pglite_active = 0; -volatile int force_host_error_recovery = 0; -volatile int pglite_wasix_startup_error_capture_active = 0; -volatile sigjmp_buf postgresmain_sigjmp_buf; -volatile bool ignore_till_sync = false; -volatile bool send_ready_for_query = false; - -extern int pg_char_to_encoding_private(const char *name); -extern const char *pg_encoding_to_char_private(int encoding); - -/* - * PGlite's libpq sources intentionally use private encoding symbols in the - * embedded backend build so libpq does not leak a second copy of the encoding - * table into the main module. A standalone WASIX pg_dump links the same static - * libpq archive, whose connection path still expects libpq's public aliases. - * Provide only those aliases here so pg_dump can use the normal static - * libpgcommon archive without also pulling in libpgcommon_shlib. - */ -int __attribute__((weak)) EMSCRIPTEN_KEEPALIVE -pg_char_to_encoding(const char *name) -{ - return pg_char_to_encoding_private(name); -} - -const char __attribute__((weak)) *EMSCRIPTEN_KEEPALIVE -pg_encoding_to_char(int encoding) -{ - return pg_encoding_to_char_private(encoding); -} - -static unsigned char *pgl_wasix_input_buf; -static size_t pgl_wasix_input_len; -static size_t pgl_wasix_input_off; - -static unsigned char *pgl_wasix_output_buf; -static size_t pgl_wasix_output_len_value; -static size_t pgl_wasix_output_cap; -enum -{ - PGL_WASIX_PROTOCOL_BUFFERED = 0, - PGL_WASIX_PROTOCOL_STREAM = 1, - PGL_WASIX_PROTOCOL_HYBRID = 2, -}; -enum -{ - PGL_WASIX_PROTOCOL_COPY_NONE = 0, - PGL_WASIX_PROTOCOL_COPY_IN = 1, - PGL_WASIX_PROTOCOL_COPY_OUT = 2, - PGL_WASIX_PROTOCOL_COPY_BOTH = 3, -}; - -static int pgl_wasix_protocol_transport; -static int pgl_wasix_protocol_copy_state; -static bool pgl_wasix_protocol_stream_requested; -static bool pgl_wasix_protocol_stream_active; -static void (*atexit_funcs[MAX_ATEXIT_FUNCS])(void); -static int atexit_func_count; - -int pgl_set_protocol_transport(int mode); - -int EMSCRIPTEN_KEEPALIVE -pgl_set_protocol_stdio(int enabled) -{ - return pgl_set_protocol_transport(enabled ? PGL_WASIX_PROTOCOL_STREAM - : PGL_WASIX_PROTOCOL_BUFFERED); -} - -int EMSCRIPTEN_KEEPALIVE -pgl_set_protocol_transport(int mode) -{ - if (mode < PGL_WASIX_PROTOCOL_BUFFERED || mode > PGL_WASIX_PROTOCOL_HYBRID) - { - errno = EINVAL; - return -1; - } - - int previous = pgl_wasix_protocol_transport; - pgl_wasix_protocol_transport = mode; - pgl_wasix_protocol_stream_active = mode == PGL_WASIX_PROTOCOL_STREAM; - if (mode != PGL_WASIX_PROTOCOL_HYBRID) - { - pgl_wasix_protocol_copy_state = PGL_WASIX_PROTOCOL_COPY_NONE; - pgl_wasix_protocol_stream_requested = false; - } - return previous; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_protocol_stream_active(void) -{ - return pgl_wasix_protocol_stream_active ? 1 : 0; -} - -void EMSCRIPTEN_KEEPALIVE -pgl_protocol_report_copy_response(int state) -{ - if (state < PGL_WASIX_PROTOCOL_COPY_NONE || - state > PGL_WASIX_PROTOCOL_COPY_BOTH) - { - errno = EINVAL; - return; - } - pgl_wasix_protocol_copy_state = state; - pgl_wasix_protocol_stream_requested = - pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_HYBRID && - state != PGL_WASIX_PROTOCOL_COPY_NONE; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_protocol_copy_state(void) -{ - return pgl_wasix_protocol_copy_state; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_set_force_host_error_recovery(int new_value) -{ - int current = force_host_error_recovery; - force_host_error_recovery = new_value != 0; - return current; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_setPGliteActive(int new_value) -{ - int current = is_pglite_active; - is_pglite_active = new_value; - if (new_value == 0) - { - struct itimerval zero = {{0, 0}, {0, 0}}; - (void) setitimer(ITIMER_REAL, &zero, NULL); - } - return current; -} - -void EMSCRIPTEN_KEEPALIVE -pgl_longjmp(jmp_buf env, int val) -{ - /* - * Some hosts can run nested WebAssembly exception unwinds and can preserve - * PostgreSQL's normal PG_TRY/PG_CATCH behavior. Hosts without that support - * must route every PostgreSQL ERROR longjmp through the existing - * single-user process-exit boundary; Rust then invokes PostgresMainLongJmp() - * to perform the same top-level cleanup and emit the backend ErrorResponse. - */ - if (is_pglite_active && - (force_host_error_recovery || - memcmp(env, (void *) postgresmain_sigjmp_buf, sizeof(jmp_buf)) == 0)) - { - exit(POSTGRES_MAIN_LONGJMP); - } - longjmp(env, val); -} - -void EMSCRIPTEN_KEEPALIVE -pgl_siglongjmp(sigjmp_buf env, int val) -{ - pgl_longjmp(env, val); -} - -#ifdef PGLITE_WASIX_BACKEND_TIMING -static uint64_t pgl_backend_timing_started_us[PGL_BACKEND_TIMING_MAX]; -static uint64_t pgl_backend_timing_elapsed_us_value[PGL_BACKEND_TIMING_MAX]; -static bool pgl_backend_timing_seen[PGL_BACKEND_TIMING_MAX]; - -static uint64_t -pgl_monotonic_us(void) -{ - struct timespec ts; - if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) - return 0; - return ((uint64_t) ts.tv_sec * 1000000ULL) + ((uint64_t) ts.tv_nsec / 1000ULL); -} - -void EMSCRIPTEN_KEEPALIVE -pgl_backend_timing_reset(void) -{ - memset(pgl_backend_timing_started_us, 0, sizeof(pgl_backend_timing_started_us)); - memset(pgl_backend_timing_elapsed_us_value, 0, sizeof(pgl_backend_timing_elapsed_us_value)); - memset(pgl_backend_timing_seen, 0, sizeof(pgl_backend_timing_seen)); -} - -void EMSCRIPTEN_KEEPALIVE -pgl_backend_timing_start(int id) -{ - if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX) - return; - pgl_backend_timing_started_us[id] = pgl_monotonic_us(); -} - -void EMSCRIPTEN_KEEPALIVE -pgl_backend_timing_end(int id) -{ - if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX) - return; - - uint64_t started = pgl_backend_timing_started_us[id]; - uint64_t ended = pgl_monotonic_us(); - if (started == 0 || ended < started) - return; - - pgl_backend_timing_elapsed_us_value[id] += ended - started; - pgl_backend_timing_seen[id] = true; - pgl_backend_timing_started_us[id] = 0; -} - -int64_t EMSCRIPTEN_KEEPALIVE -pgl_backend_timing_elapsed_us(int id) -{ - if (id <= 0 || id >= PGL_BACKEND_TIMING_MAX || !pgl_backend_timing_seen[id]) - return -1; - return (int64_t) pgl_backend_timing_elapsed_us_value[id]; -} -#endif - -int EMSCRIPTEN_KEEPALIVE -pgl_wasix_input_reset(void) -{ - pgl_wasix_input_len = 0; - pgl_wasix_input_off = 0; - return 0; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_wasix_input_write(const void *buffer, size_t length) -{ - if (length == 0) - return 0; - if (buffer == NULL) - { - errno = EINVAL; - return -1; - } - - if (pgl_wasix_input_off == pgl_wasix_input_len) - { - pgl_wasix_input_len = 0; - pgl_wasix_input_off = 0; - } - - size_t new_len = pgl_wasix_input_len + length; - unsigned char *new_buf = realloc(pgl_wasix_input_buf, new_len); - if (new_buf == NULL) - { - errno = ENOMEM; - return -1; - } - - pgl_wasix_input_buf = new_buf; - memcpy(pgl_wasix_input_buf + pgl_wasix_input_len, buffer, length); - pgl_wasix_input_len = new_len; - return (int) length; -} - -size_t EMSCRIPTEN_KEEPALIVE -pgl_wasix_input_available(void) -{ - if (pgl_wasix_input_off >= pgl_wasix_input_len) - return 0; - return pgl_wasix_input_len - pgl_wasix_input_off; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_wasix_input_peek(void) -{ - if (pgl_wasix_input_off >= pgl_wasix_input_len) - return -1; - return (int) pgl_wasix_input_buf[pgl_wasix_input_off]; -} - -static ssize_t -pgl_wasix_buffer_read(void *buffer, size_t max_length) -{ - if (buffer == NULL || max_length == 0) - return 0; - if (pgl_wasix_input_off >= pgl_wasix_input_len) - return 0; - - size_t available = pgl_wasix_input_len - pgl_wasix_input_off; - size_t to_copy = available < max_length ? available : max_length; - memcpy(buffer, pgl_wasix_input_buf + pgl_wasix_input_off, to_copy); - pgl_wasix_input_off += to_copy; - return (ssize_t) to_copy; -} - -static int -pgl_wasix_flush_output_to_stdio(void) -{ - size_t off = 0; - while (off < pgl_wasix_output_len_value) - { - ssize_t written = write(STDOUT_FILENO, - pgl_wasix_output_buf + off, - pgl_wasix_output_len_value - off); - if (written < 0) - return -1; - if (written == 0) - { - errno = EIO; - return -1; - } - off += (size_t) written; - } - pgl_wasix_output_len_value = 0; - return 0; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_wasix_output_reset(void) -{ - pgl_wasix_output_len_value = 0; - pgl_wasix_protocol_copy_state = PGL_WASIX_PROTOCOL_COPY_NONE; - pgl_wasix_protocol_stream_requested = false; - return 0; -} - -size_t EMSCRIPTEN_KEEPALIVE -pgl_wasix_output_len(void) -{ - return pgl_wasix_output_len_value; -} - -size_t EMSCRIPTEN_KEEPALIVE -pgl_wasix_output_read(void *buffer, size_t max_length) -{ - if (buffer == NULL || max_length == 0 || pgl_wasix_output_len_value == 0) - return 0; - - size_t to_copy = pgl_wasix_output_len_value < max_length - ? pgl_wasix_output_len_value - : max_length; - memcpy(buffer, pgl_wasix_output_buf, to_copy); - return to_copy; -} - -static ssize_t -pgl_wasix_buffer_write(const void *buffer, size_t length) -{ - if (length == 0) - return 0; - if (buffer == NULL) - { - errno = EINVAL; - return -1; - } - - size_t required = pgl_wasix_output_len_value + length; - if (required > pgl_wasix_output_cap) - { - size_t next_cap = pgl_wasix_output_cap ? pgl_wasix_output_cap : 8192; - while (next_cap < required) - next_cap *= 2; - unsigned char *new_buf = realloc(pgl_wasix_output_buf, next_cap); - if (new_buf == NULL) - { - errno = ENOMEM; - return -1; - } - pgl_wasix_output_buf = new_buf; - pgl_wasix_output_cap = next_cap; - } - - memcpy(pgl_wasix_output_buf + pgl_wasix_output_len_value, buffer, length); - pgl_wasix_output_len_value += length; - if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_HYBRID && - pgl_wasix_protocol_stream_requested) - { - if (pgl_wasix_flush_output_to_stdio() != 0) - return -1; - pgl_wasix_protocol_stream_active = true; - pgl_wasix_protocol_stream_requested = false; - } - return (ssize_t) length; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_system(const char *command) -{ - (void) command; - errno = ENOSYS; - return -1; -} - -__attribute__((weak)) void EMSCRIPTEN_KEEPALIVE -pg_free(void *ptr) -{ - free(ptr); -} - -static char * -pgl_locale_file_path(void) -{ - const char *sysconfdir = getenv("PGSYSCONFDIR"); - if (sysconfdir == NULL || sysconfdir[0] == '\0') - { - errno = ENOENT; - return NULL; - } - if (access(sysconfdir, F_OK) != 0) - return NULL; - - const char *name = "/locale"; - size_t len = strlen(sysconfdir) + strlen(name) + 1; - char *path = malloc(len); - if (path == NULL) - return NULL; - - snprintf(path, len, "%s%s", sysconfdir, name); - return path; -} - -static FILE * -pgl_open_locale_pipe(const char *command, const char *mode) -{ - if (command == NULL || mode == NULL || strcmp(command, "locale -a") != 0 || - strcmp(mode, "r") != 0) - { - errno = ENOSYS; - return NULL; - } - - char *path = pgl_locale_file_path(); - if (path == NULL) - { - if (errno == 0) - errno = ENOMEM; - return NULL; - } - - if (access(path, F_OK) != 0) - { - FILE *file = fopen(path, "w"); - if (file != NULL) - { - const char *encoding = getenv("PGCLIENTENCODING"); - if (encoding == NULL || encoding[0] == '\0') - encoding = "UTF8"; - fprintf(file, "C\nC.%s\nPOSIX\n%s\n", encoding, encoding); - fclose(file); - } - } - - FILE *file = fopen(path, mode); - free(path); - return file; -} - -__attribute__((weak)) FILE *EMSCRIPTEN_KEEPALIVE -pgl_popen(const char *command, const char *mode) -{ - return pgl_open_locale_pipe(command, mode); -} - -__attribute__((weak)) int EMSCRIPTEN_KEEPALIVE -pgl_pclose(FILE *file) -{ - if (file == NULL) - { - errno = EINVAL; - return -1; - } - return fclose(file); -} - -uid_t EMSCRIPTEN_KEEPALIVE -pgl_geteuid(void) -{ - return PGLITE_UID; -} - -uid_t EMSCRIPTEN_KEEPALIVE -pgl_getuid(void) -{ - return PGLITE_UID; -} - -struct passwd *EMSCRIPTEN_KEEPALIVE -pgl_getpwuid(uid_t uid) -{ - if (uid != PGLITE_UID) - { - errno = ENOENT; - return NULL; - } - - static struct passwd pw; - static char name[] = "postgres"; - static char passwd[] = "x"; - static char gecos[] = "Static User"; - static char dir[] = "/home/postgres"; - static char shell[] = "/bin/sh"; - - pw.pw_name = name; - pw.pw_passwd = passwd; - pw.pw_uid = uid; - pw.pw_gid = uid; - pw.pw_gecos = gecos; - pw.pw_dir = dir; - pw.pw_shell = shell; - - return &pw; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_atexit(void (*function)(void)) -{ - if (atexit_func_count >= MAX_ATEXIT_FUNCS) - return -1; - atexit_funcs[atexit_func_count++] = function; - return 0; -} - -void EMSCRIPTEN_KEEPALIVE -pgl_run_atexit_funcs(void) -{ - for (int i = atexit_func_count - 1; i >= 0; i--) - { - if (atexit_funcs[i]) - atexit_funcs[i](); - } - atexit_func_count = 0; -} - -static void -pgl_clear_interval_timer(void) -{ - struct itimerval zero = {{0, 0}, {0, 0}}; - (void) setitimer(ITIMER_REAL, &zero, NULL); -} - -void EMSCRIPTEN_KEEPALIVE -pgl_exit(int status) -{ - pgl_clear_interval_timer(); - optind = 1; - if (pglite_wasix_startup_error_capture_active && status != 0) - { - pglite_wasix_startup_error_capture_active = 0; - __builtin_trap(); - } - exit(status); -} - -int EMSCRIPTEN_KEEPALIVE -pgl_munmap(void *addr, size_t length) -{ - if (addr == NULL || length == 0) - { - errno = EINVAL; - return -1; - } - return munmap(addr, length); -} - -int EMSCRIPTEN_KEEPALIVE -pgl_fcntl(int fd, int cmd, ...) -{ - va_list args; - long arg = 0; - - switch (cmd) - { -#ifdef F_GETFL - case F_GETFL: - if (fd == PGLITE_PROTOCOL_FD) - return 0; - return fcntl(fd, cmd); -#endif -#ifdef F_GETFD - case F_GETFD: - if (fd == PGLITE_PROTOCOL_FD) - return 0; - return fcntl(fd, cmd); -#endif -#ifdef F_SETFL - case F_SETFL: - va_start(args, cmd); - arg = va_arg(args, long); - va_end(args); - if (fd == PGLITE_PROTOCOL_FD) - { -#ifdef O_NONBLOCK - if ((arg & ~((long) O_NONBLOCK)) == 0) - return 0; -#else - if (arg == 0) - return 0; -#endif - errno = EINVAL; - return -1; - } - return fcntl(fd, cmd, (int) arg); -#endif -#ifdef F_SETFD - case F_SETFD: - va_start(args, cmd); - arg = va_arg(args, long); - va_end(args); - if (fd == PGLITE_PROTOCOL_FD) - { -#ifdef FD_CLOEXEC - if ((arg & ~((long) FD_CLOEXEC)) == 0) - return 0; -#else - if (arg == 0) - return 0; -#endif - errno = EINVAL; - return -1; - } - return fcntl(fd, cmd, (int) arg); -#endif - default: - errno = EINVAL; - return -1; - } -} - -static int -pgl_write_int_sockopt(void *optval, socklen_t *optlen, int value) -{ - if (optval == NULL || optlen == NULL || *optlen < (socklen_t) sizeof(int)) - { - errno = EINVAL; - return -1; - } - memcpy(optval, &value, sizeof(value)); - *optlen = (socklen_t) sizeof(value); - return 0; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen) -{ - if (fd != PGLITE_PROTOCOL_FD) - return setsockopt(fd, level, optname, optval, optlen); - - if (optval == NULL && optlen != 0) - { - errno = EINVAL; - return -1; - } - - if (level == SOL_SOCKET) - { - switch (optname) - { -#ifdef SO_KEEPALIVE - case SO_KEEPALIVE: -#endif -#ifdef SO_REUSEADDR - case SO_REUSEADDR: -#endif -#ifdef SO_SNDBUF - case SO_SNDBUF: -#endif -#ifdef SO_RCVBUF - case SO_RCVBUF: -#endif -#ifdef SO_NOSIGPIPE - case SO_NOSIGPIPE: -#endif - return 0; - default: - break; - } - } - - if (level == IPPROTO_TCP) - { - switch (optname) - { -#ifdef TCP_NODELAY - case TCP_NODELAY: -#endif -#ifdef TCP_KEEPIDLE - case TCP_KEEPIDLE: -#endif -#ifdef TCP_KEEPINTVL - case TCP_KEEPINTVL: -#endif -#ifdef TCP_KEEPCNT - case TCP_KEEPCNT: -#endif -#ifdef TCP_USER_TIMEOUT - case TCP_USER_TIMEOUT: -#endif - return 0; - default: - break; - } - } - - errno = ENOPROTOOPT; - return -1; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen) -{ - if (fd != PGLITE_PROTOCOL_FD) - return getsockopt(fd, level, optname, optval, optlen); - - if (level == SOL_SOCKET) - { - switch (optname) - { -#ifdef SO_ERROR - case SO_ERROR: - return pgl_write_int_sockopt(optval, optlen, 0); -#endif -#ifdef SO_TYPE - case SO_TYPE: - return pgl_write_int_sockopt(optval, optlen, SOCK_STREAM); -#endif -#ifdef SO_SNDBUF - case SO_SNDBUF: - return pgl_write_int_sockopt(optval, optlen, 32768); -#endif -#ifdef SO_RCVBUF - case SO_RCVBUF: - return pgl_write_int_sockopt(optval, optlen, 32768); -#endif - default: - break; - } - } - - if (level == IPPROTO_TCP) - { - switch (optname) - { -#ifdef TCP_KEEPIDLE - case TCP_KEEPIDLE: -#endif -#ifdef TCP_KEEPINTVL - case TCP_KEEPINTVL: -#endif -#ifdef TCP_KEEPCNT - case TCP_KEEPCNT: -#endif -#ifdef TCP_USER_TIMEOUT - case TCP_USER_TIMEOUT: -#endif - return pgl_write_int_sockopt(optval, optlen, 0); - default: - break; - } - } - - errno = ENOPROTOOPT; - return -1; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_getsockname(int fd, struct sockaddr *addr, socklen_t *len) -{ - if (fd != PGLITE_PROTOCOL_FD) - return getsockname(fd, addr, len); - - if (addr == NULL || len == NULL || *len < (socklen_t) sizeof(sa_family_t)) - { - errno = EINVAL; - return -1; - } - - memset(addr, 0, *len); - addr->sa_family = AF_UNIX; - *len = (socklen_t) sizeof(sa_family_t); - return 0; -} - -ssize_t EMSCRIPTEN_KEEPALIVE -pgl_recv(int fd, void *buf, size_t n, int flags) -{ - if (fd != PGLITE_PROTOCOL_FD) - return recv(fd, buf, n, flags); - if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || - pgl_wasix_protocol_stream_active) - { - (void) flags; - return read(STDIN_FILENO, buf, n); - } - return pgl_wasix_buffer_read(buf, n); -} - -ssize_t EMSCRIPTEN_KEEPALIVE -pgl_send(int fd, const void *buf, size_t n, int flags) -{ - if (fd != PGLITE_PROTOCOL_FD) - return send(fd, buf, n, flags); - if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || - pgl_wasix_protocol_stream_active) - { - (void) flags; - return write(STDOUT_FILENO, buf, n); - } - return pgl_wasix_buffer_write(buf, n); -} - -int EMSCRIPTEN_KEEPALIVE -pgl_connect(int socket, const struct sockaddr *address, socklen_t address_len) -{ - if (socket != PGLITE_PROTOCOL_FD) - return connect(socket, address, address_len); - errno = ENOSYS; - return -1; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_poll(struct pollfd fds[], nfds_t nfds, int timeout) -{ - bool has_protocol_fd = false; - int ready = 0; - - for (nfds_t i = 0; i < nfds; i++) - { - if (fds[i].fd == PGLITE_PROTOCOL_FD) - { - has_protocol_fd = true; - break; - } - } - - if (!has_protocol_fd) - return poll(fds, nfds, timeout); - - for (nfds_t i = 0; i < nfds; i++) - { - fds[i].revents = 0; - if (fds[i].fd != PGLITE_PROTOCOL_FD) - { - struct pollfd one = fds[i]; - int rc = poll(&one, 1, 0); - if (rc < 0) - return rc; - fds[i].revents = one.revents; - if (rc > 0) - ready++; - continue; - } - if (pgl_wasix_protocol_transport == PGL_WASIX_PROTOCOL_STREAM || - pgl_wasix_protocol_stream_active) - { - struct pollfd one; - int rc; - - one.fd = STDIN_FILENO; - one.events = fds[i].events; - one.revents = 0; - rc = poll(&one, 1, 0); - if (rc < 0) - return rc; - fds[i].revents = one.revents; - if (rc > 0) - ready++; - continue; - } -#ifdef POLLIN - if ((fds[i].events & POLLIN) && - pgl_wasix_input_available() > 0) - fds[i].revents |= POLLIN; -#endif -#ifdef POLLOUT - if (fds[i].events & POLLOUT) - fds[i].revents |= POLLOUT; -#endif - if (fds[i].revents) - ready++; - } - return ready; -} - -typedef struct WasixShmSegment -{ - int shmid; - key_t key; - size_t size; - void *addr; - unsigned long nattch; - struct WasixShmSegment *next; -} WasixShmSegment; - -static WasixShmSegment *wasix_shm_list; -static int wasix_next_shmid = 1; - -static WasixShmSegment * -find_by_key(key_t key) -{ - for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) - { - if (seg->key == key) - return seg; - } - return NULL; -} - -static WasixShmSegment * -find_by_id(int shmid) -{ - for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) - { - if (seg->shmid == shmid) - return seg; - } - return NULL; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_shmget(key_t key, size_t size, int shmflg) -{ - WasixShmSegment *existing = find_by_key(key); - - if (existing) - { - if ((shmflg & IPC_CREAT) && (shmflg & IPC_EXCL)) - { - errno = EEXIST; - return -1; - } - return existing->shmid; - } - - if ((shmflg & IPC_CREAT) == 0) - { - errno = ENOENT; - return -1; - } - - size_t alloc_size = size ? size : 1; - long pagesize = sysconf(_SC_PAGESIZE); - if (pagesize > 0) - { - size_t page = (size_t) pagesize; - alloc_size = ((alloc_size + page - 1) / page) * page; - } - - void *addr = calloc(1, alloc_size); - if (!addr) - { - errno = ENOMEM; - return -1; - } - - WasixShmSegment *seg = calloc(1, sizeof(*seg)); - if (!seg) - { - free(addr); - errno = ENOMEM; - return -1; - } - - seg->shmid = wasix_next_shmid++; - seg->key = key; - seg->size = size; - seg->addr = addr; - seg->next = wasix_shm_list; - wasix_shm_list = seg; - - return seg->shmid; -} - -void *EMSCRIPTEN_KEEPALIVE -pgl_shmat(int shmid, const void *shmaddr, int shmflg) -{ - (void) shmaddr; - (void) shmflg; - - WasixShmSegment *seg = find_by_id(shmid); - if (!seg) - { - errno = EINVAL; - return (void *) -1; - } - - seg->nattch++; - return seg->addr; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_shmdt(const void *shmaddr) -{ - for (WasixShmSegment *seg = wasix_shm_list; seg; seg = seg->next) - { - if (seg->addr == shmaddr) - { - if (seg->nattch > 0) - seg->nattch--; - return 0; - } - } - - errno = EINVAL; - return -1; -} - -int EMSCRIPTEN_KEEPALIVE -pgl_shmctl(int shmid, int cmd, struct shmid_ds *buf) -{ - WasixShmSegment *prev = NULL; - WasixShmSegment *seg = wasix_shm_list; - - while (seg && seg->shmid != shmid) - { - prev = seg; - seg = seg->next; - } - - if (!seg) - { - errno = EINVAL; - return -1; - } - - switch (cmd) - { - case IPC_RMID: - if (prev) - prev->next = seg->next; - else - wasix_shm_list = seg->next; - free(seg->addr); - free(seg); - return 0; - - case IPC_STAT: - if (!buf) - { - errno = EINVAL; - return -1; - } - memset(buf, 0, sizeof(*buf)); -#if defined(__APPLE__) - buf->shm_perm._key = seg->key; -#else - buf->shm_perm.__key = seg->key; -#endif - buf->shm_segsz = seg->size; - buf->shm_nattch = seg->nattch; - buf->shm_atime = buf->shm_dtime = buf->shm_ctime = time(NULL); - return 0; - - case IPC_SET: - if (!buf) - { - errno = EINVAL; - return -1; - } - seg->size = buf->shm_segsz; - return 0; - - default: - errno = EINVAL; - return -1; - } -} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c b/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c deleted file mode 100644 index 742e4bc1..00000000 --- a/assets/wasix-build/wasix_shim/pglite_wasix_bridge_abi_test.c +++ /dev/null @@ -1,330 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#endif -#ifndef _DARWIN_C_SOURCE -#define _DARWIN_C_SOURCE -#endif -#ifndef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200809L -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#define CHECK(condition) \ - do \ - { \ - if (!(condition)) \ - { \ - fprintf(stderr, "bridge ABI check failed at %s:%d: %s\n", __FILE__, __LINE__, \ - #condition); \ - return 1; \ - } \ - } while (0) - -FILE *pgl_popen(const char *command, const char *mode); -int pgl_system(const char *command); -int pgl_set_force_host_error_recovery(int new_value); -int pgl_setPGliteActive(int new_value); -int pgl_atexit(void (*function)(void)); -void pgl_run_atexit_funcs(void); -uid_t pgl_geteuid(void); -uid_t pgl_getuid(void); -struct passwd *pgl_getpwuid(uid_t uid); -int pgl_wasix_input_reset(void); -int pgl_wasix_input_write(const void *buffer, size_t length); -size_t pgl_wasix_input_available(void); -int pgl_wasix_output_reset(void); -size_t pgl_wasix_output_len(void); -size_t pgl_wasix_output_read(void *buffer, size_t max_length); -int pgl_fcntl(int fd, int cmd, ...); -int pgl_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen); -int pgl_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen); -int pgl_getsockname(int fd, struct sockaddr *addr, socklen_t *len); -int pgl_set_protocol_stdio(int enabled); -int pgl_set_protocol_transport(int mode); -int pgl_protocol_stream_active(void); -void pgl_protocol_report_copy_response(int state); -int pgl_protocol_copy_state(void); -ssize_t pgl_recv(int fd, void *buf, size_t n, int flags); -ssize_t pgl_send(int fd, const void *buf, size_t n, int flags); -int pgl_connect(int socket, const struct sockaddr *address, socklen_t address_len); -int pgl_poll(struct pollfd fds[], nfds_t nfds, int timeout); -int pgl_munmap(void *addr, size_t length); -int pgl_shmget(key_t key, size_t size, int shmflg); -void *pgl_shmat(int shmid, const void *shmaddr, int shmflg); -int pgl_shmdt(const void *shmaddr); -int pgl_shmctl(int shmid, int cmd, struct shmid_ds *buf); - -int -pg_char_to_encoding_private(const char *name) -{ - return strcmp(name, "UTF8") == 0 ? 6 : -1; -} - -const char * -pg_encoding_to_char_private(int encoding) -{ - return encoding == 6 ? "UTF8" : ""; -} - -static int atexit_counter; - -static void -increment_atexit_counter(void) -{ - atexit_counter++; -} - -static int -check_locale_pipe(void) -{ - char temp_template[] = "/tmp/pglite-bridge-abi-XXXXXX"; - char *dir = mkdtemp(temp_template); - CHECK(dir != NULL); - CHECK(setenv("PGSYSCONFDIR", dir, 1) == 0); - CHECK(setenv("PGCLIENTENCODING", "UTF8", 1) == 0); - - errno = 0; - CHECK(pgl_popen("uname -a", "r") == NULL); - CHECK(errno == ENOSYS); - errno = 0; - CHECK(pgl_popen("locale -a", "w") == NULL); - CHECK(errno == ENOSYS); - - FILE *file = pgl_popen("locale -a", "r"); - CHECK(file != NULL); - char contents[128] = {0}; - size_t read_len = fread(contents, 1, sizeof(contents) - 1, file); - CHECK(fclose(file) == 0); - CHECK(read_len > 0); - CHECK(strstr(contents, "C\n") != NULL); - CHECK(strstr(contents, "C.UTF8\n") != NULL); - CHECK(strstr(contents, "POSIX\n") != NULL); - CHECK(unsetenv("PGSYSCONFDIR") == 0); - errno = 0; - CHECK(pgl_popen("locale -a", "r") == NULL); - CHECK(errno == ENOENT); - return 0; -} - -static int -check_identity_and_fail_closed_calls(void) -{ - CHECK(pgl_geteuid() == 123); - CHECK(pgl_getuid() == 123); - struct passwd *pw = pgl_getpwuid(123); - CHECK(pw != NULL); - CHECK(strcmp(pw->pw_name, "postgres") == 0); - CHECK(pw->pw_uid == 123); - errno = 0; - CHECK(pgl_getpwuid(999) == NULL); - CHECK(errno == ENOENT); - - errno = 0; - CHECK(pgl_system("echo unsafe") == -1); - CHECK(errno == ENOSYS); - - CHECK(pgl_set_force_host_error_recovery(1) == 0); - CHECK(pgl_set_force_host_error_recovery(0) == 1); - CHECK(pgl_setPGliteActive(1) == 0); - CHECK(pgl_setPGliteActive(0) == 1); - CHECK(pgl_atexit(increment_atexit_counter) == 0); - CHECK(pgl_atexit(increment_atexit_counter) == 0); - pgl_run_atexit_funcs(); - CHECK(atexit_counter == 2); - pgl_run_atexit_funcs(); - CHECK(atexit_counter == 2); - - errno = 0; - CHECK(pgl_connect(1, NULL, 0) == -1); - CHECK(errno == ENOSYS); - errno = 0; - CHECK(pgl_connect(-1, NULL, 0) == -1); - CHECK(errno == EBADF); - return 0; -} - -static int -check_protocol_socket(void) -{ - char buf[8] = {0}; - const char input[] = "abc"; - const char output[] = "xyz"; - - CHECK(pgl_wasix_input_reset() == 0); - CHECK(pgl_wasix_output_reset() == 0); - CHECK(pgl_recv(1, buf, sizeof(buf), 0) == 0); - CHECK(pgl_wasix_input_write(input, sizeof(input) - 1) == (int) (sizeof(input) - 1)); - CHECK(pgl_wasix_input_available() == sizeof(input) - 1); - CHECK(pgl_recv(1, buf, 2, 0) == 2); - CHECK(memcmp(buf, "ab", 2) == 0); - CHECK(pgl_wasix_input_available() == 1); - - CHECK(pgl_send(1, output, sizeof(output) - 1, 0) == (ssize_t) (sizeof(output) - 1)); - CHECK(pgl_wasix_output_len() == sizeof(output) - 1); - memset(buf, 0, sizeof(buf)); - CHECK(pgl_wasix_output_read(buf, sizeof(buf)) == sizeof(output) - 1); - CHECK(memcmp(buf, output, sizeof(output) - 1) == 0); - - CHECK(pgl_set_protocol_stdio(0) == 0); - CHECK(pgl_protocol_stream_active() == 0); - CHECK(pgl_set_protocol_stdio(1) == 0); - CHECK(pgl_protocol_stream_active() == 1); - CHECK(pgl_set_protocol_stdio(0) == 1); - CHECK(pgl_protocol_stream_active() == 0); - CHECK(pgl_set_protocol_transport(2) == 0); - CHECK(pgl_protocol_stream_active() == 0); - CHECK(pgl_protocol_copy_state() == 0); - pgl_protocol_report_copy_response(1); - CHECK(pgl_protocol_copy_state() == 1); - CHECK(pgl_send(1, output, sizeof(output) - 1, 0) == (ssize_t) (sizeof(output) - 1)); - CHECK(pgl_protocol_stream_active() == 1); - CHECK(pgl_set_protocol_transport(0) == 2); - CHECK(pgl_protocol_stream_active() == 0); - CHECK(pgl_protocol_copy_state() == 0); - CHECK(pgl_set_protocol_transport(2) == 0); - pgl_protocol_report_copy_response(0); - CHECK(pgl_protocol_copy_state() == 0); - CHECK(pgl_set_protocol_transport(0) == 2); - errno = 0; - CHECK(pgl_set_protocol_transport(99) == -1); - CHECK(errno == EINVAL); - -#ifdef ENOTSOCK - errno = 0; - CHECK(pgl_recv(2, buf, sizeof(buf), 0) == -1); - CHECK(errno == ENOTSOCK); - errno = 0; - CHECK(pgl_send(2, output, sizeof(output) - 1, 0) == -1); - CHECK(errno == ENOTSOCK); -#endif - - CHECK(pgl_fcntl(1, F_GETFL) == 0); - CHECK(pgl_fcntl(1, F_SETFL, O_NONBLOCK) == 0); -#ifdef O_APPEND - errno = 0; - CHECK(pgl_fcntl(1, F_SETFL, O_APPEND) == -1); - CHECK(errno == EINVAL); -#endif - - int opt = 1; - CHECK(pgl_setsockopt(1, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt)) == 0); -#ifdef TCP_NODELAY - CHECK(pgl_setsockopt(1, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)) == 0); -#endif - errno = 0; - CHECK(pgl_setsockopt(1, SOL_SOCKET, 0x7ffffffe, &opt, sizeof(opt)) == -1); - CHECK(errno == ENOPROTOOPT); - - opt = 0; - socklen_t optlen = sizeof(opt); - CHECK(pgl_getsockopt(1, SOL_SOCKET, SO_TYPE, &opt, &optlen) == 0); - CHECK(opt == SOCK_STREAM); - CHECK(optlen == (socklen_t) sizeof(opt)); - errno = 0; - optlen = sizeof(opt); - CHECK(pgl_getsockopt(1, SOL_SOCKET, 0x7ffffffd, &opt, &optlen) == -1); - CHECK(errno == ENOPROTOOPT); - - struct sockaddr_storage addr; - socklen_t addrlen = sizeof(addr); - CHECK(pgl_getsockname(1, (struct sockaddr *) &addr, &addrlen) == 0); - CHECK(addr.ss_family == AF_UNIX); - - CHECK(pgl_wasix_input_reset() == 0); - struct pollfd fds[1] = {{.fd = 1, .events = POLLIN, .revents = 0}}; - CHECK(pgl_poll(fds, 1, 0) == 0); - CHECK(fds[0].revents == 0); - CHECK(pgl_wasix_input_write("q", 1) == 1); - CHECK(pgl_poll(fds, 1, 0) == 1); - CHECK((fds[0].revents & POLLIN) != 0); - - struct pollfd ignored[1] = {{.fd = -1, .events = POLLIN, .revents = 0}}; - CHECK(pgl_poll(ignored, 1, 0) == 0); - struct pollfd mixed[2] = { - {.fd = 1, .events = POLLOUT, .revents = 0}, - {.fd = 99, .events = POLLIN, .revents = 0}, - }; - CHECK(pgl_poll(mixed, 2, 0) == 2); - CHECK((mixed[0].revents & POLLOUT) != 0); -#ifdef POLLNVAL - CHECK((mixed[1].revents & POLLNVAL) != 0); -#endif - return 0; -} - -static int -check_memory_and_shared_memory(void) -{ - errno = 0; - CHECK(pgl_munmap(NULL, 0) == -1); - CHECK(errno == EINVAL); - -#if defined(MAP_ANON) - int anon_flag = MAP_ANON; -#elif defined(MAP_ANONYMOUS) - int anon_flag = MAP_ANONYMOUS; -#else - int anon_flag = 0; -#endif - if (anon_flag != 0) - { - void *mapping = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | anon_flag, -1, 0); - CHECK(mapping != MAP_FAILED); - CHECK(pgl_munmap(mapping, 4096) == 0); - } - - key_t key = 4242; - int shmid = pgl_shmget(key, 64, IPC_CREAT | IPC_EXCL); - CHECK(shmid > 0); - errno = 0; - CHECK(pgl_shmget(key, 64, IPC_CREAT | IPC_EXCL) == -1); - CHECK(errno == EEXIST); - errno = 0; - CHECK(pgl_shmget(key + 1, 64, 0) == -1); - CHECK(errno == ENOENT); - - void *addr = pgl_shmat(shmid, NULL, 0); - CHECK(addr != (void *) -1); - memset(addr, 0x7b, 64); - - struct shmid_ds statbuf; - CHECK(pgl_shmctl(shmid, IPC_STAT, &statbuf) == 0); - CHECK(statbuf.shm_segsz == 64); - CHECK(statbuf.shm_nattch == 1); - CHECK(pgl_shmdt(addr) == 0); - CHECK(pgl_shmctl(shmid, IPC_RMID, NULL) == 0); - errno = 0; - CHECK(pgl_shmat(shmid, NULL, 0) == (void *) -1); - CHECK(errno == EINVAL); - return 0; -} - -int -main(void) -{ - CHECK(check_locale_pipe() == 0); - CHECK(check_identity_and_fail_closed_calls() == 0); - CHECK(check_protocol_socket() == 0); - CHECK(check_memory_and_shared_memory() == 0); - return 0; -} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c deleted file mode 100644 index 84bbf059..00000000 --- a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim.c +++ /dev/null @@ -1,679 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#endif -#ifndef _DARWIN_C_SOURCE -#define _DARWIN_C_SOURCE -#endif -#ifndef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200809L -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(__has_include) -#if __has_include("pg_config.h") -#include "pg_config.h" -#endif -#endif - -#ifndef PG_VERSION -#define PG_VERSION "unknown" -#endif - -#define PGLITE_UID 123 - -extern char **environ; -extern int pg_char_to_encoding_private(const char *name); -extern const char *pg_encoding_to_char_private(int encoding); - -typedef struct LocalePipe -{ - FILE *file; - struct LocalePipe *next; -} LocalePipe; - -typedef struct ChildPipe -{ - FILE *file; - pid_t pid; - struct ChildPipe *next; -} ChildPipe; - -typedef struct CommandSpec -{ - char **argv; - int argc; - char *stdin_path; - char *stdout_path; - bool stderr_to_stdout; -} CommandSpec; - -static LocalePipe *locale_pipes; -static ChildPipe *child_pipes; - -static void -free_command_spec(CommandSpec *spec) -{ - if (spec == NULL) - return; - if (spec->argv) - { - for (int i = 0; i < spec->argc; i++) - free(spec->argv[i]); - free(spec->argv); - } - free(spec->stdin_path); - free(spec->stdout_path); - memset(spec, 0, sizeof(*spec)); -} - -static char * -read_command_token(const char **cursor) -{ - const char *p = *cursor; - while (*p == ' ' || *p == '\t' || *p == '\n') - p++; - if (*p == '\0') - { - *cursor = p; - return NULL; - } - - char quote = 0; - if (*p == '\'' || *p == '"') - quote = *p++; - - size_t capacity = strlen(p) + 1; - char *token = malloc(capacity); - if (token == NULL) - return NULL; - size_t len = 0; - while (*p) - { - if (quote) - { - if (*p == quote) - { - p++; - break; - } - } - else if (*p == ' ' || *p == '\t' || *p == '\n') - { - break; - } - - if (*p == '\\' && p[1] != '\0') - p++; - token[len++] = *p++; - } - token[len] = '\0'; - while (*p == ' ' || *p == '\t' || *p == '\n') - p++; - *cursor = p; - return token; -} - -static bool -append_command_arg(CommandSpec *spec, char *arg) -{ - char **next = realloc(spec->argv, sizeof(char *) * (spec->argc + 2)); - if (next == NULL) - { - free(arg); - return false; - } - spec->argv = next; - spec->argv[spec->argc++] = arg; - spec->argv[spec->argc] = NULL; - return true; -} - -static bool -set_redirect_path(char **slot, char *path) -{ - if (path == NULL || path[0] == '\0') - { - free(path); - errno = EINVAL; - return false; - } - free(*slot); - *slot = path; - return true; -} - -static bool -parse_command(const char *command, CommandSpec *spec) -{ - memset(spec, 0, sizeof(*spec)); - const char *cursor = command; - for (;;) - { - char *token = read_command_token(&cursor); - if (token == NULL) - break; - - if (strcmp(token, "2>&1") == 0) - { - spec->stderr_to_stdout = true; - free(token); - continue; - } - if (strcmp(token, "<") == 0 || strcmp(token, ">") == 0) - { - bool input = token[0] == '<'; - free(token); - char *path = read_command_token(&cursor); - if (!set_redirect_path(input ? &spec->stdin_path : &spec->stdout_path, path)) - goto fail; - continue; - } - if ((token[0] == '<' || token[0] == '>') && token[1] != '\0') - { - bool input = token[0] == '<'; - char *path = strdup(token + 1); - free(token); - if (!set_redirect_path(input ? &spec->stdin_path : &spec->stdout_path, path)) - goto fail; - continue; - } - if (!append_command_arg(spec, token)) - { - errno = ENOMEM; - goto fail; - } - } - if (spec->argc == 0) - { - errno = EINVAL; - goto fail; - } - return true; - -fail: - free_command_spec(spec); - return false; -} - -static const char * -base_name(const char *path) -{ - const char *slash = strrchr(path, '/'); - return slash ? slash + 1 : path; -} - -static bool -is_postgres_command(const CommandSpec *spec) -{ - if (spec->argc == 0 || spec->argv == NULL || spec->argv[0] == NULL) - return false; - const char *name = base_name(spec->argv[0]); - return strcmp(name, "postgres") == 0 || strcmp(name, "pglite") == 0; -} - -static int -open_redirect(const char *path, int flags) -{ - return open(path, flags, 0600); -} - -static bool -add_redirect_action(posix_spawn_file_actions_t *actions, int fd, int target) -{ - if (posix_spawn_file_actions_adddup2(actions, fd, target) != 0) - return false; - if (posix_spawn_file_actions_addclose(actions, fd) != 0) - return false; - return true; -} - -static int -spawn_postgres_command(const CommandSpec *spec, int stdin_fd, int stdout_fd, pid_t *pid) -{ - posix_spawn_file_actions_t actions; - int rc = posix_spawn_file_actions_init(&actions); - if (rc != 0) - { - errno = rc; - return -1; - } - - int opened_stdin = -1; - int opened_stdout = -1; - if ((stdin_fd >= 0 && spec->stdin_path != NULL) || - (stdout_fd >= 0 && spec->stdout_path != NULL)) - { - errno = EINVAL; - goto fail; - } - if (stdin_fd >= 0 && !add_redirect_action(&actions, stdin_fd, STDIN_FILENO)) - goto fail; - if (stdout_fd >= 0 && !add_redirect_action(&actions, stdout_fd, STDOUT_FILENO)) - goto fail; - if (spec->stdin_path != NULL) - { - opened_stdin = open_redirect(spec->stdin_path, O_RDONLY); - if (opened_stdin < 0 || !add_redirect_action(&actions, opened_stdin, STDIN_FILENO)) - goto fail; - } - if (spec->stdout_path != NULL) - { - opened_stdout = open_redirect(spec->stdout_path, O_WRONLY | O_CREAT | O_TRUNC); - if (opened_stdout < 0 || !add_redirect_action(&actions, opened_stdout, STDOUT_FILENO)) - goto fail; - } - if (spec->stderr_to_stdout && - posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO) != 0) - goto fail; - - rc = posix_spawnp(pid, spec->argv[0], &actions, NULL, spec->argv, environ); - posix_spawn_file_actions_destroy(&actions); - if (opened_stdin >= 0) - close(opened_stdin); - if (opened_stdout >= 0) - close(opened_stdout); - if (rc != 0) - { - errno = rc; - return -1; - } - return 0; - -fail: - rc = errno ? errno : EINVAL; - posix_spawn_file_actions_destroy(&actions); - if (opened_stdin >= 0) - close(opened_stdin); - if (opened_stdout >= 0) - close(opened_stdout); - errno = rc; - return -1; -} - -static int -wait_child_status(pid_t pid) -{ - int status = 0; - while (waitpid(pid, &status, 0) < 0) - { - if (errno != EINTR) - return -1; - } - return status; -} - -static bool -remember_child_pipe(FILE *file, pid_t pid) -{ - ChildPipe *pipe = calloc(1, sizeof(*pipe)); - if (pipe == NULL) - return false; - pipe->file = file; - pipe->pid = pid; - pipe->next = child_pipes; - child_pipes = pipe; - return true; -} - -static bool -forget_child_pipe(FILE *file, pid_t *pid) -{ - ChildPipe *previous = NULL; - ChildPipe *pipe = child_pipes; - - while (pipe) - { - if (pipe->file == file) - { - if (previous) - previous->next = pipe->next; - else - child_pipes = pipe->next; - *pid = pipe->pid; - free(pipe); - return true; - } - previous = pipe; - pipe = pipe->next; - } - return false; -} - -static bool -remember_static_pipe(FILE *file) -{ - LocalePipe *pipe = calloc(1, sizeof(*pipe)); - if (pipe == NULL) - return false; - pipe->file = file; - pipe->next = locale_pipes; - locale_pipes = pipe; - return true; -} - -static bool -forget_static_pipe(FILE *file) -{ - LocalePipe *previous = NULL; - LocalePipe *pipe = locale_pipes; - - while (pipe) - { - if (pipe->file == file) - { - if (previous) - previous->next = pipe->next; - else - locale_pipes = pipe->next; - free(pipe); - return true; - } - previous = pipe; - pipe = pipe->next; - } - return false; -} - -static FILE * -open_locale_pipe(const char *command, const char *mode) -{ - if (command == NULL || mode == NULL || strcmp(command, "locale -a") != 0 || - strcmp(mode, "r") != 0) - { - errno = ENOSYS; - return NULL; - } - - FILE *file = tmpfile(); - if (file == NULL) - return NULL; - - const char *encoding = getenv("PGCLIENTENCODING"); - if (encoding == NULL || encoding[0] == '\0') - encoding = "UTF8"; - fprintf(file, "C\nC.%s\nPOSIX\n%s\n", encoding, encoding); - rewind(file); - - if (!remember_static_pipe(file)) - { - fclose(file); - errno = ENOMEM; - return NULL; - } - return file; -} - -static FILE * -open_postgres_read_pipe(const char *command, const char *mode) -{ - if (command == NULL || mode == NULL || strcmp(mode, "r") != 0) - { - errno = ENOSYS; - return NULL; - } - - CommandSpec spec; - if (!parse_command(command, &spec)) - return NULL; - if (!is_postgres_command(&spec)) - { - free_command_spec(&spec); - errno = ENOSYS; - return NULL; - } - - int fds[2]; - if (pipe(fds) != 0) - return NULL; - (void) fcntl(fds[0], F_SETFD, FD_CLOEXEC); - pid_t pid = -1; - if (spawn_postgres_command(&spec, -1, fds[1], &pid) != 0) - { - int saved = errno; - close(fds[0]); - close(fds[1]); - free_command_spec(&spec); - errno = saved; - return NULL; - } - close(fds[1]); - FILE *file = fdopen(fds[0], mode); - if (file == NULL) - { - int saved = errno; - close(fds[0]); - (void) wait_child_status(pid); - free_command_spec(&spec); - errno = saved; - return NULL; - } - if (!remember_child_pipe(file, pid)) - { - fclose(file); - (void) wait_child_status(pid); - free_command_spec(&spec); - errno = ENOMEM; - return NULL; - } - free_command_spec(&spec); - return file; -} - -int -pgl_initdb_system(const char *command) -{ - CommandSpec spec; - if (command == NULL || !parse_command(command, &spec)) - return -1; - if (!is_postgres_command(&spec)) - { - free_command_spec(&spec); - errno = ENOSYS; - return -1; - } - - pid_t pid = -1; - int rc = spawn_postgres_command(&spec, -1, -1, &pid); - free_command_spec(&spec); - if (rc != 0) - return -1; - return wait_child_status(pid); -} - -int -pgl_system(const char *command) -{ - return pgl_initdb_system(command); -} - -FILE * -pgl_initdb_popen(const char *command, const char *mode) -{ - FILE *locale = open_locale_pipe(command, mode); - if (locale != NULL) - return locale; - - if (errno != ENOSYS) - return NULL; - FILE *read_pipe = open_postgres_read_pipe(command, mode); - if (read_pipe != NULL) - return read_pipe; - if (errno != ENOSYS) - return NULL; - if (command == NULL || mode == NULL || strcmp(mode, "w") != 0) - { - errno = ENOSYS; - return NULL; - } - - CommandSpec spec; - if (!parse_command(command, &spec)) - return NULL; - if (!is_postgres_command(&spec)) - { - free_command_spec(&spec); - errno = ENOSYS; - return NULL; - } - - int fds[2]; - if (pipe(fds) != 0) - { - free_command_spec(&spec); - return NULL; - } - (void) fcntl(fds[1], F_SETFD, FD_CLOEXEC); - pid_t pid = -1; - if (spawn_postgres_command(&spec, fds[0], -1, &pid) != 0) - { - int saved = errno; - close(fds[0]); - close(fds[1]); - free_command_spec(&spec); - errno = saved; - return NULL; - } - close(fds[0]); - FILE *file = fdopen(fds[1], mode); - if (file == NULL) - { - int saved = errno; - close(fds[1]); - (void) wait_child_status(pid); - free_command_spec(&spec); - errno = saved; - return NULL; - } - if (!remember_child_pipe(file, pid)) - { - fclose(file); - (void) wait_child_status(pid); - free_command_spec(&spec); - errno = ENOMEM; - return NULL; - } - free_command_spec(&spec); - return file; -} - -FILE * -pgl_popen(const char *command, const char *mode) -{ - return pgl_initdb_popen(command, mode); -} - -int -pgl_initdb_pclose(FILE *file) -{ - if (file == NULL) - { - errno = EINVAL; - return -1; - } - if (forget_static_pipe(file)) - return fclose(file); - - pid_t pid = -1; - if (forget_child_pipe(file, &pid)) - { - int close_rc = fclose(file); - int status = wait_child_status(pid); - if (close_rc != 0) - return -1; - return status; - } - errno = EINVAL; - return -1; -} - -int -pgl_pclose(FILE *file) -{ - return pgl_initdb_pclose(file); -} - -int -__wrap_system(const char *command) -{ - return pgl_initdb_system(command); -} - -FILE * -__wrap_popen(const char *command, const char *mode) -{ - return pgl_initdb_popen(command, mode); -} - -int -__wrap_pclose(FILE *file) -{ - return pgl_initdb_pclose(file); -} - -int -pg_char_to_encoding(const char *name) -{ - return pg_char_to_encoding_private(name); -} - -const char * -pg_encoding_to_char(int encoding) -{ - return pg_encoding_to_char_private(encoding); -} - -uid_t -pgl_geteuid(void) -{ - return PGLITE_UID; -} - -uid_t -pgl_getuid(void) -{ - return PGLITE_UID; -} - -struct passwd * -pgl_getpwuid(uid_t uid) -{ - if (uid != PGLITE_UID) - { - errno = ENOENT; - return NULL; - } - - static struct passwd pw; - static char name[] = "postgres"; - static char passwd[] = "x"; - static char gecos[] = "Static User"; - static char dir[] = "/home/postgres"; - static char shell[] = "/bin/sh"; - - pw.pw_name = name; - pw.pw_passwd = passwd; - pw.pw_uid = uid; - pw.pw_gid = uid; - pw.pw_gecos = gecos; - pw.pw_dir = dir; - pw.pw_shell = shell; - - return &pw; -} - -void -pgl_exit(int status) -{ - exit(status); -} diff --git a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c b/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c deleted file mode 100644 index c3b8fd4c..00000000 --- a/assets/wasix-build/wasix_shim/pglite_wasix_initdb_shim_abi_test.c +++ /dev/null @@ -1,174 +0,0 @@ -#ifndef _GNU_SOURCE -#define _GNU_SOURCE -#endif -#ifndef _DARWIN_C_SOURCE -#define _DARWIN_C_SOURCE -#endif -#ifndef _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200809L -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define CHECK(condition) \ - do \ - { \ - if (!(condition)) \ - { \ - fprintf(stderr, "initdb shim ABI check failed at %s:%d: %s\n", __FILE__, __LINE__, \ - #condition); \ - return 1; \ - } \ - } while (0) - -FILE *pgl_initdb_popen(const char *command, const char *mode); -int pgl_initdb_pclose(FILE *file); -uid_t pgl_geteuid(void); -uid_t pgl_getuid(void); -struct passwd *pgl_getpwuid(uid_t uid); - -int -pg_char_to_encoding_private(const char *name) -{ - return strcmp(name, "UTF8") == 0 ? 6 : -1; -} - -const char * -pg_encoding_to_char_private(int encoding) -{ - return encoding == 6 ? "UTF8" : ""; -} - -static int -write_file(const char *path, const char *contents) -{ - FILE *file = fopen(path, "w"); - if (file == NULL) - return -1; - if (fputs(contents, file) < 0) - { - fclose(file); - return -1; - } - return fclose(file); -} - -static int -install_fake_postgres(char *dir) -{ - char path[512]; - snprintf(path, sizeof(path), "%s/postgres", dir); - const char *script = - "#!/bin/sh\n" - "if [ \"$1\" = \"-V\" ] || [ \"$1\" = \"--version\" ]; then\n" - " echo 'postgres (PostgreSQL) fake-from-child'\n" - " exit 0\n" - "fi\n" - "cat > \"$PGLITE_INITDB_STDIN_CAPTURE\"\n"; - CHECK(write_file(path, script) == 0); - CHECK(chmod(path, 0700) == 0); - return 0; -} - -static int -prepend_path(const char *dir) -{ - const char *old_path = getenv("PATH"); - if (old_path == NULL) - old_path = ""; - size_t len = strlen(dir) + 1 + strlen(old_path) + 1; - char *next = malloc(len); - CHECK(next != NULL); - snprintf(next, len, "%s:%s", dir, old_path); - CHECK(setenv("PATH", next, 1) == 0); - free(next); - return 0; -} - -static int -check_locale_and_fail_closed(void) -{ - CHECK(setenv("PGCLIENTENCODING", "UTF8", 1) == 0); - errno = 0; - CHECK(pgl_initdb_popen("uname -a", "r") == NULL); - CHECK(errno == ENOSYS); - errno = 0; - CHECK(pgl_initdb_popen("locale -a", "w") == NULL); - CHECK(errno == ENOSYS); - - FILE *file = pgl_initdb_popen("locale -a", "r"); - CHECK(file != NULL); - char contents[128] = {0}; - size_t read_len = fread(contents, 1, sizeof(contents) - 1, file); - CHECK(pgl_initdb_pclose(file) == 0); - CHECK(read_len > 0); - CHECK(strstr(contents, "C\n") != NULL); - CHECK(strstr(contents, "C.UTF8\n") != NULL); - CHECK(strstr(contents, "POSIX\n") != NULL); - return 0; -} - -static int -check_postgres_read_and_write_pipes(char *dir) -{ - CHECK(install_fake_postgres(dir) == 0); - CHECK(prepend_path(dir) == 0); - - FILE *read_pipe = pgl_initdb_popen("postgres -V 2>&1", "r"); - CHECK(read_pipe != NULL); - char version[128] = {0}; - CHECK(fread(version, 1, sizeof(version) - 1, read_pipe) > 0); - CHECK(pgl_initdb_pclose(read_pipe) == 0); - CHECK(strstr(version, "fake-from-child") != NULL); - - char capture[512]; - snprintf(capture, sizeof(capture), "%s/stdin.txt", dir); - CHECK(setenv("PGLITE_INITDB_STDIN_CAPTURE", capture, 1) == 0); - FILE *write_pipe = pgl_initdb_popen("postgres --boot \"quoted arg\"", "w"); - CHECK(write_pipe != NULL); - CHECK(fputs("bootstrap input\n", write_pipe) >= 0); - CHECK(pgl_initdb_pclose(write_pipe) == 0); - - FILE *captured = fopen(capture, "r"); - CHECK(captured != NULL); - char captured_text[128] = {0}; - CHECK(fread(captured_text, 1, sizeof(captured_text) - 1, captured) > 0); - CHECK(fclose(captured) == 0); - CHECK(strstr(captured_text, "bootstrap input") != NULL); - return 0; -} - -static int -check_identity(void) -{ - CHECK(pgl_geteuid() == 123); - CHECK(pgl_getuid() == 123); - struct passwd *pw = pgl_getpwuid(123); - CHECK(pw != NULL); - CHECK(strcmp(pw->pw_name, "postgres") == 0); - errno = 0; - CHECK(pgl_getpwuid(999) == NULL); - CHECK(errno == ENOENT); - return 0; -} - -int -main(void) -{ - char temp_template[] = "/tmp/pglite-initdb-shim-XXXXXX"; - char *dir = mkdtemp(temp_template); - CHECK(dir != NULL); - CHECK(check_locale_and_fail_closed() == 0); - CHECK(check_postgres_read_and_write_pipes(dir) == 0); - CHECK(check_identity() == 0); - return 0; -} diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..d1522f11 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,32 @@ +# Benchmarks + +Benchmark definitions, workload specs, baselines, and intentionally promoted +reports belong here. Executable benchmark harnesses stay under `tools/perf`. + +The long-term benchmark matrix should compare: + +- native PostgreSQL control; +- `liboliphaunt` direct mode; +- `oliphaunt` direct, broker, and server modes; +- SQLite baselines for comparable embedded workloads. + +The native `oliphaunt` matrix in +`tools/perf/matrix/run_native_oliphaunt_matrix.sh` now includes direct, broker, +server, native PostgreSQL, SQLite, streaming, direct/broker/server +prepared-update, native PostgreSQL prepared-update, resource, and artifact-size +rows. RTT report rows include p50/p90/p95/p99 tail latency. Prepared-update +report rows include fresh-process p50/p90/p95, native-PostgreSQL p90 ratios, +and command-level CPU/RSS/footprint. + +Current layout: + +- `native/sql/`: fixed SQL workloads used by native direct, broker, server, + native PostgreSQL, and SQLite comparison suites. +- `native/baselines/`: committed native baselines when promoted as release + evidence. +- `wasix/`: WASIX benchmark specs and baselines. +- `mobile/`: mobile benchmark specs and baselines. +- `reports/`: published reports promoted as release evidence. + +Tooling may live in `tools/` when it is an executable harness, but benchmark +plans, datasets, baselines, and published reports live here. diff --git a/benchmarks/mobile/README.md b/benchmarks/mobile/README.md new file mode 100644 index 00000000..f5f9fb39 --- /dev/null +++ b/benchmarks/mobile/README.md @@ -0,0 +1,4 @@ +# Mobile Benchmarks + +Mobile benchmark specs and baselines live here. Emulator, simulator, and device +orchestration stays under `tools/perf` and product-owned mobile tooling. diff --git a/benchmarks/moon.yml b/benchmarks/moon.yml new file mode 100644 index 00000000..0608d9f1 --- /dev/null +++ b/benchmarks/moon.yml @@ -0,0 +1,27 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "benchmarks" +language: "unknown" +layer: "configuration" +stack: "systems" +tags: ["benchmarks", "performance", "inputs"] + +project: + title: "Benchmark Inputs" + description: "Benchmark SQL fixtures, baselines, and report shape documentation." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/perf" + paths: + "**/*": ["@oliphaunt/perf"] + +tasks: + check: + tags: ["quality", "static"] + command: "true" + inputs: + - "/benchmarks/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/benchmarks/native/README.md b/benchmarks/native/README.md new file mode 100644 index 00000000..3c9e0ce9 --- /dev/null +++ b/benchmarks/native/README.md @@ -0,0 +1,8 @@ +# Native Benchmarks + +Native benchmark specs live here. Runner code stays under `tools/perf`. + +- `sql/`: fixed SQL workload files used by native direct, broker, server, + native PostgreSQL, and SQLite comparison suites. +- `baselines/`: committed comparison baselines when a release intentionally + records them. diff --git a/benchmarks/native/baselines/README.md b/benchmarks/native/baselines/README.md new file mode 100644 index 00000000..ceb4f024 --- /dev/null +++ b/benchmarks/native/baselines/README.md @@ -0,0 +1,5 @@ +# Native Baselines + +Committed native benchmark baselines live here when release evidence is +intentionally promoted. Local and CI-generated reports stay under `target/perf` +unless they are promoted as release artifacts. diff --git a/benchmarks/native/sql/benchmark1.sql b/benchmarks/native/sql/benchmark1.sql new file mode 100644 index 00000000..3bb2bda7 --- /dev/null +++ b/benchmarks/native/sql/benchmark1.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS bench_fixture; +CREATE TABLE bench_fixture(id integer PRIMARY KEY, value text); +INSERT INTO bench_fixture SELECT i, 'value-' || i::text FROM generate_series(1, 1000) AS i; diff --git a/benchmarks/native/sql/benchmark10.sql b/benchmarks/native/sql/benchmark10.sql new file mode 100644 index 00000000..9acf6948 --- /dev/null +++ b/benchmarks/native/sql/benchmark10.sql @@ -0,0 +1 @@ +UPDATE bench_fixture SET value = reverse(value) WHERE id BETWEEN 501 AND 1000; diff --git a/benchmarks/native/sql/benchmark11.sql b/benchmarks/native/sql/benchmark11.sql new file mode 100644 index 00000000..4b94b307 --- /dev/null +++ b/benchmarks/native/sql/benchmark11.sql @@ -0,0 +1 @@ +SELECT avg(id), percentile_cont(0.9) WITHIN GROUP (ORDER BY id) FROM bench_fixture; diff --git a/benchmarks/native/sql/benchmark12.sql b/benchmarks/native/sql/benchmark12.sql new file mode 100644 index 00000000..30988939 --- /dev/null +++ b/benchmarks/native/sql/benchmark12.sql @@ -0,0 +1 @@ +DELETE FROM bench_fixture WHERE id % 7 = 0; diff --git a/benchmarks/native/sql/benchmark13.sql b/benchmarks/native/sql/benchmark13.sql new file mode 100644 index 00000000..5a204381 --- /dev/null +++ b/benchmarks/native/sql/benchmark13.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS bench_fixture_copy AS SELECT * FROM bench_fixture WHERE false; +INSERT INTO bench_fixture_copy SELECT * FROM bench_fixture; diff --git a/benchmarks/native/sql/benchmark14.sql b/benchmarks/native/sql/benchmark14.sql new file mode 100644 index 00000000..1e73c5e8 --- /dev/null +++ b/benchmarks/native/sql/benchmark14.sql @@ -0,0 +1,2 @@ +TRUNCATE bench_fixture_copy; +INSERT INTO bench_fixture_copy SELECT * FROM bench_fixture WHERE id % 3 = 0; diff --git a/benchmarks/native/sql/benchmark15.sql b/benchmarks/native/sql/benchmark15.sql new file mode 100644 index 00000000..5c19cdcc --- /dev/null +++ b/benchmarks/native/sql/benchmark15.sql @@ -0,0 +1,2 @@ +DELETE FROM bench_fixture WHERE id > 1500; +INSERT INTO bench_fixture SELECT i, 'refill-' || i::text FROM generate_series(2001, 2500) AS i; diff --git a/benchmarks/native/sql/benchmark16.sql b/benchmarks/native/sql/benchmark16.sql new file mode 100644 index 00000000..b72422ba --- /dev/null +++ b/benchmarks/native/sql/benchmark16.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS bench_fixture_copy; +DROP TABLE IF EXISTS bench_fixture; diff --git a/benchmarks/native/sql/benchmark2.sql b/benchmarks/native/sql/benchmark2.sql new file mode 100644 index 00000000..23de70d3 --- /dev/null +++ b/benchmarks/native/sql/benchmark2.sql @@ -0,0 +1 @@ +INSERT INTO bench_fixture SELECT i, 'extra-' || i::text FROM generate_series(1001, 2000) AS i; diff --git a/benchmarks/native/sql/benchmark3.sql b/benchmarks/native/sql/benchmark3.sql new file mode 100644 index 00000000..81d48abc --- /dev/null +++ b/benchmarks/native/sql/benchmark3.sql @@ -0,0 +1 @@ +SELECT count(*), min(id), max(id) FROM bench_fixture; diff --git a/benchmarks/native/sql/benchmark4.sql b/benchmarks/native/sql/benchmark4.sql new file mode 100644 index 00000000..88959e86 --- /dev/null +++ b/benchmarks/native/sql/benchmark4.sql @@ -0,0 +1 @@ +SELECT * FROM bench_fixture WHERE id BETWEEN 250 AND 260 ORDER BY id; diff --git a/benchmarks/native/sql/benchmark5.sql b/benchmarks/native/sql/benchmark5.sql new file mode 100644 index 00000000..ba83532b --- /dev/null +++ b/benchmarks/native/sql/benchmark5.sql @@ -0,0 +1 @@ +UPDATE bench_fixture SET value = value || '-updated' WHERE id % 10 = 0; diff --git a/benchmarks/native/sql/benchmark6.sql b/benchmarks/native/sql/benchmark6.sql new file mode 100644 index 00000000..a4552db0 --- /dev/null +++ b/benchmarks/native/sql/benchmark6.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS bench_fixture_value_idx ON bench_fixture(value); +SELECT count(*) FROM bench_fixture WHERE value LIKE 'value-9%'; diff --git a/benchmarks/native/sql/benchmark7.sql b/benchmarks/native/sql/benchmark7.sql new file mode 100644 index 00000000..e8bb9bfc --- /dev/null +++ b/benchmarks/native/sql/benchmark7.sql @@ -0,0 +1 @@ +SELECT id, value FROM bench_fixture ORDER BY value LIMIT 100; diff --git a/benchmarks/native/sql/benchmark8.sql b/benchmarks/native/sql/benchmark8.sql new file mode 100644 index 00000000..b9308f94 --- /dev/null +++ b/benchmarks/native/sql/benchmark8.sql @@ -0,0 +1 @@ +SELECT value, count(*) FROM bench_fixture GROUP BY value ORDER BY count(*) DESC, value LIMIT 50; diff --git a/benchmarks/native/sql/benchmark9.sql b/benchmarks/native/sql/benchmark9.sql new file mode 100644 index 00000000..6bd1f526 --- /dev/null +++ b/benchmarks/native/sql/benchmark9.sql @@ -0,0 +1 @@ +UPDATE bench_fixture SET value = reverse(value) WHERE id BETWEEN 1 AND 500; diff --git a/benchmarks/reports/README.md b/benchmarks/reports/README.md new file mode 100644 index 00000000..01ead683 --- /dev/null +++ b/benchmarks/reports/README.md @@ -0,0 +1,4 @@ +# Benchmark Reports + +Published benchmark reports live here only when they are intentionally promoted +as release evidence. Measured local and CI output stays under `target/perf`. diff --git a/benchmarks/wasix/README.md b/benchmarks/wasix/README.md new file mode 100644 index 00000000..58d4973d --- /dev/null +++ b/benchmarks/wasix/README.md @@ -0,0 +1,4 @@ +# WASIX Benchmarks + +WASIX benchmark specs and baselines live here. Rust runner implementation and +runtime orchestration stay under `tools/perf` and the WASIX product source tree. diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..6da8c004 --- /dev/null +++ b/biome.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json", + "files": { + "includes": [ + "package.json", + "biome.json", + "renovate.json", + ".markdownlint-cli2.jsonc", + "src/docs/package.json", + "src/docs/tsconfig.json", + "src/docs/app/**/*.ts", + "src/docs/app/**/*.tsx", + "src/docs/app/**/*.css", + "src/docs/lib/**/*.ts", + "src/docs/mdx-components.tsx", + "src/docs/next.config.mjs", + "src/docs/source.config.ts", + "src/docs/tools/**/*.mjs", + "src/sdks/react-native/package.json", + "src/sdks/react-native/typedoc.json", + "src/sdks/react-native/react-native.config.js", + "src/sdks/react-native/src/**/*.ts", + "src/sdks/react-native/src/**/*.tsx", + "src/sdks/js/package.json", + "src/sdks/js/typedoc.json", + "src/sdks/js/jsr.json", + "src/sdks/js/src/**/*.ts", + "tools/perf/matrix/**/*.json", + "tools/perf/matrix/**/*.ts", + "tools/test/**/*.mjs", + "!**/node_modules", + "!**/lib", + "!**/dist", + "!**/.expo", + "!**/ios", + "!**/android" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "all" + } + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "off" + } + } + } +} diff --git a/clippy.toml b/clippy.toml index 93e1038d..3dfc3206 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,2 +1,2 @@ -msrv = "1.92.0" +msrv = "1.93.0" avoid-breaking-exported-api = true diff --git a/coverage/baseline.toml b/coverage/baseline.toml new file mode 100644 index 00000000..fde8373a --- /dev/null +++ b/coverage/baseline.toml @@ -0,0 +1,299 @@ +# Coverage baselines are measured product evidence, not policy placeholders. +# `line_threshold` is the aggregate gate. `per_file_line_threshold` prevents +# aggregate coverage from hiding weak files. `measured_line_coverage` records +# the last committed local/CI measurement for audit only. + +[policy] +fail_on_unmeasured_product = true +minimum_new_sdk_line_coverage = 80.0 +release_ratchet_percentage_points = 2.0 +target_sdk_line_coverage = 85.0 + +[products.oliphaunt-rust] +tool = "cargo-llvm-cov" +line_threshold = 80.0 +measured_line_coverage = 82.65 +summary = "target/coverage/oliphaunt-rust/summary.json" +reports = ["target/coverage/oliphaunt-rust/lcov.info"] +source_globs = ["src/sdks/rust/src/*.rs"] +exclude_globs = [ + "src/sdks/rust/tests/**", + "src/runtimes/liboliphaunt/native/**", + "src/postgres/versions/18/**", + "target/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-rust.waivers]] +path = "src/sdks/rust/src/broker.rs" +reason = "broker process orchestration is runtime/integration evidence, not deterministic unit coverage yet" +evidence = "oliphaunt-rust smoke/regression lanes and TypeScript broker helper tests" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-rust.waivers]] +path = "src/sdks/rust/src/ipc.rs" +reason = "local IPC framing is exercised through broker integration tests and shared protocol fixtures" +evidence = "oliphaunt-rust broker tests plus src/shared/fixtures/protocol/query-response-cases.json consumers" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-rust.waivers]] +path = "src/sdks/rust/src/lib.rs" +reason = "crate root is a re-export surface with no durable executable behavior to line-cover" +evidence = "cargo doctests and SDK package-shape checks" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-rust.waivers]] +path = "src/sdks/rust/src/pgwire.rs" +reason = "external wire compatibility is regression evidence rather than SDK wrapper unit coverage" +evidence = "server/client compatibility regression lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-rust.waivers]] +path = "src/sdks/rust/src/server.rs" +reason = "server process lifecycle requires runtime artifacts and belongs in smoke/regression lanes" +evidence = "oliphaunt-rust smoke/regression server tests" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[products.oliphaunt-swift] +tool = "swift test --enable-code-coverage" +line_threshold = 80.0 +measured_line_coverage = 86.83 +summary = "target/coverage/oliphaunt-swift/summary.json" +reports = ["target/coverage/oliphaunt-swift/swift-coverage.json"] +source_globs = [ + "src/sdks/swift/Sources/Oliphaunt/*.swift", + "src/sdks/swift/Sources/Oliphaunt/**/*.swift", +] +exclude_globs = [ + "src/sdks/swift/Sources/COliphaunt/**", + "src/sdks/swift/Tests/**", + "target/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-swift.waivers]] +path = "src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift" +reason = "native direct FFI shell is validated by runtime smoke/XCTest paths rather than pure Swift line coverage" +evidence = "Swift smoke, iOS/macOS packaging, and liboliphaunt C ABI tests" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[products.oliphaunt-kotlin] +tool = "kover" +line_threshold = 80.0 +measured_line_coverage = 82.04 +summary = "target/coverage/oliphaunt-kotlin/summary.json" +reports = ["target/coverage/oliphaunt-kotlin/kover.xml"] +source_globs = ["src/sdks/kotlin/oliphaunt/src/**/*.kt"] +exclude_globs = [ + "src/sdks/kotlin/oliphaunt/src/*Test/**", + "src/sdks/kotlin/oliphaunt/src/**/*Test.kt", + "src/sdks/kotlin/**/build/**", + "src/sdks/kotlin/**/.cxx/**", + "src/sdks/kotlin/**/generated/**", + "target/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt" +reason = "Android JNI-backed direct engine is runtime/device evidence, not JVM/Kover unit coverage" +evidence = "Android unit/resource tests, native smoke, and mobile E2E lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt" +reason = "Android app-facing helper binds packaged resources and is covered by Android resource/package tests" +evidence = "OliphauntAndroidRuntimeAssetsTest and package-shape checks" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidNativeBridge.kt" +reason = "JNI bridge shell is validated by native runtime smoke and bridge compile checks" +evidence = "Android CMake compile, JNI smoke, and liboliphaunt C ABI tests" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/jvmMain/kotlin/dev/oliphaunt/DefaultEngine.kt" +reason = "JVM target intentionally reports runtime unavailable until the JVM/native artifact resolver is implemented" +evidence = "Kotlin package-shape tests and SDK parity mode-support fixtures" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/DefaultEngine.kt" +reason = "Kotlin/Native default engine selection is validated with native target smoke rather than Kover JVM reports" +evidence = "Kotlin native smoke/regression lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-kotlin.waivers]] +path = "src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/NativeDirectEngine.kt" +reason = "Kotlin/Native C interop and owner-dispatcher behavior requires native runtime evidence outside Kover" +evidence = "Kotlin native smoke/regression lanes and liboliphaunt C ABI tests" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[products.oliphaunt-js] +tool = "vitest-v8" +line_threshold = 80.0 +measured_line_coverage = 81.84 +summary = "target/coverage/oliphaunt-js/summary.json" +reports = [ + "target/coverage/oliphaunt-js/coverage-summary.json", + "target/coverage/oliphaunt-js/lcov.info", +] +source_globs = [ + "src/sdks/js/src/*.ts", + "src/sdks/js/src/**/*.ts", +] +exclude_globs = [ + "src/sdks/js/src/__tests__/**", + "src/sdks/js/src/**/*.d.ts", + "src/sdks/js/src/**/types.ts", + "src/sdks/js/lib/**", + "src/sdks/js/node_modules/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/assets-deno.ts" +reason = "Deno native asset resolution is covered by Deno package/runtime checks, not Node Vitest line coverage" +evidence = "TypeScript Deno package-shape and smoke lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/bun.ts" +reason = "Bun FFI binding is covered by Bun package/runtime checks, not Node Vitest line coverage" +evidence = "TypeScript Bun package-shape and smoke lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/default.ts" +reason = "runtime selector depends on host runtime and is covered through Node/Bun/Deno package checks" +evidence = "TypeScript package and native smoke lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/deno.ts" +reason = "Deno FFI binding is covered by Deno runtime checks, not Node Vitest line coverage" +evidence = "TypeScript Deno package-shape and smoke lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/node-addon.ts" +reason = "Node native addon loader is validated by package-shape, artifact-resolution, and native smoke lanes" +evidence = "native-bindings tests, package checks, and TypeScript native smoke" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/native/tar.ts" +reason = "tar extraction helpers are covered through asset resolver tests and release artifact checks" +evidence = "asset-resolver.test.ts and release artifact validation" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/runtime/broker.ts" +reason = "broker helper lifecycle requires Rust helper/runtime artifacts and belongs in smoke/regression evidence" +evidence = "runtime-modes tests, broker frame tests, and TypeScript native broker smoke" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/runtime/node-adapter.ts" +reason = "Node process adapter is partially exercised but runtime process behavior belongs in smoke lanes" +evidence = "runtime-modes tests and broker/server smoke lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/runtime/pgwire.ts" +reason = "external PostgreSQL wire compatibility is regression evidence rather than unit line coverage" +evidence = "server-wire tests and external client regression lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[[products.oliphaunt-js.waivers]] +path = "src/sdks/js/src/runtime/server.ts" +reason = "server process lifecycle requires runtime artifacts and belongs in smoke/regression evidence" +evidence = "runtime-modes tests and TypeScript native server smoke" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[products.oliphaunt-react-native] +tool = "vitest-v8" +line_threshold = 80.0 +measured_line_coverage = 81.68 +summary = "target/coverage/oliphaunt-react-native/summary.json" +reports = [ + "target/coverage/oliphaunt-react-native/coverage-summary.json", + "target/coverage/oliphaunt-react-native/lcov.info", +] +source_globs = [ + "src/sdks/react-native/src/*.ts", + "src/sdks/react-native/src/**/*.ts", + "src/sdks/react-native/app.plugin.js", +] +exclude_globs = [ + "src/sdks/react-native/src/__tests__/**", + "src/sdks/react-native/lib/**", + "src/sdks/react-native/node_modules/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-react-native.waivers]] +path = "src/sdks/react-native/src/specs/NativeOliphaunt.ts" +reason = "React Native TurboModule Codegen spec is validated by Codegen/package checks, not runtime JS line coverage" +evidence = "codegen:check, package-shape checks, and RN native adapter compile lanes" +owner = "@oliphaunt/core" +expires = "before-0.2.0" + +[products.oliphaunt-wasix-rust] +tool = "cargo-llvm-cov" +line_threshold = 80.0 +measured_line_coverage = 85.71 +summary = "target/coverage/oliphaunt-wasix-rust/summary.json" +reports = ["target/coverage/oliphaunt-wasix-rust/lcov.info"] +source_globs = ["src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/*.rs"] +exclude_globs = [ + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/shared_fixture_tests.rs", + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/tests.rs", + "src/runtimes/liboliphaunt/wasix/assets/generated/**", + "src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/**", + "src/runtimes/liboliphaunt/wasix/crates/aot/**", + "target/**", +] +per_file_line_threshold = 50.0 +branch_coverage = "advisory" +function_coverage = "advisory" + +[[products.oliphaunt-wasix-rust.waivers]] +path = "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/mod.rs" +reason = "protocol module root is an export shim; behavior lives in the measured protocol modules" +evidence = "oliphaunt-wasix protocol nextest and doctest coverage" +owner = "@oliphaunt/core" +expires = "before-0.2.0" diff --git a/crates/aot/aarch64-apple-darwin/Cargo.toml b/crates/aot/aarch64-apple-darwin/Cargo.toml deleted file mode 100644 index f0a0dc31..00000000 --- a/crates/aot/aarch64-apple-darwin/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pglite-oxide-aot-aarch64-apple-darwin" -version = "0.5.1" -edition = "2024" -rust-version = "1.92" -description = "Internal Wasmer AOT artifacts for pglite-oxide on aarch64-apple-darwin" -repository = "https://github.com/f0rr0/pglite-oxide" -license = "MIT AND Apache-2.0 AND PostgreSQL" -publish = true -include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] - -[lib] -path = "src/lib.rs" diff --git a/crates/aot/aarch64-apple-darwin/README.md b/crates/aot/aarch64-apple-darwin/README.md deleted file mode 100644 index 9c39ab0d..00000000 --- a/crates/aot/aarch64-apple-darwin/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# pglite-oxide-aot-aarch64-apple-darwin - -Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. -Do not depend on this crate directly. diff --git a/crates/aot/aarch64-apple-darwin/build.rs b/crates/aot/aarch64-apple-darwin/build.rs deleted file mode 100644 index 9de0b650..00000000 --- a/crates/aot/aarch64-apple-darwin/build.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); - - let target = env::var("CARGO_PKG_NAME") - .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("pglite-oxide-aot-") - .expect("AOT crate name starts with pglite-oxide-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 { - write_source_only_aot(&out, &target); - } -} - -fn emit_expected_artifact_inputs(target: &str) { - if let Some(path) = env::var_os("PGLITE_OXIDE_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"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - emit_manifest_probe(&repo_root.join("target/pglite-oxide/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 { - if let Some(path) = env::var_os("PGLITE_OXIDE_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); - } - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); - if target_artifacts.join("manifest.json").is_file() { - return Some(target_artifacts); - } - } - - let package_artifacts = manifest_dir.join("artifacts"); - if package_artifacts.join("manifest.json").is_file() { - return Some(package_artifacts); - } - - None -} - -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 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); - 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 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(&manifest) - ); - fs::write(out, text).expect("write generated AOT include module"); -} - -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 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 { - "pglite" => "runtime:pglite".to_owned(), - "pg_dump" => "tool:pg_dump".to_owned(), - "initdb" => "tool:initdb".to_owned(), - "plpgsql" => "runtime-support:plpgsql".to_owned(), - "dict_snowball" => "runtime-support:dict_snowball".to_owned(), - extension => format!("extension:{extension}"), - } -} - -fn rust_string_literal(path: &Path) -> String { - format!("{:?}", path.to_string_lossy()) -} diff --git a/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml b/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml deleted file mode 100644 index 0be4496d..00000000 --- a/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" -version = "0.5.1" -edition = "2024" -rust-version = "1.92" -description = "Internal Wasmer AOT artifacts for pglite-oxide on aarch64-unknown-linux-gnu" -repository = "https://github.com/f0rr0/pglite-oxide" -license = "MIT AND Apache-2.0 AND PostgreSQL" -publish = true -include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] - -[lib] -path = "src/lib.rs" diff --git a/crates/aot/aarch64-unknown-linux-gnu/README.md b/crates/aot/aarch64-unknown-linux-gnu/README.md deleted file mode 100644 index 034067d4..00000000 --- a/crates/aot/aarch64-unknown-linux-gnu/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# pglite-oxide-aot-aarch64-unknown-linux-gnu - -Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. -Do not depend on this crate directly. diff --git a/crates/aot/aarch64-unknown-linux-gnu/build.rs b/crates/aot/aarch64-unknown-linux-gnu/build.rs deleted file mode 100644 index 9de0b650..00000000 --- a/crates/aot/aarch64-unknown-linux-gnu/build.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); - - let target = env::var("CARGO_PKG_NAME") - .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("pglite-oxide-aot-") - .expect("AOT crate name starts with pglite-oxide-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 { - write_source_only_aot(&out, &target); - } -} - -fn emit_expected_artifact_inputs(target: &str) { - if let Some(path) = env::var_os("PGLITE_OXIDE_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"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - emit_manifest_probe(&repo_root.join("target/pglite-oxide/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 { - if let Some(path) = env::var_os("PGLITE_OXIDE_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); - } - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); - if target_artifacts.join("manifest.json").is_file() { - return Some(target_artifacts); - } - } - - let package_artifacts = manifest_dir.join("artifacts"); - if package_artifacts.join("manifest.json").is_file() { - return Some(package_artifacts); - } - - None -} - -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 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); - 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 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(&manifest) - ); - fs::write(out, text).expect("write generated AOT include module"); -} - -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 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 { - "pglite" => "runtime:pglite".to_owned(), - "pg_dump" => "tool:pg_dump".to_owned(), - "initdb" => "tool:initdb".to_owned(), - "plpgsql" => "runtime-support:plpgsql".to_owned(), - "dict_snowball" => "runtime-support:dict_snowball".to_owned(), - extension => format!("extension:{extension}"), - } -} - -fn rust_string_literal(path: &Path) -> String { - format!("{:?}", path.to_string_lossy()) -} diff --git a/crates/aot/x86_64-pc-windows-msvc/Cargo.toml b/crates/aot/x86_64-pc-windows-msvc/Cargo.toml deleted file mode 100644 index 348b1f7b..00000000 --- a/crates/aot/x86_64-pc-windows-msvc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pglite-oxide-aot-x86_64-pc-windows-msvc" -version = "0.5.1" -edition = "2024" -rust-version = "1.92" -description = "Internal Wasmer AOT artifacts for pglite-oxide on x86_64-pc-windows-msvc" -repository = "https://github.com/f0rr0/pglite-oxide" -license = "MIT AND Apache-2.0 AND PostgreSQL" -publish = true -include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] - -[lib] -path = "src/lib.rs" diff --git a/crates/aot/x86_64-pc-windows-msvc/README.md b/crates/aot/x86_64-pc-windows-msvc/README.md deleted file mode 100644 index e0d849f5..00000000 --- a/crates/aot/x86_64-pc-windows-msvc/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# pglite-oxide-aot-x86_64-pc-windows-msvc - -Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. -Do not depend on this crate directly. diff --git a/crates/aot/x86_64-pc-windows-msvc/build.rs b/crates/aot/x86_64-pc-windows-msvc/build.rs deleted file mode 100644 index 9de0b650..00000000 --- a/crates/aot/x86_64-pc-windows-msvc/build.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); - - let target = env::var("CARGO_PKG_NAME") - .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("pglite-oxide-aot-") - .expect("AOT crate name starts with pglite-oxide-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 { - write_source_only_aot(&out, &target); - } -} - -fn emit_expected_artifact_inputs(target: &str) { - if let Some(path) = env::var_os("PGLITE_OXIDE_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"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - emit_manifest_probe(&repo_root.join("target/pglite-oxide/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 { - if let Some(path) = env::var_os("PGLITE_OXIDE_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); - } - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); - if target_artifacts.join("manifest.json").is_file() { - return Some(target_artifacts); - } - } - - let package_artifacts = manifest_dir.join("artifacts"); - if package_artifacts.join("manifest.json").is_file() { - return Some(package_artifacts); - } - - None -} - -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 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); - 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 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(&manifest) - ); - fs::write(out, text).expect("write generated AOT include module"); -} - -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 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 { - "pglite" => "runtime:pglite".to_owned(), - "pg_dump" => "tool:pg_dump".to_owned(), - "initdb" => "tool:initdb".to_owned(), - "plpgsql" => "runtime-support:plpgsql".to_owned(), - "dict_snowball" => "runtime-support:dict_snowball".to_owned(), - extension => format!("extension:{extension}"), - } -} - -fn rust_string_literal(path: &Path) -> String { - format!("{:?}", path.to_string_lossy()) -} diff --git a/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml b/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml deleted file mode 100644 index c1a454f8..00000000 --- a/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" -version = "0.5.1" -edition = "2024" -rust-version = "1.92" -description = "Internal Wasmer AOT artifacts for pglite-oxide on x86_64-unknown-linux-gnu" -repository = "https://github.com/f0rr0/pglite-oxide" -license = "MIT AND Apache-2.0 AND PostgreSQL" -publish = true -include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] - -[lib] -path = "src/lib.rs" diff --git a/crates/aot/x86_64-unknown-linux-gnu/README.md b/crates/aot/x86_64-unknown-linux-gnu/README.md deleted file mode 100644 index bf4ba9a8..00000000 --- a/crates/aot/x86_64-unknown-linux-gnu/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# pglite-oxide-aot-x86_64-unknown-linux-gnu - -Internal target-specific Wasmer AOT artifact crate for `pglite-oxide`. -Do not depend on this crate directly. diff --git a/crates/aot/x86_64-unknown-linux-gnu/build.rs b/crates/aot/x86_64-unknown-linux-gnu/build.rs deleted file mode 100644 index 9de0b650..00000000 --- a/crates/aot/x86_64-unknown-linux-gnu/build.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_AOT_DIR"); - - let target = env::var("CARGO_PKG_NAME") - .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("pglite-oxide-aot-") - .expect("AOT crate name starts with pglite-oxide-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 { - write_source_only_aot(&out, &target); - } -} - -fn emit_expected_artifact_inputs(target: &str) { - if let Some(path) = env::var_os("PGLITE_OXIDE_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"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - emit_manifest_probe(&repo_root.join("target/pglite-oxide/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 { - if let Some(path) = env::var_os("PGLITE_OXIDE_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); - } - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - let target_artifacts = repo_root.join("target/pglite-oxide/aot").join(target); - if target_artifacts.join("manifest.json").is_file() { - return Some(target_artifacts); - } - } - - let package_artifacts = manifest_dir.join("artifacts"); - if package_artifacts.join("manifest.json").is_file() { - return Some(package_artifacts); - } - - None -} - -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 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); - 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 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(&manifest) - ); - fs::write(out, text).expect("write generated AOT include module"); -} - -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 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 { - "pglite" => "runtime:pglite".to_owned(), - "pg_dump" => "tool:pg_dump".to_owned(), - "initdb" => "tool:initdb".to_owned(), - "plpgsql" => "runtime-support:plpgsql".to_owned(), - "dict_snowball" => "runtime-support:dict_snowball".to_owned(), - extension => format!("extension:{extension}"), - } -} - -fn rust_string_literal(path: &Path) -> String { - format!("{:?}", path.to_string_lossy()) -} diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml deleted file mode 100644 index c0ff5bed..00000000 --- a/crates/assets/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "pglite-oxide-assets" -version = "0.5.1" -edition = "2024" -rust-version = "1.92" -description = "Internal PGlite runtime and extension assets for pglite-oxide" -repository = "https://github.com/f0rr0/pglite-oxide" -homepage = "https://github.com/f0rr0/pglite-oxide" -documentation = "https://docs.rs/pglite-oxide-assets" -license = "MIT AND Apache-2.0 AND PostgreSQL" -publish = true -include = [ - "Cargo.toml", - "build.rs", - "README.md", - "src/**", - "payload/**", -] - -[lib] -path = "src/lib.rs" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" diff --git a/crates/assets/README.md b/crates/assets/README.md deleted file mode 100644 index 9fe9be2c..00000000 --- a/crates/assets/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# pglite-oxide-assets - -Internal runtime assets for `pglite-oxide`. - -Do not depend on this crate directly. It is published so Cargo can resolve the -default `pglite-oxide` feature set from crates.io. diff --git a/crates/assets/build.rs b/crates/assets/build.rs deleted file mode 100644 index 843bf0e1..00000000 --- a/crates/assets/build.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() { - println!("cargo:rerun-if-env-changed=PGLITE_OXIDE_GENERATED_ASSETS_DIR"); - emit_expected_asset_inputs(); - - 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"); - - if let Some(asset_dir) = find_asset_dir() { - emit_rerun_directives(&asset_dir); - write_generated_assets(&out, &asset_dir); - } else { - write_source_only_assets(&out); - } -} - -fn emit_expected_asset_inputs() { - if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_ASSETS_DIR") { - emit_manifest_probe(&PathBuf::from(path)); - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - if let Some(repo_root) = manifest_dir.parent().and_then(Path::parent) { - emit_manifest_probe(&repo_root.join("target/pglite-oxide/assets")); - } - emit_manifest_probe(&manifest_dir.join("payload")); -} - -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_asset_dir() -> Option { - if let Some(path) = env::var_os("PGLITE_OXIDE_GENERATED_ASSETS_DIR") { - let path = PathBuf::from(path); - if path.join("manifest.json").is_file() { - return Some(path); - } - } - - let manifest_dir = PathBuf::from( - env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), - ); - let repo_root = manifest_dir - .parent() - .and_then(Path::parent) - .map(Path::to_path_buf); - if let Some(repo_root) = repo_root { - let target_assets = repo_root.join("target/pglite-oxide/assets"); - if target_assets.join("manifest.json").is_file() { - return Some(target_assets); - } - } - - let package_payload = manifest_dir.join("payload"); - if package_payload.join("manifest.json").is_file() { - return Some(package_payload); - } - - 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_assets(out: &Path, asset_dir: &Path) { - let manifest = asset_dir.join("manifest.json"); - let runtime = asset_dir.join("pglite.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, &pg_dump, &initdb] { - assert!( - required.is_file(), - "generated asset directory {} is missing required file {}", - asset_dir.display(), - required.display() - ); - } - assert!( - pgdata_archive.is_file() && pgdata_manifest.is_file(), - "generated asset directory {} is missing the required PGDATA template; expected both {} and {}", - asset_dir.display(), - pgdata_archive.display(), - pgdata_manifest.display() - ); - - let pgdata_archive_body = optional_include_bytes_body(&pgdata_archive); - let pgdata_manifest_body = optional_include_bytes_body(&pgdata_manifest); - - let mut extension_cases = String::new(); - let extension_dir = asset_dir.join("extensions"); - if extension_dir.is_dir() { - let mut archives = fs::read_dir(&extension_dir) - .expect("read generated extension directory") - .filter_map(Result::ok) - .map(|entry| entry.path()) - .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) - .collect::>(); - archives.sort(); - for archive in archives { - let Some(file_name) = archive.file_name().and_then(|name| name.to_str()) else { - continue; - }; - let Some(sql_name) = file_name.strip_suffix(".tar.zst") else { - continue; - }; - extension_cases.push_str(&format!( - " {:?} => Some(include_bytes!({})),\n", - sql_name, - rust_string_literal(&archive) - )); - } - } - extension_cases.push_str(" _ => None,\n"); - - let text = format!( - "pub const HAS_EMBEDDED_ASSETS: bool = true;\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]> {{ Some(include_bytes!({pg_dump})) }}\n\ - pub fn initdb_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({initdb})) }}\n\ - #[rustfmt::skip]\n\ - pub fn extension_archive(name: &str) -> Option<&'static [u8]> {{\n\ - match name {{\n\ - {extension_cases} }}\n\ - }}\n", - manifest = rust_string_literal(&manifest), - runtime = rust_string_literal(&runtime), - pgdata_archive_body = pgdata_archive_body, - pgdata_manifest_body = pgdata_manifest_body, - pg_dump = rust_string_literal(&pg_dump), - initdb = rust_string_literal(&initdb), - ); - fs::write(out, text).expect("write generated asset include module"); -} - -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":[]}"#; -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 } -"##; - fs::write(out, text).expect("write source-only asset include module"); -} - -fn rust_string_literal(path: &Path) -> String { - format!("{:?}", path.to_string_lossy()) -} - -fn optional_include_bytes_body(path: &Path) -> String { - if path.is_file() { - format!("Some(include_bytes!({}))", rust_string_literal(path)) - } else { - "None".to_owned() - } -} diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs deleted file mode 100644 index 48e49e89..00000000 --- a/crates/assets/src/lib.rs +++ /dev/null @@ -1,248 +0,0 @@ -#![deny(unsafe_code)] - -use serde::{Deserialize, Serialize}; - -include!(concat!(env!("OUT_DIR"), "/generated_assets.rs")); - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct AssetManifest { - pub format_version: u32, - pub runtime: RuntimeAsset, - #[serde(default)] - pub runtime_support: Vec, - #[serde(default)] - pub pg_dump: Option, - #[serde(default)] - pub initdb: Option, - #[serde(default)] - pub pgdata_template: Option, - #[serde(default)] - pub extensions: Vec, - #[serde(default)] - pub sources: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct RuntimeAsset { - pub archive: String, - pub sha256: String, - #[serde(default)] - pub module_sha256: String, - pub postgres_version: String, - pub runtime_kind: String, - #[serde(default)] - pub link: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct BinaryAsset { - pub name: String, - pub path: String, - pub sha256: String, - #[serde(default)] - pub module_sha256: String, - #[serde(default)] - pub native_module: Option, - pub size: u64, - #[serde(default)] - pub link: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct PgDataTemplateAsset { - pub archive: String, - pub manifest: String, - pub sha256: String, - pub size: u64, - pub runtime_module_sha256: String, - pub initdb_module_sha256: String, - pub source_pins_sha256: String, - pub postgres_version: String, - pub catalog_version: String, - pub init_profile: String, - pub wasmer_version: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct ExtensionAsset { - pub name: String, - pub sql_name: String, - #[serde(default)] - pub source_kind: String, - pub archive: String, - pub sha256: String, - #[serde(default)] - pub module_sha256: String, - pub size: u64, - #[serde(default)] - pub stable: bool, - #[serde(default)] - pub control_files: Vec, - #[serde(default)] - pub dependencies: Vec, - #[serde(default)] - pub native_dependencies: Vec, - #[serde(default)] - pub load_order: Vec, - #[serde(default)] - pub lifecycle: Option, - #[serde(default)] - pub extension_imports: Vec, - #[serde(default)] - pub core_exports_required: Vec, - #[serde(default)] - pub unresolved_imports: Vec, - #[serde(default)] - pub installed_files: Vec, - #[serde(default)] - pub smoke_status: Option, - #[serde(default)] - pub link: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct ExtensionLifecycle { - pub create_extension: bool, - #[serde(default)] - pub create_schema: Option, - #[serde(default)] - pub load_sql: Vec, - #[serde(default)] - pub post_create_sql: Vec, - #[serde(default)] - pub startup_config: Vec, - #[serde(default)] - pub preload_required: bool, - #[serde(default)] - pub restart_required: bool, - #[serde(default)] - pub shared_memory_required: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct ExtensionSmokeStatus { - pub promoted: bool, - pub direct: String, - pub server: String, - pub restart: String, - pub dump_restore: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmLinkMetadata { - pub has_dylink0: bool, - #[serde(default)] - pub dylink_needed: Vec, - #[serde(default)] - pub dylink_runtime_paths: Vec, - #[serde(default)] - pub dylink_memory: Option, - #[serde(default)] - pub dylink_imports: Vec, - #[serde(default)] - pub dylink_exports: Vec, - #[serde(default)] - pub imports: Vec, - #[serde(default)] - pub exports: Vec, - #[serde(default)] - pub memories: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmDylinkMemory { - pub memory_size: u32, - pub memory_alignment: u32, - pub table_size: u32, - pub table_alignment: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmDylinkSymbol { - pub module: Option, - pub name: String, - pub flags: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmImport { - pub module: String, - pub name: String, - pub kind: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmExport { - pub name: String, - pub kind: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct WasmMemory { - pub initial_pages: u64, - pub maximum_pages: Option, - pub memory64: bool, - pub shared: bool, - pub page_size_log2: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct SourcePin { - pub name: String, - pub url: String, - pub branch: String, - pub commit: String, -} - -pub fn manifest() -> Result { - serde_json::from_str(MANIFEST_JSON) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn manifest_parses_and_lists_promoted_extensions() { - 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()); - return; - } - assert_eq!(manifest.runtime.postgres_version, "17.5"); - assert_eq!(manifest.runtime.runtime_kind, "wasix-dynamic-main"); - assert!( - manifest - .extensions - .iter() - .any(|extension| extension.sql_name == "vector" && extension.stable) - ); - assert!( - manifest - .extensions - .iter() - .any(|extension| extension.sql_name == "pg_trgm" && extension.stable) - ); - assert!( - manifest - .extensions - .iter() - .any(|extension| extension.sql_name == "hstore" && extension.stable) - ); - } -} diff --git a/deny.toml b/deny.toml index 89695623..09afa8ef 100644 --- a/deny.toml +++ b/deny.toml @@ -1,12 +1,16 @@ [advisories] yanked = "warn" ignore = [ - # Wasmer/WASIX 7.2 alpha currently pulls bincode through virtual-net and + # Wasmer/WASIX 7.2.0 currently pulls bincode through virtual-net and # wasmer-journal. Keep this explicit until Wasmer removes or replaces it. "RUSTSEC-2025-0141", - # Wasmer 7.2 alpha currently pulls paste. Keep this explicit until upstream + # Wasmer 7.2.0 currently pulls paste. Keep this explicit until upstream # moves to a maintained macro helper or removes the dependency. "RUSTSEC-2024-0436", + # Wasmer 7.2.0's current stable derive crate still uses proc-macro-error2. + # The advisory reports maintenance status, not a known vulnerability, and + # provides no safe upgrade. Remove this exception when Wasmer migrates away. + "RUSTSEC-2026-0173", ] [licenses] @@ -17,7 +21,6 @@ allow = [ "BSD-2-Clause", "BSD-3-Clause", "BSL-1.0", - "CDLA-Permissive-2.0", "ISC", "MIT", "MIT-0", @@ -29,6 +32,13 @@ allow = [ ] confidence-threshold = 0.8 +# saffron 0.1.0 ships a BSD-3-Clause LICENSE file but declares it through +# license-file instead of a machine-readable SPDX expression. +[[licenses.clarify]] +crate = "saffron@0.1.0" +expression = "BSD-3-Clause" +license-files = [{ path = "LICENSE", hash = 0xb4dc584e }] + [bans] # Wasmer and dev-only PostgreSQL client compatibility tests pull a few # legitimate duplicate transitive versions. Keep this gate focused on actionable diff --git a/docs/ASSETS.md b/docs/ASSETS.md deleted file mode 100644 index 2f4f60e5..00000000 --- a/docs/ASSETS.md +++ /dev/null @@ -1,167 +0,0 @@ -# Maintainer Asset Notes - -This page is maintainer documentation for packaged runtime assets, generated -payloads, and release provenance. It is not end-user product documentation. -Application users should start with `README.md`, `docs/USAGE.md`, and -`docs/RUNTIME.md`. - -`pglite-oxide` ships the database runtime as package-managed assets. Most users -do not need to download Postgres, run Docker, install LLVM, or configure a -runtime path. - -## What Ships - -With default features, the crate includes: - -- the portable PGlite/Postgres WASIX runtime tree; -- a prepopulated PGDATA template for faster temporary databases; -- bundled extension archives for supported SQL extensions; -- the packaged `initdb` module used by asset CI and explicit fresh-initdb paths; -- the packaged `pg_dump` module used by the public dump API and CLI; -- a target-specific Wasmer AOT pack when the current host target is supported. - -The internal asset crates exist only because crates.io packages dependencies as -separate crates. Application code should depend on `pglite-oxide`, not on -`pglite-oxide-assets` or `pglite-oxide-aot-*` directly. - -## Feature Flags - -Default install: - -```toml -pglite-oxide = "0.4" -``` - -Default features include the packaged runtime/AOT assets and bundled extension -APIs: - -```toml -pglite-oxide = { version = "0.4", default-features = false, features = ["bundled"] } -``` - -The `bundled` feature keeps the package-managed PGlite/Postgres runtime and the -current platform's AOT crate, but leaves the public extension API disabled. -This is the "embedded Postgres without extension helpers" mode. - -Size-sensitive builds can opt out of packaged assets entirely: - -```toml -pglite-oxide = { version = "0.4", default-features = false } -``` - -When bundled assets are disabled, normal database opens do not have packaged -runtime/AOT assets available. This mode is intended for specialized maintainer -and custom-runtime workflows. - -## Cache Behavior - -Runtime files are expanded into a cache and then composed with a small writable -per-root skeleton by default. Temporary and template-backed databases use a -cached PGDATA template as a lower filesystem and materialize files into the -database root only when PostgreSQL opens them for mutation. - -The runtime tree keeps both `/bin/pglite` and `/bin/postgres`. They are the same -backend module; the `postgres` path exists so upstream `initdb` can discover and -spawn the backend through PostgreSQL's normal `find_other_exec()` path. - -The cache is content-addressed by the asset manifest and artifact hashes. If an -asset hash does not match the manifest, startup fails instead of using a mixed -or corrupted runtime. - -## Extension Assets - -Extensions are demand-driven. An extension archive is installed into the -database root only when the builder requests it or `enable_extension` is called: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -let mut db = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - -db.enable_extension(extensions::PG_TRGM)?; -# Ok::<_, Box>(()) -``` - -Archive extraction rejects parent traversal, absolute paths, symlinks, -hardlinks, device nodes, and unsupported entry types. - -## Provenance - -Asset provenance is recorded in `assets/sources.toml`, the committed asset -input fingerprint, and the generated asset/AOT manifests produced by the Assets -workflow. Generated manifests record source pins, runtime hashes, `initdb` -hashes, PGDATA template hashes, extension archive hashes, target information, -and Wasmer engine identity. - -The public repository tracks source-controlled inputs and crate skeletons. It -does not track upstream source checkouts, generated PGDATA templates, portable -WASIX blobs, or native AOT binaries. -Maintainer source trees are fetched on demand into ignored -`assets/checkouts/**` directories: - -```sh -cargo run -p xtask -- assets fetch -``` - -Normal development and source-free validation do not clone upstream repositories -or run Docker. The source-free gate is: - -```sh -cargo run -p xtask -- assets verify-committed -``` - -It verifies source pins, source/build input fingerprints, extension -metadata/constants when generated manifests are installed, AOT crate templates, -and the absence of committed PGDATA template, portable WASIX, or native AOT -blobs. - -Release assets are built with the `release-o3` profile by default: WASIX C code -uses `-O3 -g0 -flto=thin`, links with `-flto=thin`, and Binaryen runs the -wasixcc default optimization plus `--converge`, `--strip-debug`, and -`--strip-producers`. - -Generated runtime hashes in package metadata are refreshed in the release -staging workspace. They are not a committed source-of-truth value in normal -development; `assets/sources.toml` and `assets/generated/asset-inputs.sha256` -are the small committed provenance files. - -The `Assets` workflow mirrors the release topology: one Linux/Docker job builds -portable WASIX modules from `assets/wasix-build` into -`target/pglite-oxide/assets`, then native matrix jobs generate and package -target-specific Wasmer AOT crates into `target/pglite-oxide/aot/`. -Artifacts are uploaded with checksums, manifests, and the committed asset-input -fingerprint. - -Manual `Assets` runs use the same producer path. Maintainers may select one -native target for focused validation, but the workflow still rebuilds portable -WASIX assets, generates AOT artifacts, runs the runtime gate, stages the release -workspace, package-checks the target crate, and uploads the canonical release -artifact shape. - -Native AOT generation intentionally installs Wasmer's LLVM 22.1.x custom build -only inside the Assets workflow or a maintainer's explicit local artifact -build. Normal contributors and end users never need LLVM; they use committed -Rust sources plus downloaded or released AOT payloads. - -The normal CI runtime matrix downloads the latest compatible Assets workflow -bundle, verifies that the downloaded fingerprint matches the current source -inputs, installs the payloads into ignored generated paths, and runs runtime -tests. Any change to source pins, WASIX patches, extension catalogs, build -scripts, or AOT crate templates is treated as asset-producing and must pass the -full `Assets` workflow. Release validation downloads the exact-SHA portable and -AOT bundles, stages them into a clean release workspace, validates package -contents, and only then publishes. - -Published releases also attach public `.tar.zst` mirrors of the validated -portable WASIX and target AOT bundles. `xtask assets download --release ` -installs those release assets directly and does not require the GitHub CLI. - -After an intentional asset-source change and regenerated artifacts, refresh the -committed input fingerprint: - -```sh -cargo run -p xtask -- assets input-fingerprint --write -``` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md deleted file mode 100644 index e935e49e..00000000 --- a/docs/DEVELOPMENT.md +++ /dev/null @@ -1,196 +0,0 @@ -# Maintainer Development Guide - -This page is maintainer documentation for repository validation, generated -artifacts, and local release workflows. It is not end-user product -documentation. - -Run the local gates before opening a PR: - -```sh -scripts/bootstrap-tools.sh -scripts/validate.sh dev -scripts/validate.sh workflows -scripts/validate.sh supply-chain -``` - -The validation entrypoint is split by maintainer workflow: - -- `scripts/validate.sh repo`: file hygiene and formatting; -- `scripts/validate.sh artifacts`: source-controlled asset input verification - plus AOT crate template checks; -- `scripts/validate.sh lint`: dependency invariants and clippy; -- `scripts/validate.sh test`: source-only no-default-features checks, - doctests, and test compilation without requiring generated runtime assets; -- `scripts/validate.sh workflows`: local `actionlint` and `zizmor` checks using - the same zizmor config and severity/persona as CI; -- `scripts/validate.sh runtime`: hard-requires portable assets plus host AOT, - installs them into ignored paths, and runs the real runtime tests; -- `scripts/validate.sh runtime-smoke`: the runtime smoke subset; -- `scripts/validate.sh examples`: Tauri/Rust/frontend example checks; -- `scripts/validate.sh package`: package all published crates and enforce - crates.io size limits; -- `scripts/validate.sh feature-powerset`: cargo-hack feature combination checks; -- `scripts/validate.sh semver`: cargo-semver-checks public API compatibility; -- `scripts/validate.sh supply-chain`: cargo-deny dependency policy checks; -- `scripts/validate.sh ci`: full local CI parity lane; -- `scripts/validate.sh dev-ci`: fast contributor lane for repo, lint, source - tests, and examples; -- `scripts/validate.sh release`: release-workspace package checks plus publish - dry-runs for internal crates after CI-generated AOT artifacts have been - downloaded. - -The hook split is intentionally small: - -- pre-commit: file hygiene and formatting -- pre-push: whitespace diff check, `scripts/validate.sh lint`, and - `scripts/validate.sh test` -- CI/release: path-aware combinations of the same validation modes, workflow - linting, feature powerset, public API compatibility, crate packaging, - native AOT runtime tests, release-plz dry-run/publish, and supply-chain - policy - -Install local hooks and pinned CLI tools when needed. The bootstrap installs -`cargo-binstall` first and uses binary installs for Rust tools before falling -back to source builds. - -```sh -scripts/bootstrap-tools.sh -scripts/install-hooks.sh -``` - -`tests/runtime_smoke.rs` starts the real WASM backend and is intentionally -slower than the protocol unit tests. - -## Maintenance Utilities - -The repository includes maintenance commands: - -- `pglite-dump` is the logical dump CLI entry point. -- `pglite-proxy` exposes a local PostgreSQL socket backed by the embedded - runtime. -- `xtask assets template` generates the architecture-independent PGDATA - template from the split WASIX `initdb` module. Portable WASIX, PGDATA - templates, and native AOT payloads remain generated-only. - -Asset and source checks: - -```sh -cargo run -p xtask -- assets verify-committed -cargo run -p xtask -- assets fetch -cargo run -p xtask -- assets check --strict-local -cargo run -p xtask -- assets check --strict-generated -cargo run -p xtask --features template-runner -- assets template -cargo run -p xtask -- assets source-spine --check-patch-applies -cargo run -p xtask -- assets audit-upstream --strict -cargo run -p xtask -- assets input-fingerprint --write -cargo run -p xtask -- package-size --enforce -``` - -## Local Runtime Development - -Local development has three supported modes. - -Fast contributor mode does not require Docker, upstream source checkouts, or -generated native AOT payloads. Use it for ordinary Rust, docs, tests, examples, -and workflow edits: - -```sh -scripts/validate.sh dev-ci -cargo check --workspace --all-targets -cargo test --workspace --no-default-features -``` - -For the shortest source-only path, use: - -```sh -scripts/validate.sh dev -``` - -Host-platform artifact mode is for runtime work on the current machine. It -builds or packages only the current host target, leaves all generated payloads -in ignored paths, and then runs the real runtime tests: - -```sh -host="$(rustc -vV | awk '/^host:/{print $2}')" -cargo run -p xtask -- assets fetch -cargo run -p xtask --features aot-serializer -- assets build-host -scripts/validate.sh runtime -``` - -Local AOT generation requires the Wasmer LLVM 22.1.x build for the -maintainer-only serializer. That build includes the LLVM target set Wasmer's -LLVM backend expects, including LoongArch and WebAssembly. Set -`LLVM_SYS_221_PREFIX` to an extracted -`wasmerio/llvm-custom-builds` 22.x archive, or use downloaded-artifact mode to -avoid local LLVM setup. - -When the portable WASIX assets are already current and only the host AOT crate -needs to be refreshed, skip the source/Docker build and generate host AOT from -the existing generated portable assets: - -```sh -host="$(rustc -vV | awk '/^host:/{print $2}')" -cargo run -p xtask -- assets aot --target-triple "$host" -cargo run -p xtask -- assets package-aot --target-triple "$host" -scripts/validate.sh runtime -``` - -Downloaded-artifact mode is the intended way to test a CI-produced runtime -locally without rebuilding Postgres/WASIX. Download the successful Assets -workflow artifacts for the exact commit and install the host target payloads -into the same ignored generated locations used by the local build path: - -```sh -host="$(rustc -vV | awk '/^host:/{print $2}')" -cargo run -p xtask -- assets download --sha --target-triple "$host" -scripts/validate.sh runtime -``` - -For Rust-only work where the asset inputs have not changed, the same command -can install the latest compatible `main` bundle after verifying the -asset-input fingerprint: - -```sh -host="$(rustc -vV | awk '/^host:/{print $2}')" -cargo run -p xtask -- assets download --latest-compatible --target-triple "$host" -scripts/validate.sh runtime -``` - -Released artifact bundles can be installed without the GitHub CLI because they -are public GitHub release assets: - -```sh -host="$(rustc -vV | awk '/^host:/{print $2}')" -cargo run -p xtask -- assets download --release --target-triple "$host" -scripts/validate.sh runtime -``` - -Release validation can download every supported target from the exact Assets -workflow SHA: - -```sh -cargo run -p xtask -- assets download --sha --all-targets -scripts/validate.sh release -``` - -Developers should not be expected to build every target locally. Local runtime -work validates the host target; the Assets workflow is the authority for the -full macOS, Linux, and Windows AOT matrix. - -Contributors do not need upstream source checkouts for normal Rust, docs, -examples, or package validation. Maintainers fetch sources only when rebuilding -the portable WASIX runtime, extensions, `initdb`, `pg_dump`, or the generated -PGDATA template. Portable WASIX artifacts, generated PGDATA templates, and -native AOT artifacts are generated under `target/pglite-oxide/**` locally or by -CI; they are not committed to git. - -Rust-only PRs download the latest compatible Assets workflow bundle, verify its -asset-input fingerprint, install it into ignored generated paths, and run the -runtime test suite on every supported host target. Asset-producing PRs run the -heavier `Assets` workflow instead: that workflow rebuilds portable WASIX from -pinned sources, generates native AOT for every target, runs smoke tests, and -uploads release artifacts. - -Release process details are tracked in [RELEASE.md](RELEASE.md). -Completed implementation work is summarized in [DONE.md](DONE.md), and the -implementation backlog is tracked in [TODO.md](TODO.md). diff --git a/docs/DONE.md b/docs/DONE.md deleted file mode 100644 index 31ee0b09..00000000 --- a/docs/DONE.md +++ /dev/null @@ -1,928 +0,0 @@ -# Done (Maintainers) - -This is the single status document for implementation work already completed. -It is maintainer-facing and intentionally separate from the end-user docs. - -## Runtime Direction - -The repository now has one production direction: WASIX dynamic linking plus -headless Wasmer loading of CI-produced LLVM AOT artifacts. - -Removed or excluded from the production path: - -- Wasmtime/static-WASI runtime path; -- Emscripten/JavaScript glue runtime path; -- user-side Docker, LLVM, Cranelift, or local Postgres compilation; -- duplicated runtime layouts and host-side timezone/path rewrite shims; -- historical spike workspaces from the tracked repository. - -Production build inputs now live under `assets/`. - -## Workspace And Asset Crates - -Implemented: - -- root `pglite-oxide` crate remains the public crate; -- `pglite-oxide-assets` is the published runtime asset crate skeleton; -- source-only target AOT crate templates exist under `crates/aot/*`; -- `xtask` owns source checks, build orchestration, packaging, manifest checks, - package sizing, upstream audits, and source-spine validation; -- upstream checkouts are no longer tracked; maintainers fetch pinned sources on - demand into ignored `assets/checkouts`; -- source pins live in `assets/sources.toml`; -- root packages exclude upstream checkouts from published crates. -- `xtask assets verify-committed` validates source-controlled asset inputs, - source pins, package metadata, AOT crate templates, and generated extension - coherence when generated manifests are installed, without local upstream - checkouts; - -Generated release asset set: - -- portable PGlite WASIX runtime archive; -- `pg_dump.wasix.wasm`; -- deterministic `.tar.zst` archives for the 37 requested extension build - candidates. All 37 packaged extensions are stable public constants after - direct, server, restart, and lifecycle materialization gates; -- prepopulated PGDATA template archive; -- native Wasmer LLVM AOT artifacts. - -These artifacts are generated locally under `target/pglite-oxide/**` or by the -Assets workflow and are consumed by release staging without being committed to -git. - -## Source And Build Spine - -Implemented: - -- active source baseline switched to `electric-sql/postgres-pglite` - `REL_17_5-pglite` at `01792c31a62b7045eb22e93d7dad022bb64b1184`, matching - the audited `@electric-sql/pglite` 0.4.5 source/artifact pair; -- `pglite-build` `portable` is pinned as build-script provenance; -- maintained WASIX build files live under `assets/wasix-build`; -- `xtask assets build --execute` can produce the main runtime, support modules, - requested contrib/PGXS extension side modules, SQL-only extension payloads, - and `pg_dump` for the local target; -- `xtask assets package` emits deterministic archives, generated manifests, and - crate assets; -- `xtask assets aot` regenerates local Wasmer LLVM AOT artifacts; -- `xtask assets check --strict-generated` validates generated metadata; -- `xtask assets source-spine --check-patch-applies` validates the maintained - source patch and C ABI harness; -- `xtask assets audit-upstream --strict` records upstream fix decisions; -- required upstream fixes from `REL_17_5-pglite` are now the active source - spine rather than comparison material. The WASIX patch keeps dynamic-main and - side-module support, C startup timers, and explicit exports for the stable - branch lifecycle while reusing upstream `pgl_startPGlite`, - `pgl_setPGliteActive`, `ProcessStartupPacket`, `PostgresMainLoopOnce`, and - `PostgresMainLongJmp`; -- source-spine review conclusion: upstream PGlite's libc/host adaptations are - purposeful for wasm hosts, not arbitrary shortcuts. `pglitec.c` supplies - stable `postgres` identity, explicit process-active state, manual top-level - longjmp recovery, socket callbacks, shared-memory emulation, and explicit - atexit replay because browser/Emscripten cannot provide normal Postgres - child processes, sockets, Unix users, SysV shared memory, or a native process - lifecycle. The WASIX bridge keeps the same architectural contracts where - Wasmer still needs host assistance, but uses Rust-owned input/output buffers - instead of Emscripten callback pointers because the Rust host does not have - Emscripten's JS table callback mechanism; -- WASIX-specific deviation from upstream PGlite: top-level Postgres longjmp - detection uses `jmp_buf` pointer identity instead of upstream's buffer-content - `memcmp`. The memcmp test is acceptable in the Emscripten artifact it was - written for, but under Wasmer/WASIX it misclassified nested PostgreSQL - `PG_TRY` handlers and skipped normal portal cleanup. Pointer identity keeps - the host escape hatch scoped to the single exported top-level recovery buffer; -- WASIX-specific PostgreSQL fix: active portal abort cleanup is owned in - `AtAbort_Portals` for `PGLITE_WASIX_DL`, not in Rust. This keeps simple-query - and COPY error recovery at the PostgreSQL portal lifecycle boundary and avoids - fabricating cleanup behavior in the wire proxy; -- stable branch behavior note: startup `ParameterStatus` messages may be emitted - on raw protocol paths before `ReadyForQuery`. Tests now allow those legal - PostgreSQL messages instead of assuming the older minimal message sequence; -- new roots are now created through the packaged PGDATA template path. The old - embedded-backend `pgl_initdb` path was removed; explicit fresh-initdb paths - now use the bundled split WASIX `initdb` command and remain outside the - default fast path; -- the old builder-branch `pglite-wasm/*` runtime wrapper is no longer the - production patch target. It remains historical/reference material only; -- `xtask package-size --enforce` passes locally for the root, asset, and macOS - arm64 AOT crates. - -Parity verified against upstream PGlite stable source and TypeScript host: - -- startup/initdb: upstream TypeScript creates a cluster with `initdb.wasm`, - dumps PGDATA, loads that tarball into the main runtime, calls - `_pgl_setPGliteActive(1)`, runs `callMain([...startParams, -D, PGDATA, - PGDATABASE])`, expects exit `99`, then calls `_pgl_startPGlite()`. - pglite-oxide matches the main-runtime lifecycle from `_pgl_setPGliteActive` - onward and deliberately consumes a packaged PGDATA template instead of - exposing split runtime `initdb` yet. That is an explicit product gap, not a - hidden fallback; -- startup packet: upstream calls `_pgl_getMyProcPort()`, - `_ProcessStartupPacket(...)`, `_pgl_sendConnData()`, and `_pgl_pq_flush()`. - pglite-oxide uses the same C exports. Server connections now open the - embedded backend against the startup packet database, apply client startup - options on the C side, and apply non-`postgres` users through PostgreSQL - `SET ROLE` semantics, matching PGlite's single-process identity model; -- query loop: upstream feeds the whole frontend message buffer, repeatedly calls - `_PostgresMainLoopOnce()` while frontend bytes or libpq buffered data remain, - catches status `100`, calls `_PostgresMainLongJmp()`, then always calls - `_PostgresSendReadyForQueryIfNecessary()` and `_pgl_pq_flush()`. The Rust host - now follows that control flow with Rust-owned input/output buffers instead of - Emscripten callback pointers; -- close: upstream clears active state, sends protocol terminate, and replays - `_pgl_run_atexit_funcs()`. The Rust host clears active state and replays - atexit on shutdown, while tests cover clean restart, root locking, and stale - runtime-state cleanup; -- host ABI: upstream `pglitec.c` emulates sockets, identity, shared memory, - `system`/`popen`, timers, longjmp, and atexit because browser/Emscripten does - not provide normal process or OS services. pglite-oxide keeps the same - categories only where WASIX still needs host assistance, and the ABI harness - tests stable identity, fail-closed `system`, protocol fd bridging, shared - memory, atexit replay, mmap, and libpq encoding aliases; -- justified deviations: WASIX longjmp detection uses pointer identity instead of - upstream's `jmp_buf` content `memcmp`; simple-query/COPY portal abort cleanup - is owned in PostgreSQL `AtAbort_Portals`; startup `ParameterStatus` messages - are accepted as legal protocol output; split WASIX `initdb` is now the owned - template-generation path instead of resurrecting the old builder wrapper. - -## Runtime Behavior - -Implemented: - -- runtime loads verified headless Wasmer AOT artifacts; -- AOT artifacts record source module hash, Wasmer version, and engine identity; -- runtime verifies asset and archive hashes before use; -- unsupported targets return a clear missing-AOT-artifact error instead of - compiling locally; -- `Pglite::preload()` and `Pglite::preload_extensions(...)` exist; -- `Pglite::preload()` now warms the persistent runtime cache, headless Wasmer - engine, main AOT module, shared WASIX runtime, and runtime side modules; -- `Pglite::preload_extensions(...)` warms requested extension artifacts and - side-module cache entries generically; -- direct, persistent, app-id, proxy, server, and temporary roots now share the - `RootPlan`/`prepare_root` root-preparation pipeline; -- direct API, server API, proxy CLI, raw protocol API, and direct `pg_dump` now - share `BackendSession` for WASIX instance creation, backend start, startup - packet handling, protocol transport, shutdown, restart, and atexit replay; -- roots can install immutable runtime files from a persistent runtime cache and - install the embedded PGDATA template without running initdb on the default - startup path; -- mutable PGDATA template files are copied or archive-installed, never - hardlinked; immutable runtime files hardlink from cache when possible; -- persistent roots use lock files to prevent concurrent direct/server opens; -- runtime and extension archive extraction rejects unsafe paths, symlinks, - hardlinks, device nodes, and unsupported archive entry types; -- runtime uses canonical Postgres paths: - `/bin`, `/lib/postgresql`, `/share/postgresql/extension`, and - `/share/postgresql/timezonesets`. - -## Public API Surface - -Implemented: - -- `PgliteBuilder::extension`; -- `PgliteBuilder::extensions`; -- `PgliteBuilder::username`; -- `PgliteBuilder::database`; -- `PgliteBuilder::debug_level`; -- `PgliteBuilder::relaxed_durability`; -- `PgliteBuilder::startup_arg`; -- `PgliteBuilder::startup_args`; -- `PgliteBuilder::load_data_dir_archive`; -- `Pglite::enable_extension`; -- `Pglite::preload`; -- `Pglite::preload_extensions`; -- `Pglite::dump_data_dir`; -- `Pglite::dump_data_dir_with_format`; -- `Pglite::try_clone`; -- physical PGDATA archives now apply Wasmer overlay whiteouts, so files deleted - from the lower template are not resurrected by dump/load/clone; -- physical PGDATA archives are written from a materialized effective PGDATA view - instead of directly mixing lower-template and upper-overlay entries in the tar - writer; -- physical PGDATA archive/clone now checkpoints, quiesces the backend, - materializes the archive, and restarts the same backend session; docs state - this is a same-runtime/same-version physical import/export path, not a - cross-version backup protocol; -- `Pglite::exec_protocol_raw`; -- `Pglite::exec_protocol_raw_stream`; -- `Pglite::dump_sql`; -- `Pglite::dump_bytes`; -- `PgliteServerBuilder::extension`; -- `PgliteServerBuilder::extensions`; -- `PgliteServerBuilder::username`; -- `PgliteServerBuilder::database`; -- `PgliteServerBuilder::debug_level`; -- `PgliteServerBuilder::relaxed_durability`; -- `PgliteServerBuilder::startup_arg`; -- `PgliteServerBuilder::startup_args`; -- `PgliteServer::database_url`; -- `PgliteServer::dump_sql`; -- `PgliteServer::dump_bytes`; -- `PgDumpOptions`; -- 37 public extension constants plus `extensions::ALL`, covering the smoke-gated - packaged PGlite/Postgres catalog: `amcheck`, `auto_explain`, `bloom`, - `age`, `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`, SQL-only `pgtap`, `seg`, - `tablefunc`, `tcn`, `tsm_system_rows`, `tsm_system_time`, `unaccent`, and - `vector`. - -`pglite-dump` no longer exposes the old archive-unpack behavior. It is now a -real logical dump CLI backed by the packaged WASIX `pg_dump` module. - -`relaxed_durability` is a startup-profile flag rather than a hidden mutation of -`PostgresConfig`; explicit user `postgres_config` values win and -`relaxed_durability(true).relaxed_durability(false)` returns to the normal -profile. - -## Protocol And Server Correctness - -Implemented coverage: - -- direct Rust API open/init/query; -- persistence, close/reopen, stale runtime-state cleanup, interrupted PGDATA - cleanup, and root-lock conflicts; -- SQLx and `tokio-postgres` local-server connections; -- SSLRequest no-SSL response; -- CancelRequest safe close; -- backend-open failures no longer map every non-`template1` startup failure to - SQLSTATE `3D000`. PostgreSQL/C now owns startup identity and database errors: - the WASIX backend captures `InitPostgres` startup `ErrorResponse` bytes, the - proxy forwards them directly, and runtime/filesystem failures before - PostgreSQL can speak protocol remain synthesized `XX000`; -- Parse, Bind, and Execute error recovery; -- SQLSTATE preservation for syntax, missing relation, invalid typed parameter, - wrong parameter count, and extension-originated errors; -- extended-query `ReadyForQuery` synchronization; -- successful pipelined extended queries; -- mixed success/error/success pipelined queries; -- explicit prepared-statement reuse; -- transaction error recovery through rollback; -- client disconnect during an extended-query exchange; -- partial TCP reads and pipelined simple queries; -- server-mode `COPY FROM STDIN` now streams through the backend-owned protocol - pump instead of Rust SQL-text detection or proxy-fabricated COPY state. Normal - SQLx/tokio-postgres traffic uses the buffered raw-protocol path; when - PostgreSQL emits a real `CopyInResponse`, `CopyOutResponse`, or - `CopyBothResponse`, the WASIX bridge flushes buffered backend output to the - attached socket continuation and lets PostgreSQL continue on the socket. Raw - wire coverage includes simple COPY, extended-protocol COPY, CSV `WITH (...)` - COPY, binary COPY, `CopyData`, `CopyDone`, `CopyFail`, Unix-socket COPY - parity, and post-COPY connection reuse. -- continuation bytes are borrowed in the proxy read loop and materialized only - after the C bridge reports active streaming COPY; -- direct raw protocol streaming is routed through the shared `BackendSession` - framed sender instead of a separate client-only transport path; -- Rust-owned guest bridge allocations are scoped through `pg_free`/`free`, and - debug builds now have a direct raw-protocol stress test proving repeated - bridge round trips keep allocation/free counters balanced; -- direct LISTEN/UNLISTEN quotes channel identifiers and dispatches notifications - by the exact backend channel name, including case-sensitive and quoted names. -- a larger PostgreSQL regression subset now ports the relevant PGlite test - surface for datatypes, DDL, transactions/savepoints, planner/index behavior, - and direct `/dev/blob` CSV COPY. The datatype coverage also found and fixed a - direct-client multidimensional array parser bug, with unit coverage for - nested arrays, quoted values, and unquoted NULL handling. - -## Independent P0 Architecture Review - -The P0 review was re-run against the current Rust host, WASIX bridge, source -patch, and regression tests. No current P0 architecture blockers remain in the -reviewed surface. The completed P0 items were moved out of the backlog; future -major protocol, backup, runtime, or source-spine changes should get a new -review entry here instead of leaving completed checklists in `TODO.md`. - -Verified ownership boundaries: - -- Rust owns hosting, root preparation, caches, process lifecycle, direct/server - API shape, and typed fallbacks for host/runtime failures before PostgreSQL can - speak wire protocol; -- PostgreSQL/C owns SQLSTATEs, startup identity/database errors, query protocol - state, COPY state, portal cleanup, and longjmp recovery boundaries; -- the WASIX bridge owns only the host ABI that Wasmer/WASIX cannot provide as a - normal OS process boundary: protocol fd transport, locale/identity shims, - single-process shared memory, fail-closed process calls, and explicit - allocation/free ownership. - -Review conclusions: - -- guest-memory ownership is scoped through `GuestAllocator`, `pg_free`/`free`, - and debug allocation/free counters; -- detached protocol stdio fails closed rather than silently accepting bytes; -- COPY state is reported by PostgreSQL through - `pgl_protocol_report_copy_response`; the proxy no longer parses SQL text, - fabricates COPY state, scans whole backend buffers, or eagerly copies - continuation bytes for ordinary traffic; -- direct raw protocol streaming and direct `pg_dump` use the shared - `BackendSession` transport instead of a separate clone/server path; -- startup role/database failures are PostgreSQL-owned: WASIX backend open - captures `InitPostgres` `ErrorResponse` bytes, the proxy forwards those bytes, - and Rust no longer probes `pg_database` or string-guesses `3D000`; -- direct API, server API, proxy CLI, raw protocol, physical archive/clone, and - direct `pg_dump` share `RootPlan`/`prepare_root` and `BackendSession` - lifecycle paths; -- side-module cache seeding is keyed by artifact name, source module hash, - Wasmer version, Wasmer-WASIX version, and engine identity; -- AOT startup keeps full SHA verification behind - `PGLITE_OXIDE_AOT_VERIFY=full` while default loading uses metadata receipts - and mmap/native deserialization; -- PGDATA physical archive/clone materializes the effective overlay view with - whiteouts, quiesces/restarts the backend, and is documented as - same-runtime/same-version physical transfer rather than a WAL-aware backup; -- public API parity additions were reviewed: `fresh_temporary()` stayed out, - raw protocol streaming is real, physical clone/export has honest semantics, - startup args remain advanced, and listener channel names are identifier - quoted. - -Residual work from this review is intentionally not P0 architecture debt: -target-matrix CI, broader extension generation, additional PostgreSQL -regression subsets, release performance gates, and future split-WASIX `initdb` -support remain tracked in `TODO.md`. - -## Extensions And `pg_dump` - -Implemented coverage: - -- `vector` direct API load, `CREATE EXTENSION`, insert, distance query, and - pgvector type cases; -- `vector` through `PgliteServer` and SQLx; -- SQLx recovery after vector-originated errors; -- demand-driven extension install and idempotent `enable_extension`; -- installed extension side modules are seeded into the headless Wasmer cache on - reopen; -- `pg_trgm` direct API and SQLx server smoke coverage; -- `hstore` direct API, persistence/reopen, and SQLx server smoke coverage; -- PGlite extension tests were ported into a generic promotion gate for direct - API, server API, restart, and lifecycle materialization. The gate now covers - every packaged candidate. AGE now uses its upstream 32-bit `SIZEOF_DATUM=4` - SQL generation path, passes direct/server/restart/lifecycle gates, and is - exposed as `extensions::AGE`; -- extension discovery now merges PGlite docs/REPL exports, PGlite package - exports, PostgreSQL contrib metadata, `postgres-pglite` `other_extensions` - pins, PGlite tests, and the packaged asset manifest into - `assets/generated/extensions.catalog.json`; -- `xtask assets fetch` now clones/fetches every pinned source from - `assets/sources.toml` into ignored `assets/checkouts/**` directories, - including the external extension sources for pgtap, pg_ivm, pg_uuidv7, - pg_hashids, AGE, PostGIS, and pg_textsearch; -- extension build intent now lives in `assets/extensions.promoted.toml` instead - of being inferred from already-packaged artifacts. The generated catalog - separates requested, packaged, stable, and publicly promoted state; -- extension smoke evidence now lives in `assets/extensions.smoke.toml`; - generated public constants require requested + packaged + stable + direct, - server, and restart smoke status recorded as passed; -- `xtask extensions build-plan --write` generates - `assets/generated/extensions.build-plan.json`, - `assets/generated/contrib-build.tsv`, and - `assets/generated/pgxs-build.tsv`; `xtask assets check --strict-generated` - fails if those generated files drift; -- the WASIX extension build spine now uses generic contrib and PGXS build - scripts driven by the generated build plans, replacing the previous - `pg_trgm`-only and `pgvector`-only Docker scripts; -- the generated catalog now requires every discovered SQL extension to be - either requested for build or explicitly blocked with a concrete reason. The - current catalog discovers 40 SQL extensions, requests/packages 37, and blocks - only `pgcrypto`, PostGIS, and `uuid-ossp` on missing pinned native dependency - stacks; -- native side-module names are generated from control-file `module_pathname` - and PGXS Makefile metadata instead of assuming `.so`. This covers - cases such as `intarray` using `_int.so` and SQL-only extensions such as - `pgtap`; -- 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 `pglite-oxide-assets`; -- contrib packaging now carries extension-owned tsearch rule files into - `share/postgresql/tsearch_data`, matching PGlite behavior for `dict_xsyn` and - `unaccent`; -- generated extension constants are emitted only for extensions that are - requested, packaged, stable, and direct/server/restart smoke-passed; generated - asset includes carry all packaged candidates so private promotion tests can - exercise candidates before they become public API; -- manifest metadata records extension source kind, control files, - dependencies, lifecycle, imports, required core exports, unresolved imports, - installed files, load order, and smoke status; -- the `wasix-dl` export list is generated from the runtime exports plus - runtime-support/extension side-module imports, rather than being a - hand-maintained export allowlist; -- extension archive hash mismatch rejection; -- public WASIX `pg_dump` runner loads through the AOT manifest, connects to - `PgliteServer`, dumps plain SQL, restores into fresh `Pglite`, and verifies - schema/data; -- direct `Pglite::dump_sql` no longer uses a temporary physical clone, public - `PgliteServer`, or OS loopback TCP; it runs the standalone WASIX `pg_dump` - against an in-process Wasmer virtual TCP connection whose host side is routed - through the same direct raw-protocol backend; -- direct `Pglite::dump_sql` rejects database/user options that would imply a - different backend than the already-open direct session; callers needing that - use the server `pg_dump` path; -- the direct `pg_dump` transport keeps `pg_dump`/libpq stock and owns the only - required semantic adapter in Rust: a first-write-readiness normalization for - Wasmer's in-memory `TcpSocketHalf` so libpq's connect-time and first-write - polls remain level-triggered; -- public `pg_dump` coverage includes indexes, views, sequences, - `--schema-only`, `--quote-all-identifiers`, source-server reuse after dump, - and vector extension dump/restore; -- `PgDumpOptions` rejects passthrough flags that conflict with the typed - output/connection contract instead of letting callers override the internal - output file, format, host, port, username, database, or job count. - -## WASIX C Boundary Ownership - -The remaining C-side differences are owned as WASIX portability and host ABI, -not hidden generic stubs: - -- `pg_proto.c` manually coordinates `ReadyForQuery` for the current - call/return protocol loop and is covered by SQLx, `tokio-postgres`, and raw - wire-protocol tests; -- `pg_main.c` drives initdb boot/single-user phases inside one embedded process, - with named helpers for boot, stdin restoration, and single-user replay; -- `pgl_os.h` emulates only the expected initdb boot/single `popen()` commands - under `PGLITE_WASIX_DL` and fails closed otherwise; -- `pgl_stubs.h` is gated to `PGLITE_WASIX_DL`, and future removals are driven by - link-symbol analysis; -- `pglite_wasix_bridge.c` owns locale command emulation, stable `postgres` - uid/passwd identity, protocol socket buffers, fail-closed `system()`, selected - fd/socket delegation to WASIX libc, and single-process SysV shared memory. - -The source-spine guard checks for removed spike smells: debug-only `#pragma` -markers, diagnostic `popen`, broad socket fake-success behavior, layout -mirroring, timezone rewrites, and generic stub logging. - -## Validation Already Run - -The following local gates passed before this consolidation: - -```sh -cargo fmt --check -cargo check -p pglite-oxide --all-targets -cargo check -p pglite-oxide --no-default-features --all-targets -cargo run -p xtask -- assets check --strict-generated -cargo run -p xtask -- assets source-spine --check-patch-applies -cargo run -p xtask -- assets audit-upstream --strict -cargo run -p xtask -- package-size --enforce -cargo test --test client_compat -cargo test --test runtime_smoke -cargo test --test extensions_smoke -``` - -The public `pg_dump` round-trip tests and asset/AOT hash-mismatch tests also -passed locally. - -## Cold-Start Performance Work - -Implemented: - -- internal phase timing via `capture_phase_timings`; -- `cargo run -p xtask -- perf cold` emits structured JSON with explicit - `cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and - `workload` fields, so first-install bootstrap, process warmup, new-root first - query, and client/server first query are no longer conflated; -- `cargo run -p xtask -- perf cold --reset-cache` removes the pglite-oxide cache - before measuring, making runtime extraction, AOT materialization, PGDATA - template install, and extension-template creation visible in the first - operation that pays each cost; -- process-wide headless Wasmer engine cache; -- process-wide AOT `Module` cache keyed by artifact hash; -- AOT manifests now include raw artifact SHA256/size metadata; the default - startup path uses an atomic cache receipt and file metadata instead of scanning - the raw AOT file; -- bundled runtime, extension, PGDATA-template, and AOT content hashes are kept - off the default startup path and are only scanned with - `PGLITE_OXIDE_AOT_VERIFY=full`; -- process-wide shared Tokio runtime, WASIX runtime, and `SharedCache`; -- side-module seeding is reused by artifact name, module hash, Wasmer version, - Wasmer-WASIX version, and engine identity; -- phase timing now propagates into the server listener thread, so - `PgliteServer` cold runs report root preparation, listener bind/spawn, - proxy backend open, client connect, first query, and shutdown phases instead - of a single opaque total; -- server accept loops now use blocking `accept()` plus an explicit wake - connection during shutdown, removing the previous nonblocking accept plus - 10ms sleep polling jitter; -- fresh proxy backend initialization no longer runs the post-client - `ROLLBACK`/`DISCARD ALL` cleanup path. Fresh startup applies default GUCs - directly; full reset remains in place after client disconnects; -- persistent runtime asset cache under the platform cache directory; -- runtime-cache repair removes mutable scratch state and restores required - support files before the cache is used as a shared overlay source; -- per-root runtime scratch directories are reset during root preparation; -- `password` is copied as per-root mutable support data instead of hardlinked - from the shared runtime cache; -- PGDATA template manifests are parsed without archive hashing on the default - path; -- the parsed generated asset manifest is cached process-wide, avoiding repeated - 1.4 MB JSON parses during AOT, extension, and PGDATA template checks; -- an eager PGDATA template overlay is implemented as the mainline template - path: the cached initialized template is mounted as lower `/base`, the - per-instance upper starts almost empty, and individual template files are - copied into the upper only before mutating opens; -- the eager PGDATA overlay is passed as a runner-level WASIX mount. Nested - mounts placed inside the supplied `WasiFsRoot` were not sufficient because - `WasiRunner::prepare_webc_env` rebuilds the final mount tree from the root - `/` filesystem plus runner-owned mounts; -- direct `Pglite::open` no longer performs a separate session-setup round trip - and no longer folds session defaults into array discovery SQL. The Rust WASIX - host now calls the real C `ProcessStartupPacket` export from - `backend_startup.c`; C `pgl_sendConnData()` applies the direct-session - defaults before connection data is sent, so `BeginReportingGUCOptions` - observes `TimeZone=UTC` and `search_path=public`; -- `PgliteBuilder::postgres_config`, `PgliteServerBuilder::postgres_config`, - and `pglite-proxy --postgres-config name=value` now pass user startup GUCs - through PostgreSQL's normal `-c name=value` argv handling. User settings are - appended after the default profile, so they override defaults without - special-casing individual GUCs such as `synchronous_commit`; -- server-mode client startup `options=-c ...` is now applied on the C side after - `ProcessStartupPacket` parses the packet and before `pgl_sendConnData()` - emits `AuthenticationOk` and `ParameterStatus`, preserving PostgreSQL's - startup-option timing for supported single-backend clients; -- extension-enabled PGDATA template caches include the startup-GUC entries in - their manifest and cache key, so a template created under one backend config - is not reused for another config; -- direct scalar open/query paths no longer scan `pg_type` for array metadata. - Built-in PostgreSQL array OIDs are registered statically in the Rust direct - client, and runtime-created enum/domain/composite arrays are discovered - lazily from parameter/result OIDs or through explicit - `refresh_array_types()` calls; -- the old `pgl_stubs.h` `ProcessStartupPacket` placeholder has been removed - from the maintained WASIX patch. Startup packet parsing now lives in - PostgreSQL's `backend_startup.c`, and the host no longer calls a separate - Rust-side default-GUC helper; -- focused tests cover process AOT cache reuse, extension preload reuse, - cross-instance state isolation, mutable PGDATA clone safety, eager PGDATA - lower-file visibility, direct runtime smoke, vector direct/server smoke, and - proxy smoke. - -Previous local debug `xtask perf cold` run after explicit preload: - -- explicit preload: about 605ms; -- temporary first query: about 553ms; -- warm temporary first query: about 547ms; -- representative extension-backed first query after extension preload: about - 646ms; -- server plus first `tokio-postgres` query: about 543ms. - -In that run bundled archive/module SHA scans were absent from the default path. -The remaining visible costs were main Wasmer deserialization at about 447ms and -temporary filesystem setup at about 321ms, mostly runtime clone plus PGDATA -template clone. - -Latest local debug `cargo run -p xtask -- perf cold` run after the shared -root-preparation work: - -- explicit preload: about 640ms; -- temporary first query: about 404ms; -- warm temporary first query: about 386ms; -- representative extension-backed first query after extension preload: about - 504ms; -- server plus first `tokio-postgres` query: about 371ms. - -That run removed the full immutable runtime clone from temporary opens. The -same prepared runtime-layout machinery now feeds direct, persistent, app-id, -proxy, and server roots as well. Per-root runtime setup was about 30ms and -`wasix.mountfs_overlay_construct` was under 1ms at that point. The dominant -remaining setup cost was PGDATA template clone/install at about 187-190ms, -followed by -backend start around 44-48ms and Wasmer instance creation around 30-36ms. - -Latest local debug `cargo run -p xtask -- perf cold` run after the eager PGDATA -overlay and parsed-manifest cache: - -- explicit preload: about 601ms; -- temporary first query: about 191ms; -- warm temporary first query: about 144ms; -- representative extension-backed first query after extension preload: about - 257ms; -- server plus first `tokio-postgres` query: about 123ms. - -In that run `pgdata.overlay_prepare` was about 0.4-0.5ms, down from the -previous 187-190ms template clone/install cost. The visible per-open costs are now -Wasmer instance creation around 30-37ms and PostgreSQL backend start around -49-52ms. Main-module AOT deserialization remains the dominant explicit preload -cost at about 506ms on this local debug profile. - -Historical local debug run after removing the separate direct session-setup -round trip, before lazy/generated array metadata: - -- explicit preload: about 535ms; -- temporary first query: about 230ms; -- warm temporary first query: about 133ms; -- representative extension-backed first query after extension preload: about - 254ms; -- server plus first `tokio-postgres` query: about 118ms. - -The warm direct `pglite.open` phase dropped to about 112ms. At that point the -remaining direct-open client-side cost was the array catalog scan, about 30ms -for the warm catalog query and less than 1ms for Rust-side parser/serializer -registration. Scalar paths no longer pay that scan after lazy/generated array -metadata. - -Latest local release work: - -- asset release builds now default to `release-o3`, which compiles WASIX C - modules with `-O3 -g0 -flto=thin` and links with `-flto=thin`; -- release profiles run wasixcc's default Binaryen optimization plus - `--converge`, `--strip-debug`, and `--strip-producers`; -- the current exact PGlite speed-suite run favors `release-o3 + converge/strip` - plus ThinLTO for SQL workload parity. The package-size gate still passes - locally with the macOS arm64 AOT crate at about 7.2MiB compressed and the - asset crate at about 5.6MiB compressed. Earlier startup-only runs favored - `release-os` over `release-oz`, and adding a project `-msimd128` flag was - redundant because the WASIX EH+PIC sysroot already invokes clang with SIMD, - relaxed SIMD, and extended const enabled; -- Wasmer LLVM AOT codegen experiments selected the mainline serializer profile: - nonvolatile memory operations plus a readonly funcref table. Nonvolatile - memory operations improved the exact PGlite server SQLx speed suite by about - 9% geomean and won all 18 cases, but Wasmer marks that optimization as not - fully WebAssembly-spec compliant. Adding readonly funcref on top was about - 1.4% faster geomean than nonvolatile-only and improved indexed updates, but - regressed CREATE INDEX and DROP TABLE cases. The risk is now explicit release - profile surface and must be covered by the correctness matrix. The macOS - arm64 packaged AOT artifacts were regenerated with this profile; -- exact PGlite speed-suite comparison now has its own harness and diagnostic - path. The latest ThinLTO `release-o3` direct run on macOS arm64 measured test - 9 at about 569ms, test 10 at about 724ms, test 11 at about 98ms, and test 14 - at about 77ms. Against the locally audited npm NodeFS reference, the direct - suite is about 1.22x faster geomean, with 16/18 wins but not a 10x-class - result under identical SQL/Postgres semantics; -- selected speed-case diagnostics show that host filesystem work is not the - remaining dominant cost on the heavy SQL cases. Test 10, for example, was - about 748ms total with about 21ms in traced filesystem work and about 743ms - inside PostgreSQL/AOT dispatch. This points the next investigation at - symbolized AOT/Postgres executor profiling, not more Rust result parsing or - root-layout tuning; -- prepared indexed-update benchmarking now compares SQLx sequential prepared - updates, tokio-postgres sequential prepared updates over TCP and Unix - sockets, tokio-postgres pipelined prepared updates over TCP and Unix sockets, - and native Postgres equivalents using the exact PGlite Test 9/10 values. - Deferring extended-protocol `Sync` flush only within bytes already read from - one socket read reduced PgliteServer TCP pipelined prepared updates from about - `612.835ms -> 399.921ms` for numeric indexed updates and - `640.691ms -> 416.837ms` for text indexed updates. Unix-socket PgliteServer - was faster again at about 374/397ms, so transport still matters for - sequential prepared execution and modestly for pipelined execution. The exact - simple-query server speed suite stayed in the same range after the change: - Test 9 about 583ms and Test 10 about 740ms locally. A larger 256KiB proxy - read buffer was tested and rejected because it regressed the same pipelined - prepared workload to about 545/562ms; -- the native Postgres benchmark helper now attempts graceful termination before - falling back to `Child::kill()`, because SIGKILL can leak SysV shared-memory - IDs on macOS. `perf prepared-updates --skip-native` exists for Pglite-only - runs when local native Postgres IPC state is unhealthy; -- `perf prepared-updates --gate` now emits protocol counters and fails if - ordinary prepared traffic activates the backend-owned streaming continuation - or if pipelined prepared traffic stops batching. The timing thresholds are - intentionally a local regression smoke gate until stable CI runner baselines - exist; -- phase timing guards are hot-path no-ops when no recorder is active, so - diagnostic spans do not call `Instant::now()` in normal runtime traffic; -- PostgreSQL spinlocks are enabled in the WASIX build. The earlier - `--disable-spinlocks` fallback is gone, and the source-spine guard rejects it - if it returns. This is a correctness/architecture baseline because wasixcc - exposes the required atomic operations; local single-backend speed numbers are - mixed enough that it should not be treated as a standalone benchmark win; -- the shared runtime overlay and eager PGDATA overlay are now mainline runtime - behavior, with the old full-local runtime and full-template clone paths kept - only as internal build/staging machinery where still required; -- local release `cargo run -p xtask -- perf cold` with no env overrides showed - warmed preload around 18ms, temporary first query around 100ms, warm temporary - first query around 83ms, representative extension-backed first query around - 148ms after extension preload, and server first query around 77ms; -- that run predated lazy/generated array metadata and showed direct open - dominated by backend startup around 33-40ms plus the old array catalog scan - around 24-33ms. Scalar paths no longer pay that catalog scan; new release - numbers should replace this historical baseline; -- after adding deeper preload instrumentation, local release runs showed - explicit preload between about 15ms and 56ms depending on OS cache warmth. - The first uncached visible run spent about 37ms in main AOT mmap - deserialization and about 10ms in runtime cache setup; repeated warmed runs - spent about 10ms in main AOT deserialization for both mmap and file modes; -- Wasmer AOT loading now uses the native mmapped-file deserializer as the only - production path; the old file deserializer runtime switch was removed; -- after promoting the mainline AOT and filesystem paths, local release - `cargo run --release -p xtask -- perf cold` showed primary visible latencies - around 36ms for preload, 55ms for a new temporary direct first query, 45ms - for a second new temporary direct first query, 47ms for server SQLx first - query, and 57ms for server SQLx vector first query; -- the same mainline artifact profile measured exact PGlite server speed-suite - Test 9 at about 587ms, Test 10 at about 730ms, Test 11 at about 91ms, Test 14 - at about 71ms, and 18-test geomean around 76ms locally. Prepared-update - server probes measured TCP pipelined prepared updates around 395/414ms and - Unix pipelined prepared updates around 366/392ms for the numeric/text indexed - workloads; -- after static built-in arrays and lazy runtime array discovery, local release - `cargo run --release -p xtask -- perf cold` showed explicit preload about - 52ms, temporary first query about 88ms, warm temporary first query about 79ms, - representative extension-backed first query about 131ms after extension - preload, and server first query about 75ms. Scalar direct paths did not emit - the `pglite.array_type_catalog_query` phase; -- after server-thread timing and accept-loop cleanup, local release - `cargo run --release -p xtask -- perf cold` showed explicit preload about - 19-22ms, temporary first query about 86-89ms, warm temporary first query about - 77ms, representative extension-backed first query about 132-140ms after - extension preload, tokio-postgres server first query about 68ms, and SQLx - server first query about 68-70ms. The server path now shows `server.start` - around 52-54ms, - `proxy.backend_open` around 44-46ms, `postgres.backend_start` around 35-37ms, - tokio-postgres connect around 0.6ms/query around 5.5ms, and SQLx connect - around 2.1ms/query around 6.0ms; -- `xtask perf cold` includes the extension-enabled SQLx server path, now named - `process_warm_new_temp_server_sqlx_vector_first_query`, which starts - `PgliteServer` with a requested bundled extension and measures a first - extension-backed SQLx query for a new temporary server root. This keeps - server-mode extension install/load, `CREATE EXTENSION`, client connect, and - first extension query visible as one product-shaped path. The first local - release run measured about 175ms total, dominated by `proxy.extension_enable` - around 107ms; SQLx connect and the - first vector query were both sub-millisecond on that run; -- cold perf reporting now breaks out preload runtime cache setup, AOT install, - mmap/file deserialization, WASIX runtime construction, instance creation, - startup-packet/default-GUC work, client protocol round trips, extension side - module seeding, and public `pg_dump` runner phases; -- instrumented WASIX runtime artifacts can export C-side backend startup timers - via `pgl_backend_timing_elapsed_us`, and the Rust host records them as - `postgres.backend.c.*` phases when the export is present. Production WASIX - artifacts keep `PGLITE_OXIDE_WASIX_BACKEND_TIMING=0`, so the C timing macros - compile away and the export is absent. Local release instrumented runs show - backend startup split mainly between `postgres.backend.c.shared_memory` around - 11-12ms and `postgres.backend.c.init_postgres` around 19-21ms, inside - `postgres.backend.c.async_single_user_main` around 33-36ms; -- C-side timers now reach inside `InitPostgres`: `StartupXLOG`, - relcache/catcache initialization, transaction snapshot, session-user setup, - database lookup/recheck/path validation, `CheckMyDatabase`, startup option - processing, session initialization, and session preload libraries are reported - as individual `postgres.backend.c.*` phases; -- the C timing ABI has additional instrumented-only IDs for - `InitializeMaxBackends`, `CreateSharedMemoryAndSemaphores`, `InitProcess`, - `RelationCacheInitializePhase3`, and `initialize_acl`, so the two remaining - startup hotspots can be subdivided without adding production clock reads; -- a generic extension-set PGDATA template cache now builds templates through - normal `CREATE EXTENSION`, runs `CHECKPOINT`, then closes the embedded backend - through the runtime `pgl_shutdown` export before caching the template. The - cache is keyed by the base runtime/template manifest plus sorted extension - archive identities and is mounted as the lower PGDATA template for direct and - server temporary roots; -- direct and server extension paths skip redundant `CREATE EXTENSION` when the - requested extension set is already present in the cached template, while still - installing/preloading side-module assets into each instance root; -- extension-template cache keys were bumped to version 2 after adding clean - backend shutdown, so older templates that left `pg_control` in a - recovery-heavy state are ignored; -- current local release timings with the clean generic extension template cache - show extension-template lookup/overlay under 1ms, extension archive install - around 5ms, and extension-enabled `StartupXLOG` around 3-4ms instead of the - previous roughly 350ms recovery path. In the steady cached run, the direct - vector first-query path for a new temporary root was about 82-93ms and the - SQLx vector first-query path for a new temporary server root was about - 74-78ms; -- pure MountFS runtime composition now keeps core runtime assets in the shared - cached lower runtime and materializes only mutable state plus requested - extension assets in the per-root upper layer. Runtime and extension smoke - tests assert that core binaries/catalog files are not copied into the upper - root and unrelated extensions are not installed. Local release comparison - showed per-root runtime setup dropping from roughly 7ms to about 0.6-0.9ms, - the SQLx first-query path for a new temporary server root around 55ms, and - the SQLx vector first-query path for a new temporary server root around 66ms - after cache cleanup; -- cold perf operations now report `primaryLatencyPhase` and - `primaryLatencyMicros` so user-visible latency is separated from teardown. - The deeper local release run showed direct first-query totals were previously - inflated by a Rust-side host directory sync during query finish; -- direct `Pglite` no longer calls host directory `sync_all` after every - non-transaction query. PostgreSQL's WAL/fsync path owns durability, and the - server path already avoided this extra host sync. In the local release run, - direct visible latency dropped from about 68ms to about 53ms for the first - new temporary root and to about 45ms for the second new temporary root; -- direct and server protocol timing now splits startup packet handling, - protocol input/output, guest `PostgresMainLoopOnce`, direct parse/describe, - direct execute, and direct result finish. The remaining first-query protocol - cost is mostly PostgreSQL main-loop work for the parse/describe or prepared - extended-query batch, not Rust parsing or buffer copies; -- `cargo run -p xtask -- perf warm` now measures true warm behavior separately - from first-open work: repeated direct scalar queries, direct transaction - batches, direct extension-backed queries, SQLx repeated queries over one - connection, SQLx repeated connect-query-close cycles, SQLx extension-backed - repeated queries, and tokio-postgres repeated queries. It reports total and - per-iteration average phases while keeping open/shutdown phases as context; -- `cargo run --release -p xtask -- perf bench` now provides a product-style - benchmark harness similar to PGlite's published benchmark families. It runs - trimmed-average CRUD round-trip benchmarks and a generated SQLite - speedtest-style suite through both the direct Rust API and `PgliteServer` - with a long-lived SQLx connection. The speed suite is generated locally - instead of vendoring PGlite's multi-megabyte generated SQL files, and supports - `--suite`, `--mode`, `--iterations`, and `--scale` for local and CI runs; -- May 1, 2026 local release parity/timing run after pinning - `REL_17_5-pglite@01792c31` recorded raw JSON under `target/perf/`: - `cold-release-latest.json`, `warm-release-latest.json`, and - `bench-release-latest.json`; -- that cold release run used existing caches and production artifacts, so C-side - backend timers were absent by design. Primary visible latencies were: - preload 28.8ms, first direct temporary query 41.1ms, second direct temporary - query 30.0ms, vector preload 8.4ms, first direct vector query 36.8ms, - first tokio-postgres server query 31.4ms, first SQLx server query 31.9ms, - first SQLx server vector query 36.9ms, and first SQLx vector query on an - existing persistent root 25.8ms; -- dominant cold phases in that run were production runtime/AOT preload - (`aot.deserialize.mmap` 16.3ms), Wasmer instance creation for new roots - (about 5.3-10.2ms), backend start for template roots (about 18-24ms), and - first protocol dispatch/query work (about 4.4-6.1ms). Per-root runtime setup - stayed below the 1ms reporting threshold for scalar temporary roots; -- warm release run with 100 query iterations and 20 connect iterations showed: - direct scalar repeated query average 0.024ms, direct transaction batch average - 0.022ms, direct vector repeated query average 0.025ms, SQLx single-connection - query average 0.054ms, SQLx vector single-connection query average 0.058ms, - tokio-postgres single-connection query average 0.175ms, and SQLx - connect-query-close average 18.565ms; -- product-style benchmark run with `--suite all --mode all --iterations 100 - --scale 1` showed RTT trimmed averages from about 0.031-0.101ms for direct - CRUD cases and about 0.055-0.130ms for SQLx server CRUD cases. The generated - speed suite remained dominated by indexed updates: direct 25k indexed update - 4.390s, direct 25k text indexed update 8.024s, SQLx server 25k indexed update - 4.350s, and SQLx server 25k text indexed update 8.057s; -- follow-up parity work found that the WASIX host was starting single-user - Postgres with `shared_buffers=400kB`, while `@electric-sql/pglite@0.4.5` - reports `shared_buffers=128MB`. The fix moved the intended buffer GUCs into - the Rust startup arguments (`shared_buffers=128MB`, `wal_buffers=4MB`, - `min_wal_size=80MB`). The exact PGlite speed-source rerun now records local - all-suite direct timings around 570ms for Test 9, 732ms for Test 10, 106ms for - Test 11, and 86ms for Test 14; SQLx server timings were about 593ms, 726ms, - 102ms, and 83ms for the same tests. - `perf diagnose-buffer-cache` verifies zero Postgres shared read blocks for the - table-copy hotspots after setup, matching PGlite's effective buffer behavior; -- `xtask assets check` now guards production WASIX inputs for mandatory - WebAssembly exception and dynamic-linking flags and rejects Asyncify markers - in production configure scripts; -- production profile scripts reject Asyncify flag injection by default; the - explicit `PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1` override is reserved for - local snapshot/journaling experiments; -- final package sizes stayed under crates.io's 10 MB compressed limit: - `pglite-oxide` about 7.15 MB, `pglite-oxide-assets` about 4.87 MB, and - `pglite-oxide-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 - `cargo run -p xtask -- package-size --limit 10000000` passed against the - regenerated artifacts. - -## CI/CD And Release Workflow - -- validation now uses a DRY `scripts/validate.sh` entrypoint with explicit - modes for repository hygiene, linting, tests, examples, package checks, and - release dry-runs; -- CI classifies changed paths through `scripts/ci-scope.sh` so docs-only, - CI-only, test-only, package-affecting, and asset-affecting PRs can run the - right checks without forcing every maintainer change through release work; -- release intent checks now focus on published package surfaces - (`Cargo.toml`, `build.rs`, `src/**`, and `crates/**`) instead of forcing - docs, tests, examples, xtask-only, or source-build-script maintenance to use - release-producing PR titles; -- the manual Release workflow keeps the three maintainer operations: - `prepare-release-pr`, `publish-dry-run`, and `publish`, with job-scoped - permissions and Trusted Publishing through `id-token: write`; -- release-plz remains the release owner with one root changelog, one version - group, exact internal dependency versions, internal asset/AOT changes folded - into the root release notes, and bare SemVer tags for the user-facing root - release; -- the Assets workflow now uses production build inputs under - `assets/wasix-build`, the `release-o3` profile, one Linux/Docker portable - WASIX build job, and native AOT matrix jobs for macOS, Linux, and Windows; -- the portable WASIX build in the Assets workflow is now the artifact producer: - it builds generated runtime assets under `target/pglite-oxide/assets`, uploads - them with provenance, and feeds native AOT matrix jobs; -- normal CI now has a Rust-only native AOT runtime matrix that downloads the - latest compatible Assets workflow bundle, verifies the asset-input - fingerprint, installs generated artifacts into ignored paths, and runs the - runtime test suite on macOS arm/x64, Linux arm/x64, and Windows x64; -- asset and AOT crates are source-only in git; release jobs download generated - portable and AOT workflow artifacts for the exact SHA, stage them into crate - skeletons, package-check that generated workspace, and publish with - release-plz dirty-publish support; -- dependency invariant checks now block Wasmtime/static-WASI regressions and - backend compiler crates such as LLVM/Cranelift/Singlepass from entering the - normal user dependency tree; -- the public dependency graph now uses Cargo target-specific dependencies for - AOT packs, so a normal `pglite-oxide` install resolves the target-independent - `pglite-oxide-assets` crate plus only the current platform's - `pglite-oxide-aot-*` crate; -- source-only `scripts/validate.sh test` no longer pretends runtime coverage - happened when AOT artifacts are absent. `scripts/validate.sh runtime` is now - the hard runtime gate and requires portable assets plus the host AOT pack; -- `.github/scripts/download-aot-artifacts.sh` is a thin wrapper over - `xtask assets download`; exact-SHA, latest-compatible, host-target, and - all-target artifact downloads share one implementation; -- AOT serialization is now owned by a maintainer-only `xtask` feature. The - normal runtime tree keeps headless Wasmer loading, while - `xtask --features aot-serializer` is the only path that enables Wasmer LLVM; -- the Assets workflow now probes the LLVM AOT serializer before full AOT - generation, validates generated portable assets before AOT work, smokes the - target runtime before packaging/upload, and fails on empty/missing AOT - manifests instead of uploading placeholder crates; -- `wasmer-wasix` is now explicitly feature-minimized for the runtime path - (`sys-minimal`, `sys-poll`, `host-vnet`, and `time`). The root dependency gate - rejects Wasmtime, backend compiler crates, Cranelift/Singlepass, LLVM, and - broad HTTP/TLS stacks such as `reqwest`, `hyper`, and `rustls`; -- normal CI cache writes are limited to `main` while PRs still restore existing - Rust caches. Release and AOT-heavy jobs opt into cache writes explicitly. diff --git a/docs/EXTENSIONS.md b/docs/EXTENSIONS.md deleted file mode 100644 index d36cd272..00000000 --- a/docs/EXTENSIONS.md +++ /dev/null @@ -1,164 +0,0 @@ -# Extensions - -Bundled SQL extensions are enabled explicitly. The runtime installs only the -extension assets each database asks for. - -The public extension API is available through the default feature set. If you -disable default features, enable `extensions`; it currently implies `bundled` -because extension constants are backed by packaged, smoke-tested extension -payloads. - -## Enable Extensions At Open Time - -The builder path is the easiest option and resolves bundled extension -dependencies before the database opens. - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .extension(extensions::PG_TRGM) - .open()?; - db.close()?; - Ok(()) -} -``` - -You can also add multiple extensions at once: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder() - .temporary() - .extensions([extensions::HSTORE, extensions::LTREE, extensions::UNACCENT]) - .open()?; - db.close()?; - Ok(()) -} -``` - -## Enable Extensions After Open - -Use `enable_extension(...)` when you want to install an extension into an -already-open direct database: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.enable_extension(extensions::VECTOR)?; - db.close()?; - Ok(()) -} -``` - -For dependency-heavy extensions, prefer the builder path. Builder requests are -resolved as a set before open, while `enable_extension(...)` installs the -extension you name into the current root. - -## Preload Extension Artifacts - -Use `preload_extensions(...)` when an extension-backed first query sits on a hot -path: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - Pglite::preload_extensions([extensions::VECTOR, extensions::PG_TRGM])?; - Ok(()) -} -``` - -## Server And CLI Usage - -Extensions work in server mode too: - -```rust,no_run -use pglite_oxide::{extensions, PgliteServer}; - -fn main() -> Result<(), Box> { - let server = PgliteServer::builder() - .temporary() - .extension(extensions::VECTOR) - .start()?; - server.shutdown()?; - Ok(()) -} -``` - -The proxy CLI accepts SQL extension names: - -```sh -pglite-proxy --temporary --extension vector --extension pg_trgm --print-uri -``` - -## Available Bundled Extensions - -Current public constants: - -- `extensions::AGE` -- `extensions::AMCHECK` -- `extensions::AUTO_EXPLAIN` -- `extensions::BLOOM` -- `extensions::BTREE_GIN` -- `extensions::BTREE_GIST` -- `extensions::CITEXT` -- `extensions::CUBE` -- `extensions::DICT_INT` -- `extensions::DICT_XSYN` -- `extensions::EARTHDISTANCE` -- `extensions::FILE_FDW` -- `extensions::FUZZYSTRMATCH` -- `extensions::HSTORE` -- `extensions::INTARRAY` -- `extensions::ISN` -- `extensions::LO` -- `extensions::LTREE` -- `extensions::PAGEINSPECT` -- `extensions::PG_BUFFERCACHE` -- `extensions::PG_FREESPACEMAP` -- `extensions::PG_HASHIDS` -- `extensions::PG_IVM` -- `extensions::PG_SURGERY` -- `extensions::PG_TEXTSEARCH` -- `extensions::PG_TRGM` -- `extensions::PG_UUIDV7` -- `extensions::PG_VISIBILITY` -- `extensions::PG_WALINSPECT` -- `extensions::PGTAP` -- `extensions::SEG` -- `extensions::TABLEFUNC` -- `extensions::TCN` -- `extensions::TSM_SYSTEM_ROWS` -- `extensions::TSM_SYSTEM_TIME` -- `extensions::UNACCENT` -- `extensions::VECTOR` - -`extensions::ALL` is the slice of all currently public bundled extensions. -`extensions::by_sql_name(...)` resolves a bundled extension constant from its SQL -name, for example `"vector"` or `"pg_trgm"`. - -## Not Currently Available - -The generated extension catalog currently tracks additional candidates that are -not part of the bundled public surface: - -- `pgcrypto` -- `uuid-ossp` -- `postgis` - -They are not in `extensions::ALL` and do not have public constants in the -current asset set. - -## Safety And Install Behavior - -Bundled extension archives are installed into the database root before their SQL -setup runs. Archive extraction is path-safe and validated against the packaged -asset manifest. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md deleted file mode 100644 index 84cacd38..00000000 --- a/docs/PERFORMANCE.md +++ /dev/null @@ -1,109 +0,0 @@ -# Performance - -`pglite-oxide` is built to stay close to native Postgres while keeping the -database embedded in the Rust process. - -This page tracks the repo benchmark matrix. The main comparison uses SQLx on -each wire-protocol path: - -- native Postgres with SQLx; -- `pglite-oxide + SQLx`; -- vanilla `@electric-sql/pglite` persisted with NodeFS and reached through - `@electric-sql/pglite-socket`, then measured with SQLx. - -## Snapshot - -Snapshot run: `20260507T113000Z` - -Environment: - -- OS: `macOS 26.4.1 (Darwin 25.4.0 arm64)` -- CPU: `Apple M1 Pro` -- RAM: `16 GB` -- Logical cores: `10` -- Node: `v24.13.0` -- Node packages: `@electric-sql/pglite@0.4.5`, - `@electric-sql/pglite-socket@0.1.5` -- Native Postgres: `18.3 (Homebrew)` -- Oxide Wasmer: `7.2.0-alpha.2` -- Oxide Wasmer WASIX: `0.702.0-alpha.2` -- RTT iterations: `100` -- Speed source: exact upstream SQL from - `assets/checkouts/pglite/packages/benchmark/src` - -Every mode was run serially. - -## Representative Operations - -Lower is better. - -| Operation | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | -|---|---:|---:|---:| -| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | -| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | -| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | -| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | -| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | - -## Full Operation Table - -| ID | Test | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + SQLx | -|---|---|---:|---:|---:| -| 1 | Test 1: 1000 INSERTs | 9.13 ms | 19.76 ms | 15.66 ms | -| 2 | Test 2: 25000 INSERTs in a transaction | 132.36 ms | 149.54 ms | 257.02 ms | -| 2.1 | Test 2.1: 25000 INSERTs in single statement | 46.14 ms | 59.39 ms | 117.19 ms | -| 3 | Test 3: 25000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | -| 3.1 | Test 3.1: 25000 INSERTs into an indexed table in single statement | 66.41 ms | 95.12 ms | 93.88 ms | -| 4 | Test 4: 100 SELECTs without an index | 107.63 ms | 162.89 ms | 242.03 ms | -| 5 | Test 5: 100 SELECTs on a string comparison | 305.38 ms | 338.01 ms | 434.63 ms | -| 6 | Test 6: Creating indexes | 9.94 ms | 13.08 ms | 17.12 ms | -| 7 | Test 7: 5000 SELECTs with an index | 81.39 ms | 125.31 ms | 203.05 ms | -| 8 | Test 8: 1000 UPDATEs without an index | 47.91 ms | 74.42 ms | 103.66 ms | -| 9 | Test 9: 25000 UPDATEs with an index | 351.05 ms | 578.96 ms | 720.63 ms | -| 10 | Test 10: 25000 text UPDATEs with an index | 471.74 ms | 712.38 ms | 858.95 ms | -| 11 | Test 11: INSERTs from a SELECT | 65.64 ms | 97.43 ms | 112.87 ms | -| 12 | Test 12: DELETE without an index | 7.54 ms | 9.74 ms | 11.69 ms | -| 13 | Test 13: DELETE with an index | 9.31 ms | 26.58 ms | 27.7 ms | -| 14 | Test 14: A big INSERT after a big DELETE | 53 ms | 71.6 ms | 87.72 ms | -| 15 | Test 15: A big DELETE followed by 12000 small INSERTs | 58.98 ms | 74.49 ms | 112.18 ms | -| 16 | Test 16: DROP TABLE | 3.43 ms | 10.17 ms | 6.74 ms | - -## Reproduce - -Run the serial matrix: - -```sh -scripts/perf/run_bench_matrix.sh -``` - -That command runs: - -1. `pglite-oxide + SQLx` RTT + speed benchmarks -2. native Postgres + SQLx RTT + speed benchmarks -3. vanilla PGlite + SQLx RTT + speed benchmarks -4. a markdown comparison report - -Outputs land under `target/perf/`: - -- `bench-oxide-.json` -- `bench-native-postgres-sqlx-.json` -- `bench-pglite-nodefs-sqlx-.json` -- `bench-pglite-nodefs-sqlx-ready-.json` -- `bench-comparison-.md` - -Override the native Postgres binaries when needed: - -```sh -PGLITE_OXIDE_NATIVE_POSTGRES=/path/to/postgres \ -PGLITE_OXIDE_NATIVE_INITDB=/path/to/initdb \ -scripts/perf/run_bench_matrix.sh -``` - -## Reading The Matrix - -- `pglite-oxide + SQLx` is the product-style path for apps that connect through - standard Postgres clients. -- `vanilla PGlite + SQLx` keeps upstream PGlite on NodeFS, but uses the same Rust - SQLx client path as the other wire-protocol rows. -- These are machine-local numbers. Re-run the matrix before quoting them in a - release note or public comparison. diff --git a/docs/PERFORMANCE_INTERNAL.md b/docs/PERFORMANCE_INTERNAL.md deleted file mode 100644 index 705b8742..00000000 --- a/docs/PERFORMANCE_INTERNAL.md +++ /dev/null @@ -1,309 +0,0 @@ -# Performance Internals - -This page is maintainer documentation for performance tuning, measurement -harnesses, and release profiling. Public benchmark results now live in -`docs/PERFORMANCE.md`. - -`pglite-oxide` is optimized for test setup and local-app startup. The runtime -avoids user-side compilation: supported targets load packaged Wasmer AOT -artifacts and reuse cached runtime files. - -## Fast Startup Practices - -For test suites: - -- use `Pglite::temporary()` or `PgliteServer::temporary_tcp()`; -- reuse the process when possible so the template and module caches stay warm; -- keep Postgres client pools at one connection; -- call `Pglite::preload()` once before a visible UI path or a large test group; -- call `Pglite::preload_extensions([...])` when extension setup is on the hot - path. - -Example: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - Pglite::preload_extensions([extensions::VECTOR])?; - - let mut db = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - - db.exec("CREATE TABLE items (embedding vector(3))", None)?; - Ok(()) -} -``` - -## Cache Layers - -The runtime uses several cache layers: - -- a process cache for loaded modules; -- a persistent AOT artifact cache; -- a runtime asset cache for immutable files; -- an extension asset cache; -- a template PGDATA cache for roots that use template initialization; -- an eager PGDATA template overlay that avoids cloning the whole initialized - template before first query. - -The older full-local path hardlinks immutable files into database roots when -the filesystem supports it, then falls back to copying when linking is -unavailable. The default path avoids that per-root immutable-file population. - -## Default Filesystem Fast Path - -By default, database roots use Wasmer filesystem composition. Fresh roots use a -pure mount-composition layout: immutable runtime files are served from the -shared cached lower runtime, while the per-root upper layer contains only -mutable state, device/tmp files, and the extension assets explicitly requested -for that root. The same prepared layout is used by direct databases, persistent -paths, app-id paths, fresh and cached temporary databases, proxy roots, and -local server mode. - -The eager PGDATA template overlay is also enabled by default. It mounts the -cached initialized template as the lower `/base` filesystem and starts each -database with a tiny per-instance upper directory. When PostgreSQL opens a -template-backed file for mutation, the runtime copies that one file into the -upper directory before opening it. - -This is intentionally not a pre-provisioned pool: each database root is still -created on demand and owns its mutable files. In local release runs, runtime -composition is now about 0.6-0.9ms, down from roughly 7ms for the previous -per-root asset population. PGDATA setup is under 1ms. The remaining direct -first-query costs are mostly PostgreSQL backend startup, Wasmer instance -creation, and the protocol roundtrip for the query itself. - -Direct `Pglite::open` no longer runs a separate session-setup query. Direct -session defaults are applied during startup before connection data is sent, not -through SQL. The regenerated WASIX runtime owns this as a required -`pgl_apply_default_gucs` bridge helper. - -Direct `Pglite` no longer forces a host directory `sync_all` after every -non-transaction query. PostgreSQL's own WAL/fsync behavior owns durability; the -extra Rust-side directory sync was expensive and weaker than file-level database -fsyncs. This matches the server path, which did not pay that cost. - -Direct `Pglite` also no longer scans `pg_type` on scalar open/query paths. -Built-in PostgreSQL array OIDs are registered statically in the Rust direct -client. Runtime-created enum/domain/composite arrays are discovered lazily when -they appear in direct API parameters or result metadata, or explicitly through -`Pglite::refresh_array_types()`. - -The WASIX startup arguments explicitly preserve PGlite's effective buffer -profile: `shared_buffers=128MB`, `wal_buffers=4MB`, and `min_wal_size=80MB`. -This matters for PGlite benchmark parity. Without those GUCs, single-user -startup fell back to a tiny `shared_buffers=400kB`, causing table-copy and -indexed-update workloads to reread relation pages from the host filesystem. - -Detailed C-side backend startup timers are an instrumented-build diagnostic, not -production runtime surface. Build WASIX assets with -`PGLITE_OXIDE_WASIX_BACKEND_TIMING=1` when investigating `shared_memory`, -`InitPostgres`, or relcache work. Production WASIX artifacts leave that flag off, -so timing macros compile away and the `pgl_backend_timing_elapsed_us` export is -absent. - -Nested child mounts inside a supplied `WasiFsRoot` were tested first. They were -rejected because Wasmer's `WasiRunner::prepare_webc_env` rebuilds the final -mount tree from the supplied `/` filesystem plus runner-owned mounts, so child -mounts must be passed as runner mounts or represented inside the root -filesystem itself. - -## Release Asset Profile - -The default asset release profile is `release-o3`: WASIX C modules are compiled -with `-O3 -g0 -flto=thin`, linked with `-flto=thin`, then Binaryen runs with the -wasixcc default optimization level plus `--converge`, `--strip-debug`, and -`--strip-producers`. This is the current SQL-workload profile: local PGlite -benchmark parity runs showed broad speed wins over non-LTO O3 and `release-os`, -and package-size checks still stayed comfortably under crates.io limits. - -Available profile knobs: - -- `PGLITE_OXIDE_BUILD_PROFILE=release-o3` is the default release asset profile; -- `release`, `release-o3`, `release-os`, and `release-oz` remain available for - comparison builds. `release-o3` is the performance profile and includes - ThinLTO by default; -- set `PGLITE_OXIDE_WASM_OPT_FLAGS=none` to disable the release-profile - Binaryen converge/strip extras for local build iteration; -- set `PGLITE_OXIDE_WASM_OPT_FLAGS=''` to override the - release-profile Binaryen extras. - -The WASIX toolchain already enables the relevant Wasm feature baseline for this -EH+PIC sysroot, including SIMD, relaxed SIMD, and extended const. Adding an -extra `-msimd128` did not change the generated AOT artifact sizes in the local -release experiment, so it is not carried as a project-specific flag. - -Wasmer LLVM AOT is generated with the selected mainline codegen profile: -nonvolatile memory operations and a readonly funcref table. Local exact PGlite -speed-suite measurements showed nonvolatile memory operations improving the -server SQLx suite by about 9% geomean. Adding the readonly funcref table on top -was about 1.4% faster geomean than nonvolatile-only and improved the indexed -update cases (`557.152ms -> 534.737ms` and `695.663ms -> 681.778ms`), while -regressing CREATE INDEX and DROP TABLE cases. Wasmer documents nonvolatile -memory operations as faster but not fully WebAssembly-spec compliant; this is a -conscious mainline runtime-profile decision for the packaged single-process -Postgres runtime and must stay covered by the correctness matrix. - -WebAssembly exceptions are mandatory for production artifacts. The Postgres -runtime depends on exception/longjmp recovery across the main module and side -modules, so there is no supported non-EH fallback and no opt-out flag. Asyncify -is not part of production builds; it may only be used in an isolated -snapshot/journaling experiment if a specific restore design proves it needs -that control-flow model. The build scripts reject Asyncify flags by default; -`PGLITE_OXIDE_ALLOW_ASYNCIFY_EXPERIMENT=1` is reserved for local experiment -branches only. - -WASIX dynamic linking is also mandatory. The main module is built as a -dynamic-main module, extension/tool modules are PIC side modules, and all -runtime, extension, and `pg_dump` artifacts must come from the same configured -source tree. - -## Native Deserialization - -The runtime loads Wasmer AOT artifacts through Wasmer's native mmapped-file -deserializer. This keeps the startup path off the old read-the-whole-native- -artifact path and does not reintroduce full artifact hashing. There is no -runtime opt-out for the older file deserializer. - -## Strict Verification - -By default, startup avoids content-hashing bundled assets. Cached Wasmer AOT -artifacts use fast receipt verification: the runtime checks the cache receipt -and file metadata, then lets Wasmer deserialize the cached native artifact. If -deserialization fails, the cache entry is deleted, rebuilt once from the bundled -artifact, and retried. - -Set `PGLITE_OXIDE_AOT_VERIFY=full` to force full SHA-256 verification of cached -AOT files, bundled runtime archives, bundled extension archives, PGDATA template -archives, and runtime/template module matches. This is useful for debugging -cache corruption or CI integrity checks, but it adds cold-start latency and is -not the default. - -## Snapshot And Journal Work - -Wasmer 7.2 exposes WASIX journal/process snapshot APIs, and `StoreSnapshot` -captures store globals. That is not enough by itself to ship an instant restore -path for Postgres: a promoted design must prove correctness for PGDATA state, -mount state, file descriptors, direct protocol state, server mode, extensions, -and `pg_dump`. This remains a first-class performance track, but it must beat -the current template/overlay path while passing the same runtime and extension -suite before it becomes default. - -## Measuring Locally - -The smoke benchmark prints preload and open timings: - -```sh -cargo test --test performance_smoke -- --nocapture -``` - -To measure the current cold-start path: - -```sh -cargo run -p xtask -- perf cold -``` - -This runs operations sequentially in one process. Each operation reports -`cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and -`workload`, so a "first query" is explicitly the first query for that -operation's newly opened root/server, not necessarily a cold cache or cold -process. Each operation also reports `primaryLatencyPhase` and -`primaryLatencyMicros`; this is the user-visible latency target for that -operation and excludes cleanup/teardown where appropriate. - -To include first-install cache bootstrap costs in the first measured preload: - -```sh -cargo run -p xtask -- perf cold --reset-cache -``` - -To measure true warm behavior after startup, use the warm harness: - -```sh -cargo run -p xtask -- perf warm -``` - -It keeps databases/servers alive and measures repeated direct queries, -transactions, SQLx/tokio-postgres queries, repeated SQLx connections, and -extension-backed queries separately from open and shutdown phases. Use -`--iterations N` and `--connections N` for shorter local probes. - -To run product-style SQL benchmarks similar to PGlite's published benchmark -families: - -```sh -cargo run --release -p xtask -- perf bench -``` - -This emits JSON with two benchmark suites: - -- `rtt`: PGlite-style CRUD round-trip microbenchmarks. Each query runs many - times, the lowest and highest 10% are discarded when enough samples exist, - and the trimmed average is reported. -- `speed`: a generated SQLite speedtest-style SQL suite with large insert, - select, update, index, delete, and drop workloads. - -The RTT suite can run through the direct Rust API, through `PgliteServer` with a -single long-lived SQLx connection, and through `PgliteServer` with a raw -`tokio-postgres` simple-query-protocol connection. The raw `tokio-postgres` -mode is there to separate proxy/wire overhead from SQLx client overhead: - -```sh -cargo run --release -p xtask -- perf bench --suite rtt --mode server-sqlx -cargo run --release -p xtask -- perf bench --suite rtt --mode server-tokio-postgres-simple -cargo run --release -p xtask -- perf bench --suite speed --mode direct --scale 0.05 -cargo run --release -p xtask -- perf bench --suite speed --speed-source pglite -``` - -The speed suite is generated locally instead of vendoring PGlite's generated -multi-megabyte SQL files. Use `--scale` for quick local probes and `--scale 1` -for the full default shape. Use `--speed-source pglite` when you need exact -parity with the SQL files checked out under -`assets/checkouts/pglite/packages/benchmark/src`; this mode requires -`--scale 1`. - -To compare simple-query indexed updates against parameterized prepared updates -and client pipelining: - -```sh -cargo run --release -p xtask -- perf prepared-updates -cargo run --release -p xtask -- perf prepared-updates --skip-native -cargo run --release -p xtask -- perf prepared-updates --skip-native --gate -``` - -This parses the exact update values from PGlite benchmark Tests 9 and 10, uses -the same indexed-table setup, and measures SQLx sequential prepared execution, -tokio-postgres sequential prepared execution, tokio-postgres pipelined prepared -execution over TCP and Unix sockets, and the same tokio-postgres modes against -native Postgres. Use `--skip-native` when local native Postgres IPC state is not -healthy or when only PgliteServer modes are needed. This is a server/protocol -benchmark; it does not replace the exact PGlite simple-query suite. - -`--gate` is a local regression smoke gate, not a final CI performance oracle. -It checks the transport shape that caused the COPY/prepared-update regression: -non-COPY prepared traffic must not activate the backend-owned streaming -continuation, pipelined prepared traffic must remain batched, SQLx and -sequential tokio-postgres must stay below 5s per 25k updates, and pipelined -tokio-postgres must stay below 1.5s per 25k updates. The command emits per-run -protocol counters so failures show whether the problem is batching, protocol -pump activation, or backend execution. - -For focused investigation of indexed update hotspots, run: - -```sh -cargo run --release -p xtask -- perf diagnose-indexed-update -cargo run --release -p xtask -- perf diagnose-buffer-cache -``` - -This opens fresh temporary databases, runs setup outside the measured section, -then compares exact PGlite Test 9/10 SQL against controlled variants: lookup -index only, unlogged table, text update after numeric update, vacuumed variants, -and one set-based update. The buffer-cache diagnostic runs -`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` for the remaining table-copy hotspots -and reports the effective Postgres memory GUCs plus host filesystem trace data. - -Treat these numbers as machine-local diagnostics. CI performance gates and -release targets depend on the runner, host filesystem, and cache state. diff --git a/docs/PG_DUMP.md b/docs/PG_DUMP.md deleted file mode 100644 index 2e90660b..00000000 --- a/docs/PG_DUMP.md +++ /dev/null @@ -1,155 +0,0 @@ -# Dump, Restore, And Upgrade - -`pglite-oxide` ships a bundled WASIX `pg_dump` path behind the `extensions` -feature, which is enabled by default. Use it for portable SQL exports, -restores, and version-to-version upgrades. - -## Choose The Right Export Format - -Use logical dumps when you need: - -- a portable SQL export; -- an upgrade path between `pglite-oxide` releases; -- a way to move data between different roots safely. - -Use physical data-dir archives when you need: - -- a same-version clone; -- a same-runtime restore into another `pglite-oxide` root; -- a fast local snapshot of the current cluster state. - -Physical archives are not a cross-version upgrade path. - -## Direct API - -Dump an already-open `Pglite` database to SQL: - -```rust,no_run -use pglite_oxide::{PgDumpOptions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE items(value TEXT)", None)?; - db.exec("INSERT INTO items VALUES ('alpha')", None)?; - - let sql = db.dump_sql(PgDumpOptions::new())?; - assert!(sql.contains("INSERT INTO")); - - db.close()?; - Ok(()) -} -``` - -Get UTF-8 bytes instead: - -```rust,no_run -use pglite_oxide::{PgDumpOptions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - let bytes = db.dump_bytes(PgDumpOptions::new())?; - assert!(!bytes.is_empty()); - db.close()?; - Ok(()) -} -``` - -Direct dumps run against the already-open embedded backend. If you need to dump -as a different user or from a different database, start a `PgliteServer` and -use the server dump path instead. - -## Server API - -Dump through a local Postgres endpoint when another part of your workflow -already uses `PgliteServer`: - -```rust,no_run -use pglite_oxide::{PgDumpOptions, PgliteServer}; - -fn main() -> Result<(), Box> { - let server = PgliteServer::temporary_tcp()?; - let sql = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; - assert!(!sql.is_empty()); - server.shutdown()?; - Ok(()) -} -``` - -`PgliteServer::dump_sql(...)` currently requires a TCP endpoint. - -## `PgDumpOptions` - -`PgDumpOptions` controls the managed parts of the dump command: - -```rust,no_run -use pglite_oxide::PgDumpOptions; - -let options = PgDumpOptions::new() - .username("postgres") - .database("template1") - .args(["--schema-only", "--quote-all-identifiers"]); -``` - -Useful passthrough flags include dump-shaping options such as: - -- `--schema-only` -- `--quote-all-identifiers` -- `-n ` -- `-t ` - -Managed connection and output flags are reserved by the API. Do not pass -`--file`, `--format`, `--host`, `--port`, `--username`, `--dbname`, or `--jobs` -through `arg(...)` or `args(...)`. - -## CLI - -Dump a persistent root: - -```sh -pglite-dump --root ./.pglite -``` - -Pass through normal `pg_dump` shaping flags after `--`: - -```sh -pglite-dump --root ./.pglite -- --schema-only -pglite-dump --root ./.pglite -- --quote-all-identifiers -``` - -## Restore - -Restore a logical dump by executing the SQL against a new database: - -```rust,no_run -use pglite_oxide::{PgDumpOptions, Pglite}; - -fn main() -> Result<(), Box> { - let mut source = Pglite::temporary()?; - source.exec("CREATE TABLE items(value TEXT)", None)?; - source.exec("INSERT INTO items VALUES ('alpha')", None)?; - let dump_sql = source.dump_sql(PgDumpOptions::new())?; - - let mut restored = Pglite::temporary()?; - restored.exec(&dump_sql, None)?; - - source.close()?; - restored.close()?; - Ok(()) -} -``` - -For same-version root copies, prefer `dump_data_dir()` / -`load_data_dir_archive(...)` or `try_clone()`. - -## Upgrade Guidance - -Use logical dump and restore when upgrading between `pglite-oxide` versions or -changing packaged runtime assets: - -1. Open the old database with the old crate/runtime. -2. Create a logical dump with `dump_sql(...)` or `pglite-dump`. -3. Open a fresh database with the new crate/runtime. -4. Execute the dump SQL into the new database. - -Do not treat physical data-dir archives as a general upgrade mechanism. They are -for the same runtime family and database format. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..6c10ccc8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# Repository Docs + +This directory is for maintainer, architecture, and internal documentation. +Public product documentation lives in `src/docs`; SDK pages are +centralized under `src/docs/content/sdk`. Product roots keep only +package README/CHANGELOG files and source-adjacent API comments. + +- `architecture/`: design rationale and product-boundary decisions. The status + banner in each investigation determines whether it is current or historical. +- `maintainers/`: repository, tooling, release, testing, and benchmark process. +- `internal/`: archived progress notes, audits, and implementation history. + These files are non-normative; start with `maintainers/README.md`. diff --git a/docs/RELEASE.md b/docs/RELEASE.md deleted file mode 100644 index 66d3b15d..00000000 --- a/docs/RELEASE.md +++ /dev/null @@ -1,166 +0,0 @@ -# Release Process (Maintainers) - -This page is maintainer documentation for versioning, release CI, and crates.io -publishing. It is not part of the end-user documentation path. - -Release automation is workspace-aware. `release-plz` owns version bumps for the -root crate, `pglite-oxide-assets`, and every `pglite-oxide-aot-*` crate. -Feature PRs should not edit package versions directly. - -The root crate is the only user-facing release and changelog. Asset and AOT -crate changes are included in the root `CHANGELOG.md`, while those internal -crates do not create separate GitHub releases or tags. The public Git tag stays -the bare SemVer version, for example `0.4.0`, because the internal crates are -implementation details. - -Before publishing, CI packages every published crate and enforces crates.io's -10 MB compressed `.crate` limit. - -Releases use source-controlled inputs plus CI-generated portable WASIX and AOT -artifacts. The git repo intentionally does not commit portable runtime blobs or -native AOT binaries. The release workflow first verifies source pins, asset -input fingerprints, extension metadata, and crate templates; then it requires a -successful `Assets` workflow for the same SHA, downloads the generated portable -and AOT artifacts, stages them into a clean release workspace, package-checks -that workspace, and only then runs release-plz. - -The release source of truth is the exact Assets workflow output for the release -SHA. Portable WASIX artifacts are built from pinned sources, native AOT -artifacts are generated from that portable asset set, and release validation -checks package contents, target triples, Wasmer versions, runtime hashes, and -crate sizes before publishing. The release workflow runs source/lint/example -checks before artifact download, then reruns the Rust test gate after artifact -installation so the release host executes against materialized native artifacts -instead of compile-only tests. - -Normal CI and release CI split responsibilities deliberately. Rust-only changes -download the latest compatible Assets workflow bundle, verify its asset-input -fingerprint, and run native AOT runtime tests on the supported host matrix. -Asset-producing changes run the `Assets` workflow, which rebuilds portable -WASIX from pinned sources before generating and smoking the target AOT packs. -The release workflow refuses to publish unless the generated portable and AOT -artifacts for the exact release SHA are downloaded, staged, and package-checked. - -`pglite-oxide` publishes source crates to crates.io with release-plz. The CLI -binaries in this repository are maintenance helpers, so the release path -deliberately avoids binary artifact tooling such as cargo-dist until there is a -user-facing binary to distribute. - -## One-time setup - -- Ensure the crate owner has crates.io publish rights for `pglite-oxide`, - `pglite-oxide-assets`, and every `pglite-oxide-aot-*` crate. -- Configure crates.io Trusted Publishing for every published crate. Use - repository `f0rr0/pglite-oxide`, workflow `.github/workflows/release.yml`, - and environment `crates-io`. -- Do not configure `CARGO_REGISTRY_TOKEN`; the release workflow relies on the - GitHub OIDC token granted by `id-token: write`. -- Repository Actions settings must allow GitHub Actions to create pull requests. -- The `Release` workflow uses job-scoped permissions. The release-PR job needs - `contents: write` and `pull-requests: write`; the publish job needs - `contents: write` and `id-token: write`. -- If release PRs should run normal PR CI automatically, configure a - `RELEASE_PLZ_TOKEN` secret backed by a GitHub App or maintainer bot token. - Without it, release-plz falls back to `GITHUB_TOKEN`; GitHub does not trigger - normal PR workflows from PRs opened by that token. -- Do not set `package.publish = ["crates-io"]`; crates.io is Cargo's default - registry, and release-plz treats `package.publish` entries as named alternate - registries. - -## Release intent - -release-plz uses Conventional Commits as the release changeset. PRs that touch -release-affecting package files must use one of these PR title types: - -- `feat:` for user-facing additions -- `fix:` for behavior fixes -- `perf:` for performance improvements -- `refactor:` for behavior-preserving package changes that still need a release -- `revert:` for reverted release-affecting changes -- any type with `!` for breaking changes - -Docs, CI, issue-template, tests, examples, xtask-only maintenance, source -checkout scripts, and other repository-only changes may use non-release types -such as `docs:`, `ci:`, `chore:`, `style:`, or `test:`. The CI release intent -check treats these paths as release-affecting: `Cargo.toml`, `build.rs`, -`src/**`, and `crates/**`. - -Package version bumps are release-plz owned. Feature and fix PRs may change -package code, dependencies, and generated assets, but they must not change -workspace package versions. The version bump and matching `CHANGELOG.md` -section must come from a `release-plz-*` PR titled `chore(release): ...`. - -## Maintainer paths - -- Docs-only and repository-only PRs run the lightweight repository hygiene and - workflow checks. They do not need a release title or changelog entry. -- Test-only PRs run Rust checks but do not need a release title unless they also - change published package code. -- Runtime, API, generated asset, and AOT crate changes are release-affecting. - Use a release-producing PR title such as `fix:`, `feat:`, `perf:`, or - `refactor:`. -- Source-spine and asset-build script changes are not automatically - release-affecting until they change generated package contents under - `src/**` or `crates/**`, but CI treats them as asset-producing changes and - requires committed artifact verification plus the `Assets` workflow when they - affect release artifacts. - -## Releasing from main - -1. Merge release-worthy work to `main`. -2. Open GitHub Actions, run `Release` from `main`, and choose - `prepare-release-pr`. -3. Review and merge the release-plz PR. It updates `Cargo.toml`, `Cargo.lock`, - and `CHANGELOG.md`. -4. Wait for the `Assets` workflow on `main` to pass for the release commit. -5. Run `Release` from `main` with `publish-dry-run`. -6. If the dry run passes, run `Release` again with `publish`. - -For portable asset-source changes, regenerate and verify the generated artifact -set before merging: - -```sh -cargo run -p xtask -- assets fetch -cargo run -p xtask --features aot-serializer -- assets build-host -cargo run -p xtask -- assets verify-committed -``` - -Portable WASIX and native AOT artifacts are not committed. They are produced by -the `Assets` workflow matrix and downloaded by -`.github/scripts/download-aot-artifacts.sh` during dry-run and publish jobs. The -architecture-independent PGDATA template is also generated by that workflow -from the split WASIX `initdb` module and is not checked in. -`xtask release stage` materializes those generated payloads into crate skeletons -inside `target/pglite-oxide/release/workspace`; packaging and publish dry-runs -run from that staged workspace. The real `release-plz` publish step is also -pointed at the staged workspace manifest so the published asset and AOT crates -contain those generated payloads. - -After `release-plz` creates the root GitHub release, the publish job also -packages the same generated portable WASIX and target AOT payloads with -`xtask release package-assets` and uploads them as public GitHub release assets. -Those public `.tar.zst` bundles are mirrors of the validated generated inputs; -the crates.io packages remain the distribution path used by normal consumers. - -The manual publish job uses `release_always = true` because the workflow is not -triggered on every merge; it only runs when a maintainer explicitly selects a -publish operation. The job fails if release-plz reports that it created no -release, so a green publish run means a crate/GitHub release was actually -produced. The dry-run operation stops after staged package validation because -same-release internal crates are not present in crates.io until the real -release-plz publish step. - -The publish job also validates release-note readiness before running expensive -package checks. The current root package version must be the first release -section in `CHANGELOG.md`, that section must contain release-note body content, -and the `[Unreleased]` compare link must start at that version. If this check -fails, run `prepare-release-pr` and merge the generated release-plz PR before -publishing. - -release-plz publishes unpublished package versions to crates.io, creates the -bare SemVer tag such as `0.4.0`, and creates the GitHub release from the -generated changelog. The root crate depends on internal crates with exact -versions. Plain Cargo and release-plz dry-runs cannot fully dry-run the root -crate before those exact internal versions exist in the registry, so validation -dry-runs every internal crate, enforces package sizes, attempts the root checks, -and leaves final workspace publish ordering to the real release-plz publish. diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md deleted file mode 100644 index fa1b499f..00000000 --- a/docs/RUNTIME.md +++ /dev/null @@ -1,109 +0,0 @@ -# Runtime Guide - -`pglite-oxide` embeds a PostgreSQL-compatible runtime in the current Rust -process. The direct API talks to that backend directly, and `PgliteServer` -exposes the same backend through a local Postgres connection string. - -## Choose A Mode - -Use `Pglite` when your Rust code owns the database calls: - -- direct function and method calls; -- no socket listener; -- best fit for tests, commands, jobs, and Tauri state. - -Use `PgliteServer` when a library expects a PostgreSQL URI: - -- SQLx, Diesel, SeaORM, `tokio-postgres`, or cross-language clients; -- local TCP or Unix socket listener; -- compatibility layer for existing Postgres clients. - -Both modes still use one embedded backend. - -## Persistence Modes - -Direct and server builders expose the same root choices: - -- `path(...)` for a persistent database under an explicit directory; -- `app(...)` or `app_id(...)` for a persistent database under app data; -- `temporary()` for a fast cached temporary database; -- `fresh_temporary()` for an explicit fresh-cluster path. - -Choose `temporary()` for most tests. Choose `fresh_temporary()` only when you -need a brand-new cluster and are willing to pay its slower startup path. - -## Operational Limits - -The current runtime model is single-backend: - -- one `Pglite` instance owns one embedded backend; -- one `PgliteServer` exposes one embedded backend; -- downstream client pools should use one connection; -- server mode is for local compatibility, not a multi-user Postgres replacement. - -Generated server URLs include `sslmode=disable`. `CancelRequest` and normal -startup packets are supported, but there is still one backend behind the server. - -## Root Locking And Lifecycle - -Persistent roots are locked while open. A second direct or server open against -the same root fails instead of sharing one data directory unsafely. - -Close database clients before calling `PgliteServer::shutdown()`. The current -server thread waits for active client work to finish before exiting. - -If you need a same-version physical clone, use `dump_data_dir()` / -`load_data_dir_archive(...)` or `try_clone()`. For portable exports and -upgrades, use logical dumps through `pg_dump`. - -## Startup And Preload - -The crate exposes two preload hooks: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -fn main() -> Result<(), Box> { - Pglite::preload()?; - Pglite::preload_extensions([extensions::VECTOR])?; - Ok(()) -} -``` - -Call them before a visible startup path when you want to warm the packaged -runtime and bundled extension artifacts. - -Startup configuration belongs on the builders: - -- `postgres_config(...)` for PostgreSQL GUCs; -- `username(...)` and `database(...)` for the session target; -- `relaxed_durability(true)` for cacheable local workloads; -- `startup_arg(...)` only for advanced cases. - -## Supported Targets - -Default builds include packaged runtime assets and host artifacts for: - -- macOS arm64; -- Linux x64; -- Linux arm64; -- Windows x64. - -Unsupported host targets fail with a missing-artifact error instead of trying -to compile PostgreSQL locally. - -Browser, worker, and mobile topics from upstream PGlite docs do not apply to -this crate. `pglite-oxide` is a Rust crate for local embedded and desktop/server -workloads. - -## What Server Mode Is For - -Reach for `PgliteServer` when you need client-library compatibility: - -- SQLx migrations and query APIs; -- ORMs that expect a PostgreSQL URI; -- test fixtures for Python, Go, or Node clients; -- local tools that already speak the Postgres wire protocol. - -Reach for `Pglite` when you control the Rust call site. It avoids the extra -socket layer and keeps the API surface smaller. diff --git a/docs/TAURI.md b/docs/TAURI.md deleted file mode 100644 index 97e6f19f..00000000 --- a/docs/TAURI.md +++ /dev/null @@ -1,80 +0,0 @@ -# Tauri Usage - -Use `pglite-oxide` from Rust state, not from the webview. The crate's main value -in Tauri is a sidecar-free local Postgres runtime that commands, background -tasks, and Rust libraries can share. - -See the -[Tauri SQLx example](https://github.com/f0rr0/pglite-oxide/blob/main/examples/tauri-sqlx-vanilla/README.md) -for a Tauri v2 app that keeps the database in Rust state and exposes a small -SQLx-backed profile command to the frontend. - -## Direct Rust State - -Use `Pglite` when your Tauri commands own the database calls: - -```rust,no_run -use pglite_oxide::Pglite; -use serde_json::json; -use std::sync::Mutex; -use tauri::State; - -struct Db(Mutex); - -#[tauri::command] -fn add_item(db: State<'_, Db>, value: String) -> Result<(), String> { - let mut db = db.0.lock().map_err(|err| err.to_string())?; - db.query( - "INSERT INTO items(value) VALUES ($1)", - &[json!(value)], - None, - ) - .map_err(|err| err.to_string())?; - Ok(()) -} -``` - -Open the database under your app data directory during setup: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder() - .app("com", "example", "desktop-app") - .open()?; - db.close()?; - Ok(()) -} -``` - -## Existing Postgres Clients - -Use `PgliteServer` when another Rust library expects a PostgreSQL URL: - -```rust,no_run -use pglite_oxide::PgliteServer; - -fn main() -> Result<(), Box> { - let server = PgliteServer::builder() - .path("./.pglite") - .start()?; - - let database_url = server.database_url(); - println!("{database_url}"); - - server.shutdown()?; - Ok(()) -} -``` - -This is the right fit for SQLx or other client libraries that already speak the -Postgres wire protocol. - -## Operational Guidance - -- Keep database access serialized around one backend. -- Configure SQLx and other pools with one connection. -- Prefer `Pglite` over `PgliteServer` when you do not need a PostgreSQL URI. -- Use `temporary()` or `temporary_tcp()` for tests. -- Use `fresh_temporary()` only when you need fresh-cluster semantics. diff --git a/docs/TESTING.md b/docs/TESTING.md deleted file mode 100644 index a6364dbb..00000000 --- a/docs/TESTING.md +++ /dev/null @@ -1,136 +0,0 @@ -# Testing With pglite-oxide - -`pglite-oxide` is intended for tests that need real Postgres semantics without -Docker. - -## Direct Rust Tests - -Use `Pglite::temporary()` when the code under test can call the direct Rust API: - -```rust,no_run -use pglite_oxide::Pglite; - -#[test] -fn stores_rows() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - - db.exec("CREATE TABLE items (id int primary key, name text)", None)?; - db.exec("INSERT INTO items VALUES (1, 'alpha')", None)?; - - let rows = db.query("SELECT name FROM items WHERE id = 1", &[], None)?; - assert_eq!(rows.rows[0].get("name").unwrap(), "alpha"); - - db.close()?; - Ok(()) -} -``` - -Use `fresh_temporary()` only when the test must validate fresh-cluster -initialization behavior: - -```rust,no_run -use pglite_oxide::Pglite; - -#[test] -fn fresh_cluster_path() -> Result<(), Box> { - let mut db = Pglite::builder().fresh_temporary().open()?; - db.close()?; - Ok(()) -} -``` - -## Server Tests - -Use `PgliteServer` when the application already talks to Postgres through a -client library: - -```rust,no_run -use pglite_oxide::PgliteServer; -use sqlx::{Connection, Row}; - -#[tokio::test] -async fn sqlx_query() -> Result<(), Box> { - let server = PgliteServer::temporary_tcp()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; - - let row = sqlx::query("SELECT $1::int4 + 1 AS n") - .bind(41_i32) - .fetch_one(&mut conn) - .await?; - assert_eq!(row.try_get::("n")?, 42); - - conn.close().await?; - server.shutdown()?; - Ok(()) -} -``` - -Keep client pools at one connection. - -## Extension Tests - -Enable bundled extensions through the builder: - -```rust,no_run -use pglite_oxide::{extensions, Pglite}; - -#[test] -fn vector_query() -> Result<(), Box> { - let mut db = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - - db.exec("CREATE TABLE items (embedding vector(3))", None)?; - db.exec("INSERT INTO items VALUES ('[1,2,3]')", None)?; - db.exec("SELECT embedding <-> '[1,2,4]' FROM items", None)?; - - db.close()?; - Ok(()) -} -``` - -When an extension has bundled dependencies, prefer the builder path over -post-open `enable_extension(...)`. - -## Snapshot And Fixture Setup - -Use physical data-dir archives or `try_clone()` when a test suite needs a -pre-populated same-version fixture: - -```rust,no_run -use pglite_oxide::Pglite; - -#[test] -fn clone_fixture() -> Result<(), Box> { - let mut seed = Pglite::temporary()?; - seed.exec("CREATE TABLE items(value TEXT)", None)?; - seed.exec("INSERT INTO items VALUES ('alpha')", None)?; - - let mut clone = seed.try_clone()?; - clone.exec("SELECT * FROM items", None)?; - - clone.close()?; - seed.close()?; - Ok(()) -} -``` - -Use logical dumps, not physical archives, when you need a portable export. - -## Cross-Language Tests - -Use `pglite-proxy` when the test process lives outside Rust: - -```sh -pglite-proxy --temporary --tcp 127.0.0.1:0 --print-uri -``` - -Pass the printed URI to Python `psycopg`, Go `pgx`, Node `pg`, or another -standard Postgres client. - -## COPY And Raw Protocol Tests - -Direct `Pglite` supports `/dev/blob` for `COPY TO` and `COPY FROM`. Server mode -supports ordinary client-driven `COPY FROM STDIN` and other standard wire -protocol flows through the local Postgres endpoint. diff --git a/docs/TODO.md b/docs/TODO.md deleted file mode 100644 index e00d9c16..00000000 --- a/docs/TODO.md +++ /dev/null @@ -1,517 +0,0 @@ -# To Do (Maintainers) - -This is the single implementation backlog for `pglite-oxide`. It is -maintainer-facing and intentionally separate from the user-facing docs. - -This file should contain only unfinished architecture, implementation, release, -and research work. - -## Product Target - -`pglite-oxide` should provide embedded Postgres for Rust tests and local apps: - -- no Docker, local LLVM, Cranelift, or Postgres build step for users; -- direct Rust API for embedded use; -- local server mode for SQLx, `tokio-postgres`, Diesel, SeaORM, Python, Go, - Node, and any other Postgres client; -- bundled pgvector and common SQL extensions; -- `pg_dump` support driven by the same packaged runtime. - -The production runtime target is PGlite/Postgres built as WASIX dynamic-linking -modules, precompiled with Wasmer LLVM AOT in CI, then loaded through headless -Wasmer in applications. - -## Release Blockers - -These are the top-level blockers before calling the WASIX/Wasmer path -production ready: - -1. Generate and validate asset/AOT packs across the supported target matrix: - macOS arm64/x64, Linux x64/arm64, and Windows x64. -2. Enforce cold-start and warm-path release gates in CI after collecting - release-mode baselines on GitHub-hosted runners. -3. Finish the remaining extension dependency stacks: pinned WASIX - OpenSSL/libcrypto for `pgcrypto`, pinned WASIX OSSP UUID/libuuid for - `uuid-ossp`, and the pinned PostGIS geospatial stack. -4. Harden public `pg_dump` API/CLI across target platforms and add release-mode - performance gates. -5. Harden CI, release-plz, package-size gates, Trusted Publishing, and - dependency invariants for all internal asset and AOT crates. -6. Validate the split WASIX `initdb` artifact end to end in the Assets workflow: - generated template determinism, fresh direct/server temporary roots, - interrupted-initdb cleanup, and package-size impact. - -## Bucket 1: Performance And Runtime Architecture - -Performance work is product work, not optional research. If a work item affects -both cold and warm behavior, track it under cold. - -### Current State - -- Default runtime uses headless Wasmer and packaged AOT artifacts. -- Runtime assets use pure mount composition; immutable runtime files stay in the - shared cache and per-root upper layers contain mutable state and requested - extension assets. -- PGDATA uses the eager template overlay; local PGDATA setup is under 1ms. -- Direct scalar paths no longer scan `pg_type` for arrays on open/query. -- Direct query paths no longer force Rust-side directory `sync_all`. -- Direct API, server API, proxy CLI, raw protocol, and `pg_dump` share - `BackendSession`. -- Current local release runs show visible first-query paths mostly in the - tens-of-ms range, dominated by PostgreSQL backend startup, Wasmer instance - creation, and first protocol round trips. The latest public benchmark - snapshot lives in [PERFORMANCE.md](PERFORMANCE.md). Maintainer tuning details - live in [PERFORMANCE_INTERNAL.md](PERFORMANCE_INTERNAL.md); historical - rollout notes stay in [DONE.md](DONE.md). - -### Release Gates - -- temporary database first `SELECT 1` under 500ms on GitHub Ubuntu; -- persistent database first `SELECT 1` under 500ms on GitHub Ubuntu; -- `PgliteServer` start plus first SQLx query under 500ms on GitHub Ubuntu; -- temporary database with requested extensions plus first extension-backed - query under 500ms on GitHub Ubuntu; -- public `pg_dump` startup plus first dump row under 500ms on GitHub Ubuntu; -- warm temporary direct first query under 100ms on GitHub Ubuntu; -- warm temporary server plus first SQLx query under 100ms on GitHub Ubuntu; -- warm temporary server plus first extension-backed SQLx query under 125ms on - GitHub Ubuntu; -- no more than 15% regression after stable baselines. - -### Cold Path Work - -- Turn the local `xtask perf cold`, `perf warm`, speed-suite, and prepared - update baselines into CI checks on representative runners. -- Keep the source/runtime invariants in the performance matrix: - `postgres-pglite` `REL_17_5-pglite` at - `01792c31a62b7045eb22e93d7dad022bb64b1184`, Wasmer/WebAssembly exceptions, - WASIX dynamic linking, spinlock-enabled WASIX build, and PGlite buffer - profile (`shared_buffers=128MB`, `wal_buffers=4MB`, `min_wal_size=80MB`). -- Reduce explicit `Pglite::preload()` latency further by profiling runtime - cache setup, mmap/native deserialization, and Wasmer native artifact loading. -- Keep Wasmer native mmap deserialization as the only production AOT loading - path. Do not reintroduce full content hashing on default startup. -- Cross-platform validate the default pure mount composition and eager PGDATA - overlay across direct, persistent, app-id, temporary, proxy, and server roots. -- Validate lazy/generated direct-client array metadata across built-in arrays, - enum arrays, domain arrays, composite arrays, explicit - `refresh_array_types`, transactions, row-mode results, and caller-supplied - parser/serializer overrides. -- Deepen C-side timers inside remaining backend-open costs: - shared-memory initialization, relcache/catcache work, database lookup, - `CheckMyDatabase`, and session initialization. Keep these timers gated out of - production artifacts unless explicitly enabled. -- Keep an explicit regression guard so extension-template opens do not fall - back into slow `StartupXLOG` recovery. -- Extend server perf visibility to proxy CLI runs, TCP, Unix sockets, - tokio-postgres, SQLx, scalar roots, and extension-enabled roots. -- Remove or mount-compose the remaining per-root requested-extension asset - materialization cost. This must stay generic by requested extension set, not - special-cased to vector. -- Add an init-profile dimension to extension-set PGDATA template cache keys - once init options become configurable. -- Evaluate catalog/syscache warmup during template creation: `SELECT 1`, - representative prepared/extended query, extension type I/O/query smoke, - SQLx/tokio-postgres startup flow, and representative extension setup. -- Run temporary durability experiments (`fsync=off`, `synchronous_commit=off`, - reduced WAL work) for temporary roots only. Persistent databases stay - conservative unless separately proven safe. -- Ensure side-module AOT artifact identity is tied to main runtime identity. -- Add Linux perf/perfmap support for symbolized AOT profiling so remaining time - can be attributed to executor, btree, heap, WAL, memory, or Wasmer-generated - code. -- Validate ThinLTO build time, package-size budget, and performance on every - supported CI target. -- Validate the current Wasmer LLVM codegen profile across the full target and - correctness matrix: nonvolatile memory operations plus readonly funcref table. -- Benchmark pgvector insert/query/distance workloads with the default WASIX - toolchain feature baseline, including SIMD/relaxed-SIMD behavior. -- Inspect tail calls, extended const expressions, and wide arithmetic use so - feature usage is consistent across target artifacts. - -### Warm/Steady-State Work - -- Reduce PostgreSQL backend startup without changing semantics, especially - `shared_memory`, `InitPostgres`, `relcache_phase3`, database/session setup, - and PGlite-specific startup work. -- Evaluate safe relcache/catcache/syscache warmup only if it is normal - Postgres-compatible state and cannot cache broken process-global state. -- Keep warm opens free of AOT decompression, full hashing, and asset extraction. -- Test whether any supported Wasmer engine/runtime reuse can reduce instance - creation without leaking Store, WASI env, fd, mount, protocol, or database - state. -- Use `perf warm` for long-lived `PgliteServer` baselines: repeated - connections, repeated SQLx/tokio-postgres prepared queries, transaction - batches, extension-backed queries, reconnect after client disconnect, and - idle-to-next-query latency. -- Keep `perf prepared-updates --skip-native --gate` in the local regression - path while CI baselines settle. -- Extend extended-protocol batching only through protocol-correct reductions in - host/backend crossings and buffer copies. Do not add sleep-based coalescing. -- Add direct warm benchmarks for prepared query reuse and first unknown runtime - array type discovery. -- Add warm public `pg_dump` benchmarks that measure startup separately from - dump volume. -- Evaluate context switching, experimental async APIs, CPU idle/backoff, and - opt-in backend pools only if correctness and state reset semantics are - stronger than user expectations. -- Defer threaded/multi-backend execution until the single-backend path is stable - and atomics/shared memory do not break dynamic linking or Postgres - process-global assumptions. - -### Runtime Experiments - -Experiments are real work. Each must report timing, correctness, state -isolation, artifact size impact, and implementation risk. - -- WASIX journaling, Wasmer `StoreSnapshot`, or InstaBoot-style restore: - re-enter only with a small upstream repro or a fixed journal layer. The last - local spike passed `SELECT 1` from an instance-created restore but did not - skip Postgres startup; backend-ready/protocol-ready snapshots were too slow - and failed fd seek replay. -- Cranelift: evaluate direct `SELECT 1`, SQL error recovery, representative - extension create/query, server SQLx smoke, compile speed, and cross-platform - exception/dynamic-linking behavior. -- Singlepass: evaluate only after the same longjmp/error and extension suite - passes. -- Asyncify: keep out of production unless a specific snapshot or journaling path - proves a need on an experiment branch. -- Alternative engines such as V8 or JavaScriptCore: evaluate only for mobile or - special embedded targets, checking WASIX, dynamic linking, filesystem, - exceptions, and headless/AOT implications. -- Native CPU tuning: evaluate only if artifacts remain portable or target packs - are split intentionally. - -## Bucket 2: CI, CD, Release, And Workspace Hygiene - -### CI/CD Target Model - -- Validate the source-controlled-inputs model on GitHub: normal CI must keep - using only source templates plus downloaded compatible Assets workflow - bundles, and asset-producing changes must remain the only PR path that - fetches upstream sources or runs Docker. -- Harden `xtask release publish`: the local command should stage and validate - exactly what the Release workflow publishes, then either invoke release-plz in - a Trusted Publishing environment or fail before any partial publish is - possible. -- Keep `xtask release stage` and `scripts/validate.sh release` as the only - packaging path for generated portable/AOT crate contents. Any future release - check must run against the staged workspace, not ad hoc copied artifacts. -- Split packaged runtime payloads from extension payloads after the `bundled` - feature model lands. Today `bundled` gives users an embedded-runtime install - mode without the public extension API, but the single `pglite-oxide-assets` - crate still carries extension archives and the target AOT pack can carry - extension AOT artifacts. A future crate split should make `bundled` - runtime-only at download/package-size level and keep extension archives plus - extension AOT artifacts behind `extensions`. -- Keep the local development split into three modes: fast assetless contributor - checks, host-platform artifact-backed runtime work, and downloaded CI - artifact testing. Developers validate their host platform locally; CI remains - responsible for the full target matrix. -- Ensure new asset-producing inputs update the committed asset-input - fingerprint and are covered by source-free `assets verify-committed`, - generated-asset validation, package-size checks, and runtime smoke tests. -- Release CI must publish only artifacts generated and tested for the exact - release SHA, with package checks performed against the same staged crate - contents that are published. - -### Target Matrix - -First-class targets: - -- `aarch64-apple-darwin`; -- `x86_64-unknown-linux-gnu`; -- `aarch64-unknown-linux-gnu`; -- `x86_64-pc-windows-msvc`. - -Experimental targets: - -- Linux musl; -- Android; -- iOS through V8/JSC/interpreter paths if feasible; -- RISC-V after Wasmer target support matures. - -### Asset And AOT CI - -- Validate the first full `Assets` workflow run after the portable-WASIX plus - native-AOT CI split lands. -- Keep dynamic-link closure checks, manifest validation, package-size checks, - and smoke tests coupled to asset release orchestration. -- Package strategy order: - 1. raw AOT artifact if the crate stays under crates.io's compressed limit; - 2. `.zst` compressed artifact with one-time expansion into the persistent - cache; - 3. deterministic split AOT/asset packs if compression is still too large. - -### Workspace Hygiene - -- Keep root `pglite-oxide` as the public crate. -- Keep asset/AOT crates internal implementation details with exact internal - dependency versions. -- Keep the source-free asset workflow honest as new asset-producing inputs are - added: every new input must be covered by `assets verify-committed`, the - input fingerprint, or an explicit asset CI gate. -- Keep one active source root: configured `postgres-pglite` - `REL_17_5-pglite` pinned to the audited commit. -- Keep user-facing docs free of implementation backlog/status notes. -- Keep [DONE.md](DONE.md) as the only completed-work/status document and this - file as the only implementation backlog. - -### Normal CI - -- Validate the first path-aware CI run for docs-only, CI-only, test-only, and - package-affecting PRs. -- Validate the first Rust-only native-AOT runtime matrix run across macOS - arm/x64, Linux arm/x64, and Windows x64. -- Keep doctests, no-default-features checks, feature powerset, dependency - invariants, package checks, supply-chain - checks, and example checks routed through the DRY validation script. -- keep the minimal `wasmer` and `wasmer-wasix` feature sets while retaining - filesystem mounts, WASIX env/args, networking required by `pg_dump`, and - dynamic linking; -- macOS multi-module LLVM exception gate: main module plus at least two side - modules, SQL error recovery after each load, and normal parallel test - scheduling on macOS arm64 and x64; -- keep actionlint, cargo-deny, and GitHub Actions security audit green. - -### Release And Publishing - -- Configure Trusted Publishing for every published crate. -- Complete first-publish/bootstrap verification for internal asset/AOT crates on - crates.io. -- Validate that release-plz publishes internal asset/AOT crates before the root - crate when exact internal dependency versions require that order. -- Keep release PRs using a GitHub App or bot token if maintainers want normal PR - CI to run automatically on release-plz branches. - -### Reproducibility - -- Record Wasmer crate version, Wasmer CLI/tool version, wasixcc/toolchain - version, WASIX libc/EH-PIC sysroot identity, LLVM version, postgres-pglite - commit, pglite-build commit, extension repository commits, Docker image - digest, and build profile in manifests. -- Use Wasmer reproducible-build controls such as `WASMER_REPRODUCIBLE_BUILD=1` - where applicable. -- Add deterministic two-build comparisons for identical source pins. -- Make asset and AOT crate hashes stable enough for audit and cache - invalidation. - -## Bucket 3: Source, Build Spine, And Asset Provenance - -The active source baseline is `electric-sql/postgres-pglite` -`REL_17_5-pglite` at `01792c31a62b7045eb22e93d7dad022bb64b1184`, matching the -`@electric-sql/pglite` 0.4.5 source/artifact pair. The historical -`REL_17_5_WASM-pglite-builder` branch remains reference material for extension -and `pg_dump` packaging ideas, not the production source spine. -`electric-sql/pglite-build` `portable` remains pinned as build-script -provenance, not as a second runtime source root. - -### Source-Spine Work - -- Keep stable branch lifecycle/protocol exports: - `_start`/single-user startup, `pgl_setPGliteActive`, `pgl_startPGlite`, - `ProcessStartupPacket`, `PostgresMainLoopOnce`, and `PostgresMainLongJmp`. -- Critique and document each PGlite adaptation before copying it. Keep only host - ABI adaptations still necessary under Wasmer/WASIX. -- Keep `pglite-build` and the builder branch as reference inputs for extension - symbol discovery and packaging, without reintroducing the old `pglite-wasm/*` - wrapper as production runtime code. -- Keep the generated `wasix-dl` export list wired to side-module import - discovery and extend negative tests as more extension packs are added. -- Replace catalog-driven extension packaging with install-delta packaging before - promoting extensions that scatter files outside the standard `.so`, - `.control`, and extension SQL layout. Reuse upstream `pack_extension.py` - concepts without using non-deterministic archive writing. -- Keep manifest fields for extension imports and core exports current so - dynamic-link failures are diagnosed before startup. -- Add negative fixtures proving wrong-core side modules and unresolved imports - fail during validation, before runtime startup. -- Verify `vector`, at least one contrib extension, one PGXS extension, and - `pg_dump` are always built from the same configured tree before release. - -### Upstream Audit - -- Keep `xtask assets audit-upstream --strict` as the source of truth for newer - upstream `postgres-pglite` fixes, marking each item as included, replaced by - WASIX architecture, optional, or pending. -- Keep the WASIX longjmp bridge intentionally narrower than upstream - Emscripten's `jmp_buf` content comparison: pointer identity against exported - top-level `postgresmain_sigjmp_buf`. -- Keep active-portal abort cleanup in PostgreSQL-owned code for - `PGLITE_WASIX_DL`; do not reintroduce Rust-side synthetic `Sync` or portal - cleanup without a failing upstream regression test. -- Keep startup identity/database handling owned by PostgreSQL startup code; Rust - should synthesize only runtime/host failures that occur before PostgreSQL can - emit wire output. -- Audit, cherry-pick, or explicitly reject remaining upstream/runtime items: - background-worker disable semantics, artifact cache fixes, data-directory - locking deltas, upstream `postgresConfig` parity beyond the Rust startup-GUC - API, and `pgoutput` symbol exports. -- Decide whether proxy/frontend startup should eventually stop fabricating - startup responses in Rust and converge further toward upstream - `interactive_one`/`ProcessStartupPacket` lifecycle for every client - connection. -- Keep future config changes flowing through the Rust startup-GUC API, a pinned - upstream `postgresConfig` surface, or a documented initdb-time config model. - -### Canonical Assets - -- Keep timezone data generated by `zic` inside the pinned build image from - PostgreSQL `tzdata.zi`, never from a maintainer host. -- Generate PGDATA with the desired timezone instead of patching extracted config - text. -- Keep runtime prefix files packaged from the pinned configured tree, including - timezone files, extension SQL/control files, and installed support libraries. -- Keep asset manifests tied to source commits, Docker image digest, Wasmer - version, engine identity, source module hashes, import/export sets, archive - hashes, and package sizes. - -## Bucket 4: Runtime Correctness And Protocol - -- Continue expanding PostgreSQL regression coverage beyond the current PGlite - parity subset into less common planner, catalog, lock, utility-command, and - wait/socket behavior. -- Add broader raw wire-protocol and fuzz coverage around extended query - sequencing. -- Keep export guards requiring `PostgresMainLongJmp`, - `PostgresSendReadyForQueryIfNecessary`, `pgl_pq_flush`, and WASIX - input/output symbols. -- If a future Wasmer version resumes the C `sigsetjmp` boundary directly, keep - the explicit recovery export as a tested no-op fallback until tests prove it - can be removed. -- Keep the guard that treats missing `ParseComplete` as an error on successful - Parse paths. -- Keep the production patch free of `pglite-wasm/*`; future frontend/initdb - stubs must be justified by link-symbol analysis against the stable branch. -- Add a C/link audit for the split-initdb child-process shim and keep it - fail-closed to locale discovery plus upstream initdb's `postgres` boot/check - commands. -- Keep interrupted-PGDATA and root-locking tests as the owned coverage for - failed opens. Do not add a fake child-process kill model unless the runtime - grows a real child-process boundary. -- Harden backend-side COPY error coverage beyond the current suite. -- Investigate returning from COPY streaming continuation to buffered mode after - COPY if it can be proven correct for SQLx, tokio-postgres, raw TCP, Unix - sockets, `CopyFail`, and post-COPY reuse. -- Keep direct raw protocol streaming and direct `pg_dump` on the shared - `BackendSession` path; do not reintroduce clone/server indirection. -- Reject asset mixing through negative tests: wrong runtime, wrong side module, - wrong AOT identity, wrong extension archive, and stale manifest. - -## Bucket 5: Extensions - -Extension catalog generation discovers 40 SQL extensions from PGlite docs/REPL -exports, PostgreSQL contrib, `postgres-pglite/pglite/other_extensions`, pinned -external repositories, and the packaged asset manifest. The current build plan -requests and packages 37 extensions. All 37 packaged extensions have passed -direct, server, restart, and lifecycle materialization gates and are public -constants. `pgcrypto`, `uuid-ossp`, and PostGIS remain explicitly blocked until -their native dependency stacks are pinned and smoke-tested for WASIX. - -### Remaining Promotion Order - -1. Add pinned WASIX OpenSSL/libcrypto sysroot and promote `pgcrypto`. -2. Add pinned WASIX OSSP UUID/libuuid sysroot and promote `uuid-ossp`. -3. Add pinned WASIX geospatial dependency stack and install-delta packaging for - PostGIS. - -### Extension Rules And Hardening - -- Generate public constants only after direct, server, restart, and lifecycle - smoke gates pass for the current asset set. -- Keep PGlite `live` out of SQL extension constants until there is a - Rust-native live-query API. -- Keep `extensions::ALL` limited to extensions passing for the current asset - set. -- Keep every discovered SQL extension either build-requested or blocked with a - concrete reason. -- Verify that manifest metadata remains sufficient for preload/config/shared - memory/restart/dependency/load-order needs; add fields only where the current - dependency, load-order, and lifecycle metadata are insufficient. -- Prove preload-required extensions such as `pg_stat_statements` apply - `shared_preload_libraries` before backend startup before exposing them. -- Make extension dependency errors fail at manifest/build-plan generation time - where possible. -- Add extension load-order and missing-native-dependency failure tests. -- Add preload/startup-config extension tests before exposing extensions that - require postmaster-time configuration. -- Add lifecycle negative tests for missing side modules, wrong core runtime, - missing SQL/control files, repeated enable, reopen after install, and missing - requested archives. -- Keep generated native-module metadata authoritative: SQL extension names and - native side-module names can differ, and some extensions are SQL-only. -- Replace remaining PGXS build assumptions with extension-specific build - metadata where external modules require extra flags, generated headers, - install hooks, generated SQL, or multiple side modules. -- Add automation that updates `assets/extensions.smoke.toml` from reviewed smoke - suite output instead of requiring maintainers to edit it by hand. - -## Bucket 6: `pg_dump` - -- Keep public dump/restore tests for direct `Pglite`, `PgliteServer`, vector, - indexes, views, sequences, `--schema-only`, and quoted identifiers. -- Keep direct `Pglite::dump_sql` no-clone, no-public-server, and no-OS-loopback: - stock WASIX `pg_dump`/libpq should route through Wasmer virtual networking and - host-side `exec_protocol_raw`. -- Do not add a pglite-oxide-specific `pg_dump` callback ABI unless stock libpq - over virtual networking fails a concrete correctness or performance gate. -- Keep rejecting passthrough flags that conflict with the typed API's managed - output file, output format, host, port, username, database, and job count. -- Add release-mode performance and cross-platform CI for `PgDumpOptions`, - `Pglite::dump_sql`, `Pglite::dump_bytes`, `PgliteServer::dump_sql`, - `PgliteServer::dump_bytes`, and the real `pglite-dump` CLI. - -## Bucket 7: Examples, Docs, And Ecosystem Tests - -- Add examples and CI for SQLx, `tokio-postgres`, rstest, Diesel, SeaORM, - Tauri, pgvector local RAG, Python/psycopg, Go/pgx, and Node `pg`. -- Add Python, Go, and Node proxy examples that verify SQLSTATE preservation and - recovery behavior through ordinary client libraries. -- Keep README first screen focused on embedded Postgres, tests, local apps, - pgvector/common extensions, no Docker, and any Postgres client through local - server mode. -- Keep user-facing docs free of internal status notes; implementation notes stay - in this file or [DONE.md](DONE.md). - -Required release test categories: - -- direct `SELECT 1`, persistence, restart, temporary template cache, and root - locks; -- SQLx and `tokio-postgres` server connections; -- SSLRequest, CancelRequest, Parse/Bind/Execute error recovery, and pipelined - extended queries; -- vector create/insert/query/distance through direct API and server mode; -- generated extension smoke suite; -- unsafe archive rejection and canonical path validation; -- manifest SHA validation and AOT source-module identity verification; -- unsupported target errors; -- macOS multi-module exception recovery; -- public dump/restore; -- Python, Go, and Node proxy tests; -- package size checks and publish dry-runs. - -## Experiment And Decision Policy - -Every runtime-affecting experiment must end in one repo-visible state: - -- `promoted`: implementation is on the production path; -- `blocked`: evidence and blocker are documented; -- `rejected`: reason and alternative are documented. - -Do not leave runtime-affecting experiments as loose notes. - -## Reference Material To Recheck - -- Wasmer 7 announcement and runtime feature docs; -- Wasmer 7.2 alpha release notes; -- Wasmer WASIX dynamic-linking docs; -- Wasmer WordPress/WebAssembly case study; -- Wasmer InstaBoot documentation; -- Wasmer macOS multi-module LLVM exception issue; -- Wasmer embedded/iOS tracking issue; -- Wasmer Rust API docs; -- PGlite extension docs and extension-development docs; -- `postgres-pglite` `REL_17_5-pglite` and historical - `REL_17_5_WASM-pglite-builder` reference branch; -- `pglite-build` `portable`; -- PGlite data-directory locking, startup config, and `pgoutput` upstream PRs. diff --git a/docs/USAGE.md b/docs/USAGE.md deleted file mode 100644 index 77b20663..00000000 --- a/docs/USAGE.md +++ /dev/null @@ -1,449 +0,0 @@ -# Usage Guide - -`pglite-oxide` has two primary entry points: - -- `Pglite` for direct embedded queries from Rust; -- `PgliteServer` for libraries that need a PostgreSQL connection URI. - -Prefer `Pglite` unless you specifically need a Postgres client connection. - -## Install Modes - -Most projects should use the default install: - -```toml -pglite-oxide = "0.4" -``` - -Default features include the packaged embedded Postgres runtime, the -target-specific Wasmer AOT artifact for the current platform, and the public -bundled-extension API. - -If you want embedded Postgres without the extension API, keep the packaged -runtime and turn off defaults explicitly: - -```toml -pglite-oxide = { version = "0.4", default-features = false, features = ["bundled"] } -``` - -Use the minimal mode only when you intentionally do not want packaged runtime -assets in the dependency graph: - -```toml -pglite-oxide = { version = "0.4", default-features = false } -``` - -Minimal mode is for specialized integrations and maintainer workflows. Normal -database opens need the packaged runtime/AOT assets provided by `bundled`. - -## Opening Databases - -Persistent database under an explicit path: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::open("./.pglite")?; - db.close()?; - Ok(()) -} -``` - -Persistent database under the platform app-data directory: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder() - .app("com", "example", "desktop-app") - .open()?; - db.close()?; - Ok(()) -} -``` - -Fast temporary database for tests: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.close()?; - Ok(()) -} -``` - -Explicit fresh-cluster temporary database: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder().fresh_temporary().open()?; - db.close()?; - Ok(()) -} -``` - -`temporary()` uses the cached template path. `fresh_temporary()` disables that -cache and runs the packaged `initdb` path instead. Use it when a test needs a -brand-new cluster, not for the common fast path. - -The direct builder also exposes: - -- `path(...)`, `app(...)`, and `app_id(...)` for persistent roots; -- `temporary()`, `template_cache(bool)`, and `fresh_temporary()` for ephemeral - roots; -- `load_data_dir_archive(...)` for restoring a physical data-dir archive before - open. - -## Startup Configuration - -Use builder methods for startup-time database settings: - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::builder() - .temporary() - .postgres_config("synchronous_commit", "off") - .postgres_config("work_mem", "8MB") - .username("postgres") - .database("template1") - .relaxed_durability(true) - .open()?; - db.close()?; - Ok(()) -} -``` - -Relevant direct and server builder methods: - -- `postgres_config(name, value)` and `postgres_configs(...)`; -- `username(...)` and `database(...)`; -- `debug_level(level)` with PostgreSQL levels `0..=5`; -- `relaxed_durability(true)` for cacheable local workloads; -- `startup_arg(...)` and `startup_args(...)` for advanced PostgreSQL arguments. - -Use `postgres_config` for ordinary GUCs. It follows PostgreSQL's normal -`-c name=value` startup behavior, and explicit values override the default -startup profile. - -For `PgliteServer`, the same startup methods are available on -`PgliteServer::builder()`. The `pglite-proxy` CLI exposes startup GUCs with -`--postgres-config NAME=VALUE`. - -## Queries - -`exec` runs SQL without parameters. `query` runs the extended protocol with -JSON parameters. - -```rust,no_run -use pglite_oxide::Pglite; -use serde_json::json; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - - db.exec("CREATE TABLE items(id INT PRIMARY KEY, value TEXT)", None)?; - db.query( - "INSERT INTO items(id, value) VALUES ($1, $2)", - &[json!(1), json!("alpha")], - None, - )?; - - let result = db.query("SELECT value FROM items WHERE id = $1", &[json!(1)], None)?; - println!("{:?}", result.rows); - - db.close()?; - Ok(()) -} -``` - -Parameters are `serde_json::Value`. Default parsers and serializers cover -common Postgres scalar types, JSON, bytea, UUIDs, timestamps, and built-in -arrays. - -When you add runtime-created array types such as arrays of enums, domains, or -composites, `pglite-oxide` usually discovers them lazily. If you want to refresh -that state explicitly, call `refresh_array_types()`. - -## Query Options - -`QueryOptions` controls result parsing and protocol behavior: - -- `row_mode` switches between object rows and positional arrays; -- `parsers` and `serializers` override type handling for specific OIDs; -- `blob` attaches bytes to `/dev/blob` for `COPY FROM`; -- `param_types` pins parameter OIDs for cases where PostgreSQL cannot infer - them cleanly; -- `on_notice` handles backend notices on a query-by-query basis. - -Example: - -```rust,no_run -use pglite_oxide::{Pglite, QueryOptions, RowMode}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - let options = QueryOptions { - row_mode: Some(RowMode::Array), - ..QueryOptions::default() - }; - - let result = db.query("SELECT 1, 2", &[], Some(&options))?; - println!("{:?}", result.rows); - - db.close()?; - Ok(()) -} -``` - -Use `describe_query(...)` when you need parameter and result type metadata -without executing the query. - -## Transactions - -Use `transaction` when several direct calls should commit or roll back together. - -```rust,no_run -use pglite_oxide::Pglite; -use serde_json::json; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE items(value TEXT)", None)?; - - db.transaction(|tx| { - tx.query("INSERT INTO items(value) VALUES ($1)", &[json!("alpha")], None)?; - tx.query("INSERT INTO items(value) VALUES ($1)", &[json!("beta")], None)?; - Ok(()) - })?; - - db.close()?; - Ok(()) -} -``` - -The `Transaction` handle also exposes `exec`, `query`, `refresh_array_types`, -`commit`, and `rollback`. - -## Notifications - -Use `listen` when you want channel-specific `LISTEN/NOTIFY` callbacks, and -`on_notification` when you want to observe every notification. - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - - let specific = db.listen("events", |payload| { - println!("events payload: {payload}"); - })?; - let global = db.on_notification(|channel, payload| { - println!("{channel}: {payload}"); - }); - - db.exec("NOTIFY events, 'hello'", None)?; - - db.unlisten(specific)?; - db.off_notification(global); - db.close()?; - Ok(()) -} -``` - -`unlisten_channel(...)` removes all listeners for a specific channel. - -## `/dev/blob` and COPY - -Direct `Pglite` can send and receive bytes through the virtual `/dev/blob` -device. - -```rust,no_run -use pglite_oxide::{Pglite, QueryOptions}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE items(value TEXT)", None)?; - - let import = QueryOptions { - blob: Some(b"alpha\nbeta\n".to_vec()), - ..QueryOptions::default() - }; - db.exec("COPY items FROM '/dev/blob'", Some(&import))?; - - let exported = db.exec("COPY items TO '/dev/blob'", None)?; - let blob = exported[0].blob.clone().expect("COPY TO blob"); - println!("{}", String::from_utf8(blob)?); - - db.close()?; - Ok(()) -} -``` - -If you already use a standard Postgres client, `PgliteServer` also supports -client-driven `COPY FROM STDIN` through the normal wire protocol. - -## SQL Helpers - -`format_query` asks Postgres to quote parameter values. `QueryTemplate` and -`quote_identifier` help build SQL while keeping identifiers and values separate. - -```rust,no_run -use pglite_oxide::{Pglite, QueryTemplate, format_query, quote_identifier}; -use serde_json::json; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - - let sql = format_query(&mut db, "SELECT $1::int", &[json!(42)])?; - assert_eq!(sql, "SELECT '42'::int"); - - let mut template = QueryTemplate::new(); - template.push_sql("SELECT * FROM "); - template.push_identifier("items"); - template.push_sql(" WHERE value = "); - template.push_param(json!("alpha")); - let built = template.build(); - - assert_eq!(built.query, "SELECT * FROM \"items\" WHERE value = $1"); - assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\""); - - db.close()?; - Ok(()) -} -``` - -## Server Mode - -Use `PgliteServer` when another crate expects a PostgreSQL URL. - -```rust,no_run -use pglite_oxide::PgliteServer; -use sqlx::{Connection, Row}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let server = PgliteServer::temporary_tcp()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; - - let row = sqlx::query("SELECT $1::int4 + 1 AS answer") - .bind(41_i32) - .fetch_one(&mut conn) - .await?; - assert_eq!(row.try_get::("answer")?, 42); - - conn.close().await?; - server.shutdown()?; - Ok(()) -} -``` - -`PgliteServer::builder()` supports: - -- `path(...)`, `temporary()`, and `fresh_temporary()`; -- `tcp(...)`, and on Unix hosts `unix(...)`; -- the same startup configuration methods as `PgliteBuilder`; -- bundled extensions with `extension(...)` and `extensions(...)`. - -Use `connection_uri()` or `database_url()` to hand a URI to a client library. -Generated URLs include `sslmode=disable`. - -Server mode still exposes one embedded backend. Configure SQLx, Diesel, -SeaORM, `tokio-postgres`, and framework pools with one connection. - -## Raw Protocol - -`exec_protocol` is the safest low-level wire-protocol entry point. It returns -parsed backend messages and still handles notices and notifications. - -```rust,no_run -use pglite_oxide::{ExecProtocolOptions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - let mut query = vec![b'Q']; - query.extend_from_slice(&13_i32.to_be_bytes()); - query.extend_from_slice(b"SELECT 1\0"); - - let result = db.exec_protocol(&query, ExecProtocolOptions::default())?; - assert!(!result.messages.is_empty()); - - db.close()?; - Ok(()) -} -``` - -Use `exec_protocol_raw(...)` when you need raw bytes, and -`exec_protocol_raw_stream(...)` when you want to forward backend bytes as they -arrive. - -## Physical Data-Dir Archives - -Use physical archives for same-version restore and fast cloning. They are not a -cross-version backup protocol. - -```rust,no_run -use pglite_oxide::Pglite; - -fn main() -> Result<(), Box> { - let mut source = Pglite::temporary()?; - source.exec("CREATE TABLE items(value TEXT)", None)?; - source.exec("INSERT INTO items VALUES ('alpha')", None)?; - - let archive = source.dump_data_dir()?; - - let mut restored = Pglite::builder() - .temporary() - .load_data_dir_archive(archive) - .open()?; - let mut cloned = restored.try_clone()?; - - cloned.exec("SELECT * FROM items", None)?; - - restored.close()?; - cloned.close()?; - source.close()?; - Ok(()) -} -``` - -Use `dump_data_dir_with_format(...)` when you want an explicit -`DataDirArchiveFormat::Tar` or `DataDirArchiveFormat::TarGz`. - -## Logical Dumps - -With the default feature set, both direct and server APIs expose logical dumps -through `PgDumpOptions`. - -```rust,no_run -use pglite_oxide::{PgDumpOptions, Pglite}; - -fn main() -> Result<(), Box> { - let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE items(value TEXT)", None)?; - db.exec("INSERT INTO items VALUES ('alpha')", None)?; - - let sql = db.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; - println!("{sql}"); - - db.close()?; - Ok(()) -} -``` - -CLI: - -```sh -pglite-dump --root ./.pglite -pglite-dump --root ./.pglite -- --schema-only -``` - -See [PG_DUMP.md](PG_DUMP.md) for dump/restore and upgrade guidance. diff --git a/docs/architecture/final-product-source-architecture.md b/docs/architecture/final-product-source-architecture.md new file mode 100644 index 00000000..b6105b11 --- /dev/null +++ b/docs/architecture/final-product-source-architecture.md @@ -0,0 +1,292 @@ +# Oliphaunt Source Architecture + +This document describes the active repository model. It is not a migration log. + +## Authority Boundaries + +Oliphaunt uses one source graph and one release identity system: + +- Moon owns projects, task execution, affectedness, dependency scopes, and task + caching. +- Release Please manifest mode owns product versions, changelogs, and the + generated release PR. +- The protected release workflow owns exact-SHA product tags and draft GitHub + releases; it stages them only after candidate qualification and current-main + validation. +- Product-local `release.toml` files own package metadata that release-please + does not model: owner, kind, publish targets, registry coordinates, release + artifacts, and compatibility-version files. +- Product-local `targets/*.toml` files own platform artifact metadata. +- Bun entrypoints under `tools/release/*.mjs` own release checks, dry-runs, + publication routing, checksums, attestations, registry checks, and artifact + verification. + +There is no separate release graph, release-input graph, CI jobs graph, or +consumer lockfile. If a relationship affects source, task execution, or release +coupling, it must be visible in Moon or release-please/product-local metadata. + +## Source Shape + +Source products and shared domains live under `src/`: + +```text +src/postgres/versions/18/ PostgreSQL 18 source pin and validation +src/sources/ shared source and toolchain pins +src/extensions/ exact SQL extension catalog, recipes, evidence +src/runtimes/liboliphaunt/native native C ABI runtime +src/runtimes/liboliphaunt/wasix WASIX runtime and AOT assets +src/runtimes/broker Rust broker helper runtime +src/runtimes/node-direct Node direct native runtime +src/sdks/rust Rust SDK +src/sdks/swift Swift SDK +src/sdks/kotlin Kotlin/Android SDK +src/sdks/react-native React Native SDK +src/sdks/js TypeScript SDK +src/bindings/wasix-rust Rust binding for the WASIX runtime +src/shared/contracts cross-language protocol and API contracts +src/shared/extension-runtime-contract extension/runtime ABI contract +src/shared/fixtures shared semantic test fixtures +src/docs public docs site +``` + +Generated local state lives outside source roots or in ignored product build +directories. Root `target/`, `.moon/cache/`, `node_modules/`, Gradle build +state, Swift `.build/`, Xcode DerivedData, Expo state, and docs build output are +generated state and must not be tracked. + +## Moon Graph + +Moon is the only task and affectedness graph. Project dependencies represent +real source relationships and use dependency scopes: + +- `production` and `peer` are release-affecting compatibility edges. +- `build` is a test, generation, fixture, or package-shape edge. It affects CI + and local affected tasks, but it does not force downstream product releases. + +Examples: + +- `liboliphaunt-native -> oliphaunt-rust`, `oliphaunt-swift`, + `oliphaunt-kotlin`, `oliphaunt-node-direct`, and `oliphaunt-broker` are + production edges. +- `oliphaunt-swift -> oliphaunt-react-native` and + `oliphaunt-kotlin -> oliphaunt-react-native` are production edges. +- `oliphaunt-rust -> oliphaunt-js` is a production edge because the TypeScript + SDK uses the Rust broker helper. +- `extensions -> SDKs` is a build edge. SDK tests and generated metadata react + to extension catalog changes, but exact extension source releases do not + automatically release SDK packages. +- `shared-fixtures -> SDKs` is a build edge. Fixtures affect tests and coverage, + not package releases. + +Use Moon queries for graph inspection: + +```sh +moon query projects +moon query tasks +moon query affected --upstream none --downstream deep +moon project-graph +moon action-graph oliphaunt-rust:test +``` + +Do not add a second graph format to answer questions Moon already answers. + +## CI Model + +CI has stable GitHub job names because branch protection needs stable checks. +Moon decides which tasks are affected and how tasks depend on each other. + +The flow is: + +1. The affected planner calls Moon queries and maps `ci-*` task tags to stable + GitHub job names and exact Moon task targets. +2. Product jobs call `.github/scripts/run-planned-moon-job.sh `, which + reads the planned target map and delegates to `.github/scripts/run-moon-targets.sh`. + That helper intentionally uses `moon run` for planned artifact targets because + the planner may select artifact producer jobs that are required by a changed + product but are not themselves directly affected. +3. GitHub matrix is used only for real runner or target fan-out: OS, CPU, ABI, + simulator, device, native runtime target, broker target, Node direct target, + and WASIX AOT target. +4. The affected planner emits visible `Checks / ` and + `Tests / ` matrices plus one compact `Policy` batch from + Moon-selected targets. `Checks / ` is for normal + static/lint/typecheck-style work. `Policy` is for repository assertions that + parse code, workflows, release metadata, or generated graphs and enforce + invariants. Check and test matrix jobs delegate one exact target to + `moon run --upstream deep`, so task inheritance and target dependencies stay + in Moon without pulling unrelated affected tests into the checks phase. The + policy batch runs its exact selected targets with `--upstream none`, because + package prerequisites already have their own visible jobs. Tasks that truly + need Android tooling declare the `ci-android-sdk` capability tag; the planner + projects it into runner setup instead of provisioning Android for every + check, policy, and test shard. When a selected build lane already inherits a + `check` or `test` target, the phase selector skips that covered target. +5. The aggregate `Checks` and `Tests` jobs are gates over those visible target + jobs. `Checks` waits for both `Checks / ` jobs and the selected + `Policy` batch, but policy assertions are named separately because they are + not normal package checks. +6. Artifact-producing jobs call + `.github/scripts/run-planned-moon-job.sh `. +7. The `builds` aggregate answers the release-deliverable question: + every selected runtime, helper runtime, SDK package, extension artifact, + extension package, and mobile app builder must finish successfully. + React Native package changes select both Android and iOS mobile app builders, + because the RN release surface is the JS package plus native Swift/Kotlin + integration built from staged runtime and extension artifacts. +8. The `required` aggregate is intentionally thin. It gates `affected`, + `checks`, `tests`, `builds`, and `e2e`, so the job names remain true to their + phase and mobile installed-app E2E stays an artifact consumer rather than a + build producer. +9. Builder jobs invoke only their planned builder Moon targets. GitHub `needs:` + expresses artifact ordering for uploaded artifacts. Builder jobs that can + run local task prerequisites keep Moon upstream inheritance enabled; jobs + that consume downloaded artifacts pass `--upstream none` through + `.github/scripts/run-planned-moon-job.sh` so producer artifacts are not + rebuilt in the consumer job. + SDK `package-artifacts` tasks depend on the product `package` task and + consume its package-shape outputs instead of rerunning package assertions + inside the artifact staging script. +10. Expensive runtime, mobile, benchmark, publish, registry, and provenance jobs + are selected by affectedness, but they execute live when current runner state + matters. + +Mobile build jobs do not own ABI lists. They request target surfaces such as +`react-native-android` and `react-native-ios`; the selected native runtime +target IDs come from `src/runtimes/liboliphaunt/native/targets/*.toml`. Mobile +E2E is a separate installed-app phase that consumes the app artifacts from the +same CI run; it must not rebuild runtimes, SDKs, or extension packages. + +Moon task options must be semantic: + +- cache deterministic checks, tests, package-shape checks, generated freshness, + docs builds, and measured unit coverage with declared inputs and outputs. +- use `runInCI: skip` for expensive dependency tasks that should remain valid + in CI action graphs but should not run as broad affected work. +- use `runInCI: false` only for local/manual tasks that CI must never invoke. +- keep runtime/device/provenance tasks uncached in CI with `MOON_CACHE=off`. + +## Release Model + +Release decisions come from release-please components and Moon dependency +scopes: + +1. Release Please identifies product components, versions, and changelogs and + prepares the generated release PR. +2. Product-local `release.toml` adds publish and artifact metadata. +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`. + +The protected publish workflow derives `-v` from that +reviewed release state and creates the product tags and draft GitHub releases +at the exact qualified commit. Release Please never targets the moving `main` +ref to create them. + +This keeps release behavior explicit without duplicating source globs. A +PostgreSQL 18 source change releases native and WASIX runtimes plus downstream +products that have production/peer compatibility edges. A shared fixture change +runs affected tests but releases no package. An exact extension source change +releases that exact extension artifact product, not every SDK. + +Release planning adapts Moon project sources and dependency scopes for +product-tag diffs; it must not introduce hand-authored source glob or dependency +metadata. CI execution must run the exact task targets emitted by the affected +planner instead of recomputing affectedness inside each product job. + +Release publishing consumes artifacts only from the current-main, same-SHA `CI` +run whose `Builds`, `Required`, and `Qualified` gates succeeded. It downloads +runtime, SDK, helper, extension, and mobile build artifacts only after that +qualification record proves the named check, test, build, policy, and selected +E2E phases completed successfully. + +## Extensions + +Extensions remain exact SQL-selectable artifacts. Release-product ownership is +separate from SQL selection: PostgreSQL 18 contrib members share one +runtime-bound distribution product, while each external extension remains an +independently versioned product. + +- Public selection is by SQL extension name, for example `vector` or `postgis`. +- PostgreSQL 18 contrib release ownership lives at `src/extensions/contrib/` in + one `oliphaunt-extension-contrib-pg18` product with one `VERSION`, changelog, + and `release.toml`. The canonical member inventory stays in + `src/extensions/contrib/postgres18.toml`; member folders retain exact target, + source, and evidence metadata but do not own SemVer or registry identities. +- External extensions own folders under `src/extensions/external//` with + source pin, recipe, target metadata, tests, changelog, version, and + `release.toml`. +- Complex external extensions keep dependency source pins under their own + extension folder, for example PostGIS dependency pins under + `src/extensions/external/postgis/dependencies/`. +- `src/shared/extension-runtime-contract/` defines the runtime contract shared + by native and WASIX extension artifacts. +- `src/extensions/artifacts/native/` and `src/extensions/artifacts/wasix/` + validate publishable exact-extension artifact shape. +- Contrib publication groups those exact member artifacts into one deterministic + carrier per family/target. Every carrier records each member's nested path, + byte count, and checksum; consumers select and verify only the requested SQL + members. The bundle is a registry/distribution envelope, not an instruction + to install every contrib extension in an application. +- Native runtime targets may opt out of exact-extension artifact publication + with product-local target metadata when no real extension producer exists for + that target. They must not appear in exact-extension matrices until the + producer exists. + +SDK packages must not ship all extensions. App developers install or configure +only the SQL extension artifacts they use; generated registries and package +checks must prove unselected extension files do not enter app artifacts. + +## Tool Entrypoints + +Use Moon directly for repository tasks: + +```sh +moon run :check +moon run :test +moon run :coverage +moon run :package +moon run :smoke --cache off +moon query affected --upstream none --downstream deep +``` + +`moon run :package` is the local package-shape lane. It must not build +platform runtimes, exact-extension matrices, mobile apps, or publishable SDK +artifact envelopes. Publishable artifacts are produced by explicit +`package-artifacts`, runtime, extension, and mobile builder tasks selected by +the `CI` workflow. + +Use pnpm only for JavaScript dependency installation and package-manager +commands. Use Cargo, SwiftPM/Xcode, Gradle, npm/JSR, and Expo through +product-local Moon tasks or product-owned scripts. Do not add root alias layers +over Moon. + +## Mobile E2E Decision + +Decision (2026-06-08): installed-app mobile E2E uses the pinned open-source +Maestro CLI on GitHub-hosted emulator/simulator jobs. This is not an open +research loop. Do not keep re-checking Maestro, Detox, Appium, EAS-only flows, +Firebase Test Lab, BrowserStack, Sauce, AWS Device Farm, or paid hosted-device +services during routine implementation. + +Reopen the decision only with a written implementation proposal that names a +concrete installed-app E2E requirement that the pinned open-source Maestro CLI +cannot satisfy. When mobile E2E breaks, debug the chosen implementation first: +app artifact shape, simulator/emulator setup, Maestro flow files, logs, and CI +runner assumptions. The default proof path must stay free and reproducible from +a public checkout; paid hosted-device services are not part of the default +model. + +## Removed Surfaces + +These surfaces are retired and must not reappear: + +- `release-plz.toml` and release-plz release PR/changelog/tag ownership. +- `tools/release/release-graph.toml`. +- `tools/release/release-inputs.toml`. +- `tools/graph/jobs.toml`. +- custom affected task runners that bypass Moon. +- broad registry reinstall gates as routine CI policy. +- extension packs, aliases, or grouped selectors. +- root product aliases such as `crates/`, `sdks/`, root `assets/`, and + root-level runtime build trees. diff --git a/docs/architecture/ios.md b/docs/architecture/ios.md new file mode 100644 index 00000000..4a2d32f7 --- /dev/null +++ b/docs/architecture/ios.md @@ -0,0 +1,410 @@ +# iOS Architecture Investigation + +This document evaluates the strongest iOS architecture for `liboliphaunt` without +assuming the current direct-mode shape is the final answer. + +## Confidence Reframe + +The iOS product should not depend on a speculative broker to feel reliable. +The reliable, shippable baseline is: + +- crash-consistent storage through PostgreSQL WAL; +- a resident in-process engine with one root and one physical session; +- explicit lifecycle APIs for backgrounding, cancellation, checkpoint, backup, + and logical detach; +- test evidence that app relaunch after process death reopens the same root + cleanly. + +That is not crash isolation. It is crash consistency. If native PostgreSQL +crashes in direct mode, the host app process dies. The next app launch should +recover the database by WAL replay and reopen the same root. That is the honest +guarantee we can make on every supported iOS version. + +Crash isolation is a different product capability. On iOS it requires a proven +separate process. Today the only credible Apple-supported path is +`ExtensionFoundation.AppExtensionProcess` on iOS 26+. That should be built as a +separate, availability-gated `NativeExtensionBroker` mode. If it fails the +feasibility gate, the product answer is not to fake isolation in direct mode; +the answer is that iOS supports fast embedded direct mode, and crash-contained +embedded PostgreSQL is unavailable on that OS/configuration. + +## Hard Constraints + +### iOS Does Not Have A General Helper Daemon Model + +The macOS-style "app plus bundled XPC service" model is not generally available +to iOS apps. The iPhoneOS SDK marks `NSXPCConnection.init(serviceName:)` and +Mach-service creation unavailable on iOS, and the Foundation `Process`/`NSTask` +API is not present in the iPhoneOS Foundation headers. The App Store rules also +require apps to stay self-contained and not download, install, or execute code +that changes app functionality after review. + +Implication: a robust iOS database process cannot be designed as a normal +spawned helper, launch agent, or app-owned daemon. + +### ExtensionFoundation Is Real, But It Is Not A Normal Daemon + +Starting in the current iOS SDK, `ExtensionFoundation.AppExtensionProcess` is +available on iOS 26+. It creates or attaches to a separate app-extension +process, exposes `makeXPCConnection()` and `makeXPCSession()`, and reports +unexpected extension termination through an interruption handler. + +This is the first credible Apple-supported path to iOS process isolation for +`liboliphaunt`. It still has limits: + +- it requires iOS 26+; +- it runs an app extension, not an arbitrary service; +- the host must keep a strong `AppExtensionProcess` reference; +- the system can suspend an extension if no XPC connection is established; +- an app extension is not a long-running background worker; +- app and extension storage must be explicit, usually through an App Group; +- one extension identity appears to map to one running process, so it should + not be treated as an unlimited worker pool. + +Implication: an iOS broker is possible enough to deserve a serious spike, but +it must be modeled as a constrained extension-process broker, not as the same +broker implementation we can ship on macOS or Android. + +### Background Execution Is Finite + +UIKit gives apps a short background window and a finite extension mechanism via +`beginBackgroundTask`. BackgroundTasks can relaunch the app for scheduled work, +but not as an interactive database server. App extensions cannot call +`UIApplication` and must use extension-safe background mechanisms. + +Implication: iOS cannot promise an always-on local PostgreSQL service. The SDK +should make foreground database calls fast, finish or cancel foreground work +cleanly when the app backgrounds, and use scheduled background work only for +maintenance such as checkpoint, vacuum policy, sync, or backups. + +### PostgreSQL Wants A Process Boundary For Crash Recovery + +Normal PostgreSQL robustness comes from a supervisor process, child backend +processes, shared-memory reinitialization after abnormal child death, and WAL +replay after immediate shutdown or crash. Our embedded direct path starts a +standalone PostgreSQL backend inside the app process and routes FE/BE protocol +I/O through host callbacks. + +The direct path can recover from PostgreSQL `ERROR`, protocol errors, +cancellation, and many controlled `proc_exit` paths. It cannot recover from a +native crash, abort, memory corruption, or process-wide PostgreSQL global state +poisoning without taking down the app process. Full in-process close/reopen +across arbitrary roots is not a sound product promise until PostgreSQL can be +proven re-entrant under our patches. + +Implication: direct mode can be SQLite-like in latency and embedding, but not in +crash containment. Only a separate iOS extension process can make PostgreSQL +death survivable for the host app. + +## Brittle Assumptions To Remove + +The architecture should explicitly reject these assumptions: + +- `close()` means full PostgreSQL shutdown. In mobile direct mode it does not; + it is logical detach from a resident process-wide runtime. +- `reopenable` is a single capability. Same-root logical reopen, root switching, + and crash restart are separate properties. +- iOS broker means desktop broker. iOS process isolation is extension-process + isolation with lifecycle and OS-version limits. +- iOS server mode can be emulated with a loopback listener. That gives a + connection-shaped API without PostgreSQL's real process semantics. +- background execution can keep an interactive database alive indefinitely. It + cannot. +- WASIX makes iOS direct mode safe. A same-process Wasm runtime may improve + portability or memory sandboxing, but it does not provide host-app survival + after database runtime failure. + +Removing these assumptions makes the architecture less magical and more +defensible. + +## Candidate Architectures + +| Candidate | Strengths | Failure Modes | Verdict | +| --- | --- | --- | --- | +| In-process `NativeDirect` | Lowest latency, App Store viable on all supported iOS versions, simplest Swift/RN DX, static extensions work | App crashes if native PostgreSQL crashes; one resident root/session; logical close only | Ship as universal fast path, with honest capabilities | +| In-process multi-session | Looks like server semantics without IPC | PostgreSQL globals, shared memory, signals, session state, temp objects, GUCs, transactions; no crash isolation | Reject unless upstream PostgreSQL grows a real embeddable multi-session runtime | +| In-process loopback server | Familiar connection string shape | No process isolation; true multi-client sessions still require PostgreSQL's process model; background behavior is misleading | Do not ship on iOS | +| Spawned helper/XPC service | Would solve crash isolation and restart | Not generally available to iOS apps | Reject for App Store iOS | +| ExtensionFoundation broker | Separate process, XPC, interruption handling, App Store-shaped packaging path on iOS 26+ | New OS floor, extension lifecycle limits, likely one process per extension identity, App Group storage, unknown memory/throughput ceilings | Best robust iOS direction; spike and gate before promising | +| System extension / Network Extension / File Provider abuse | Separate process in some cases | Wrong extension point, entitlement/review risk, user-visible policy mismatch | Reject | +| BackgroundTasks broker | System-supported background launch | Scheduled, finite, non-interactive; not request/response | Use only for maintenance | +| WASIX/Wasm engine | Sandboxed memory model and legacy portability | Same host process, likely lower perf, no native extension story, does not create iOS process isolation | Keep as legacy/compatibility, not best iOS default | +| Remote/cloud broker | Strong isolation | Not embedded/offline, not SQLite competitor | Out of scope for the embedded product | + +## Recommended iOS Product Shape + +### 1. Universal Fast Path: `NativeDirect` + +`NativeDirect` should remain the default iOS runtime for broad OS support and +low latency. It should be marketed as an embedded single-session PostgreSQL +runtime, not as a local server. + +The API should present this as an app-scope resident database, not a disposable +object that happens to keep native state behind the scenes. + +Contract: + +- one resident PostgreSQL backend per app process; +- one physical session; +- one database root per process lifetime; +- many Swift/RN callers may enqueue work, but execution is serialized; +- transaction APIs pin the physical session and reject unpinned work; +- `close()` is logical detach, not full PostgreSQL shutdown; +- same-root logical reopen is supported; +- root switching requires a fresh process; +- native crashes terminate the host app; +- WAL recovery happens after the app relaunches and opens the same root. + +DX requirements: + +- provide an app-scope `OliphauntContainer` or `OliphauntResidentDatabase` manager; +- open once per app process and reuse stable handles; +- make `close()`/`detach()` release the logical SDK handle only; +- provide `destroyRoot(...)` only when the root is not resident in the current + process; +- provide `fullShutdown` as unsupported in iOS direct mode rather than a best + effort; +- serialize all work through a fair owner queue; +- expose query cancellation and statement timeouts; +- make transaction helpers pin the physical session and reject unpinned work; +- expose `prepareForBackground(deadline:)` and `resumeFromBackground()`; +- default Swift/RN integrations should register lifecycle hooks when the app + framework is present, while still allowing manual control. + +This turns the current "close is special" behavior into the public model rather +than a surprising implementation detail. + +### 2. Robust Path: `NativeExtensionBroker` On iOS 26+ + +The best process-isolated iOS architecture is a bundle-only custom app extension +owned by the app and launched through `ExtensionFoundation.AppExtensionProcess`. +The extension owns `liboliphaunt`, the PostgreSQL runtime, and the selected static +extensions. The host app talks to it over XPC/XPCSession. + +Target contract: + +- host app defines a bundle-only extension point for `liboliphaunt`; +- broker extension links the same `liboliphaunt` C ABI and selected extension + objects; +- PGDATA and runtime resources live in an App Group container; +- XPC messages carry control requests and raw protocol chunks; +- the broker serializes one physical PostgreSQL session per opened root; +- host observes `onInterruption`/XPC invalidation and marks in-flight requests + as failed with unknown transaction outcome; +- reconnect starts a fresh extension process and reopens the same root after + WAL recovery; +- no automatic replay of writes unless the user opts into an idempotent request + envelope; +- capability reporting says `processIsolated=true`, + `crashRestartable=true`, and `independentSessions=false`. + +Important limit: this should not advertise `multiRoot=true` until proven. If +one extension identity maps to one running process, and `liboliphaunt` embeds one +process-wide PostgreSQL runtime, then iOS broker v1 is still a single-root +process-isolated runtime. A future multi-root design would need either a +defensible worker-slot model with multiple bundled extension identities, or a +much deeper PostgreSQL re-entrancy breakthrough. + +This mode should fail closed: + +- unavailable below iOS 26; +- unavailable without the broker extension target; +- unavailable without the required App Group; +- unavailable when extension lifecycle or XPC throughput evidence is missing; +- unavailable for multi-root unless worker multiplicity is proven. + +It should be named and documented as iOS process-isolated mode, not as generic +desktop broker parity. + +### 3. Server Mode: Explicitly Unavailable On iOS + +`NativeServer` should not be faked on iOS. A same-process loopback listener does +not provide independent sessions or crash isolation, and a real PostgreSQL +postmaster-style process tree is not an iOS app model. If a future +ExtensionFoundation broker can safely host a compatibility socket for one client, +that should be named as compatibility, not as true server mode. + +## Lifecycle Policy + +The SDK should provide explicit lifecycle APIs instead of hoping app authors +guess correctly: + +- `prepareForBackground(deadline:)`: stop accepting new work, cancel or allow + bounded active work, optionally `CHECKPOINT` if idle, then return before the + system deadline. +- `resumeFromBackground()`: verify the session still responds; if broker mode + was interrupted, reconnect and reopen. +- `cancel()`: interrupt the current PostgreSQL statement and surface a normal + PostgreSQL cancellation error when possible. +- `checkpoint()`: explicit durability boundary for apps about to background. +- crash drill helpers for tests: kill broker extension, kill host process, and + reopen root to prove WAL recovery. + +For direct mode, background handling improves data durability and UX but cannot +make native PostgreSQL crash-isolated. For broker mode, background handling must +also account for the extension being suspended or killed independently. + +The mobile SDK should treat lifecycle as part of the database API: + +- every long-running call accepts cancellation; +- every queued call has a bounded wait/cancellation path; +- background transition blocks new work before attempting checkpoint/cancel; +- foreground transition verifies the live session with a cheap query or protocol + sync before accepting normal traffic; +- memory warning handling can recommend `DISCARD ALL`, checkpoint, or app-level + query cancellation, but must not pretend to free PostgreSQL's process-wide + runtime in direct mode. + +## Extension Policy + +iOS extension support should stay static and opt-in: + +- selected extensions are compiled into the app and, for broker mode, into + the broker extension target; +- SQL/control/share assets are packaged as resources and copied/materialized by + the SDK; +- `CREATE EXTENSION` succeeds only when the selected extension is present and its + static registry is ready; +- dynamic extension loading is not the portable iOS path; +- downloading executable extension code after review is not allowed. + +This is the right tradeoff for iOS package size, App Store review, and +predictable crash/debug symbols. + +## Capability Vocabulary Needed + +The current broad `reopenable` bit is too coarse. iOS needs these distinct +capability fields across Swift, Kotlin, Rust, and React Native: + +- `sameRootLogicalReopen` +- `rootSwitchable` +- `crashRestartable` +- `processIsolated` +- `independentSessions` +- `maxClientSessions` +- `multiRoot` +- `backgroundContinuable` +- `requiresAppGroup` +- `minimumOS` + +This avoids selling direct mode as more recoverable than it is, and avoids +selling an iOS extension broker as a full desktop broker before worker +multiplicity is proven. + +Example direct-mode capabilities: + +```json +{ + "engine": "nativeDirect", + "processIsolated": false, + "sameRootLogicalReopen": true, + "rootSwitchable": false, + "crashRestartable": false, + "independentSessions": false, + "maxClientSessions": 1, + "backgroundContinuable": false +} +``` + +Example extension-broker capabilities after the feasibility gate passes: + +```json +{ + "engine": "nativeExtensionBroker", + "processIsolated": true, + "sameRootLogicalReopen": true, + "rootSwitchable": false, + "crashRestartable": true, + "independentSessions": false, + "maxClientSessions": 1, + "backgroundContinuable": false, + "requiresAppGroup": true, + "minimumOS": "iOS 26" +} +``` + +The broker should not report `rootSwitchable=true` or `multiRoot=true` until a +real multi-worker model is proven. + +## Direct-Mode Confidence Gate + +Direct mode is shippable on iOS only when these tests pass on simulator and real +devices: + +1. Open the same persistent root repeatedly through the app-scope manager and + prove all logical handles share the resident runtime. +2. Reject opening a different root in the same process with a precise error. +3. Run concurrent Swift tasks/RN promises and prove fair serialization, + transaction pinning, cancellation, and close/detach behavior. +4. Enter background during idle, during a read, during a write transaction, and + during a long-running query; prove the lifecycle policy either finishes + within deadline or cancels cleanly. +5. Kill the app process after committed writes, after uncommitted writes, and + during WAL activity; relaunch and prove PostgreSQL recovery returns a + consistent database. +6. Inject PostgreSQL `ERROR`, malformed protocol, cancellation, and controlled + `proc_exit`; prove the host surfaces errors without corrupting the session + when PostgreSQL allows recovery. +7. Inject an actual native crash in the backend thread; document that direct + mode crashes the host app, then prove app relaunch recovers the root. +8. Verify selected static extensions, backup/restore, memory warning + handling, and package-size reporting. + +This gate does not claim direct mode is crash-isolated. It proves the direct +mode guarantee: fast, single-session, crash-consistent embedded PostgreSQL. + +## Feasibility Gate For The iOS Broker + +Do not productize `NativeExtensionBroker` until these pass on real devices: + +1. Build an iOS 26+ app with a bundle-only custom ExtensionFoundation extension + point and a broker extension target. +2. Start the extension, establish XPCSession or NSXPCConnection, and roundtrip a + 1 MB binary payload without JS/UI blocking. +3. Link `liboliphaunt` into the extension, open a packaged-template PGDATA root in + an App Group container, run `SELECT 1`, close, and reopen. +4. Kill the extension process during idle and during a transaction; host app + must stay alive, report unknown transaction state, reconnect, and pass WAL + recovery. +5. Background and foreground during an active query; the SDK must either finish + within the deadline or cancel cleanly. +6. Package selected static extensions in the extension and verify + `CREATE EXTENSION vector` and `CREATE EXTENSION graph` when selected. +7. Measure XPC raw protocol RTT, streaming throughput, memory, app IPA size, and + extension memory ceiling against direct mode. +8. Verify TestFlight/App Store review viability with the extension declared as a + private bundle-only app extension and no downloaded executable code. + +## Decision + +The strongest iOS architecture is a two-tier product: + +- `NativeDirect` is the universal, fastest, SQLite-competitive embedded mode. + It must be honest about single-root, single-session, logical close, and lack + of crash isolation. +- `NativeExtensionBroker` is the best robust iOS mode for iOS 26+ if the + feasibility gate passes. It gives the host app a recoverable process boundary, + but should start as single-root and single-session rather than pretending to + be a desktop broker. + +WASIX does not solve the iOS stability problem unless it runs out of process, +and it is unlikely to be the performance-default path for competing with SQLite. +It remains useful as compatibility and cross-platform fallback, not as the ideal +iOS architecture. + +## References + +- Apple ExtensionFoundation `AppExtensionProcess`: + https://developer.apple.com/documentation/ExtensionFoundation/AppExtensionProcess +- Apple custom app-extension support: + https://developer.apple.com/documentation/extensionfoundation/adding-support-for-app-extensions-to-your-app +- Apple app extensions overview: + https://developer.apple.com/documentation/technologyoverviews/app-extensions +- Apple background execution: + https://developer.apple.com/documentation/uikit/extending-your-app-s-background-execution-time +- Apple App Store Review Guidelines: + https://developer.apple.com/app-store/review/guidelines +- PostgreSQL server shutdown and WAL recovery: + https://www.postgresql.org/docs/current/server-shutdown.html +- PostgreSQL `CREATE EXTENSION`: + https://www.postgresql.org/docs/current/sql-createextension.html diff --git a/docs/architecture/native-liboliphaunt.md b/docs/architecture/native-liboliphaunt.md new file mode 100644 index 00000000..ecbd7331 --- /dev/null +++ b/docs/architecture/native-liboliphaunt.md @@ -0,0 +1,556 @@ +# Native liboliphaunt PG18 Path + +Status: archived implementation investigation; non-normative. Paths, gaps, +benchmarks, and readiness statements below describe an earlier native-track +checkpoint. Use `docs/architecture/final-product-source-architecture.md`, +`docs/maintainers/README.md`, and executable product/target metadata for the +current source and release model. + +The native product is split into two repo boundaries: + +- `oliphaunt/` owns the C ABI, PostgreSQL 18 source pin, patch stack, build + scripts, C smoke harness, and runtime header. +- `src/sdks/rust/` owns the Rust SDK shape over that C ABI. + +The existing `oliphaunt-wasix` crate and WASIX release lane remain in place while +the native path is built out separately. The WASIX crate does not select or load +native `liboliphaunt`; Rust native behavior is owned by `oliphaunt`. + +## Source And Patch Stack + +The C lane is pinned in: + +```text +src/runtimes/liboliphaunt/native/postgres18/source.toml +``` + +It currently targets PostgreSQL `18.4` and applies the patch stack in +`src/runtimes/liboliphaunt/native/patches/postgresql-18.4`. + +External PG18 extension candidates are pinned separately in: + +```text +src/runtimes/liboliphaunt/native/postgres18/external-extensions.toml +``` + +That manifest is an internal research input, not a public SDK extension catalog. The +native validation wrapper runs +`src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh` without network access, +which verifies the manifest shape and any local checkout that exists. Use the +script's `--online` mode only when deliberately refreshing those pins against +upstream. + +The entrypoint does not use `postgres --single`. It adds a dedicated embedded +backend entrypoint, routes libpq backend reads/writes through host-owned I/O +callbacks, and runs PostgreSQL's normal exit callbacks without calling +`exit(3)`. + +## Build + +Build the macOS happy-path dylib with: + +```sh +src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +``` + +The script emits: + +```text +target/liboliphaunt-pg18/out/liboliphaunt.dylib +target/liboliphaunt-pg18/install/bin/initdb +target/liboliphaunt-pg18/install/bin/postgres +``` + +`ccache` is used automatically when available. Set `OLIPHAUNT_CCACHE=off` to +disable it, or set `OLIPHAUNT_CCACHE=/path/to/ccache` to force a specific +binary. + +The dylib build is also stamped. The script hashes the edited `liboliphaunt` +headers/sources and fingerprints the PostgreSQL embedded object/archive inputs; +if the stamp still matches and the dylib exports the required C ABI symbols, it +prints `reusing native liboliphaunt dylib` and skips the C object compile plus +dylib relink. Harnesses call `src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +--check-oliphaunt-current` in no-build mode so stale C ABI sources fail fast +instead of producing false-green native evidence. Set +`OLIPHAUNT_FORCE_RELINK=1` to force that relink. + +Extension builds are opt-in and intentionally cacheable. The default direct +build is core-only for fast C ABI iteration. When +`OLIPHAUNT_BUILD_EXTENSIONS=1` is set, the build script fingerprints the +extension source trees, compiler selection, PostgreSQL patch/build inputs, and +`liboliphaunt` C ABI sources. If the fingerprint and required normal/embedded +artifacts are still valid, the script reuses the extension artifacts instead of +running the expensive clean/rebuild loop again. Set +`OLIPHAUNT_FORCE_EXTENSION_REBUILD=1` to force a full extension rebuild. +Harnesses can call `src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +--check-extension-artifacts-current` to prove the same fingerprint and artifact +readiness without downloading, extracting, configuring, compiling, or relinking. + +External pgrx candidates have their own opt-in harness: + +```sh +src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh --fetch +src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh +``` + +The fetch boundary is transactional: each immutable commit is shallow-fetched +over HTTPS into a bounded unique sibling stage, verified with strict Git object +checks and a clean detached `HEAD`, then promoted by rename. Transient failures +have a small attempt limit and linear backoff; every retry initializes a fresh +stage and requests the commit directly rather than a mutable ref. Dirty durable +checkouts are never fetched into or replaced, and interruption or promotion +failure restores the previous clean checkout before temporary state is removed. + +It reads the same source-pin manifest, uses the manifest-pinned `cargo-pgrx` +major/minor, builds the crate subdirectory recorded by the manifest as a normal +PostgreSQL package with the native PG18 `pg_config`, then rebuilds with linker +flags that bind the module to `@rpath/liboliphaunt.dylib` for direct/broker +embedded loading. It writes per-extension input stamps and exposes +`src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh --check-current`, which +is the no-build gate used by `src/runtimes/liboliphaunt/native/tools/check-track.sh +external-pgrx`. The build lane is explicit and disk-guarded because candidate +extensions can have very different compile and artifact-size profiles. A repo-local +`target/liboliphaunt-tools/bin/cargo-pgrx` is discovered automatically. The +external-pgrx fingerprint tracks build-affecting inputs rather than the whole +harness file, so comments and DX-only script edits do not stale heavy extension +artifacts. `--refresh-current-stamps` validates existing normal and embedded +payloads, then rewrites the input stamps without repackaging. + +## C ABI Contract + +The canonical header is: + +```text +src/runtimes/liboliphaunt/native/include/oliphaunt.h +``` + +The current ABI is intentionally small: + +- `oliphaunt_init` +- `oliphaunt_init_ex` +- `oliphaunt_exec_protocol` +- `oliphaunt_exec_simple_query` +- `oliphaunt_exec_protocol_stream` +- `oliphaunt_cancel` +- `oliphaunt_close` +- `oliphaunt_register_static_extensions` +- `oliphaunt_last_error` +- `oliphaunt_version` +- `oliphaunt_capabilities` +- `oliphaunt_free_response` + +`oliphaunt_exec_protocol` accepts frontend PostgreSQL protocol frames after +`oliphaunt_init` has initialized the embedded backend session. Responses are owned +by the native library until `oliphaunt_free_response`. + +`oliphaunt_init_ex` extends initialization without changing the shared ABI v6 +record. Its independently versioned `OliphauntInitOptions` can provide a +per-handle `module_dir`. A non-empty value must be an existing directory, is +copied during initialization, and is authoritative for PostgreSQL `$libdir`. +This is how the Rust SDK binds an embedded handle to the exact selected-module +tree in its materialized runtime cache without mutating process environment +before the call. Existing callers continue to use `oliphaunt_init`: a valid +`OLIPHAUNT_EMBEDDED_MODULE_DIR` remains higher priority than release-layout +discovery, so JavaScript and direct C hosts keep their established behavior. + +Direct mode sets the process `PGDATA` environment variable to the active +`config.pgdata` for the backend lifetime so PostgreSQL extensions that consult +standard process state resolve files inside the selected root. `oliphaunt_close` +restores the caller's previous value, or unsets `PGDATA` if it was unset before +open. Broker and server modes provide stronger process isolation for apps that +cannot tolerate a process-wide environment mutation. + +Desktop dynamic extension modules also require PostgreSQL backend symbols from +the loaded `liboliphaunt` image. `oliphaunt_init_ex` therefore promotes that +exact image to `RTLD_NOW | RTLD_GLOBAL` before backend startup; explicit Rust, +Node, Swift, and Kotlin loaders request the same scope as defense in depth. The +central promotion is required for FFI hosts such as Bun and Deno that do not +offer loader-flag control. The promoted reference is retained for process +lifetime. A second embedded PostgreSQL image and caller-created `dlmopen` +namespaces are outside the direct-mode contract; process isolation belongs in +broker or server mode. + +`oliphaunt_register_static_extensions` is the direct/mobile extension module +loader boundary. A process that links extension code statically calls it before +`oliphaunt_init` with module stems, PostgreSQL magic functions, optional `_PG_init` +callbacks, and exported C symbols. The PostgreSQL `dfmgr` patch then resolves +those entries through the normal `CREATE EXTENSION`/`LOAD` path instead of +calling `dlopen`/`dlsym`. The registry is process-wide, rejects malformed or +duplicate entries, and freezes at first backend startup. `oliphaunt_capabilities` +advertises this as `OLIPHAUNT_CAP_STATIC_EXTENSIONS`. + +The Rust runtime resources now emit the portable platform handoff for this: +`oliphaunt/static-registry/manifest.properties` and, for mobile-ready packages, +`oliphaunt/static-registry/oliphaunt_static_registry.c`. That generated source +exports `liboliphaunt_selected_static_extensions(size_t *count)`. Swift, Kotlin, +and React Native native bridges discover that optional symbol through the +process image and register its rows through the loaded `liboliphaunt` +`oliphaunt_register_static_extensions` symbol before `oliphaunt_init`. This keeps the +generated registry source independent of whether a platform links liboliphaunt +statically or loads it dynamically. + +`oliphaunt_exec_simple_query` executes one SQL buffer through PostgreSQL's +simple-query protocol without requiring SDKs to allocate a frontend frame first. +This is a convenience and performance ABI for the common direct `execute(sql)` +path; raw protocol remains the cross-language compatibility boundary. + +`oliphaunt_exec_protocol_stream` executes the same request shape but delivers +backend bytes to a callback as chunks. The C runtime scans backend frames +incrementally and completes the call when it observes `ReadyForQuery`. Streamed +backend chunks use a bounded in-process queue with producer backpressure so a +slow callback cannot turn a large result or `COPY` stream into unbounded RSS +growth. The default queue budget is 4 MiB and can be overridden for diagnostics +with `OLIPHAUNT_STREAM_QUEUE_MAX_BYTES`. + +Protocol execution does not impose a default query timeout. Startup readiness +uses `OLIPHAUNT_STARTUP_TIMEOUT_MS` with the legacy `OLIPHAUNT_TIMEOUT_MS` +fallback, but long-running SQL must run until PostgreSQL completes or the owner +explicitly calls `oliphaunt_cancel`. Ordinary close waits for active SQL and then +detaches/closes the owning SDK handle. + +`oliphaunt_cancel` requests cancellation of the active embedded backend query +out-of-band. It maps to PostgreSQL's normal interrupt path by setting +`InterruptPending` and `QueryCancelPending`, waking `MyLatch` when available, +and waking the host I/O condition variables. It is advertised through +`OLIPHAUNT_CAP_QUERY_CANCEL`; the Rust SDK maps broker and server cancellation onto +their own transport-native mechanisms. + +Bootstrap no longer shells through `system(3)`: the C runtime forks and execs +`initdb` directly when a PGDATA root has no `PG_VERSION`. The Rust SDK now uses +the production bootstrap path first: `BootstrapStrategy::PackagedTemplate` +hydrates new roots from a cached base PGDATA template before entering +`oliphaunt_init`, so direct mode does not pay `initdb` on every fresh open. +`initdb` remains the explicit tooling fallback. + +Native v1 is one active embedded PostgreSQL backend per process. The product +path keeps this honest with a process-wide guard; robust multi-root app behavior +belongs in broker mode rather than fake direct-mode multiplexing. Every live +root is protected by explicit root ownership, so direct, broker-helper, server, +backup, and restore fail fast instead of racing on the same PGDATA directory. +Plain C ABI callers get a default stable sibling filesystem lease from +`oliphaunt_init` for ``, plus +`/.oliphaunt.lock` as the visible root marker. C +`oliphaunt_restore` takes the same stable lease before staging or publishing a +restored root, so restore cannot replace a root currently owned by the direct +C ABI. Stable lease filenames live beside the root directory and use the shared +`.oliphaunt-root-.lock` algorithm used by the Rust SDK, so C, Rust, +Swift, Kotlin, and React Native platform adapters contend on the same root +identity. SDKs that own a broader root coordinator set +`OLIPHAUNT_CONFIG_EXTERNAL_ROOT_LOCK`; the Rust SDK does this because its +coordinator uses a same-process canonical root registry plus stable filesystem +leases across direct, broker, server, backup, and restore paths. The stable +leases are keyed by the canonical root path and remain held while restore +replacement moves the old root aside and publishes the validated new root. +Restore/import uses stable path reservation before publish, which means a +missing or intentionally empty target stays empty until the validated archive is +ready to publish. + +## Rust SDK + +Point the Rust SDK at the build outputs: + +```sh +export LIBOLIPHAUNT_PATH="$PWD/target/liboliphaunt-pg18/out/liboliphaunt.dylib" +export OLIPHAUNT_INSTALL_DIR="$PWD/target/liboliphaunt-pg18/install" +``` + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn demo() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .open() + .await?; + +let result = db.query("SELECT 1::text AS value").await?; +assert_eq!(result.get_text(0, "value")?, Some("1")); + +let parameterized = db + .query_params( + "SELECT ($1::int4 + $2::int4)::text AS sum", + [1_i32, 41_i32], + ) + .await?; +assert_eq!(parameterized.get_text(0, "sum")?, Some("42")); + +db.execute("CREATE TABLE items(id bigint PRIMARY KEY)").await?; + +let tx = db.transaction().await?; +tx.query_params("INSERT INTO items VALUES ($1)", [1_i64]).await?; +tx.commit().await?; + +db.close().await?; +# Ok(()) +# } +``` + +Without `LIBOLIPHAUNT_PATH`, the native runtime returns a clear startup +error. The old `oliphaunt-wasix` crate is still the WASIX-oriented release lane. +`NativeBroker` and `NativeServer` are selected from the same builder through +`.native_broker()` and `.native_server()`. +Broker mode starts the helper with the same storage bootstrap policy selected on +the builder. In particular, `.existing_only()` is enforced by the helper and +will not silently create a new root. + +The `OliphauntRuntime` implementation is intentionally split by responsibility: + +- `src/sdks/rust/src/runtimes/liboliphaunt/native/mod.rs`: runtime/session behavior. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/ffi.rs`: C ABI structs, symbols, and + library resolution. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root.rs`: process/file root locks and + PGDATA path preparation. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/runtime.rs`: runtime cache + orchestration. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/runtime/locate.rs`: native install + and embedded-module discovery. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/runtime/install.rs`: selected + runtime asset installation. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/runtime/cache_key.rs`: runtime + cache manifest, key, and validation logic. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/template.rs`: packaged-template + PGDATA bootstrap and root hydration. +- `src/sdks/rust/src/runtimes/liboliphaunt/native/root/extensions.rs`: selected extension + SQL/data/module materialization policy. + +Runtime materialization is profile-aware. Direct/broker use liboliphaunt-linked +extension modules because the embedded backend resolves modules inside the +`liboliphaunt` process. Server mode uses standalone PostgreSQL extension modules +from the install tree. The filtered `share/postgresql` tree is still manifest +gated, so a symlink back to the full install tree is not used because it would +make unselected extensions visible. + +The materialized runtime and base PGDATA template are content-keyed under +`$TMPDIR/oliphaunt-runtime-cache` by default. Override with +`OLIPHAUNT_RUNTIME_CACHE_DIR` when benchmarking, testing cache invalidation, +or packaging a controlled runtime cache location. Template hydration defaults to +physical byte-copy because current native matrix evidence shows better p90 +stability than APFS clone-on-write on the benchmark host. Set +`OLIPHAUNT_PGDATA_COPY_MODE=prefer-clone` for diagnostics that need to +compare clone-on-write hydration explicitly. + +`NativeBroker` uses the same direct C ABI inside a helper process. On Unix +platforms the Rust SDK connects to that helper over a per-session Unix-domain +socket in `/tmp`; TCP loopback remains available by setting +`OLIPHAUNT_BROKER_TRANSPORT=tcp`. Every broker session uses a generated +per-process authentication token passed to the helper through its environment +and verified as the first IPC frame, so an unrelated local client cannot drive a +fresh helper merely by finding the socket or TCP port. Broker sessions retain +their launch plan and relaunch the helper against the same root when the helper +has exited between operations. In-flight requests are not automatically replayed +after a crash because their commit state may be unknown; the caller sees that +error, and subsequent operations can recover through normal PostgreSQL WAL +recovery. + +Direct/server physical backup uses PostgreSQL's low-level online backup API: +`pg_backup_start`, archive `pgdata`, then `pg_backup_stop(wait_for_archive => +false)` to obtain and write `backup_label` and `tablespace_map` into the +archive. `pg_wal` is collected after backup stop so the archive has the WAL +needed for same-version recovery. Broker forwards the same physical backup from +its helper process. The physical archive format is a concrete single-root +archive: restore accepts only regular files and directories under `pgdata`, and +backup fails if PGDATA contains anything else. Symlinks, hardlinks, FIFOs, +sockets, device nodes, sparse/special tar records, external tablespaces, and +linked WAL directories are rejected rather than silently producing a +non-portable archive. Server mode also supports `BackupRequest::sql()` through +packaged `pg_dump`. + +Same-version physical restore is a first-class Rust SDK operation: +`Oliphaunt::restore(RestoreRequest::physical_archive(path, artifact))`. The SDK +does not ask callers to unpack tar archives manually. It stages restore output +next to the target root, rejects archive path traversal, duplicate canonical +paths, malformed tar framing, unsupported tar header formats, invalid tar +numeric and fixed-width string fields, unexpected link metadata, directory +entries with payload bytes, and unsupported archive entry types. Restore writes +only to the validated canonical archive path for each entry, validates the +archive tree shape before writing staging files, validates `PG_VERSION`, +`global/pg_control`, and `backup_label`, then publishes the root atomically. +The C ABI performs the same pre-extraction archive validation, so Swift, +Kotlin, and React Native platform adapters inherit the lower-level restore +contract rather than relying on Rust-only checks. +Existing roots are protected +by default and can be replaced only with `replace_existing()`, which first takes +the root lock. Existing symlink targets are rejected because restore publishes +with directory renames at the target path; callers should pass the real database +root path explicitly. + +## Smoke Tests + +Use the C smoke as the fastest C ABI harness: + +```sh +src/runtimes/liboliphaunt/native/bin/smoke-host-happy-path.sh +``` + +It compiles the host C harness, opens a database, sends raw protocol bytes for +`SELECT 1 AS value`, verifies ABI version/capability reporting, invalid init and +invalid exec argument errors, malformed frontend frame rejection and recovery, +SQL-error recovery with a second successful query, large owned-response growth, +streaming callback delivery, stream-callback failure recovery, active-query +cancellation and recovery, idempotent response cleanup, static extension +registration and PostgreSQL symbol resolution through `CREATE FUNCTION ... AS +'module', 'symbol'`, C ABI backup/restore, restore rejection for malformed tar +metadata and unsupported archive entries, backup rejection for symlinked PGDATA +entries, `PGDATA` pointing at the live root while the direct backend is active, +and restoration of the caller's `PGDATA` environment after direct backend +shutdown. It then closes cleanly and asserts that same-process direct reopen is +rejected with the documented process-lifetime error. The shell harness launches +the binary again against the same PGDATA root to verify persistence/reopen +across process boundaries. + +Use the Rust SDK shape test for the separate package: + +```sh +LIBOLIPHAUNT_PATH="$PWD/target/liboliphaunt-pg18/out/liboliphaunt.dylib" \ +OLIPHAUNT_INSTALL_DIR="$PWD/target/liboliphaunt-pg18/install" \ +cargo test -p oliphaunt --test sdk_shape -- --nocapture +``` + +The env-gated Rust SQL regression test exercises the same native runtime modes +for broader SQL behavior. It includes client-driven `COPY FROM STDIN` through +raw protocol frames, validates `CopyInResponse`, `CommandComplete`, and +`ReadyForQuery`, verifies the inserted payloads, and then runs a normal query to +prove post-COPY session reuse. It also drives invalid COPY input and an explicit +frontend `CopyFail`, verifies `ErrorResponse` plus `ReadyForQuery`, proves no +rows were committed, and runs follow-up queries to catch stuck COPY state. It +also includes `COPY TO STDOUT` streaming through `exec_protocol_raw_stream`, +validates `CopyOutResponse`, `CopyData`, `CommandComplete`, and `ReadyForQuery`, +verifies the expected streamed line and payload counts, and again proves the +session can execute normal queries after COPY: + +```sh +OLIPHAUNT_TRACK_BUILD=never src/runtimes/liboliphaunt/native/tools/check-track.sh quick +``` + +## Implementation Snapshot Gaps + +- One active direct backend per process; use broker/server for process-isolated + lifecycles and multi-root app designs. +- `NativeDirect`, `NativeBroker`, and `NativeServer` expose out-of-band query + cancellation. Direct maps to `oliphaunt_cancel`, broker uses a separate + authenticated cancel IPC endpoint, and server sends PostgreSQL's native + CancelRequest packet with the startup `BackendKeyData`. +- Rust `Oliphaunt::close()` rejects queued non-close work with `EngineStopped` + once close begins, waits for the active SDK-owned operation to finish, and + then closes or logically detaches the runtime. Interruption is explicit + through `Oliphaunt::cancel()`. +- Direct native protocol streaming is implemented in the C ABI. Broker mode + forwards native chunks over IPC. Server mode forwards complete PostgreSQL wire + frames as it reads them from the local server connection. Direct streaming + applies C-level producer backpressure with a bounded chunk queue. +- Extensions are materialized for selected PG18-supported extensions, and + `NATIVE_EXTENSION_MANIFEST` records SQL/control assets, native module + requirements, data files, smoke SQL strategy, coverage evidence, mobile + static-link status, and first-party/external packaging policy for every + supported row. `Extension::RELEASE_READY_PG18_SUPPORTED` is the public exact + extension catalog; custom static manifests are restricted to release-ready + first-party extensions. The external pgrx lane remains internal/deferred and + must not be surfaced as shippable SDK extensions until licensing, static + mobile linkage, and lifecycle evidence are complete. Required preload hooks + are derived from selected extensions, so extensions that need preload can add + `shared_preload_libraries` to direct, broker, and server startup from manifest + data instead of app code. Resource + packages now record package-level mobile + static-registry readiness and the runtime-resource generator can fail iOS/Android release builds + with `--require-mobile-static-registry` when selected module-backed extensions + are still pending. The low-level C ABI registry now exists through + `oliphaunt_register_static_extensions`; the Rust runtime-resource generator now generates the + platform registry source for complete mobile packages, and platform packages + still have to link the selected extension objects with the expected renamed + magic/init symbols before marking a package complete. Platform package builds + that actually link static extension + registry rows can declare exact module stems with `--mobile-static-module`; + unknown or unselected stems are rejected. Kotlin and React Native Android + split-resource packaging deliberately cannot declare those stems complete, + because that path cannot generate or verify the static-registry source; use + the Rust runtime resources for mobile release extension artifacts. Generated + packages record the exact selected extensions, dependency-expanded runtime + manifest, static-registry state, and per-extension size evidence, so + app-bundled resources are auditable without local path leakage. Signed + dynamic desktop extension artifacts and device-tested per-platform extension + object builds are not complete yet. The gated + native extension matrix iterates the manifest and covers install/load, + restart, physical backup, and physical restore for every currently packaged + extension across broker/direct-C-ABI and server paths. +- Broker mode starts one helper process per active root and uses a shared Rust + supervisor to enforce `.broker_max_roots(n)` and duplicate-root admission. + Sessions report `multi_root=true` when the configured broker root budget is + greater than one. The helper still takes the same filesystem root lock, so + independent broker runtimes cannot accidentally own the same root. Durable + reconnect, crash-restart policy, and upgrade orchestration remain broker + release gates. +- 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. 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 + provenance at + `target/perf/native-liboliphaunt-20260524T090412Z/provenance.json` for that + recorded source/artifact set. The checkout has since gained backup ABI and + tar-writer changes, so refresh the full matrix before making a current-source + release claim. The + PostgreSQL control is `postgres (PostgreSQL) 18.4`, with matched `safe` + durability and `throughput` runtime footprint across native liboliphaunt, + native PostgreSQL, and SQLite controls. Template bootstrap keeps the + open-time gate fixed: direct open p90 is `440.28 ms` versus native PostgreSQL + tokio at `576.4 ms`. The safe-profile direct path passes repeated RTT, open, + and RSS gates, but still misses speed-suite p90 (`2.668 s` versus `2.419 s`), + speed tail throughput (`0.907x` native PostgreSQL), and physical + backup/restore (`0.558 s` versus `0.344 s`) against the new native PostgreSQL + physical-archive control with equal `56.17 MB` p50 payloads. + The benchmark harness uses 10 fresh-process RTT repeats, 20 fresh-process + speed repeats, 10 prepared repeats, and 10 backup/restore repeats before + classifying a run as release evidence. Direct, broker, server, native + PostgreSQL tokio, and SQLite are `stable` on that host run. Individual direct + speed cases `1`, `2`, `2.1`, `3`, `3.1`, `4`, `5`, `10`, and `13` remain + above the 5% per-case tolerance in the complete matrix; isolated + fresh-process diagnostics reproduce `1`, `2.1`, `3`, `4`, `10`, and `13`. +- A current-source focused backup diagnostic lives at + `target/perf/native-liboliphaunt-20260524Tbackup-final-direct/report.md`. + It is partial evidence only, but it verifies the new `oliphaunt_backup_ex` + path that appends SDK metadata during the C archive write. Direct p90 improved + to `0.534 s`; native PostgreSQL physical p90 in the same run is `0.324 s`. + `OLIPHAUNT_TRACE_BACKUP=1` attributes the remaining direct cost mainly to + `pg_backup_start` and PGDATA archiving. + The matrix includes SQLite embedded comparison rows, + artifact-size rows, large-result streaming, `COPY TO STDOUT` streaming, and + prepared-update rows for sequential and pipelined direct/broker/server/native + PostgreSQL paths. Native matrix runs build `oliphaunt-perf` explicitly, and + the native boundary guard keeps Wasmer runtime crates behind opt-in feature + gates. Remaining gaps are the measured speed + and backup misses, SQLite open/RSS competitiveness, dedicated extended-query + and typed-helper benchmark lanes, and repeating the full matrix whenever + benchmark harness or runtime inputs change. +- Swift now has a native-direct C ABI runtime path through + `OliphauntNativeDirectEngine`, with env-backed tests for open, raw protocol + execution, cancellation, and close. Kotlin/Native now has the same direct + C ABI path through `NativeDirectEngine`, including env-backed tests for + missing-library diagnostics, raw protocol execution, process-bound reopen, + cancellation, and close. The Kotlin public handle now treats `close()` as a + lifecycle primitive: it marks the handle closed, waits for serialized + execution to drain, then detaches native direct handles. Swift, Kotlin, + and React Native now expose simple and parameterized typed result helpers + layered over raw protocol execution, matching the Rust SDK concept without + adding SQL semantics to the C ABI. React Native exposes the same `cancel()` + lifecycle method in its Codegen-safe TurboModule surface. React Native iOS now + delegates its TurboModule calls to `Oliphaunt` through an + Objective-C-visible Swift adapter instead of carrying a duplicate C ABI + runtime. React Native Android delegates its TurboModule calls to the + Kotlin SDK instead of carrying a separate Kotlin/JNI/CMake runtime. The React + Native SDK verifier now runs Android Codegen, Kotlin compilation, Gradle + `assembleDebug`, Kotlin SDK JNI syntax, Swift adapter compilation against + `Oliphaunt`, and a synthetic Android runtime/template asset packaging check + when `ANDROID_HOME` is set. Android runtime materialization and template + PGDATA hydration are owned by the Kotlin SDK; Apple materialization and + template hydration are owned by `Oliphaunt`. Packaged Android + `liboliphaunt.so`, extension loading with real artifacts, full New + Architecture app builds, and iOS/Android device smoke tests are not complete + yet. + +The related track critique and blocker snapshot is archived under +`docs/internal/OLIPHAUNT_TRACK_REVIEW.md`; it is not a current release +checklist. diff --git a/docs/assets/oliphaunt.png b/docs/assets/oliphaunt.png new file mode 100644 index 00000000..f77687b8 Binary files /dev/null and b/docs/assets/oliphaunt.png differ diff --git a/docs/internal/DONE.md b/docs/internal/DONE.md new file mode 100644 index 00000000..f2b87415 --- /dev/null +++ b/docs/internal/DONE.md @@ -0,0 +1,1844 @@ +# Done (Maintainers) + +> Archived completion log; non-normative. Start with +> `docs/maintainers/README.md` and executable configuration. + +This was the chronological status log for implementation work completed during +the original build-out. It is retained as maintainer-facing history and is +intentionally separate from current policy and end-user documentation. + +## Native Perf Evidence + +Implemented: + +- native benchmark provenance now distinguishes release evidence, partial + reports, and diagnostic runs; +- React Native benchmark reports now include app-reported process memory through + `Oliphaunt.processMemory()`. iOS records Mach task resident/physical-footprint + bytes, Android records `Debug.MemoryInfo` PSS/dirty/heap fields, and the + mobile footprint matrix summarizes those fields instead of trusting missing + host-side process output as zero; +- release-grade perf validation requires the full native matrix: direct, + broker, server, native PostgreSQL controls, SQLite control, RTT, speed, + streaming, and prepared-update suites; +- release-grade perf validation now verifies the raw benchmark JSON and + resource files exist for base runs and configured repeats, including p50, + p90, p95, and p99 latency fields for benchmark reports; +- no-build harness checks synthesize a tiny release fixture so provenance and + raw-output validation stay in the fast maintainer loop without launching the + native benchmark suite. +- native perf reports include throughput, backup/restore, p90/p99 CPU/RSS and + child-RSS evidence, plus a `Native Direct Regression Diagnostics` section with + focused rerun and `perf diagnose-speed-cases` commands when NativeDirect + misses a native PostgreSQL gate; +- native perf reports now keep backup payload bytes visible and include a + same-semantics native PostgreSQL physical-archive control. The current matrix + compares liboliphaunt physical backup/restore against a `pg_backup_start` / + `pg_backup_stop` filtered-PGDATA tar control with equal `56.17 MB` p50 + payloads. The logical `pg_dump`/`pg_restore -Fc` row remains comparison data, + not the direct parity gate. +- direct physical backups now prefer `oliphaunt_backup_ex`, which appends SDK + root/archive metadata while the C archive is still being written instead of + validating and copying the full archive in Rust afterward. The C tar writer + also uses direct `read(2)` file reads, per-entry buffer reservation, and + opt-in `OLIPHAUNT_TRACE_BACKUP=1` phase diagnostics. +- `tools/perf/matrix/run_native_speed_diagnostics.sh` runs repeated + fresh-process native-direct and native-PostgreSQL speed-case diagnostics and + writes versioned `oliphaunt.native-speed-diagnostics.v1` summaries. The first + current-source follow-up run for `20260524T090412Z` reproduced speed misses + for cases `1`, `2.1`, `3`, `4`, `10`, and `13`; cases `2`, `3.1`, and `5` + did not reproduce above the + 5% tolerance in isolated diagnostics. +- the mobile footprint matrix emits Android/iOS Expo dev-client benchmark and + process-death recovery cases for the requested shared-buffer, WAL-buffer, + WAL-size, and Safe/Balanced durability sweep; its no-build guard verifies + honest case counts, skips invalid 8MB/16MB WAL-minimum cases for the current + 16MB WAL-segment PG18 build, and summarizes p50/p90/p95/p99, package size, + Android PSS/RSS, and iOS resident memory. +- the iOS device artifact lane now builds a current XCFramework with device and + simulator slices that fail freshness checks if PostgreSQL imports + mobile-forbidden shared-memory or semaphore APIs; the physical iPhone + installed-app path reached successful build/install, crash-recovery verify, + a full smoke run with automatic physical-device background/foreground + lifecycle exercise through Safari, and quick plus full-candidate installed-app + footprint matrices with Safe/Balanced durability, same-device SQLite + baselines, process-death WAL recovery, and app-reported Mach task + resident/physical-footprint memory. A follow-up physical iPhone quick tuning + slice varied `shared_buffers=8/16/32/64/128MB` and + `min_wal_size=8/16/32MB` under Balanced durability with 15/15 passing cases, + proving effective GUC capture and showing physical footprint is mostly flat + across small WAL minima while 128MB shared buffers add a modest footprint + step. The harness also has a reuse-installed-app retry mode for locked-device + launch failures, and a post-change physical iPhone reuse-installed smoke + passed background/foreground lifecycle SQL after adding a bounded Expo launch + URL read in the example app. +- the Android Expo installed-app lane was recovered on the local API 34 emulator + by cold-starting the AVD with `-gpu swiftshader_indirect` and + `-no-snapshot-load`; the balancedMobile Safe 32MB/shared-buffers, 4MB-WAL + quick slice passed benchmark and process-death recovery, including same-device + SQLite comparison and Android PSS/RSS capture. Later Android emulator retry + evidence also proved app-reported `Debug.MemoryInfo` capture in one + balancedMobile quick case, but the same local AVD killed a follow-up app + process before attach/startup, so that failure is recorded as harness/device + instability rather than database performance evidence. +- SDK parity checks now guard the mobile direct-mode lifecycle contract: + one resident backend per process, one physical session, serialized requests, + same-root logical reopen only, no crash isolation, and + `prepareForBackground`/`resumeFromBackground` documentation. +- `target/perf/native-liboliphaunt-20260524T090412Z/report.md` is a complete + PostgreSQL 18.4 native release matrix with direct, broker, server, native + PostgreSQL, and SQLite rows for RTT, speed, streaming, prepared updates, and + backup/restore. Strict + `tools/perf/check-native-perf-report.sh` provenance + verification passed against that recorded source/artifact set; later backup + ABI/tar-writer changes require a refreshed full matrix before current-source + release claims. The report shows + NativeDirect passing RTT, open, and RSS gates while still missing speed-suite + p90, speed tail throughput, physical backup/restore p90, and physical backup + throughput, so those misses remain tracked work instead of parity claims. +- `tools/xtask` now keeps `wasmer-types` behind the AOT serializer feature. + Native no-default-feature builds no longer compile that legacy runtime crate, + and `tools/policy/check-native-boundaries.sh` guards the feature boundary. + +## Runtime Direction + +The repository now has one production direction: WASIX dynamic linking plus +headless Wasmer loading of CI-produced LLVM AOT artifacts. + +Removed or excluded from the production path: + +- Wasmtime/static-WASI runtime path; +- Emscripten/JavaScript glue runtime path; +- user-side Docker, LLVM, Cranelift, or local Postgres compilation; +- duplicated runtime layouts and host-side timezone/path rewrite shims; +- historical spike workspaces from the tracked repository. + +Production build inputs now live under `assets/`. + +## Workspace And Asset Crates + +Implemented: + +- root `oliphaunt-wasix` crate remains the public crate; +- `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; +- upstream checkouts are no longer tracked; maintainers fetch pinned sources on + demand into ignored `target/oliphaunt-sources/checkouts`; +- source pins live in `src/sources/third-party/**`; +- root packages exclude upstream checkouts from published crates. +- `xtask assets verify-committed` validates source-controlled asset inputs, + source pins, package metadata, AOT crate templates, and generated extension + coherence when generated manifests are installed, without local upstream + checkouts; + +Generated release asset set: + +- portable Oliphaunt WASIX runtime archive; +- `pg_dump.wasix.wasm`; +- deterministic `.tar.zst` archives for the 37 requested extension build + candidates. All 37 promoted exact extensions are stable public constants after + direct, server, restart, and lifecycle materialization gates; +- prepopulated PGDATA template archive; +- native Wasmer LLVM AOT artifacts. + +These artifacts are generated locally under `target/oliphaunt-wasix/**` or by the +Builds workflow WASIX runtime jobs and are consumed by release staging without +being committed to git. + +## Builder-First Release Staging + +Implemented: + +- the Builds workflow now owns release-shaped runtime, SDK, and exact-extension + package artifacts; the Release workflow requires same-SHA Builds artifacts + and consumes them instead of rebuilding native assets during publish; +- exact-extension package staging now renames native and WASIX assets into the + independent extension product/version namespace and writes a release manifest + beside the archive assets; +- exact-extension artifact targets are now product-local metadata under each + external extension, and the Builds workflow derives the native extension + matrix from those targets across macOS, Linux, Windows, iOS, and Android; +- React Native mobile build jobs consume native exact-extension artifact + outputs and stage package-shaped manifests locally instead of waiting for the + aggregate native+WASIX extension package release assembly; +- GitHub release asset verification reads staged exact-extension package + manifests for extension products, and attestation verification includes + extension, broker, Node direct, liboliphaunt, and WASIX asset families; +- the Rust SDK no longer declares a false `github-release-assets` publish target + because it has no GitHub release asset payload beyond the registry package. + +## Source And Build Spine + +Implemented: + +- active source baseline switched to `postgres/postgres` + `PG17 legacy lane` at `01792c31a62b7045eb22e93d7dad022bb64b1184`, matching + the audited `@electric/wasm` 0.4.5 source/artifact pair; +- `oliphaunt-build` `portable` is pinned as build-script provenance; +- maintained WASIX build files live under `src/runtimes/liboliphaunt/wasix/assets/build`; +- `xtask assets build --execute` can produce the main runtime, support modules, + requested contrib/PGXS extension side modules, SQL-only extension payloads, + and `pg_dump` for the local target; +- `xtask assets package` emits deterministic archives, generated manifests, and + crate assets; +- `xtask assets aot` regenerates local Wasmer LLVM AOT artifacts; +- `xtask assets check --strict-generated` validates generated metadata; +- `xtask assets source-spine --check-patch-applies` validates the maintained + source patch and C ABI harness; +- `xtask assets audit-upstream --strict` records upstream fix decisions; +- required upstream fixes from `PG17 legacy lane` are now the active source + spine rather than comparison material. The WASIX patch keeps dynamic-main and + side-module support, C startup timers, and explicit exports for the stable + branch lifecycle while reusing upstream `oliphaunt_wasix_start`, + `pgl_setOliphauntActive`, `ProcessStartupPacket`, `PostgresMainLoopOnce`, and + `PostgresMainLongJmp`; +- source-spine review conclusion: upstream Oliphaunt's libc/host adaptations are + purposeful for wasm hosts, not arbitrary shortcuts. `oliphauntc.c` supplies + stable `postgres` identity, explicit process-active state, manual top-level + longjmp recovery, socket callbacks, shared-memory emulation, and explicit + atexit replay because browser/Emscripten cannot provide normal Postgres + child processes, sockets, Unix users, SysV shared memory, or a native process + lifecycle. The WASIX bridge keeps the same architectural contracts where + Wasmer still needs host assistance, but uses Rust-owned input/output buffers + instead of Emscripten callback pointers because the Rust host does not have + Emscripten's JS table callback mechanism; +- WASIX-specific deviation from upstream Oliphaunt: top-level Postgres longjmp + detection uses `jmp_buf` pointer identity instead of upstream's buffer-content + `memcmp`. The memcmp test is acceptable in the Emscripten artifact it was + written for, but under Wasmer/WASIX it misclassified nested PostgreSQL + `PG_TRY` handlers and skipped normal portal cleanup. Pointer identity keeps + the host escape hatch scoped to the single exported top-level recovery buffer; +- WASIX-specific PostgreSQL fix: active portal abort cleanup is owned in + `AtAbort_Portals` for `OLIPHAUNT_WASIX_DL`, not in Rust. This keeps simple-query + and COPY error recovery at the PostgreSQL portal lifecycle boundary and avoids + fabricating cleanup behavior in the wire proxy; +- stable branch behavior note: startup `ParameterStatus` messages may be emitted + on raw protocol paths before `ReadyForQuery`. Tests now allow those legal + PostgreSQL messages instead of assuming the older minimal message sequence; +- new roots are now created through the packaged PGDATA template path. The old + embedded-backend `pgl_initdb` path was removed; explicit fresh-initdb paths + now use the bundled split WASIX `initdb` command and remain outside the + default fast path; +- the old builder-branch `oliphaunt-wasix/*` runtime wrapper is no longer the + production patch target. It remains historical/reference material only; +- `xtask package-size --enforce` passes locally for the root, asset, and macOS + arm64 AOT crates. + +Parity verified against upstream Oliphaunt stable source and TypeScript host: + +- startup/initdb: upstream TypeScript creates a cluster with `initdb.wasm`, + dumps PGDATA, loads that tarball into the main runtime, calls + `_pgl_setOliphauntActive(1)`, runs `callMain([...startParams, -D, PGDATA, + PGDATABASE])`, expects exit `99`, then calls `_oliphaunt_wasix_start()`. + oliphaunt-wasix matches the main-runtime lifecycle from `_pgl_setOliphauntActive` + onward and deliberately consumes a packaged PGDATA template instead of + exposing split runtime `initdb` yet. That is an explicit product gap, not a + hidden fallback; +- startup packet: upstream calls `_pgl_getMyProcPort()`, + `_ProcessStartupPacket(...)`, `_pgl_sendConnData()`, and `_pgl_pq_flush()`. + oliphaunt-wasix uses the same C exports. Server connections now open the + embedded backend against the startup packet database, apply client startup + options on the C side, and apply non-`postgres` users through PostgreSQL + `SET ROLE` semantics, matching Oliphaunt's single-process identity model; +- query loop: upstream feeds the whole frontend message buffer, repeatedly calls + `_PostgresMainLoopOnce()` while frontend bytes or libpq buffered data remain, + catches status `100`, calls `_PostgresMainLongJmp()`, then always calls + `_PostgresSendReadyForQueryIfNecessary()` and `_pgl_pq_flush()`. The Rust host + now follows that control flow with Rust-owned input/output buffers instead of + Emscripten callback pointers; +- close: upstream clears active state, sends protocol terminate, and replays + `_pgl_run_atexit_funcs()`. The Rust host clears active state and replays + atexit on shutdown, while tests cover clean restart, root locking, and stale + runtime-state cleanup; +- host ABI: upstream `oliphauntc.c` emulates sockets, identity, shared memory, + `system`/`popen`, timers, longjmp, and atexit because browser/Emscripten does + not provide normal process or OS services. oliphaunt-wasix keeps the same + categories only where WASIX still needs host assistance, and the ABI harness + tests stable identity, fail-closed `system`, protocol fd bridging, shared + memory, atexit replay, mmap, and libpq encoding aliases; +- justified deviations: WASIX longjmp detection uses pointer identity instead of + upstream's `jmp_buf` content `memcmp`; simple-query/COPY portal abort cleanup + is owned in PostgreSQL `AtAbort_Portals`; startup `ParameterStatus` messages + are accepted as legal protocol output; split WASIX `initdb` is now the owned + template-generation path instead of resurrecting the old builder wrapper. + +## Runtime Behavior + +Implemented: + +- runtime loads verified headless Wasmer AOT artifacts; +- AOT artifacts record source module hash, Wasmer version, and engine identity; +- runtime verifies asset and archive hashes before use; +- unsupported targets return a clear missing-AOT-artifact error instead of + compiling locally; +- `Oliphaunt::preload()` and `Oliphaunt::preload_extensions(...)` exist; +- `Oliphaunt::preload()` now warms the persistent runtime cache, headless Wasmer + engine, main AOT module, shared WASIX runtime, and runtime side modules; +- `Oliphaunt::preload_extensions(...)` warms requested extension artifacts and + side-module cache entries generically; +- direct, persistent, app-id, proxy, server, and temporary roots now share the + `RootPlan`/`prepare_root` root-preparation pipeline; +- direct API, server API, proxy CLI, raw protocol API, and direct `pg_dump` now + share `BackendSession` for WASIX instance creation, backend start, startup + packet handling, protocol transport, shutdown, restart, and atexit replay; +- roots can install immutable runtime files from a persistent runtime cache and + install the embedded PGDATA template without running initdb on the default + startup path; +- mutable PGDATA template files are copied or archive-installed, never + hardlinked; immutable runtime files hardlink from cache when possible; +- persistent roots use lock files to prevent concurrent direct/server opens; +- runtime and extension archive extraction rejects unsafe paths, symlinks, + hardlinks, device nodes, and unsupported archive entry types; +- runtime uses canonical Postgres paths: + `/bin`, `/lib/postgresql`, `/share/postgresql/extension`, and + `/share/postgresql/timezonesets`. + +## Public API Surface + +Implemented: + +- `OliphauntBuilder::extension`; +- `OliphauntBuilder::extensions`; +- `OliphauntBuilder::username`; +- `OliphauntBuilder::database`; +- `OliphauntBuilder::debug_level`; +- `OliphauntBuilder::relaxed_durability`; +- `OliphauntBuilder::startup_arg`; +- `OliphauntBuilder::startup_args`; +- `OliphauntBuilder::load_data_dir_archive`; +- `Oliphaunt::enable_extension`; +- `Oliphaunt::preload`; +- `Oliphaunt::preload_extensions`; +- `Oliphaunt::dump_data_dir`; +- `Oliphaunt::dump_data_dir_with_format`; +- `Oliphaunt::try_clone`; +- physical PGDATA archives now apply Wasmer overlay whiteouts, so files deleted + from the lower template are not resurrected by dump/load/clone; +- physical PGDATA archives are written from a materialized effective PGDATA view + instead of directly mixing lower-template and upper-overlay entries in the tar + writer; +- physical PGDATA archive/clone now checkpoints, quiesces the backend, + materializes the archive, and restarts the same backend session; docs state + this is a same-runtime/same-version physical import/export path, not a + cross-version backup protocol; +- `Oliphaunt::exec_protocol_raw`; +- `Oliphaunt::exec_protocol_raw_stream`; +- `Oliphaunt::dump_sql`; +- `Oliphaunt::dump_bytes`; +- `OliphauntServerBuilder::extension`; +- `OliphauntServerBuilder::extensions`; +- `OliphauntServerBuilder::username`; +- `OliphauntServerBuilder::database`; +- `OliphauntServerBuilder::debug_level`; +- `OliphauntServerBuilder::relaxed_durability`; +- `OliphauntServerBuilder::startup_arg`; +- `OliphauntServerBuilder::startup_args`; +- `OliphauntServer::database_url`; +- `OliphauntServer::dump_sql`; +- `OliphauntServer::dump_bytes`; +- `PgDumpOptions`; +- 37 public extension constants plus `extensions::ALL`, covering the smoke-gated + packaged Oliphaunt/Postgres catalog: `amcheck`, `auto_explain`, `bloom`, + `age`, `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`, SQL-only `pgtap`, `seg`, + `tablefunc`, `tcn`, `tsm_system_rows`, `tsm_system_time`, `unaccent`, and + `vector`. + +`oliphaunt-wasix-dump` no longer exposes the old archive-unpack behavior. It is now a +real logical dump CLI backed by the packaged WASIX `pg_dump` module. + +`relaxed_durability` is a startup-profile flag rather than a hidden mutation of +`PostgresConfig`; explicit user `postgres_config` values win and +`relaxed_durability(true).relaxed_durability(false)` returns to the normal +profile. + +## Protocol And Server Correctness + +Implemented coverage: + +- direct Rust API open/init/query; +- persistence, close/reopen, stale runtime-state cleanup, interrupted PGDATA + cleanup, and root-lock conflicts; +- SQLx and `tokio-postgres` local-server connections; +- SSLRequest no-SSL response; +- CancelRequest safe close; +- backend-open failures no longer map every non-`template1` startup failure to + SQLSTATE `3D000`. PostgreSQL/C now owns startup identity and database errors: + the WASIX backend captures `InitPostgres` startup `ErrorResponse` bytes, the + proxy forwards them directly, and runtime/filesystem failures before + PostgreSQL can speak protocol remain synthesized `XX000`; +- Parse, Bind, and Execute error recovery; +- SQLSTATE preservation for syntax, missing relation, invalid typed parameter, + wrong parameter count, and extension-originated errors; +- extended-query `ReadyForQuery` synchronization; +- successful pipelined extended queries; +- mixed success/error/success pipelined queries; +- explicit prepared-statement reuse; +- transaction error recovery through rollback; +- client disconnect during an extended-query exchange; +- partial TCP reads and pipelined simple queries; +- server-mode `COPY FROM STDIN` now streams through the backend-owned protocol + pump instead of Rust SQL-text detection or proxy-fabricated COPY state. Normal + SQLx/tokio-postgres traffic uses the buffered raw-protocol path; when + PostgreSQL emits a real `CopyInResponse`, `CopyOutResponse`, or + `CopyBothResponse`, the WASIX bridge flushes buffered backend output to the + attached socket continuation and lets PostgreSQL continue on the socket. Raw + wire coverage includes simple COPY, extended-protocol COPY, CSV `WITH (...)` + COPY, binary COPY, `CopyData`, `CopyDone`, `CopyFail`, Unix-socket COPY + parity, and post-COPY connection reuse. +- continuation bytes are borrowed in the proxy read loop and materialized only + after the C bridge reports active streaming COPY; +- direct raw protocol streaming is routed through the shared `BackendSession` + framed sender instead of a separate client-only transport path; +- Rust-owned guest bridge allocations are scoped through `pg_free`/`free`, and + debug builds now have a direct raw-protocol stress test proving repeated + bridge round trips keep allocation/free counters balanced; +- direct LISTEN/UNLISTEN quotes channel identifiers and dispatches notifications + by the exact backend channel name, including case-sensitive and quoted names. +- a larger PostgreSQL regression subset now ports the relevant Oliphaunt test + surface for datatypes, DDL, transactions/savepoints, planner/index behavior, + and direct `/dev/blob` CSV COPY. The datatype coverage also found and fixed a + direct-client multidimensional array parser bug, with unit coverage for + nested arrays, quoted values, and unquoted NULL handling. + +## Independent P0 Architecture Review + +The P0 review was re-run against the current Rust host, WASIX bridge, source +patch, and regression tests. No current P0 architecture blockers remain in the +reviewed surface. The completed P0 items were moved out of the backlog; future +major protocol, backup, runtime, or source-spine changes should get a new +review entry here instead of leaving completed checklists in `TODO.md`. + +Verified ownership boundaries: + +- Rust owns hosting, root preparation, caches, process lifecycle, direct/server + API shape, and typed fallbacks for host/runtime failures before PostgreSQL can + speak wire protocol; +- PostgreSQL/C owns SQLSTATEs, startup identity/database errors, query protocol + state, COPY state, portal cleanup, and longjmp recovery boundaries; +- the WASIX bridge owns only the host ABI that Wasmer/WASIX cannot provide as a + normal OS process boundary: protocol fd transport, locale/identity shims, + single-process shared memory, fail-closed process calls, and explicit + allocation/free ownership. + +Review conclusions: + +- guest-memory ownership is scoped through `GuestAllocator`, `pg_free`/`free`, + and debug allocation/free counters; +- detached protocol stdio fails closed rather than silently accepting bytes; +- COPY state is reported by PostgreSQL through + `pgl_protocol_report_copy_response`; the proxy no longer parses SQL text, + fabricates COPY state, scans whole backend buffers, or eagerly copies + continuation bytes for ordinary traffic; +- direct raw protocol streaming and direct `pg_dump` use the shared + `BackendSession` transport instead of a separate clone/server path; +- startup role/database failures are PostgreSQL-owned: WASIX backend open + captures `InitPostgres` `ErrorResponse` bytes, the proxy forwards those bytes, + and Rust no longer probes `pg_database` or string-guesses `3D000`; +- direct API, server API, proxy CLI, raw protocol, physical archive/clone, and + direct `pg_dump` share `RootPlan`/`prepare_root` and `BackendSession` + lifecycle paths; +- side-module cache seeding is keyed by artifact name, source module hash, + Wasmer version, Wasmer-WASIX version, and engine identity; +- AOT startup keeps full SHA verification behind + `OLIPHAUNT_WASM_AOT_VERIFY=full` while default loading uses metadata receipts + and mmap/native deserialization; +- PGDATA physical archive/clone materializes the effective overlay view with + whiteouts, quiesces/restarts the backend, and is documented as + same-runtime/same-version physical transfer rather than a WAL-aware backup; +- public API parity additions were reviewed at that point: raw protocol + streaming is real, physical clone/export has honest semantics, startup args + remain advanced, and listener channel names are identifier quoted. + +Residual work from this review is intentionally not P0 architecture debt: +target-matrix CI, broader extension generation, additional PostgreSQL +regression subsets, release performance gates, and future split-WASIX `initdb` +support remain tracked in `TODO.md`. + +## Extensions And `pg_dump` + +Implemented coverage: + +- `vector` direct API load, `CREATE EXTENSION`, insert, distance query, and + pgvector type cases; +- `vector` through `OliphauntServer` and SQLx; +- SQLx recovery after vector-originated errors; +- demand-driven extension install and idempotent `enable_extension`; +- installed extension side modules are seeded into the headless Wasmer cache on + reopen; +- `pg_trgm` direct API and SQLx server smoke coverage; +- `hstore` direct API, persistence/reopen, and SQLx server smoke coverage; +- Oliphaunt extension tests were ported into a generic promotion gate for direct + API, server API, restart, and lifecycle materialization. The gate now covers + every packaged candidate. AGE now uses its upstream 32-bit `SIZEOF_DATUM=4` + SQL generation path, passes direct/server/restart/lifecycle gates, and is + exposed as `extensions::AGE`; +- extension discovery now merges Oliphaunt docs/REPL exports, Oliphaunt package + exports, PostgreSQL contrib metadata, `postgres-oliphaunt` `other_extensions` + pins, Oliphaunt tests, and the packaged asset manifest into + `src/extensions/generated/extensions.catalog.json`; +- `xtask assets fetch` now clones/fetches every pinned source from + `src/sources/third-party/**` into ignored `target/oliphaunt-sources/checkouts/**` directories, + including the external extension sources for pgtap, pg_ivm, pg_uuidv7, + pg_hashids, AGE, PostGIS, and pg_textsearch; +- extension build intent now lives in `src/extensions/catalog/extensions.promoted.toml` instead + of being inferred from already-packaged artifacts. The generated catalog + separates requested, packaged, stable, and publicly promoted state; +- extension smoke evidence now lives in `src/extensions/catalog/extensions.smoke.toml`; + generated public constants require requested + packaged + stable + direct, + server, and restart smoke status recorded as passed; +- `xtask extensions build-plan --write` generates + `src/extensions/generated/extensions.build-plan.json`, + `src/extensions/generated/contrib-build.tsv`, and + `src/extensions/generated/pgxs-build.tsv`; `xtask assets check --strict-generated` + fails if those generated files drift; +- the WASIX extension build spine now uses generic contrib and PGXS build + scripts driven by the generated build plans, replacing the previous + `pg_trgm`-only and `pgvector`-only Docker scripts; +- the generated catalog now requires every discovered SQL extension to be + either requested for build or explicitly blocked with a concrete reason. The + current catalog discovers 40 SQL extensions, requests/packages 37, and blocks + only `pgcrypto`, PostGIS, and `uuid-ossp` on missing pinned native dependency + stacks; +- native side-module names are generated from control-file `module_pathname` + and PGXS Makefile metadata instead of assuming `.so`. This covers + cases such as `intarray` using `_int.so` and SQL-only extensions such as + `pgtap`; +- 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 `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`; +- generated extension constants are emitted only for extensions that are + requested, packaged, stable, and direct/server/restart smoke-passed; generated + asset includes carry all packaged candidates so private promotion tests can + exercise candidates before they become public API; +- manifest metadata records extension source kind, control files, + dependencies, lifecycle, imports, required core exports, unresolved imports, + installed files, load order, and smoke status; +- the `wasix-dl` export list is generated from the runtime exports plus + runtime-support/extension side-module imports, rather than being a + hand-maintained export allowlist; +- extension archive hash mismatch rejection; +- public WASIX `pg_dump` runner loads through the AOT manifest, connects to + `OliphauntServer`, dumps plain SQL, restores into fresh `Oliphaunt`, and verifies + schema/data; +- direct `Oliphaunt::dump_sql` no longer uses a temporary physical clone, public + `OliphauntServer`, or OS loopback TCP; it runs the standalone WASIX `pg_dump` + against an in-process Wasmer virtual TCP connection whose host side is routed + through the same direct raw-protocol backend; +- direct `Oliphaunt::dump_sql` rejects database/user options that would imply a + different backend than the already-open direct session; callers needing that + use the server `pg_dump` path; +- the direct `pg_dump` transport keeps `pg_dump`/libpq stock and owns the only + required semantic adapter in Rust: a first-write-readiness normalization for + Wasmer's in-memory `TcpSocketHalf` so libpq's connect-time and first-write + polls remain level-triggered; +- public `pg_dump` coverage includes indexes, views, sequences, + `--schema-only`, `--quote-all-identifiers`, source-server reuse after dump, + and vector extension dump/restore; +- `PgDumpOptions` rejects passthrough flags that conflict with the typed + output/connection contract instead of letting callers override the internal + output file, format, host, port, username, database, or job count. + +## WASIX C Boundary Ownership + +The remaining C-side differences are owned as WASIX portability and host ABI, +not hidden generic stubs: + +- `pg_proto.c` manually coordinates `ReadyForQuery` for the current + call/return protocol loop and is covered by SQLx, `tokio-postgres`, and raw + wire-protocol tests; +- `pg_main.c` drives initdb boot/single-user phases inside one embedded process, + with named helpers for boot, stdin restoration, and single-user replay; +- `pgl_os.h` emulates only the expected initdb boot/single `popen()` commands + under `OLIPHAUNT_WASIX_DL` and fails closed otherwise; +- `pgl_stubs.h` is gated to `OLIPHAUNT_WASIX_DL`, and future removals are driven by + link-symbol analysis; +- `oliphaunt_wasix_bridge.c` owns locale command emulation, stable `postgres` + uid/passwd identity, protocol socket buffers, fail-closed `system()`, selected + fd/socket delegation to WASIX libc, and single-process SysV shared memory. + +The source-spine guard checks for removed spike smells: debug-only `#pragma` +markers, diagnostic `popen`, broad socket fake-success behavior, layout +mirroring, timezone rewrites, and generic stub logging. + +## Validation Already Run + +The following local gates passed before this consolidation: + +```sh +cargo fmt --check +cargo check -p oliphaunt-wasix --all-targets +cargo check -p oliphaunt-wasix --no-default-features --all-targets +cargo run -p xtask -- assets check --strict-generated +cargo run -p xtask -- assets source-spine --check-patch-applies +cargo run -p xtask -- assets audit-upstream --strict +cargo run -p xtask -- package-size --enforce +cargo test --test client_compat +cargo test --test runtime_smoke +cargo test --test extensions_smoke +``` + +The public `pg_dump` round-trip tests and asset/AOT hash-mismatch tests also +passed locally. + +## Cold-Start Performance Work + +Implemented: + +- internal phase timing via `capture_phase_timings`; +- `cargo run -p oliphaunt-perf -- cold` emits structured JSON with explicit + `cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and + `workload` fields, so first-install bootstrap, process warmup, new-root first + query, and client/server first query are no longer conflated; +- `cargo run -p oliphaunt-perf -- cold --reset-cache` removes the oliphaunt-wasix cache + before measuring, making runtime extraction, AOT materialization, PGDATA + template install, and extension-template creation visible in the first + operation that pays each cost; +- process-wide headless Wasmer engine cache; +- process-wide AOT `Module` cache keyed by artifact hash; +- AOT manifests now include raw artifact SHA256/size metadata; the default + startup path uses an atomic cache receipt and file metadata instead of scanning + the raw AOT file; +- bundled runtime, extension, PGDATA-template, and AOT content hashes are kept + off the default startup path and are only scanned with + `OLIPHAUNT_WASM_AOT_VERIFY=full`; +- process-wide shared Tokio runtime, WASIX runtime, and `SharedCache`; +- side-module seeding is reused by artifact name, module hash, Wasmer version, + Wasmer-WASIX version, and engine identity; +- phase timing now propagates into the server listener thread, so + `OliphauntServer` cold runs report root preparation, listener bind/spawn, + proxy backend open, client connect, first query, and shutdown phases instead + of a single opaque total; +- server accept loops now use blocking `accept()` plus an explicit wake + connection during shutdown, removing the previous nonblocking accept plus + 10ms sleep polling jitter; +- fresh proxy backend initialization no longer runs the post-client + `ROLLBACK`/`DISCARD ALL` cleanup path. Fresh startup applies default GUCs + directly; full reset remains in place after client disconnects; +- persistent runtime asset cache under the platform cache directory; +- runtime-cache repair removes mutable scratch state and restores required + support files before the cache is used as a shared overlay source; +- per-root runtime scratch directories are reset during root preparation; +- `password` is copied as per-root mutable support data instead of hardlinked + from the shared runtime cache; +- PGDATA template manifests are parsed without archive hashing on the default + path; +- the parsed generated asset manifest is cached process-wide, avoiding repeated + 1.4 MB JSON parses during AOT, extension, and PGDATA template checks; +- an eager PGDATA template overlay is implemented as the mainline template + path: the cached initialized template is mounted as lower `/base`, the + per-instance upper starts almost empty, and individual template files are + copied into the upper only before mutating opens; +- the eager PGDATA overlay is passed as a runner-level WASIX mount. Nested + mounts placed inside the supplied `WasiFsRoot` were not sufficient because + `WasiRunner::prepare_webc_env` rebuilds the final mount tree from the root + `/` filesystem plus runner-owned mounts; +- direct `Oliphaunt::open` no longer performs a separate session-setup round trip + and no longer folds session defaults into array discovery SQL. The Rust WASIX + host now calls the real C `ProcessStartupPacket` export from + `backend_startup.c`; C `pgl_sendConnData()` applies the direct-session + defaults before connection data is sent, so `BeginReportingGUCOptions` + observes `TimeZone=UTC` and `search_path=public`; +- `OliphauntBuilder::postgres_config`, `OliphauntServerBuilder::postgres_config`, + and `oliphaunt-wasix-proxy --postgres-config name=value` now pass user startup GUCs + through PostgreSQL's normal `-c name=value` argv handling. User settings are + appended after the default profile, so they override defaults without + special-casing individual GUCs such as `synchronous_commit`; +- server-mode client startup `options=-c ...` is now applied on the C side after + `ProcessStartupPacket` parses the packet and before `pgl_sendConnData()` + emits `AuthenticationOk` and `ParameterStatus`, preserving PostgreSQL's + startup-option timing for supported single-backend clients; +- extension-enabled PGDATA template caches include the startup-GUC entries in + their manifest and cache key, so a template created under one backend config + is not reused for another config; +- direct scalar open/query paths no longer scan `pg_type` for array metadata. + Built-in PostgreSQL array OIDs are registered statically in the Rust direct + client, and runtime-created enum/domain/composite arrays are discovered + lazily from parameter/result OIDs or through explicit + `refresh_array_types()` calls; +- the old `pgl_stubs.h` `ProcessStartupPacket` placeholder has been removed + from the maintained WASIX patch. Startup packet parsing now lives in + PostgreSQL's `backend_startup.c`, and the host no longer calls a separate + Rust-side default-GUC helper; +- focused tests cover process AOT cache reuse, extension preload reuse, + cross-instance state isolation, mutable PGDATA clone safety, eager PGDATA + lower-file visibility, direct runtime smoke, vector direct/server smoke, and + proxy smoke. + +Previous local debug `oliphaunt-perf cold` run after explicit preload: + +- explicit preload: about 605ms; +- temporary first query: about 553ms; +- warm temporary first query: about 547ms; +- representative extension-backed first query after extension preload: about + 646ms; +- server plus first `tokio-postgres` query: about 543ms. + +In that run bundled archive/module SHA scans were absent from the default path. +The remaining visible costs were main Wasmer deserialization at about 447ms and +temporary filesystem setup at about 321ms, mostly runtime clone plus PGDATA +template clone. + +Latest local debug `cargo run -p oliphaunt-perf -- cold` run after the shared +root-preparation work: + +- explicit preload: about 640ms; +- temporary first query: about 404ms; +- warm temporary first query: about 386ms; +- representative extension-backed first query after extension preload: about + 504ms; +- server plus first `tokio-postgres` query: about 371ms. + +That run removed the full immutable runtime clone from temporary opens. The +same prepared runtime-layout machinery now feeds direct, persistent, app-id, +proxy, and server roots as well. Per-root runtime setup was about 30ms and +`wasix.mountfs_overlay_construct` was under 1ms at that point. The dominant +remaining setup cost was PGDATA template clone/install at about 187-190ms, +followed by +backend start around 44-48ms and Wasmer instance creation around 30-36ms. + +Latest local debug `cargo run -p oliphaunt-perf -- cold` run after the eager PGDATA +overlay and parsed-manifest cache: + +- explicit preload: about 601ms; +- temporary first query: about 191ms; +- warm temporary first query: about 144ms; +- representative extension-backed first query after extension preload: about + 257ms; +- server plus first `tokio-postgres` query: about 123ms. + +In that run `pgdata.overlay_prepare` was about 0.4-0.5ms, down from the +previous 187-190ms template clone/install cost. The visible per-open costs are now +Wasmer instance creation around 30-37ms and PostgreSQL backend start around +49-52ms. Main-module AOT deserialization remains the dominant explicit preload +cost at about 506ms on this local debug profile. + +Historical local debug run after removing the separate direct session-setup +round trip, before lazy/generated array metadata: + +- explicit preload: about 535ms; +- temporary first query: about 230ms; +- warm temporary first query: about 133ms; +- representative extension-backed first query after extension preload: about + 254ms; +- server plus first `tokio-postgres` query: about 118ms. + +The warm direct `oliphaunt.open` phase dropped to about 112ms. At that point the +remaining direct-open client-side cost was the array catalog scan, about 30ms +for the warm catalog query and less than 1ms for Rust-side parser/serializer +registration. Scalar paths no longer pay that scan after lazy/generated array +metadata. + +Latest local release work: + +- asset release builds now default to `release-o3`, which compiles WASIX C + modules with `-O3 -g0 -flto=thin` and links with `-flto=thin`; +- release profiles run wasixcc's default Binaryen optimization plus + `--converge`, `--strip-debug`, and `--strip-producers`; +- the current exact Oliphaunt speed-suite run favors `release-o3 + converge/strip` + plus ThinLTO for SQL workload parity. The package-size gate still passes + locally with the macOS arm64 AOT crate at about 7.2MiB compressed and the + asset crate at about 5.6MiB compressed. Earlier startup-only runs favored + `release-os` over `release-oz`, and adding a project `-msimd128` flag was + redundant because the WASIX EH+PIC sysroot already invokes clang with SIMD, + relaxed SIMD, and extended const enabled; +- Wasmer LLVM AOT codegen experiments selected the mainline serializer profile: + nonvolatile memory operations plus a readonly funcref table. Nonvolatile + memory operations improved the exact Oliphaunt server SQLx speed suite by about + 9% geomean and won all 18 cases, but Wasmer marks that optimization as not + fully WebAssembly-spec compliant. Adding readonly funcref on top was about + 1.4% faster geomean than nonvolatile-only and improved indexed updates, but + regressed CREATE INDEX and DROP TABLE cases. The risk is now explicit release + profile surface and must be covered by the correctness matrix. The macOS + arm64 packaged AOT artifacts were regenerated with this profile; +- exact Oliphaunt speed-suite comparison now has its own harness and diagnostic + path. The latest ThinLTO `release-o3` direct run on macOS arm64 measured test + 9 at about 569ms, test 10 at about 724ms, test 11 at about 98ms, and test 14 + at about 77ms. Against the locally audited npm NodeFS reference, the direct + suite is about 1.22x faster geomean, with 16/18 wins but not a 10x-class + result under identical SQL/Postgres semantics; +- selected speed-case diagnostics show that host filesystem work is not the + remaining dominant cost on the heavy SQL cases. Test 10, for example, was + about 748ms total with about 21ms in traced filesystem work and about 743ms + inside PostgreSQL/AOT dispatch. This points the next investigation at + symbolized AOT/Postgres executor profiling, not more Rust result parsing or + root-layout tuning; +- prepared indexed-update benchmarking now compares SQLx sequential prepared + updates, tokio-postgres sequential prepared updates over TCP and Unix + sockets, tokio-postgres pipelined prepared updates over TCP and Unix sockets, + and native Postgres equivalents using the exact Oliphaunt Test 9/10 values. + Deferring extended-protocol `Sync` flush only within bytes already read from + one socket read reduced OliphauntServer TCP pipelined prepared updates from about + `612.835ms -> 399.921ms` for numeric indexed updates and + `640.691ms -> 416.837ms` for text indexed updates. Unix-socket OliphauntServer + was faster again at about 374/397ms, so transport still matters for + sequential prepared execution and modestly for pipelined execution. The exact + simple-query server speed suite stayed in the same range after the change: + Test 9 about 583ms and Test 10 about 740ms locally. A larger 256KiB proxy + read buffer was tested and rejected because it regressed the same pipelined + prepared workload to about 545/562ms; +- the native Postgres benchmark helper now attempts graceful termination before + falling back to `Child::kill()`, because SIGKILL can leak SysV shared-memory + IDs on macOS. `perf prepared-updates --skip-native` exists for Oliphaunt-only + runs when local native Postgres IPC state is unhealthy; +- `perf prepared-updates --gate` now emits protocol counters and fails if + ordinary prepared traffic activates the backend-owned streaming continuation + or if pipelined prepared traffic stops batching. The timing thresholds are + intentionally a local regression smoke gate until stable CI runner baselines + exist; +- phase timing guards are hot-path no-ops when no recorder is active, so + diagnostic spans do not call `Instant::now()` in normal runtime traffic; +- PostgreSQL spinlocks are enabled in the WASIX build. The earlier + `--disable-spinlocks` fallback is gone, and the source-spine guard rejects it + if it returns. This is a correctness/architecture baseline because wasixcc + exposes the required atomic operations; local single-backend speed numbers are + mixed enough that it should not be treated as a standalone benchmark win; +- the shared runtime overlay and eager PGDATA overlay are now mainline runtime + behavior, with the old full-local runtime and full-template clone paths kept + only as internal build/staging machinery where still required; +- local release `cargo run -p oliphaunt-perf -- cold` with no env overrides showed + warmed preload around 18ms, temporary first query around 100ms, warm temporary + first query around 83ms, representative extension-backed first query around + 148ms after extension preload, and server first query around 77ms; +- that run predated lazy/generated array metadata and showed direct open + dominated by backend startup around 33-40ms plus the old array catalog scan + around 24-33ms. Scalar paths no longer pay that catalog scan; new release + numbers should replace this historical baseline; +- after adding deeper preload instrumentation, local release runs showed + explicit preload between about 15ms and 56ms depending on OS cache warmth. + The first uncached visible run spent about 37ms in main AOT mmap + deserialization and about 10ms in runtime cache setup; repeated warmed runs + spent about 10ms in main AOT deserialization for both mmap and file modes; +- Wasmer AOT loading now uses the native mmapped-file deserializer as the only + production path; the old file deserializer runtime switch was removed; +- after promoting the mainline AOT and filesystem paths, local release + `cargo run --release -p oliphaunt-perf -- cold` showed primary visible latencies + around 36ms for preload, 55ms for a new temporary direct first query, 45ms + for a second new temporary direct first query, 47ms for server SQLx first + query, and 57ms for server SQLx vector first query; +- the same mainline artifact profile measured exact Oliphaunt server speed-suite + Test 9 at about 587ms, Test 10 at about 730ms, Test 11 at about 91ms, Test 14 + at about 71ms, and 18-test geomean around 76ms locally. Prepared-update + server probes measured TCP pipelined prepared updates around 395/414ms and + Unix pipelined prepared updates around 366/392ms for the numeric/text indexed + workloads; +- after static built-in arrays and lazy runtime array discovery, local release + `cargo run --release -p oliphaunt-perf -- cold` showed explicit preload about + 52ms, temporary first query about 88ms, warm temporary first query about 79ms, + representative extension-backed first query about 131ms after extension + preload, and server first query about 75ms. Scalar direct paths did not emit + the `oliphaunt.array_type_catalog_query` phase; +- after server-thread timing and accept-loop cleanup, local release + `cargo run --release -p oliphaunt-perf -- cold` showed explicit preload about + 19-22ms, temporary first query about 86-89ms, warm temporary first query about + 77ms, representative extension-backed first query about 132-140ms after + extension preload, tokio-postgres server first query about 68ms, and SQLx + server first query about 68-70ms. The server path now shows `server.start` + around 52-54ms, + `proxy.backend_open` around 44-46ms, `postgres.backend_start` around 35-37ms, + tokio-postgres connect around 0.6ms/query around 5.5ms, and SQLx connect + around 2.1ms/query around 6.0ms; +- `oliphaunt-perf cold` includes the extension-enabled SQLx server path, now named + `process_warm_new_temp_server_sqlx_vector_first_query`, which starts + `OliphauntServer` with a requested bundled extension and measures a first + extension-backed SQLx query for a new temporary server root. This keeps + server-mode extension install/load, `CREATE EXTENSION`, client connect, and + first extension query visible as one product-shaped path. The first local + release run measured about 175ms total, dominated by `proxy.extension_enable` + around 107ms; SQLx connect and the + first vector query were both sub-millisecond on that run; +- cold perf reporting now breaks out preload runtime cache setup, AOT install, + mmap/file deserialization, WASIX runtime construction, instance creation, + startup-packet/default-GUC work, client protocol round trips, extension side + module seeding, and public `pg_dump` runner phases; +- instrumented WASIX runtime artifacts can export C-side backend startup timers + via `pgl_backend_timing_elapsed_us`, and the Rust host records them as + `postgres.backend.c.*` phases when the export is present. Production WASIX + artifacts keep `OLIPHAUNT_WASM_WASIX_BACKEND_TIMING=0`, so the C timing macros + compile away and the export is absent. Local release instrumented runs show + backend startup split mainly between `postgres.backend.c.shared_memory` around + 11-12ms and `postgres.backend.c.init_postgres` around 19-21ms, inside + `postgres.backend.c.async_single_user_main` around 33-36ms; +- C-side timers now reach inside `InitPostgres`: `StartupXLOG`, + relcache/catcache initialization, transaction snapshot, session-user setup, + database lookup/recheck/path validation, `CheckMyDatabase`, startup option + processing, session initialization, and session preload libraries are reported + as individual `postgres.backend.c.*` phases; +- the C timing ABI has additional instrumented-only IDs for + `InitializeMaxBackends`, `CreateSharedMemoryAndSemaphores`, `InitProcess`, + `RelationCacheInitializePhase3`, and `initialize_acl`, so the two remaining + startup hotspots can be subdivided without adding production clock reads; +- a generic extension-set PGDATA template cache now builds templates through + normal `CREATE EXTENSION`, runs `CHECKPOINT`, then closes the embedded backend + through the runtime `pgl_shutdown` export before caching the template. The + cache is keyed by the base runtime/template manifest plus sorted extension + archive identities and is mounted as the lower PGDATA template for direct and + server temporary roots; +- direct and server extension paths skip redundant `CREATE EXTENSION` when the + requested extension set is already present in the cached template, while still + installing/preloading side-module assets into each instance root; +- extension-template cache keys were bumped to version 2 after adding clean + backend shutdown, so older templates that left `pg_control` in a + recovery-heavy state are ignored; +- current local release timings with the clean generic extension template cache + show extension-template lookup/overlay under 1ms, extension archive install + around 5ms, and extension-enabled `StartupXLOG` around 3-4ms instead of the + previous roughly 350ms recovery path. In the steady cached run, the direct + vector first-query path for a new temporary root was about 82-93ms and the + SQLx vector first-query path for a new temporary server root was about + 74-78ms; +- pure MountFS runtime composition now keeps core runtime assets in the shared + cached lower runtime and materializes only mutable state plus requested + extension assets in the per-root upper layer. Runtime and extension smoke + tests assert that core binaries/catalog files are not copied into the upper + root and unrelated extensions are not installed. Local release comparison + showed per-root runtime setup dropping from roughly 7ms to about 0.6-0.9ms, + the SQLx first-query path for a new temporary server root around 55ms, and + the SQLx vector first-query path for a new temporary server root around 66ms + after cache cleanup; +- cold perf operations now report `primaryLatencyPhase` and + `primaryLatencyMicros` so user-visible latency is separated from teardown. + The deeper local release run showed direct first-query totals were previously + inflated by a Rust-side host directory sync during query finish; +- direct `Oliphaunt` no longer calls host directory `sync_all` after every + non-transaction query. PostgreSQL's WAL/fsync path owns durability, and the + server path already avoided this extra host sync. In the local release run, + direct visible latency dropped from about 68ms to about 53ms for the first + new temporary root and to about 45ms for the second new temporary root; +- direct and server protocol timing now splits startup packet handling, + protocol input/output, guest `PostgresMainLoopOnce`, direct parse/describe, + direct execute, and direct result finish. The remaining first-query protocol + cost is mostly PostgreSQL main-loop work for the parse/describe or prepared + extended-query batch, not Rust parsing or buffer copies; +- `cargo run -p oliphaunt-perf -- warm` now measures true warm behavior separately + from first-open work: repeated direct scalar queries, direct transaction + batches, direct extension-backed queries, SQLx repeated queries over one + connection, SQLx repeated connect-query-close cycles, SQLx extension-backed + repeated queries, and tokio-postgres repeated queries. It reports total and + per-iteration average phases while keeping open/shutdown phases as context; +- `cargo run --release -p oliphaunt-perf -- bench` now provides a product-style + benchmark harness similar to Oliphaunt's published benchmark families. It runs + trimmed-average CRUD round-trip benchmarks and a generated SQLite + speedtest-style suite through both the direct Rust API and `OliphauntServer` + with a long-lived SQLx connection. The speed suite is generated locally + instead of vendoring Oliphaunt's multi-megabyte generated SQL files, and supports + `--suite`, `--mode`, `--iterations`, and `--scale` for local and CI runs; +- May 1, 2026 local release parity/timing run after pinning + `PG17 legacy lane@01792c31` recorded raw JSON under `target/perf/`: + `cold-release-latest.json`, `warm-release-latest.json`, and + `bench-release-latest.json`; +- that cold release run used existing caches and production artifacts, so C-side + backend timers were absent by design. Primary visible latencies were: + preload 28.8ms, first direct temporary query 41.1ms, second direct temporary + query 30.0ms, vector preload 8.4ms, first direct vector query 36.8ms, + first tokio-postgres server query 31.4ms, first SQLx server query 31.9ms, + first SQLx server vector query 36.9ms, and first SQLx vector query on an + existing persistent root 25.8ms; +- dominant cold phases in that run were production runtime/AOT preload + (`aot.deserialize.mmap` 16.3ms), Wasmer instance creation for new roots + (about 5.3-10.2ms), backend start for template roots (about 18-24ms), and + first protocol dispatch/query work (about 4.4-6.1ms). Per-root runtime setup + stayed below the 1ms reporting threshold for scalar temporary roots; +- warm release run with 100 query iterations and 20 connect iterations showed: + direct scalar repeated query average 0.024ms, direct transaction batch average + 0.022ms, direct vector repeated query average 0.025ms, SQLx single-connection + query average 0.054ms, SQLx vector single-connection query average 0.058ms, + tokio-postgres single-connection query average 0.175ms, and SQLx + connect-query-close average 18.565ms; +- product-style benchmark run with `--suite all --mode all --iterations 100 + --scale 1` showed RTT trimmed averages from about 0.031-0.101ms for direct + CRUD cases and about 0.055-0.130ms for SQLx server CRUD cases. The generated + speed suite remained dominated by indexed updates: direct 25k indexed update + 4.390s, direct 25k text indexed update 8.024s, SQLx server 25k indexed update + 4.350s, and SQLx server 25k text indexed update 8.057s; +- follow-up parity work found that the WASIX host was starting single-user + Postgres with `shared_buffers=400kB`, while `@electric/wasm@0.4.5` + reports `shared_buffers=128MB`. The fix moved the intended buffer GUCs into + the Rust startup arguments (`shared_buffers=128MB`, `wal_buffers=4MB`, + `min_wal_size=80MB`). The exact Oliphaunt speed-source rerun now records local + all-suite direct timings around 570ms for Test 9, 732ms for Test 10, 106ms for + Test 11, and 86ms for Test 14; SQLx server timings were about 593ms, 726ms, + 102ms, and 83ms for the same tests. + `perf diagnose-buffer-cache` verifies zero Postgres shared read blocks for the + table-copy hotspots after setup, matching Oliphaunt's effective buffer behavior; +- `xtask assets check` now guards production WASIX inputs for mandatory + WebAssembly exception and dynamic-linking flags and rejects Asyncify markers + in production configure scripts; +- production profile scripts reject Asyncify flag injection by default; the + 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, `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 + `cargo run -p xtask -- package-size --limit 10000000` passed against the + regenerated artifacts. + +## CI/CD And Release Workflow + +- CI now uses Moon as the source and task graph. The affected planner calls + Moon queries and maps tagged tasks to stable GitHub `Checks` jobs; product + jobs run `.github/scripts/run-moon-targets.sh`, which delegates execution to + `moon run`; +- GitHub matrix is reserved for real target fan-out: native runtime targets, + broker targets, Node direct targets, mobile build/E2E targets, and WASIX AOT + targets. There is no separate CI jobs graph; +- release-please manifest mode owns version bumps, changelog updates, release + PRs, and product tags. Product-local `release.toml` files own publish targets + and artifact metadata, while Moon production/peer scopes provide release + coupling; +- the manual Release workflow keeps protected `prepare-release-pr`, + `publish-dry-run`, and `publish` operations with job-scoped permissions, + trusted publishing, checksums, attestations, and product-native publish + commands; +- native runtime, broker, Node direct, mobile, and WASIX AOT artifacts are + selected by Moon affectedness and built through target metadata. Release jobs + download those CI artifacts into `target/release-assets/`, validate package + shape, and publish through the owning product release step; +- dependency invariant checks now block Wasmtime/static-WASI regressions and + backend compiler crates such as LLVM/Cranelift/Singlepass from entering the + 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 + `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 + assets plus the host AOT pack; +- `.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 + normal runtime tree keeps headless Wasmer loading, while + `xtask --features aot-serializer` is the only path that enables Wasmer LLVM; +- the WASIX runtime CI jobs now probe the LLVM AOT serializer before full AOT + generation, validates generated portable assets before AOT work, smokes the + target runtime before packaging/upload, and fails on empty/missing AOT + manifests instead of uploading placeholder crates; +- `wasmer-wasix` is now explicitly feature-minimized for the runtime path + (`sys-minimal`, `sys-poll`, `host-vnet`, and `time`). The root dependency gate + rejects Wasmtime, backend compiler crates, Cranelift/Singlepass, LLVM, and + broad HTTP/TLS stacks such as `reqwest`, `hyper`, and `rustls`; +- normal CI cache writes are limited to `main` while PRs still restore existing + Rust caches. Release and AOT-heavy jobs opt into cache writes explicitly. + +## Backlog Grooming Verification + +The implementation backlog was reconciled against the repository state and the +following completed work was removed from `TODO.md`: + +- current runtime-state notes for headless Wasmer AOT loading, pure MountFS + runtime composition, eager PGDATA overlays, direct scalar no-`pg_type` array + startup scans, no direct-query host `sync_all`, and shared + `BackendSession`/root-preparation paths. These are already covered by the + runtime, performance, and architecture sections above; +- CI/CD scaffolding that is present in the repo: Moon affected planning, + tag-driven exact Moon target execution, native/WASIX target matrices, source-only AOT + crate templates, staged release workspaces, exact-SHA artifact downloads, + package-size gates, and Release workflow trusted-publishing permissions + through `id-token: write`; +- public `pg_dump` functionality that is already implemented and tested: + `PgDumpOptions`, direct and server `dump_sql`/`dump_bytes`, the `oliphaunt-wasix-dump` + CLI, typed rejection of managed passthrough flags, no-clone/no-public-server + direct dumps, stock libpq over virtual networking, indexes/views/sequences, + `--schema-only`, `--quote-all-identifiers`, source-server reuse after dump, + and vector dump/restore coverage; +- split WASIX `initdb` runtime support that is present locally: direct and + server `fresh_temporary()`/`template_cache(false)` paths, split-initdb module + execution, interrupted-PGDATA cleanup, initdb shim ABI/source-spine checks, + and PGDATA template manifest checks. The remaining backlog tracks only full + WASIX runtime CI proof, deterministic two-build comparison, and package-size + impact; +- extension catalog/promotion infrastructure already in place: generated + catalog/build plans, `src/extensions/catalog/extensions.promoted.toml`, + `src/extensions/catalog/extensions.smoke.toml`, public constants for the 37 smoke-passed + exact extensions, candidate/private smoke gates, generated native-module + metadata, load-order metadata, and generated `wasix-dl` export lists; +- protocol and runtime guards already covered by tests or xtask checks: + SSLRequest, CancelRequest, Parse/Bind/Execute recovery, SQLSTATE preservation, + pipelined simple and extended query recovery, COPY streaming over TCP and Unix + sockets, bridge allocation accounting, startup role/database ownership, + export guards, longjmp boundary checks, broad-stub rejection, unsafe archive + rejection, unsupported-target errors, and package-size checks; +- performance tooling already available locally: `oliphaunt-perf cold`, + `oliphaunt-perf warm`, `oliphaunt-perf bench`, + `oliphaunt-perf prepared-updates --skip-native --gate`, primary-latency phase + reporting, production buffer-profile validation, warm server benchmarks, and + product-style SQLx/native/Oliphaunt comparison outputs. The remaining backlog + tracks turning these into stable GitHub-runner release gates. + +## Native SDK API Surface Inventory + +The native SDK parity track now has a no-build public surface inventory: + +- `tools/policy/generate-sdk-api-surface.mjs --write` regenerates + `src/docs/content/reference/sdk-api-surface.md` from the current Rust, Swift, Kotlin, and React + Native SDK sources; +- `tools/policy/check-sdk-parity.sh` runs the generator in `--check` mode so + accidental public symbol drift is visible in the fast parity gate; +- `docs/maintainers/sdk-parity-policy.md` links the inventory next to `docs/products/sdk-manifest.toml`, so + ownership, supported platform shape, and public API review evidence stay + together. + +## SDK Parity Edge-Case Tests + +Rust, Swift, Kotlin, and React Native now cover the edge cases that define the +current public SDK contract: + +- escaped transaction handles are rejected after rollback or commit; +- transaction-owned streaming uses the pinned session boundary; +- closing during an active transaction closes the session and rejects pinned + work instead of committing; +- PostgreSQL query cancellation remains a structured SQLSTATE `57014` + ErrorResponse on typed query paths; +- `connectionString` is present only on server-capable sessions that advertise + independent PostgreSQL client connections; +- startup `username` and `database` identity is first-class across Rust, Swift, + Kotlin, and React Native. Rust now feeds the configured identity through + direct, broker, and server startup paths, while mobile SDKs reject empty or + NUL-containing values before crossing native/TurboModule boundaries; +- backend query parsers now use strict UTF-8 semantics across Rust, Swift, + Kotlin, and React Native. Row-description C-strings and text accessors reject + malformed backend bytes instead of silently replacement-decoding them; +- simple-query protocol builders now reject NUL-containing SQL across Rust, + Swift, Kotlin, and React Native before constructing a frontend C-string frame; +- extended-query protocol builders now reject NUL-containing SQL and parameter + lists above the PostgreSQL protocol `Int16` limit across Rust, Swift, Kotlin, + and React Native before constructing frontend frames; +- typed query parsers now reject unexpected backend message tags across Rust, + Swift, Kotlin, and React Native instead of silently ignoring them, while + preserving known simple/extended-query control tags, validating async backend + control-message framing, and validating the `ReadyForQuery` + transaction-status byte; +- extension IDs are validated and normalized before public SDK open calls + cross into the engine or TurboModule boundary. + +## Native Mobile Exact-Extension Smoke Coverage + +The SDK parity track now has explicit mobile packaging smoke evidence for +selected and unselected extension assets: + +- Swift `OliphauntRuntimeResources` tests materialize a vector-selected resource + package and assert `vector.control` / `vector--1.0.sql` are present while + `hstore.control` stays absent; +- the Kotlin Android SDK check builds a synthetic runtime resources and verifies + generated Android assets preserve the selected vector extension control file + without leaking unselected hstore assets; +- the React Native Android SDK check performs the same assertion against the + produced AAR, proving React Native inherits the Kotlin packaging boundary + rather than carrying a private resource runtime. + +## Native Extension Asset Shape Guards + +The native extension release lane now rejects incomplete selected extension +asset sets before an app reaches `CREATE EXTENSION`: + +- create-extension assets must include both the control file and at least one + SQL install file when materializing runtime resources; +- loadable-module-only extensions such as `auto_explain` are still allowed to + omit create-extension SQL/control files; +- cached native runtimes are invalidated when a selected extension has + SQL/control assets but lacks the matching native module; +- cached native runtimes are also invalidated when a selected native-module + extension has a control/module pair but no SQL install file; +- the gated native extension matrix already covers install/load, reopen, + physical backup, restore, and restored reopen for direct, broker, and server + modes when extension artifacts are available. + +## Native Extension Dependency And Recovery Fixtures + +The extension release lane now has explicit evidence for the remaining negative +fixtures: + +- runtime materialization resolves extension dependencies before copying assets, + so selecting `earthdistance` requires `cube` SQL/control/module assets and + fails fast when the transitive dependency artifact set is incomplete; +- the gated native extension matrix reruns `CREATE EXTENSION ` after a + successful install for every create-extension row, asserts PostgreSQL returns + an `ErrorResponse` plus `ReadyForQuery`, and immediately proves the same + session can run a follow-up query; +- loadable-module-only extensions such as `auto_explain` remain outside the + repeated `CREATE EXTENSION` fixture because their product contract is `LOAD`. + +## Native Extension Preload Startup Proof + +Preload-required extensions now have fast source-level regression proof across +all native modes: + +- direct mode builds `shared_preload_libraries=pg_search` into the C ABI + `oliphaunt_init` startup argument vector before the embedded backend starts, and + deduplicates repeated selections; +- broker mode forwards the resolved extension list, including `pg_search`, to + the helper process before the helper opens its direct-mode backend; +- server mode builds the same preload setting into the `postgres -c ...` + startup arguments before spawning the local PostgreSQL-compatible server; +- extensions that do not require preload hooks, such as `graph`, do not add a + `shared_preload_libraries` setting. + +## Native Extension Size Report + +Rust SDK resource outputs now include selected-extension size evidence as part +of the portable Swift/Kotlin/React Native handoff: + +- `package_native_resources(...)` writes `oliphaunt/package-size.tsv` and + exposes `NativeRuntimeResources::size_report`; +- the report records runtime, template PGDATA, static-registry, package, + de-duplicated selected-extension, and per-extension asset bytes; +- per-extension rows count the concrete resolved extension assets for each + selected extension, including required dependencies, while the selected + extension row is de-duplicated for total app-size impact; +- the `oliphaunt-resources` CLI prints `packageSizeReport=...`, + `selectedExtensionBytes=...`, and `extensionBytes=:` so CI + can diff package-size impact without custom filesystem walkers. +- Swift consumes the same TSV through + `OliphauntRuntimeResources.packageSizeReport()` and tests both valid and + malformed report parsing; +- Kotlin Android consumes it through `OliphauntAndroid.packageSizeReport(context)` + with Android unit coverage for the parser; +- Kotlin and React Native Android packaging checks now preserve + `assets/oliphaunt/package-size.tsv`, so mobile artifacts carry the Rust + runtime-resource generator's extension byte evidence instead of forcing SDK-specific + resource walkers. + +## Native PostgreSQL Patch-Stack Review Gate + +The native PostgreSQL 18 patch stack now has deterministic source-only release +evidence: + +- `src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write` generates + `docs/internal/OLIPHAUNT_PATCH_STACK.md` from `src/runtimes/liboliphaunt/native/postgres18/source.toml` + and the maintained patch directory; +- `src/runtimes/liboliphaunt/native/tools/check-track.sh` runs the same script in `--check` + mode before native Rust or SDK checks, so stale patch review evidence fails + fast without rebuilding PostgreSQL; +- `source.toml` now lists every maintained patch, including the static extension + loader patch, and the static loader patch has the same deterministic + `Subject: [PATCH] liboliphaunt-native: ...` and `diff --git` metadata as the rest of + the series; +- the generated review lists patch order, subject lines, changed upstream files, + and patch-introduced `oliphaunt_*` symbols, while rejecting SDK/runtime/product + terms that belong above PostgreSQL. +- the patch-stack review now assigns each audit requirement to its owning patch + or patches and verifies the required evidence inside those patches, so host + I/O, lifecycle, cleanup, cwd restore, runtime-path, static-extension, shell + exclusion, Android shared-memory, and event-trigger changes stay independently + reviewable; +- the checker now rejects unexpected upstream PostgreSQL touchpoints unless + they are added to the expected touchpoint table with a rationale, preventing + quiet patch-stack growth. + +## Public C ABI Conformance Gate + +`liboliphaunt` now has a consumer-style C ABI conformance check that compiles and +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/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 + +The Rust SDK now covers cancellation while a direct-mode streaming response is +active: + +- `streaming_cancel_uses_out_of_band_cancel_and_releases_owner` proves the + owner executor can cancel a large active stream without queueing cancellation + behind the stream callback, then accepts follow-up raw protocol work on the + same handle; +- the native `OliphauntRuntime` smoke path now runs a streaming + `pg_sleep`/large-response query, cancels it through the opened handle, checks + for PostgreSQL's query-canceled response, and verifies the backend can execute + another query afterward. + +## Broker And Server Cancellation Reuse Regression + +Native runtime cancellation coverage now proves the process-isolated modes keep +their real PostgreSQL semantics after interruption: + +- broker mode records `pg_backend_pid()` before repeated `Oliphaunt::cancel()` + calls, runs cancellation and recovery through the helper process, then asserts + the same helper/backend identity is still serving the handle afterward; +- server mode's external Tokio PostgreSQL client smoke starts independent + clients, issues a PostgreSQL `CancelRequest` through the client cancel token + after startup, asserts SQLSTATE `QUERY_CANCELED`, and then reuses that client + for another query. + +## Direct And Broker Transaction Failure Regressions + +The Rust SDK now exercises serial-session transaction failure semantics across +both `NativeDirect` and `NativeBroker` modes: + +- `with_transaction_commits_rolls_back_and_rejects_unpinned_interleaving` now + runs for direct and broker mode configurations, proving body failure rolls + back and releases the single physical session in both modes; +- `transaction_commit_and_rollback_failures_release_serial_session` uses a + scripted runtime to force `COMMIT` and `ROLLBACK` failures, proving failed + commit attempts trigger best-effort rollback and failed rollback attempts + still release the session pin before subsequent work; +- dropped transactions and close-while-transaction-active coverage now run for + both direct and broker mode configurations instead of only the default direct + mode. + +## Native Server Lifecycle Regressions + +Server mode now has explicit lifecycle evidence for the connection string and +external clients: + +- server smoke asserts the opened handle's `connection_string()` matches the + advertised capability value and remains stable after protocol, streaming, + cancellation, and backup work; +- the Tokio PostgreSQL external-client smoke asserts the connection string stays + stable after independent clients use the server; +- SQLx pool coverage now closes the owned server while the pool is alive and + verifies subsequent pool work is rejected; +- active external client coverage starts a long-running PostgreSQL query through + `tokio-postgres`, closes the owned server, verifies the active query is + interrupted, and verifies the external client cannot run more SQL afterward. + +## Native Root Manifest Gate + +Live native roots now carry a root-owned `manifest.properties` compatibility +record: + +- root preparation adopts existing PostgreSQL 18 roots by writing the manifest + under the native root lock; +- direct, broker-helper, and server paths reject roots whose manifest or + `pgdata/PG_VERSION` targets a PostgreSQL major other than 18 before exposing + the engine; +- initdb-style uninitialized roots are marked as pending and refreshed after + direct/server initialization observes `PG_VERSION`; +- unit coverage validates adoption, uninitialized-to-initialized refresh, and + incompatible PGDATA rejection, while broker smoke corrupts the manifest and + verifies `existing_only()` fails during helper startup. + +## Physical Archive Compatibility Metadata + +Native physical backups now carry compatibility metadata instead of relying on +implicit PGDATA shape alone: + +- Rust direct, broker, and server backup paths annotate physical archives with + the root `manifest.properties` plus `.oliphaunt/backup-manifest.properties`; +- the backup manifest records PostgreSQL major/version number, PGDATA version, + server encoding, locale, data-checksum state, active + `shared_preload_libraries`, required preload libraries, selected extensions, + and installed PostgreSQL extensions; +- restore validates root and backup manifests before publishing a target root, + while still accepting legacy archives that only contain `pgdata/**`; +- legacy restores adopt a current root manifest during staging, so restored + roots satisfy the same open-time root gate as newly bootstrapped roots; +- C archive restore now accepts the same root-level metadata paths, keeping the + archive shape compatible with the C ABI boundary. + +## React Native Package-Size Report Parity + +React Native now exposes the same package-size evidence as the platform SDKs +instead of merely preserving the TSV inside app artifacts: + +- the TypeScript SDK exposes `Oliphaunt.packageSizeReport(...)` with a typed + `PackageSizeReport`/`ExtensionSizeReport` shape; +- RN Android delegates report lookup to `OliphauntAndroid.packageSizeReport(...)`, + including a Kotlin SDK resource-root overload for local unpacked package + smoke tests; +- RN iOS delegates report lookup to `OliphauntRuntimeResources.packageSizeReport()` + through the existing Swift adapter; +- SDK parity checks, RN unit tests, Android boundary tests, and generated API + surface docs now guard the public report API and platform delegation. + +## Native Extended-Protocol Recovery Regression + +The native SQL regression now exercises raw extended protocol error recovery +across every mode that the env-gated test can open: + +- a failing `Parse` request must return an `ErrorResponse` followed by + `ReadyForQuery`, and the same session must accept a normal query afterward; +- a valid named prepared statement is kept alive across a failing `Bind` with an + invalid integer parameter; +- the same prepared statement is then rebound and executed successfully, + proving direct, broker, and server paths recover without losing the physical + session or desynchronizing the PostgreSQL frontend/backend protocol. + +## Native Privilege, Utility, And Lock Regression + +The native SQL regression now also covers more PostgreSQL behavior that has to +work identically through direct, broker, and server modes: + +- role creation, `SET ROLE`, schema/table grants, and structured `42501` + privilege errors for disallowed writes; +- SQL-language functions over composite table rows; +- post-error session reuse after a privilege failure; +- standalone `VACUUM` and `ANALYZE` utility commands; +- transactional table locks plus session advisory lock/unlock behavior. + +## Native Broker Root Metadata Recovery + +Broker-mode root metadata now has explicit PGDATA-version recovery coverage: + +- a broker-opened persistent root records `pgdataVersion=18` in + `manifest.properties`; +- corrupting that manifest to claim `pgdataVersion=17` is rejected before the + helper reopens the root; +- restoring the valid manifest lets the same broker path reopen the root and + read previously committed data, proving failed metadata validation does not + leave the root unusable. + +## Native Performance Evidence Classification + +The native benchmark matrix now classifies benchmark plans before running +expensive work: + +- default direct/broker/server and rtt/speed/streaming/prepared runs are marked + `releaseEvidence=1` only when they meet the release minimums: 100 RTT samples, + 10 RTT repeats, 25,000 prepared-update rows, 10 prepared repeats, and 20 speed + repeats; +- quick all-mode runs are marked as diagnostic but not partial, so maintainers + can use them for plumbing checks without mistaking them for release evidence; +- focused runs are marked `partialReport=1` and `diagnosticRun=1`; +- the no-build perf harness verifies these classifications through + `--plan-only`, so the evidence contract is checked without rebuilding or + running the full matrix. + +## Native Performance Report Release Gate + +The native perf report validator now rejects weak evidence by default: + +- `tools/perf/check-native-perf-report.sh` passes + `--require-release-evidence` to the provenance verifier; +- release verification requires `releaseEvidence=true`, `partialReport=false`, + `diagnosticRun=false`, all native engines, all benchmark suites, SQLite and + prepared-update controls, and counts at or above the recorded release + minimums; +- focused diagnostic reports can still verify source/artifact provenance with + `OLIPHAUNT_PERF_ALLOW_DIAGNOSTIC=1`, but that path is explicitly not + release evidence; +- the no-build perf harness creates temporary release and diagnostic provenance + fixtures and proves the strict verifier accepts only the release fixture. + +## Kotlin Native-Direct Owner Thread Tightening + +Kotlin direct runtimes now route every handle-bound native call through the +dedicated owner thread: + +- Kotlin/Native capabilities are serialized through the same owner dispatcher + and execution mutex as raw protocol, streaming, backup, and close; +- Android JNI opens the backend on the session's single-thread dispatcher + instead of opening on the caller coroutine and only moving later work; +- Android capability reads also run on that dispatcher, leaving only + out-of-band cancellation outside the owner queue; +- Android native-direct session backup now rejects unsupported formats before + crossing JNI, matching Kotlin/Native's defensive session boundary. + +## React Native Exact-Extension Bridge Validation + +React Native native adapters now preserve extension validation instead of +silently dropping malformed bridge values: + +- iOS rejects non-array or non-string `extensions` before opening through + `Oliphaunt`; +- Android rejects non-array or non-string `extensions` before opening + through `OliphauntAndroid`; +- the SDK parity guard rejects the previous lossy `compactMap`/nullable + `getString` patterns. + +## React Native Startup Identity Bridge Validation + +React Native native adapters now validate startup identity before platform SDK +open calls: + +- iOS preserves empty `username`/`database` values instead of converting them to + `nil` and falling back to `postgres`; +- iOS and Android reject blank or NUL-containing startup identity at the native + adapter boundary before opening through Swift or Kotlin; +- the SDK parity guard checks that the native adapters keep those bridge + validations in place. + +## React Native Scalar Config Bridge Validation + +React Native native adapters now reject malformed scalar config values instead +of treating them as omitted: + +- iOS no longer uses a lossy optional string cast for open/runtime resource + fields; +- iOS rejects blank `resourceRoot`/`iosResourceRoot` before falling back to + bundled resources; +- Android rejects non-string scalar values with an explicit bridge error before + opening through `OliphauntAndroid`; +- the SDK parity guard rejects the previous lossy Swift cast pattern and checks + scalar validation in both native adapters. + +## React Native Native Override Path Validation + +React Native now rejects malformed native override paths before they can suppress +default native resolution: + +- TypeScript rejects blank or NUL-containing `libraryPath`, `runtimeDirectory`, + and open-time `resourceRoot` before crossing the TurboModule boundary; +- restore validates `libraryPath` before forwarding the C ABI override; +- iOS and Android adapters repeat the same blank/NUL checks at the native bridge + boundary so direct native calls cannot bypass the JS guard. + +## SDK Root Path NUL Validation + +Rust, Swift, Kotlin, and React Native now reject NUL-containing database roots +before filesystem work, platform engine calls, TurboModule calls, or C ABI +conversion: + +- Rust validates persistent open roots before native runtime selection and + restore target roots before physical archive unpack; +- Swift validates file URL roots for `OliphauntDatabase` and the native direct + engine before open/restore work reaches the C bridge; +- Kotlin validates common open/restore roots and repeats the guard in native and + Android direct engines; +- React Native validates open/restore roots in TypeScript and Android native + adapter code, while the iOS adapter routes path values through the same + NUL-aware helper used for native override paths. + +## Rust SDK Direct Session And Extension Manifest Guardrails + +The Rust SDK now rejects two malformed native-product inputs before they reach +runtime, filesystem, or extension packaging work: + +- `NativeDirect` and `NativeBroker` accept exactly one logical client session; + requesting zero sessions now fails at `OpenConfig::validate`, and requesting + more than one still returns the explicit unsupported-session error; +- Rust SDK-owned `initdb`, broker helper, and server executable paths reject + empty or NUL-containing values before process startup; +- static and signed-dynamic extension manifest paths reject embedded NUL + bytes during config validation, and direct manifest loading rejects the same + malformed paths before attempting any filesystem read. + +## React Native JSI Transport Selection + +React Native now has a real TypeScript-side fast-path selector for raw protocol +bytes: + +- the public `execProtocolRaw` API probes the versioned + `globalThis.__oliphauntReactNativeJsi` host transport and sends `Uint8Array` + requests without base64 when it is installed; +- capability normalization reports `rawProtocolTransport = "jsi-array-buffer"` + for opened handles and supported-mode discovery when that host transport is + available; +- the Codegen TurboModule no longer exposes base64 binary methods; protocol, + backup, and restore bytes require the JSI transport, and tests reject missing + or non-binary host transports before native sessions are used; +- the iOS adapter exposes an `NSData` raw-protocol handoff into `Oliphaunt`, + and the Android adapter exposes a `ByteArray` handoff into the Kotlin SDK + session, so the native JSI installer can avoid base64 without creating a + private React Native database runtime. + +## React Native Native JSI Installers + +React Native New Architecture builds now install the high-throughput +`jsi-array-buffer` transport on both native platforms: + +- iOS implements `RCTTurboModuleWithJSIBindings`, installs + `globalThis.__oliphauntReactNativeJsi`, accepts ArrayBuffer/typed-array + requests, and resolves promises with ArrayBuffer responses from the Swift + `Oliphaunt` adapter; +- Android implements `TurboModuleWithJSIBindings`, builds a small + ReactAndroid Prefab C++/JNI library, accepts ArrayBuffer/typed-array + requests, and resolves promises through the Kotlin SDK + `OliphauntDatabase.execProtocolRaw` byte-array hook; +- the React Native package check generates the real iOS Codegen header and + syntax-checks the `RCT_NEW_ARCH_ENABLED` Objective-C++ path, builds the + Android C++ JSI installer through the selected debug ABI matrix, and still + rejects any duplicate React Native Android native database runtime. + +## React Native Binary Transport Guard + +React Native now treats Codegen as lifecycle/control glue only and enforces the +JSI binary path in validation: + +- `src/sdks/react-native/tools/check-sdk.sh` fails if `execProtocolRaw`, + streaming, backup, or restore byte methods are added to the TurboModule + Codegen spec; +- the same check rejects base64, `atob`/`btoa`, or Node `Buffer` binary + conversion in React Native runtime source; +- PostgreSQL protocol, backup, and restore traffic therefore remain on the + versioned JSI `ArrayBuffer` transport while `open`, `close`, `cancel`, + `capabilities`, mode discovery, and package-size reporting keep the official + TurboModule Codegen surface. + +## SDK Package Artifact Checks + +Every native SDK lane now has a fast package-surface check before broader +release publishing: + +- Rust records and verifies `cargo package -p oliphaunt --list`, requiring + the public SDK sources/tests and rejecting product-external generated trees; +- Swift archives a sanitized SwiftPM source package from a scratch copy, + verifies `Package.swift`, podspecs, C bridge, Swift sources, and tests are + present, and rejects generated `.build`/`.swiftpm` content; +- Kotlin assembles the Multiplatform metadata/source jars, JVM jar, Android + release AAR, and macOS/native source jar, then verifies common/platform API + files, metadata linkdata, JVM unavailable-runtime classes, selected Android + JNI adapter ABIs, and absence of bundled PostgreSQL runtime binaries by + default; +- React Native already inspects `npm pack --dry-run --json`, so the SDK parity + gate now requires all four package artifact checks to remain in place. + +## SDK README Example Coverage + +The SDK parity gate now mechanically links public README code examples to +compiled or tested SDK coverage: + +- Rust, Swift, Kotlin, and React Native README code blocks carry + `liboliphaunt-doc-example:` markers; +- `tools/policy/check-sdk-doc-examples.mjs` rejects unmarked Rust/Swift/Kotlin/ + TypeScript README examples, duplicate IDs, stale coverage markers, and + examples without SDK test/source coverage; +- the current coverage set includes Rust backup/restore and typed-query + examples, Swift open/raw/streaming/typed/parameterized examples, Kotlin + Android-open/streaming/typed/parameterized examples, and React Native + open/query plus parameterized query examples. + +## Android Fast ABI Validation + +Android SDK validation now supports a shared Gradle ABI filter for the Kotlin +SDK, React Native adapter, and delegated Kotlin runtime: + +- `src/sdks/react-native/tools/check-sdk.sh` defaults Android Gradle/CMake work + to one ABI selected from the host CPU for fast local iteration; +- `src/sdks/kotlin/tools/check-sdk.sh` uses the same default for Kotlin Android + validation; +- `OLIPHAUNT_REACT_NATIVE_ANDROID_ABI_FILTERS=all` or + `OLIPHAUNT_KOTLIN_ANDROID_ABI_FILTERS=all` restores every locally buildable + ABI; the published Android support envelope remains `arm64-v8a,x86_64`; +- comma-separated subsets are forwarded as + `-PoliphauntAndroidAbiFilters=...`, and both + `src/sdks/react-native/android` and `src/sdks/kotlin/oliphaunt` validate the + same supported ABI set. + +## React Native JSI Argument Hardening + +The React Native New Architecture transport now validates numeric JSI arguments +before crossing into Swift or Kotlin: + +- iOS and Android reject non-finite, fractional, negative, or unsafe database + handles before native handle casts; +- typed-array `byteOffset` and `byteLength` values are checked as + non-negative integers before copying protocol, backup, or restore bytes; +- the React Native SDK check guards both native JSI installers for these + validations, and the Android boundary test asserts the same invariant for the + Kotlin-backed adapter. + +## React Native Installed-App Smoke Entrypoint + +React Native now ships a reusable app/device smoke runner instead of leaving +each app to invent its own native-boundary test: + +- `runInstalledOliphauntReactNativeSmoke(...)` uses the installed package singleton, + so it proves the app's TurboModule/JSI installation path rather than a mock + client; +- the smoke opens a delegated Swift/Kotlin SDK session, checks the expected + engine and `jsi-array-buffer` transport, runs `SELECT 1`, runs a parameterized + query, optionally requires packaged resource-size evidence, records JS timer + progress, and always closes the database; +- the pure `runOliphauntReactNativeSmoke(client, ...)` form is covered by the + React Native TypeScript tests, while the remaining release gap is wiring that + installed-app runner into iOS simulator/device and Android emulator/device CI + jobs with real packaged `liboliphaunt` runtime resources. + +## Native Direct Reopen Capability + +The SDKs now publish reopenability as an explicit capability instead of letting +apps infer it from process isolation: + +- Rust `EngineCapabilities`, Swift `OliphauntCapabilities`, Kotlin + `EngineCapabilities`, and React Native `EngineCapabilities` all expose + `reopenable`; +- `NativeDirect` reports `false` because the embedded PostgreSQL backend is a + process-lifetime direct session, while `NativeBroker` and `NativeServer` + report `true`; +- a local C-core experiment removed the process-spent guard and crashed on the + second same-process direct open in PostgreSQL relation/storage startup, which + shows this is not an fd-table-only reset problem. Broker/server remain the + robust close/reopen paths. + +## Expo Android Installed-App Smoke Harness + +React Native now has a repeatable real-app Android validation path instead of +only package-level checks: + +- `src/sdks/react-native/examples/expo` is an Expo SDK 56 development-build + app pinned to React Native 0.85 and the local packed + `@oliphaunt/react-native` SDK, and its app smoke now calls the installed + package runner directly before attaching the example's CRUD/perf workload via + the same live NativeDirect session; +- `src/sdks/react-native/tools/expo-android-runner.sh` packs the RN SDK when + sources changed, installs the tarball into the example, packages Android + `liboliphaunt.so` plus runtime/template PGDATA assets, builds the dev-client + APK, launches it through the Expo development-client deep link, waits for + `OLIPHAUNT_EXPO_SMOKE_PASS`, and prints APK/package size plus Android + PSS/RSS; +- the smoke generates the ignored Expo `android/` project on demand, so a clean + checkout does not need committed native project output before app-level + validation can run; +- `pnpm --dir src/sdks/react-native/examples/expo run smoke:android` exposes the same + installed-app gate as a named validation lane, and SDK parity checks require + the harness, docs, example command, and machine-readable pass signal to stay + present; +- the installed app has validated the New Architecture JSI `ArrayBuffer` + transport, delegated Kotlin runtime path, `SELECT 1`, parameterized query, + 100-row transaction insert, select p90, package-size reporting, and JS timer + liveness on an Android emulator. + +## Expo iOS And MCP Smoke Harness + +React Native now has the matching iOS app-level validation scaffold and local +Expo MCP tool path: + +- `src/sdks/react-native/tools/expo-ios-runner.sh` packages the RN SDK with an + iOS `oliphaunt/` resource bundle, accepts only an iOS + XCFramework/framework or an iOS/iOS-simulator dylib, and rejects macOS + `liboliphaunt.dylib` artifacts before CocoaPods/Xcode work starts; +- the iOS harness patches only the ignored generated Expo `ios/Podfile` to use + local `COliphaunt` and `Oliphaunt` pods, runs `pod install`, builds the + dev-client app, and can launch a booted simulator to wait for + `OLIPHAUNT_EXPO_SMOKE_PASS`; +- `OLIPHAUNT_EXPO_IOS_BUILD_ONLY=1` keeps the harness useful on machines where + CoreSimulator is unavailable, while still validating generated-spec, + CocoaPods, Swift SDK, resource bundle, and Xcode integration; +- the React Native iOS Swift adapter now discovers bundled `liboliphaunt` from the + packaged resource root or app frameworks when `libraryPath` is not supplied, + so app developers do not need host-environment library overrides for normal + packaged builds; +- `src/sdks/react-native/examples/expo` installs `expo-mcp` and exposes + `npm run mcp:start`, which runs + `EXPO_UNSTABLE_MCP_SERVER=1 expo start --dev-client` for Codex/MCP-driven + local logs, DevTools, screenshots, and automation. + +## Apple Mobile Template-Only Bootstrap Guard + +The C layer now enforces the mobile bootstrap model before the full iOS +PostgreSQL artifact lane exists: + +- `src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c` compiles out the `fork`/`exec` initdb + path on Apple mobile platforms and returns an actionable error when PGDATA has + 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/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 + +React Native no longer advertises streaming based on the raw owned-response +transport alone: + +- the TypeScript transport reports `protocolStream=true` only when the installed + versioned JSI transport exposes `execProtocolStream`; +- iOS installs an `execProtocolStream` host function that delegates to + `OliphauntCore.execProtocolStream` and calls the JS chunk callback for each + native chunk; +- Android installs the same host function through JNI, delegates to + `OliphauntDatabase.execProtocolStream`, and keeps stream completion separate from + chunk emission; +- package tests cover native chunk use, malformed chunks, and JS chunk callback + failures, while Android boundary tests keep the Kotlin and C++ JSI stream + hooks present. + +## Native Regression Coverage Expansion + +The native regression backlog now has concrete coverage instead of a product +placeholder: + +- `src/sdks/rust/tests/native_sql_regression.rs` runs a broader curated + PostgreSQL suite across `NativeDirect`, `NativeBroker`, and `NativeServer` + when native artifacts are available, covering domains, enums, generated + columns, deferrable uniqueness, foreign keys, JSONB, range types, recursive + CTEs, window functions, lateral joins, partial expression indexes, `MERGE`, + privileges, utility commands, table locks, advisory locks, COPY success, + COPY input errors, COPY fail recovery, streaming `COPY TO STDOUT`, extended + protocol parse/bind error recovery, and post-error session reuse; +- `src/sdks/rust/tests/protocol_parser_fuzz.rs` adds deterministic + fuzz-style corpora for backend query response parsing, mutated valid backend + frames, and frontend simple-query request construction, proving parser paths + return structured errors instead of panicking on malformed bytes; +- `src/sdks/rust/tests/sdk_shape.rs` now has optional native-server + compatibility smokes for `tokio-postgres` prepared/pipelined clients and + `pg_dump`, alongside the existing SQLx pool and `psql` checks. These tests + skip cleanly when the native artifact or matching PostgreSQL tools are absent. + +## Runtime Footprint Profiles And Startup GUC Overrides + +Rust, Swift, Kotlin, and React Native now expose the same startup-tuning shape: + +- Rust adds `RuntimeFootprintProfile`, `PostgresStartupGuc`, builder + `runtime_footprint(...)`, and builder `startup_guc(...)`/`startup_gucs(...)`; +- direct and broker pass profile/durability/explicit GUCs through the existing + C ABI startup-arg vector, while broker forwards them to helper restarts and + server mode preserves its `max_client_sessions` contract as `max_connections`; +- Swift, Kotlin, and React Native expose matching profile and startup-GUC + configuration, validate names/values before native open, and default mobile + SDK opens to `balanced` durability with the `balancedMobile` footprint; +- docs now describe the throughput, balanced-mobile, and small-mobile profiles + plus the override precedence for benchmark matrices. + +The native perf harness now accepts the same tuning shape: + +- `oliphaunt-perf native-liboliphaunt` and `oliphaunt-perf native-postgres` accept + `--runtime-footprint` and repeatable `--startup-guc name=value`; +- native benchmark JSON includes `nativeTuning` with profile, explicit + overrides, SDK startup assignments, and native-PostgreSQL control assignments; +- the release matrix script records profile/GUCs in its plan, provenance, and + markdown summary, and forwards the tuning to direct, broker, server, prepared, + streaming, and native-PostgreSQL control runs; +- Expo Android/iOS smoke and benchmark harnesses forward durability, runtime + footprint, and startup GUCs through Metro env and dev-client links; +- `tools/perf/matrix/run_mobile_footprint_matrix.sh` enumerates the requested + Android/iOS shared-buffer, WAL-buffer, WAL-minimum, and Safe/Balanced device + sweep. It skips `min_wal_size=8MB/16MB` by default because the current PG18 + artifact uses 16MB WAL segments and PostgreSQL rejects those GUC-only minima. + +## Explicit Lifecycle Capability Vocabulary + +The SDK contract no longer relies on a single ambiguous `reopenable` boolean: + +- Rust `EngineCapabilities` now exposes `same_root_logical_reopen`, + `root_switchable`, and `crash_restartable`; +- Swift, Kotlin, and React Native expose matching camelCase fields in their + capability structs/dictionaries; +- native direct reports same-root resident logical reopen only, with + `rootSwitchable=false` and `crashRestartable=false`; +- broker reports process isolation, root-switchability, and helper + crash-restartability; server reports root-switchability but no in-place + crash restart for the current SDK-owned server handle; +- SDK tests and docs assert these semantics so mobile callers do not infer crash + isolation from direct-mode logical close/reopen. + +## Exact Extension Packaging Recipes + +Public extension and SDK docs now describe exact-extension packaging without a +pack/group concept: + +- `src/docs/content/reference/extensions.md` documents Rust runtime-resource generation, + prebuilt third-party artifacts, mobile static registry generation, package + size reports, and exact selected extension manifests; +- the Swift README documents `COliphaunt`/`Oliphaunt` CocoaPods resource + packaging, selected iOS extension XCFramework placement, and link-time + failure for missing selected modules; +- the Kotlin README documents Android runtime resources, `jniLibs`, selected + extension archives, and `liboliphaunt_extensions.so` generation; +- the React Native README documents that RN delegates extension packaging to + Swift/Kotlin, does not ship native runtime or extension assets implicitly, + and uses the same exact SQL extension names as the platform SDKs. diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md new file mode 100644 index 00000000..4ef0cefa --- /dev/null +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -0,0 +1,3216 @@ +# Example and Release Validation Tasks + +> Archived task history; non-normative. Use `.codex/skills/` and +> `docs/maintainers/README.md` for current procedures. + +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. + +## Historical Continuation Queue: 2026-06-27 + +This section records the working queue used for the 2026-06-27 validation pass. +All items below are historical evidence; use the current maintainer procedures +and executable gates for fresh validation. + +### 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` 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`, `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. +- [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 + WASIX artifact package graph in release rendering, staged-artifact validation, + and example lockfile validation. +- [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 + `runtimeFeatures` through the native spec, Android bridge, iOS bridge, and JS + normalization. +- [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. +- [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 + 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`. + +### P2: Cleanup and Tooling Migration + +- [x] Run targeted dead-code detection for Rust, TypeScript/JavaScript, shell, + Python, and release helpers. +- [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. +- [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. +- [x] Re-run Linux CI-like and release/local-registry lanes after each tooling + migration batch. + +### 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 + 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 + 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 + 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 + `--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 + 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 + `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 + `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 + `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 + 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 + `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 + 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 + `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 + 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 + selected products are entirely in the low-risk SDK set currently owned in Bun: + `oliphaunt-js`, `oliphaunt-kotlin`, `oliphaunt-react-native`, + `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 + 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` + 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. 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 + 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, + 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 + 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 + 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 + `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 + operations through `tools/dev/bun.sh tools/release/release-publish.mjs`. + 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 + `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: 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 + 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 + `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-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; 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 + 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 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: 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 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; + 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-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-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-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. 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. 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 + 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-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 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 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 + 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-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 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 + 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-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`. + 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 + 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 + 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 + 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 + 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. + 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 + `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 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, + 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 + 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 + 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 + 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, + 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`, + `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: 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. `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 at the time between + `tools/dev/bun.sh tools/release/release_plan.mjs --base-ref origin/main + --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`, + `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 + 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, + release-intent checker, CI summary action, maintainer release docs, and + 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`, 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`, + `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 + 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, + 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 + 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 + 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 + 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 + 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 + 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 + `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 + 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 + --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 + --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 + [--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 + 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 + [--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, + 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 + `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 + `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 + `/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 + `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 + `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 + `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 + 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 + 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`. + 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 + 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: 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 + 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 + 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 + 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: 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 + 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 + 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 + `--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, + 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 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 + `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, + Python, and JavaScript helper entrypoints and reports low-reference + 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 + --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 + `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: 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 + `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 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 + 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 + 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: 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 + 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 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 + 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 + 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 + 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 + 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 + `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 + 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 + 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 + `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.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/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: 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: 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 + 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.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 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/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 + 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: 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: 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 + 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: 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`, + `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` 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-*` + 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`; + 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: 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: 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: 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: 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: 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: 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: 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/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 + 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 + `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 + 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: 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: 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: 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: 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`, + `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`, + `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 + 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: 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: 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/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`, `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 + `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 + 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. +- 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. +- 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: 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 + 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. +- 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 + +- [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` 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. +- [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 + +- [x] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. +- [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. + +## Priority 2: CI and Release Shape + +- [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. +- [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. +- [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. +- [x] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. +- [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. +- [ ] 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. +- [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 + +- [ ] 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. +- [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. +- [ ] Validate local release dry-run lanes with local registry publishing after script conversions. + +## 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")`. + 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 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 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, + 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 + `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. +- 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 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 + `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`, + `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 + 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. +- 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. +- 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 + 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. +- 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. +- WASIX Cargo artifact package-family checks now derive the portable runtime, + 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 + `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 + 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/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 + 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. +- 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. +- 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 `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 + 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 `moon` on `PATH`; it never consults a + mutable home-directory proto installation. +- 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`. +- `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. +- `.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. +- `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. +- `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. +- 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`. +- 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. +- 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 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 `release.py` last, when the underlying validators and registry helpers + have Bun entrypoints; + 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 + 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. + 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, + 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 + 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. +- 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. +- 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_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` + 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 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. +- 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. +- 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. + 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. +- 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`. +- 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, 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`. +- 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. +- 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`). 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`. 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 + 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 + 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 + 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 + 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 + 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. +- 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 + 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 + 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. +- 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. +- 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 + 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, + 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. +- 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. +- 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. +- 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. +- 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. + 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, 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. +- `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`, + `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. +- 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. +- 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. +- 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. +- 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. +- 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. +- 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` 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`. +- 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. +- 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. +- 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. +- 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. `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, 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` 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 + 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, + 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. +- 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. +- 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. +- 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. +- 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`. +- 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. +- 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`. +- 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 + `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`. +- 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`. +- 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`. +- 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`. +- 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`. +- 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`. +- 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`. +- 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. +- 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/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md new file mode 100644 index 00000000..41a23862 --- /dev/null +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -0,0 +1,1209 @@ +# Archived Final Source Architecture Implementation Checklist + +> **Archived implementation history; non-normative.** Do not use the counts, +> run results, checked boxes, or "Immediate Next Work" below to plan current +> work. Start with the [maintainer index](../maintainers/README.md) and the +> executable sources it identifies. + +This was the implementation checklist used while making the repository match +`docs/architecture/final-product-source-architecture.md`. Its checked state is +historical evidence, not a statement about the current tree. Use the current +maintainer index and executable validation for present-day decisions. In +particular, derive CI selection from [`tools/graph/ci_plan.mjs`](../../tools/graph/ci_plan.mjs), +extension membership from the +[`src/extensions/catalog`](../../src/extensions/catalog) and product target +manifests, artifact/package targets from +[`tools/release/release-artifact-targets.mjs`](../../tools/release/release-artifact-targets.mjs), +and release procedure from the [release runbook](../maintainers/release.md). +Numbers such as the historical 39-extension total are snapshots and are +intentionally not maintained here. + +## Status Legend + +- `[x]` implemented and locally verified in this branch. +- `[~]` implemented or partially implemented, but needs broader evidence. +- `[ ]` missing, contradictory, or not yet proven. + +## Source Shape + +- [x] Product source roots live under `src/`: + - `src/postgres/versions/18/` + - `src/sources/` + - `src/extensions/` + - `src/runtimes/liboliphaunt/native/` + - `src/runtimes/liboliphaunt/wasix/` + - `src/runtimes/broker/` + - `src/runtimes/node-direct/` + - `src/sdks/rust/` + - `src/sdks/swift/` + - `src/sdks/kotlin/` + - `src/sdks/react-native/` + - `src/sdks/js/` + - `src/bindings/wasix-rust/` + - `src/shared/contracts/` + - `src/shared/extension-runtime-contract/` + - `src/shared/fixtures/` + - `src/docs/` +- [x] Generated local state is ignored and untracked. Evidence: + `bash tools/policy/check-repo-structure.sh` passes, and tracked-file scans + find no root `assets/`, root `crates/`, root `sdks/`, root runtime build + trees, or generated local state under product source roots. +- [x] Retired root aliases and old product roots are rejected. Evidence: + `bash tools/policy/check-repo-structure.sh` passes, `find . -maxdepth 2` + finds no root `assets`, `crates`, or `sdks` directories, and `git ls-files` + finds no tracked files under those retired roots. + +## Moon Graph + +- [x] Moon is the only task and affectedness graph. Evidence: + `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`. +- [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` + asserts Android mobile builds request only `android-arm64-v8a` and + `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/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 + WASIX patches release only WASIX runtime plus the WASIX Rust binding. +- [x] React Native depends on Swift/Kotlin at the product graph level. Mobile + installed-app builder jobs consume target-scoped exact-extension package + artifacts through CI artifact handoff, not a Moon product dependency. + Evidence: focused Android/iOS planner output selects + `mobile-extension-packages` plus only the Android or iOS native extension + targets, and `oliphaunt-react-native` no longer has `extension-packages` in + Moon project dependencies. + +## CI Builder Model + +- [x] Builds workflow owns release-shaped runtime artifacts: + - `liboliphaunt-native` matrix + - `liboliphaunt-native-release-assets` + - `liboliphaunt-wasix-runtime` + - `liboliphaunt-wasix-aot` + - `liboliphaunt-wasix-release-assets` + Evidence: target jobs emit per-platform release assets, then + `liboliphaunt-native-release-assets` runs the Moon-modeled + `liboliphaunt-native:release-assets` aggregate task against downloaded target + artifacts instead of inline workflow shell. +- [x] Builds workflow owns release-shaped helper artifacts: + - `broker-runtime` + - `node-direct` + Evidence: the CI planner maps these jobs to + `oliphaunt-broker:release-assets` and + `oliphaunt-node-direct:release-assets`, not package-shape or + release-check tasks. +- [x] Builds workflow owns SDK package artifacts: + - Rust SDK + - Swift SDK + - Kotlin SDK + - React Native SDK + - TypeScript SDK + - WASIX Rust binding +- [x] Builds workflow owns exact-extension artifacts: + - `extension-artifacts-native` matrix over published native targets. Each + target row carries the selected exact-extension product set in + `extensions_csv`, so a full builder plan emits 7 native rows instead of + rebuilding PostgreSQL once per product-target pair. Linux, macOS, Android, + iOS, and Windows rows currently carry all 39 exact-extension products. + - `extension-artifacts-wasix` matrix over published WASIX targets. A full + builder plan emits 1 `wasix-portable` row covering 39 exact-extension + products. + - `extension-packages` for full native+WASIX release packages + - `mobile-extension-packages` for Android/iOS installed-app builds that need + only the selected mobile native extension targets + Evidence: mobile package assembly now fails closed unless + `OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS` and + `OLIPHAUNT_EXTENSION_PACKAGE_NATIVE_TARGETS` are explicit; only the + release-wide `extension-packages` path may stage all exact-extension + products. +- [x] Builds workflow has a builder-only aggregate. Evidence: + `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 + builders, with no docs, coverage, regression, release-readiness, or mobile + E2E work folded into the build gate. An + `extension-artifacts-native:build-target`-only plan selects + only the native extension artifact builder and does not select + `liboliphaunt-native`. WASIX AOT target fan-out is emitted by the affected + planner as matrix data, not by a separate CI planner job. +- [x] Builds workflow disables Moon output cache for every artifact-producing + builder invocation. Evidence: `tools/policy/check-release-policy.py` rejects + any selected `ci-*` builder job whose `run-planned-moon-job.sh` line omits + `MOON_CACHE=off`; compiler/package-manager caches remain available below + Moon for ccache, Cargo, Gradle, pnpm, and Docker layers. +- [x] Builds workflow disables upstream Moon expansion for every + artifact-producing builder invocation. Evidence: + `tools/policy/check-release-policy.py` rejects any selected `ci-*` builder + job whose `run-planned-moon-job.sh` line omits + `OLIPHAUNT_MOON_UPSTREAM=none`, so `Builds` jobs cannot silently pull in + `check`, `test`, docs, coverage, regression, or release-readiness work while + producing runtime, SDK, extension, or mobile app artifacts. +- [x] Native exact-extension artifact builders are independent target builders + from the same PostgreSQL/liboliphaunt source and ABI inputs. They are now + addressed by target, receive the exact selected product set as + `OLIPHAUNT_EXTENSION_PRODUCTS`, do not run the `liboliphaunt-native` runtime + artifact producer through Moon upstream expansion, and upload target indexes + containing one row per produced exact-extension artifact. Native + exact-extension builders restore the same target-scoped compiler/build cache + family as base native runtime builders, with extension inputs in the cache + key, so repeated exact-extension jobs do not intentionally recompile + unchanged PostgreSQL/liboliphaunt inputs. The Moon task now depends on + `source-inputs:source-fetch-native-runtime`, not the weaker + extension-only source fetch, because native extension builds need shared and + native third-party source pins such as ICU. Mobile and Swift jobs still + select `liboliphaunt-native` explicitly because they consume its packaged + runtime artifacts. +- [x] Public artifact matrix labels use friendly target ids consistently. + Evidence: WASIX AOT CI matrix emits `target_id` values such as + `macos-arm64`, `linux-x64-gnu`, `linux-arm64-gnu`, and + `windows-x64-msvc`, matching native/helper target ids. +- [x] WASIX AOT builder target metadata is product-local. Evidence: + `src/runtimes/liboliphaunt/wasix/targets/*` declares runner, triple, asset, + and `llvm_url`; `tools/release/artifact_target_matrix.py` reads those fields + instead of carrying target-specific URL maps in the CI planner. +- [x] WASIX AOT build artifacts use an explicit GitHub artifact envelope. + Evidence: AOT builders stage `target-triple.txt` plus a `files/` payload + before upload; release aggregation restores the target layout from that + marker instead of assuming GitHub artifact downloads preserve + `target/oliphaunt-wasix/aot/` parent paths. +- [x] Mobile build jobs consume prebuilt native runtime and target-scoped native + extension package artifacts. They do not source-build liboliphaunt or stage + extension packages locally, and focused Android/iOS mobile plans build only + 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.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 + exact-extension target. Focused Android/iOS builder plans now emit zero + WASIX exact-extension matrix rows because the WASIX extension builder is not + selected. +- [x] Exact-extension product selection is scoped by intent. Evidence: + focused mobile builder plans emit `OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS` for + `oliphaunt-extension-vector` only; full builder plans emit the explicit full + exact-extension product list so `extension-packages` builds every + exact-extension product across native and WASIX publishable targets. Policy + also checks single-product matrix narrowing for both external + (`oliphaunt-extension-vector`) and PostgreSQL contrib + (`oliphaunt-extension-amcheck`) extension products, and checks the real + affectedness shape where a single exact-extension product change also + selects aggregate native/WASIX/package tasks. +- [x] Mobile build jobs require staged SDK package artifacts in CI. Evidence: + CI sets `OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS=1`; Android consumes the staged + React Native tarball plus Kotlin Maven repository artifacts; iOS consumes the + staged React Native tarball and creates a local Git source from the staged + Swift source archive for CocoaPods. +- [x] Mobile build jobs inspect the produced app artifact for selected-extension + correctness. Evidence: CI runs + `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. +- [x] iOS selected-extension artifact inspection is link-aware instead of + metadata-only. Evidence: + `src/sdks/react-native/tools/mobile-extension-runtime.sh` now rejects missing + or unselected `liboliphaunt_extension_.xcframework` inputs after + 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/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. +- [x] Android selected-extension artifact inspection is link-aware at the + Android SDK/RN package build boundary instead of metadata-only. Evidence: + the Kotlin Android SDK accepts `oliphauntAndroidLinkEvidenceFile`, passes it + into its CMake static-extension target, and writes + `oliphaunt-android-static-extension-link-v1` rows for ABI, liboliphaunt, each + selected static extension archive, and dependency archives. React Native + Android passes the same property through its builder and + `src/sdks/react-native/tools/check-sdk.sh build-android-bridge` asserts that + vector's `liboliphaunt_extension_vector.a` was linked for the selected ABI. + The staged mobile artifact checker now requires this Android link evidence + whenever `--require-mobile android --require-mobile-prebuilt-extensions` is + used, validates the linked `liboliphaunt.so`, verifies selected extension + archive paths against the static-registry manifest target id + (`android-arm64-v8a`/`android-x86_64`), and rejects missing or unselected + dependency archive rows. +- [x] Swift SDK package artifacts render the public SwiftPM release manifest + from the real Apple liboliphaunt SwiftPM target artifact in CI, not a local + fixture and not the all-platform aggregate release asset job. Evidence: + `swift-sdk-package` depends on `liboliphaunt-native-ios`, downloads + `liboliphaunt-native-release-assets-ios-xcframework`, sets + `OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR`, and + `src/sdks/swift/tools/check-sdk.sh package-shape` fails closed unless that + directory contains a real + `liboliphaunt--apple-spm-xcframework.zip` with macOS, iOS device, + and iOS simulator slices. +- [x] Downloaded-artifact consumer jobs run their explicit Moon target with + `OLIPHAUNT_MOON_UPSTREAM=none`, so they do not re-run producer tasks after + GitHub artifact handoff. SDK package jobs also run their package target with + upstream traversal disabled so they cannot silently rebuild helper/runtime + package tasks inside an SDK builder. +- [x] WASIX exact-extension packaging consumes portable runtime outputs instead + of rerunning source-generation checks. Evidence: strict generated asset + validation remains in `liboliphaunt-wasix:runtime-portable`; the WASIX + extension artifact packager now only validates and stages archives from the + downloaded/generated portable runtime asset root. +- [x] Release dry-run SDK validation consumes staged builder artifacts in + all release modes instead of rebuilding SDKs from source. Evidence: + `release.py` validates staged Cargo crates, Kotlin Maven repository + artifacts, Swift release manifests/source archives, and npm/JSR tarballs; + Kotlin, React Native, TypeScript, WASIX Rust, and Rust dry-runs return after + 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/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 + 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`, + and `E2E` aggregate jobs; `Builds` verifies every selected runtime, helper + runtime, SDK package, extension artifact, extension package, and mobile app + builder job from `builder_jobs`, while mobile E2E consumes app artifacts only + after the relevant mobile build jobs finish. +- [x] Required job aggregate gates the visible phase aggregates. Evidence: + `required` gates `affected`, `checks`, `tests`, `builds`, and `e2e`; policy + jobs are included through the checks gate, and installed-app E2E cannot replace + 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.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, + E2E, coverage, or release-readiness. +- [x] Mobile app builders consume staged SDK, runtime, and exact-extension + artifacts with source-build fallbacks disabled in CI. Evidence: + `tools/policy/check-release-policy.py` requires Android/iOS mobile build jobs + to depend on the relevant liboliphaunt target builder, SDK package builders, + and `mobile-extension-packages`; download the staged artifacts; set + `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.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 + `OLIPHAUNT_EXPO_IOS_SDK=iphonesimulator`) so installed-app E2E consumes the + same artifact class produced by `Builds`. +- [x] Mobile installed-app E2E is separated from the builder gate and consumes + built app artifacts from `Builds`. Evidence: `.github/workflows/ci.yml` + defines `E2E / mobile-android` and `E2E / mobile-ios` jobs that depend on the + corresponding mobile app build jobs, download + `react-native-mobile-android-app-android-x86_64` and + `react-native-mobile-ios-app`, run the pinned Maestro path through + `src/sdks/react-native/tools/mobile-e2e.sh`, start Android with + `tools/dev/start-android-emulator-ci.sh`, and do not invoke + `run-planned-moon-job.sh`, `mobile-build:*`, or native/source-build fallback + paths. `tools/policy/check-release-policy.py` enforces these invariants. +- [x] React Native mobile task semantics match the Moon CI model. Evidence: + `oliphaunt-react-native:e2e`, `mobile-drill-android`, and `mobile-drill-ios` + are `runInCI=false` because routine CI must never invoke aggregate/manual + device drills. Platform installed-app E2E lanes + `mobile-e2e-android` and `mobile-e2e-ios` are `runInCI=skip` so broad + `moon ci` does not start emulator/simulator jobs, while the tasks remain + graph-valid for explicit installed-app CI workflows. Both + `tools/policy/check-test-strategy.mjs` and + `tools/policy/check-moon-product-graph.mjs` enforce this distinction. + +## Release Model + +- [x] release-please manifest mode owns product components, versions, + 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/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/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. +- [x] Release workflow consumes Builds artifacts instead of rebuilding native, + helper, SDK, WASIX, or extension artifacts during publish. +- [x] Swift SDK release dry-run/publish consumes the Swift SDK builder output + directly: `Oliphaunt-source.zip` plus `Package.swift.release` under + `target/sdk-artifacts/oliphaunt-swift`. Aggregate liboliphaunt release + assets are only downloaded when `liboliphaunt-native` itself is selected for + release. Evidence: `release.py` validates and copies the staged SwiftPM + manifest, and `.github/workflows/release.yml` no longer ties Swift releases + to the aggregate native asset download. +- [x] Staged npm SDK artifacts are real release inputs. Evidence: + `release.py` validates `@oliphaunt/ts` and `@oliphaunt/react-native` + tarballs for exact filename, package name, version, no `workspace:` + dependency specs, and built `package/lib` output before dry-run/publish. +- [x] WASIX runtime release download searches successful same-SHA Builds runs + until it finds the complete portable/AOT artifact set, so focused reruns do + 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.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. +- [x] Broker release asset publishing verifies the `oliphaunt-broker` release + tag before uploading artifacts. +- [x] Exact-extension GitHub release verification includes every uploaded + package file: JSON manifest, `.properties` manifest, checksum manifest, and + payload assets. +- [~] Release provenance and attestations cover runtime/helper/extension/WASIX + asset families. Local policy checks pass; full GitHub release dry-run and + verification still need CI evidence. +- [x] Release dry-run/publish rejects incomplete staged exact-extension package + artifacts. Evidence: `tools/release/release.py` validates staged extension + package manifest identity, JSON asset checksums, checksum-manifest entries, + and declared native/WASIX target coverage; `release.py` now has no native, + WASIX, or exact-extension local builder fallback, so missing staged extension + package artifacts fail immediately. + +## Extensions + +- [x] Public selection model is exact SQL extension name only. No packs, + aliases, or grouped selectors. +- [x] Every public extension in the generated SDK catalog is modeled as an + exact-extension release product. Evidence: + `src/extensions/generated/sdk/rust.json` drives + `tools/policy/check-release-policy.py`, which requires the release product + set to match the public catalog. The current graph has 39 exact-extension + products: PostgreSQL contrib products under `src/extensions/contrib//` + and external products under `src/extensions/external//`. +- [x] Exact-extension products have product-local `release.toml`, `VERSION`, + `CHANGELOG.md`, and target metadata. PostgreSQL contrib exact-extension + products depend on `extension-contrib-postgres18` and + `extension-runtime-contract`; external exact-extension products depend on + `extension-runtime-contract` and their product-local source/recipe metadata. +- [x] Exact-extension target metadata must declare every native runtime target + that advertises exact-extension artifact support, with unpublished opt-outs + required when no real producer exists. Published WASIX target coverage remains + exact. Evidence: + `tools/release/check_artifact_targets.py`, + `src/runtimes/liboliphaunt/native/targets/*.toml`, per-extension + `targets/artifacts.toml` rows, and full builder planner output that includes + `windows-x64-msvc` in the native exact-extension artifact matrix with all 39 + exact-extension products selected. +- [x] Native and WASIX extension artifact builders emit target-addressed + release assets consumed by package assembly. +- [x] Exact-extension package assembly is single-path. Release builds the + complete native+WASIX package set; mobile app builders consume target-scoped + package artifacts through the same manifest/checksum/staging code and only + require Android/iOS native targets. +- [x] Exact-extension package assembly reads target-addressed artifact indexes + only from declared published targets, not recursive scratch globs. Evidence: + 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.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: + `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.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`. +- [x] SDK package checks prove wrapper packages do not ship runtime or + extension payloads. Evidence: + `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.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/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, + selected extensions, CocoaPods extension link file lists, and Xcode linked + products. GitHub CI run `27744307637` also passed `Builds / mobile-android + (android-arm64-v8a)`, `Builds / mobile-android (android-x86_64)`, and + `Builds / mobile-ios`, including strict Android/iOS prebuilt-extension staged + artifact validation. +- [x] Staged artifact inspection covers wrapper packages and exact-extension + package shape. Strict iOS installed-app artifact inspection is green locally, + and GitHub CI run `27744307637` proves the Android and iOS release-mode mobile + app artifacts rebuild and validate from staged SDK/runtime/exact-extension + package handoff without stale local Android state. +- [~] Each advertised extension needs current target smoke evidence across + desktop native, mobile static registry targets, and WASIX. Builder targeting + now covers every published native target plus WASIX: full builder planning + emits 7 native target rows, including all 39 exact-extension products on + `windows-x64-msvc`, and 1 `wasix-portable` row carrying the WASIX + exact-extension product set. GitHub CI run `27744307637` proves the native + artifact builder rows are green, including `Builds / extension-native + (windows-x64-msvc)`. Current runtime smoke evidence is still + transitional/catalog-level and needs real target smoke results from CI before + this item can be marked complete. + +## SDK Contracts + +- [x] Rust, Swift, Kotlin, React Native, TypeScript, and WASIX Rust binding are + peer products with product-local Moon tasks and package artifacts. Evidence: + each product is tagged `release-product`, declares release-please component + metadata, has a product-local `package-artifacts` task, writes to + `target/sdk-artifacts//**/*`, and maps to a `ci-*-sdk-package` + builder tag. `tools/policy/check-moon-product-graph.mjs` enforces the + commands, outputs, cache policy, and CI tag mappings for all six peer SDKs. +- [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/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 + `android/src/main/assets/**`, `android/src/main/jniLibs/**`, and + `ios/resources/**`, and staged package validation passes for + `oliphaunt-react-native`. +- [x] React Native is TypeScript/TurboModule glue over Swift and Kotlin, not a + private database runtime. Static checks exist, and the installed-app E2E + phase now consumes `Builds` app artifacts without rebuilding source. iOS + selected-extension packaging requires exact XCFramework unpacking, + compile-only static-registry source staging, Xcode link evidence, explicit + resource-bundle discovery, and static registry linker retention in the final + app binary. Local iOS installed-app E2E is green for the `vector` selection, + and GitHub CI run `27744307637` passed `E2E / mobile-ios`, `E2E / + mobile-android`, and the aggregate `E2E` gate against same-run mobile app + artifacts. +- [x] Kotlin Android and Swift iOS/macOS consume liboliphaunt and exact selected + extension artifacts through ecosystem-native package surfaces. Kotlin Android + now resolves exact extension releases independently from `liboliphaunt-native` + and verifies per-extension checksums; Swift base release now explicitly stays + extension-free, no longer advertises nonexistent `OliphauntExtension*` + SwiftPM products, renders its release manifest from the CI-built Apple + liboliphaunt XCFramework artifact, and exposes + checksum-bound `render-extension-products.mjs` carrier composition to select + the exact target artifacts, nested contrib members, identity-qualified native + dependencies, and SQL dependency closure for Swift app integrations. The + runtime SDK intentionally has no parallel `.properties`-based remote resolver. + Swift SDK package artifact creation now passes against a deterministic + release-shaped Apple XCFramework fixture, proving the public SwiftPM + manifest/package boundary locally. React Native iOS now has strict link-aware + selected-extension package inspection plus local release-mode installed-app + proof using the real native Apple XCFramework artifact and exact-extension + 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/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 + released. +- [x] Node direct optional npm packages are built in the Builds workflow and + published from staged tarballs. Evidence: + `src/runtimes/node-direct/tools/build-node-addon.sh` emits both + `target/oliphaunt-node-direct/release-assets/*` and + `target/oliphaunt-node-direct/npm-packages/*.tgz`; the release workflow + downloads `oliphaunt-node-direct-npm-package-*`; `release.py` validates and + publishes those tarballs directly. +- [x] WASIX Rust binding package artifacts stay binding-scoped and do not force + a runtime rebuild. Evidence: + `MOON_BIN=$HOME/.proto/shims/moon + .github/scripts/run-moon-targets.sh oliphaunt-wasix-rust:package-artifacts` + passes. The builder packages the root `oliphaunt-wasix` crate through a + 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 + `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. + +## Tool Entrypoints And Policy + +- [x] Repository tasks are Moon-first. Root package-manager aliases are not the + public orchestration surface. +- [x] pnpm remains JS dependency/package-manager tooling, not the global graph. +- [x] Cargo, SwiftPM/Xcode, Gradle, npm/JSR, and Expo are invoked through + product-local Moon tasks or product-owned scripts. +- [x] Policy checks reject stale release graphs, root product aliases, broad + 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.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. +- [~] Policy scripts should remain minimal and guard real architecture + invariants only. Continue pruning brittle substring checks as better + structural checks become available. + +## Verification Commands + +Run before claiming this architecture complete: + +- [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 + tools/release/check_release_metadata.py` +- [x] `tools/dev/bun.sh tools/graph/ci_plan.mjs --help` +- [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` +- [x] `moon run ci-workflows:check graph-tools:check` +- [x] `MOON_BIN=$HOME/.proto/shims/moon + .github/scripts/run-moon-targets.sh ci-workflows:check` +- [x] `bash src/sdks/react-native/tools/check-sdk.sh build-android-bridge` +- [x] `moon run policy-tools:check release-tools:check graph-tools:check` +- [x] `MOON_BIN=$HOME/.proto/shims/moon + .github/scripts/run-moon-targets.sh extensions:check` +- [x] `MOON_BIN=$HOME/.proto/shims/moon + .github/scripts/run-moon-targets.sh extension-model:check + extension-artifacts-native:check extension-artifacts-wasix:check` +- [x] `moon query projects` +- [x] `moon query tasks` +- [x] `moon run :check` completed locally with 84 tasks, 72 cached, after + builder-first CI policy, conditional matrix emission, and JS staged-artifact + release checks were aligned. +- [x] `moon run :test` completed locally with 28 tasks, including native + host smoke, Rust nextest, Swift tests, Kotlin Gradle tests, React Native and + TypeScript Vitest suites, docs tests, broker tests, and WASIX Rust tests. +- [x] `moon run :package` completed locally with 14 tasks, 5 cached. This now + 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] `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 + 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. + 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/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/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 + liboliphaunt-wasix-aot-runtime` emits friendly `target_id` values for every + WASIX AOT builder target from product-local target metadata. +- [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/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 + `Package.swift.release`, stages `Oliphaunt-source.zip`, and passes + `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 + 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 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.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.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.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. +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-rust` +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-kotlin` +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-swift` +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-react-native` +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-js` +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs + --require-sdk-product oliphaunt-wasix-rust` +- [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 + keeps generated static-registry C under compile-only + `ios/generated/static-registry`, bundles runtime resources under + `OliphauntReactNativeResources.bundle`, and proves selected + `liboliphaunt_extension_vector` linkage through CocoaPods and Xcode build + products. +- [x] Local iOS release-mode installed-app E2E passes with exact-extension + `vector` using the staged native Apple XCFramework, generated exact-extension + package, and clean simulator install: + `OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS=0 + OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS=1 + OLIPHAUNT_EXPO_IOS_EXTENSIONS=vector + OLIPHAUNT_EXPO_IOS_REPACK_RN=1 + bash src/sdks/react-native/tools/mobile-build.sh ios`, followed by + `bash src/sdks/react-native/tools/mobile-e2e.sh ios` against + `target/mobile-build/react-native/ios-local-vector-fix-v4`. The app launches + from `OliphauntReactNativeResources.bundle`, the bundled template PGDATA uses + `dynamic_shared_memory_type = mmap`, UTC time zones, and `C` locale settings, + the final app binary contains + `_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 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 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 + `MOBILE_TARGET=android NATIVE_TARGET=ios-xcframework` now fail closed in the + planner. +- [x] Android SDK provisioning is shared and reproducible. Evidence: + `.github/actions/setup-android` calls `tools/dev/setup-android-sdk.sh`; the + script bootstraps Android command-line tools when `sdkmanager` is absent, + installs the pinned platform-tools/platform/build-tools/CMake/NDK packages + through `sdkmanager`, and passes idempotently on the local Android SDK with + 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] `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` +- [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/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. +- [x] Local builder-architecture checks passed after the builder-first CI + cleanup: `git diff --check`, `actionlint .github/workflows/ci.yml + .github/workflows/mobile-e2e.yml .github/workflows/release.yml`, + `node tools/policy/check-moon-product-graph.mjs`, + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_artifact_targets.py`, and + `python3 tools/policy/check-release-policy.py`. +- [x] Local PR 38 CI hardening checks passed after fixing the observed builder + failures: `cargo run -p xtask -- assets verify-committed`, `bash -n` for the + touched native scripts, `sh -n src/runtimes/node-direct/tools/build-node-addon.sh`, + `actionlint .github/workflows/ci.yml .github/workflows/mobile-e2e.yml + .github/workflows/release.yml`, `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `node tools/policy/check-moon-product-graph.mjs`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `bash tools/policy/check-sdk-parity.sh`, and + `bash tools/policy/check-repo-structure.sh`. +- [x] Local PR 38 Windows builder follow-up checks passed after making native + extension source fetch skip PostgreSQL preparation and making the Windows base + runtime prune exact-extension artifacts before packaging: + `bash -n src/extensions/artifacts/native/tools/package-release-assets.sh`, + `git diff --check`, `cargo run -p oliphaunt --bin oliphaunt-resources + --locked -- --list-extensions`, `cargo run -p xtask -- assets + verify-committed`, `actionlint .github/workflows/ci.yml + .github/workflows/mobile-e2e.yml .github/workflows/release.yml`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `node tools/policy/check-moon-product-graph.mjs`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `bash tools/policy/check-sdk-parity.sh`, `bash tools/policy/check-repo-structure.sh`, + and `bash src/runtimes/node-direct/tools/check-package.sh check-static`. + PowerShell parsing/execution still needs the GitHub Windows runner because + `pwsh` is not installed in this macOS worktree. +- [x] GitHub Builds run `27380605889` on `ff25ab64` proved the next CI-only + blockers after the Windows pruning patch: Windows desktop source fetch still + prepared the WASIX PostgreSQL tree, Windows exact-extension Meson setup needed + WinFlexBison provisioning, and WASIX runtime artifact tasks were skipped by + `runInCI: skip`. The follow-up patch makes every native-runtime source fetch + that only needs pinned checkouts pass `--skip-postgres-prepare`, lets Windows + native extension builders run `.github/scripts/setup-native-build-tools.sh`, + marks WASIX artifact-producing Moon tasks `runInCI: true`, and extends the + executable ownership policy to `src/extensions/artifacts/packages/tools/*`. + Local checks after this patch passed: `bash -n` for touched shell scripts, + `actionlint .github/workflows/ci.yml .github/workflows/mobile-e2e.yml + .github/workflows/release.yml`, `node tools/policy/check-moon-product-graph.mjs`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `bash tools/policy/check-repo-structure.sh`, + `node tools/policy/check-test-strategy.mjs`, + `cargo run -p xtask -- assets verify-committed`, + `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. +- [x] GitHub Builds run `27381488172` on `9181f71a` proved the next CI-only + blockers: Apple native extension jobs failed on macOS Bash 3.2 empty-array + expansion in `build-postgres18-macos.sh`, the iOS XCFramework validator + incorrectly applied mobile-forbidden shm/semaphore checks to the macOS + framework slice, and the Windows native extension row selected external + PGXS/PostGIS products even though Windows has no producer for those sources. + The Android native extension jobs also reached the combined mobile static + link and failed on duplicate `difference` / `pg_finfo_difference` symbols + exported by contrib `fuzzystrmatch` and PostGIS `postgis_legacy.o`. + The follow-up patch guards Apple empty-array expansion, limits the mobile API + import check to iOS/iOS simulator slices, marks external PGXS/PostGIS Windows + targets unpublished with explicit reasons, and relaxes policy to require + explicit native target opt-outs rather than false published coverage. It also + namespaces PostGIS legacy mobile static symbols, records those mappings as + exact-extension `staticSymbolAliases`, and teaches generated mobile static + registry source to keep SQL-visible symbol names while pointing at aliased + link-time identifiers. Local checks after this patch passed: `bash -n` for + touched shell scripts, `cargo check --manifest-path src/sdks/rust/Cargo.toml + --locked`, focused Rust static-registry alias test, `git diff --check`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, `python3 -m py_compile` for + 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/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, + Android x86_64 and Linux arm64 native-runtime source fetches failed through + `ftpmirror.gnu.org` libiconv 502s, Apple extension runtime builds passed the + embedded `-bundle_loader` through `PG_LDFLAGS` so PostgreSQL's Darwin default + `BE_DLLLIBS=-bundle_loader ../../src/backend/postgres` won and failed when + the backend executable link was intentionally tolerated, and the Windows + exact-extension row still selected `pgcrypto` even though the current Windows + runtime disables SSL/OpenSSL and does not package that dependency. The + follow-up refreshes `asset-inputs.sha256`, switches libiconv inputs to the + canonical GNU URL, routes Darwin PGXS embedded bundle-loader wiring through + `BE_DLLLIBS`, keeps Bash strict-mode arrays guarded by explicit counts, and + marks Windows `pgcrypto` unpublished with an OpenSSL runtime dependency + reason. The native exact-extension matrix now keeps Android/iOS/Linux/macOS + at 39 products and Windows at 31 before the next Windows `uuid-ossp` follow-up. + Local evidence after this patch passed: + focused macOS `OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES=amcheck` + `build-postgres18-macos.sh`, `bash -n` for touched shell scripts, + `cargo run -p xtask -- assets verify-committed`, + `bun tools/policy/assertions/assert-source-inputs.mjs`, + `python3 src/extensions/tools/check-extension-model.py --check`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `node tools/policy/check-moon-product-graph.mjs`, and `git diff --check`. +- [x] Builds workflow green on PR for affected builder jobs, including + Android/iOS release-mode mobile build jobs when selected. +- [x] GitHub Builds run `27384916687` on `eab81d45` proved the Windows + `pgcrypto` opt-out but exposed one more Windows native exact-extension + metadata gap: the row still selected `uuid-ossp`; the Windows MSVC producer + reached `oliphaunt-extension-artifact` for `uuid-ossp` and failed because the + staged runtime lacked the required control and SQL install files. The same run + exposed a Darwin external PGXS `pg_textsearch` link gap: its `SHLIB_LINK=-lm` + override dropped the embedded `BE_DLLLIBS=-bundle_loader ...` path after the + tolerated backend executable link failure, so iOS/macOS builds saw unresolved + PostgreSQL backend symbols. The follow-up marks the `uuid-ossp` Windows target + unpublished with an explicit UUID dependency/control-file reason, reduces the + Windows native exact-extension row to 30 products until the MSVC producer has + portable UUID packaging support, and folds Darwin `pg_textsearch`/`pgvector` + `-lm` linkage into `BE_DLLLIBS` when the embedded bundle-loader path is + active. Local focused macOS + `OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES=pg_textsearch` + `build-postgres18-macos.sh` passed, and `otool -L` on the packaged module + shows `@rpath/liboliphaunt.dylib`. +- [x] GitHub Builds run `27386002923` on `3397cd67` proved the Windows + `uuid-ossp` opt-out and Darwin PGXS link fix: Windows exact-extension + `windows-x64-msvc` and macOS exact-extension `macos-arm64` both passed. The + same run exposed two mobile exact-extension blockers: Android + `android-arm64-v8a` and `android-x86_64` failed because PostGIS + `postgis_legacy.o` exports token-pasted + `pg_finfo_oliphaunt_static_postgis_3_difference`, while the static alias + metadata expected `oliphaunt_static_postgis_3_pg_finfo_difference`; iOS + `ios-xcframework` failed because mobile static contrib compiles such as + `dict_xsyn` did not inherit PostgreSQL ICU include flags and could not find + `unicode/ucol.h`. The follow-up maps `pg_finfo_difference` to the + token-pasted PostGIS symbol in both mobile object staging and release + artifacts, and feeds `icu_cflags` into iOS/Android mobile static extension + compiles. Local evidence after this patch passed: focused Android arm64 + `OLIPHAUNT_MOBILE_STATIC_EXTENSIONS=postgis` produced + `target/mobile-smoke-android-arm64-postgis/out/liboliphaunt.so` with + `symbol-aliases.list` mapping `pg_finfo_difference` to + `pg_finfo_oliphaunt_static_postgis_3_difference`, `llvm-nm` proved the + matching `postgis_legacy.o` symbols, and focused iOS simulator/device + `OLIPHAUNT_MOBILE_STATIC_EXTENSIONS=dict_xsyn` builds produced + `out/liboliphaunt.dylib` while compiling `dict_xsyn.o` with the matching + `icu-ios-*/include` path. +- [x] GitHub Builds run `27388349669` on `274f86a6` proved the Android + PostGIS alias and mobile static ICU fixes: native exact-extension + `android-arm64-v8a`, `android-x86_64`, Linux, macOS, and Windows rows all + passed, as did WASIX runtime and WASIX exact-extension packaging. The run + exposed the next iOS-only packaging blocker: `ios-xcframework` got past the + `dict_xsyn` ICU compile and device static archive production, then failed in + `build-ios-extension-xcframeworks.sh` because Bash 3.2 plus `set -u` treats + empty selected dependency arrays as unbound during XCFramework manifest and + dependency packaging. The follow-up guards the empty-array expansions for + selected dependencies/extensions/stems and adds a mobile policy assertion for + the strict-mode guard. Local evidence after this patch passed: `bash -n` for + the touched shell scripts, `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `git diff --check`, and a focused `dict_xsyn` iOS XCFramework packaging + smoke that produced simulator/device archives under + `target/ios-xcframework-dict-xsyn-smoke/out` with `dependencies=` in the + manifest. +- [x] GitHub Builds run `27390718093` on `964cc35` proved the iOS + XCFramework manifest/dependency guard got past the previous + `selected_dependencies[@]` failure, but exposed the same Bash 3.2 + strict-mode class in native extension release packaging: + `package-release-assets.sh` failed at `mobile_dependency_args[@]` while + packaging iOS products with no mobile dependency archives. The follow-up + guards empty `mobile_dependency_args` and `extra_args` expansions in both + iOS and Android package paths, and pins those guards in the mobile extension + surface policy. Local evidence after this patch passed under Bash 3.2: + unguarded empty-array harness reproduces `mobile_dependency_args[@]: unbound + variable`, the guarded harness passes, and the exact CI-shaped + `extension-artifacts-native:build-target` command for `ios-xcframework` + completed all 39 selected products with `/bin/bash` 3.2 selected by PATH. +- [x] GitHub Builds run `27392985628` on `e831c4a9` proved the native + exact-extension matrix and mobile extension package assembly are now green, + including `ios-xcframework`, both Android exact-extension targets, and + `build-mobile-extension-packages`. The run exposed the next mobile app + builder regression: `mobile-build-ios`, `mobile-build-android + (android-arm64-v8a)`, and `mobile-build-android (android-x86_64)` failed + before emitting runner logs, with the aggregate `artifact-builders` and + `required` jobs failing only because those builders failed. Local + reproduction with downloaded CI iOS native/SDK/mobile-extension inputs + showed the silent exit came from + `static_registry_source="$(mobile_static_registry_source_for_library ...)"` + when the prebuilt liboliphaunt artifact does not have an adjacent + `liboliphaunt_mobile_static_registry.c`; under `set -e` the helper's final + missing-file test returned 1 and aborted the script without a diagnostic. + The follow-up makes Android and iOS static-registry lookup return success + with an empty result so exact-extension packages can provide the generated + static registry source, and also hardens React Native runner empty-array + expansions under Bash 3.2 strict mode. Local evidence after this patch + passed: Bash syntax checks for the touched runner/policy scripts, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `node tools/policy/check-moon-product-graph.mjs`, + `python3 tools/release/check_artifact_targets.py`, `git diff --check`, + CI-artifact package lookup for the selected `vector` iOS XCFramework zip, + and the iOS mobile-build entrypoint progressed to + `Preparing iOS runtime resources from exact-extension package artifacts: + vector`; the remaining local stop is expected because the downloaded CI + `initdb` dylib install names point at the GitHub runner workspace path. +- [x] GitHub Builds run `27400178224` on `e494777f` proved the previous silent + React Native mobile runner abort is fixed and that the upstream builder + chain stayed green: native runtime Android/iOS rows, Android/iOS native + exact-extension rows, and `build-mobile-extension-packages` all passed. The + remaining leaf failures were the three mobile app builders, all with the + same CI-only terminal error: + `oliphaunt-resources: run native PGDATA template initdb: Permission denied`. + The failure occurs after GitHub artifact handoff, before Xcode or Gradle app + packaging, and is consistent with downloaded native runtime artifacts losing + executable bits on PostgreSQL tools such as `bin/initdb`. +- [x] GitHub Builds run `27403805978` on `aac266ca` selected the mobile + artifact/app lanes again after the runtime tool permission fix and kept the + native runtime, native exact-extension, SDK, WASIX runtime, and portable + WASIX extension builders green. The next direct failure is + `build-liboliphaunt-wasix-aot (windows-x64-msvc)` failing before compilation + in `.github/actions/setup-wasmer-llvm` while downloading + `llvm-windows-amd64.tar.xz` with `curl: (52) Empty reply from server`; the + remaining AOT/package/mobile rows were still running or waiting. The + follow-up hardens the Wasmer LLVM installer with the same retry-all-errors + and connection-timeout policy used by other repo downloaders. +- [x] GitHub Builds run `27406731304` on `682840b2` verified the Wasmer LLVM + download hardening, then exposed the next macOS Bash compatibility gap: + `build-plan` passed, early fan-out jobs are succeeding, and no completed + failures were reported while native runtime, native extension, and WASIX + runtime rows continued running. The Windows AOT row reached and passed + `Install Wasmer LLVM 22.1 for AOT generation`, proving the retry-hardened + setup step clears the previous `curl: (52) Empty reply from server` blocker. + AOT/package aggregation advanced further, but `mobile-build-ios` then failed + in `src/sdks/react-native/tools/expo-ios-runner.sh` with + `mapfile: command not found` on macOS Bash 3.2 after successfully preparing + iOS runtime resources from exact-extension package artifacts. The follow-up + replaces that `mapfile` use with a Bash 3-compatible read loop and adds a + policy guard against reintroducing Bash 4-only `mapfile`/`readarray` usage in + the iOS mobile runner. Follow-up run `27410008857` below proved AOT artifact + completion, mobile app builders, and the required aggregate. +- [x] GitHub Builds run `27410008857` on `443bf1b8` completed successfully + with all 44 PR checks green. This proves the Wasmer LLVM setup retry + hardening, all native/WASIX runtime and exact-extension builders, AOT + artifact fan-out, artifact/package aggregation, `mobile-build-ios`, both + Android mobile build rows, `artifact-builders`, and `required` on the same + SHA. The iOS mobile app build advanced past the previous macOS Bash 3.2 + `mapfile` failure and published the release-mode simulator app artifact. +- [x] Local installed-app iOS validation after the green `Builds` run exposed + runtime issues that artifact inspection alone did not catch, then proved the + local fixes. The failure chain was: React Native resources existed under + `OliphauntReactNativeResources.bundle` but Swift/native-direct bundle + discovery did not find them; the local `pnpm pack` path excluded + `ios/resources/**` and did not inject staged mobile assets after install; Rust + exact-extension packaging generated a host-locale PGDATA template with + `dynamic_shared_memory_type = posix`; the iOS app did not force-link the pure + C selected-extension registry object, so `CREATE EXTENSION vector` could not + resolve `vector`; and simulator data-container reuse could preserve stale bad + PGDATA. The follow-up adds explicit RN/iOS resource-bundle discovery, shared + staged asset injection for artifact and local pack installs, mobile-safe Rust + template initdb/config normalization with a template cache bump, clean + simulator reinstall by default, and `-u + _liboliphaunt_selected_static_extensions` when generated static-registry + sources are present. Local release-mode iOS app build and installed-app E2E + now pass for selected `vector`. +- [x] GitHub Builds run `27420575821` on `ed24c6e` picked up the installed-app + fixes but exposed a new Android SDK provisioning flake before any native + Android source build: `build-native-runtime-android (android-x86_64)` failed + during `.github/actions/setup-android` while `sdkmanager` downloaded NDK + `27.0.12077973`, ending with `Error on ZipFile unknown archive` at 21% + download/unzip progress. The follow-up hardens the shared + `tools/dev/setup-android-sdk.sh` package install with bounded sdkmanager + retries and cleanup of partial selected platform/build-tools/CMake/NDK + directories before retrying, and pins that invariant in + `tools/policy/check-tooling-stack.sh`. Local evidence after this patch passed: + `bash -n tools/dev/setup-android-sdk.sh tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. A + replacement `Builds` run is required because `27420575821` cannot be the green + builder evidence. +- [x] GitHub Builds run `27420928633` on `e3b9667` completed successfully with + all 44 job conclusions green. This proves the Android SDK retry hardening + cleared the previous corrupt NDK download/setup failure, and that the full + builder graph still passes afterward: native Android/iOS/desktop runtime + artifacts, native/WASIX exact-extension artifacts, WASIX portable and AOT + runtime artifacts, exact-extension package aggregation, mobile + exact-extension packages, iOS and Android release-mode mobile app builders, + `artifact-builders`, and `required`. +- [x] GitHub Builds run `27425676382` on `b9320719` completed successfully with + all 44 job conclusions green after recording the `27420928633` evidence. The + latest PR-head builder evidence remained fully green for native runtime, + 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/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 + 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/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 + 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 `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 + 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 + commit still had a stale committed WASIX asset-input fingerprint. The + `build-liboliphaunt-wasix-runtime` job failed during + `cargo run -p xtask -- assets verify-committed` with computed fingerprint + `aed54dc5dbe84544a6627a5fe30d8a7670ea670558e0bc184d57061f8848911e` while + `src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256` + still held `183cff37e33e3349577c6061a85e9ee96a2e30ee5dfeddc93b0eb7789a1f926a`. + The follow-up refreshes the committed fingerprint with + `cargo run -p xtask -- assets input-fingerprint --write`. A replacement + same-SHA `Builds` run is required because `27434296236` cannot be green + builder evidence. +- [x] GitHub Builds run `27448574605` on `927457d3` proved the native + exact-extension source-fetch gap after decoupling artifact packaging from Rust: + all native extension rows failed before build work because CI runs + `extension-artifacts-native` with `OLIPHAUNT_MOON_UPSTREAM=none`, so the + `source-inputs:source-fetch-native-runtime` dependency did not materialize ICU, + OpenSSL, and extension checkouts. The follow-up makes source checkout + materialization a Bun-native Git/curl/tar script, removes Cargo/`xtask` inputs + from source-fetch Moon tasks, wires explicit Bun source fetches into native + extension/native runtime/WASIX runtime CI jobs, and keeps standalone native + release helpers on the same source-fetch path. A replacement run on `3ffaaae` + proved the missing-checkout failure was gone and exposed a Windows Git Bash + `tar` absolute-drive path issue during libiconv archive extraction; the fetcher + now passes workspace-relative forward-slash paths to `tar`. The next + replacement run on `cb43c96` proved that fix and got Windows PostGIS to SQL + generation, where the producer still treated `uninstall_rtpostgis.sql` as a + source template; the Windows path now mirrors the PostGIS Makefile by + preprocessing `rtpostgis.sql` and generating `uninstall_rtpostgis.sql` through + `utils/create_uninstall.pl`. The `1e88feb` retry got past that point and failed + later because `extensions/postgis_extension_helper.sql` was not generated; the + Windows path now preprocesses `postgis_extension_helper.sql.in` before composing + extension upgrade SQL. The `d3cb9bd` replacement run `27451612217` got past + the missing helper but failed because the handwritten SQL preprocessor treated + a commented usage example inside `libpgcommon/sql/AddToSearchPath.sql.inc` as a + live recursive include; the preprocessor now ignores directives while inside + block comments and reports explicit include cycles. The `00f268f` replacement + run `27468133225` proved that fix and all non-Windows native extension rows + passed, but Windows then failed compiling the bundled PostGIS FlatGeobuf C++ + shim because `liblwgeom.h` includes `proj.h`; the FlatGeobuf compile now uses + the pinned PROJ include directory and native command log tails are written to + stderr so follow-up CI failures show the compiler diagnostic directly. The + `5031bb9` replacement run `27471755925` got past that include gap and failed + later in `postgis-flatgeobuf-geometrywriter.log` because `geometrywriter.h` + includes `lwgeom_log.h` before `liblwgeom.h`, so MSVC saw PostGIS' + GCC-style `__attribute__((format(...)))` declarations before PostGIS' + compatibility define was visible. The FlatGeobuf compile now force-includes a + build-local MSVC compatibility header for that bundled C++ shim. The + `8702131` replacement run `27473015280` proved that compatibility header and + advanced to `postgis-flatgeobuf-geometryreader.log`, where MSVC then rejected + GCC's C++ compound-literal extension in `POINT4D` assignments; the Windows + producer now patches those two checked-out FlatGeobuf assignments to explicit + `POINT4D.x/y/z/m` field writes before compiling the shim. The `2cb5864` + replacement run `27473624660` proved the FlatGeobuf static library build + completed and then failed at Meson setup because the handwritten Windows + module listed optional `doc/postgis_comments.sql` without generating it; the + Windows producer now materializes that optional comments SQL before writing the + Meson module. The `2e42999` replacement run `27474227200` proved that Meson + setup and all non-Windows native extension rows plus Windows WASIX AOT passed, + then failed compiling `pg_textsearch` because MSVC saw SQL-callable type + functions declared without `PGDLLEXPORT` in `types/vector.h` and + `types/query.h` before their `PG_FUNCTION_INFO_V1` definitions; the Windows + source patch now gives those declarations the same exported linkage as the + generated function-info declarations. The `d30dcd7f` replacement run + `27476719363` proved that `pg_textsearch` compiles and again left only the + Windows native extension row failing; PostGIS proper then reached Meson + compilation and failed because its module did not get the FlatGeobuf + MSVC compatibility shim, so `lwgeom_log.h`/`lwgeom_pg.h` exposed GCC + `__attribute__` annotations, and because PostGIS has many SQL-callable + forward declarations without `PGDLLEXPORT`. The Windows PostGIS producer now + force-includes a small MSVC compatibility header for the Meson module and + normalizes checked-out PostGIS `Datum ... (PG_FUNCTION_ARGS);` declarations + to exported linkage before build. The `27fae19b` replacement run + `27488767029` proved the compatibility header is present in PostGIS compile + commands and cleared the `__attribute__` failure, while showing that the first + PowerShell traversal used `Get-ChildItem -Include` in a way that silently did + not patch declarations on CI; the producer now uses an explicit extension + filter, a lower no-op count guard, and targeted assertions for the PostGIS + declarations that MSVC reported with mismatched exported linkage. The + `cc2e21d` replacement run `27489945425` showed the targeted assertion still + missed CI's declaration form, so the normalizer now also handles optional + leading whitespace and existing `extern Datum` declarations. `d7ab3ce` run + `27490693556` showed Windows still missed the declaration after checkout, so + the normalizer also tolerates CRLF line endings and optional space before + `PG_FUNCTION_ARGS`. `f00624a` run `27491174973` got past the source-patch + assertions and reached PostGIS MSVC compilation, then failed on + `FALLTHROUGH` expanding to `[[fallthrough]]` in C mode; the forced Windows + compatibility header now maps `FALLTHROUGH` to `((void)0)` under MSVC. + `e263edb` run `27491847193` reached later PostGIS compilation and failed on + the `POSTGIS_DEPRECATE` macro's generated legacy declarations; the Windows + patch now exports that macro's generated declarations as well. `b94e3f7` run + `27492582908` then reached deeper `liblwgeom` compilation and failed to + resolve `ryu/ryu.h`; the Windows PostGIS include roots now include + `deps/`. `68ab0c3` run `27493306145` reached `liblwgeom/topo` compilation + and showed that the single Meson target's include order let the server-side + `postgis/lwgeom_geos.h` shadow `liblwgeom/lwgeom_geos.h`, which pulled + `fmgr.h` into topology sources without PostgreSQL's core prelude. The + Windows PostGIS include roots now put `liblwgeom` before `postgis`; source + files under `postgis/` still resolve their source-local headers first, while + `liblwgeom/topo` fallback includes resolve to liblwgeom headers. `0b124b9` + run `27494545976` then compiled all `postgis-3` objects and failed at final + link because PROJ 9.8's `proj.h` emitted `dllimport` references while the + producer links pinned static PROJ, and because MSVC lacks POSIX + `strcasecmp`/`strncasecmp`. `74f403f` run `27495281356` proved the string + aliases but still linked against `__imp_proj_*`, showing that `PROJ_STATIC` + is not the PROJ 9.8 export-control macro. The Windows PostGIS and FlatGeobuf + compatibility headers now define `PROJ_DLL` empty before `proj.h` is + included, and the PostGIS compatibility header maps those case-insensitive + string calls to MSVC's `_stricmp`/`_strnicmp`. Local evidence passed: + `bun tools/policy/fetch-sources.mjs native-runtime --force`, + `bun tools/policy/fetch-sources.mjs wasix-runtime --force`, + `node tools/policy/check-moon-product-graph.mjs`, + `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-repo-structure.sh`, Bash syntax checks for touched + shell scripts, direct execution of the embedded PostGIS SQL preprocessor + against `postgis_extension_helper.sql.in`, a focused PostGIS Windows source + patch anchor check, PowerShell tokenization for the touched Windows packager, + and `git diff --check`. +- [x] Mobile E2E is green on PR for selected Android/iOS app artifacts from the + same successful CI SHA. Evidence: GitHub CI run `27744307637` on + `71215cac524dd2c4c45589749e2a4c8d4811e8e8` passed `Builds / mobile-ios`, + both Android mobile build rows, `E2E / mobile-ios`, `E2E / mobile-android`, + the aggregate `E2E` gate, the aggregate `Builds` gate, and `Required`. +- [ ] Release workflow dry-run green for selected products. Historical note: + an earlier local check used `release-check-registries.mjs + --require-identities` and therefore reported the not-yet-created + first-release identities as a blocker. That is intentionally not the hosted + first-release order. The current `publish-dry-run` is credential-free and + permits absent bootstrap-eligible npm/crates names so it can freeze the + publication lock and emit the manifest-bound bootstrap capsule. Only then + may `publish-bootstrap` create those missing identities from the approved + bytes; normal publication subsequently requires every selected identity. + Treat [the release runbook](../maintainers/release.md) and [registry setup + guide](../maintainers/release-setup.md) as canonical rather than replaying + the historical command above. Same-SHA builder evidence for the branch at + the time was green in GitHub CI run `27744307637`. +- [x] Consumer-shape validation for the full selected product closure is green. + The checker now treats `oliphaunt-node-direct` as a consumer-facing helper + product: the private root source package stays unpublishable, optional + 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/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 + previous Windows gaps. The Windows build script now generates Meson + producers inside the patched PostgreSQL source tree for `pg_hashids`, + `pg_ivm`, `pg_textsearch`, `pg_uuidv7`, `vector`, `pgcrypto`, and + `uuid-ossp`, builds pinned static OpenSSL for `pgcrypto`, links the + first-party portable UUID source into `uuid-ossp`, and stages pgTAP's + generated SQL/control files without a native module. It also builds a Windows + PostGIS producer that compiles pinned static GEOS, PROJ, SQLite, json-c, and + libxml2 dependencies, links `postgis-3`, and stages PostGIS SQL/data plus + `proj/proj.db`. Target metadata now publishes those nine rows on + `windows-x64-msvc`, so the native exact-extension matrix reports 39 Windows + 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`, + `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 + 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. + +## Historical Next Work Snapshot + +1. Run the credential-free `publish-dry-run` first so the exact-SHA lock and + bootstrap capsule exist. If first-release npm/crates names are absent, run + `publish-bootstrap` from those approved bytes, configure trusted publishers, + revoke the bootstrap credentials, and only then resume normal publication. + Follow the canonical maintainer runbook rather than historical checklist + commands that used `--require-identities` before bootstrap. diff --git a/docs/internal/OLIPHAUNT_PATCH_STACK.md b/docs/internal/OLIPHAUNT_PATCH_STACK.md new file mode 100644 index 00000000..0e2e1f66 --- /dev/null +++ b/docs/internal/OLIPHAUNT_PATCH_STACK.md @@ -0,0 +1,153 @@ + +# liboliphaunt PostgreSQL 18 Patch Stack Review + +This source-only review artifact keeps the native PostgreSQL patch stack deterministic and reviewable without rebuilding PostgreSQL. + +Regenerate with: + +```sh +src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --write +``` + +## Source Pin + +- PostgreSQL: `18.4` +- URL: `https://ftp.postgresql.org/pub/source/v18.4/postgresql-18.4.tar.bz2` +- SHA-256: `81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094` +- Patch directory: `../patches/postgresql-18.4` + +## Patch Series + +| Order | Patch | Author | Subject | +| --- | --- | --- | --- | +| 1 | `0001-liboliphaunt-add-backend-host-io.patch` | liboliphaunt | liboliphaunt: add backend host I/O callbacks | +| 2 | `0002-liboliphaunt-add-embedded-entrypoint.patch` | liboliphaunt | liboliphaunt: add embedded backend entrypoint | +| 3 | `0003-liboliphaunt-return-from-embedded-frontend-terminate.patch` | liboliphaunt | liboliphaunt: return from embedded frontend terminate | +| 4 | `0004-liboliphaunt-run-embedded-exit-cleanup.patch` | liboliphaunt | liboliphaunt: run embedded exit cleanup without exiting | +| 5 | `0005-liboliphaunt-restore-host-cwd.patch` | liboliphaunt | liboliphaunt: restore host cwd after embedded shutdown | +| 6 | `0006-liboliphaunt-add-static-extension-loader.patch` | liboliphaunt | liboliphaunt: add static extension loader | +| 7 | `0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch` | liboliphaunt | liboliphaunt: disable shell commands on Apple mobile targets | +| 8 | `0008-liboliphaunt-clean-embedded-symbols.patch` | liboliphaunt | liboliphaunt: clean embedded symbols | +| 9 | `0009-liboliphaunt-guard-embedded-proc-exit.patch` | liboliphaunt | liboliphaunt: guard embedded proc_exit failures | +| 10 | `0010-liboliphaunt-use-host-runtime-paths.patch` | liboliphaunt | liboliphaunt: use host-provided embedded runtime paths | +| 11 | `0011-liboliphaunt-add-android-embedded-shared-memory.patch` | liboliphaunt | liboliphaunt: add embedded mobile shared memory and semaphores | +| 12 | `0012-liboliphaunt-enable-event-triggers-in-embedded-backend.patch` | liboliphaunt | liboliphaunt: enable event triggers in embedded backend sessions | +| 13 | `0013-liboliphaunt-fix-embedded-backend-main-return-contract.patch` | liboliphaunt | liboliphaunt: fix embedded BackendMain return contract | +| 14 | `0014-liboliphaunt-use-portable-embedded-socketpair.patch` | liboliphaunt | liboliphaunt: use portable embedded socketpair | +| 15 | `0015-liboliphaunt-add-embedded-meson-option.patch` | liboliphaunt | liboliphaunt: add embedded meson option | +| 16 | `0016-liboliphaunt-skip-icu-collation-version-without-icu-data.patch` | liboliphaunt | liboliphaunt: skip ICU collation setup without ICU data | +| 17 | `0017-liboliphaunt-namespace-dynahash-host-collisions.patch` | liboliphaunt | liboliphaunt: namespace Apple dynahash host collisions | +| 18 | `0018-liboliphaunt-contain-embedded-proc-signals.patch` | liboliphaunt | liboliphaunt: contain embedded process signals | +| 19 | `0019-liboliphaunt-link-windows-embedded-modules-to-host.patch` | liboliphaunt | liboliphaunt: link Windows embedded modules to host | +| 20 | `0020-liboliphaunt-enforce-embedded-signal-boundary.patch` | liboliphaunt | liboliphaunt: enforce embedded signal boundary | + +## Changed Upstream Files + +- `meson.build` (`0015-liboliphaunt-add-embedded-meson-option.patch`) +- `meson_options.txt` (`0015-liboliphaunt-add-embedded-meson-option.patch`, `0019-liboliphaunt-link-windows-embedded-modules-to-host.patch`) +- `src/backend/access/transam/xlogarchive.c` (`0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch`) +- `src/backend/archive/shell_archive.c` (`0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch`) +- `src/backend/commands/event_trigger.c` (`0012-liboliphaunt-enable-event-triggers-in-embedded-backend.patch`) +- `src/backend/libpq/be-secure.c` (`0001-liboliphaunt-add-backend-host-io.patch`) +- `src/backend/libpq/pqcomm.c` (`0001-liboliphaunt-add-backend-host-io.patch`) +- `src/backend/meson.build` (`0019-liboliphaunt-link-windows-embedded-modules-to-host.patch`) +- `src/backend/port/Makefile` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/backend/port/meson.build` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/backend/port/oliphaunt_embedded_sema.c` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/backend/port/oliphaunt_embedded_shmem.c` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/backend/storage/ipc/ipc.c` (`0004-liboliphaunt-run-embedded-exit-cleanup.patch`, `0009-liboliphaunt-guard-embedded-proc-exit.patch`) +- `src/backend/storage/ipc/procsignal.c` (`0018-liboliphaunt-contain-embedded-proc-signals.patch`) +- `src/backend/tcop/postgres.c` (`0002-liboliphaunt-add-embedded-entrypoint.patch`, `0003-liboliphaunt-return-from-embedded-frontend-terminate.patch`, `0004-liboliphaunt-run-embedded-exit-cleanup.patch`, `0005-liboliphaunt-restore-host-cwd.patch`, `0009-liboliphaunt-guard-embedded-proc-exit.patch`, `0010-liboliphaunt-use-host-runtime-paths.patch`, `0014-liboliphaunt-use-portable-embedded-socketpair.patch`, `0018-liboliphaunt-contain-embedded-proc-signals.patch`) +- `src/backend/utils/fmgr/dfmgr.c` (`0006-liboliphaunt-add-static-extension-loader.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch`) +- `src/bin/initdb/initdb.c` (`0016-liboliphaunt-skip-icu-collation-version-without-icu-data.patch`) +- `src/include/libpq/libpq-be.h` (`0001-liboliphaunt-add-backend-host-io.patch`) +- `src/include/port.h` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`, `0020-liboliphaunt-enforce-embedded-signal-boundary.patch`) +- `src/include/storage/dsm_impl.h` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/include/storage/ipc.h` (`0004-liboliphaunt-run-embedded-exit-cleanup.patch`, `0009-liboliphaunt-guard-embedded-proc-exit.patch`) +- `src/include/tcop/backend_startup.h` (`0013-liboliphaunt-fix-embedded-backend-main-return-contract.patch`) +- `src/include/tcop/tcopprot.h` (`0003-liboliphaunt-return-from-embedded-frontend-terminate.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch`) +- `src/include/utils/hsearch.h` (`0017-liboliphaunt-namespace-dynahash-host-collisions.patch`) +- `src/port/chklocale.c` (`0011-liboliphaunt-add-android-embedded-shared-memory.patch`) +- `src/port/pqsignal.c` (`0020-liboliphaunt-enforce-embedded-signal-boundary.patch`) + +## Expected Upstream Touchpoints + +| File | Rationale | +| --- | --- | +| `meson.build` | Meson-hosted embedded builds enable OLIPHAUNT_EMBEDDED through an explicit opt-in build option. | +| `meson_options.txt` | Meson-hosted embedded builds declare opt-in backend and Windows module-provider options without changing default PostgreSQL builds. | +| `src/backend/access/transam/xlogarchive.c` | Apple mobile embedded builds compile out optional archive shell commands. | +| `src/backend/archive/shell_archive.c` | Apple mobile embedded builds compile out optional archive shell commands. | +| `src/backend/commands/event_trigger.c` | Embedded FE/BE protocol sessions can run event triggers without changing standalone recovery behavior. | +| `src/backend/libpq/be-secure.c` | Backend secure read/write path delegates to a host I/O vtable only when OLIPHAUNT_EMBEDDED is set. | +| `src/backend/libpq/pqcomm.c` | Standalone embedded sessions avoid waiting on a non-existent postmaster death latch. | +| `src/backend/meson.build` | Embedded MSVC extension modules link to the oliphaunt host import library instead of the standalone postgres executable. | +| `src/backend/port/Makefile` | Embedded mobile builds swap unavailable SysV shared memory and semaphores for process-local implementations. | +| `src/backend/port/meson.build` | Android embedded builds swap unavailable SysV shared memory and semaphores for process-local implementations. | +| `src/backend/port/oliphaunt_embedded_sema.c` | Embedded mobile semaphore implementation for one backend in one process. | +| `src/backend/port/oliphaunt_embedded_shmem.c` | Embedded mobile shared memory implementation for one backend in one process. | +| `src/backend/storage/ipc/ipc.c` | Embedded backend cleanup and proc_exit unwinding stay at PostgreSQL lifecycle boundaries. | +| `src/backend/storage/ipc/procsignal.c` | The one-backend embedded runtime dispatches ProcSignal flags without sending process-directed host signals. | +| `src/backend/tcop/postgres.c` | Embedded backend entrypoint, protocol lifecycle, cwd restoration, host runtime paths, and host-owned SIGUSR1 disposition. | +| `src/backend/utils/fmgr/dfmgr.c` | Static extension lookup reuses PostgreSQL dynamic function manager semantics. | +| `src/bin/initdb/initdb.c` | Base runtimes skip ICU-backed collation setup until optional ICU data is present. | +| `src/include/libpq/libpq-be.h` | Host I/O vtable is attached to PostgreSQL Port state under OLIPHAUNT_EMBEDDED. | +| `src/include/port.h` | Embedded mobile builds avoid POSIX shared memory declarations and route embedded backend signal calls through the host-safe provider boundary. | +| `src/include/storage/dsm_impl.h` | Embedded mobile builds keep DSM on mmap instead of POSIX or SysV shared memory. | +| `src/include/storage/ipc.h` | Embedded cleanup and proc_exit guard declarations. | +| `src/include/tcop/backend_startup.h` | Embedded BackendMain may return after its returning PostgresMain call without retaining an invalid pg_noreturn declaration. | +| `src/include/tcop/tcopprot.h` | Embedded entrypoint and returning PostgresMain declarations. | +| `src/include/utils/hsearch.h` | Apple builds namespace PostgreSQL dynahash symbols that otherwise bind to unrelated libSystem exports. | +| `src/port/chklocale.c` | Android embedded builds avoid unsupported locale-environment mutation. | +| `src/port/pqsignal.c` | Embedded backend signal registration and emission preserve the host-owned SIGUSR1 disposition while delegating other signals. | + +## PostgreSQL Patch Symbols + +- `oliphaunt_embedded_kill` (`0020-liboliphaunt-enforce-embedded-signal-boundary.patch`) +- `oliphaunt_embedded_main` (`0002-liboliphaunt-add-embedded-entrypoint.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch`) +- `oliphaunt_embedded_proc_exit` (`0004-liboliphaunt-run-embedded-exit-cleanup.patch`) +- `oliphaunt_embedded_proc_exit_handler` (`0009-liboliphaunt-guard-embedded-proc-exit.patch`) +- `oliphaunt_embedded_raise` (`0020-liboliphaunt-enforce-embedded-signal-boundary.patch`) +- `oliphaunt_embedded_set_proc_exit_handler` (`0009-liboliphaunt-guard-embedded-proc-exit.patch`) +- `oliphaunt_static_extension_init` (`0006-liboliphaunt-add-static-extension-loader.patch`) +- `oliphaunt_static_extension_lookup` (`0006-liboliphaunt-add-static-extension-loader.patch`) +- `oliphaunt_static_extension_magic` (`0006-liboliphaunt-add-static-extension-loader.patch`) +- `oliphaunt_static_extension_symbol` (`0006-liboliphaunt-add-static-extension-loader.patch`) + +## Audit Checklist + +| Requirement | Owning Patch | Required Evidence | Review Posture | +| --- | --- | --- | --- | +| Host-owned protocol I/O vtable | `0001-liboliphaunt-add-backend-host-io.patch` | `OliphauntEmbeddedIO`, `secure_raw_read`, `secure_raw_write` | Generic libpq backend hook; normal socket I/O remains untouched. | +| Standalone backend waitset guard | `0001-liboliphaunt-add-backend-host-io.patch` | `WL_POSTMASTER_DEATH`, `if (IsUnderPostmaster)` | Embedded standalone sessions avoid a postmaster-death wait handle that cannot exist. | +| Explicit embedded backend entrypoint | `0002-liboliphaunt-add-embedded-entrypoint.patch` | `oliphaunt_embedded_main`, `pq_init(&client_sock)`, `PostgresMain(dbname, username)` | Uses PostgreSQL backend initialization and FE/BE protocol instead of single-user query transport. | +| Embedded BackendMain may return without violating its declaration | `0013-liboliphaunt-fix-embedded-backend-main-return-contract.patch` | `#ifdef OLIPHAUNT_EMBEDDED`, `extern void BackendMain`, `pg_noreturn extern void BackendMain` | Only embedded builds drop pg_noreturn; normal PostgreSQL server builds retain the upstream non-returning contract. | +| Frontend Terminate returns to host owner | `0003-liboliphaunt-return-from-embedded-frontend-terminate.patch` | `frontend sends Terminate`, `return;`, `proc_exit(0)` | Only OLIPHAUNT_EMBEDDED changes backend termination into a returning thread lifecycle. | +| PostgreSQL exit callbacks still run | `0004-liboliphaunt-run-embedded-exit-cleanup.patch` | `oliphaunt_embedded_proc_exit`, `proc_exit_prepare(code)` | Keeps upstream cleanup ordering for shmem, locks, callbacks, and backend-local state. | +| Startup FATAL does not exit the host process | `0009-liboliphaunt-guard-embedded-proc-exit.patch` | `oliphaunt_embedded_set_proc_exit_handler`, `siglongjmp`, `proc_exit_handler` | Embedded startup failures unwind to liboliphaunt after PostgreSQL cleanup callbacks run. | +| Embedded proc_exit guard is cleared before returning to host | `0009-liboliphaunt-guard-embedded-proc-exit.patch` | `embedded_cleanup:`, `oliphaunt_embedded_set_proc_exit_handler(NULL, NULL)`, `chdir(original_cwd)` | Normal and FATAL startup paths share one cleanup label so thread-local exit guards and host cwd are restored before returning. | +| Host working directory is restored | `0005-liboliphaunt-restore-host-cwd.patch` | `original_cwd`, `getcwd(original_cwd`, `chdir(original_cwd)` | Contains PostgreSQL standalone ChangeToDataDir side effects inside the backend lifetime. | +| Static extension registry uses PostgreSQL dfmgr path | `0006-liboliphaunt-add-static-extension-loader.patch` | `oliphaunt_static_extension_lookup`, `lookup_library_symbol`, `oliphaunt_static_extension_symbol` | CREATE EXTENSION/LOAD semantics stay in PostgreSQL; hosts only provide module symbols. | +| MSVC PostgreSQL tools link without static extension providers | `0006-liboliphaunt-add-static-extension-loader.patch` | `defined(_MSC_VER) && (defined(_M_X64) || defined(_M_ARM64))`, `/alternatename:oliphaunt_static_extension_lookup=oliphaunt_static_extension_lookup_default`, `oliphaunt_static_extension_symbol_default` | Meson-built PostgreSQL tools get no-op static extension hooks on MSVC; liboliphaunt still overrides them by linking the real registry provider. | +| Portable PostgreSQL tools link without static extension providers | `0006-liboliphaunt-add-static-extension-loader.patch` | `#define OLIPHAUNT_OPTIONAL_HOOK __attribute__((weak))`, `oliphaunt_static_extension_lookup(const char *filename)`, `oliphaunt_static_extension_init(const OliphauntStaticExtension *extension)` | Non-MSVC embedded PostgreSQL tool links get weak no-op static extension hooks; liboliphaunt overrides them by linking the real registry provider. | +| Static extension ABI magic is validated | `0006-liboliphaunt-add-static-extension-loader.patch`, `0008-liboliphaunt-clean-embedded-symbols.patch` | `oliphaunt_static_extension_magic`, `Pg_magic_struct`, `memcmp(&magic_data_ptr->abi_fields` | Static modules still pass PostgreSQL ABI checks before symbols are used. | +| Runtime paths come from host-packaged resources | `0010-liboliphaunt-use-host-runtime-paths.patch` | `oliphaunt_embedded_set_runtime_paths`, `OLIPHAUNT_EMBEDDED_MODULE_DIR`, `my_exec_path`, `PGSYSCONFDIR` | Avoids executable-bit assumptions for mobile resources while preserving runtime path derivation and using host-packaged embedded modules for pkglib_path. | +| Apple mobile builds do not call system(3) | `0007-liboliphaunt-disable-shell-commands-on-apple-mobile.patch` | `OLIPHAUNT_EMBEDDED_NO_SHELL_COMMANDS`, `TARGET_OS_IPHONE`, `archive_command cannot be executed` | Mobile direct mode fails optional shell archive/restore hooks explicitly instead of compiling unavailable APIs. | +| Embedded mobile shared memory and semaphores are process-local | `0011-liboliphaunt-add-android-embedded-shared-memory.patch` | `oliphaunt_embedded_shmem.c`, `oliphaunt_embedded_sema.c`, `OLIPHAUNT_EMBEDDED_MOBILE_SHMEM` | Android and Apple mobile builds avoid unavailable SysV shared memory and semaphores while direct mode remains one backend per process. | +| Event triggers run in embedded protocol sessions | `0012-liboliphaunt-enable-event-triggers-in-embedded-backend.patch` | `EventTriggersHaveRunnableBackend`, `OLIPHAUNT_EMBEDDED`, `event_triggers` | Keeps upstream single-user escape hatch outside OLIPHAUNT_EMBEDDED but treats embedded protocol sessions as runnable backends. | +| Meson builds expose an explicit embedded backend option | `0015-liboliphaunt-add-embedded-meson-option.patch` | `oliphaunt_embedded`, `add_project_arguments`, `-DOLIPHAUNT_EMBEDDED` | Windows and other Meson-hosted embedded builds enable the backend entrypoint through PostgreSQL build configuration while default server builds remain unchanged. | +| Optional ICU data stays optional during initdb | `0016-liboliphaunt-skip-icu-collation-version-without-icu-data.patch` | `getenv("ICU_DATA")`, `pg_collation_actual_version`, `pg_import_system_collations` | Base liboliphaunt runtimes can bootstrap non-ICU databases without bundling optional ICU data; ICU packages keep upstream collation setup by setting ICU_DATA. | +| Apple builds namespace PostgreSQL dynahash symbols that collide with libSystem | `0017-liboliphaunt-namespace-dynahash-host-collisions.patch` | `#ifdef __APPLE__`, `oliphaunt_pg_hash_create`, `oliphaunt_pg_hash_destroy`, `oliphaunt_pg_hash_search` | Apple backend and extension objects share collision-free dynahash names; non-Apple PostgreSQL binary names remain unchanged. | +| Embedded ProcSignal delivery cannot escape into the host process | `0018-liboliphaunt-contain-embedded-proc-signals.patch` | `oliphaunt_send_proc_signal`, `pid != MyProcPid`, `procsignal_sigusr1_handler(SIGUSR1)`, `host owns SIGUSR1` | The one-backend embedded runtime dispatches ProcSignal flags synchronously, rejects foreign PIDs, and leaves the host SIGUSR1 disposition untouched; normal PostgreSQL server builds retain upstream signal delivery. | +| Windows embedded extension modules link to the host DLL provider | `0019-liboliphaunt-link-windows-embedded-modules-to-host.patch` | `oliphaunt_embedded_module_provider`, `requires an embedded MSVC Windows build`, `pg_mod_link_args += oliphaunt_embedded_module_provider`, `oliphaunt_embedded_module_provider == ''` | Embedded MSVC extension modules resolve PostgreSQL backend symbols from the oliphaunt host import library; ordinary PostgreSQL modules retain the upstream postgres executable link contract. | +| Embedded backend and extension signal calls preserve host SIGUSR1 ownership | `0020-liboliphaunt-enforce-embedded-signal-boundary.patch` | `oliphaunt_embedded_kill`, `oliphaunt_embedded_raise`, `!defined(FRONTEND)`, `if (signo == SIGUSR1)` | Embedded backend and extension calls cannot replace or emit host-owned SIGUSR1; other signals delegate to the platform implementation, while frontend tools and normal PostgreSQL builds retain upstream behavior. | + +## Guardrails + +- `source.toml` patch series exactly matches the patch directory. +- Every patch has a deterministic `From: liboliphaunt ` header. +- Every patch has a deterministic `Subject: [PATCH] liboliphaunt: ...` header. +- Entire patch files are checked for trailing whitespace; added PostgreSQL lines are also checked for space-before-tab indentation and SDK/runtime/product-specific terms that belong above PostgreSQL. +- Changed upstream files must exactly match the expected touchpoint table above; new upstream touchpoints need an explicit rationale before landing. +- Required audit checks prove their evidence in the named owning patch or patches, keeping host I/O, embedded lifecycle, cleanup, cwd restore, runtime paths, static extensions, host-signal containment, Windows module linkage, mobile shell exclusion, embedded mobile shared memory, and event triggers reviewable independently. +- Changed upstream files and patch-introduced `oliphaunt_*` symbols are listed here for release review. diff --git a/docs/internal/OLIPHAUNT_README.md b/docs/internal/OLIPHAUNT_README.md new file mode 100644 index 00000000..55febeae --- /dev/null +++ b/docs/internal/OLIPHAUNT_README.md @@ -0,0 +1,166 @@ +# Oliphaunt Internal README + +> Archived product-copy draft; non-normative. The repository root `README.md`, +> `docs/maintainers/README.md`, and executable product metadata describe the +> current product and release contract. + +This draft is retained only as implementation history. It predates the current +Oliphaunt root README and must not be used to infer release status, package +identity, or supported targets. + +Native-first embedded PostgreSQL for application developers who want PostgreSQL +semantics without running a separate database service. + +The long-term product is a small family of native SDKs over the same engine: + +- direct embedded mode for the lowest-latency in-process Tauri and Rust desktop + apps; +- broker mode for robust desktop apps that need crash isolation today. The + durable multi-root daemon is the longer-term broker shape and is not + advertised as available until it exists; +- server mode for real PostgreSQL client compatibility with `psql`, `pg_dump`, + ORMs, and connection pools. + +This repository now has two product lanes: + +- `liboliphaunt`: the C ABI boundary over embedded PostgreSQL 18. +- `oliphaunt`: the Rust SDK built on that native boundary. + +The existing `oliphaunt-wasix` WASIX release lane is preserved in +`src/bindings/wasix-rust/crates/oliphaunt-wasix` while native parity is built out. It remains separate from +the native SDK so we can keep the legacy release path stable without shaping the +new architecture around it. Native Rust APIs are not routed through +`oliphaunt-wasix`; they live in `oliphaunt`. + +SDK ownership is explicit. Rust is the SDK for Tauri and Rust desktop apps, +Swift is the SDK for iOS and macOS apps, Kotlin is the SDK for Android apps, +and React Native is the TypeScript/TurboModule SDK over the Swift and Kotlin +SDKs. TypeScript is the SDK for Node.js, Bun, and Deno. Tauri apps currently +keep Oliphaunt in Rust state behind narrow app-owned commands; a direct +JavaScript/webview adapter is planned, not part of the first release. +SDK features should have parity where the platform can support them honestly; +platform support is summarized in the +[`Capability Matrix`](../../src/docs/content/reference/capabilities.mdx), +with the maintainer contract in +[`SDK Parity`](../maintainers/sdk-parity-policy.md). + +## Layout + +- `src/runtimes/liboliphaunt/native/`: C ABI, PostgreSQL 18 source pin, patch stack, native build and + smoke harnesses. +- `src/sdks/rust/`: native Rust SDK surface. +- `src/bindings/wasix-rust/crates/oliphaunt-wasix/`: existing WASIX-based Rust package. +- `src/runtimes/liboliphaunt/wasix/crates/assets/` and `src/runtimes/liboliphaunt/wasix/crates/aot/`: packaged WASIX release assets. +- `src/sdks/swift/`, `src/sdks/kotlin/`, `src/sdks/react-native/`, + and `src/sdks/js/`: platform and runtime SDKs. +- `tools/policy/sdk-manifest.toml`: SDK ownership registry used by parity checks. +- `tools/`: repo automation, including `xtask` and validation scripts. +- `benchmarks/`: benchmark plans and future cross-engine harnesses. +- `src/docs/`: public Fumadocs/Next docs product, generated matrices, + tested snippets, API-reference stubs, and LLM docs. +- Public SDK docs live under `src/docs/content/sdk/`; product roots + keep only package README/CHANGELOG files and source-adjacent API comments. +- `docs/`: architecture, release, development, maintainer, and internal source + material. +- `docs/internal/`: maintainer-only progress notes and generated patch-stack + audits. + +See [repo-structure.md](../maintainers/repo-structure.md) for the repository policy and the evidence behind +the layout. + +## Current Native Status + +The native track is usable as an active development lane, not yet a default +release replacement: + +- macOS arm64 native `liboliphaunt` builds against PostgreSQL 18.4; +- the C smoke opens, executes raw protocol queries, recovers after SQL errors, + streams a large protocol response, closes, and reopens the same PGDATA from a + new process; +- the Rust SDK for Tauri and Rust desktop apps exposes `NativeDirect`, + `NativeBroker`, and `NativeServer`; +- broker mode uses Unix-domain sockets on Unix platforms, with explicit TCP + fallback for portability and debugging, and enforces the selected bootstrap + policy inside the helper; +- direct and broker expose same-version physical backup/restore, while server + mode also exposes logical SQL backup through packaged `pg_dump`; +- the gated native extension matrix creates or loads release-ready PostgreSQL 18 + extensions by exact SQL name, then verifies restart and physical restore + through broker/direct-C-ABI and server paths; +- Rust, Swift, Kotlin, React Native, and TypeScript SDK lanes track the same product + concepts where platform constraints allow it, with platform status summarized + in `src/docs/content/reference/capabilities.mdx`; +- the benchmark matrix measures native direct, broker, server, native + PostgreSQL controls, and SQLite comparison data without entering the legacy + WASIX release lane. + +Maintainers track release-claim evidence and open blocker audits in +[OLIPHAUNT_TRACK_REVIEW.md](OLIPHAUNT_TRACK_REVIEW.md). + +## Common Commands + +```sh +moon query projects +moon query tasks +moon run repo:check +moon run :check +moon run :test +moon run :package +moon run :coverage +moon run liboliphaunt-native:host-smoke +moon run oliphaunt-react-native:smoke-mobile +moon run oliphaunt-js:check +``` + +Moon is the contributor command surface. `.prototools` pins Moon, Node, pnpm, +Bun, and Deno. Use pnpm to install JavaScript workspace dependencies when +working on JavaScript-family projects; do not use it as a repo-wide task +router. Bun is required for the +TypeScript SDK check because Bun installs `@oliphaunt/ts` from npm; Deno is +used by strict JSR consumer-release gates. + +React Native installed-app validation uses the Expo development-client example +as the default harness because the package always exercises custom Swift/Kotlin +native code. `moon run oliphaunt-react-native:smoke-android`, +`moon run oliphaunt-react-native:smoke-ios`, and +`moon run oliphaunt-react-native:smoke-mobile` run the installed app lanes. +`moon run oliphaunt-react-native:check` is the package-only TypeScript, +Codegen, and native-source lane. `moon run +oliphaunt-js:check` validates the desktop JavaScript SDK, including npm and JSR +package shape. + +For liboliphaunt work, use the product Moon tasks above. Product inner loops +should use `moon run :check` and `moon run :test`; CI lanes +use `moon ci` through `.github/scripts/run-moon-ci.sh`. + +## Native Performance Matrix + +After building `liboliphaunt`, run: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh +``` + +For fast local plumbing checks: + +```sh +cargo build -p oliphaunt-perf +target/debug/oliphaunt-perf native-liboliphaunt --engine direct --suite rtt --iterations 10 +target/debug/oliphaunt-perf native-liboliphaunt --engine broker --suite rtt --iterations 10 +target/debug/oliphaunt-perf native-liboliphaunt --engine server --suite rtt --iterations 10 +``` + +The native matrix opts in to `perf-runner support explicitly, so ordinary +asset/release automation does not compile legacy WASIX or benchmark-only code. +Use `--quick` for a one-repeat plumbing run and `--plan-only` to inspect the +native-only command plan without checking artifacts or building anything. +Focused diagnostic runs can select one engine or suite without changing the +release default: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh \ + --quick --engines broker --suites streaming +``` + +Selector runs are for local evidence and debugging. Release evidence uses the +default all-engine/all-suite matrix. diff --git a/docs/internal/OLIPHAUNT_TRACK_REVIEW.md b/docs/internal/OLIPHAUNT_TRACK_REVIEW.md new file mode 100644 index 00000000..49f28480 --- /dev/null +++ b/docs/internal/OLIPHAUNT_TRACK_REVIEW.md @@ -0,0 +1,766 @@ +# liboliphaunt Track Review + +Date: 2026-05-16 + +> **Historical snapshot — non-normative.** This review records the repository +> as it existed on 2026-05-16. Words such as “current,” “remaining,” “blocker,” +> and “not production-complete” below do not describe the present release +> candidate and must not be used by maintainers or agents as release policy. +> Determine current readiness from +> [`docs/maintainers/release.md`](../maintainers/release.md), +> [`docs/maintainers/release-setup.md`](../maintainers/release-setup.md), the +> generated target/catalog contracts, and the repository +> [`release-oliphaunt`](../../.codex/skills/release-oliphaunt/SKILL.md) and +> [`qualify-oliphaunt-change`](../../.codex/skills/qualify-oliphaunt-change/SKILL.md) +> skills. Verify any still-interesting observation against the current source +> and exact-SHA evidence before acting on it. + +## Executive Summary + +The native track is directionally correct: `liboliphaunt` is the C boundary over +PostgreSQL 18, `oliphaunt` is the canonical Rust SDK, and the existing +WASIX `oliphaunt-wasix` lane is no longer shaping native architecture decisions. +That separation is the right foundation if the goal is an embedded PostgreSQL +product that can credibly compete with SQLite for application developers. + +The current implementation is still not complete enough to market as that +product. The biggest remaining gaps are not API naming or package polish. They +are: + +- lifecycle: direct mode is one active embedded PostgreSQL backend per process; +- streaming: native direct has a chunked C ABI, broker forwards chunks over IPC, + and server forwards PostgreSQL wire messages per frame; large results and + `COPY TO STDOUT` now have native matrix rows plus a native SQL regression + proving post-COPY session reuse across direct, broker, and server. The + source-current full matrix has been refreshed after the latest harness + changes; +- coverage: we have smoke and selected SQL regression coverage, not a + PostgreSQL-grade regression matrix; +- benchmarking: direct, broker, server, native PostgreSQL, and SQLite embedded + are now measured from one reproducible harness with p50/p90/p95, CPU, RSS, + footprint, artifact-size rows, and source/artifact provenance. The latest + full source-current matrix passes provenance verification; the remaining + discipline is to refresh that run after every benchmark harness or runtime + input change and reject stale reports; +- mobile packaging: Swift and Kotlin/Native now have concrete native-direct + C ABI runtime paths; React Native has the typed New Architecture package + shape, and Android now has a content-keyed runtime/template asset lane, but + final platform artifacts and device distribution are not wired end to end; +- extensions: extensions are opt-in in the Rust model, and the packaged + PG18 extension matrix now passes install/load, restart, physical backup, and + physical restore checks across broker/direct-C-ABI and server paths; pgGraph + and ParadeDB external smokes now also cover core functional queries across + direct, broker, and server; the C ABI static registry exists and is smoke + tested, while generated platform registry sources/device packaging and signed + manifests remain release blockers. + +This track pass addressed concrete gaps: + +- C `initdb` bootstrap no longer uses `system(3)` or shell quoting. It now + forks and execs `initdb` directly, preserves stderr for diagnosis, suppresses + stdout, reports exec/status/signal failures, and avoids command-injection + classes of bugs. +- Direct native streaming now exists at the C ABI through + `oliphaunt_exec_protocol_stream`. It drains backend writes into a bounded chunk + queue with producer backpressure, scans protocol frames incrementally for + `ReadyForQuery`, and invokes the sink from the caller thread instead of the + backend thread. +- Broker streaming now uses dedicated IPC stream request/chunk/end frames + instead of materializing the whole response in the client process. +- Broker IPC now uses Unix-domain sockets by default on Unix platforms, keeping + the helper-process path off localhost TCP while preserving an explicit TCP + fallback for portability and debugging. +- Broker IPC now requires a per-session authentication frame before any protocol + or control request. The SDK generates the token and passes it to the helper + through the child environment rather than argv. +- Broker startup now propagates the parent bootstrap policy to the helper, + including `ExistingOnly` and the explicit `initdb` tooling fallback, so the + helper cannot silently hydrate a root the caller intended to treat as + pre-existing. Broker shutdown also waits briefly for graceful helper exit + before falling back to kill. +- Broker capabilities now stay honest: direct mode remains one process-global + backend, while broker mode supervises one isolated helper per active root. + A shared broker runtime enforces `.broker_max_roots(n)`, rejects duplicate + active roots, and reports multi-root capability only when the configured root + budget is greater than one. +- Broker sessions now retain their helper launch plan and relaunch a fresh + helper against the same root when the previous helper exits between + operations. The SDK deliberately does not replay an in-flight request after a + crash because the request's commit state may be unknown; a caller observes + that failure and later operations can recover through PostgreSQL WAL recovery. +- The native root/runtime module is split by responsibility: process plus + filesystem root locking and PGDATA path preparation stay in + `oliphaunt/root.rs`; runtime-cache discovery + and materialization are split across `oliphaunt/root/runtime.rs`, + `oliphaunt/root/runtime/locate.rs`, `oliphaunt/root/runtime/install.rs`, and + `oliphaunt/root/runtime/cache_key.rs`; deterministic filesystem copying lives + in `oliphaunt/root/files.rs`; runtime/template cache fingerprinting lives in + `oliphaunt/root/fingerprint.rs`; and packaged-template PGDATA cache + construction lives in `oliphaunt/root/template.rs`. +- Selected extension asset materialization now lives in + `oliphaunt/root/extensions.rs`, including SQL/control file selection, data + files, module-file sets, and the filters that hide unselected extension + assets from materialized runtime trees. +- Server mode now streams PostgreSQL wire messages frame-by-frame and returns + SQL `ErrorResponse` frames as raw protocol bytes instead of converting normal + SQL errors into Rust execution failures. +- Server mode now separates short connection-attempt timeouts from longer query + I/O timeouts, so normal work such as `CREATE EXTENSION` is not constrained by + the retry loop's 250 ms connection probe. +- Server mode now retries auto-assigned localhost ports only when PostgreSQL + reports a bind conflict during startup. This removes parallel-test and local + process races without hiding fixed-port failures or unrelated server startup + errors. +- Server mode now uses a short SDK-owned Unix-domain socket on Unix for + internal protocol traffic while keeping the public TCP connection string for + external clients. The raw-wire client also reads backend frames through a + reusable buffered reader instead of issuing header/body reads and allocating + body/frame buffers per message. +- The Rust owner executor now uses `crossbeam-channel` for the per-session + command queue instead of `std::sync::mpsc`, preserving the serialized owner + thread while reducing direct-mode RTT handoff overhead. +- `liboliphaunt` now exposes `oliphaunt_exec_simple_query` and + `OLIPHAUNT_CAP_SIMPLE_QUERY` so SDK simple-query calls do not need to build and + revalidate frontend protocol frames outside the engine. The Rust direct + engine loads this symbol opportunistically, while broker mode forwards a + first-class simple-query IPC frame to the helper and lets the helper call the + same engine hook. +- The C ABI no longer imposes a hard-coded default execution timeout while + waiting for `ReadyForQuery`. Startup readiness still has a bounded wait, but + normal owned-response and streaming protocol calls run until PostgreSQL + completes, exits, closes, or is explicitly canceled. The C smoke harness sets + the legacy wait-timeout env after startup and proves a `pg_sleep` query still + succeeds, preventing accidental reintroduction of a synthetic query cap. +- The native performance harness now measures `NativeDirect`, `NativeBroker`, + and `NativeServer` as distinct SDK modes instead of reporting broker as + unavailable or using native PostgreSQL control numbers as a substitute. +- Native benchmark matrix runs now write `provenance.json` with SHA-256s for + benchmark harness sources, `liboliphaunt`/PostgreSQL patch inputs, Rust SDK + sources, `xtask`, and native artifacts. Product-local perf checks verify a + retained run directory against the current checkout so stale reports are not + accidentally treated as current release evidence. +- The native benchmark matrix now records prepared-update rows for + `NativeDirect`, `NativeBroker`, and `NativeServer`. Each row uses PostgreSQL's + extended protocol with one named prepared statement and covers both + sequential Bind/Execute/Sync traffic and a pipelined Bind/Execute batch inside + one transaction. +- The gated native extension matrix now creates or loads every currently + release-ready exact extension through broker/direct-C-ABI and server paths, reopens the + root, takes a physical backup, restores it into a new root, and verifies the + extension remains visible after restore. The manifest also distinguishes + SQL-only extensions such as `pgtap` from extensions that require a native + module. +- The native PostgreSQL build harness now fingerprints extension source trees, + compiler selection, PostgreSQL patch/build inputs, and `liboliphaunt` C ABI + sources before rebuilding extension artifacts. Gated extension validation can + reuse existing normal and embedded module artifacts unless those inputs + change, with `OLIPHAUNT_FORCE_EXTENSION_REBUILD=1` as the explicit escape + hatch. +- Repeated native C validation no longer relinks `liboliphaunt.dylib` when the + edited C ABI sources and PostgreSQL embedded object/archive inputs are + unchanged. The build harness writes a separate dylib input stamp, verifies + required exported C ABI symbols before reuse, and exposes + `OLIPHAUNT_FORCE_RELINK=1` for deliberate relink diagnostics. +- The Rust SDK now exposes `NATIVE_EXTENSION_MANIFEST` as the product manifest + for supported PG18 extensions. It records each extension's SQL/control asset + class, native module requirement, dependencies, runtime data files, smoke SQL + strategy, gated direct-C-ABI/broker/server coverage, and mobile static-link + status. The gated native extension matrix iterates this manifest directly. +- The C smoke harness now covers ABI version/capability reporting, invalid init + and invalid exec/stream arguments, malformed frontend frame rejection and + recovery, normal protocol success, SQL error recovery, large owned responses, + stream callback delivery, stream callback failure recovery, response cleanup, + active-query cancellation and recovery, direct C ABI backup/restore, + malicious archive-entry rejection, symlinked PGDATA backup rejection, caller + `PGDATA` environment restoration after close, explicit same-process direct + reopen rejection, and process-bound reopen through a second harness process. +- `liboliphaunt` now exposes out-of-band direct query cancellation through + `oliphaunt_cancel` and `OLIPHAUNT_CAP_QUERY_CANCEL`. The Rust SDK surfaces this as + `Oliphaunt::cancel()` backed by an `EngineCancel` handle that bypasses the owner + queue, so a long-running direct query can be interrupted without waiting + behind itself. +- Broker and server now preserve that same out-of-band cancellation contract at + their natural transport layer. Broker mode creates a separate authenticated + cancel IPC endpoint so cancellation never competes with the busy query stream. + Server mode captures PostgreSQL startup `BackendKeyData` and sends the native + CancelRequest packet over a fresh connection to cancel the SDK-owned backend. +- `Oliphaunt::close()` now treats close as a lifecycle boundary rather than a + cancellation primitive. Once close begins, the executor rejects queued + non-close work with `EngineStopped`, waits for active work to finish, then + lets the owner thread close or detach the runtime. Query interruption remains + explicit through `Oliphaunt::cancel()`. +- The native runtime now has a profile-aware content-keyed runtime cache. + Direct/broker use liboliphaunt-linked extension modules; server mode uses + standalone PostgreSQL modules. This fixes the previous server-mode crash caused + by loading embedded modules into a standalone server while preserving + opt-in extension isolation. +- Direct liboliphaunt and managed server startup now provide `PGDATA` in the + backend environment. PostgreSQL itself receives `-D`, but some existing + extension code, including pgGraph persistence, reads `PGDATA` directly; the + native runtime now supplies that root and restores the caller's environment + after the embedded backend exits. +- `liboliphaunt` now exposes `oliphaunt_register_static_extensions` and + `OLIPHAUNT_CAP_STATIC_EXTENSIONS`. The PostgreSQL `dfmgr` patch resolves + registered in-binary extension modules through the normal dynamic-loader path, + validates PostgreSQL magic, calls the registered init hook once, and resolves + exported SQL-callable C symbols without requiring a module file. The C smoke + harness registers a fixture extension before `oliphaunt_init` and proves + `CREATE FUNCTION ... AS 'module', 'symbol' LANGUAGE C` executes through that + registry. +- The Rust runtime resourcesr now emits a static-registry package alongside + runtime/template resources. Mobile-ready packages contain + `static-registry/oliphaunt_static_registry.c`, generated from selected + extension SQL assets, plus a manifest that records module stems, symbol + prefixes, and SQL-callable symbols. The generated source exports + `liboliphaunt_selected_static_extensions`; Swift and Kotlin native bridges look + up that optional process symbol and register the rows through the loaded + `oliphaunt_register_static_extensions` symbol before `oliphaunt_init`. +- Extension configuration now fails during `OpenConfig` validation for + duplicate extension names, empty or non-portable IDs, unsupported extension + source/loading combinations, and source/loading mismatches. + This keeps extension packaging mistakes out of runtime materialization. +- `BootstrapStrategy::PackagedTemplate` now materializes a content-keyed base + PGDATA template and hydrates new roots before entering the engine. Template + hydration now defaults to physical byte-copy because paired local evidence + showed better p90 stability than APFS copy-on-write cloning; clone mode + remains available as an explicit diagnostic setting. +- The speed-case diagnostic harness now supports native liboliphaunt direct for a + single case per process and native PostgreSQL controls for matched case-level + diagnosis. The native PostgreSQL control connects to `template1`, matching + liboliphaunt's current session target, so per-case comparisons no longer mix + different database targets. Diagnostic output now records the process model + and key PostgreSQL GUCs, and ad hoc native PostgreSQL diagnostics default to + the repo's pinned `target/liboliphaunt-pg18/install/bin` tools when present + instead of accidentally using a different `postgres` on `PATH`. +- The native benchmark matrix has a source-current PostgreSQL 18.4 full local + run at `target/perf/native-liboliphaunt-20260524T090412Z/report.md` with + matched `safe` durability for native liboliphaunt, native PostgreSQL, and + SQLite controls plus strict verified `provenance.json` for that recorded + source/artifact set. Later backup ABI and tar-writer changes require a new + full matrix before using the report as current-checkout release evidence. + Native direct passes + repeated RTT, open, and RSS gates against the native PostgreSQL control in + that run; RTT gate p90 is `107 us` versus `112 us` for native PostgreSQL + tokio, and open p90 is `440.28 ms` versus `576.4 ms`. Native direct still + misses speed-suite p90 (`2.668 s` versus `2.419 s`), speed tail throughput + (`0.907x` native PostgreSQL), and physical backup/restore (`0.558 s` versus + `0.344 s`) against the native PostgreSQL physical-archive control. The matrix uses 10 + fresh-process RTT repeats, 20 fresh-process speed repeats, 10 prepared + repeats, and 10 backup/restore repeats before classifying evidence as + release-grade. Direct, broker, server, native PostgreSQL tokio, and SQLite are + `stable` on that host run. Per-case speed misses in the complete matrix are + `1`, `2`, `2.1`, `3`, `3.1`, `4`, `5`, `10`, and `13`; isolated repeated + diagnostics reproduce `1`, `2.1`, `3`, `4`, `10`, and `13`. A focused + current-source backup diagnostic at + `target/perf/native-liboliphaunt-20260524Tbackup-final-direct/report.md` + verifies `oliphaunt_backup_ex` and the current C tar writer, improving direct + physical backup/restore p90 to `0.534 s` while still missing native + PostgreSQL physical p90 at `0.324 s`. Against SQLite, direct wins total + speed-suite p90 but still loses open p90 and RSS by large margins. The streaming section + includes large row results and `COPY TO STDOUT`, and prepared-update rows + cover sequential and pipelined extended-protocol traffic across direct, + broker, server, and native PostgreSQL controls. +- The first source-current full-matrix attempt exposed a server-mode raw + protocol bug where a large pipelined frontend batch could fill the PostgreSQL + server's output socket while the SDK was still writing the request. + `PostgresWireClient` now switches large raw requests to duplex read/write, + and the full rerun passes with server pipelined prepared p90 at `0.239 s` + numeric and `0.265 s` text versus native PostgreSQL tokio at `0.288 s` and + `0.291 s`. +- Direct/server physical backup now uses PostgreSQL's low-level online backup + API (`pg_backup_start` and `pg_backup_stop`) and writes the generated + `backup_label`/`tablespace_map` into the physical archive. The implementation + collects `pg_wal` after backup stop, and the native server smoke restores the + archive into a new root and reads user data from it. Broker mode forwards the + same direct physical backup through the helper process. Physical archives are + explicitly single-root concrete archives: backup fails on non-regular PGDATA + entries, and restore accepts only regular files and directories under + `pgdata`. Restore also rejects malformed framing, missing terminators, + trailing non-zero data, unsupported tar header formats, invalid tar numeric + and fixed-width string fields, duplicate canonical paths, unexpected link + metadata, and directory entries with payload bytes. Restore extracts through + the validated canonical archive path rather than delegating destination + interpretation to the tar reader, and validates archive tree shape before + writing staging files in both the Rust SDK and C ABI restore paths. Symlinks, + hardlinks, FIFOs, sockets, device nodes, sparse/special tar records, external + tablespaces, and linked WAL directories are rejected. Server mode also + exposes logical SQL backup through packaged `pg_dump`. +- Restore/import is now a first-class Rust SDK operation through + `Oliphaunt::restore(RestoreRequest::physical_archive(...))`. It stages restore + output, rejects archive traversal and unsupported archive entry types, + validates required recovery files, rejects symlink targets, protects + existing roots by default, and supports explicit locked replacement. Swift, + Kotlin, and React Native API shapes mirror the same root-level restore model. +- The native benchmark matrix now includes a file-backed SQLite control via + rusqlite plus artifact-size rows for `liboliphaunt`, embedded modules, and the + PostgreSQL install tree. +- Native liboliphaunt benchmark runs now sample child-process RSS for broker and + server modes from the xtask process tree. RTT, speed, and streaming reports no + longer have to infer helper/server memory solely from `/usr/bin/time` on the + parent benchmark process. +- The xtask RSS/process-tree sampler has been extracted to + `tools/xtask/src/process_rss.rs` with focused unit coverage for descendant + aggregation and cycle/double-count protection. This keeps benchmark resource + accounting separate from command orchestration. +- The Swift SDK now includes `OliphauntNativeDirectEngine`, backed by a small + C bridge that dynamically loads `liboliphaunt` or resolves already-linked C ABI + symbols. Env-backed Swift tests open a temporary native-direct root, execute + raw protocol bytes, cancel an active `pg_sleep`, and close through the C ABI. +- The Kotlin SDK now includes a Kotlin/Native `NativeDirectEngine`, backed by a + small static cinterop bridge that dynamically loads `liboliphaunt`, keeps the + public API suspend-first, defaults `OliphauntDatabase.open` to native direct on + Kotlin/Native, runs blocking protocol work off the caller coroutine, exposes + cancellation outside the serialized execution queue, makes `close()` wait for + the execution lane before detaching, and recursively cleans temporary roots + with symlink-safe POSIX tree removal. +- The React Native TurboModule surface now exposes `cancel(handle)` and the + TypeScript `OliphauntDatabase.close()` path delegates wait-and-detach close to + the platform SDK, matching the Rust/Swift/Kotlin lifecycle contract at the + public API layer. +- React Native iOS now delegates its TurboModule implementation to `Oliphaunt` + through an Objective-C-visible Swift adapter. The Objective-C++ file keeps + only React Native handle/promise plumbing and New Architecture registration; + Swift owns open, protocol execution, backup, restore, cancellation, close, + resource materialization, template hydration, and extension checks. +- React Native Android now delegates its TurboModule implementation through the + Kotlin SDK `OliphauntAndroid` facade and stores the returned `OliphauntDatabase` + handle. The package Gradle build includes the local `:oliphaunt` + project in this repo, falls back to the published Kotlin SDK coordinate for + packaged app builds, generates the official Codegen TurboModule base class, + compiles Kotlin, verifies the Kotlin SDK JNI bridge, and exercises synthetic + runtime/template asset packaging in the verifier. Runtime materialization, + template hydration, JNI loading, and extension manifest checks are owned + by the Kotlin SDK instead of duplicated in the RN package. Device validation + remains blocked on packaged Android `liboliphaunt.so` and real runtime/template + artifacts. +- Server compatibility coverage now includes real `psql`, `tokio-postgres` + independent-client, `sqlx` pool, packaged `pg_dump` through SQL backup, and + persistent close/reopen smoke. Broker coverage now includes `ExistingOnly` + rejection for empty roots plus persistent close/reopen smoke. +- The Rust SDK now exposes `Oliphaunt::query(sql)` over the native simple-query + path. The parser keeps the C ABI raw, but gives Rust callers field metadata, + rows, command tags, null handling, and PostgreSQL `ErrorResponse` propagation + without asking applications to decode backend protocol frames for ordinary + one-result-set queries. Multi-result-set and COPY traffic stay on the raw or + streaming protocol APIs. +- Swift, Kotlin, and React Native now expose the same typed result concept for + simple SQL and PostgreSQL extended-protocol parameters. Their parsers and + frontend message builders live in the SDK language layer, not in the C ABI, + and cover field metadata, rows, command tags, nulls, and SQL errors while + keeping multi-result-set and COPY traffic on raw protocol APIs. +- The same query layer now exposes `query_params(sql, params)` using + PostgreSQL's extended protocol. Parameters are encoded into `Parse`/`Bind`/ + `Describe`/`Execute`/`Sync` frames in Rust and then sent through the same raw + protocol engine path, so the low-level C ABI remains stable while the Rust DX + gets a safe non-interpolating query API. +- Dropping an unfinished `Transaction` now queues a best-effort `ROLLBACK` on + the owner executor before releasing the physical-session pin. This prevents a + Rust lifetime mistake from leaving the single direct/broker backend session + inside an open SQL transaction and blocking unrelated follow-up work. +- Native SQL regression coverage now lives in `oliphaunt` itself instead + of depending on the legacy WASIX crate's regression tests. The new + `native_sql_regression` test runs the same compact PostgreSQL behavior suite + through direct, broker, and server modes: parameterized inserts, numeric/bool/ + JSONB/bytea/array values, trigger side effects, views, constraint errors, + savepoint recovery, committed transactions, index-plan checks, and SQL error + recovery. + +## Product Architecture Judgment + +The ultimate product should remain Rust-first for now: + +- `liboliphaunt` owns the embeddable PostgreSQL C ABI and upstream patch stack. +- `oliphaunt` owns Rust configuration, root management, extensions, + async execution semantics, broker/server selection, tests, and benchmarks. +- Swift, Kotlin, and React Native should follow the Rust SDK semantics instead + of defining parallel product behavior. + +This is the right split because the hardest correctness decisions are +PostgreSQL lifecycle, storage roots, extension loading, backup/restore, +concurrency, and performance. Rust is the best place in this repo to encode +those decisions once and make other SDKs thinner. + +The three-mode model is also correct: + +- `NativeDirect` is the lowest-latency embedded path. It must stay honest: one + physical backend session, no fake pools, no fake independent connections. +- `NativeBroker` is the robust app mode. It should become the default desktop + recommendation when developers need multiple roots, crash isolation, upgrade + orchestration, or long-running app behavior. +- `NativeServer` is the compatibility mode. It must be a real PostgreSQL server + process for `psql`, `pg_dump`, ORMs, pools, and independent sessions. + +The mode split is how this competes with SQLite without pretending PostgreSQL +has SQLite's process model. SQLite wins by having a small, direct, single-file +engine. Native Oliphaunt can compete only if it is honest about where PostgreSQL is +stronger: SQL compatibility, extensions, types, query planner, ecosystem, and +server compatibility. Direct mode should win on embedded latency where possible; +broker/server modes should win on robustness and compatibility where direct mode +cannot. + +## C ABI And PostgreSQL Patch Stack + +The current C ABI is deliberately small: + +- `oliphaunt_init` +- `oliphaunt_exec_protocol` +- `oliphaunt_exec_simple_query` +- `oliphaunt_exec_protocol_stream` +- `oliphaunt_cancel` +- `oliphaunt_close` +- `oliphaunt_last_error` +- `oliphaunt_version` +- `oliphaunt_capabilities` +- `oliphaunt_free_response` + +That is a good first boundary. It keeps query semantics on PostgreSQL's native +wire protocol and avoids inventing a second SQL API at the C layer. + +The PostgreSQL 18 patch stack is mostly defensible: + +- host I/O hooks are added below backend libpq communication; +- a dedicated embedded backend entrypoint is used instead of single-user mode; +- embedded shutdown runs PostgreSQL cleanup without exiting the host process; +- current-working-directory restoration is explicit. + +The patches should remain generic and upstreamable. They must not learn about +Rust, React Native, iOS, Kotlin, extension manifests, or product policy. +The upstream shape should be "PostgreSQL can run one backend session with +host-provided read/write callbacks and explicit lifecycle." + +Open patch-stack risks: + +- simultaneous direct instances are rejected; repeated direct lifetimes in one + process remain a release-gated lifecycle area because PostgreSQL process + globals are not designed as a normal library lifecycle; +- embedded lifecycle still touches `postgres.c` enough that every PostgreSQL + minor bump needs careful review; +- no upstream-style test target exists inside the patched PostgreSQL tree for + the embedded entrypoint; +- cancellation now has mode-specific implementations for direct, broker, and + server, and close now waits for active SDK-owned work before shutdown/detach. + Rust SDK smoke now covers repeated cancellation/recovery for direct, broker, + and server plus PostgreSQL CancelRequest behavior from an external + `tokio-postgres` server-mode client; +- COPY now has direct/broker/server/native-PostgreSQL rows in the + source-current native matrix and env-gated Rust regression coverage for both + client-driven `COPY FROM STDIN` and streamed `COPY TO STDOUT`. The regression + sends frontend CopyData/CopyDone frames, validates the `CopyInResponse`, + inserted payloads, and post-COPY reuse, then drives invalid COPY input and + frontend `CopyFail` to verify `ErrorResponse`, `ReadyForQuery`, zero committed + rows, and post-error session reuse. It then streams `COPY TO STDOUT`, + validates the backend protocol frames and payload size, and proves the + session can execute a normal query afterward. The remaining release + discipline is to refresh those measurements whenever the harness, runtime + inputs, or protocol paths change. + +## Rust SDK Review + +Strengths: + +- `oliphaunt-wasix` and `oliphaunt` are separate packages. +- `EngineMode` separates direct, broker, and server instead of mixing WASIX and + native selection. +- direct mode is cloneable at the Rust handle level but serialized through an + owner executor. +- transaction/session pinning prevents unpinned work from interleaving with a + physical-session-sensitive transaction. +- extension selection is explicit by exact PostgreSQL extension name. +- root locking treats live storage as a directory and now combines a + same-process canonical root registry with the `.oliphaunt.lock` filesystem + marker plus a stable sibling filesystem lease keyed by the canonical root + path. The stable lease is used across direct, broker-helper, server, backup, + and restore paths, so root replacement stays locked while the old directory is + moved aside and the restored directory is published. Restore/import reserves + missing and empty target paths with the stable lease without creating an + in-root marker before publish. +- plain C ABI callers now also get default root ownership at `oliphaunt_init` + through a stable sibling filesystem lease keyed to ``, with + `/.oliphaunt.lock` kept as the visible root marker. + C `oliphaunt_restore` takes the same stable lease before staging or publishing, + so it cannot replace a live direct-C-ABI root. The stable filename now uses + the same SHA-256 prefix algorithm as the Rust SDK rather than a C-only hash, + so cross-SDK missing-target restore reservations contend on one root + identity. SDKs may opt out only with `OLIPHAUNT_CONFIG_EXTERNAL_ROOT_LOCK`; the + Rust SDK sets that flag because it already owns the broader + direct/broker/server/backup/restore coordinator. + +Gaps: + +- `NativeDirect` now uses the chunked `liboliphaunt` streaming ABI. `NativeBroker` + forwards those chunks over broker IPC. `NativeServer` streams complete + PostgreSQL wire frames as they are read from the local server connection. +- `NativeBroker` is a supervised worker-per-root architecture using + authenticated Unix-domain socket IPC on Unix. That is the correct shape while + the direct C ABI is process-global: one worker can crash without taking down + the application or other broker roots, and the SDK can bound root fan-out. + Helper relaunch after an observed crash is now covered; durable request + replay, richer crash policy, and upgrade orchestration remain + broker-supervisor release gates. +- `NativeServer` is a real server process with release-gate smoke for `psql`, + packaged `pg_dump`, `tokio-postgres`, `tokio-postgres` external cancellation, + `sqlx` pools, restart, and concurrent sessions. It still needs broader + ORM-specific coverage beyond SQLx. +- direct and broker expose same-version physical backup; server additionally + exposes logical SQL backup through `pg_dump`. Physical restore/import is now + a first-class Rust SDK API with safe staging and locked replacement, and the + server restore smoke proves the archive can recover user data. +- root/runtime/template materialization is no longer carrying the low-level + filesystem copy, fingerprinting, selected-extension asset policy, + runtime-cache orchestration, runtime asset installation, runtime cache + key/validation, or packaged-template bootstrap policy in the same file. + Focused cache-key tests now prove selected extension SQL/module changes + invalidate runtime caches, unselected extension assets remain invisible, and + selected extensions are required during cache validation. + +## Extension Architecture + +The correct extension model is manifest-gated and opt-in: + +- no default "everything" bundle; +- `CREATE EXTENSION` succeeds only when the selected exact extension provides + SQL/control files and the linked or packaged module; +- mobile uses static registries and resource bundles; +- desktop may additionally support signed dynamic extension artifacts; +- Rust defines the exact-extension resource contract first, other SDKs mirror it. + +The current Rust surface supports explicit extension selection, and the root +materializer copies selected extension assets. Native smoke coverage verifies +that an unselected extension fails and the selected `vector` extension works +through direct, broker, and server. The external pgrx lane also builds pgGraph +`graph` and ParadeDB `pg_search` as opt-in extension candidates, and an +env-gated external matrix now +proves install/load, extension-specific behavior, restart, physical backup, and +physical restore for both external modules across direct, broker, and server. +The pgGraph smoke builds and traverses a tiny graph, then verifies root-scoped +persisted mmap auto-load, exact search, and shortest path after reopen/restore. +The ParadeDB smoke creates a real BM25 index and exercises `@@@`, +`paradedb.all`, `pdb.score`, and tokenizer stopword behavior after +reopen/restore. +`NATIVE_EXTENSION_MANIFEST` now records, per extension: + +- PostgreSQL 18 support status; +- SQL/control asset class; +- shared module requirement; +- transitive data files; +- smoke SQL strategy; +- restart and backup/restore coverage status; +- direct-C-ABI/broker/server coverage status; +- mobile static-link status. + +The remaining extension release blocker is now real platform extension-object +builds and device evidence, not the low-level loader or generated registry +source. Runtime resources record `mobileStaticRegistryState`, +`mobileStaticRegistryPending`, `nativeModuleStems`, and +`mobileStaticRegistrySource`, and the runtime-resource generator has a +`--require-mobile-static-registry` release gate. Kotlin and React Native +Android split-resource builds now stay pending for module-backed extensions +instead of accepting a static-module declaration without generated registry +source; mobile-complete Android/iOS selected-extension artifacts must consume Rust +runtime-resource generator output that includes `static-registry/oliphaunt_static_registry.c`. +pgGraph `graph` and ParadeDB `pg_search` are represented as internal external +PG18 candidates, not release-ready first-party selections, so they cannot +silently enter mobile or desktop bundles. Generated runtime resources now +record exact selected extensions, dependency-expanded runtime manifests, +static-registry state, and per-extension size evidence. Module-backed +extensions still need generated, linked, and device-tested platform extension +objects with the expected static symbol names before iOS/Android release, and +signed dynamic desktop extension artifacts still need a real signature and +loader policy before they can be accepted. + +Extensions without official PostgreSQL 18 support should stay out of the first +native release lane. + +## Swift, Kotlin, And React Native DX + +The SDK direction is sound: + +- Swift now exposes actor/async APIs and a native-direct C ABI engine. It still + needs platform-native XCFramework/resource packaging for release. +- Kotlin now exposes suspend APIs, a Kotlin/Native native-direct C ABI engine, + and an Android JNI-backed native-direct engine behind the same common API. + Android hides JNI/threading and runtime materialization behind that SDK shape + rather than introducing a second product boundary. +- React Native should use New Architecture TurboModules and a typed TypeScript + API. The current TypeScript/TurboModule shape now includes `cancel()` and + wait-and-detach close. iOS delegates to `Oliphaunt`; Android delegates to the + Kotlin SDK. Both still need full app/device smoke coverage with packaged + `liboliphaunt` and real runtime artifacts. + +The React Native package now keeps Codegen for typed lifecycle/control calls +and requires a versioned New Architecture JSI direct-buffer transport for +protocol, backup, and restore bytes. That is the right performance stance for +this product: apps fail early if the JSI installer is missing instead of +silently accepting a serialized binary fallback. The remaining React Native gap is +full app/device smoke coverage with packaged `liboliphaunt` and real runtime +artifacts. + +The mobile SDKs are not production-complete until they package and load the +real runtime resources and pass device/simulator tests that open, query, cancel, +close, restart, and load selected extensions. + +## Testing Strategy + +PostgreSQL's own testing model should be the north star. The official +PostgreSQL 18 docs describe regression tests as a comprehensive SQL test suite +covering standard SQL and PostgreSQL extensions. PostgreSQL also documents TAP +tests for executable/client behavior and temporary test servers. + +liboliphaunt should adopt that shape: + +1. C ABI tests: + - covered by the C smoke: init/shutdown, first bootstrap, process-bound + reopen, protocol success, protocol error recovery, invalid init and + invalid exec/stream arguments, malformed frontend frame rejection and + recovery, large owned responses, stream callback success/failure, close + after error, active-query cancellation and recovery, capabilities, and + version. + +2. SQL regression: + - run a curated PostgreSQL regression subset through direct mode; + - run the same SQL through broker and server; + - compare against a native PostgreSQL control where output is stable; + - classify expected differences explicitly. + +3. Client/server compatibility: + - `psql`; + - `pg_dump` and restore; + - `sqlx`; + - `tokio-postgres`; + - connection pools; + - concurrent sessions in server mode. + +4. Concurrency: + - many async Rust tasks sharing one direct handle; + - fair queueing; + - transaction pinning; + - cancellation and close during active SDK-owned work; + - queued work rejection once close begins; + - broker crash/reconnect. + + Broker crash/reconnect now has an env-gated native smoke that kills the + helper, waits for PostgreSQL crash recovery on relaunch, and reads data + through the same Rust handle. + +5. Extensions: + - absent extension fails; + - selected extension succeeds; + - unselected extension fails; + - each PG18-supported extension has direct/broker/server/restart/dump smoke. + +6. Mobile and RN: + - Swift XCTest on macOS and iOS simulator/device; + - Kotlin/JNI Android instrumentation; + - React Native iOS/Android sample with Codegen and nonblocking JS thread; + - large payload test using the future direct-buffer transport. + +## Benchmark Strategy + +The product gate should be native PostgreSQL parity. The native path calls the +same database engine and should not add material latency, CPU, or memory +overhead beyond its chosen embedding mode. + +Required benchmark dimensions: + +- simple-query RTT p50/p90/p95/p99; +- extended protocol prepare/bind/execute; +- typed query overhead; +- transaction throughput; +- batched insert/update/delete; +- indexed update workloads; +- COPY in/out; +- large result streaming; +- cold open and warm open; +- close/reopen across process; +- backup/restore; +- RSS, peak footprint, and CPU seconds; +- artifact size and resource bundle size; +- native PostgreSQL control; +- SQLite control; +- native direct/broker/server modes. + +The matrix script now measures the three native SDK modes separately: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh +``` + +For fast local checks, run: + +```sh +target/debug/oliphaunt-perf native-liboliphaunt --engine direct --suite rtt --iterations 10 +target/debug/oliphaunt-perf native-liboliphaunt --engine broker --suite rtt --iterations 10 +target/debug/oliphaunt-perf native-liboliphaunt --engine server --suite rtt --iterations 10 +``` + +Release claims should come only from serial matrix runs on an idle machine, with +the run directory and exact binary versions retained. The native matrix writes +`provenance.json`; verify retained evidence with: + +```sh +OLIPHAUNT_PERF_RUN_DIR="$PWD/target/perf/native-liboliphaunt-" \ +tools/perf/check-native-perf-report.sh +``` + +## Code Organization Review + +Good boundaries: + +- `oliphaunt/` owns the native C and PostgreSQL patch stack. +- `src/sdks/rust/` owns the Rust SDK. +- `src/bindings/wasix-rust/crates/oliphaunt-wasix/` remains WASIX-focused. +- `src/sdks/swift`, `src/sdks/kotlin`, and `src/sdks/react-native` own language/platform packages. +- `tools/` owns repo automation. + +Files to split next: + +- `src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c`: backup/restore archive handling has been + split into `src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c`; bootstrap, runtime-tool + discovery, process-global instance guarding, protocol tracing, filesystem + helpers, ustar archive read/write, raw protocol execution, streaming + backpressure, readiness scanning, embedded backend read/write callbacks, and + backend argv/default-GUC construction now live in dedicated C translation + units behind `src/runtimes/liboliphaunt/native/src/liboliphaunt_internal.h`. The remaining native + lifecycle file is small enough to keep focused on backend ownership and + public non-query ABI orchestration. +- `src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c`: physical archive lifecycle is now + separated from tar mechanics. If the archive format grows beyond same-version + physical tar, introduce a format dispatcher instead of adding branches to + the tar module. +- `tools/xtask/src/main.rs`: extension cataloging, process RSS sampling, and + perf command orchestration now live in dedicated modules. The remaining split + is asset/release orchestration; benchmark result/report models can move again + if `perf.rs` keeps growing. + +## Release Blockers + +Native direct should not be the default until these are true: + +- no native direct benchmark gate regresses beyond the accepted tolerance + against native PostgreSQL control; +- direct/broker/server each have real performance rows, including + prepared-update rows measured beside the native PostgreSQL prepared-update + control; +- selected extensions pass direct/broker/server tests; +- large response streaming is native, benchmarked, and covered for direct mode; +- direct, broker, and server streaming are benchmarked against native + PostgreSQL controls; +- lifecycle policy is documented and enforced; +- restore/import coverage includes direct, broker, and server backup/restore + smokes with selected extensions; +- mobile packaging can open/query/restart with selected extensions; +- React Native has a direct-buffer path for large results. The TurboModule + Codegen surface is lifecycle/control-only; protocol, backup, and restore + bytes require the versioned JSI `ArrayBuffer` transport, and validation + rejects base64 or Codegen binary regressions. + +## References + +- PostgreSQL 18 regression testing documentation: + https://www.postgresql.org/docs/current/regress.html +- PostgreSQL TAP testing documentation: + https://www.postgresql.org/docs/current/regress-tap.html +- React Native Turbo Native Modules: + https://reactnative.dev/docs/turbo-native-modules-introduction +- React Native Codegen type appendix: + https://reactnative.dev/docs/appendix diff --git a/docs/internal/PERFORMANCE.md b/docs/internal/PERFORMANCE.md new file mode 100644 index 00000000..f5ef7e15 --- /dev/null +++ b/docs/internal/PERFORMANCE.md @@ -0,0 +1,325 @@ +# Performance Internals + +This page is maintainer documentation for performance tuning, measurement +harnesses, and release profiling. Public benchmark results now live in +[`src/docs/content/reference/performance.mdx`](../../src/docs/content/reference/performance.mdx). + +`oliphaunt-wasix` is optimized for test setup and local-app startup. The runtime +avoids user-side compilation: supported targets load packaged Wasmer AOT +artifacts and reuse cached runtime files. + +## Fast Startup Practices + +For test suites: + +- use `Oliphaunt::temporary()` or `OliphauntServer::temporary_tcp()`; +- reuse the process when possible so the template and module caches stay warm; +- keep Postgres client pools at one connection; +- call `Oliphaunt::preload()` once before a visible UI path or a large test group; +- call `Oliphaunt::preload_extensions([...])` when extension setup is on the hot + path. + +Example: + +```rust,no_run +use oliphaunt_wasix::{extensions, Oliphaunt}; + +fn main() -> Result<(), Box> { + Oliphaunt::preload_extensions([extensions::VECTOR])?; + + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + db.exec("CREATE TABLE items (embedding vector(3))", None)?; + Ok(()) +} +``` + +## Cache Layers + +The runtime uses several cache layers: + +- a process cache for loaded modules; +- a persistent AOT artifact cache; +- a runtime asset cache for immutable files; +- an extension asset cache; +- a template PGDATA cache for roots that use template initialization; +- an eager PGDATA template overlay that avoids cloning the whole initialized + template before first query. + +The older full-local path hardlinks immutable files into database roots when +the filesystem supports it, then falls back to copying when linking is +unavailable. The default path avoids that per-root immutable-file population. + +## Default Filesystem Fast Path + +By default, database roots use Wasmer filesystem composition. Fresh roots use a +pure mount-composition layout: immutable runtime files are served from the +shared cached lower runtime, while the per-root upper layer contains only +mutable state, device/tmp files, and the extension assets explicitly requested +for that root. The same prepared layout is used by direct databases, persistent +paths, app-id paths, fresh and cached temporary databases, proxy roots, and +local server mode. + +The eager PGDATA template overlay is also enabled by default. It mounts the +cached initialized template as the lower `/base` filesystem and starts each +database with a tiny per-instance upper directory. When PostgreSQL opens a +template-backed file for mutation, the runtime copies that one file into the +upper directory before opening it. + +This is intentionally not a pre-provisioned pool: each database root is still +created on demand and owns its mutable files. In local release runs, runtime +composition is now about 0.6-0.9ms, down from roughly 7ms for the previous +per-root asset population. PGDATA setup is under 1ms. The remaining direct +first-query costs are mostly PostgreSQL backend startup, Wasmer instance +creation, and the protocol roundtrip for the query itself. + +Direct `Oliphaunt::open` no longer runs a separate session-setup query. Direct +session defaults are applied during startup before connection data is sent, not +through SQL. The regenerated WASIX runtime owns this as a required +`pgl_apply_default_gucs` bridge helper. + +Direct `Oliphaunt` no longer forces a host directory `sync_all` after every +non-transaction query. PostgreSQL's own WAL/fsync behavior owns durability; the +extra Rust-side directory sync was expensive and weaker than file-level database +fsyncs. This matches the server path, which did not pay that cost. + +Direct `Oliphaunt` also no longer scans `pg_type` on scalar open/query paths. +Built-in PostgreSQL array OIDs are registered statically in the Rust direct +client. Runtime-created enum/domain/composite arrays are discovered lazily when +they appear in direct API parameters or result metadata, or explicitly through +`Oliphaunt::refresh_array_types()`. + +The WASIX startup arguments explicitly preserve Oliphaunt's effective buffer +profile: `shared_buffers=128MB`, `wal_buffers=4MB`, and `min_wal_size=80MB`. +This matters for Oliphaunt benchmark parity. Without those GUCs, single-user +startup fell back to a tiny `shared_buffers=400kB`, causing table-copy and +indexed-update workloads to reread relation pages from the host filesystem. + +Native `oliphaunt` exposes the same throughput profile explicitly through +`RuntimeFootprintProfile::Throughput`, then adds mobile profiles for benchmark +matrices that need lower resident memory: `BalancedMobile` reduces hidden +server slot counts, sets `shared_buffers=32MB`, shrinks WAL targets to the +smallest valid default for the current 16MB WAL-segment build +(`min_wal_size=32MB`), caps mobile WAL growth at `max_wal_size=64MB`, and +forces PG18 sync I/O; `SmallMobile` reduces shared buffers to `8MB` and further +shrinks work memory. The mobile matrix also sweeps explicit `max_wal_size` +values (`32MB`, `64MB`, and default) so WAL footprint wins are attributable to +bounded checkpoint behavior rather than hidden defaults. Explicit startup GUC +overrides are appended after the profile and durability settings so benchmark +reports can attribute wins or regressions to concrete PostgreSQL knobs. +Experiments below `min_wal_size=32MB` require a template cluster initialized +with a smaller WAL segment size, such as `initdb --wal-segsize=4`; this is a +PGDATA/template property, not a startup GUC. The Expo mobile footprint harness +passes the requested segment size through to template generation and records the +effective read-only `wal_segment_size` setting next to the intended GUCs. + +Detailed C-side backend startup timers are an instrumented-build diagnostic, not +production runtime surface. Build WASIX assets with +`OLIPHAUNT_WASM_WASIX_BACKEND_TIMING=1` when investigating `shared_memory`, +`InitPostgres`, or relcache work. Production WASIX artifacts leave that flag off, +so timing macros compile away and the `pgl_backend_timing_elapsed_us` export is +absent. + +Nested child mounts inside a supplied `WasiFsRoot` were tested first. They were +rejected because Wasmer's `WasiRunner::prepare_webc_env` rebuilds the final +mount tree from the supplied `/` filesystem plus runner-owned mounts, so child +mounts must be passed as runner mounts or represented inside the root +filesystem itself. + +## Release Asset Profile + +The default asset release profile is `release`: WASIX C modules are compiled +with `-O2 -g0`, then Binaryen runs with the wasixcc default optimization level +plus `--converge`, `--strip-debug`, and `--strip-producers`. This is the current +PG18 SQL-workload profile: local parity runs kept the O2 lane strict green, +while `release-o3`/ThinLTO was mixed and did not justify becoming the default. + +Available profile knobs: + +- `OLIPHAUNT_WASM_BUILD_PROFILE=release` is the default release asset profile; +- `release`, `release-o3`, `release-os`, and `release-oz` remain available for + comparison builds. `release-o3` includes ThinLTO by default; +- set `OLIPHAUNT_WASM_WASM_OPT_FLAGS=none` to disable the release-profile + Binaryen converge/strip extras for local build iteration; +- set `OLIPHAUNT_WASM_WASM_OPT_FLAGS=''` to override the + release-profile Binaryen extras. + +The WASIX toolchain already enables the relevant Wasm feature baseline for this +EH+PIC sysroot, including SIMD, relaxed SIMD, and extended const. Adding an +extra `-msimd128` did not change the generated AOT artifact sizes in the local +release experiment, so it is not carried as a project-specific flag. + +Wasmer LLVM AOT is generated with the selected mainline codegen profile: +nonvolatile memory operations and a readonly funcref table. Local exact Oliphaunt +speed-suite measurements showed nonvolatile memory operations improving the +server SQLx suite by about 9% geomean. Adding the readonly funcref table on top +was about 1.4% faster geomean than nonvolatile-only and improved the indexed +update cases (`557.152ms -> 534.737ms` and `695.663ms -> 681.778ms`), while +regressing CREATE INDEX and DROP TABLE cases. Wasmer documents nonvolatile +memory operations as faster but not fully WebAssembly-spec compliant; this is a +conscious mainline runtime-profile decision for the packaged single-process +Postgres runtime and must stay covered by the correctness matrix. + +WebAssembly exceptions are mandatory for production artifacts. The Postgres +runtime depends on exception/longjmp recovery across the main module and side +modules, so there is no supported non-EH fallback and no opt-out flag. Asyncify +is not part of production builds; it may only be used in an isolated +snapshot/journaling experiment if a specific restore design proves it needs +that control-flow model. The build scripts reject Asyncify flags by default; +`OLIPHAUNT_WASM_ALLOW_ASYNCIFY_EXPERIMENT=1` is reserved for local experiment +branches only. + +WASIX dynamic linking is also mandatory. The main module is built as a +dynamic-main module, extension/tool modules are PIC side modules, and all +runtime, extension, and `pg_dump` artifacts must come from the same configured +source tree. + +## Native Deserialization + +The runtime loads Wasmer AOT artifacts through Wasmer's native mmapped-file +deserializer. This keeps the startup path off the old read-the-whole-native- +artifact path and does not reintroduce full artifact hashing. There is no +runtime opt-out for the older file deserializer. + +## Strict Verification + +By default, startup avoids content-hashing bundled assets. Cached Wasmer AOT +artifacts use fast receipt verification: the runtime checks the cache receipt +and file metadata, then lets Wasmer deserialize the cached native artifact. If +deserialization fails, the cache entry is deleted, rebuilt once from the bundled +artifact, and retried. + +Set `OLIPHAUNT_WASM_AOT_VERIFY=full` to force full SHA-256 verification of cached +AOT files, bundled runtime archives, bundled extension archives, PGDATA template +archives, and runtime/template module matches. This is useful for debugging +cache corruption or CI integrity checks, but it adds cold-start latency and is +not the default. + +## Snapshot And Journal Work + +Wasmer 7.2 exposes WASIX journal/process snapshot APIs, and `StoreSnapshot` +captures store globals. That is not enough by itself to ship an instant restore +path for Postgres: a promoted design must prove correctness for PGDATA state, +mount state, file descriptors, direct protocol state, server mode, extensions, +and `pg_dump`. This remains a first-class performance track, but it must beat +the current template/overlay path while passing the same runtime and extension +suite before it becomes default. + +## Measuring Locally + +The smoke benchmark prints preload and open timings: + +```sh +cargo test --test performance_smoke -- --nocapture +``` + +To measure the current cold-start path: + +```sh +cargo run -p oliphaunt-perf -- cold +``` + +This runs operations sequentially in one process. Each operation reports +`cacheStateBefore`, `processStateBefore`, `rootState`, `queryState`, and +`workload`, so a "first query" is explicitly the first query for that +operation's newly opened root/server, not necessarily a cold cache or cold +process. Each operation also reports `primaryLatencyPhase` and +`primaryLatencyMicros`; this is the user-visible latency target for that +operation and excludes cleanup/teardown where appropriate. + +To include first-install cache bootstrap costs in the first measured preload: + +```sh +cargo run -p oliphaunt-perf -- cold --reset-cache +``` + +To measure true warm behavior after startup, use the warm harness: + +```sh +cargo run -p oliphaunt-perf -- warm +``` + +It keeps databases/servers alive and measures repeated direct queries, +transactions, SQLx/tokio-postgres queries, repeated SQLx connections, and +extension-backed queries separately from open and shutdown phases. Use +`--iterations N` and `--connections N` for shorter local probes. + +To run product-style SQL benchmarks similar to Oliphaunt's published benchmark +families: + +```sh +cargo run --release -p oliphaunt-perf -- bench +``` + +This emits JSON with two benchmark suites: + +- `rtt`: Oliphaunt-style CRUD round-trip microbenchmarks. Each query runs many + times, the lowest and highest 10% are discarded when enough samples exist, + and the trimmed average is reported. +- `speed`: a generated SQLite speedtest-style SQL suite with large insert, + select, update, index, delete, and drop workloads. + +The RTT suite can run through the direct Rust API, through `OliphauntServer` with a +single long-lived SQLx connection, and through `OliphauntServer` with a raw +`tokio-postgres` simple-query-protocol connection. The raw `tokio-postgres` +mode is there to separate proxy/wire overhead from SQLx client overhead: + +```sh +cargo run --release -p oliphaunt-perf -- bench --suite rtt --mode server-sqlx +cargo run --release -p oliphaunt-perf -- bench --suite rtt --mode server-tokio-postgres-simple +cargo run --release -p oliphaunt-perf -- bench --suite speed --mode direct --scale 0.05 +cargo run --release -p oliphaunt-perf -- bench --suite speed --speed-source oliphaunt +``` + +The speed suite is generated locally instead of vendoring Oliphaunt's generated +multi-megabyte SQL files. Use `--scale` for quick local probes and `--scale 1` +for the full default shape. Use `--speed-source oliphaunt` when you need exact +parity with the SQL files checked out under +`target/oliphaunt-sources/checkouts/oliphaunt/packages/benchmark/src`; this mode requires +`--scale 1`. + +To compare simple-query indexed updates against parameterized prepared updates +and client pipelining: + +```sh +cargo run --release -p oliphaunt-perf -- prepared-updates +cargo run --release -p oliphaunt-perf -- prepared-updates --skip-native +cargo run --release -p oliphaunt-perf -- prepared-updates --skip-native --gate +``` + +This parses the exact update values from Oliphaunt benchmark Tests 9 and 10, uses +the same indexed-table setup, and measures SQLx sequential prepared execution, +tokio-postgres sequential prepared execution, tokio-postgres pipelined prepared +execution over TCP and Unix sockets, and the same tokio-postgres modes against +native Postgres. Use `--skip-native` when local native Postgres IPC state is not +healthy or when only OliphauntServer modes are needed. This is a server/protocol +benchmark; it does not replace the exact Oliphaunt simple-query suite. + +`--gate` is a local regression smoke gate, not a final CI performance oracle. +It checks the transport shape that caused the COPY/prepared-update regression: +non-COPY prepared traffic must not activate the backend-owned streaming +continuation, pipelined prepared traffic must remain batched, SQLx and +sequential tokio-postgres must stay below 5s per 25k updates, and pipelined +tokio-postgres must stay below 1.5s per 25k updates. The command emits per-run +protocol counters so failures show whether the problem is batching, protocol +pump activation, or backend execution. + +For focused investigation of indexed update hotspots, run: + +```sh +cargo run --release -p oliphaunt-perf -- diagnose-indexed-update +cargo run --release -p oliphaunt-perf -- diagnose-buffer-cache +``` + +This opens fresh temporary databases, runs setup outside the measured section, +then compares exact Oliphaunt Test 9/10 SQL against controlled variants: lookup +index only, unlogged table, text update after numeric update, vacuumed variants, +and one set-based update. The buffer-cache diagnostic runs +`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` for the remaining table-copy hotspots +and reports the effective Postgres memory GUCs plus host filesystem trace data. + +Treat these numbers as machine-local diagnostics. CI performance gates and +release targets depend on the runner, host filesystem, and cache state. diff --git a/docs/internal/PG18_WASIX_PERF_STATUS.md b/docs/internal/PG18_WASIX_PERF_STATUS.md new file mode 100644 index 00000000..d08ca146 --- /dev/null +++ b/docs/internal/PG18_WASIX_PERF_STATUS.md @@ -0,0 +1,1010 @@ +# PG18 WASIX Performance Status + +Date: 2026-05-29 + +## 2026-05-29 Recheck + +The active PG18 WASIX runtime is now the 37-patch Oliphaunt-style +implementation stack. The apparent `1.26x`-`1.28x` regression came from +running the perf harness through a dev `cargo run` host. The comparable +product path is `cargo run --release -p xtask ...`; with that release host, the +regenerated 37-patch PG18 artifact is faster than same-host PG17.5 and the +documented PG17.5 release table on every speed-test head. + +Upstream `postgres/postgres` was checked against real PostgreSQL +base tags by fetching `REL_17_5` and `REL_18_3` from `postgres/postgres` into +the local Oliphaunt checkouts. `PG17 legacy lane` changes 54 files versus +PostgreSQL `REL_17_5`; `PG18 legacy lane` changes 55 files versus PostgreSQL +`REL_18_3`. The core runtime deltas are the same in both branches: +`build-oliphaunt.sh`, `oliphaunt/src/oliphauntc/oliphauntc.c`, `xlog.c`, +`checkpointer.c`, `postgres.c`, `postinit.c`, `guc.c`, plus storage/init +support files. The upstream PG18 branch does not contain hidden btree, hash, +LIKE, or executor speed patches beyond the Oliphaunt single-backend lifecycle +shape. + +Diagnostic timing/count patches `0035`-`0039` are not release implementation +patches and are not in the active series. `0039` was explicitly harmful +because it added live `XLogWrite()` counters even when backend timing was not +compiled in. A later release-candidate `0035` experiment that tried to coalesce +single-user `XLogWrite()` buffer writes was also rejected: the broadened +ring-contiguous version made the Test 11 diagnostic worse (`INSERT 2` about +`204 ms`, `COMMIT` about `38 ms`) than the 34-patch lane (`INSERT 2` about +`93 ms`, `COMMIT` about `14 ms`). + +Another rejected `0035` experiment tried to avoid the `xlblocks` readiness scan in +single-user `XLogWrite()` while preserving write grouping and LSN advancement. +It built, but regressed the focused Test 11 buffer/cache diagnostic +(`INSERT 1` `45.412 ms`, `INSERT 2` `144.774 ms`, `COMMIT` `21.959 ms`), so it +was removed and the active artifacts were restored to the 34-patch fingerprint +below. + +The accepted `0035` patch, +`0035-oliphaunt-wasix-avoid-xlogwrite-prevseg-division.patch`, keeps the +PostgreSQL `XLogWrite()` grouping and LSN advancement semantics but replaces the +per-WAL-page `XLByteInPrevSeg()` dynamic division in the hot scan loop with +cached open-segment byte bounds. This did not fix the isolated Test 11 COMMIT +diagnostic (`15.603 ms`, versus `13.993 ms` for the 34-patch baseline and +`0.711 ms` for same-host PG17.5), but it materially improved the full speed +suite. + +A release-o3/ThinLTO rebuild of the same 35-patch source was tested at +`target/perf/pg18-wasix-core-release-o3-35patch-prevseg-bounds-release-host-speed-server-sqlx.json`. +It improved Test 15 (`73.637 ms`, `0.989x` documented PG17.5) but regressed +overall (`1.104x` geomean versus the O2 35-patch median) and missed documented +PG17.5 on Tests 1, 3, 6, 10, 11, and 16. Keep the active perf artifact on the +O2 release profile. + +Current active artifact: + +- PostgreSQL version: `18.4` +- active patch count: `36` +- source fingerprint: + `18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:3cfc56f67ba63996e8efd7414464c269a3e65f3d4abbd3c65c7a4cf8e8b0d7c4` +- latest accepted perf patch: + `0036-oliphaunt-wasix-skip-activity-id-reporting.patch` +- verification: + `assets release-build --profile release + --skip-build --skip-package-size` passes source-spine, source-isolation, + canonical-layout, manifest-pin, and AOT packaging checks. +- current release-host result files: + `target/perf/pg18-wasix-core-release-o2-36patch-skip-activity-id-release-host-speed-server-sqlx.json`, + `target/perf/pg18-wasix-core-release-o2-36patch-skip-activity-id-release-host-speed-server-sqlx-rerun.json`, + `target/perf/pg18-wasix-core-release-o2-36patch-skip-activity-id-release-host-speed-server-sqlx-rerun2.json` +- release-host reruns can use the stable PG18 generated asset and AOT paths: + `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR=.../target/oliphaunt-wasix/assets`, + and `OLIPHAUNT_WASM_GENERATED_AOT_DIR=.../target/oliphaunt-wasix/aot`. + Asset, PGDATA-template, and AOT manifests carry source-fingerprint metadata, + so a stale artifact fails with a fingerprint mismatch before measurement. + +Current 37-patch O2 release-host three-run median against same-host PG17.5 +`0.5.0` and the documented PG17.5 release-lane table in +`src/docs/content/reference/performance.md`: + +| Test | PG18 37-patch O2 median ms | PG17.5 same-host median ms | PG18 / same-host | Documented PG17.5 ms | PG18 / documented | +| --- | ---: | ---: | ---: | ---: | ---: | +| 1 | 19.565 | 19.893 | 0.984 | 19.76 | 0.990 | +| 2 | 144.833 | 165.821 | 0.873 | 149.54 | 0.969 | +| 2.1 | 50.490 | 61.670 | 0.819 | 59.39 | 0.850 | +| 3 | 209.441 | 253.007 | 0.828 | 253.38 | 0.827 | +| 3.1 | 79.769 | 107.124 | 0.745 | 95.12 | 0.839 | +| 4 | 142.026 | 190.276 | 0.746 | 162.89 | 0.872 | +| 5 | 236.305 | 388.982 | 0.607 | 338.01 | 0.699 | +| 6 | 11.756 | 14.996 | 0.784 | 13.08 | 0.899 | +| 7 | 120.748 | 142.110 | 0.850 | 125.31 | 0.964 | +| 8 | 68.267 | 85.746 | 0.796 | 74.42 | 0.917 | +| 9 | 520.213 | 610.845 | 0.852 | 578.96 | 0.899 | +| 10 | 583.991 | 775.341 | 0.753 | 712.38 | 0.820 | +| 11 | 76.259 | 97.814 | 0.780 | 97.43 | 0.783 | +| 12 | 6.814 | 10.813 | 0.630 | 9.74 | 0.700 | +| 13 | 11.898 | 27.974 | 0.425 | 26.58 | 0.448 | +| 14 | 69.192 | 78.119 | 0.886 | 71.60 | 0.966 | +| 15 | 74.343 | 90.388 | 0.822 | 74.49 | 0.998 | +| 16 | 6.777 | 9.970 | 0.680 | 10.17 | 0.666 | + +Geomean: `0.759x` same-host PG17.5 and `0.826x` documented PG17.5. This is +per-head parity against both the same-host PG17.5 release-lane rerun and the +documented PG17.5 release table. The previous 35-patch O2 median was already +green against same-host PG17.5 but missed the documented table on Test 15 +(`75.954 ms` versus `74.49 ms`). The accepted 36th patch removes unused +pg_stat_activity query/plan ID reporting from the embedded WASIX runtime, +bringing Test 15 to `74.343 ms` while keeping every other head under both +baselines. + +The current Test 15 diagnostic is at +`target/perf/pg18-wasix-core-release-o2-36patch-skip-activity-id-diagnose-speed-15.json`. +It records `74.108 ms` total for the selected benchmark buffer, with +`73.530 ms` inside `postgres.protocol.dispatch_buffer`, `253 us` in +`postgres.protocol.input_write`, `30 us` in output read, and `0 us` in +`client.finish.sync_to_fs`. The same diagnostic on the old 31-patch stack was +`104.165 ms` with `103.748 ms` in dispatch, and the 35-patch release candidate +was `76.756 ms` with `76.271 ms` in dispatch. + +The current branch also cannot directly rerun the released PG17.5 AOT: the Rust +loader now expects the newer PG18 protocol-buffer export +`oliphaunt_wasix_input_reset`, while the released PG17.5 module does not export +`_oliphaunt_wasix_input_reset`. Therefore the preserved `0.5.0` same-host JSON +files remain the PG17.5 comparison baseline unless a compatibility runner is +restored. + +Implementation comparison status: + +- The PG18 37-patch lane has the important Oliphaunt-style PG18.3 lifecycle hints + from upstream `PG18 legacy lane` commit + `cf82a9936be24e6b4203855b34d77a49c83ba2bd`: postmaster-environment flags, + XLog checkpoint-request guard, local in-process checkpoint behavior, GUC + report allocation skip, and the POSIX semaphore reset fast path. The + LIKE/hash/btree/top-XID fast paths are not upstream Oliphaunt deltas; they were + ported from the concurrent WASIX experiment and are already in this PG18 + lane. +- `XLogFlush()` is not materially different between PG17.5 Oliphaunt and PG18.3 + Oliphaunt. The earlier isolated COMMIT diagnostic gap pointed at PG18 + `XLogWrite()`/WAL-buffer scan behavior or the current Oliphaunt WASIX runtime + state around it, but that sub-step no longer blocks speed-suite parity. +- Strict speed-suite parity is now met on the current three-run median. Future + work should focus on validating the activity-ID reporting tradeoff against + any pg_stat_activity/query-id tests we decide to support in the embedded + embedded product, plus broader smoke coverage for extensions and pg_dump. + +## Current Status + +Superseded by the `2026-05-29 Recheck` section above. This section is retained +as historical context for the earlier 31-patch and pre-release-host +measurements; do not use its fingerprint or result files as the active PG18 lane +status. + +The PG18 WASIX runtime builds and runs the product-style +`OliphauntServer` + SQLx speed suite as a core-only perf probe. It is not yet a +replacement candidate for the released PG17.5 WASIX lane. + +Current repeated PG18 probe: + +- PostgreSQL version: `18.4` +- source fingerprint: + `18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:56d87b5a7e76fca055e49574e8c09df1e363905a2d60d0e81853a679824c192a` +- build profile: `release` (`-O2 -g0`) +- scope: core-only, extensions and `pg_dump` skipped through + `OLIPHAUNT_WASM_SKIP_EXTENSIONS_FOR_PERF=1` +- result files: + `target/perf/pg18-wasix-core-release-o2-31patch-speed-server-sqlx.json`, + `target/perf/pg18-wasix-core-release-o2-31patch-speed-server-sqlx-rerun.json`, + and + `target/perf/pg18-wasix-core-release-o2-31patch-speed-server-sqlx-rerun2.json` + +The current fair comparison is the product-style `OliphauntServer` + SQLx speed +suite against the documented PG17.5 release-lane table in `src/docs/content/reference/performance.md`. +Across three local PG18 runs, the median geomean is `1.063x` PG17.5, not the +older `1.4-1.5x` estimate from stale single-run files. The first run after AOT +crate rebuild was a cold outlier (`1.410x` geomean); the next two runs were +`0.983x` and `1.046x`. + +Median PG18 server-SQLx misses vs documented PG17.5 release lane: + +- Test 15, big DELETE plus 12k small INSERTs: `95.92 ms` vs `74.49 ms` + (`1.29x`). +- Test 1, 1000 INSERTs: `24.40 ms` vs `19.76 ms` (`1.23x`). +- Test 14, big INSERT after big DELETE: `87.57 ms` vs `71.60 ms` (`1.22x`). +- Test 11, INSERTs from SELECT: `119.07 ms` vs `97.43 ms` (`1.22x`). +- Test 10, 25k text indexed UPDATEs: `862.53 ms` vs `712.38 ms` (`1.21x`). +- Test 7, 5000 indexed SELECTs: `146.06 ms` vs `125.31 ms` (`1.17x`). + +The current lane is therefore near parity, but not equal across all heads. The +remaining repeatable gap is simple-query per-statement overhead, especially +Test 15's `BEGIN; DELETE FROM t1;` followed by 12k one-row `INSERT` statements. +Focused Test 15 diagnostic +`target/perf/pg18-wasix-core-release-o2-31patch-speed-diagnose-15.json` +recorded `104.17 ms` (`1.40x` PG17.5), all inside backend protocol dispatch. + +A same-host PG17.5 release-lane rerun was attempted from the public `0.5.0` +release artifacts. Those artifacts cannot be run through the current PG18 +branch loader because the released PG17.5 module uses the old `oliphaunt` archive +names and does not export the newer `oliphaunt_wasix_input_reset` +protocol-buffer entrypoint. Running the `0.5.0` code in a detached worktree +with the public `0.5.0` assets does work, and gives a same-host baseline: + +- PG17.5 same-host server-SQLx files: + `target/perf/oliphaunt17-0.5.0-samehost-speed-server-sqlx.json` and + `target/perf/oliphaunt17-0.5.0-samehost-speed-server-sqlx-rerun.json`. +- PG17.5 same-host median is `1.088x` the documented release table geomean. +- PG18 median is `0.977x` the same-host PG17.5 median geomean. + +The same-host comparison means PG18 is at overall parity with the released +PG17.5 lane on this machine. The remaining work is per-head parity, not broad +throughput parity. PG18 still trails same-host PG17.5 most on: + +- Test 1, 1000 INSERTs: `24.40 ms` vs PG17.5 `19.89 ms` (`1.23x`). +- Test 11, INSERTs from SELECT: `119.07 ms` vs PG17.5 `97.81 ms` (`1.22x`). +- Test 14, big INSERT after big DELETE: `87.57 ms` vs PG17.5 `78.12 ms` + (`1.12x`). +- Test 10, 25k text indexed UPDATEs: `862.53 ms` vs PG17.5 `775.34 ms` + (`1.11x`). + +## Upstream PG18.3 Oliphaunt Branch Hints + +Checked upstream `postgres/postgres` branch `PG18 legacy lane` at +commit `cf82a9936be24e6b4203855b34d77a49c83ba2bd` on 2026-05-29. + +Superseded by the `2026-05-29 Recheck` implementation-comparison bullets above. +The active 37-patch PG18 lane now carries the relevant upstream Oliphaunt +lifecycle/runtime choices under Oliphaunt-owned markers. The historical detail +below records what was found before those patches were incorporated. + +The branch has six relevant PG18.3 choices: + +- `src/backend/tcop/postgres.c`: `oliphaunt_wasix_start()` sets + `IsPostmasterEnvironment = true` and `IsUnderPostmaster = true`. The PG18 + Oliphaunt lane now ports those flags in `oliphaunt_wasix_start()` as patch + `0034`. +- `src/backend/access/transam/xlog.c`: upstream Oliphaunt wraps the + `XLogCheckpointNeeded(openLogSegNo) -> RequestCheckpoint(CHECKPOINT_CAUSE_XLOG)` + path in `#ifndef __OLIPHAUNT__`, preventing automatic XLog-size checkpoint + requests in the Oliphaunt build. +- `src/backend/postmaster/checkpointer.c`: upstream Oliphaunt wraps the + `if (!IsPostmasterEnvironment)` guard in `#ifndef __OLIPHAUNT__`, so + `RequestCheckpoint()` runs the local `CreateCheckPoint(... | + CHECKPOINT_IMMEDIATE)` body even after Oliphaunt marks itself as a postmaster + environment. +- `src/backend/port/posix_sema.c`: upstream Oliphaunt changes + `PGSemaphoreReset()` to a single `sem_trywait()` under `__OLIPHAUNT__`. +- `src/backend/utils/misc/guc.c`: upstream Oliphaunt skips the + `guc_strdup(record->last_reported)` copy in `ReportGUCOption()`. +- `build-oliphaunt.sh`: upstream PG18.3 still uses `--disable-spinlocks`. + +The old PG17.5 release lane already guarded for the upstream Oliphaunt +checkpointer shape in its source-spine checks (`stable-checkpointer-disable`, +`stable-external-checkpointer`, and `stable-postmaster-environment`). The PG18 +embedded WASIX runtime intentionally renamed the markers to `OLIPHAUNT_WASM_*` and +currently bans direct `__OLIPHAUNT__` inheritance, so these should be ported as +Oliphaunt-specific patches if we adopt them. + +This upstream comparison lines up with the focused Test 11 evidence. In the +same-host focused buffer/cache diagnostic, PG18 and PG17.5 had near-identical +`INSERT INTO ... SELECT` execution times, but PG18 spent `13.993 ms` in +`COMMIT` while PG17.5 spent only `0.711 ms`. The most plausible upstream hint +for that miss is not btree/executor work; it is the XLog/checkpoint behavior +around transaction end. + +Later WAL instrumentation narrows this further. Timing patch `0038` showed the +PG18 COMMIT miss is not raw `pg_pwrite`, WALWriteLock, pgstat accounting, fsync, +or walsender wakeup. Almost all time is in `XLogWrite()`'s per-page loop: + +- `COMMIT`: `13.912 ms` +- `commit_xlog_flush`: `13.824 ms` +- `xlog_flush_xlog_write`: `13.821 ms` +- `xlog_write_loop`: `13.819 ms` +- `xlog_write_loop_scan`: `12.884 ms` +- `xlog_write_pwrite`: `0.819 ms` + +Counter patch `0039` shows why: on the Test 11 COMMIT, PG18 writes one full +`wal_buffers` ring worth of WAL: + +- `xlog_write_loop_count`: `512` +- `xlog_write_page_count`: `512` +- `xlog_write_group_count`: `2` +- `xlog_write_pwrite_count`: `2` +- `xlog_write_pwrite_bytes`: `4,194,304` +- `xlog_write_request_bytes`: `4,188,592` + +That means PG18 is doing 512 page-readiness checks and two actual writes at +COMMIT. + +A PG17.5 `0.5.0` same-host WAL-state rerun with the same diagnostic shape shows +that PG17.5 reaches COMMIT with essentially the same WAL state, but completes +the tail write much faster: + +| Statement | PG17.5 elapsed | PG17.5 insert/flush gap | PG18 elapsed | PG18 insert/flush gap | +| --- | ---: | ---: | ---: | ---: | +| `BEGIN` | `0.065 ms` | `0` | `0.022 ms` | `0` | +| first `INSERT INTO ... SELECT` | `102.184 ms` | `2,757,824` | `29.965 ms` | `2,757,824` | +| second `INSERT INTO ... SELECT` | `197.732 ms` | `9,277,984` | `94.253 ms` | `9,278,000` | +| `COMMIT` | `1.093 ms` | `0` | `13.890 ms` | `0` | + +The PG17.5 elapsed values in this WAL-state rerun were produced from a local +diagnostic build of the `0.5.0` xtask, so they should not replace the documented +release-lane timing table. The WAL LSN state is still decisive: PG18 is not slow +because it uniquely defers more WAL to COMMIT. It is slow because the same +already-open WAL tail write path costs much more in the current PG18 WASIX +artifact. + +A direct single-user `XLogWrite()` fast-path experiment was tested and rejected. +The first version skipped the normal segment initialization path and failed +during buffer-cache setup case 10 with `WASI exited with code: ExitCode::127`. +Changing the fast path to use `XLogFileInit()` fixed the crash, but made Test 11 +COMMIT slower: `23.197 ms` vs the existing PG18 `13.993 ms` and PG17.5 +`0.711 ms`. The experiment was removed from the active patch series. The +saved failed-result file is +`target/perf/pg18-wasix-core-release-o2-40patch-xlog-fastpath-init-buffer-cache.json`. + +An even narrower replacement experiment, +`0040-oliphaunt-wasix-fast-path-open-segment-xlog-write.patch`, was also tested +and rejected. It only ran when the WAL segment was already open and the write +stayed inside that segment. The focused diagnostic result is saved at +`target/perf/pg18-wasix-core-release-o2-40patch-open-segment-xlog-fastpath-buffer-cache.json`. +It reduced Test 11 `COMMIT` only from `13.890 ms` to `11.890 ms`, while +regressing the two `INSERT INTO ... SELECT` statements from `29.965/94.253 ms` +to `119.319/209.435 ms`. It also produced a WAL state after `BEGIN` where +write/flush LSN was ahead of insert LSN by `2216` bytes, so the approach is not +correct enough to keep in the active patch series. The active lane is back to +39 patches. + +Patch status: + +- `0032-oliphaunt-wasix-avoid-xlog-size-checkpoint-requests.patch` now ports the + upstream XLog/checkpointer behavior under `OLIPHAUNT_WASM_SINGLE_USER`. + It intentionally does not set `IsPostmasterEnvironment` or + `IsUnderPostmaster` in `oliphaunt_wasix_start()`, so the startup-environment + change remains isolated for a later A/B test. +- `0033-oliphaunt-wasix-use-lightweight-embedded-runtime-paths.patch` now + ports upstream PG18.3 Oliphaunt's smaller `PGSemaphoreReset()` and + `ReportGUCOption()` shortcuts under `OLIPHAUNT_WASM_SINGLE_USER`. +- `0034-oliphaunt-wasix-set-embedded-postmaster-environment.patch` now matches + the released PG17.5 lane and upstream PG18.3 Oliphaunt by setting + `IsPostmasterEnvironment = true` and `IsUnderPostmaster = true` in + `oliphaunt_wasix_start()`. This is paired with `0032` so the lane does not + inherit the multi-process XLog/checkpointer path. +- The sibling PG18/native worktree's perf notes called out the release-lane + WASIX buffer profile as critical for Oliphaunt benchmark parity: + `shared_buffers=128MB`, `wal_buffers=4MB`, and `min_wal_size=80MB`. The PG18 + single-user Rust startup path already preserves those same defaults in + `DEFAULT_STARTUP_GUCS`, matching the PG17.5 release lane, so the current + remaining misses are not explained by falling back to PostgreSQL's tiny + standalone `shared_buffers` default. The source-spine guard now checks this + explicitly. + +Recommended next measurement order: + +1. Rerun the full server-SQLx median table from the restored 39-patch artifact + if we need a fresh all-head view after the 0040 rejection. The generated + runtime and AOT manifests are back on source fingerprint + `18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:56d87b5a7e76fca055e49574e8c09df1e363905a2d60d0e81853a679824c192a`. +2. Avoid direct XLogWrite fast paths that advance write/flush from rounded page + boundaries without preserving PostgreSQL's insert-LSN invariants. The next + WAL attempt should either reduce per-page metadata cost without changing LSN + advancement, or move unavoidable work earlier only if the full per-head + median table improves. +3. Keep `PGSemaphoreReset()` and `ReportGUCOption()` as secondary evidence + until measured. They are credible micro-optimizations, but the current + evidence points first at COMMIT/checkpoint behavior and many-small-statement + overhead. + +The previous `target/perf/pg18-wasix-core-release-o2-31patch-speed-diagnose-9-10.json` +file is stale for current status. Rerunning the same focused diagnostic in +`target/perf/pg18-wasix-core-release-o2-31patch-speed-diagnose-9-10-rerun.json` +gave: + +- Test 9, 25k indexed UPDATEs: `645.27 ms` vs documented PG17.5 + release-lane `578.96 ms` (`1.11x` slower). +- Test 10, 25k text indexed UPDATEs: `949.05 ms` vs documented PG17.5 + release-lane `712.38 ms` (`1.33x` slower). + +Additional controlled indexed-update diagnostic: +`target/perf/pg18-wasix-core-release-o2-31patch-diagnose-indexed-update.json` + +This synthetic run uses the PG18 AOT/runtime asset lane and fresh temporary +databases for each case. It is useful for isolating logged/indexed update +mechanics, but it is not the fair release-lane speed benchmark shape because it +does not run all earlier benchmark cases before measuring Test 9/10: + +- exact numeric indexed update: `1128.52 ms`, `1.95x` the documented PG17.5 + Test 9 number (`578.96 ms`). +- exact text indexed update: `1500.82 ms`, `2.11x` the documented PG17.5 Test + 10 number (`712.38 ms`). +- lookup-index-only numeric update: `964.99 ms`, `1.67x` PG17.5 Test 9. +- lookup-index-only text update: `995.51 ms`, `1.40x` PG17.5 Test 10. +- unlogged numeric update: `1196.71 ms`, not better than logged. +- unlogged text update: `1160.32 ms`, better than the exact logged text case + but still `1.63x` PG17.5 Test 10. + +This rules out WAL/fsync as the dominant explanation for the synthetic cold +indexed-update miss. Removing the updated-column index helps, but only +partially; the remaining cost is still in repeated statement execution plus +tuple/index maintenance. + +Upstream PG18.3 Oliphaunt keeps using `--disable-spinlocks`, but this is no longer +a leading explanation for the PG18 Test 11 gap. The current PG18 WASIX +`pg_config.h` has no `HAVE_SPINLOCKS` define and links `src/backend/port/tas.s` +to `tas/dummy.s`; the lane is already on PostgreSQL's no-native-spinlock TAS +path. The `OLIPHAUNT_WASM_PG18_DISABLE_SPINLOCKS=1` knob remains wired, but the +configure script reports the option as unrecognized on this PG18 branch and the +observed build state is already spinlock-disabled. + +For historical context, the last full PG18 speed-table probe before the later +diagnostic patches was: +`target/perf/pg18-wasix-core-release-o2-24patch-speed-server-sqlx.json`. Against +the documented PG17.5 release-lane table in `src/docs/content/reference/performance.md`, that run is +`1.480x` geomean, or about `48.0%` slower. Against native Postgres + SQLx from +the same table, it is `2.270x` geomean, or about `127.0%` slower. + +In that older 24-patch run, the only wins over the documented PG17.5 release +lane were: + +- Test 5, string comparison SELECTs: `271.85 ms` vs `338.01 ms` +- Test 12, DELETE without index: `8.73 ms` vs `9.74 ms` +- Test 13, DELETE with index: `15.24 ms` vs `26.58 ms` + +The release-blocking misses are concentrated in ordinary insert/index/update +workloads: + +- Test 2, 25k INSERTs in a transaction: `317.42 ms` vs `149.54 ms` +- Test 3, 25k INSERTs into an indexed table: `463.47 ms` vs `253.38 ms` +- Test 7, 5000 indexed SELECTs: `316.48 ms` vs `125.31 ms` +- Test 9, 25k indexed UPDATEs: `1322.57 ms` vs `578.96 ms` +- Test 10, 25k text indexed UPDATEs: `1520.13 ms` vs `712.38 ms` +- Test 15, big DELETE plus 12k small INSERTs: `186.20 ms` vs `74.49 ms` + +## Backend Timing Probe + +A timing-enabled core-only artifact was built with +`OLIPHAUNT_WASM_WASIX_BACKEND_TIMING=1` and used only for diagnostics. Do not +treat that artifact as a release artifact. + +Diagnostic file: +`target/perf/pg18-wasix-core-release-o2-backend-timing-diagnose-speed-9-10.json` + +The probe measured fresh-database speed cases 9 and 10 through +`perf diagnose-speed-cases --engine wasix --ids 9,10`. It recorded backend C +timings around `exec_simple_query`, `PortalRun`, and `finish_xact_command`: + +- Test 9, indexed integer UPDATEs: `1422.63 ms` measured. `exec_simple_query` + was `1414.48 ms`; `PortalRun` was `593.69 ms`; `finish_xact_command` was only + `0.58 ms`. +- Test 10, indexed text UPDATEs: `1611.18 ms` measured. `exec_simple_query` + was `1599.00 ms`; `PortalRun` was `731.02 ms`; `finish_xact_command` was only + `0.41 ms`. + +This rules out transaction finish, fsync, or client/protocol transport as the +primary explanation for these two misses. The time is almost entirely inside +backend simple-query execution, but only about `42-45%` of elapsed time is +currently attributed to `PortalRun`. The remaining `54-58%` is still inside +`exec_simple_query` and needs finer probes around parse/rewrite/plan, command +counter, command completion, receiver teardown, and per-statement loop costs. + +Expanded diagnostic file: +`target/perf/pg18-wasix-core-release-o2-backend-timing-expanded-diagnose-speed-9-10.json` + +The expanded probe adds timing around simple-query parse, snapshot, +analyze/rewrite, plan, portal start, destination receiver setup, command +counter, and command completion. Its absolute elapsed times are not directly +comparable to release numbers because it adds many timing calls inside the 25k +statement loop, but the distribution is useful: + +- Test 9: `exec_plan` was `684.44 ms` (`35.0%`), `PortalRun` was `548.21 ms` + (`28.0%`), and `pg_analyze_and_rewrite_fixedparams` was `118.44 ms` + (`6.1%`). `finish_xact_command` was `0.62 ms`. +- Test 10: `exec_plan` was `719.02 ms` (`32.9%`), `PortalRun` was `707.39 ms` + (`32.4%`), and `pg_analyze_and_rewrite_fixedparams` was `135.94 ms` + (`6.2%`). `finish_xact_command` was `0.41 ms`. + +The indexed-update regression is therefore not just btree execution. Repeated +simple-query planning is as large as, or larger than, execution for these +cases. That makes the next high-signal experiment a protocol/query-path +comparison: run equivalent prepared extended-protocol updates against PG18 +WASIX and the documented PG17.5 release lane before adding more btree-specific +shortcuts. + +## Prepared Update Comparison + +Prepared-update diagnostics were run against the timing-enabled PG18 artifact +with source fingerprint +`18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:62572d0cf8eb828bf90085986da48f17bccfd06cca8444224f431e326b4f8983`. +The timing patch only instruments simple-query execution; the direct raw +extended-protocol measurement below does not run through those simple-query +probes during the measured update batch. + +SQLx prepared update file: +`target/perf/pg18-wasix-core-release-o2-prepared-updates-sqlx.json` + +SQLx prepared updates are not a useful rescue path in the current server shape: + +- numeric indexed update: `3790.06 ms` +- text indexed update: `3828.13 ms` +- protocol shape: `50000` Bind, `50000` Execute, and `50002` Sync messages + across the two cases, with `50010` protocol batches and `50084` socket + flushes. + +This is slower than simple-query speed tests 9 and 10 because SQLx issues a +Sync boundary per update in this harness. Avoiding planning does not help if +the protocol path adds 25k round-trip/flush boundaries. + +Direct raw pipelined prepared update file: +`target/perf/pg18-wasix-core-release-o2-prepared-updates-direct-raw-pipelined.json` + +The raw frontend/backend protocol path prepares one statement, sends all 25k +Bind/Execute/ClosePortal messages in one batch, then sends one Sync: + +- numeric indexed update: `1029.91 ms`, down from the PG18 simple-query + `1322.57 ms` (`22.1%` faster), but still `1.78x` the documented PG17.5 + release-lane simple-query time of `578.96 ms`. +- text indexed update: `1026.58 ms`, down from the PG18 simple-query + `1520.13 ms` (`32.5%` faster), but still `1.44x` the documented PG17.5 + release-lane simple-query time of `712.38 ms`. + +Conclusion: repeated simple-query planning and protocol shape explain a large +piece of the PG18 indexed-update regression, especially for text updates, but +they do not explain enough to make PG18 competitive with the released PG17.5 +lane. Even with planning mostly removed and the protocol batched, PG18 still +needs executor/storage/btree work before it can replace the release lane. + +The tokio-postgres prepared server path currently fails before measurement with +`invalid message length: expected buffer to be empty`. That should be tracked +as an extended-protocol compatibility bug, but it is not the primary perf +blocker because the direct raw protocol path can already run the prepared batch. + +## Storage/Executor Timing Probe + +Patch `0026` adds diagnostic-only timing around coarse executor/storage hot +paths: + +- `heapam_tuple_update` +- `_bt_doinsert` +- `XLogInsertRecord` + +Storage diagnostic file: +`target/perf/pg18-wasix-core-release-o2-backend-timing-storage-diagnose-speed-9-10.json` + +This diagnostic was run against the 26-patch timing-enabled artifact with +source fingerprint +`18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:4a71c8a39ce745890a54f69c9f55b069122729a6f08fe15159de07e2925a4b94`. +The absolute elapsed numbers are inflated by additional timing calls inside the +25k statement loop, but the distribution is useful: + +- Test 9: `exec_plan` was `689.95 ms`; `PortalRun` was `1106.64 ms`; + `_bt_doinsert` was `316.71 ms`; `heapam_tuple_update` was `254.55 ms`; + `XLogInsertRecord` was `184.12 ms`; `finish_xact_command` was `0.54 ms`. +- Test 10: `exec_plan` was `725.38 ms`; `PortalRun` was `1276.53 ms`; + `_bt_doinsert` was `446.81 ms`; `heapam_tuple_update` was `266.85 ms`; + `XLogInsertRecord` was `202.59 ms`; `finish_xact_command` was `0.42 ms`. + +The storage timings are nested/coarse and should not be summed as independent +costs. They do show that after separating planning/protocol overhead, btree +insertion is the largest named execution component for indexed updates, +especially for text updates. Heap update and WAL insertion are material but +smaller. The next probe should split `_bt_doinsert` into scan-key build, +unique check/search, insert-location search, page insertion/split, and WAL +record construction. + +## Btree Insert Timing Probe + +Patch `0027` splits the coarse `_bt_doinsert` probe into btree subphases: + +- scan-key construction +- root-to-leaf insert search +- uniqueness check +- insert-location search +- page insertion +- page split + +Btree diagnostic file: +`target/perf/pg18-wasix-core-release-o2-backend-timing-btree-diagnose-speed-9-10.json` + +This diagnostic was run against the 27-patch timing-enabled artifact. As with +the other timing builds, absolute elapsed time is inflated by instrumentation in +the 25k statement loop. The subphase distribution is still useful: + +- Test 9: `_bt_doinsert` was `862.00 ms`; scan-key build was `71.30 ms`; + insert search was `87.28 ms`; insert-location search was `96.72 ms`; + page insertion was `260.63 ms`; page split was only `14.12 ms`. +- Test 10: `_bt_doinsert` was `987.99 ms`; scan-key build was `71.21 ms`; + insert search was `88.75 ms`; insert-location search was `223.50 ms`; + page insertion was `259.59 ms`; page split was only `13.91 ms`. + +No uniqueness-check timing was reported in cases 9 or 10 because these benchmark +indexes are not unique. Page splits are also not the culprit. The text update +case pays much more in `_bt_findinsertloc` than the numeric update case, while +both cases spend a similar amount in page insertion. A substantial residual +remains inside `_bt_doinsert` after the named subphases, so the next btree probe +should look at comparator/TID/posting-list/dedup support paths used between the +current timers rather than at split handling. + +Patch `0028` adds that next diagnostic layer. It times leaf-page insert binary +search and `_bt_compare` separately as `btree_binsrch_insert` and +`btree_compare`. `_bt_compare` is intentionally a diagnostic-only timer because +it can run many times per statement and will perturb absolute elapsed time; the +useful signal is whether comparison/search accounts for the residual left by +the `0027` split. The probe uses only `oliphaunt_wasix_*` timing symbols and +remains compiled out of normal release artifacts. + +The 29-patch timing artifact was rebuilt and packaged with +`OLIPHAUNT_WASM_SKIP_EXTENSIONS_FOR_PERF=1` for the focused diagnostic. That +perf package includes the PG18 runtime, plpgsql, dict_snowball, initdb, and AOT +artifacts, but intentionally skips external PGXS extensions. + +Btree compare diagnostic file: +`target/perf/pg18-wasix-core-release-o2-backend-timing-btree-compare-diagnose-speed-9-10.json` + +This run is more invasive than the `0027` split because it times `_bt_compare` +inside hot binary-search loops and nested timers double-count. Absolute elapsed +time should therefore not be compared to release numbers. The distribution is +still useful: + +- Test 9: elapsed was `7310.06 ms`; `_bt_doinsert` was `3578.45 ms`; + `_bt_compare` was `1867.51 ms`; leaf insert binary search was `1333.26 ms`; + root-to-leaf insert search was `1307.54 ms`; insert-location search was + `1507.74 ms`; page insertion was `293.59 ms`; page split was `16.12 ms`. +- Test 10: elapsed was `8093.33 ms`; `_bt_doinsert` was `3999.24 ms`; + `_bt_compare` was `2023.19 ms`; leaf insert binary search was `1392.06 ms`; + root-to-leaf insert search was `1468.11 ms`; insert-location search was + `1725.99 ms`; page insertion was `311.86 ms`; page split was `18.77 ms`. + +Conclusion: comparison/search work is now the leading btree signal for indexed +integer lookup and reinsertion. The next candidate should not target page +splits. It should either revive a corrected first-column int4 tuple-data +compare shortcut for the simple integer benchmark shape or split heap update +and WAL behavior before changing more release-default behavior. + +Patch `0030` is the first release-default candidate from that conclusion. It +adds a corrected first-column int4 leaf-tuple fast path. Unlike the earlier +full-concurrent experiment shortcut, this version keeps the old `oliphaunt` naming +out of the source, requires the built-in integer btree opfamily, requires a +normal non-null non-posting non-pivot leaf tuple, and treats equality as a +successful key comparison so the attribute loop is skipped while PostgreSQL's +existing heap-TID/truncated-key tie-break code still runs. It should primarily +affect Test 9. Test 10 still needs heap update, non-HOT update, and WAL +investigation because it updates an unindexed text column while looking up rows +through the integer index on `a`. + +Focused non-timing diagnostic file after `0030`: +`target/perf/pg18-wasix-core-release-o2-30patch-speed-diagnose-9-10.json` + +This was run against a rebuilt non-timing release-profile artifact with external +PGXS extensions skipped for the perf package: + +- Test 9: `1256.64 ms`. +- Test 10: `1476.79 ms`. + +Compared with the earlier PG18 simple-query speed run (`1322.57 ms` and +`1520.13 ms`), `0030` is a small directional win for the int4 indexed-update +case and a marginal win for the text case. It is not enough to catch the +documented PG17.5 release lane (`578.96 ms` and `712.38 ms`). Keep the patch +as a candidate, but the next material optimization needs to address heap +update/WAL behavior and/or remove more repeated planning/protocol overhead. + +Patch `0031` corrects the next diagnostic direction: Test 10 updates the +unindexed text column `c` while using the integer index on `a` for lookup, so it +is not primarily a text btree comparator workload. The patch adds +diagnostic-only heap update timers for modified-column detection, toast work, +new-page buffer selection, heap tuple insertion, and heap update WAL logging. + +Heap diagnostic file: +`target/perf/pg18-wasix-core-release-o2-31patch-backend-timing-heap-diagnose-speed-9-10.json` + +This timing artifact is intentionally not a release-performance comparison. It +adds hot-loop timing calls around btree comparison and heap update internals and +inflates the absolute elapsed time to `6657.05 ms` for Test 9 and `6944.91 ms` +for Test 10. The useful signal is distribution: + +- Test 9: `_bt_doinsert` was `3161.08 ms`; `_bt_compare` was `1642.43 ms`; + `exec_plan` was `694.00 ms`; `heapam_tuple_update` was `459.31 ms`; + `XLogInsertRecord` was `176.97 ms`; heap subprobes were small + (`heap_get_buffer_for_tuple` `44.50 ms`, `heap_determine_columns` + `40.03 ms`, `heap_put_tuple` `37.47 ms`). +- Test 10: `_bt_doinsert` was `3338.45 ms`; `_bt_compare` was `1683.47 ms`; + `exec_plan` was `716.89 ms`; `heapam_tuple_update` was `469.71 ms`; + `XLogInsertRecord` was `193.83 ms`; heap subprobes were again small + (`heap_get_buffer_for_tuple` `45.60 ms`, `heap_put_tuple` `37.76 ms`, + `heap_determine_columns` `37.15 ms`). + +Conclusion: the Test 10 miss is not primarily TOAST, heap-page selection, heap +tuple copy, or heap-update WAL logging. The timing artifact still points at +non-HOT indexed update behavior, btree reinsertion/search/compare work, and +repeated simple-query planning as the high-value areas. The heap subprobes do +not justify a heap-specific shortcut yet. + +The full-concurrent experiment's btree bottom-up-delete runtime toggle was also +tested as a possible diagnostic import. It is not carried in the patch stack. +Even with upstream behavior selected by default, the local embedded port +regressed the no-timing 9/10 run to `2284.27 ms` and `2795.44 ms`; the +`index-unchanged-off` override was no better overall (`2327.91 ms` and +`2765.07 ms`). That makes the hook itself too perturbing for this lane, and +disabling bottom-up deletion remains rejected as a default behavior change. + +Patch `0029` also fixes a buildability issue found while refreshing the timing +package: standalone PG18 WASIX `pg_dump` references `fork()` through PostgreSQL's +parallel dump support, but the single-user sysroot does not provide that symbol. +The patch gives the packaged tool a local `ENOSYS` fork stub under +`OLIPHAUNT_WASM_SINGLE_USER`, so `pg_dump` links without a fork runtime import +and any accidental parallel worker use fails through the existing error path. + +## Release Hygiene + +The rebuilt PG18 runtime binary does not contain the old `pgl_*`/`Oliphaunt` +runtime symbol strings when inspected from the packaged `oliphaunt/bin/oliphaunt` +module. The PG18 patch stack and build scripts also keep the new +`oliphaunt_wasix_*` naming. + +Patch `0030` work also tightened generated metadata: PG18 asset manifests now +filter released-lane source pins whose names, URLs, or branch labels refer to +the old Oliphaunt provenance and replace the PostgreSQL source pin with the PG18 +tarball plus the PG18 patch-series fingerprint. The regenerated PG18 perf +manifest now reports PostgreSQL `18.4`, and +zero old `pgl`/`oliphaunt` provenance references in its source pins. + +## Patch Disposition From This Run + +The full-concurrent experiment's LIKE literal substring fast path remains in +the embedded WASIX runtime as patch `0024`. It appears directionally useful for Test +5, but it does not address the dominant misses. + +The full-concurrent experiment's first-column int4 tuple-data btree shortcut was +tested and removed from the default patch stack. With that shortcut present, +the core-only O2 run was still not competitive (`1.385x` geomean vs PG17.5), +and the indexed update cases remained more than `2x` slower. The patch remains +recorded as deferred in +`src/runtimes/liboliphaunt/wasix/assets/build/postgres/experiment-patch-disposition.toml`. + +## Full Concurrent WASIX Direction + +The concurrent PG18 WASIX experiment is still valuable runtime research, but it +is not a near-term performance replacement path. Its upstream Wasmer and +WASIX-libc patches are mostly correctness/runtime-enablement work: + +- fixed shared file-backed memory remapping and `msync` +- memory-copy exclusion for forked stores +- resource limits for stack reporting +- socket, epoll, waitpid, futex, signal, and `sigsetjmp` behavior fixes +- fork declarations and full EXEC_BACKEND runtime unblockers + +Those are necessary for proper multi-process PostgreSQL semantics under WASIX, +but they add lifecycle and memory-management work that the product-style +single-backend lane intentionally avoids. They should be tracked upstream and +mined for narrow fixes, not used as the default product architecture until the +runtime can show competitive steady-state SQL numbers. + +The actual local concurrent experiment branch is +`/Users/sid/dev/oliphaunt-oxide-wasix-pg18-experiment` on +`f0rr0/wasix-pg18-experiment`, with a detached copy at +`/Users/sid/.codex/worktrees/2eae/oliphaunt-oxide`. Its query-hot PostgreSQL patch +stack has now been fully triaged against this embedded WASIX runtime: + +- `0006-like-literal-substring-fast-path.patch`: already ported as `0024` with + tighter LIKE/collation guards. +- `0007-top-xid-current-transaction-fast-path.patch`: already ported as `0015`. +- `0008-btree-int4-compare-fast-path.patch`: already ported as `0016` with + tighter opfamily/type/collation guards. +- `0009-btree-delete-stack-state.patch`: already ported as `0017` under + `__wasi__ && OLIPHAUNT_WASM_SINGLE_USER`. +- `0010-btree-bottomup-delete-runtime-toggle.patch`: remains rejected for the + default lane. A local embedded port of the diagnostic hook made the default + release-profile Test 9/10 run slower before any override was enabled, and + disabling bottom-up deletion changes PostgreSQL index maintenance behavior. +- `0011-btree-first-int4-compare-fast-path.patch`: already ported as `0030` + with tighter leaf/non-null/non-posting/non-pivot/built-in-int4 guards. + +That leaves the remaining same-host misses after `0032`-`0034` as measurement +work rather than an obvious unported concurrent-experiment patch. The next +artifact rebuild needs to answer whether the newly ported checkpoint, +postmaster-environment, semaphore-reset, and GUC-reporting changes close Test +11 and improve the smaller Test 1/10/14/15 misses. + +## Recommendation + +Do not promote PG18 WASIX as the `0.6.0` replacement yet solely on this local +evidence. The lane is close enough to parity that the old "2x slower indexed +updates" framing should be retired, but release confidence still needs a +repeat-based run under the same conditions as the PG17.5 release-lane table. + +Continue the PG18 WASIX runtime, not the full concurrent WASIX lane, as the +main product direction. The next perf work should focus on closing the +remaining median misses rather than adding broad speculative PostgreSQL +shortcuts: + +- use repeated server-SQLx runs and compare medians/p90s, because the first run + after AOT crate rebuild can be a cold outlier; +- focus next on Test 15 and other many-small-simple-statement cases, where PG18 + still shows per-statement overhead; +- keep the spinlock-disabled build knob as the highest-signal upstream Oliphaunt + PG18.3 experiment once Docker is available; +- treat old indexed-update files above `2x` as stale unless reproduced with the + current AOT/runtime lane; +- keep `release`/O2 as the measured baseline until `release-o3`/thin-LTO or + wasm-opt proves a repeatable win for this workload; +- leave the full concurrent WASIX mmap/fork patches as runtime research unless + a narrow part directly improves the single-backend lane. + +## 2026-05-29 34-Patch Rebuild + +Docker was brought up locally and the PG18 WASIX runtime was rebuilt with the +34-patch stack in release/O2 profile: + +```sh +OLIPHAUNT_WASM_SKIP_EXTENSIONS_FOR_PERF=1 \ +OLIPHAUNT_WASM_BUILD_PROFILE=release \ +FORCE_RECONFIGURE=1 \ +cargo run -p xtask -- assets release-build \ + \ + --profile release \ + --skip-package-size +``` + +The backend/runtime build completed. Packaging then required the maintainer-only +template runner, so the packaging/AOT phase was rerun with: + +```sh +OLIPHAUNT_WASM_SKIP_EXTENSIONS_FOR_PERF=1 \ +OLIPHAUNT_WASM_BUILD_PROFILE=release \ +cargo run -p xtask --features template-runner -- \ + assets release-build \ + \ + --profile release \ + --skip-build \ + --skip-package-size +``` + +The generated runtime and AOT manifests now report fingerprint +`18.4:81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094:941081dbd639e3aa39f631673060cb4b9f506bcb6fb81da821959bb64eab4552`. + +Focused diagnostics after the rebuild: + +- Test 1: `20.720 ms` +- Test 11: `114.064 ms` + +That improves the old focused PG18 31-patch Test 1/11 numbers (`24.40 ms` and +`119.07 ms`), but still does not fully match same-host PG17.5 for Test 11. + +The full server-SQLx speed benchmark was run twice against the rebuilt PG18 AOT +artifact: + +- `target/perf/pg18-wasix-core-release-o2-34patch-speed-server-sqlx.json` +- `target/perf/pg18-wasix-core-release-o2-34patch-speed-server-sqlx-rerun.json` + +Using the median of those two PG18 runs against the median of the two same-host +PG17.5 `0.5.0` runs gives `1.002x` geomean. Against the documented release-lane +table it is still `1.090x` geomean. The parity is therefore geomean-only; it is +not per-head parity. + +| Test | PG18 median ms | PG17.5 same-host median ms | PG18 / PG17.5 | PG18 / documented | +| --- | ---: | ---: | ---: | ---: | +| 1 | 21.40 | 19.89 | 1.076 | 1.083 | +| 2 | 170.08 | 165.82 | 1.026 | 1.137 | +| 2.1 | 62.47 | 61.67 | 1.013 | 1.052 | +| 3 | 289.88 | 253.01 | 1.146 | 1.144 | +| 3.1 | 90.27 | 107.12 | 0.843 | 0.949 | +| 4 | 191.12 | 190.28 | 1.004 | 1.173 | +| 5 | 361.47 | 388.98 | 0.929 | 1.069 | +| 6 | 17.42 | 15.00 | 1.162 | 1.332 | +| 7 | 164.94 | 142.11 | 1.161 | 1.316 | +| 8 | 94.25 | 85.75 | 1.099 | 1.266 | +| 9 | 665.56 | 610.85 | 1.090 | 1.150 | +| 10 | 822.84 | 775.34 | 1.061 | 1.155 | +| 11 | 112.66 | 97.81 | 1.152 | 1.156 | +| 12 | 9.90 | 10.81 | 0.916 | 1.016 | +| 13 | 15.38 | 27.97 | 0.550 | 0.579 | +| 14 | 80.28 | 78.12 | 1.028 | 1.121 | +| 15 | 103.93 | 90.39 | 1.150 | 1.395 | +| 16 | 8.73 | 9.97 | 0.876 | 0.858 | + +The Test 11 buffer/cache diagnostic still shows the same COMMIT gap as the +31-patch lane: + +| Statement | PG18 34-patch ms | PG17.5 same-host ms | +| --- | ---: | ---: | +| `BEGIN` | 0.019 | 0.023 | +| `INSERT INTO t1 SELECT b,a,c FROM t2` | 29.808 | 29.384 | +| `INSERT INTO t2 SELECT b,a,c FROM t1` | 93.056 | 90.342 | +| `COMMIT` | 13.993 | 0.711 | + +This confirms that patches `0032` and `0034` are applied and built, but they do +not explain or remove the remaining Test 11 COMMIT cost. Source inspection also +confirms the rebuilt PG18 source contains the same upstream PG18.3 Oliphaunt-style +`IsPostmasterEnvironment = true`, `IsUnderPostmaster = true`, +`XLogCheckpointNeeded` guard, and `RequestCheckpoint` guard. The remaining +COMMIT gap is therefore either a different PG18 WAL/checkpoint/pgstat/SLRU path, +or an interaction with the current Oliphaunt WASIX runtime/startup state, not a +simple failure to port those upstream Oliphaunt branch hints. + +The status after this rebuild is: + +- PG18 WASIX is now geomean-parity with same-host PG17.5 within local + two-run noise. +- PG18 WASIX is still not per-test parity and is still behind the + documented release-lane table. +- The broad "PG18 is catastrophically slower" claim is no longer accurate for + the current 34-patch release/O2 artifact. +- The remaining high-signal misses are Test 3, 6, 7, 8, 11, and 15, with Test + 11 still carrying a clear COMMIT-specific gap. + +## 2026-05-29 COMMIT Timing Split + +The Test 11 COMMIT gap was split with diagnostic-only timing patches: + +- `0035-oliphaunt-wasix-add-commit-backend-timing-probes.patch` +- `0036-oliphaunt-wasix-add-commit-record-backend-timing-probes.patch` +- `0037-oliphaunt-wasix-add-xlog-flush-write-timing-probes.patch` +- `0038-oliphaunt-wasix-split-xlog-write-tail-timing.patch` + +Both patches are compiled out unless `OLIPHAUNT_WASIX_BACKEND_TIMING=1`. + +The first timing build showed that the cost is inside +`RecordTransactionCommit()`: + +| Phase | Time | +| --- | ---: | +| `COMMIT` wall time | `13.870 ms` | +| `exec_finish_xact` | `13.805 ms` | +| `commit_record` | `13.780 ms` | +| `commit_resource_locks` | `0.010 ms` | +| `commit_local_cleanup` | `0.004 ms` | +| all other commit cleanup probes | `0.001 ms` each | + +The second timing build split `RecordTransactionCommit()`: + +| Phase | Time | +| --- | ---: | +| `COMMIT` wall time | `20.725 ms` | +| `commit_record` | `20.642 ms` | +| `commit_xlog_flush` | `20.634 ms` | +| `commit_xlog_record` | `0.002 ms` | +| `commit_clog_commit_tree` | `0.002 ms` | +| `commit_sync_rep_wait` | `0.001 ms` | + +The timing build has extra probe overhead and a different code shape, so its +absolute COMMIT time should not be compared directly to release/O2. The useful +fact is the attribution: essentially all of the PG18 Test 11 COMMIT gap is +`XLogFlush(XactLastRecEnd)`, even with: + +| Setting | Value | +| --- | --- | +| `fsync` | `off` | +| `synchronous_commit` | `on` | +| `shared_buffers` | `128MB` | +| `wal_buffers` | `4MB` | + +This rules out the PG18 transaction cleanup additions as the main cause: +`AtEOXact_Aio`, relcache/typecache cleanup, invalidation, procarray end, +resource-owner cleanup, notification, memory cleanup, CLOG commit, WAL record +construction, and sync-rep wait are all microsecond-level in the diagnostic. + +The PG17.5 Oliphaunt-style source and the current PG18 source are structurally +similar in `RecordTransactionCommit()`: both call `XLogFlush(XactLastRecEnd)` +when `synchronous_commit > off`. Upstream `PG18 legacy lane` does not carry an +additional Oliphaunt-specific `XLogFlush` fast path; it only has the checkpoint +guard that is already ported as `OLIPHAUNT_WASM_SINGLE_USER`. + +The third timing build ruled out the obvious lower-level suspects: + +| Phase | Time | +| --- | ---: | +| `COMMIT` wall time | `13.750 ms` | +| `commit_xlog_flush` | `13.653 ms` | +| `xlog_flush_xlog_write` | `13.650 ms` | +| `xlog_write_pwrite` | `0.760 ms` | +| `xlog_flush_wal_write_lock` | `0.001 ms` | + +A `max_wal_senders=0` startup-GUC diagnostic still showed the same shape: +`COMMIT` was `15.535 ms` and `commit_xlog_flush` was `15.437 ms`. So the +remaining gap is not walsender wakeup, WALWriteLock waiting, fsync, or raw host +write time. + +The fourth timing build split `XLogWrite()` itself: + +| Phase | Time | +| --- | ---: | +| `COMMIT` wall time | `13.912 ms` | +| `commit_xlog_flush` | `13.824 ms` | +| `xlog_flush_xlog_write` | `13.821 ms` | +| `xlog_write_loop` | `13.819 ms` | +| `xlog_write_loop_scan` | `12.884 ms` | +| `xlog_write_pwrite` | `0.819 ms` | +| `xlog_write_pgstat_io` | `0.001 ms` | +| `xlog_write_fsync` | `0.001 ms` | +| `xlog_write_walsnd_request` | `0.001 ms` | + +This changes the diagnosis: the PG18 COMMIT miss is dominated by the +per-WAL-page scan/grouping loop in `XLogWrite()`, not by PG18's new +`pgstat_count_io_op_time()` accounting, the filesystem call, fsync, or +walsender signaling. Relation sizes match the PG17.5 diagnostic exactly, so the +next question is whether PG18 is arriving at COMMIT with more unwritten WAL or +whether the same WAL-buffer walk is materially slower in the PG18/WASIX codegen +path. + +Next high-signal checks: + +- add a count/byte diagnostic for `XLogWrite()` loop iterations, pages grouped, + and bytes passed to `pg_pwrite()` for PG18, then compare against a PG17.5 + timing build if feasible; +- inspect `AdvanceXLInsertBuffer()`/WAL-buffer-full write behavior to see whether + PG18 defers more WAL to COMMIT than PG17.5 under the same `wal_buffers=4MB`; +- evaluate a WASIX-single-user fast path that writes WAL from contiguous buffer + ranges without the full per-page scan when the target range is known ready; +- separately test `relaxed_durability(true)` / `synchronous_commit=off`, because + that should bypass the synchronous `XLogFlush()` commit path and establish the + upper bound for Test 11 parity. diff --git a/docs/internal/PG18_WASIX_POSTGRES.md b/docs/internal/PG18_WASIX_POSTGRES.md new file mode 100644 index 00000000..c2d01a1c --- /dev/null +++ b/docs/internal/PG18_WASIX_POSTGRES.md @@ -0,0 +1,666 @@ +# PG18 WASIX PostgreSQL Runtime + +This runtime is the fresh PostgreSQL 18 WASIX build that keeps the released +Oliphaunt WASM product shape: one embedded backend behind the direct Rust API +and the local server wrapper. It is not the concurrent full PostgreSQL WASIX +experiment and should not take postmaster or multi-backend assumptions as a +performance constraint. + +## Target Shape + +- PostgreSQL 18.4 source, pinned independently from the released PG17.5 lane. +- Oliphaunt-style single backend execution, engineered from first principles. +- Existing released-lane capabilities preserved before replacement: + protocol execution, template packaging, initdb, pg_dump, bundled extensions, + runtime support modules, and the server wrapper. +- The released artifact pipeline packages standalone WASIX `initdb` and + `pg_dump` tools. `pg_dumpall.c` is patched only because it shares the + renamed pg_dump helper; pg_dumpall and psql are not separate packaged WASIX + tools in this lane unless future work adds them explicitly. +- Patch series kept small, ordered, and reviewable. Each patch should explain + which PostgreSQL invariant it changes and why the embedded WASIX runtime + still preserves the useful part of that invariant. +- Performance goal is to beat the released PG17.5 WASIX lane on the existing + benches before this becomes a replacement candidate. + +## Research Assessment + +The concurrent full-PostgreSQL WASIX experiment is still valuable for finding +Wasmer, WASIX libc, fork, socket, shared-memory, and toolchain blockers, but it +is not the right replacement path for the released embedded product yet. The +experiment evidence points to persistent process/fork/shmem/socket/RSS costs +before query execution, while the released product wins by keeping one backend, +one host lifecycle, direct FE/BE pumping, prebuilt PGDATA, and AOT reuse. + +The practical direction is therefore: + +- Keep full concurrent PostgreSQL under WASIX as upstream/runtime research and + a correctness oracle for patches that should eventually make WASIX more + POSIX-like. +- Build the replacement product as PG18 WASIX `wasix-dl`, preserving the + released Oliphaunt-style execution model and only taking runtime patches that + improve that model. +- Treat the PG18 experiment's PostgreSQL hot-path patches as candidates after + parity is buildable. The strongest candidates are WASIX-gated + `hash_bytes()` load folding, top-level `TransactionIdIsCurrentTransactionId` + short-circuiting, and narrow btree int4 comparator fast paths. They attack + hot guest CPU paths without changing the host lifecycle. +- Keep diagnostic toggles, such as bottom-up btree delete disabling, out of the + default product path unless benchmarks prove a production-safe default. +- Defer broader planner/executor shortcuts, locale-sensitive LIKE shortcuts, + and stack-allocation rewrites until they have focused regression coverage, + because they can silently change SQL semantics or memory pressure. +- Record every full-PG experiment patch in + `src/runtimes/liboliphaunt/wasix/assets/build/postgres/experiment-patch-disposition.toml` + before porting or rejecting it. The source-spine guard checks that manifest + so experiment patches cannot be copied into this runtime without a WASIX + rationale. + +The immediate conclusion is that a "proper" concurrent PostgreSQL under WASIX +is unlikely to match native PostgreSQL or released Oliphaunt-style WASM performance +soon. A fresh PG18 WASIX runtime can plausibly beat the released PG17.5 lane +because it keeps the low-overhead lifecycle while inheriting newer PostgreSQL, +newer WASIX/Wasmer fixes, tighter host ABI boundaries, and targeted hot-path +patches from the experiment. + +## PG17.5 Release-Lane Implementation Comparison + +The released PG17.5 WASIX lane is represented by the monolithic patch: + +```sh +src/runtimes/liboliphaunt/wasix/assets/build/patches/postgres-oliphaunt-wasix-dl.patch +``` + +The PG18 lane represents the same product shape as a 37-patch PostgreSQL 18.4 +series under: + +```sh +src/runtimes/liboliphaunt/wasix/assets/build/postgres/patches +``` + +The top-level execution model is equivalent. Both lanes start PostgreSQL +through `PostgresSingleUserMain()`, attach a host-backed frontend/backend +`Port`, let the Rust host call PostgreSQL's startup-packet parser, emit normal +startup protocol messages, and then drive PostgreSQL one frontend message at a +time through an exported `PostgresMainLoopOnce()`. + +The ABI is intentionally not equivalent. PG17.5 still exposes the +Oliphaunt-shaped names and compile macro, such as `OLIPHAUNT_WASIX_DL`, +`oliphaunt_wasix_start`, `pgl_pq_flush`, `pgl_getMyProcPort`, and +`pgl_sendConnData`. PG18 replaces those with Oliphaunt-owned symbols: +`OLIPHAUNT_WASM_SINGLE_USER`, `oliphaunt_wasix_start`, +`oliphaunt_wasix_pq_flush`, `oliphaunt_wasix_get_proc_port`, and +`oliphaunt_wasix_send_conn_data`. The Rust host now resolves the +`oliphaunt_wasix_*` exports directly, so PG18 should not leak old `pgl*` +symbols as part of the public lane contract. + +The build spine is similar but split for reviewability. Both lanes add a +`wasix-dl` PostgreSQL template, position-independent side-module builds, a +backend `libpgcore` object, WASIX dynamic-linker makefile support, and PGXS +side-module extension installation. PG18 keeps the same broad shape while +using `OLIPHAUNT_WASM_SINGLE_USER`, `DLSUFFIX=".so"`, and explicit pthread and +unnamed POSIX semaphore settings in the template. PG18 also carries a later +patch that forces backend-core linking through `wasm-ld --relocatable`. + +The main implementation delta is protocol I/O. PG17.5 routes backend protocol +bytes through the old broad WASIX/Oliphaunt shim path. PG18 adds an explicit +`OliphauntWasmHostIO` callback table to `Port`, and the backend libpq +`secure_raw_read()` and `secure_raw_write()` functions dispatch through those +callbacks only when the embedded WASIX port installs them. This is a +cleaner boundary, but it is also one of the few places where the PG18 port is +not mechanically identical to PG17.5 and should remain on the copy/flush audit +list. + +The error-recovery and protocol-state work is at least as complete in PG18 as +in PG17.5. PG18 carries the host-callable top-level recovery export, COPY +state reset on error, active portal failure during abort, post-error +ReadyForQuery scheduling for simple-protocol recovery, and re-arming of +`PG_exception_stack` after host-forced recovery. These are the pieces that let +one backend survive query failures in the embedded host. + +The PG17.5 monolithic patch has one source hunk not currently present in the +PG18 patch stack: `src/common/file_utils.c` treats `EISDIR` like `EBADF` and +`EINVAL` when fsyncing a directory. That is a portability/tooling delta, not a +steady-state query hot path, and it does not explain the PG18 indexed-update +regression. It should still be either ported as a narrow PG18 patch if a WASIX +tool path needs it, or documented as unnecessary after initdb/pg_dump coverage. + +PG18 carries several PostgreSQL hot-path patches that PG17.5 did not carry: +WASIX-gated `hash_bytes()` load folding, a top-XID visibility shortcut, guarded +btree int4 comparator shortcuts, btree delete scratch-buffer stack placement, +a deterministic `%literal%` LIKE fast path, upstream-style checkpoint/runtime +fast paths, an `XLogWrite()` hot-loop segment-bounds check, and an embedded-only +activity-ID reporting guard. Diagnostic timing probes remain available around +simple-query execution, executor/storage, btree insertion, btree comparison, +and heap update subphases. With a release-built host and the 37-patch O2 +artifact, PG18 is faster than same-host PG17.5 and the documented PG17.5 +release table on every speed-test head; see +`docs/internal/PG18_WASIX_PERF_STATUS.md`. + +## Upstream PG18 legacy lane Hints + +The upstream Oliphaunt branch +`https://github.com/postgres/postgres/tree/PG18 legacy lane` was +compared against PostgreSQL `REL_18_3` on 2026-05-29. It is an Emscripten +single-artifact port, not a WASIX dynamic-main lane, so its symbol names and +build scripts are reference material only. It should not be copied into PG18 +WASIX with `pgl*` or `oliphaunt` ABI names. + +For the comparison, the local Oliphaunt checkouts were given the real upstream +PostgreSQL base tags by fetching `REL_17_5` and `REL_18_3` from +`postgres/postgres`. `PG17 legacy lane` changes 54 files versus PostgreSQL +`REL_17_5`; `PG18 legacy lane` changes 55 files versus PostgreSQL `REL_18_3`. +The core runtime patch surface is the same in both Oliphaunt branches: +`build-oliphaunt.sh`, `oliphaunt/src/oliphauntc/oliphauntc.c`, +`src/backend/access/transam/xlog.c`, `src/backend/postmaster/checkpointer.c`, +`src/backend/tcop/postgres.c`, `src/backend/utils/init/postinit.c`, +`src/backend/utils/misc/guc.c`, and a small set of storage/init support files. +There are no hidden upstream PG18 Oliphaunt btree, hash, LIKE, executor, or planner +speed patches. + +Useful findings from that branch: + +- The top-level single-user architecture matches our direction: dummy + frontend/backend port, exported startup packet parser, startup protocol + emission, loop-pumped `PostgresMainLoopOnce()`, and host-managed top-level + longjmp recovery. +- The branch still relies on broad C preprocessor remaps for `recv`, `send`, + `system`, `popen`, `pclose`, identity calls, SysV shared memory, `fcntl`, + `munmap`, and longjmp. Our PG18 lane deliberately replaces the protocol + side of that with `OliphauntWasmHostIO` callbacks and uses + `oliphaunt_wasix_*` names for the remaining WASIX bridge. +- It sets both `IsPostmasterEnvironment` and `IsUnderPostmaster` when the host + starts the embedded backend, then patches checkpoint paths so Oliphaunt does not + try to signal a missing checkpointer from WAL segment pressure. Our active + PG18 lane now ports that shape under Oliphaunt-owned markers: patch `0034` + sets those environment flags in `oliphaunt_wasix_start()`, and patch `0032` + keeps XLog-size checkpoint requests disabled while preserving local + in-process `RequestCheckpoint()` behavior for embedded WASIX. +- It patches `RequestCheckpoint()` to run in-process under Oliphaunt even when the + backend was made to look postmaster-owned. The active PG18 lane now does the + same under `OLIPHAUNT_WASM_SINGLE_USER`. +- It shortens `PGSemaphoreReset()` to one `sem_trywait()` under Oliphaunt. The + active PG18 lane ports this as part of the current 37-patch stack. It is a + lifecycle/runtime cleanup optimization, not an explanation for the earlier + isolated Test 11 COMMIT gap. +- It changes `pg_flush_data()` to call `fsync()` under oliphaunt. That is a + portability choice for Emscripten MEMFS, not an obvious WASIX performance + improvement. It is likely too blunt for the PG18 release lane without a + tool-specific failure. +- It skips `guc_strdup()` for `record->last_reported` in `ReportGUCOption()`. + The active PG18 lane now ports this. It can reduce long-lived GUC-report + allocations, but it affects startup or changed GUC reporting, not the speed + suite's steady-state query work. +- It adds private encoding symbol shims in libpq and pg_dump to avoid static + link/LTO collisions. Our WASIX bridge and initdb shim already expose + `pg_char_to_encoding_private` and `pg_encoding_to_char_private`; the PG18 + lane also carries a narrower pg_dump LTO helper rename. If future pg_dump or + extension builds fail with encoding symbol collisions, this branch is useful + prior art. +- It collects extension undefined symbols into import lists and then builds the + main oliphaunt export list from those imports plus a manually included export + file. Our PGXS side-module lane already has analogous import-list + generation, but the upstream branch is a good checklist for extension + packaging completeness. +- It packages a minimal ICU data tree. Our release lane should keep validating + ICU/collation behavior through the existing asset pipeline instead of + inheriting Oliphaunt's Emscripten preload layout. + +None of the PG18 legacy lane source deltas explained the earlier PG18 WASIX +per-head misses. The upstream branch's relevant lifecycle/runtime deltas are +already present in the active PG18 lane, and the branch does not carry the +btree/hash/top-XID/LIKE hot-path patches already present here. The final Test +15 gap was closed by an Oliphaunt-specific embedded activity-ID reporting guard, +not by wholesale copying more upstream Oliphaunt code. + +## Patch Stack Plan + +1. Build spine: teach PostgreSQL 18 about the `wasix-dl` dynamic-main build + target without adding behavioral changes. Done in patch 0001. This patch + keeps PostgreSQL spinlocks on the normal WASIX toolchain atomics path; the + old disabled-spinlock fallback is not part of this lane. +2. Host I/O: add explicit embedded backend I/O hooks instead of relying on + broad syscall remapping. Done in patch 0002. +3. Startup parsing: expose PostgreSQL's own startup packet parser to the + WASIX host under `OLIPHAUNT_WASM_SINGLE_USER`. Done in patch 0003. +4. Host lifecycle exports: attach a host-backed FE/BE Port after standalone + initialization and emit the startup protocol messages expected by the + released Rust host. Done in patch 0004. +5. Loop-pumped protocol: split the `PostgresMain()` loop so the host can drive + one frontend message at a time without starting a second backend lifecycle. + Done in patch 0005. +6. COPY protocol handoff: report PostgreSQL CopyIn/CopyOut/CopyBoth response + transitions to the WASIX host so the released proxy can switch from buffered + pumping to streaming at PostgreSQL-owned protocol boundaries. Done in patch + 0006. +7. PGXS side-module parity: add the `wasix-dl` platform makefile expected by + configure and emit WASIX extension import lists from PGXS installs without + inheriting Emscripten-named variables or absolute non-`DESTDIR` paths. Done + in patch 0007. +8. Error recovery protocol state: reset PostgreSQL-owned COPY handoff state + when top-level error recovery aborts the active subprotocol. Done in patch + 0008. +9. Error recovery: keep PostgreSQL's top-level cleanup path host-callable so + one backend can recover from query failures. Initial recovery export done in + patch 0005; the WASIX bridge and Rust host already route forced + process-exit ERROR recovery through `PostgresMainLongJmp()`. +10. WASIX process identity: route PostgreSQL's process identity lookups through + the `wasix-dl` port header instead of PG18 configure-script-wide identity + remaps. Done in patch 0009. +11. Shared memory and semaphores: route SysV shared-memory calls through the + `wasix-dl` port header and local WASIX headers instead of PG18 + configure-script-wide shared-memory remaps. Done for SysV shared memory in + patch 0010. Prefer unnamed POSIX semaphores explicitly in patch 0011. +12. Startup error capture: route `InitPostgres()` failures through the + host-backed protocol Port before the exported main-loop recovery buffer is + active, so database/role/startup-option errors remain PostgreSQL-owned. + Done in patch 0012. +13. Host-recovery portal cleanup: mark active portals failed during + `AtAbort_Portals()` while the embedded backend is active, preserving + PostgreSQL-owned cleanup when a WASIX host routes nested ERROR unwinds + through the top-level recovery export. Done in patch 0013. +14. Hash hot path: use WASIX-only `memcpy`-based 32-bit loads in the + unaligned little-endian `hash_bytes()` and `hash_bytes_extended()` paths. + This keeps unaligned C access defined while giving LLVM a single-load + lowering opportunity in hash-table-heavy query paths. Done in patch 0014; + promotion still requires benchmark evidence. +15. Transaction visibility hot path: short-circuit + `TransactionIdIsCurrentTransactionId()` for the common top-level case after + the top XID comparison fails and no parallel, parent, or child XID source + can still match. Done in patch 0015; promotion still requires benchmark + evidence. +16. Btree int4 compare hot path: avoid the fmgr trampoline for WASIX + embedded-runtime comparisons that prove they are the built-in integer btree + family with int4 input on both sides and InvalidOid collation, while still + using `index_getattr()` and falling back to upstream comparison for every + other case. Done in patch 0016; the more aggressive direct tuple-data + shortcut from the full-PG experiment remains intentionally unported pending + runtime evidence and tighter layout proof. +17. Btree delete scratch buffers: keep `MaxTIDsPerBTreePage` simple-deletion + and bottom-up-deletion scratch arrays on the stack in the embedded WASIX + lane. The arrays are page-size bounded and PostgreSQL already uses similar + page-local stack buffers elsewhere; upstream heap allocation remains for + all other builds. Done in patch 0017. +18. pg_dump LTO hygiene: rename pg_dump's generic `executeQuery()` helper to + `executeDumpQuery()` so standalone WASIX pg_dump builds do not expose an + unnecessarily collision-prone external symbol under thin LTO. Done in patch + 0018. +19. Host-forced recovery ReadyForQuery scheduling: preserve PostgreSQL's + post-ERROR `send_ready_for_query` behavior when the WASIX host invokes the + recovery export directly, while still withholding ReadyForQuery during + extended-protocol skip-till-Sync recovery. Done in patch 0019. +20. Host-forced recovery exception boundary: re-arm `PG_exception_stack` to + `postgresmain_sigjmp_buf` when the WASIX host invokes the recovery export + directly, matching PostgreSQL's normal post-`sigsetjmp` recovery flow. Done + in patch 0020. +21. Extension/tool parity: rebuild contrib, pg_dump, initdb, and packaged + extensions against the PG18 lane. +22. Performance work: carry Wasmer, WASIX libc, LTO/codegen, file-system, and + memory-growth improvements into this lane only after parity is testable. + +## Experiment Patch Disposition + +The full-concurrent PG18 WASIX experiment remains useful prior art, but its +patches are not the default source of truth for this lane. The reviewed +disposition manifest is: + +```sh +src/runtimes/liboliphaunt/wasix/assets/build/postgres/experiment-patch-disposition.toml +``` + +The manifest records each experiment patch by filename, whether it was ported, +replaced, deferred, or rejected, and why that decision fits a single-backend +WASIX product. The currently carried performance/tool patches are the hash +load fast path, top-XID visibility fast path, guarded btree int4 comparator, +btree delete scratch-buffer stack placement, LIKE substring shortcut, first +int4 leaf compare shortcut, and pg_dump LTO symbol hygiene. The +bottom-up-delete runtime toggle and full EXEC_BACKEND/fork runtime patches +remain outside the default lane. + +When the full-PG experiment checkout exists locally, the source-spine guard +also compares the disposition manifest against the actual experiment patch +directory, so new experiment patches cannot appear without an explicit +embedded-runtime decision. + +## Current Slice + +The initial source-prep entrypoint is: + +```sh +src/runtimes/liboliphaunt/wasix/assets/build/prepare_postgres_source.sh +``` + +It downloads or reuses the PostgreSQL 18.4 tarball, verifies the upstream +checksum, extracts into `target/oliphaunt-wasix/wasix-build`, +and applies the patch series in +`src/runtimes/liboliphaunt/wasix/assets/build/postgres/patches/series`. +The source-spine guard also requires that file to match the duplicate +`[patches].series` list in `postgres/source.toml`, so build metadata +and the applied patch order cannot drift silently. It also rejects orphan +`.patch` files that are not listed in the series; source fingerprints and +applied patch contents must describe the same stack. The guard also checks +that the stack remains reviewable: currently exactly 31 sequentially numbered +`oliphaunt-wasix` patches, each with a matching subject/filename slug, an +Oliphaunt maintainer header, and a short rationale before the diff. When a +prepared PG18 source tree exists, xtask recomputes the source fingerprint from +the PostgreSQL tarball metadata, patch series file, and patch file hashes, then +compares both the source-tree marker and work-root marker against that value. +The same prepared-source verifier is used by PG18 source prep and build-output +discovery, so template/package/AOT commands do not accept a stale prepared +source tree. If source prep uses an overridden PG18 work root, xtask verifies +the marker in that actual work root rather than the default target directory. +The same guard scans the PG18 patch stack, PG18 build scripts, Rust host loader, +prepared source tree, and generated PG18 manifests/AOT metadata for legacy +Oliphaunt ABI tokens such as `__OLIPHAUNT__`, `OLIPHAUNT_*`, `PGL_*`, and `pgl_*`. PG17 +and upstream Oliphaunt remain valid references, but the PG18 release lane must use +only `oliphaunt_wasix_*` and `OLIPHAUNT_WASM_*` names in runtime/build +surfaces. + +The existing PG17.5 released build remains untouched. + +## Build Entry Points + +The PG18 WASIX backend has dedicated configure/backend entrypoints: + +```sh +src/runtimes/liboliphaunt/wasix/assets/build/configure_wasix_dl.sh +src/runtimes/liboliphaunt/wasix/assets/build/docker_oliphaunt.sh +``` + +The PG18 configure script uses the new source-prep path and a smaller compile +profile than the released lane: it does not define `__OLIPHAUNT__` or +`OLIPHAUNT_WASIX_DL`, and it relies on the explicit host I/O hooks instead of +remapping `recv()`/`send()`. It still uses the existing WASIX bridge for +runtime process, identity, shared-memory, and longjmp compatibility while those +areas are being replaced by smaller PostgreSQL patches. +The PG18 backend Docker entrypoint uses the same `source_lane.sh` helper as the +companion build scripts for its build directory and prepared source path, so the +backend and extension/tool stages do not drift onto different build roots. + +The companion build scripts now default to the stable PostgreSQL source and +build tree: + +```sh + src/runtimes/liboliphaunt/wasix/assets/build/docker_runtime_support.sh + src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh + src/runtimes/liboliphaunt/wasix/assets/build/docker_pgdump.sh + src/runtimes/liboliphaunt/wasix/assets/build/docker_pgxs_extensions.sh + src/runtimes/liboliphaunt/wasix/assets/build/docker_contrib_extensions.sh +``` + +The standalone WASIX `initdb` build keeps explicit frontend-tool remaps for +process identity lookups and links the dedicated `oliphaunt_wasix_initdb_shim`. +The `pg_config_wasix.sh` helper is also source-marker aware. PG18 runs must +receive an explicit prepared `PGSRC`, so standalone PGXS invocations fail closed +instead of silently returning unrelated include paths. `PGSRC` must carry the +prepared-source PostgreSQL version and source-fingerprint markers, so an +arbitrary upstream checkout cannot masquerade as the patched source stack. The +helper exposes `--includedir-server` as the selected build tree's `src/include`, +which keeps direct extension Makefile calls on the same server-header surface +as the Docker PGXS wrapper. +That shim now covers the PG18 port surface used by `wasix-dl`, including +`getegid`, `getgid`, and `getpwuid_r`, so tool builds do not accidentally depend +on whichever subset WASIX libc happens to provide. + +`cargo run -p xtask -- assets build` prints the +PG18 build spine, and adding `--execute` runs the PG18 backend script followed +by the same released-lane companion scripts against the PG18 build tree. + +`assets fetch` and +`assets release-build --fetch` now skip only the +released `postgres-oliphaunt` backend checkout. They still fetch the shared +non-backend source pins, then run +`src/runtimes/liboliphaunt/wasix/assets/build/prepare_postgres_source.sh` to +materialize the pinned PostgreSQL 18.4 tarball plus the PG18 patch stack. +`assets source-spine` uses the same PG18 +source-spine guard instead of the released patch checkout guard; adding +`--check-patch-applies` materializes the pinned PG18 source tree and validates +the applied runtime, tool, contrib, and hot-path patch markers. The PG18 +source-spine command defaults to source-only validation; `--strict-local` also +requires the shared non-backend source checkouts to be present, clean, and pinned. + +Once those outputs exist, `assets template` and +`assets package` discover the PG18 build tree, +derive manifest PostgreSQL versions from the prepared PG18 source markers, and +write explicit PG18 source-fingerprint and PG18 source pins into +generated asset manifests. PG18 packaged asset discovery rejects manifests +whose fingerprint does not match the current PostgreSQL tarball plus patch +series hash. Contrib extension control files are staged from the active +PostgreSQL source tree, not from the generated PG17 catalog metadata. The +default path remains the released PG17.5 lane unless the source selection is +explicitly selected. + +PG18 build outputs must also carry the same source fingerprint and PostgreSQL +version markers as the prepared source. The backend Docker entrypoint stamps +those markers after configure, companion build stages fail closed if either +marker drifts, and xtask checks the markers again before packaging, template, or +build-output manifest generation can consume an existing build tree. + +PGDATA template manifests produced by `assets template --source fingerprint ...` also +carry camelCase `sourceLane` metadata, and the asset manifest's +`pgdata-template` entry records the same lane. PG18 templates also carry the +same source fingerprint as the runtime assets. Older released templates without +those fields still parse as released-lane templates. + +The source-spine guard also checks the prepared PG18 tree against the runtime +assets and promoted contrib build plan. The required `plpgsql`, +`dict_snowball`, and timezone source inputs must be present; every promoted +contrib extension must have its PG18 `contrib/` source directory and +Makefile; and CREATE EXTENSION entries must have the matching PG18 control file +plus at least one packaged extension SQL file. +When optional upstream discovery checkouts are absent, xtask falls back to the +committed generated extension build plan so this parity check can still run in +source-only worktrees. + +The same guard validates the applied PG18 source tree, not just the patch files: +the prepared source must contain the host I/O Port hooks, startup packet export, +single-backend lifecycle exports, loop-pumped protocol exports, COPY handoff +reports, host-forced recovery fixes, portal abort cleanup, and the carried +WASIX-gated hot-path patches. + +The source-controlled WASIX export list treats hybrid protocol switching as part +of the host/runtime ABI. `pgl_set_protocol_transport` and +`pgl_protocol_stream_active` are exported alongside the older buffered protocol +helpers so COPY streaming cannot silently disappear from a PG18 build. +The source-only guards also compare the Rust host's loaded runtime symbols with +the WASIX runtime export validator and the PG18 PostgreSQL-side +`OLIPHAUNT_WASM_HOST_EXPORT` declarations, so host ABI drift fails before a +build artifact is packaged. + +The same selector is also accepted by `assets release-build`, `assets aot`, +`assets package-aot`, `assets check-aot`, and `assets export-list`, but PG18 +PG18 WASIX is now the default stable runtime. Build-output manifests remain +stamped under the PG18 build tree at +`target/oliphaunt-wasix/wasix-build/build/outputs.json`, with +the old stable manifest path accepted only as a compatibility fallback during +local discovery. + +Portable assets now write to the stable generated directory +`target/oliphaunt-wasix/assets`. AOT intermediates remain under +`target/oliphaunt-wasix/wasix-build/build/aot`, while packaged +AOT outputs write to the stable generated directory +`target/oliphaunt-wasix/aot`. Packaged AOT manifests carry explicit +`source fingerprint`, source-fingerprint, and `postgres-version` metadata, and +`assets check-aot` verifies those fields before checking module hashes. + +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 `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 +PostgreSQL 18 runtime version before embedding. + +Runtime reuse has the same fail-closed stance. A full-local runtime root is +only reused when its saved runtime source key matches the currently embedded +runtime archive key; otherwise the runtime archive is reinstalled before use. +Existing PGDATA roots are also checked against the current runtime PostgreSQL +major version before they are accepted, including overlay PGDATA manifests. A +PG17 root must fail with an explicit migration/separate-root error under the +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 `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 +released PG17 crate payload. + +Perf reports now carry WASIX runtime asset provenance when the measured engine +is the bundled WASIX runtime. The JSON field is `wasixRuntimeAssets` and +records the asset source selection, PostgreSQL version, optional PG18 source +fingerprint, and PGDATA-template lane/fingerprint/version. Native PostgreSQL, +SQLite, native liboliphaunt, and Node Oliphaunt controls omit the field. This +keeps future PG18-versus-released-lane benchmark reports self-identifying even +when both lanes can be built from the same xtask binary. + +`assets check` and `assets verify-committed` now include a source-fingerprint isolation +guard. It proves the released and PG18 build manifests, portable asset +directories, and AOT directories are distinct; it also checks that public +download, local install, and release bundle paths still target the released +PG17.5 lane, and that package-size enforcement does not silently fall back to +the released crate lane for a PG18 release-build. The same guard checks that +PG18 fetch/release-build preflight skips the released backend source pin and +uses the tarball source-prep script instead. PG18-owned source-prep, configure, +and backend Docker scripts are also source-guarded against released checkout +paths, the released `postgres-oliphaunt` source name, the generated PG17 patched +source root, and the old `__OLIPHAUNT__`/`OLIPHAUNT_WASIX_DL` export macro style. +Generated asset manifest +validation also checks the manifest `source fingerprint` field when a lane is selected, +so PG18 packaged assets cannot pass as released-lane assets by version inference +alone. Build-output manifests carry the same PG18 source fingerprint and are +ignored by export-list generation if the fingerprint no longer matches the +current source stack; PG18 build-output manifest module paths must also stay +under the PG18 build root instead of the released build root. + +Promoted extension packaging is also fail-closed. PostgreSQL contrib and PGXS +style extensions are lane-scoped through the selected build directory. The PG18 +source-spine guard verifies that promoted PGXS source directories stay under +shared `target/oliphaunt-sources/checkouts/*` pins instead of the released backend checkout, that +each source directory is represented in `src/sources/third-party/**`, and, when the +checkout is present, that packaging-visible Makefile/control/SQL inputs exist. +PG18 asset manifests derive extension `control-files` from the packaged +extension archive contents, so contrib metadata cannot leak released +`postgres-oliphaunt/contrib` source paths into a PG18 manifest. +PostGIS remains `build = false` until there is a dedicated WASIX geospatial +dependency stack and lane-scoped PostGIS builder. If PostGIS is promoted before +that work lands, xtask must fail early instead of pointing at the released +`postgres-oliphaunt/oliphaunt/other_extensions` artifact tree. + +## Verification Status + +This slice has moved beyond source-only verification. The PG18 source spine, +backend build path, non-timing runtime artifact, AOT/package metadata, and +focused speed probes for the indexed-update misses have all been exercised. +The current perf package is still core-only because external PGXS extensions +were skipped with `OLIPHAUNT_WASM_SKIP_EXTENSIONS_FOR_PERF=1`; full extension +release packaging remains a promotion blocker. + +Verified locally: + +- PostgreSQL 18.4 source preparation reuses or downloads the pinned tarball, + checks the upstream checksum, and applies patches 0001 through 0031 cleanly. + Cached prepared sources are rebuilt if patch backup/reject files are present, + and a prepared tree containing `.orig` or `.rej` artifacts is rejected. The + prepared source and work-root fingerprint markers must also match the current + tarball metadata and patch stack hash. +- The PG18 patch stack is source-guarded for review hygiene: sequential patch + numbering, Oliphaunt subjects matching filenames, maintainer headers, rationale + text, and no TODO/FIXME placeholders. +- The prepared PG18 source contains the standalone `initdb` and `pg_dump` source + files, the released-lane runtime-support inputs for `plpgsql`, `dict_snowball`, + and timezones, plus every promoted contrib source/control input required by + the generated extension build plan. CREATE EXTENSION contrib entries also + have root extension SQL files available for packaging. Unsupported promoted + builders such as PostGIS are rejected until they have a dedicated PG18 WASIX + build path. Promoted PGXS extensions are checked for lane-neutral pinned + source directories and, when local checkouts exist, Makefile/control/SQL + packaging inputs. The PGXS `pg_config_wasix.sh` helper is source-marker aware + and rejects PG18 use without an explicit prepared-source path and source + fingerprint markers. Extension build metadata and extension manifest metadata + both fall back to the generated build plan when upstream Oliphaunt discovery + inputs are absent, so PG18 packaging does not need live released-lane discovery + files just to recover lifecycle/dependency metadata. PG18 extension manifest + control-file metadata is derived from packaged archive contents rather than + released-lane source paths. +- The applied PG18 source contains the expected embedded runtime ABI hooks and + the currently carried performance/tool patches, including hash load folding, + top-XID lookup short-circuiting, btree int4 comparison and delete scratch + paths, and pg_dump helper renaming. +- The source-controlled WASIX export list includes the Rust host's required + lifecycle, protocol, buffered I/O, and hybrid streaming symbols. +- The Rust WASIX host loader, runtime export validator, and PG18 PostgreSQL + host-export patch surface are checked together so required runtime symbols do + not silently drift between layers. +- `assets check` validates the PG18 WASIX source-spine guard and the Rust + startup ABI boundary in + source-only mode. It also runs `bash -n` over the WASIX build shell scripts, + including the PG18 source-prep and backend entrypoint scripts. +- `assets verify-committed` additionally validates the source-fingerprint isolation + guard and the source-controlled WASIX export list. +- Generated asset manifests now carry explicit `source fingerprint` metadata and a + source fingerprint that must match the current PG18 tarball plus patch stack. +- Existing PG18 build trees are accepted only when their stamped source + fingerprint and PostgreSQL version markers match the prepared source. +- Packaged AOT manifests now carry explicit `source fingerprint`, `postgres-version`, + and source-fingerprint metadata. +- PGDATA template manifests and asset-manifest `pgdata-template` entries now + carry lane metadata as well, plus PG18 source fingerprints for experimental + PG18 templates. +- Runtime asset parsing preserves PG18 source fingerprints, and embedded PGDATA + template/AOT manifests are checked against the bundled asset manifest before + use. +- WASIX perf reports include bundled runtime asset provenance, so benchmark JSON + identifies the measured source selection, PostgreSQL version, and PG18 source + fingerprint before the numbers are compared. +- Unit coverage checks that PG18 extension manifests use packaged control files + and reject released-lane path leaks; the legacy PG17 source selection is no longer + selectable. +- The asset crate build script is source-marker aware: selected manifests are + checked against the requested lane before embedding. +- Public release-asset bundling now validates PG18/stable portable and AOT + manifests before writing public release archives. The download/install path + performs the same checks on downloaded portable and AOT manifests before + copying them into canonical generated asset directories. +- `assets build` emits the PG18 lane build + commands without executing them; plain `assets build` selects the same PG18 + stable lane. +- `assets fetch` and release-build `--fetch` + prepare the pinned PG18 tarball source instead of fetching the released + `postgres-oliphaunt` backend checkout. +- `assets source-spine` validates the PG18 + source-spine guard; its `--check-patch-applies` path goes through the PG18 + tarball source-prep script, and `--strict-local` additionally checks shared + non-backend source checkout pins. +- The PG18 source-spine guard rejects released PG17/Oliphaunt checkout markers in + the PG18 patch stack and PG18-owned build scripts. +- `assets source-spine --check-patch-applies` + passes against the prepared PostgreSQL 18.4 source tree in source-only mode. +- `assets source-spine` passes after the + 37-patch stack, including the PG18 source-spine guard and source fingerprint + isolation guard. +- `assets release-build --skip-build --skip-aot + --skip-package-size` regenerates the PG18 asset manifest, and the regenerated + source pins contain the PostgreSQL 18.4 tarball plus PG18 patch-series + fingerprint rather than released-lane backend provenance. +- `assets release-build` carries source-fingerprint + validation through its package-size step; without `--skip-package-size` it now + fails explicitly because PG18 is not staged into publishable crates yet. +- `cargo check -p xtask` passes for the current edited + xtask surface. Earlier `cargo check -p xtask --all-features` coverage passed + for the feature-gated packaging/AOT/perf code paths. +- Full server-SQLx evidence after the accepted `0036` O2 artifact is in + `target/perf/pg18-wasix-core-release-o2-36patch-skip-activity-id-release-host-speed-server-sqlx.json` + plus its two reruns. The three-run median is `0.759x` same-host PG17.5 + geomean and `0.826x` documented PG17.5 geomean; PG18 is faster than both + baselines on every speed-test head. +- Shell syntax, the C bridge/initdb shim ABI harnesses, and the PG18 `wasix-dl` + port header syntax pass local static checks. + +Remaining promotion blockers are full extension/tool release packaging parity +and runtime protocol compatibility for the tokio-postgres prepared server path. +Strict speed-suite parity against both same-host PG17.5 and the documented +PG17.5 release table is now green on the current three-run median. diff --git a/docs/internal/PHYSICAL_ARCHIVE_FORMAT.md b/docs/internal/PHYSICAL_ARCHIVE_FORMAT.md new file mode 100644 index 00000000..17b57d48 --- /dev/null +++ b/docs/internal/PHYSICAL_ARCHIVE_FORMAT.md @@ -0,0 +1,92 @@ +# Physical Archive Format + +This is the maintainer contract for `BackupFormat::PhysicalArchive` in the +native Rust SDK and `liboliphaunt` integration. It is intentionally narrower +than `pg_basebackup` and is versioned as `oliphaunt-physical-archive-v1`. + +## Scope + +Physical archives are same-major PostgreSQL 18 restore artifacts for Oliphaunt +native roots. They are used by direct and broker mode backups and are the only +restore artifact accepted by the native SDK today. SQL backups and the future +`OliphauntArchive` format are separate contracts. + +## Container + +The container is a tar archive. Restore validation accepts GNU or ustar headers +only, verifies header checksums and numeric/string fields, requires a complete +tar terminator, and rejects trailing bytes after the terminator. + +Archive entries may be regular files or directories only. Symlinks, hard links, +FIFOs, device nodes, and all other entry types are rejected. Regular file +entries must not carry link metadata, directory entries must not carry payload +bytes, and duplicate canonical paths are rejected. + +## Paths + +Allowed canonical archive paths are: + +- `pgdata/` and descendants; +- `manifest.properties`; +- `.oliphaunt/backup-manifest.properties`. + +Path canonicalization removes `.` components and rejects absolute paths, parent +directory traversal, Windows prefixes, and entries that would place a file below +an already-seen file or replace an already-seen subtree. + +## Metadata + +`manifest.properties` is the native root manifest. It must validate through the +same parser used for opened native roots. + +`.oliphaunt/backup-manifest.properties` identifies the archive and compatibility +metadata. Required keys are: + +- `archiveLayout=oliphaunt-physical-archive-v1`; +- `product=oliphaunt`; +- `postgresMajor=18`; +- `pgdataVersion`; +- `postgresVersionNum`; +- `serverEncoding`; +- `lcCollate`; +- `lcCtype`; +- `dataChecksums`; +- `sharedPreloadLibraries`; +- `requiredPreloadLibraries`; +- `selectedExtensions`; +- `installedExtensions`. + +`postgresVersionNum` must be a PostgreSQL 18 version number, and `pgdataVersion` +must agree with `pgdata/PG_VERSION` when that file is present. + +## Backup Creation + +Backup creation starts PostgreSQL backup mode, archives `PGDATA`, stops backup +mode, then appends required WAL plus generated `backup_label` and +`tablespace_map` files. The initial `PGDATA` pass skips runtime-local state: +`postmaster.pid`, `postmaster.opts`, `pg_internal.init`, `pgsql_tmp*`, transient +content directories, and `pg_wal` contents before `pg_backup_stop`. + +Metadata may be appended to an existing physical archive. If metadata entries +already exist, they are replaced rather than duplicated. + +## Restore + +Restore unpacks into a staging directory first, validates required PostgreSQL +files, validates archive/root metadata, applies regular-file and directory +permissions, then publishes the staged root according to the selected restore +target policy. Existing targets must be empty unless replacement was explicitly +requested, and restore target paths must not be empty or contain NUL bytes. + +The required restored files are: + +- `pgdata/PG_VERSION`; +- `pgdata/global/pg_control`; +- `pgdata/backup_label`. + +## Verification + +The Rust unit tests under `backup::tests` are the executable contract for this +format. They cover valid annotation/restore behavior and malicious or malformed +tar cases such as traversal, links, duplicate paths, invalid checksums, invalid +numeric fields, truncated terminators, trailing data, and tree-shape conflicts. diff --git a/docs/internal/README.md b/docs/internal/README.md new file mode 100644 index 00000000..71d203bc --- /dev/null +++ b/docs/internal/README.md @@ -0,0 +1,5 @@ +# Internal documents + +Status: archived implementation history and evidence. + +These files record earlier plans, completed work, investigations, and migration details. They are not current maintainer policy. Start with `docs/maintainers/README.md`, then inspect executable configuration and generated catalogs. A current maintainer document may link to a specific internal artifact as supporting evidence; that does not make the rest of the internal document normative. diff --git a/docs/internal/TODO.md b/docs/internal/TODO.md new file mode 100644 index 00000000..98377ba1 --- /dev/null +++ b/docs/internal/TODO.md @@ -0,0 +1,299 @@ +# Historical Native Product Backlog (Non-normative) + +> **Historical research backlog — not a release gate.** This file preserves +> native product investigations and longer-horizon hardening ideas. Its `P0`, +> “remaining work,” acceptance, device-evidence, and production-readiness +> language reflects the track that created each entry; it is not an assertion +> about the current release candidate. Maintainers and agents must use +> [`docs/maintainers/release.md`](../maintainers/release.md), +> [`docs/maintainers/release-setup.md`](../maintainers/release-setup.md), the +> generated target/catalog contracts, and the repository +> [`release-oliphaunt`](../../.codex/skills/release-oliphaunt/SKILL.md) and +> [`qualify-oliphaunt-change`](../../.codex/skills/qualify-oliphaunt-change/SKILL.md) +> skills for current policy and exact-SHA readiness. Re-verify an item against +> the current tree before promoting it into normative maintainer documentation. + +This was the unfinished implementation backlog for the native `liboliphaunt` +and `oliphaunt` product track. Completed historical work belongs in +[DONE.md](DONE.md). + +The product objective for this historical track was native PostgreSQL through +`liboliphaunt`, not the legacy runtime lane. The entries were focused on work +that made the native direct, broker, server, and SDK surfaces more correct, +faster, easier to ship, or easier to validate. + +Historical backlog priorities (not current release severity): + +- `P0`: blocks calling the native product production-ready. +- `P1`: hardening needed for a durable, low-maintenance product. +- `P2`: future capabilities that should not shape the current release contract + until they have measured evidence. + +When reusing an item, move the verified durable result into current normative +documentation rather than treating this file as an active checklist. + +## Product Target + +`liboliphaunt` is the C engine boundary over patched PostgreSQL 18. +`oliphaunt` is the canonical Rust SDK and the shape followed by Swift, +Kotlin, and React Native: + +- `NativeDirect` for lowest-latency embedded use; +- `NativeBroker` for robust desktop apps, multi-root ownership, crash isolation, + recovery, and upgrade orchestration; +- `NativeServer` for true PostgreSQL client compatibility, independent + connections, pools, `psql`, `pg_dump`, SQLx, and other external clients; +- explicit opt-in extensions with static registry support for mobile and a + manifest model that can later carry signed desktop dynamic extension + artifacts; +- benchmarks and release gates against native PostgreSQL and SQLite, using p90 + and p99 latency, throughput, CPU, memory, RSS, child-process RSS, open time, + backup/restore time, and artifact size. + +The native product must not fake semantics for convenience. Direct mode has one +physical backend session. Broker mode provides process isolation and multi-root +supervision. Server mode is the only mode that advertises independent +concurrent PostgreSQL sessions. + +## P0 Native Release Backlog + +### P0-01: Keep The PostgreSQL Patch Stack Minimal And Defensible + +Outcome: `liboliphaunt` patches stay generic, reviewable, and upstreamable. + +Remaining work: + +- Re-audit every PostgreSQL 18 patch after each backend change: + - host I/O vtables; + - embedded entrypoint and lifecycle; + - frontend terminate return path; + - cleanup and current-working-directory restoration; + - static extension loader. +- Keep patch comments and exported symbols generic. They must not mention a + language SDK, product packaging detail, or temporary experiment. + +Acceptance: + +- `src/runtimes/liboliphaunt/native/tools/check-track.sh quick` proves C smoke plus Rust SDK + smoke without rebuilding current artifacts. +- Patch-stack review output is deterministic and checked into release evidence. +- No patch grows product-specific branching that belongs above PostgreSQL. + +### P0-02: Finish Rust SDK Runtime Semantics + +Outcome: the Rust SDK is complete and honest across direct, broker, and server. + +Remaining work: + +- Keep `Oliphaunt` clone semantics explicit: clones share one executor in direct + and broker sessions; server mode exposes a connection string for independent + clients instead of pretending direct mode can pool. + +Acceptance: + +- Rust direct, broker, and server tests cover close, cancel, checkpoint, + transaction pinning, backup, restore, reopen, and external client recovery. +- No direct-mode API advertises independent concurrent sessions. + +### P0-03: Complete SDK Parity For The Public Contract + +Outcome: Rust, Swift, Kotlin, and React Native expose the same product concepts +where the platform can support them. + +Remaining work: + +- Keep Rust classified as an SDK and the canonical product shape. +- Keep React Native as TypeScript and TurboModule glue over Swift and Kotlin; + it must not grow a private database runtime. +- Build Android `NativeBroker` as a remote-process bound service with binder + death/reconnect and WAL-recovery tests. This is the first mobile process + isolation path. +- Run and document an iOS ExtensionFoundation/AppExtensionProcess broker + feasibility track before promising iOS process isolation. Keep iOS direct as + non-isolated unless that track passes real-device lifecycle and App Store + constraints. +- Expand physical iOS benchmark-matrix evidence for the current `liboliphaunt` + XCFramework before choosing mobile defaults. Device and simulator slices now + build and reject forbidden mobile IPC imports, and the physical iPhone + app build/install, crash-recovery verify, full smoke/lifecycle lanes, and + process-memory-capable quick plus full-candidate Safe/Balanced footprint + matrices pass. A Balanced quick tuning slice across + `shared_buffers=8/16/32/64/128MB` and `min_wal_size=8/16/32MB` also passes. + The next iOS evidence steps are Safe coverage for the chosen candidate axes, + `wal_buffers` variation, runtime-footprint profile variation, and then the + full preset physical-device matrix for the selected mobile default. +- Keep `pnpm moon run oliphaunt-swift:smoke` green as the fast + no-artifact gate for PostgreSQL 18 embedded patch portability while the full + iOS simulator/device artifacts are being built. +- Keep the Android Expo installed-app smoke lane reproducible from a checkout + without a committed generated `android/` directory. The local + `Pixel_9_API_34_Google_API` AVD now has benchmark and crash-recovery evidence + when cold-started with software GPU/no snapshot, but physical Android device + evidence and process-memory-capable full candidate/tuning slices are still + needed before release claims. A later local AVD retry killed the app process + before attach/startup and produced no Metro bundle request; treat that as a + harness/device reliability gap, not PostgreSQL tuning evidence. + +Acceptance: + +- `src/runtimes/liboliphaunt/native/tools/check-track.sh sdks` passes on a current native + runtime. +- `pnpm --dir src/sdks/react-native/examples/expo run smoke:android` passes on an Android + emulator/device with current native Android artifacts. +- `pnpm moon run oliphaunt-swift:smoke` passes on macOS with Xcode and + stays warning-clean for the PostgreSQL embedded patch objects. +- `pnpm moon run liboliphaunt-native:build-ios-xcframework` produces current + iOS simulator/device `liboliphaunt.dylib` slices with the public C ABI + symbols. +- `pnpm --dir src/sdks/react-native/examples/expo run smoke:ios` passes on an iOS + simulator/device with current native iOS artifacts. +- Every row in `docs/maintainers/sdk-parity-policy.md` has SDK-specific tests or a documented + product reason for non-parity. + +### P0-04: Finish Extension Release Evidence + +Outcome: extensions are opt-in, size-conscious, and backed by lifecycle +evidence across direct, broker, server, and mobile static registry packaging. + +Remaining work: + +- Keep exact PostgreSQL extension names as the only public selection primitive; + do not introduce first-party selection aliases. +- Prove the native exact-extension artifact matrix is green for every published + native runtime target: `macos-arm64`, `linux-x64-gnu`, `linux-arm64-gnu`, + `windows-x64-msvc`, `ios-xcframework`, `android-arm64-v8a`, and + `android-x86_64`. The package graph must not relabel one target's artifacts + as cross-platform evidence; each builder must emit product-versioned + extension release assets, target metadata, and smoke evidence for the target + it built. +- Keep pgGraph and ParadeDB `pg_search` as explicit external extension + candidates with pinned source, license, build fingerprint, preload metadata, + and smoke evidence. + +Acceptance: + +- `src/runtimes/liboliphaunt/native/tools/check-track.sh extensions` passes with first-party + extension artifacts. +- `extension-packages:assemble-release` receives native extension artifacts for every + published native runtime target and WASIX extension artifacts for every + published WASIX target. +- `extension-packages:assemble-mobile` receives only the Android/iOS native + extension artifacts needed by focused mobile installed-app builders and does + not force WASIX or desktop extension builders into mobile E2E runs. +- External extensions pass the opt-in external pgrx lane before they are + advertised as shippable. +- Mobile package checks reject module-backed extensions unless a complete static + registry is present. + +### P0-05: Make Benchmarks Release-Grade + +Outcome: native release claims are backed by reproducible benchmark reports that +can be compared against native PostgreSQL and SQLite. + +Remaining work: + +- Keep the native matrix native-only and no-build by default when artifacts are + current. +- Keep the verified source-current full report fresh after benchmark harness, + Rust SDK, or liboliphaunt runtime input changes. Latest complete verified + baseline before the backup ABI/tar-writer updates: + `target/perf/native-liboliphaunt-20260524T090412Z/report.md`. A new full + matrix is required before current-checkout release claims. +- Investigate measured NativeDirect misses from the current report: + speed-suite p90 (`1.103x` native PostgreSQL), speed tail throughput (`0.907x` + native PostgreSQL), physical backup/restore total p90 (`1.622x` native + PostgreSQL physical), physical backup throughput (`0.617x` native PostgreSQL + physical), and speed cases `1`, `2.1`, `3`, `4`, `10`, and `13`, which + reproduced in + `target/perf/native-speed-diagnostics-20260524T090412Z-speed-misses/summary.md`. + Cases `2`, `3.1`, and `5` missed in the full matrix but did not reproduce + above tolerance in isolated fresh-process diagnostics. +- Keep investigating the current-source focused backup miss from + `target/perf/native-liboliphaunt-20260524Tbackup-final-direct/report.md`: + direct physical backup/restore p90 is `0.534 s` versus native PostgreSQL + physical at `0.324 s`. `OLIPHAUNT_TRACE_BACKUP=1` shows the remaining direct + cost is concentrated in `pg_backup_start` and PGDATA archiving after metadata + append/copy overhead was removed from the hot path. +- Expand the release matrix beyond the current verified coverage where still + missing: extended-query RTT as its own lane, typed query helper overhead, + transaction throughput, dedicated bulk load variants, and cold/warm open + repeat rows. +- Keep benchmark quality rules and NativeDirect regression diagnostics green as + the suite changes. + +Acceptance: + +- `tools/perf/check-native-perf-harness.sh` passes. +- A complete provenance-verified report exists for direct, broker, server, + native PostgreSQL, and SQLite. +- `src/docs/content/reference/performance.md` is updated only from verified output. + +## P1 Product Hardening Backlog + +### P1-01: Improve Repository And Release Organization + +Outcome: `liboliphaunt`, `oliphaunt`, Swift, Kotlin, and React Native remain +separate products with clean ownership. + +Remaining work: + +- Keep native C sources under `src/runtimes/liboliphaunt/native/`. +- Keep the Rust SDK under `src/sdks/rust/`. +- Keep platform SDKs under `src/sdks/swift`, `src/sdks/kotlin`, and `src/sdks/react-native`. +- Keep legacy package code from becoming a dependency of native product checks. + +### P1-02: Harden Storage And Backup + +Outcome: directory storage feels ergonomic without hiding PostgreSQL realities. + +Remaining work: + +- Add multi-version upgrade policy fixtures once a second root schema or + PostgreSQL major exists. +- Add restore upgrade choreography once archive metadata has more than one + supported PostgreSQL major or archive layout. +- Add import/export documentation for desktop and mobile apps. + +### P1-03: Reduce Open-Time And Steady-State Overhead + +Outcome: native direct is not slower than the native PostgreSQL control for SDK +traffic after accounting for process model differences. + +Remaining work: + +- Profile direct-mode copies across C ABI, Rust protocol buffers, Swift `Data`, + Kotlin `ByteArray`, JNI byte arrays, and React Native JSI ArrayBuffer + transport. +- Add zero-copy or single-copy transport paths where the platform API supports + them. +- Investigate warm template cache behavior and file-copy strategy per platform. +- Keep any startup GUC tuning explicit, documented, and safe for persistent + roots. + +### P1-04: Strengthen Developer Experience + +Outcome: app developers can adopt native Oliphaunt with predictable packaging and +idiomatic APIs. + +Remaining work: + +- Add guided quickstarts for Rust/Tauri, Swift iOS, Swift macOS, Android, and + React Native. +- Add troubleshooting docs for root locks, missing runtime resources, missing + static registry entries, preload-required extensions, and benchmark misses. +- Add example apps that exercise open, query, transaction, extension, backup, + restore, and close. + +## P2 Future Capabilities + +These are not release blockers until they have evidence and a crisp product +boundary. + +- Signed dynamic desktop extensions. +- Out-of-process broker pools for multiple active backend workers per root. +- Mobile broker/server adapters when platform constraints and app-store rules + are fully understood. +- Live-query APIs designed as native Rust/Swift/Kotlin/TypeScript surfaces + rather than a compatibility shim. +- Cross-language generated bindings after the Rust, Swift, Kotlin, and React + Native SDK shapes are stable. diff --git a/docs/internal/WASIX_PATCH_STACK.md b/docs/internal/WASIX_PATCH_STACK.md new file mode 100644 index 00000000..7aad3911 --- /dev/null +++ b/docs/internal/WASIX_PATCH_STACK.md @@ -0,0 +1,186 @@ + +# oliphaunt-wasix PostgreSQL 18 WASIX Patch Stack Review + +This source-only review artifact keeps the WASIX PostgreSQL patch stack deterministic and reviewable without rebuilding PostgreSQL. + +Regenerate with: + +```sh +src/runtimes/liboliphaunt/wasix/tools/check-patch-stack.mjs --write +``` + +## Source Pin + +- PostgreSQL: `18.4` +- URL: `https://ftp.postgresql.org/pub/source/v18.4/postgresql-18.4.tar.bz2` +- SHA-256: `81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094` +- Patch directory: `src/runtimes/liboliphaunt/wasix/assets/build/postgres/patches` +- Experiment disposition policy: `do-not-port-experiment-patches-without-a-recorded-wasix-runtime-rationale` + +## Patch Series + +| Order | Patch | Author | Subject | +| --- | --- | --- | --- | +| 1 | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add wasix-dl build spine | +| 2 | `0002-oliphaunt-wasix-add-backend-host-io-hooks.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add backend host I/O hooks | +| 3 | `0003-oliphaunt-wasix-export-startup-packet-parser.patch` | Oliphaunt Maintainers | oliphaunt-wasix: export startup packet parser | +| 4 | `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add host lifecycle exports | +| 5 | `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add loop-pumped protocol exports | +| 6 | `0006-oliphaunt-wasix-report-copy-protocol-state.patch` | Oliphaunt Maintainers | oliphaunt-wasix: report COPY protocol state | +| 7 | `0007-oliphaunt-wasix-add-wasix-pgxs-side-module-support.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add wasix PGXS side-module support | +| 8 | `0008-oliphaunt-wasix-reset-copy-state-on-error-recovery.patch` | Oliphaunt Maintainers | oliphaunt-wasix: reset copy state on error recovery | +| 9 | `0009-oliphaunt-wasix-route-process-identity-through-port.patch` | Oliphaunt Maintainers | oliphaunt-wasix: route process identity through port | +| 10 | `0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch` | Oliphaunt Maintainers | oliphaunt-wasix: route sysv shmem through port | +| 11 | `0011-oliphaunt-wasix-prefer-posix-semaphores.patch` | Oliphaunt Maintainers | oliphaunt-wasix: prefer POSIX semaphores | +| 12 | `0012-oliphaunt-wasix-capture-startup-errors.patch` | Oliphaunt Maintainers | oliphaunt-wasix: capture startup errors | +| 13 | `0013-oliphaunt-wasix-fail-active-portals-on-host-recovery.patch` | Oliphaunt Maintainers | oliphaunt-wasix: fail active portals on host recovery | +| 14 | `0014-oliphaunt-wasix-speed-up-hash-bytes-unaligned-loads.patch` | Oliphaunt Maintainers | oliphaunt-wasix: speed up hash_bytes unaligned loads | +| 15 | `0015-oliphaunt-wasix-add-top-xid-current-transaction-fast-path.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add top xid current transaction fast path | +| 16 | `0016-oliphaunt-wasix-add-btree-int4-compare-fast-path.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add btree int4 compare fast path | +| 17 | `0017-oliphaunt-wasix-keep-btree-delete-scratch-on-stack.patch` | Oliphaunt Maintainers | oliphaunt-wasix: keep btree delete scratch on stack | +| 18 | `0018-oliphaunt-wasix-avoid-pg-dump-executequery-lto-collision.patch` | Oliphaunt Maintainers | oliphaunt-wasix: avoid pg_dump executeQuery LTO collision | +| 19 | `0019-oliphaunt-wasix-schedule-ready-after-host-recovery.patch` | Oliphaunt Maintainers | oliphaunt-wasix: schedule ready after host recovery | +| 20 | `0020-oliphaunt-wasix-rearm-exception-stack-after-host-recovery.patch` | Oliphaunt Maintainers | oliphaunt-wasix: rearm exception stack after host recovery | +| 21 | `0021-oliphaunt-wasix-declare-wasix-fork.patch` | Oliphaunt Maintainers | oliphaunt-wasix: stub fork_process in embedded WASIX runtime | +| 22 | `0022-oliphaunt-wasix-use-wasm-ld-for-backend-core.patch` | Oliphaunt Maintainers | oliphaunt-wasix: use wasm-ld for backend core | +| 23 | `0023-oliphaunt-wasix-skip-data-dir-ownership-check-under-embedded-wasix.patch` | Oliphaunt Maintainers | oliphaunt-wasix: skip data-dir ownership check under embedded WASIX | +| 24 | `0024-oliphaunt-wasix-add-like-literal-substring-fast-path.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add like literal substring fast path | +| 25 | `0025-oliphaunt-wasix-add-simple-query-backend-timing-probes.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add simple-query backend timing probes | +| 26 | `0026-oliphaunt-wasix-add-executor-storage-backend-timing-probes.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add executor storage backend timing probes | +| 27 | `0027-oliphaunt-wasix-add-btree-insert-backend-timing-probes.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add btree insert backend timing probes | +| 28 | `0028-oliphaunt-wasix-add-btree-search-compare-timing-probes.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add btree search compare timing probes | +| 29 | `0029-oliphaunt-wasix-stub-pg-dump-parallel-fork.patch` | Oliphaunt Maintainers | oliphaunt-wasix: stub pg_dump parallel fork | +| 30 | `0030-oliphaunt-wasix-add-first-int4-leaf-compare-fast-path.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add first int4 leaf compare fast path | +| 31 | `0031-oliphaunt-wasix-add-heap-update-backend-timing-probes.patch` | Oliphaunt Maintainers | oliphaunt-wasix: add heap update backend timing probes | +| 32 | `0032-oliphaunt-wasix-avoid-xlog-size-checkpoint-requests.patch` | Oliphaunt Maintainers | oliphaunt-wasix: avoid XLog-size checkpoint requests | +| 33 | `0033-oliphaunt-wasix-use-lightweight-embedded-runtime-paths.patch` | Oliphaunt Maintainers | oliphaunt-wasix: use lightweight embedded runtime paths | +| 34 | `0034-oliphaunt-wasix-set-embedded-postmaster-environment.patch` | Oliphaunt Maintainers | oliphaunt-wasix: set embedded postmaster environment | +| 35 | `0035-oliphaunt-wasix-avoid-xlogwrite-prevseg-division.patch` | Oliphaunt Maintainers | oliphaunt-wasix: avoid xlogwrite prevseg division | +| 36 | `0036-oliphaunt-wasix-skip-activity-id-reporting.patch` | Oliphaunt Maintainers | oliphaunt-wasix: skip activity id reporting | +| 37 | `0037-oliphaunt-wasix-treat-directory-fsync-eisdir-as-unsupported.patch` | Oliphaunt Maintainers | oliphaunt-wasix: treat directory fsync EISDIR as unsupported | +| 38 | `0038-oliphaunt-wasix-skip-icu-collation-setup-without-icu-data.patch` | Oliphaunt Maintainers | oliphaunt-wasix: skip ICU collation setup without ICU data | + +## Changed Upstream Files + +| File | Owning Patch(es) | Rationale | +| --- | --- | --- | +| `src/Makefile.shlib` | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch` | Defines the WASIX dynamic-link shared-library shape. | +| `src/backend/Makefile` | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch`, `0022-oliphaunt-wasix-use-wasm-ld-for-backend-core.patch` | Builds the dynamic-main backend module without changing other ports. | +| `src/backend/access/heap/heapam.c` | `0031-oliphaunt-wasix-add-heap-update-backend-timing-probes.patch` | Adds embedded timing probes and heap fast-path scope. | +| `src/backend/access/heap/heapam_handler.c` | `0026-oliphaunt-wasix-add-executor-storage-backend-timing-probes.patch` | Keeps embedded heap update timing observable. | +| `src/backend/access/nbtree/nbtdedup.c` | `0017-oliphaunt-wasix-keep-btree-delete-scratch-on-stack.patch` | Keeps btree delete scratch storage on stack under embedded WASIX. | +| `src/backend/access/nbtree/nbtinsert.c` | `0017-oliphaunt-wasix-keep-btree-delete-scratch-on-stack.patch`, `0026-oliphaunt-wasix-add-executor-storage-backend-timing-probes.patch`, `0027-oliphaunt-wasix-add-btree-insert-backend-timing-probes.patch` | Adds embedded btree insert timing and int4 fast-path scope. | +| `src/backend/access/nbtree/nbtsearch.c` | `0016-oliphaunt-wasix-add-btree-int4-compare-fast-path.patch`, `0028-oliphaunt-wasix-add-btree-search-compare-timing-probes.patch`, `0030-oliphaunt-wasix-add-first-int4-leaf-compare-fast-path.patch` | Adds embedded btree search timing and guarded int4 leaf fast paths. | +| `src/backend/access/transam/xact.c` | `0015-oliphaunt-wasix-add-top-xid-current-transaction-fast-path.patch` | Adds top-level current-transaction shortcut for embedded WASIX. | +| `src/backend/access/transam/xlog.c` | `0026-oliphaunt-wasix-add-executor-storage-backend-timing-probes.patch`, `0032-oliphaunt-wasix-avoid-xlog-size-checkpoint-requests.patch`, `0035-oliphaunt-wasix-avoid-xlogwrite-prevseg-division.patch` | Avoids expensive segment division under embedded WASIX. | +| `src/backend/commands/copyfromparse.c` | `0006-oliphaunt-wasix-report-copy-protocol-state.patch` | Reports COPY protocol state to the host. | +| `src/backend/commands/copyto.c` | `0006-oliphaunt-wasix-report-copy-protocol-state.patch` | Reports COPY protocol state to the host. | +| `src/backend/libpq/be-secure.c` | `0002-oliphaunt-wasix-add-backend-host-io-hooks.patch` | Routes embedded protocol reads and writes through host-owned callbacks. | +| `src/backend/libpq/pqcomm.c` | `0002-oliphaunt-wasix-add-backend-host-io-hooks.patch` | Skips unavailable postmaster-death wait handles in embedded WASIX. | +| `src/backend/optimizer/plan/planner.c` | `0036-oliphaunt-wasix-skip-activity-id-reporting.patch` | Suppresses activity identifier reporting in embedded WASIX. | +| `src/backend/port/posix_sema.c` | `0033-oliphaunt-wasix-use-lightweight-embedded-runtime-paths.patch` | Uses POSIX semaphore behavior selected by the WASIX template. | +| `src/backend/postmaster/checkpointer.c` | `0032-oliphaunt-wasix-avoid-xlog-size-checkpoint-requests.patch` | Keeps checkpoint requests local to embedded WASIX. | +| `src/backend/postmaster/fork_process.c` | `0021-oliphaunt-wasix-declare-wasix-fork.patch` | Declares the WASIX fork boundary without enabling postmaster concurrency. | +| `src/backend/replication/walsender.c` | `0006-oliphaunt-wasix-report-copy-protocol-state.patch` | Suppresses activity identifier reporting in embedded WASIX. | +| `src/backend/storage/file/fd.c` | `0037-oliphaunt-wasix-treat-directory-fsync-eisdir-as-unsupported.patch` | Treats data-directory ownership and directory sync as WASIX platform boundaries. | +| `src/backend/tcop/backend_startup.c` | `0003-oliphaunt-wasix-export-startup-packet-parser.patch` | Exports the startup packet parser for host-driven startup. | +| `src/backend/tcop/postgres.c` | `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`, `0008-oliphaunt-wasix-reset-copy-state-on-error-recovery.patch`, `0012-oliphaunt-wasix-capture-startup-errors.patch`, `0019-oliphaunt-wasix-schedule-ready-after-host-recovery.patch`, `0020-oliphaunt-wasix-rearm-exception-stack-after-host-recovery.patch`, `0025-oliphaunt-wasix-add-simple-query-backend-timing-probes.patch`, `0034-oliphaunt-wasix-set-embedded-postmaster-environment.patch`, `0036-oliphaunt-wasix-skip-activity-id-reporting.patch` | Owns embedded lifecycle, protocol loop, error recovery, and timing hooks. | +| `src/backend/utils/adt/like.c` | `0024-oliphaunt-wasix-add-like-literal-substring-fast-path.patch` | Adds guarded LIKE literal fast path for embedded WASIX. | +| `src/backend/utils/adt/like_match.c` | `0024-oliphaunt-wasix-add-like-literal-substring-fast-path.patch` | Adds guarded LIKE literal fast path for embedded WASIX. | +| `src/backend/utils/init/miscinit.c` | `0023-oliphaunt-wasix-skip-data-dir-ownership-check-under-embedded-wasix.patch` | Routes process identity through the WASIX port layer. | +| `src/backend/utils/init/postinit.c` | `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch` | Skips data-directory ownership checks under embedded WASIX. | +| `src/backend/utils/misc/guc.c` | `0033-oliphaunt-wasix-use-lightweight-embedded-runtime-paths.patch` | Uses the embedded WASIX postmaster-style environment. | +| `src/backend/utils/mmgr/portalmem.c` | `0013-oliphaunt-wasix-fail-active-portals-on-host-recovery.patch` | Fails active portals on host-forced recovery. | +| `src/bin/initdb/initdb.c` | `0038-oliphaunt-wasix-skip-icu-collation-setup-without-icu-data.patch` | Skips ICU collation setup when optional ICU data is absent during WASIX initdb bootstrap. | +| `src/bin/pg_dump/connectdb.c` | `0018-oliphaunt-wasix-avoid-pg-dump-executequery-lto-collision.patch` | Avoids pg_dump LTO symbol collisions. | +| `src/bin/pg_dump/connectdb.h` | `0018-oliphaunt-wasix-avoid-pg-dump-executequery-lto-collision.patch` | Avoids pg_dump LTO symbol collisions. | +| `src/bin/pg_dump/parallel.c` | `0029-oliphaunt-wasix-stub-pg-dump-parallel-fork.patch` | Stubs unavailable pg_dump parallel fork behavior under WASIX. | +| `src/bin/pg_dump/pg_dumpall.c` | `0018-oliphaunt-wasix-avoid-pg-dump-executequery-lto-collision.patch` | Avoids pg_dump LTO symbol collisions. | +| `src/common/file_utils.c` | `0037-oliphaunt-wasix-treat-directory-fsync-eisdir-as-unsupported.patch` | Treats EISDIR directory fsync as unsupported on WASIX. | +| `src/common/hashfn.c` | `0014-oliphaunt-wasix-speed-up-hash-bytes-unaligned-loads.patch` | Uses defined unaligned load fast path under WASIX. | +| `src/include/libpq/libpq-be.h` | `0002-oliphaunt-wasix-add-backend-host-io-hooks.patch` | Adds the host I/O callback table to Port only for embedded WASIX. | +| `src/include/port/wasix-dl.h` | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch`, `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`, `0006-oliphaunt-wasix-report-copy-protocol-state.patch`, `0009-oliphaunt-wasix-route-process-identity-through-port.patch`, `0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`, `0012-oliphaunt-wasix-capture-startup-errors.patch`, `0013-oliphaunt-wasix-fail-active-portals-on-host-recovery.patch`, `0025-oliphaunt-wasix-add-simple-query-backend-timing-probes.patch`, `0026-oliphaunt-wasix-add-executor-storage-backend-timing-probes.patch`, `0027-oliphaunt-wasix-add-btree-insert-backend-timing-probes.patch`, `0028-oliphaunt-wasix-add-btree-search-compare-timing-probes.patch`, `0031-oliphaunt-wasix-add-heap-update-backend-timing-probes.patch` | Defines the embedded WASIX port header and ABI redirects. | +| `src/include/port/wasix-dl/sys/ipc.h` | `0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch` | Provides the WASIX SysV IPC shim surface. | +| `src/include/port/wasix-dl/sys/shm.h` | `0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch` | Provides the WASIX SysV shared-memory shim surface. | +| `src/makefiles/Makefile.wasix-dl` | `0007-oliphaunt-wasix-add-wasix-pgxs-side-module-support.patch` | Builds side modules and PGXS artifacts for WASIX dynamic linking. | +| `src/makefiles/pgxs.mk` | `0007-oliphaunt-wasix-add-wasix-pgxs-side-module-support.patch` | Installs PGXS extension artifacts for WASIX packaging. | +| `src/template/wasix-dl` | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch`, `0011-oliphaunt-wasix-prefer-posix-semaphores.patch` | Keeps the WASIX template and atomics invariants source-controlled. | + +## Audit Checklist + +| Requirement | Owning Patch(es) | Required Evidence | Review Posture | +| --- | --- | --- | --- | +| WASIX dynamic-main build spine is isolated | `0001-oliphaunt-wasix-add-wasix-dl-build-spine.patch` | `PORTNAME), wasix-dl`, `oliphaunt: $(OBJS)` | Build plumbing lands before lifecycle behavior, so linker changes are reviewable alone. | +| Backend protocol I/O is host-owned without touching normal sockets | `0002-oliphaunt-wasix-add-backend-host-io-hooks.patch` | `OliphauntWasmHostIO`, `secure_raw_read`, `secure_raw_write` | Only OLIPHAUNT_WASM_SINGLE_USER installs the callback table. | +| Startup packet parsing remains PostgreSQL-owned | `0003-oliphaunt-wasix-export-startup-packet-parser.patch` | `ProcessStartupPacket`, `OLIPHAUNT_WASM_HOST_EXPORT("ProcessStartupPacket")` | The host can call the parser, but PostgreSQL still validates the startup packet. | +| Host lifecycle exports stay explicit | `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch` | `oliphaunt_wasix_start`, `oliphaunt_wasix_pq_flush`, `oliphaunt_wasix_get_proc_port` | Host-visible entry points are named exports instead of broad syscall remaps. | +| Protocol loop recovery remains at the PostgresMain boundary | `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`, `0019-oliphaunt-wasix-schedule-ready-after-host-recovery.patch`, `0020-oliphaunt-wasix-rearm-exception-stack-after-host-recovery.patch` | `PostgresMainLoopOnce`, `PostgresMainLongJmp`, `send_ready_for_query = true` | The host pumps PostgreSQL one loop at a time and recovery re-enters the upstream exception stack. | +| COPY protocol state is host-observable | `0006-oliphaunt-wasix-report-copy-protocol-state.patch`, `0008-oliphaunt-wasix-reset-copy-state-on-error-recovery.patch` | `oliphaunt_wasix_protocol_report_copy_response`, `OLIPHAUNT_WASIX_PROTOCOL_COPY_NONE` | COPY state is reported and cleared around PostgreSQL error recovery. | +| PGXS side modules use the WASIX dynamic-link contract | `0007-oliphaunt-wasix-add-wasix-pgxs-side-module-support.patch`, `0022-oliphaunt-wasix-use-wasm-ld-for-backend-core.patch` | `PGXS`, `WASM_LD ?= $(shell $(CC) -print-prog-name=wasm-ld)` | Extension and backend side-module behavior is source-reviewed with the linker path. | +| Process identity and shared memory stay behind the port header | `0009-oliphaunt-wasix-route-process-identity-through-port.patch`, `0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`, `0011-oliphaunt-wasix-prefer-posix-semaphores.patch` | `oliphaunt_wasix_geteuid`, `oliphaunt_wasix_shmget`, `PREFERRED_SEMAPHORES=UNNAMED_POSIX` | WASIX platform gaps are explicit port-layer dependencies, not scattered runtime guesses. | +| Tool/runtime platform stubs fail closed | `0021-oliphaunt-wasix-declare-wasix-fork.patch`, `0029-oliphaunt-wasix-stub-pg-dump-parallel-fork.patch`, `0037-oliphaunt-wasix-treat-directory-fsync-eisdir-as-unsupported.patch` | `fork_process`, `oliphaunt_wasix_pgdump_fork`, `errno == EISDIR` | Unavailable WASIX behavior is explicit and narrow instead of silently emulated. | +| Optional ICU data stays optional during initdb | `0038-oliphaunt-wasix-skip-icu-collation-setup-without-icu-data.patch` | `getenv("ICU_DATA")`, `pg_collation_actual_version`, `pg_import_system_collations` | WASIX initdb skips ICU-backed collation setup until the optional ICU data package is present. | + +## PostgreSQL Patch Symbols + +- `OLIPHAUNT_WASM_EXIT_ALIVE` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`) +- `OLIPHAUNT_WASM_HOST_EXPORT` (`0003-oliphaunt-wasix-export-startup-packet-parser.patch`, `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`) +- `OLIPHAUNT_WASM_SINGLE_USER` (`0002-oliphaunt-wasix-add-backend-host-io-hooks.patch`, `0003-oliphaunt-wasix-export-startup-packet-parser.patch`, `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`, `0006-oliphaunt-wasix-report-copy-protocol-state.patch`, `0008-oliphaunt-wasix-reset-copy-state-on-error-recovery.patch`, `0012-oliphaunt-wasix-capture-startup-errors.patch`, `0013-oliphaunt-wasix-fail-active-portals-on-host-recovery.patch`, `0016-oliphaunt-wasix-add-btree-int4-compare-fast-path.patch`, `0017-oliphaunt-wasix-keep-btree-delete-scratch-on-stack.patch`, `0019-oliphaunt-wasix-schedule-ready-after-host-recovery.patch`, `0021-oliphaunt-wasix-declare-wasix-fork.patch`, `0023-oliphaunt-wasix-skip-data-dir-ownership-check-under-embedded-wasix.patch`, `0029-oliphaunt-wasix-stub-pg-dump-parallel-fork.patch`, `0030-oliphaunt-wasix-add-first-int4-leaf-compare-fast-path.patch`, `0032-oliphaunt-wasix-avoid-xlog-size-checkpoint-requests.patch`, `0033-oliphaunt-wasix-use-lightweight-embedded-runtime-paths.patch`, `0036-oliphaunt-wasix-skip-activity-id-reporting.patch`) +- `PostgresMainLongJmp` (`0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`) +- `PostgresMainLoopOnce` (`0005-oliphaunt-wasix-add-loop-pumped-protocol-exports.patch`) +- `ProcessStartupPacket` (`0003-oliphaunt-wasix-export-startup-packet-parser.patch`) +- `oliphaunt_wasix_backend_timing_end` (`0025-oliphaunt-wasix-add-simple-query-backend-timing-probes.patch`) +- `oliphaunt_wasix_backend_timing_start` (`0025-oliphaunt-wasix-add-simple-query-backend-timing-probes.patch`) +- `oliphaunt_wasix_begin_startup_error_capture` (`0012-oliphaunt-wasix-capture-startup-errors.patch`) +- `oliphaunt_wasix_end_startup_error_capture` (`0012-oliphaunt-wasix-capture-startup-errors.patch`) +- `oliphaunt_wasix_get_proc_port` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_getegid` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_geteuid` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_getgid` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_getpwuid` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_getpwuid_r` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_getuid` (`0009-oliphaunt-wasix-route-process-identity-through-port.patch`) +- `oliphaunt_wasix_hash_load32` (`0014-oliphaunt-wasix-speed-up-hash-bytes-unaligned-loads.patch`) +- `oliphaunt_wasix_host_read` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_host_write` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_init_protocol_port` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`, `0012-oliphaunt-wasix-capture-startup-errors.patch`) +- `oliphaunt_wasix_io` (`0002-oliphaunt-wasix-add-backend-host-io-hooks.patch`, `0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_pgdump_fork` (`0029-oliphaunt-wasix-stub-pg-dump-parallel-fork.patch`) +- `oliphaunt_wasix_pq_flush` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_process_startup_options` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_protocol_io` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_protocol_report_copy_response` (`0006-oliphaunt-wasix-report-copy-protocol-state.patch`, `0008-oliphaunt-wasix-reset-copy-state-on-error-recovery.patch`) +- `oliphaunt_wasix_send_conn_data` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_shmat` (`0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`) +- `oliphaunt_wasix_shmctl` (`0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`) +- `oliphaunt_wasix_shmdt` (`0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`) +- `oliphaunt_wasix_shmget` (`0010-oliphaunt-wasix-route-sysv-shmem-through-port.patch`) +- `oliphaunt_wasix_start` (`0004-oliphaunt-wasix-add-host-lifecycle-exports.patch`) +- `oliphaunt_wasix_startup_error_capture_active` (`0012-oliphaunt-wasix-capture-startup-errors.patch`) +- `oliphaunt_wasix_startup_error_saved_dest` (`0012-oliphaunt-wasix-capture-startup-errors.patch`) + +## Experiment Patch Disposition + +| Experiment Patch | Status | WASIX Runtime Decision | Rationale | +| --- | --- | --- | --- | +| `0001-wasix-use-posix-dsm-not-sysv.patch` | `not-carried` | replaced where relevant by 0010 and 0011 | The experiment patch changes full-concurrent PostgreSQL dynamic shared memory selection. The embedded WASIX runtime does not take postmaster DSM as a product constraint; it instead routes SysV shmem through the wasix-dl port header and explicitly selects POSIX semaphores. | +| `0003-wasix-libpq-static-encoding-shim.patch` | `covered-by-build-spine` | covered by the WASIX bridge aliases and standalone pg_dump build path | The released-lane bridge already provides weak pg_char_to_encoding and pg_encoding_to_char aliases for static pg_dump/libpq linkage. No PG18 source patch is needed unless a future configured build proves a tool-specific gap. | +| `0004-wasix-core-execbackend-initdb-runtime.patch` | `not-carried` | full-concurrent runtime blocker patch, not embedded product shape | The patch addresses EXEC_BACKEND, fork/exec, root checks, locale command probing, and directory fsync behavior for proper concurrent PostgreSQL under WASIX. The embedded WASIX runtime avoids the postmaster/fork lifecycle; any tool blocker found later should become a smaller tool-specific patch. | +| `0005-pg-dump-avoid-lto-executequery-collision.patch` | `ported` | ported as 0018-oliphaunt-wasix-avoid-pg-dump-executequery-lto-collision.patch | This is a narrow pg_dump source hygiene patch that supports standalone WASIX tool builds under thin LTO without changing query behavior. | +| `0006-like-literal-substring-fast-path.patch` | `ported-with-tighter-guards` | ported as 0024-oliphaunt-wasix-add-like-literal-substring-fast-path.patch | The PG18 patch narrows the experiment shortcut to deterministic, case-sensitive LIKE matching for the simple %literal% shape and rejects escapes, _, inner %, lower/case-insensitive variants, and nondeterministic collations before using memchr/memcmp. | +| `0007-top-xid-current-transaction-fast-path.patch` | `ported` | ported as 0015-oliphaunt-wasix-add-top-xid-current-transaction-fast-path.patch | The final PG18 patch keeps the upstream parallel and subtransaction paths and only short-circuits the ordinary top-level case after all alternate current-XID sources are absent. | +| `0008-btree-int4-compare-fast-path.patch` | `ported-with-tighter-guards` | ported as 0016-oliphaunt-wasix-add-btree-int4-compare-fast-path.patch | The PG18 patch keeps index_getattr(), requires the built-in integer btree family, int4 input type, int4 or InvalidOid subtype, and InvalidOid collation. It does not assume every int4 opclass has the built-in ordering. | +| `0009-btree-delete-stack-state.patch` | `ported-with-single-user-gate` | ported as 0017-oliphaunt-wasix-keep-btree-delete-scratch-on-stack.patch | The PG18 patch is restricted to __wasi__ and OLIPHAUNT_WASM_SINGLE_USER. It keeps deletion policy and tableam behavior unchanged while avoiding page-local allocator churn. | +| `0010-btree-bottomup-delete-runtime-toggle.patch` | `rejected-for-default-lane` | diagnostic toggle was tested with Oliphaunt env names but not kept in the patch stack | Disabling bottom-up deletion changes PostgreSQL's index maintenance behavior. A local PG18 WASIX port of the diagnostic hook made the default release-profile 9/10 run much slower before any override was enabled, so it is not a defensible carried patch. | +| `0011-btree-first-int4-compare-fast-path.patch` | `ported-with-tighter-guards` | ported as 0030-oliphaunt-wasix-add-first-int4-leaf-compare-fast-path.patch | The PG18 patch keeps the direct tuple-data read only for embedded WASIX, leaf pages, one-key non-null non-posting non-pivot tuples, built-in int4 btree family, int4 input type, InvalidOid collation, and non-DESC scan keys. Equal values still fall through to PostgreSQL's existing heap-TID and truncated-key tie-break logic. | +| `0012-hash-bytes-unaligned-load-fast-path.patch` | `ported` | ported as 0014-oliphaunt-wasix-speed-up-hash-bytes-unaligned-loads.patch | The PG18 patch uses memcpy-based little-endian 32-bit loads under __wasi__ only, preserving defined C behavior while giving LLVM a wasm-load lowering opportunity. | + +## Guardrails + +- `source.toml` patch series exactly matches the patch directory. +- Every patch has a deterministic `From: Oliphaunt Maintainers ` header. +- Every patch has a deterministic `Subject: [PATCH] oliphaunt-wasix: ...` header and a rationale before the diff. +- Added PostgreSQL lines are checked for trailing whitespace and space-before-tab indentation. +- Changed upstream files must exactly match the expected touchpoint table above; new upstream touchpoints need an explicit rationale before landing. +- Required audit checks prove their evidence in the named owning patch or patches. +- Experiment patches can only be ported, rejected, or replaced with a recorded WASIX runtime decision and rationale. diff --git a/docs/maintainers/README.md b/docs/maintainers/README.md new file mode 100644 index 00000000..cd5f81a8 --- /dev/null +++ b/docs/maintainers/README.md @@ -0,0 +1,37 @@ +# Maintainer documentation + +Status: index. Last verified: 2026-07-16. Owner: repository maintainers. + +Executable configuration is authoritative. Documentation explains intent and operation; it must not invent workflow names, package identities, targets, or release state. When prose conflicts with the sources below, fix the prose in the same change. + +| Topic | Maintainer entry point | Executable source | +| --- | --- | --- | +| Release products, versions, tags, and recovery | `release.md` | `release-please-config.json`, `.release-please-manifest.json`, product `release.toml`, `tools/release/publication-catalog.mjs`, `.github/scripts/manage-release-drafts.mjs` | +| Registry and GitHub environment setup | `release-setup.md` | `.github/workflows/release.yml`, `.github/workflows/release-execute.yml`, `tools/release/check_publish_environment.mjs` | +| CI gates and test selection | `testing.md`, `tooling.md` | `.github/workflows/ci.yml`, `tools/graph/ci_plan.mjs`, Moon project files | +| Binary artifacts and WASIX provenance | `assets.md`, `compiler-caching.md` | runtime target metadata, `tools/xtask`, the committed binary-semantic fingerprint | +| Extension support and packaging | `extension-packaging-policy.md` | extension catalog, product `targets/artifacts.toml`, evidence matrix, release catalog | +| SDK contracts | `sdk-products-policy.md`, `sdk-parity-policy.md`, `sdk-api-surface.md` | SDK manifests, package manifests, generated extension metadata, clean-consumer tests | +| Repository layout | `repo-structure.md` | Moon graph and build/package manifests | + +The repository-local skills under `.codex/skills/` are the procedural entry +points for agents: + +- `release-oliphaunt` owns release preparation, bootstrap, publish, recovery, + and registry setup; +- `qualify-oliphaunt-change` owns affected local feedback and exact-SHA GitHub + qualification; +- `add-oliphaunt-extension` owns extension catalog, version, target, evidence, + carrier, and package changes. + +Those skills route to executable checks and the focused references above; they +do not replace the executable sources of truth. + +`consumer-dx-release-blueprint.md` is archived design history. Files under `docs/internal/` are implementation history, investigations, or evidence snapshots unless a current maintainer document links to a specific section. They are not policy and must not be used to override the executable sources above. + +When changing a workflow or contract: + +1. update the executable source and behavior tests; +2. update the relevant maintainer entry point and its verified date; +3. regenerate derived tables instead of hand-editing them; +4. avoid policy assertions that depend on YAML step order, display text, or helper filenames unless the string itself is an external API. diff --git a/docs/maintainers/assets.md b/docs/maintainers/assets.md new file mode 100644 index 00000000..5d66b3c5 --- /dev/null +++ b/docs/maintainers/assets.md @@ -0,0 +1,236 @@ +# Maintainer Asset Notes + +This page is maintainer documentation for packaged runtime assets, generated +payloads, and release provenance. It is not end-user product documentation. +Native application users should start with +`src/docs/content/learn/native-runtime.md` and the SDK README for their +platform. WASIX users should use +the public WASM SDK guide and `src/docs/content/sdk/wasm/runtime.md`. + +`oliphaunt-wasix` does not embed the database runtime in the SDK crate. Runtime, +PGDATA template, extension, and AOT payloads are package-manager-resolved +artifact products staged by the language build integration. + +## What Ships + +The WASIX artifact products contain: + +- the portable Oliphaunt/Postgres WASIX runtime tree; +- a prepopulated PGDATA template for faster temporary databases; +- bundled extension archives for supported SQL extensions; +- the packaged `initdb` module used by asset CI and explicit fresh-initdb paths; +- the packaged `pg_dump` module used by the public dump API and CLI; +- a target-specific Wasmer AOT pack when the current host target is supported. + +Application code depends on `oliphaunt-wasix` plus the selected artifact +packages. The build integration stages only selected package-manager artifacts +into the application output. + +## Feature Flags + +Default SDK dependency after the first public release (use the exact version +selected by the application lockfile): + +```toml +oliphaunt-wasix = "0.1" +``` + +Enable the extension API explicitly: + +```toml +oliphaunt-wasix = { version = "0.1", features = ["extensions"] } +``` + +The repository source version remains `0.0.0` until Release Please creates the +first `0.1.0` release PR. Do not copy the repository source version into a +consumer manifest and do not reuse the legacy repository-wide `0.5.x` tags; +they predate the independently versioned Oliphaunt products. + +The crate exposes no `bundled` feature. Runtime and AOT assets enter the +application through package-manager artifact products, not through SDK default +features or public archive environment variables. + +## Cache Behavior + +Runtime files are expanded into a cache and then composed with a small writable +per-root skeleton by default. Temporary and template-backed databases use a +cached PGDATA template as a lower filesystem and materialize files into the +database root only when PostgreSQL opens them for mutation. + +The runtime tree keeps both `/bin/oliphaunt` and `/bin/postgres`. They are the same +backend module; the `postgres` path exists so upstream `initdb` can discover and +spawn the backend through PostgreSQL's normal `find_other_exec()` path. + +The cache is content-addressed by the asset manifest and artifact hashes. If an +asset hash does not match the manifest, startup fails instead of using a mixed +or corrupted runtime. + +## Extension Assets + +Extensions are demand-driven. An extension archive is installed into the +database root only when the builder requests it or `enable_extension` is called: + +```rust,no_run +use oliphaunt_wasix::{extensions, Oliphaunt}; + +let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + +db.enable_extension(extensions::PG_TRGM)?; +# Ok::<_, Box>(()) +``` + +Archive extraction rejects parent traversal, absolute paths, symlinks, +hardlinks, device nodes, and unsupported entry types. + +## Provenance + +Asset provenance is recorded in runtime source pins under +`src/sources/third-party/**`, extension-owned source pins under +`src/extensions/external/**/source.toml` and +`src/extensions/external/**/dependencies/**/source.toml`, +`src/sources/toolchains/**`, the committed asset input fingerprint, and the +generated runtime/AOT manifests produced by the +`CI` workflow's WASM runtime lane. Generated manifests record source pins, +runtime hashes, `initdb` hashes, PGDATA template hashes, extension archive +hashes, target information, and Wasmer engine identity. PostgreSQL ICU support +uses the same provenance path: ICU is source-pinned in +`src/sources/third-party/shared/icu.toml`, checked out under +`target/oliphaunt-sources/checkouts/icu`, and built as target-specific static +libraries by the native and WASIX runtime builders. ICU data is packaged as a +separate `oliphaunt-icu` payload; base native and WASIX runtime artifacts do +not carry `share/icu`. + +The public repository tracks source-controlled inputs and crate skeletons. It +does not track upstream source checkouts, generated PGDATA templates, portable +WASIX blobs, or native AOT binaries. +Maintainer source trees are fetched on demand into ignored +`target/oliphaunt-sources/checkouts/**` directories: + +```sh +cargo run -p xtask -- assets fetch +``` + +A Git source may declare one manually reviewed `mirror_url` when upstream +operates an authoritative HTTPS mirror. The canonical `url` remains the +durable `origin`; the fetcher alternates the canonical endpoint and mirror +within one bounded retry budget, then accepts bytes only when Git resolves +`FETCH_HEAD` to the declared 40-hex commit. Mirror selection never changes the +branch, commit, checkout-safety checks, or transactional promotion boundary. +Do not infer mirrors from host names or add an unauthenticated community fork. +Acquisition-policy changes require the source-fetch fault suite, manifest +validation, and a live exact-commit fetch from every newly declared endpoint. + +WASIX build and work trees are generated under +`target/oliphaunt-wasix/wasix-build/**`. The source tree +`src/runtimes/liboliphaunt/wasix/assets/build/**` is reserved for scripts, patches, +Docker inputs, and shims that should affect the committed asset fingerprint. + +Normal development and source-free validation do not clone upstream repositories +or run Docker. The source-free gate is: + +```sh +cargo run -p xtask -- assets verify-committed +``` + +It verifies source pins, source/build input fingerprints, extension +metadata/constants when generated manifests are installed, AOT crate templates, +and the absence of committed PGDATA template, portable WASIX, or native AOT +blobs. + +Release assets are built with the `release` profile by default: WASIX C code +uses `-O2 -g0`, and Binaryen runs the wasixcc default optimization plus +`--converge`, `--strip-debug`, and `--strip-producers`. The `release-o3` +profile remains available for explicit O3/ThinLTO comparison builds. + +Generated runtime hashes in package metadata are refreshed in the release +staging workspace. The committed `asset-inputs.sha256` is a +binary-semantic digest of source pins, patches, build recipes, producer code, +toolchain inputs, and normalized dependency locks. Release versions, +changelogs, package descriptions, and smoke expectations belong to the +publication envelope/lock and do not invalidate the expensive binary build. + +The WASIX builder declares its immutable bootstrap inputs in +`src/sources/toolchains/wasix.toml`: the Ubuntu base image digest, Dockerfile +frontend digest, Ubuntu snapshot timestamp, and the committed TLS root used to +reach `snapshot.ubuntu.com`. The APT helper writes one isolated deb822 source +containing only `noble`, `noble-updates`, and `noble-security` with the `main` +and `universe` components. Every update and install explicitly binds that +source, disabled source-parts discovery (`Dir::Etc::sourceparts=-`), a reset +list directory, and the verified CA bundle. A transient failure retries the +complete update/install transaction with a fixed bound; it never falls back to +a live mirror or disables TLS verification. `ca-certificates` is installed in +the same pinned transaction as the builder packages. + +The committed `isrg-root-x1.pem` is independently SHA-256 pinned, and +`builder.snapshot_tls_root_not_after` records its certificate-derived expiry +boundary. Rotate it before the manifest-declared boundary, or sooner if the +snapshot service changes its certificate chain: + +1. Obtain the replacement trust root from its authoritative CA distribution, + verify its subject, issuer, fingerprint, and `notAfter` value independently, + and replace only the committed PEM. +2. Update `snapshot_tls_root_sha256` and `snapshot_tls_root_not_after` in the + WASIX toolchain manifest, then update the Docker SHA-256 build argument to + match. If the Dockerfile frontend changes, pin its content digest in the + same change. +3. Run the pinned APT helper fault tests, source-spine verification, and a clean + Docker builder build. The build must reach the snapshot with normal peer + verification and print the pinned wasixcc, Clang, and Binaryen versions. +4. Refresh `asset-inputs.sha256`, then require the complete portable/AOT build + and exact-SHA hosted qualification. + +Treat any base image, frontend, snapshot, trust-root, source-set, APT helper, or +package-list change as a binary-semantic toolchain change. Ubuntu documents +archive snapshot availability for at least two years, so advance and qualify +the snapshot before that retention window expires or preserve it in an +authenticated archival mirror. + +The `CI` workflow's WASM runtime/AOT build lane mirrors the release topology on +trusted producer runs: one Linux/Docker job builds portable WASIX modules from +`src/runtimes/liboliphaunt/wasix/assets/build` into `target/oliphaunt-wasix/assets`, +then native matrix jobs generate and package target-specific Wasmer AOT crates +into `target/oliphaunt-wasix/aot/`. Artifacts are uploaded with +checksums, manifests, and the committed asset-input fingerprint. + +Pull requests run a Moon-based asset plan instead of GitHub path-filtering the +workflow. The plan uses `moon query affected` for the PR base/head, plus the +asset producer path allowlist, to decide whether the expensive producer jobs are +required. Non-asset PRs become an explicit no-op after the source-controlled +asset-input checks. Asset-producing PRs run those input checks and the same full +portable/AOT producer path as `main` and explicit maintainer dispatches. + +Manual `CI` dispatches use the same producer path. Maintainers may select +one native target for focused validation, but the workflow still rebuilds +portable WASIX assets, generates AOT artifacts, runs the runtime gate, stages the +release workspace, package-checks the target crate, and uploads the canonical +release artifact shape. + +Native AOT generation intentionally installs Wasmer's LLVM 22.1.x custom build +only inside the `CI` workflow's WASM AOT jobs or a maintainer's explicit +local artifact build. Normal contributors and end users never need LLVM; they +use committed Rust sources plus downloaded or released AOT payloads. + +The normal CI runtime matrix downloads the latest compatible `CI` workflow +WASM runtime bundle, verifies that the downloaded fingerprint matches the +current source inputs, installs the payloads into ignored generated paths, and +runs runtime tests. Changes to source pins, WASIX patches, extension catalogs, +build scripts, or AOT crate templates are treated as asset-producing: pull +requests must pass the source-controlled asset-input gate and the full producer +workflow before merge, while `main` and explicit maintainer dispatches remain +trusted producer lanes for release artifacts. Release validation downloads the +exact-SHA portable and AOT bundles, stages them into a clean release workspace, +validates package contents, and only then publishes. + +Published releases also attach public `.tar.zst` mirrors of the validated +portable WASIX and target AOT bundles. `xtask assets download --release ` +installs those release assets directly and does not require the GitHub CLI. + +After an intentional asset-source change and regenerated artifacts, refresh the +committed input fingerprint: + +```sh +cargo run -p xtask -- assets input-fingerprint --write +``` diff --git a/docs/maintainers/compiler-caching.md b/docs/maintainers/compiler-caching.md new file mode 100644 index 00000000..a8f91c08 --- /dev/null +++ b/docs/maintainers/compiler-caching.md @@ -0,0 +1,202 @@ +# Compiler Caching + +Oliphaunt uses three separate cache layers. Keep them separate: + +- Moon caches deterministic task outputs. +- Cargo, Gradle, pnpm, SwiftPM, and Xcode cache their own dependency/build + state through their native tools. +- Compiler caches reuse object-code compilation when a native lane has to run. + +Moon decides whether a product task runs. Compiler caches make the task cheaper +when it does run. Do not replace affectedness with compiler cache hits, and do +not treat compiler cache hits as release evidence. + +## Current Decision + +Use `ccache` for C/PostgreSQL native runtime builds on macOS, Linux, +iOS-simulator/device, and Android host builds when the build runs from a Unix +shell. These lanes compile PostgreSQL and liboliphaunt through clang or gcc, and +the build scripts already route `CC`/`CXX` through ccache when it is available. + +Use Cargo cache actions for Rust dependencies and `target` reuse. Do not enable +`sccache` by default yet. + +Use normal Gradle, SwiftPM, pnpm, Moon, and GitHub Actions caches for their +ecosystems. Do not put simulator state, device state, registry responses, +PostgreSQL source checkouts, or release artifacts into Moon's cache. + +Do not share native build roots across targets. Sharing object caches can be +reasonable when the compiler cache understands the compiler identity and flags, +but sharing build directories across macOS, Linux, iOS, Android, and Windows is +not. + +## Local Native Builds + +The liboliphaunt build scripts automatically use `ccache` when it is on `PATH`. + +```sh +brew install ccache +src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +ccache --show-stats +``` + +On Linux: + +```sh +sudo apt-get install ccache gcc-12 g++-12 +src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh +ccache --show-stats +``` + +Linux release carriers are compiled with GCC/G++ 12 so they cannot require a +newer GNU C++ ABI than the published `GLIBCXX_3.4.30` ceiling. The build script +selects `gcc-12` and `g++-12` by default, verifies their major versions before +starting an expensive build, and fingerprints the exact compiler binaries into +the PostGIS dependency cache. Set `OLIPHAUNT_CC` and `OLIPHAUNT_CXX` together +only when pointing at an equivalent GCC 12 toolchain; other compiler majors are +rejected before compilation. + +The iOS and Android native build scripts use the same `OLIPHAUNT_CCACHE` +contract because they are clang-based cross-builds launched from macOS. + +Override or disable it with: + +```sh +OLIPHAUNT_CCACHE=/opt/homebrew/bin/ccache src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +OLIPHAUNT_CCACHE=off src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +``` + +The build scripts use prefix mode (`CC="ccache cc"` style). Keep cache +configuration boring: set a local size, review `ccache --show-stats`, and avoid +broad `CCACHE_SLOPPINESS` settings unless a measured local experiment proves the +trade-off is worth it. + +Recommended local defaults: + +```sh +ccache --max-size=10G +ccache --set-config=compression=true +``` + +Use a per-workstation cache directory only when you need to isolate experiments: + +```sh +CCACHE_DIR="$HOME/.cache/oliphaunt-ccache" src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +``` + +## CI Native Builds + +Native extension jobs give each target an explicit local ccache directory under +`.ci-cache/ccache/native-extension/`. Keeping that directory outside the +native work roots prevents source preparation or stale-root cleanup from +deleting compiler objects. `CCACHE_BASEDIR` anchors paths to the checkout, +`compiler_check=content` binds hits to the actual compiler, and compression is +enabled. CI scopes compiler caches by runtime target: + +```text +liboliphaunt-native-ccache-v2---- +liboliphaunt-native-extension-ccache-v2---- +release-native-assets-ccache---- +``` + +The desktop and iOS jobs explicitly point `CCACHE_DIR` at +`.ci-cache/ccache/native-runtime/` and persist that directory alongside +the target-specific build root on Unix targets. The Windows desktop row creates +and restores only its relative target build root; it neither creates nor passes +the workspace-style `CCACHE_DIR` to the cache action. This keeps Git Bash path +conversion and Windows drive/backslash paths outside the Windows cache contract, +where ccache is disabled for the MSVC build. Do not use `~/.ccache`: current +ccache defaults use platform-specific cache directories, so that legacy path silently misses +the actual compiler objects. The target id is part of the key because the +cached build root is target specific. A Linux x64 build tree, Linux arm64 build +tree, macOS build tree, iOS build tree, Android build tree, and Windows build +tree must not share the same build-root cache. `ccache` itself is designed to +key on compiler inputs, but the surrounding build tree is not a generic object +cache. + +The repository cache is already close to GitHub's 10 GiB budget, and WASIX/LLVM +entries are the largest high-value caches. Persist only the iOS extension +ccache, which serves the roughly 70-minute native-extension critical path, and +cap it at 512 MiB. Other extension targets may use local ephemeral ccache during +their job, but do not consume repository cache quota. Do not persist complete +extension work roots: the iOS host, simulator, device, dependency, and +XCFramework trees are multi-build state with no demonstrated transfer win, can +consume multiple GiB, and are not safe to reuse through broad restore prefixes. +The ccache restore prefix is safe because ccache independently keys objects by +compiler content, source content, and compilation options. + +The input hash must track the same source domains that drive the Moon native +runtime tasks: PostgreSQL pins, third-party pins, extension metadata, all +`src/runtimes/liboliphaunt/native/**` sources and scripts, `tools/xtask/**`, `Cargo.toml`, +`Cargo.lock`, and `rust-toolchain.toml`. A cache hit across any of those changes +is treated as stale build-root reuse, not an acceptable optimization. + +CI prints `ccache --show-stats` after native builds. Treat those stats as the +first signal before changing cache size, keys, or storage. + +## Windows + +The Windows liboliphaunt lane uses MSVC and Meson/Ninja. It currently gets +target-scoped build-root reuse through GitHub Actions cache but not object-code +reuse. `ccache` is not the right default there; if Windows object caching becomes +necessary, use `sccache` for MSVC in a dedicated CI experiment before promoting +it to the release path. + +Use a native Windows Perl for PostgreSQL's MSVC build, such as Strawberry Perl. +Do not let Meson discover Git/MSYS Perl for this lane: MSYS path rewriting can +turn native linker options into bogus paths before PostgreSQL's Windows export +generation calls `dumpbin`. + +## sccache + +`sccache` is attractive because it supports Rust, C/C++, MSVC, local caches, +GitHub Actions cache storage, cloud storage, and distributed compilation. It is +also a bigger operational choice: + +- Rust cache hits require `RUSTC_WRAPPER=sccache`, and incremental workspace + crates are not cacheable. +- C/C++ cache hits require build-system launcher integration per build path. +- Shared cache hit rates depend heavily on stable toolchains, SDKs, compiler + flags, absolute path normalization, and per-platform keys. +- Remote or GitHub-backed caches add a new failure mode and need stats before + being trusted. + +Recommended adoption path: + +1. Keep current `ccache` plus Cargo cache as the release path. +2. Add a manual `workflow_dispatch` experiment that enables + `mozilla-actions/sccache-action`, `SCCACHE_GHA_ENABLED=true`, and + `RUSTC_WRAPPER=sccache` for Rust-heavy lanes only. Set `CARGO_INCREMENTAL=0` + in that experiment so Rust compiler outputs are cacheable and comparable. +3. Compare wall time, cache hit rate, upload/download time, and flake rate + against the current CI for at least five same-SHA reruns. +4. If Rust results are good, add a second experiment for Windows MSVC and + Unix C/C++ builds through explicit compiler launchers. Keep target-specific + cache scopes; do not let Linux, macOS, iOS, Android, and Windows publish into + one undifferentiated cache namespace. +5. Promote sccache only after the experiment writes stable stats to the CI + summary and has a documented rollback switch. + +Use these environment variables only in the experiment workflow: + +```text +SCCACHE_GHA_ENABLED=true +RUSTC_WRAPPER=sccache +CARGO_INCREMENTAL=0 +``` + +For local Rust experimentation, keep it opt-in: + +```sh +brew install sccache +RUSTC_WRAPPER=sccache CARGO_INCREMENTAL=0 cargo test -p oliphaunt +sccache --show-stats +``` + +## References + +- ccache manual 4.13.6: +- sccache README: +- sccache GitHub Action: +- GitHub Actions cache: +- Meson machine files: diff --git a/docs/maintainers/consumer-dx-release-blueprint.md b/docs/maintainers/consumer-dx-release-blueprint.md new file mode 100644 index 00000000..843c2089 --- /dev/null +++ b/docs/maintainers/consumer-dx-release-blueprint.md @@ -0,0 +1,592 @@ +# Consumer DX Release Implementation Plan + +Status: archived design plan; non-normative. Do not use package names, carrier +counts, workflow steps, or release policy from this file. They are historical +snapshots and may intentionally be stale. See `docs/maintainers/README.md`, +`docs/maintainers/release.md`, and `docs/maintainers/extension-packaging-policy.md` +for the current operational contract. + +## Decisions + +1. Ship no runtime artifact downloads in public SDK paths. +2. Ship no consumer install lifecycle scripts. +3. Remove CLI-first consumer flows from public docs. +4. Keep `oliphaunt-resources`, extension artifact tools, and release download + helpers as maintainer and CI tools. +5. Publish byte-carrying artifacts through the package manager used by each + ecosystem. +6. Publish every byte-carrying PostgreSQL extension as an explicit dependency or + product. Do not hide contrib extension bytes in base packages. +7. Keep the base runtime free of selected extension payloads and full ICU data. +8. Publish full ICU data as runtime-owned sidecar packages. Do not publish an + `icu-full` package. Model ICU as a runtime/data capability, not as a SQL + extension. +9. Generate every consumer package descriptor from the repo product graph, + release metadata, and extension catalog evidence. +10. Fail builds at package/install/build time on missing artifacts. Never defer + artifact errors to first database open. + +## Grounding + +- npm supports platform package selection with `os`, `cpu`, `libc`, and + `optionalDependencies`; npm also supports omitting dependencies from that + field. Resolver code must treat missing selected platform packages as install + errors. + Source: [npm package.json docs](https://docs.npmjs.com/files/package.json/). +- crates.io has a 10 MB compressed `.crate` limit. Oliphaunt publishes WASIX + runtime and AOT bytes through direct public artifact crates and fails release + packaging when a generated `.crate` exceeds that limit. Source: + [Cargo publishing docs](https://doc.rust-lang.org/cargo/reference/publishing.html). +- SwiftPM binary artifacts are declared in `Package.swift` with URL and checksum. + Plugins do not add new products after package resolution. Source: + [SwiftPM binary dependencies](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0272-swiftpm-binary-dependencies.md), + [SwiftPM plugin docs](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageManagerDocs/Documentation.docc/Plugins.md). +- iOS apps are self-contained bundles and do not download executable code that + changes functionality after review. Source: + [Apple App Review Guideline 2.5.2](https://developer.apple.com/app-store/review/guidelines/). +- Android and Gradle support variant-aware publication and dependency + verification. Source: + [Android publication variants](https://developer.android.com/build/publish-library/configure-pub-variants), + [Gradle dependency verification](https://docs.gradle.org/current/userguide/dependency_verification.html). + +## Current Code To Remove Or Replace + +### JavaScript + +Remove runtime GitHub downloads from: + +- `src/sdks/js/src/native/assets-node.ts` +- `src/sdks/js/src/native/assets-deno.ts` +- `src/sdks/js/src/native/node-addon.ts` +- `src/sdks/js/src/runtime/broker.ts` + +Replace those paths with package-local artifact resolution. Delete public use of: + +- `OLIPHAUNT_RELEASE_BASE_URL` +- `OLIPHAUNT_LIBOLIPHAUNT_ASSET_DIR` +- `OLIPHAUNT_NODE_DIRECT_RELEASE_BASE_URL` +- runtime cache installation as a normal public path + +Keep fixture directories for tests under test-owned names. Do not expose them as +consumer configuration. + +### Rust Native + +Remove CLI-first consumer install guidance from: + +- `src/sdks/rust/README.md` +- `docs/maintainers/sdk-products-policy.md` +- release metadata checks that treat `--resolve-release-assets` as the consumer + path + +Keep `oliphaunt-resources` for CI and maintainer packaging. Add the consumer +path through `oliphaunt-build`. + +### Rust WASIX + +Remove the public env-var archive path from consumer docs: + +- `OLIPHAUNT_WASM_RUNTIME_ARCHIVE` +- `OLIPHAUNT_WASM_AOT_ARCHIVE` + +Delete the inert `bundled` feature contract that advertises a feature and then +hard-fails. Replace it with a build-produced asset manifest contract. + +### Android And Kotlin + +Remove direct GitHub asset downloads from: + +- `src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java` +- the duplicate resolver in `src/sdks/kotlin/oliphaunt/build.gradle.kts` + +Replace downloads with Gradle/Maven artifact dependencies and Gradle dependency +verification. + +### React Native + +Replace the iOS config-only path in: + +- `src/sdks/react-native/app.plugin.js` +- `src/sdks/react-native/OliphauntReactNative.podspec` +- `src/sdks/react-native/ios/podspecs/*.podspec` + +The config plugin writes a locked native artifact plan. The native build consumes +real local package artifacts installed by npm packages. + +### Swift + +Extend the generated SwiftPM release manifest in: + +- `tools/release/render_swiftpm_release_package.mjs` + +Generate extension products and checksum-pinned binary targets. Do not use a +plugin to add dependencies. + +## Target Consumer DX + +### JavaScript: Node And Bun + +Base install: + +```sh +pnpm add @oliphaunt/ts +``` + +Runtime artifacts: + +- `@oliphaunt/ts` declares platform packages for `liboliphaunt`: + `@oliphaunt/liboliphaunt-darwin-arm64`, + `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/liboliphaunt-linux-arm64-gnu`, and + `@oliphaunt/liboliphaunt-win32-x64-msvc`. +- Each `@oliphaunt/liboliphaunt-*` platform package includes the matching + `liboliphaunt` library and PostgreSQL runtime tree for that platform. +- `@oliphaunt/ts` declares platform packages for `oliphaunt-broker`: + `@oliphaunt/broker-darwin-arm64`, `@oliphaunt/broker-linux-x64-gnu`, + `@oliphaunt/broker-linux-arm64-gnu`, and + `@oliphaunt/broker-win32-x64-msvc`. +- `@oliphaunt/ts` keeps the existing platform packages for + `oliphaunt-node-direct`. +- Platform packages use `os`, `cpu`, and `libc`. +- Runtime resolution uses literal package names and an exported artifact + manifest. +- Missing selected platform packages fail with an install message. +- Runtime resolution never fetches GitHub assets. + +Extension install: + +```sh +pnpm add @oliphaunt/ts @oliphaunt/extension-vector +``` + +Usage: + +```ts +import { Oliphaunt } from '@oliphaunt/ts'; +import { vector } from '@oliphaunt/extension-vector'; + +const db = await Oliphaunt.open({ + root: '.oliphaunt', + extensions: [vector], +}); +``` + +Contrib uses the same explicit package shape: + +```sh +pnpm add @oliphaunt/ts @oliphaunt/extension-pg-trgm +``` + +```ts +import { pgTrgm } from '@oliphaunt/extension-pg-trgm'; +``` + +Package layout: + +- `@oliphaunt/extension-` is a descriptor package. +- Descriptor packages declare platform artifact packages. +- npm byte packages cover native desktop, WASIX, Android ABI, and iOS + XCFramework target classes. +- Maven byte packages use + `dev.oliphaunt.extensions:-android-arm64-v8a` and + `dev.oliphaunt.extensions:-android-x86_64`. +- SwiftPM binary targets are generated into the release manifest. +- Platform artifact packages carry exact extension bytes for one target family. +- Descriptor exports include SQL name, product version, runtime compatibility, + target package names, checksums, byte sizes, dependencies, and required + preload libraries. + +### JavaScript: Deno And JSR + +`jsr:@oliphaunt/ts` is a TypeScript protocol/client package. It does not expose +native-direct or broker runtime artifact resolution. + +Deno native users install through npm: + +```ts +import { Oliphaunt } from 'npm:@oliphaunt/ts'; +``` + +The Deno native path uses the same npm-installed platform packages as Node and +Bun. The JSR package fails on native runtime creation with an error that points +to `npm:@oliphaunt/ts`. + +### Rust Native + +Base install: + +```sh +cargo add oliphaunt +cargo add --build oliphaunt-build +``` + +Extension install: + +```sh +cargo add oliphaunt-extension-vector +cargo add oliphaunt-extension-contrib-pg18 +``` + +ICU install: + +```sh +cargo add oliphaunt-icu +``` + +Application manifest: + +```toml +[package.metadata.oliphaunt] +runtime = "liboliphaunt-native" +runtime-version = "0.1.0" +extensions = ["vector", "pg_trgm"] +``` + +The contrib dependency is a shared distribution carrier. The metadata list +still selects exact SQL members; it does not stage every contrib extension. + +Add `icu = true` under `[package.metadata.oliphaunt]` after adding the +`oliphaunt-icu` dependency. + +Build script: + +```rust +fn main() { + oliphaunt_build::configure(); +} +``` + +`oliphaunt-build` is a build-dependency crate released with `oliphaunt-rust`. +It is the Rust application integration point for local runtime packaging. +Cargo resolves the target-specific artifact crates; `oliphaunt-build` only +stages the already-resolved files for the current application build. It performs +all app packaging work: + +- reads `[package.metadata.oliphaunt]`; +- reads generated product metadata; +- reads the resolved Cargo package graph; +- resolves native target from Cargo target variables; +- locates Cargo-resolved native runtime, broker, ICU, and exact extension + artifact crates; +- performs no network I/O; +- adds no dependencies to the Cargo graph; +- writes `OUT_DIR/oliphaunt/oliphaunt-assets.lock`; +- validates the lockfile and manifests; +- writes generated Rust constants and Cargo env vars; +- copies the exact runtime, broker helper, ICU data, and selected extension + artifacts into `OUT_DIR/oliphaunt/resources`; +- writes no generated files outside `OUT_DIR`; +- emits `cargo:rerun-if-changed=Cargo.toml`; +- fails on missing Cargo dependencies, missing checksums, unknown targets, + missing extension artifacts, selected ICU without `oliphaunt-icu`, and + unselected extension leakage. + +Cargo owns artifact selection: + +- Direct application dependencies expose artifact manifests through Cargo + `links` metadata. `oliphaunt-build` consumes only the metadata that Cargo + passes to the application build script. +- `oliphaunt` owns the base runtime and broker selector build script. It reads + its target-specific artifact dependencies and re-emits the selected manifests + as `oliphaunt-build` inputs. +- `oliphaunt-extension-*` crates own exact extension selector build scripts. + They read their target-specific native and WASIX artifact dependencies and + re-emit selected manifests as `oliphaunt-build` inputs. +- `oliphaunt-icu` is the Cargo ICU data artifact crate. It owns the ICU data + selector build script and re-emits selected manifests as `oliphaunt-build` + inputs. +- Raw target artifact crates are private implementation dependencies. Consumers + do not add target artifact crates directly. +- `oliphaunt-build` stages only artifacts exposed by direct application + dependencies. It never relies on transitive Cargo metadata. + +Consumer code uses the SDK without environment variables: + +```rust +let db = oliphaunt::Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .extension(oliphaunt::Extension::Vector) + .open() + .await?; +``` + +Tauri documentation shows the one bundler stanza that copies the generated +Oliphaunt resource directory into the app image. + +### Rust WASIX + +Base install: + +```sh +cargo add oliphaunt-wasix +cargo add --build oliphaunt-build +``` + +Extension install: + +```sh +cargo add oliphaunt-extension-vector +``` + +Application manifest: + +```toml +[package.metadata.oliphaunt] +runtime = "liboliphaunt-wasix" +runtime-version = "0.1.0" +extensions = ["vector"] +``` + +Build script: + +```rust +fn main() { + oliphaunt_build::configure(); +} +``` + +WASIX uses Cargo-selected runtime artifacts. The public `oliphaunt-wasix` crate +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 +crates, then publishes `oliphaunt-wasix`. Consumers add only +`oliphaunt-wasix`; Cargo selects the matching target dependency. The crate does +not expose a `bundled` feature and does not read runtime/AOT archive env vars in +the normal path. + +Delete source-only/private WASIX asset and AOT fallbacks from the consumer path. + +### Android And Kotlin + +Base install: + +```gradle +plugins { + id("com.android.application") + id("dev.oliphaunt.android") version "0.1.0" +} + +dependencies { + implementation("dev.oliphaunt:oliphaunt-android:0.1.0") +} + +oliphaunt { + liboliphauntVersion.set("0.1.0") + selectedExtensions.add("vector") +} +``` + +Plugin behavior: + +- maps selected ABIs to Maven runtime artifacts; +- adds runtime artifact dependencies; +- adds exact extension artifact dependencies; +- writes generated Android assets and `jniLibs`; +- verifies dependencies through Gradle dependency verification; +- fails on missing artifacts, checksum mismatches, unknown extensions, and + unselected extension leakage. + +Published artifacts: + +- `dev.oliphaunt.runtime:liboliphaunt-android-arm64-v8a` +- `dev.oliphaunt.runtime:liboliphaunt-android-x86_64` +- `dev.oliphaunt.extensions:-android-arm64-v8a` +- `dev.oliphaunt.extensions:-android-x86_64` +- `dev.oliphaunt.runtime:oliphaunt-icu` + +### Swift + +Base install: + +```swift +.package(url: "https://github.com/f0rr0/oliphaunt.git", exact: "0.6.0") +``` + +Base product: + +```swift +.product(name: "Oliphaunt", package: "oliphaunt") +``` + +Extension product: + +```swift +.product(name: "OliphauntExtensionVector", package: "oliphaunt") +``` + +Generated release manifest: + +- declares `Oliphaunt`; +- declares one product per exact extension; +- declares `OliphauntICU`; +- declares checksum-pinned binary targets for base runtime artifacts; +- declares checksum-pinned binary targets for exact extension artifacts; +- links selected extension products before app archive. + +Runtime open validates that every configured SQL extension has a linked product. +Missing extension products fail before database open. iOS never downloads +runtime, extension, or ICU executable payloads after app archive. + +### React Native + +Install: + +```sh +pnpm add @oliphaunt/react-native @oliphaunt/extension-vector +``` + +Expo config: + +```json +{ + "expo": { + "plugins": [ + ["@oliphaunt/react-native", { "extensions": ["vector"] }] + ] + } +} +``` + +Config plugin behavior: + +- reads installed `@oliphaunt/extension-*` descriptors; +- writes one locked native artifact plan for iOS and Android; +- wires Android through the Kotlin Gradle plugin; +- runs `oliphaunt-react-native-stage-ios` before CocoaPods; +- materializes `ios/frameworks`, `ios/extension-frameworks`, `ios/resources`, + and `ios/generated/static-registry` from npm-installed extension packages; +- patches Podfile/podspec entries to use staged local artifacts; +- fails during prebuild on missing npm packages, version mismatch, missing iOS + artifacts, missing Android artifacts, and unselected extension leakage. + +React Native owns no independent runtime resolver. + +### ICU + +ICU product model: + +- base runtime excludes full ICU data; +- ICU-enabled runtime capability is explicit in release metadata; +- full ICU data ships as runtime-owned sidecar packages; +- do not publish `icu-full`; +- do not publish a Cargo crate named `icu`; +- package surfaces: + - npm: `@oliphaunt/icu` + - Maven: `dev.oliphaunt.runtime:oliphaunt-icu` + - SwiftPM: `OliphauntICU` + - Rust native: `oliphaunt-icu` plus `[package.metadata.oliphaunt] icu = true` + - WASIX Rust: `oliphaunt-icu` behind the `oliphaunt-wasix/icu` feature +- builders fail on ICU selection without a matching ICU data artifact. + +## Release Graph + +Keep source ownership split and explicit: + +- Moon owns tasks, dependency edges, and artifact target membership. +- `release.toml` owns publish targets and registry package names. +- Release Please owns versions, changelogs, and the generated release PR; the + protected workflow owns exact-SHA tags and draft GitHub releases. +- extension catalog/evidence/recipes own extension identity, readiness, + dependencies, and capability data. + +Add release surfaces, not aggregate products: + +- `liboliphaunt-native` owns JS base runtime platform packages, Cargo native + runtime artifact crates, Android Maven runtime payloads, SwiftPM base binary + metadata, and native runtime package-size budgets. +- `oliphaunt-broker` owns JS broker helper platform packages and Cargo broker + artifact crates. +- `oliphaunt-node-direct` keeps the existing npm addon platform packages and + drops runtime download code. +- each existing `oliphaunt-extension-*` product owns generated npm descriptor + packages, npm platform byte packages, Cargo descriptor and artifact crates, + and Maven payload packages. +- `oliphaunt-swift` generates SwiftPM extension products from exact extension + product metadata. +- `oliphaunt-rust` owns the derived `crates:oliphaunt-build` crate, SDK crate, + target-specific Cargo dependency wiring, and version file. +- `liboliphaunt-native` owns native ICU release assets plus `@oliphaunt/icu`, + `dev.oliphaunt.runtime:oliphaunt-icu`, and `OliphauntICU` packaging. +- `liboliphaunt-wasix` owns the WASIX ICU release asset and the + `crates:oliphaunt-icu` artifact crate. + +Do not add a second catalog. Generate package descriptors from existing product +metadata and extension evidence. Commit the generated descriptor snapshots used +by release checks. + +## Minimal Release Gates + +Keep these gates: + +1. Metadata parity: Moon, `release.toml`, release-please, and extension generated + catalogs agree. +2. Package-size gate: every npm tarball, crate, Maven artifact, Swift binary + archive, ICU archive, and extension package stays within its declared budget. +3. No runtime download gate: public SDK packages contain no `fetch(`, + GitHub-release URL construction, or runtime asset cache installer in normal + runtime paths. +4. Real package smoke: install from packed artifacts in scratch projects for npm, + JSR protocol package, Cargo native, Cargo WASIX, Android Gradle, SwiftPM, and + React Native. +5. Network-off smoke: after package install, disable network and open a database + with one contrib extension and one external extension. +6. Exact extension gate: selected extension artifacts enter the app; unselected + extension artifacts stay out. +7. Publish idempotency gate: already-published registry artifacts with matching + version/provenance skip; GitHub assets with matching checksum skip; mismatched + bytes fail. +8. Release-plan gate: contrib source/runtime-contract changes release contrib + products only; external extension source changes release that external + extension only; runtime ABI changes release compatible runtime and extension + products. + +Enforcement ownership: + +- `tools/dev/bun.sh tools/release/release-consumer-shape.mjs` validates + structured package manifests, registry carriers, platform selectors, + exact-extension targets, and the Moon tasks that own executable consumer + proofs. It does not scan implementation spelling or documentation prose. +- Product `package` and `release-check` tasks own packed-package, clean-project, + no-network, and runtime behavior. Candidate CI executes those tasks on the + runner and artifact combinations they require. +- The Rust package-shape check builds a scratch Cargo application from the + generated `oliphaunt` release source, calls `oliphaunt-build`, and verifies + that native-runtime and broker-helper artifacts are staged into the generated + lockfile. + +Remove these gates and policies: + +- checks requiring TypeScript runtime GitHub downloads; +- checks requiring Android GitHub asset downloads; +- checks treating `oliphaunt-resources --resolve-release-assets` as consumer DX; +- WASIX checks that keep an inert `bundled` feature; +- docs that present env vars as normal consumer setup; +- duplicate Kotlin asset resolver code outside the Android Gradle plugin. +- Swift metadata gates that forbid generated extension products. + +## Implementation Order + +1. Replace public docs with the target DX above. +2. Add generated descriptor schema for runtime, extension, ICU, and platform + artifacts. +3. Add JS `liboliphaunt` and broker platform packages. +4. Remove JS runtime GitHub download paths and update tests to expect missing + package errors. +5. Generate npm extension descriptor/platform packages for all exact extension + products. +6. Add JS bundler/package smokes. +7. Add Cargo native runtime, broker, extension, and ICU artifact crates. +8. Add the derived `oliphaunt-build` crate and Rust native build integration. +9. Switch Rust WASIX to Cargo-selected artifact crates, + `oliphaunt-build` manifest loading, and no public env-var archive setup. +10. Publish Android runtime and extension artifacts to Maven and replace Gradle + downloads with dependency resolution. +11. Generate SwiftPM extension products and `OliphauntICU` in the release + manifest. +12. Implement React Native iOS staging from installed npm packages. +13. Add ICU data packages and capability validation. +14. Update release CI to the minimal gates above. +15. Delete obsolete release metadata checks, docs, tests, and environment + overrides tied to removed paths. diff --git a/docs/maintainers/development.md b/docs/maintainers/development.md new file mode 100644 index 00000000..66f70929 --- /dev/null +++ b/docs/maintainers/development.md @@ -0,0 +1,417 @@ +# Maintainer Development Guide + +Status: normative local-development guide. Last verified: 2026-07-15. Owner: repository maintainers. + +This page is maintainer documentation for repository validation, generated +artifacts, and local release metadata checks. It is not end-user product +documentation. + +Bootstrap the pinned local toolchain once: + +```sh +moon run dev-tools:doctor +tools/dev/bootstrap-tools.sh +``` + +For each change, follow `.codex/skills/qualify-oliphaunt-change/SKILL.md`: +inspect Moon affectedness, run focused checks first, and expand only when the +changed contract requires it. A normal affected source feedback pass is: + +```sh +moon query affected --upstream none --downstream deep +moon run :check --affected +moon run :test --affected +``` + +Run `moon run ci-workflows:check` for workflow changes and +`tools/dev/bun.sh tools/policy/check-supply-chain.mjs` for dependency or +supply-chain policy changes; neither is an unconditional pre-PR ceremony. + +Tool versions for Moon, Node, pnpm, Bun, and Deno are pinned in `.prototools`. +Bun is required for the TypeScript SDK checks because `@oliphaunt/ts` supports +Bun through the npm artifact; local checks use `tools/dev/bun.sh` when the shell +does not already provide the pinned Bun. Deno is optional for normal local checks +and uses `tools/dev/deno.sh` on demand for JSR package validation. + +Windows native builds obtain WinFlexBison from the exact upstream archive pinned +in `src/sources/toolchains/winflexbison.toml`. The shared native setup verifies +the archive size and digest, safe ZIP layout, complete extracted-tree digest, +and both executable digests before adding the atomic cache payload to `PATH`. +Do not replace this path with a live Chocolatey lookup; Chocolatey is retained +only for Strawberry Perl when the hosted image does not already provide it, and +that fallback must prove the expected executable after every install attempt. + +Tool choices and rejected alternatives are recorded in +[tooling.md](tooling.md). Update that decision record before adding a new +repo-wide tool or hand-rolled release helper. + +Moon is the product graph and affected-task entrypoint. A fresh checkout should +install the pinned proto/Moon toolchain from `.prototools`, then call Moon +directly: + +```sh +moon query projects +moon query affected --upstream none --downstream deep +moon run :coverage --affected +``` + +Use `moon query affected` to inspect affectedness and `moon run ` for +explicit local targets. GitHub CI executes the exact planned target list with +Moon so jobs do not expand into unrelated downstream work. Normal commands use +Moon's own concurrency instead of a forced single-worker debug mode. + +The validation entrypoint is split by maintainer workflow: + +- `moon run liboliphaunt-native:host-smoke`: no-build host C ABI/runtime smoke for the + current native target. It compiles and runs the consumer-style ABI harness and + the full C smoke against the release-runtime artifact for macOS, Linux, or + Windows. `OLIPHAUNT_TRACK_BUILD=never` makes missing or stale artifacts fail + immediately instead of entering any build path; +- `moon run liboliphaunt-native:release-check`: release-owned, no-build host + C ABI/runtime smoke. It depends on the native release-runtime producer and + then refuses any implicit rebuild inside the smoke; extension and peer SDK + evidence remains in their own Moon tasks; +- `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/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; +- `moon run ci-workflows:check`: local `actionlint` and `zizmor` checks using + the same zizmor config and severity/persona as CI. `actionlint` covers + workflow syntax, expression, and shell wiring; `zizmor` covers workflow + security findings. Keep both, but do not add another workflow linter unless + it replaces one of these responsibilities; +- `moon run liboliphaunt-wasix:smoke`: hard-requires portable assets plus host AOT, + installs them into ignored paths, and runs the real runtime tests; +- `moon run integration-examples:check`: Tauri/Rust/frontend example checks; +- `moon run liboliphaunt-native:smoke`: native-only C ABI smoke and + Rust native SDK tests. This delegates to the same fast product-track harness + as `check-track.sh quick`, so it reuses `target/liboliphaunt-pg18` by default + and only builds missing artifacts. Set `OLIPHAUNT_TRACK_BUILD=never` to prove + the command will not rebuild, `missing` to build absent artifacts, or `always` + for a deliberate rebuild. Use `check-track.sh extensions` or `full` for the + gated extension matrix. The native dylib is stamped and reused + unless edited C ABI sources, PostgreSQL embedded object inputs, compiler, or + patch/build inputs change; set `OLIPHAUNT_FORCE_RELINK=1` for a deliberate + relink. Extension artifact builds are separately fingerprinted and reused + across runs unless native C ABI, PostgreSQL patch/build, compiler, or + extension source inputs change. Set `OLIPHAUNT_FORCE_EXTENSION_REBUILD=1` for + a deliberate clean extension rebuild; +- `src/runtimes/liboliphaunt/native/tools/check-track.sh [host-smoke|quick|rust|extensions|sdks|external-pgrx|full]`: + native-only liboliphaunt product validation. This is the preferred iteration lane + for the new product track because it avoids the WASIX release lane, exports the + local `target/liboliphaunt-pg18` runtime for Rust/Swift/Kotlin/RN tests, and only + runs `src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh` when native artifacts are + missing. Extension/full modes first call the build script's no-build + `--check-extension-artifacts-current` freshness probe; they only enter the + normal build path when the stamped extension fingerprint or required artifacts + are stale or absent. The readiness check consumes + `src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh --print-required-extension-artifacts`, + so it validates the complete artifact inventory used by the build instead of a + sample subset. Set + `OLIPHAUNT_TRACK_BUILD=never` to fail immediately instead of building, + `missing` to build only absent or stale required artifacts, or `always` for a + deliberate rebuild; +- `tools/perf/check-native-perf-harness.sh`: fast no-build guard + proving the native perf script plans direct/broker/server/native-PostgreSQL + work with explicit `--perf-runner` support, without invoking the separate + WASIX comparison lane; +- `moon run oliphaunt-rust:check`: static Cargo checks for `oliphaunt` and + `oliphaunt-build`, plus the artifact-relay build-script test. Unit, package, + and native runtime evidence remain separate `test`, `package`, and `smoke` + targets; +- `moon run oliphaunt-rust:test`: the hosted-equivalent Rust source-test lane. + It runs documentation tests, `oliphaunt-build` tests, and all `oliphaunt` + library, executable, and integration tests. A focused command such as + `cargo test -p oliphaunt --lib` is useful while iterating, but excludes the + executable tests under `src/bin/**` and is not qualification evidence; +- `moon run oliphaunt-rust:package`: the compact Rust SDK pre-push gate. Its + Moon dependencies own `oliphaunt-rust:check` and `oliphaunt-rust:test`, then + it verifies the publishable package without selecting native OS builders, + runtime smoke, or E2E lanes. Use the corresponding affected SDK `package` + targets together when a shared contract changes; +- `moon run oliphaunt-rust:smoke`: native SDK runtime proof. It reuses an + existing host runtime only when the matching liboliphaunt library, + PostgreSQL tools, normal extension files, and embedded extension modules are + all present and current. The shared preflight fails closed on a partial + extension inventory instead of silently skipping native SQL coverage; +- `moon run sdk-contracts:check`: fast generated API, SDK registry, C ABI + header-copy, native-boundary, and README-example contract validation. Use + product `check`, `test`, and `package` targets for behavior and package proof; + `tools/policy/check-sdk-parity.sh` remains a compatible local aggregate; +- `moon run oliphaunt-swift:check`: SwiftPM package description and build checks + for the SDK package and repository root package; +- `moon run oliphaunt-swift:smoke`: Swift SDK tests against the current native + host runtime; on macOS it also requires the iOS simulator preflight; +- `moon run oliphaunt-swift:package`: validates the Swift source package + shape without building platform release artifacts; +- `moon run liboliphaunt-native:build-ios-xcframework`: explicitly builds and + freshness-checks iOS simulator and device `liboliphaunt.dylib` slices from + the same PostgreSQL 18 patch stack, then packages them as + `liboliphaunt.xcframework`; +- `moon run oliphaunt-kotlin:smoke`: builds and freshness-checks the selected + Android ABI's `liboliphaunt.so` artifact, then runs the Android SDK smoke; +- `moon run oliphaunt-kotlin:check`: Kotlin Multiplatform formatting, lint, + compilation, and Android-only Maven publication-shape checks. Unit and + host-native tests remain in `oliphaunt-kotlin:test`; +- `moon run oliphaunt-react-native:smoke-android`: Android React Native + installed-app harness over the Expo development-client sample; +- `moon run oliphaunt-react-native:smoke-ios`: iOS React Native + installed-app harness over the Expo development-client sample; +- `moon run oliphaunt-react-native:check`: React Native TypeScript build and + typecheck, Codegen, and native source-contract checks. Package-shape work is + owned by `oliphaunt-react-native:package`; +- `moon run oliphaunt-react-native:smoke-mobile`: aggregate local Expo + development-client installed-app lane. It runs both platform-specific smokes + against the packed SDK and real native artifacts; +- `pnpm --dir src/sdks/react-native/examples/expo run smoke:android`: real Android Expo + development-client smoke for the installed React Native package. It reuses + current native artifacts, generates the ignored Expo `android/` project only + when missing, packages `liboliphaunt.so` plus runtime/template resources, starts + Metro when needed, installs the app, and waits for + `OLIPHAUNT_EXPO_SMOKE_PASS`; +- `pnpm --dir src/sdks/react-native/examples/expo run smoke:ios`: real iOS Expo + development-client build/smoke harness for the installed React Native package. + For simulator builds it produces or reuses the current iOS simulator + `liboliphaunt.dylib` automatically when no explicit artifact override is set, + packages the same runtime/template resources, patches only the ignored + generated `ios/` Podfile for local Swift pods, rejects macOS dylibs, and can + run in `OLIPHAUNT_EXPO_IOS_BUILD_ONLY=1` mode when CoreSimulator is + unavailable; +- `tools/policy/check-crate-package.sh`: package all published crates and enforce + crates.io size limits; +- `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`: + explicit full local parity lane, including measured coverage; +- `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; +- `moon run repo:release-check`: the canonical full local release gate. It + delegates to `release-tools:check`, so `repo:release-check`, + `release-tools:release-check`, and a workspace `:release-check` do not replay + the same mutation suite under different target names. The direct equivalent + is `tools/dev/bun.sh tools/release/release-check.mjs`. This release-owned + metadata and mutation gate does not replace affected product `check`, `test`, + or `package` tasks; +- `tools/dev/bun.sh tools/release/release-metadata-check.mjs`: internal + protected-workflow replay after a generated release commit has passed its + structured verifier or after the exact hosted `Qualified` record has been + reverified against a clean checkout. It is not a replacement for the full + local gate. Candidate artifact dry-runs run only through the protected GitHub + `Release` workflow after exact-SHA qualification. + +Moon caches deterministic task results when their declared source inputs and +task dependencies have not changed. Local `:smoke` targets use `cache: local`, +so repeated `moon run :smoke` runs can return a cached result for the same source +graph. Use `moon run :smoke --cache off` when you need a live +device, simulator, or runtime probe regardless of the cache. Generated report +aggregates, such as `repo:coverage`, depend on upstream task outputs with Moon +2.3 `cacheStrategy: outputs`, so downstream cache invalidation follows the +artifact contract instead of every private upstream source edit. + +Kotlin and React Native Android SDK validation uses Gradle's configuration +cache by default so repeated local runs do not reconfigure the same Android/KMP +graphs. Set `OLIPHAUNT_GRADLE_CONFIGURATION_CACHE=0` only when diagnosing +Gradle configuration-cache behavior itself. + +The hook split is intentionally small: + +- pre-commit: file hygiene and formatting +- 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 + linting, feature powerset, public API compatibility, crate packaging, + native AOT runtime tests, frozen Cargo publication dry-runs, and supply-chain + policy + +Install local hooks and pinned CLI tools when needed. Maintainer bootstrap +release assets are an explicit source contract in +`src/sources/toolchains/maintainer-tools.toml`: every supported Linux and macOS +host has an exact URL, archive SHA-256, extracted-binary SHA-256, archive +layout, and size bound. The installer accepts only bounded HTTPS downloads, +checks the complete archive before extraction, rejects unexpected or non-file +members, and promotes a staged binary and its identity marker atomically. A +matching version string alone is not a cache hit. + +`cargo-binstall` may fall back only after a transport failure or an unsupported +binary host. That fallback is an isolated, exact-version `cargo install +--locked` build and is promoted through the same rollback-safe path; it never +reuses a partial download. `actionlint` has no source fallback because the +repository does not pin a Go toolchain. Update the manifest and the fault tests +together when either maintainer tool is upgraded. + +```sh +tools/dev/bootstrap-tools.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 +is intentionally slower than the protocol unit tests. + +## Maintenance Utilities + +The repository includes maintenance commands: + +- `oliphaunt-wasix-dump` is the logical dump CLI entry point. +- `oliphaunt-wasix-proxy` exposes a local PostgreSQL socket backed by the embedded + runtime. +- `xtask assets template` generates the architecture-independent PGDATA + template from the split WASIX `initdb` module. Portable WASIX, PGDATA + templates, and native AOT payloads remain generated-only. + +Asset and source checks: + +```sh +cargo run -p xtask -- assets verify-committed +cargo run -p xtask -- assets fetch +cargo run -p xtask -- assets check --strict-local +cargo run -p xtask -- assets check --strict-generated +cargo run -p xtask --features template-runner -- assets template +cargo run -p xtask -- assets source-spine --check-patch-applies +cargo run -p xtask -- assets audit-upstream --strict +cargo run -p xtask -- assets input-fingerprint --write +cargo run -p xtask -- package-size --enforce +``` + +## Local Runtime Development + +Local development has three supported modes. + +Fast contributor mode does not require Docker, upstream source checkouts, or +generated native AOT payloads. Use it for ordinary Rust, docs, tests, examples, +and workflow edits: + +```sh +moon run :check && moon run :test +``` + +For native liboliphaunt work, prefer the native-only track. It keeps the C ABI, +Rust SDK, Swift/Kotlin/React Native SDK package lanes, extension matrix, and +local runtime smoke tests separated from the WASIX release lane: + +```sh +moon run liboliphaunt-native:host-smoke +src/runtimes/liboliphaunt/native/tools/check-track.sh quick +src/runtimes/liboliphaunt/native/tools/check-track.sh sdks +src/runtimes/liboliphaunt/native/tools/check-track.sh full +``` + +`quick` is the normal native inner loop: it reuses +`target/liboliphaunt-pg18` when present, runs the C smoke, and runs the Rust +native SDK tests. `rust` skips the C smoke but still exports or creates the +native runtime before Rust env-gated tests, so it is the faster Rust-only native +validation lane. `moon run oliphaunt-rust:regression` uses the basic native +runtime and runs SQL/protocol regression across direct, broker, and server mode. +`moon run oliphaunt-rust:extension-regression` is the separate +extension-artifact lane; it depends on `liboliphaunt-native:release-check` and is +intentionally not part of normal PR CI. `extensions` and `full` use the build +script's no-build extension freshness probe before running the matrix, which avoids both +unnecessary rebuilds and the failure mode where a core-only runtime is +accidentally treated as extension ready. `sdks` validates SDK ownership/parity, +then runs the Rust, Swift, Kotlin, and React Native package checks. See +[`docs/maintainers/sdk-parity-policy.md`](../../docs/maintainers/sdk-parity-policy.md) for the SDK ownership contract. `full` enables +native extension artifacts and the extension matrix in addition to the SDK +checks. Use +`OLIPHAUNT_TRACK_BUILD=never` when you want to prove the harness is not +rebuilding anything. + +Host-platform artifact mode is for runtime work on the current machine. It +builds or packages only the current host target, leaves all generated payloads +in ignored paths, and then runs the real runtime tests: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets fetch +cargo run -p xtask --features aot-serializer -- assets build-host +moon run liboliphaunt-wasix:smoke +``` + +Local AOT generation requires the Wasmer LLVM 22.1.x build for the +maintainer-only serializer. That build includes the LLVM target set Wasmer's +LLVM backend expects, including LoongArch and WebAssembly. Set +`LLVM_SYS_221_PREFIX` to an extracted +`wasmerio/llvm-custom-builds` 22.x archive, or use downloaded-artifact mode to +avoid local LLVM setup. + +When the portable WASIX assets are already current and only the host AOT crate +needs to be refreshed, skip the source/Docker build and generate host AOT from +the existing generated portable assets: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets aot --target-triple "$host" +cargo run -p xtask -- assets package-aot --target-triple "$host" +moon run liboliphaunt-wasix:smoke +``` + +Downloaded-artifact mode is the intended way to test a CI-produced runtime +locally without rebuilding Postgres/WASIX. Download the successful `CI` +workflow runtime artifacts for the exact commit and install the host target +payloads into the same ignored generated locations used by the local build path: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets download --sha --target-triple "$host" +moon run liboliphaunt-wasix:smoke +``` + +For Rust-only work where the asset inputs have not changed, the same command +can install the latest compatible `main` bundle after verifying the +asset-input fingerprint: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets download --latest-compatible --target-triple "$host" +moon run liboliphaunt-wasix:smoke +``` + +Released artifact bundles can be installed without the GitHub CLI because they +are public GitHub release assets: + +```sh +host="$(rustc -vV | awk '/^host:/{print $2}')" +cargo run -p xtask -- assets download --release --target-triple "$host" +moon run liboliphaunt-wasix:smoke +``` + +Release validation can download every supported target from the exact `CI` +workflow SHA: + +```sh +cargo run -p xtask -- assets download --sha --all-targets +tools/dev/bun.sh tools/release/release-check.mjs +``` + +Developers should not be expected to build every target locally. Local runtime +work validates the host target; the `CI` workflow's WASM runtime/AOT lane is +the authority for the full macOS, Linux, and Windows AOT matrix. + +Contributors do not need upstream source checkouts for normal Rust, docs, +examples, or package validation. Maintainers fetch sources only when rebuilding +the portable WASIX runtime, extensions, `initdb`, `pg_dump`, or the generated +PGDATA template. Portable WASIX artifacts, generated PGDATA templates, and +native AOT artifacts are generated under `target/oliphaunt-wasix/**` locally or by +CI; they are not committed to git. + +The `CI` pull-request job uses Moon affectedness over `postgres18`, `third-party`, +`source-toolchains`, `extensions`, and `oliphaunt-wasix:release-check`, plus a small producer path +allowlist, to decide whether the expensive asset build is required. Non-asset +PRs become an explicit no-op after source-controlled input checks. +Asset-producing PRs verify source pins, the committed asset-input fingerprint, +extension catalog metadata, generated metadata policy, and then run the full +portable/AOT producer workflow before merge. `main` and +explicit maintainer dispatches remain trusted producer lanes for release +artifacts. + +Release process details are tracked in [release.md](release.md). Historical +progress notes under `docs/internal/` are archived and non-normative; they are +not the current backlog or release checklist. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md new file mode 100644 index 00000000..68f79b17 --- /dev/null +++ b/docs/maintainers/examples-ci-release-validation.md @@ -0,0 +1,384 @@ +# Examples, CI, Release, and SDK Validation Tracker + +> Archived validation log; non-normative. Dates, tool inventories, run ids, and +> package examples below describe historical checkpoints. Start with +> `docs/maintainers/README.md` and the executable release graph for current +> policy and package identities. + +This historical checklist covered the registry-first example flow and the +release/tooling surface after the runtime tool crate split. It is retained only +as dated implementation evidence. + +## P0: Registry-First Example Validation + +- [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 + - `liboliphaunt-wasix-portable` + - `oliphaunt-wasix-tools` + - host WASIX AOT and tools-AOT crates + - 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 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: + - 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 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. + +## P1: CI and Release Shape + +- [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 + - WASIX runtime Cargo crates + - WASIX tools Cargo crates + - WASIX AOT crates + - WASIX tools-AOT crates + - extension runtime/AOT crates +- [x] Verify release dry-runs publish the same package families to local registries. +- [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. +- [x] Derive release workflow artifact downloads and node-direct package dirs from the + same target graph used by CI. +- [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. +- [x] Document local runner limitations instead of pretending macOS, Windows, iOS, or + Android lanes were validated on Linux. + +## P1: SDK Consistency + +- [x] Compare native runtime/tool/extension/ICU resolution across Rust, JS, React + Native, Swift, and Kotlin. +- [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. +- [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 + and test Deno as intentionally unsupported for registry-managed extensions. +- [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. + +## P2: Dead Code and Tooling Cleanup + +- [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. +- [x] Identify Rust xtask code that is not performance-sensitive or domain-critical and + can be moved to Bun without compiling unnecessary crates. +- [x] Keep build/runtime-critical Rust and platform shell where they remain idiomatic. + +## 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: + `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`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `tools/dev/bun.sh tools/policy/assertions/repository-semantics.mjs structure`, + `tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check`, and + `git diff --check`. The lightweight parity entries in that historical command + list now resolve to generated/schema validators; Kotlin product tests own the + rejection behavior. +- 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`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, + `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`. Those semantics are + covered by affected SDK `check`, `test`, `package`, and `release-check` + targets, the generated `sdk-contracts:check`, `extension-model:check`, and + platform lifecycle lanes; the compatibility parity script no longer infers + them from source spellings or test names. 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, 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/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 + 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 + 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 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 unused Python release metadata compatibility module was + deleted after the remaining executable consumers moved to the Bun release + graph query. The active Bun release-metadata validator consumes that graph + directly; the Python tooling inventory records only intentional Python + domain tools. +- 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. 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/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/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 + current split runtime/tools package surface with + `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: + `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 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 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`, + `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. +- 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, + 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 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 + `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, + 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`. +- 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 + 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`. + 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`, + `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 + fingerprint and extension evidence source digests. + `tools/dev/bun.sh tools/release/release-check.mjs` 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 semantic release metadata + and consumer-readiness validators derive those identities from 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. +- 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 + 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`. +- 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. +- SDK package artifact names now derive from release products with + `kind = "sdk"`. Release downloads and local registry publication ask + `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 + --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. +- 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 + 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. +- 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 + product test. Cross-product validators require the Swift package/test proof + tasks, while Swift owns the test's implementation and naming. +- 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 broader SDK resolver/control-flow parity + still needed a full pass. Prose-only expectations must become generated + contracts, parsed package/schema checks, or product-owned behavior tests + before they can block CI. +- React Native iOS runtime-resource resolution no longer repeats the + `OliphauntResources` bundle candidate in its native-library fallback. The + React Native package-shape check owns that clean-consumer behavior; `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. +- 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/docs/maintainers/extension-packaging-policy.md b/docs/maintainers/extension-packaging-policy.md new file mode 100644 index 00000000..95b16e7d --- /dev/null +++ b/docs/maintainers/extension-packaging-policy.md @@ -0,0 +1,503 @@ +# Extensions + +Status: normative extension packaging policy. Last verified: 2026-07-16. +Owner: repository maintainers. + +Oliphaunt uses exact, opt-in PostgreSQL extension selection. App developers +select the SQL extension names they intend to ship, and the generated runtime +assets contain only those extension assets plus mandatory manifest +dependencies. `vector` means the PostgreSQL SQL extension named `vector`. +There is no selector expansion, alias, shorthand, or release selector that +expands to multiple extensions. Names such as `core`, `search`, or `geo` are +not Oliphaunt catalog entries or release units. A name is selectable only when +it is an exact PostgreSQL extension name from the built-in catalog or a +verified external artifact index. + +## Release products and carriers + +Release ownership is not the same thing as SQL selection. The 32 promoted +PostgreSQL 18 contrib members belong to one independently tagged distribution +product, `oliphaunt-extension-contrib-pg18`. Its packaging version, changelog, +and compatibility metadata live at `src/extensions/contrib/`; it is +runtime-bound and shares the runtime linked-version group. Contrib member +folders retain exact build, target, and evidence metadata but do not own leaf +versions, changelogs, tags, or package identities. + +Every build-enabled, stable promoted external extension is an independently +tagged release product. Its packaging version, changelog, immutable upstream +source identity, and compatibility metadata live with that product. A catalog +entry with `build = false` or `stable = false` is not a release member/product +and must retain a concrete blocker until it qualifies; it must not acquire +packages merely because its source pin exists. + +Each exact SQL member has an explicit `targets/artifacts.toml`. The file opts +into named target profiles and may add member-specific target rows; the expanded +rows enumerate every published target and any member-specific planned or +unsupported target with evidence references. Targets outside the global release +OS policy are fail-closed and need not be repeated in every extension manifest. +Missing metadata for a claimed target is an error; release tooling must never +infer that an extension supports every runtime target merely because the +runtime supports it. + +Ecosystem packages are carriers, not extra products: a stable Cargo façade, +native/WASIX Cargo leaves, an npm façade plus selected platform packages, +Android ABI Maven artifacts, and SwiftPM/GitHub assets as declared by the +target contract. Contrib carriers are shared by the contrib product and contain +an exact member inventory with nested path, byte count, and checksum for every +SQL member. External products have separate carriers and versions. Every +consumer resolves by SQL name and extracts/stages only the selected member and +its declared dependencies. The publication catalog defines the stable +identities and the frozen publication lock records the actual files. Only +oversized crates.io payload parts may be generated dynamically. + +Physical aggregate carriers use `oliphaunt-extension-bundle-v1`. Their +manifest describes the immutable nested archives and compatibility contract; +every `kind=runtime` member has `identity=null` and is uniquely located by its +SQL name, kind, and nested path. The expanded npm platform package is a +different representation: publication extracts every nested archive into a +per-member runtime tree and writes `oliphaunt-npm-extension-bundle-v1`, adding +`runtimeRelativePath` and, when present, `moduleRelativePath`. These schemas +are deliberately not interchangeable. Published JavaScript consumers reject a +physical carrier manifest where the expanded npm index is required. + +Every direct or bundle npm platform package also carries and exports +`extension-contract.json` with schema `oliphaunt-npm-extension-contract-v1`. +That manifest freezes each member's `createsExtension`, native module stem, +dependencies, data files, additional SQL names/prefixes, and preload libraries +at the independently versioned extension release. JavaScript validates the +exact contract schema and uses it—not the SDK's current catalog snapshot—to +qualify the installed package's runtime leaf inventory. The SDK catalog remains +authoritative for selection, release/package ownership, and dependency +compatibility. + +## Rust + +The release invariant is strict: generated app resources must contain only the +selected exact extensions plus mandatory manifest dependencies. + +```rust,no_run +use oliphaunt::{Extension, Oliphaunt}; + +# async fn demo() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .temporary() + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + +db.execute("CREATE EXTENSION vector").await?; +db.close().await?; +# Ok(()) +# } +``` + +The same rule applies to package tooling: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --output target/oliphaunt-resources \ + --extension vector \ + --force +``` + +Selecting `vector` ships `vector`. It must not ship `hstore`, `pg_trgm`, +`cube`, `earthdistance`, pgGraph, ParadeDB, or any other unselected extension. +The only exception is a mandatory dependency declared by +`NATIVE_EXTENSION_MANIFEST`; for example `earthdistance` includes `cube`. + +End developers should not have to build PostgreSQL or extension source to know +what they can ship. The runtime-resource CLI exposes the release-ready prebuilt +catalog without requiring a local native build: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- --list-extensions +``` + +The catalog is TSV so CI, SwiftPM plugins, Gradle tasks, Expo config plugins, +and release automation can consume it directly. `desktop_prebuilt=yes` means +the extension is available for Rust/Tauri, macOS, Linux, and desktop resource +artifacts from Oliphaunt release artifacts. `mobile_prebuilt=yes` means iOS and +Android apps can include the extension from Oliphaunt prebuilt mobile artifacts +without compiling extension source. `mobile_prebuilt=no` is a hard release +boundary, not a hint to make app developers compile source locally. + +## Prebuilt Third-Party Artifacts + +The open-ended extension path is also exact-name based. A third-party +extension is selected by passing a prebuilt artifact directory or archive, not +by compiling source inside the app project: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --output target/oliphaunt-resources \ + --extension vector \ + --prebuilt-extension vendor/acme_ext.tar.zst \ + --liboliphaunt-native-version 0.1.0 \ + --force +``` + +Artifacts are produced from already-built PostgreSQL runtime files with the +Rust SDK artifact tool: + +```sh +cargo run -p oliphaunt --bin oliphaunt-extension-artifact -- \ + --runtime target/acme-pg18-runtime/files \ + --sql-name acme_ext \ + --native-module-stem acme_ext \ + --native-module-file acme_ext.so \ + --native-target linux-x64-gnu \ + --embedded-module-root target/acme-pg18-embedded/modules \ + --native-runtime-version 0.1.0 \ + --data-file data/acme_ext.rules \ + --output vendor/acme_ext.tar.zst \ + --format tar-zst \ + --force +``` + +For desktop module extensions, `--runtime` supplies the standalone PostgreSQL +module under `lib/postgresql`, while `--embedded-module-root` supplies the +native-direct module. The artifact preserves both profile paths under +`files/lib/postgresql` and `files/lib/modules`; native server consumers select +the former, while native-direct and native-broker consumers select the latter. +Both paths are mandatory, even when their files are byte-identical. + +Binary qualification derives each profile's backend binding from the binary's +actual import inventory, never from the extension name. On Windows, a +host-bound server profile may import `postgres.exe` but not `oliphaunt.dll`, and +a host-bound embedded profile may import `oliphaunt.dll` but not `postgres.exe`. +Crossed provider bindings are always packaging errors. Host-bound profile copies +must also have distinct bytes. A profile that imports neither backend provider +is host-neutral; server and embedded copies may be byte-identical only when both +are host-neutral. Omitting either desktop profile remains a packaging error. + +The command does not build PostgreSQL or extension source. The producer and +consumer share the same schema validation, so the generated artifact is +immediately consumable by `oliphaunt-resources --prebuilt-extension`. + +For release distribution, publish an exact artifact index next to the binary +artifacts: + +```sh +cargo run -p oliphaunt --bin oliphaunt-extension-index -- \ + --output vendor/oliphaunt-extensions.toml \ + --target macos-arm64 \ + --artifact vendor/acme_ext-macos-arm64.tar.zst \ + --base-url https://cdn.example.com/oliphaunt/extensions/macos-arm64 \ + --signing-key-file acme-release-2026q2:keys/acme-extension-index.ed25519 \ + --force +``` + +The index producer validates each artifact manifest, rejects built-in extension +name overrides, computes byte counts and SHA-256 digests, and records relative +artifact paths plus catalog metadata such as dependencies, native module stem, +preload requirements, and mobile-prebuilt readiness. That metadata lets app +tooling list exact external extension names from the index without downloading +or building extension source. `--base-url` additionally records a URL for each +exact artifact row so release tooling can fetch missing artifacts into a cache +before verification. Release indexes should also publish a detached Ed25519 +sidecar signature at `.sig`; `--signing-key-file :` signs +the exact index bytes after writing the TOML. The signing key file contains a +hex-encoded 32-byte Ed25519 signing key. + +```toml +schema = "oliphaunt-extension-artifact-index-v1" +pg_major = 18 + +[[artifacts]] +sql_name = "acme_ext" +target = "macos-arm64" +creates_extension = true +native_module_stem = "acme_ext" +dependencies = [] +shared_preload_libraries = [] +mobile_prebuilt = true +mobile_static_archive_targets = ["ios-simulator", "ios-device", "arm64-v8a"] +path = "acme_ext-macos-arm64.tar.zst" +url = "https://cdn.example.com/oliphaunt/extensions/macos-arm64/acme_ext-macos-arm64.tar.zst" +sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +bytes = 123456 +``` + +Developers can inspect built-in plus signed external availability without a +native build: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --list-extensions \ + --extension-index vendor/oliphaunt-extensions.toml \ + --extension-target macos-arm64 \ + --trusted-extension-index-key-file acme-release-2026q2:keys/acme-extension-index.ed25519.pub +``` + +Then app/package tooling can select the external extension by exact SQL name: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --output target/oliphaunt-resources \ + --extension acme_ext \ + --extension-index vendor/oliphaunt-extensions.toml \ + --extension-target macos-arm64 \ + --extension-cache ~/.cache/oliphaunt/extensions \ + --trusted-extension-index-key-file acme-release-2026q2:keys/acme-extension-index.ed25519.pub \ + --force +``` + +`oliphaunt-resources` verifies the artifact byte count, SHA-256 digest, PG major, +target, and artifact manifest before consuming it. It also follows exact +extension dependencies from the index. Built-in release-ready extension names +cannot be overridden by index entries. Local sidecar artifacts next to the index +are preferred. If a URL-backed artifact is missing locally, `--extension-cache` +downloads it to a target-scoped cache and verifies bytes, SHA-256, and manifest +before packaging. HTTPS artifact downloads are a packaging-tool capability; Rust +SDK release binaries enable the `extension-download` feature, while the embedded +library remains usable without HTTP/TLS dependencies. Signed index verification +uses `--trusted-extension-index-key-file :`, which requires a +matching `.sig` sidecar before any indexed artifact can be used. The key +file contains a hex-encoded 32-byte Ed25519 public key. Signing and verification +are packaging-tool capabilities behind the `extension-signing` feature, so +embedded Rust/Tauri apps do not compile signing code unless they opt into it. + +`--prebuilt-extension` accepts an unpacked artifact directory, `.tar`, +`.tar.gz`, or `.tar.zst`. The artifact root must contain +`manifest.properties` plus a +`files/` runtime tree: + +```properties +packageLayout=oliphaunt-extension-artifact-v1 +pgMajor=18 +sqlName=acme_ext +createsExtension=yes +nativeModuleStem=acme_ext +nativeModuleFile=acme_ext.so +nativeTarget=linux-x64-gnu +nativeRuntimeProduct=liboliphaunt-native +nativeRuntimeVersion=0.1.0 +dependencies= +dataFiles= +extensionSqlFileNames= +extensionSqlFilePrefixes= +sharedPreloadLibraries= +mobilePrebuilt=yes +mobileStaticArchives=android-arm64-v8a:mobile-static/android-arm64-v8a/extensions/acme_ext/liboliphaunt_extension_acme_ext.a,ios-device:mobile-static/ios-device/extensions/acme_ext/liboliphaunt_extension_acme_ext.a,ios-simulator:mobile-static/ios-simulator/extensions/acme_ext/liboliphaunt_extension_acme_ext.a +mobileStaticDependencyArchives=android-arm64-v8a:openssl:mobile-static/android-arm64-v8a/dependencies/openssl/libcrypto.a,ios-device:openssl:mobile-static/ios-device/dependencies/openssl/libcrypto.a,ios-simulator:openssl:mobile-static/ios-simulator/dependencies/openssl/libcrypto.a +staticSymbolPrefix=acme_static +staticSymbolAliases= +files=files +``` + +The v1 manifest has exactly these 20 fields; all fields are present even when +their value is empty, and unknown fields are rejected. `nativeRuntimeProduct` +is always `liboliphaunt-native`. `nativeRuntimeVersion` is a stable `X.Y.Z` +version and must equal the version explicitly selected by +`oliphaunt-resources --liboliphaunt-native-version` (or +`OLIPHAUNT_LIBOLIPHAUNT_VERSION`). The consumer checks every direct and +index-resolved prebuilt artifact before materializing the output resource tree, +so one mismatched artifact also rejects a mixed-version package. + +`files/` mirrors PostgreSQL runtime paths, for example +`files/share/postgresql/extension/acme_ext.control`, +`files/share/postgresql/extension/acme_ext--1.0.sql`, and +`files/lib/postgresql/acme_ext.dylib` on macOS. The runtime-resource generator copies only files +declared by the exact selected extension: matching control/SQL files, declared +`dataFiles`, and the declared native module. Extra files in the artifact are +ignored. A prebuilt artifact cannot override a built-in release-ready extension +name. Dependencies are exact extension names and must resolve either to the +built-in catalog or to another provided prebuilt artifact. + +For mobile, `mobilePrebuilt=yes` on a native-module artifact means the artifact +itself carries matching prebuilt static archives in `mobileStaticArchives`. +The runtime-resource generator copies only selected archives into +`static-registry/archives//extensions//`. Dependency-backed +mobile artifacts can also carry `mobileStaticDependencyArchives` entries, which +the runtime-resource generator copies into +`static-registry/archives//dependencies//`. Android SDK builds +link those dependency archives when present, and the Apple packaging helper +emits matching `liboliphaunt_dependency_.xcframework` outputs for Swift +and React Native CocoaPods consumers. Every extension and dependency +XCFramework contains macOS arm64, iOS device arm64, and iOS simulator arm64 +slices; missing slices fail the binary contract before the archive is admitted. +The generated static-registry source uses +`staticSymbolPrefix` when present; missing selected archives remain build/link +errors. + +## Runtime Resources + +The Rust SDK owns the runtime-resource CLI and manifest contract. + +Runtime resources are shared by Swift, Kotlin, and React Native: + +```text +oliphaunt/ + runtime/ + manifest.properties + files/ + lib/postgresql/ # standalone PostgreSQL server profile + lib/modules/ # embedded direct/broker profile when selected + share/postgresql/ + template-pgdata/ + manifest.properties + files/ + PG_VERSION + package-size.tsv +``` + +The runtime manifest records four distinct, canonical extension domains. Every +CSV is sorted and duplicate-free: + +```properties +schema=oliphaunt-runtime-resources-v1 +layout=postgres-runtime-files-v1 +selectedExtensions=auto_explain,vector +extensions=vector +runtimeFeatures= +sharedPreloadLibraries= +mobileStaticRegistryState=complete +mobileStaticRegistryRegistered=auto_explain,vector +mobileStaticRegistryPending= +nativeModuleStems=auto_explain,vector +``` + +`selectedExtensions` is the full dependency-closed set of SQL identities +materialized into the package. `extensions` is exactly the subset whose +canonical metadata has `creates-extension=true`; module-only entries such as +`auto_explain` therefore remain selected without pretending to support +`CREATE EXTENSION`. `mobileStaticRegistryRegistered` is the selected subset +with native modules, expressed as SQL identities, and `nativeModuleStems` is +the corresponding native stem set. The static-registry manifest must repeat +the registered SQL-name and native-stem domains exactly. A nonempty native +domain requires `mobileStaticRegistryState=complete` with empty pending fields; +an empty one requires `not-required`. + +These are exact identities, not selection aliases or catalog expansions. SDK +availability checks use `selectedExtensions`; they must never use the narrower +createable `extensions` field to decide whether module-only resources exist. +SDKs reject `open(... extensions: ["vector"])` when the selected runtime does +not advertise `vector` in `selectedExtensions`. + +The size report is exact-extension based: + +```text +kind id extensions files bytes +package total - 42 123456 +package runtime - 30 100000 +package template-pgdata - 10 20000 +package static-registry - 2 3456 +extensions selected - 3 63478 +extension vector - 3 63478 +``` + +Swift reads this through `OliphauntRuntimeResources.packageSizeReport()`; +Kotlin reads packaged app assets through +`OliphauntAndroid.packageSizeReport(context)` and unpacked smoke roots through +`OliphauntAndroid.packageSizeReport(resourceRoot)`; React Native delegates +`Oliphaunt.packageSizeReport(...)` to those platform SDK readers. + +## Mobile Static Registry + +iOS and Android cannot rely on arbitrary dynamic extension loading. A mobile +release package that includes module-backed extensions must also include and +register a matching static extension registry: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --output target/oliphaunt-resources \ + --extension vector \ + --mobile-static-module vector \ + --require-mobile-static-registry \ + --force +``` + +`--mobile-static-module` is an assertion that the platform build actually links +the selected module. Unknown or unselected stems fail the package build. +Mobile native build lanes emit one prebuilt archive per selected module at +`out/extensions//liboliphaunt_extension_.a`, so release packaging +can link only the extensions the app selected. +Android SDK builds first consume selected archives carried by the resource +package under `static-registry/archives`; `-PoliphauntAndroidExtensionArchivesDir=` +is the first-party build-output override. The Gradle/CMake build produces an +app-local `liboliphaunt_extensions.so` support library from prebuilt extension +objects plus generated registry glue. That build step links binary artifacts +only; it does not compile PostgreSQL or extension source in the app project. +The iOS XCFramework runtime-resource generator accepts the same Rust runtime-resource output via +`--runtime-resources ` and derives `nativeModuleStems` from +`runtime/manifest.properties`; it uses carried `ios-simulator`/`ios-device` +archives when present and otherwise falls back to first-party build outputs. +There is still only one extension selection list. +The generated registry source deliberately uses strong references for selected +extension magic and SQL entry points. A missing selected prebuilt archive must +fail the app build or link, not degrade into a late runtime `CREATE EXTENSION` +failure. + +## Manifest + +`NATIVE_EXTENSION_MANIFEST` is the PG18 native build inventory. It is not a +public target-support declaration. Each row records: + +- SQL extension name; +- required control, SQL, data, and native module assets; +- mandatory extension dependencies; +- smoke SQL strategy; +- direct, broker, and server coverage expectation; +- mobile static-link status; +- first-party or external artifact policy. + +`Extension::FIRST_PARTY_PG18_SUPPORTED` is the exact inventory of first-party +PG18 rows known to the native SDK. It is not a shipping promise. + +`Extension::RELEASE_READY_PG18_SUPPORTED` is a generated desktop native view +for release packages. A row enters this catalog only when the product's +explicit target manifest and current evidence declare that desktop target +published. There is no contrib/default fallback. +Rows can be first-party inventory without being desktop release-ready. PostGIS +is target-specific rather than a blanket exception: native desktop, mobile, and +WASIX readiness are controlled by their target metadata and build recipes. +PostGIS mobile metadata is target-owned: a mobile row remains candidate until the selected iOS and Android static +dependency archives, hash-dependency sets, runtime data, and smoke evidence are +present. + +External candidates such as pgGraph and ParadeDB remain internal metadata until +they have pinned artifacts, redistribution clearance, and direct, broker, +server, restart, backup, restore, and mobile static-registry evidence. + +`Extension::MOBILE_RELEASE_READY_PG18_SUPPORTED` is the mobile exact-extension +catalog. Release readiness is target-specific: mobile can intentionally be +smaller than desktop native or WASIX support when static archives, dependency +archives, runtime data, or mobile smoke evidence are incomplete. The +runtime-resource CLI rejects attempts to mark a non-mobile-ready module as +complete with `--mobile-static-module`; that prevents apps from shipping a +manifest that claims an extension is linked when the prebuilt mobile artifact +does not exist. + +`pgcrypto` is mobile-prebuilt through the first-party OpenSSL for `pgcrypto` +static `libcrypto` archive. The Windows native producer also builds the pinned +OpenSSL checkout and links `pgcrypto` against the staged static `libcrypto`. +`uuid-ossp` is mobile-prebuilt through the first-party portable UUID static +`libuuid` archive. The Windows native producer links the same portable UUID +source directly into the `uuid-ossp` module and installs the matching +control/SQL files. The Windows native PostGIS producer builds the pinned +GEOS, PROJ, SQLite, json-c, and libxml2 dependency stack, links the generated +`postgis-3` module against those static archives, and stages the matching +extension SQL plus `proj/proj.db`. + +## Target-Specific PG18.4 Readiness + +The generated catalog is a derived view of Oliphaunt-compatible PG18.4 +extension metadata. Product target manifests and evidence own release +readiness. WASIX, +native desktop, and mobile can move independently when their build recipes, +artifacts, smoke evidence, or platform constraints differ. The invariant is +strict: a public selection surface may advertise only the exact extensions that +the selected target can actually package and run. + +Oliphaunt-listed extensions that are not stable stay out of every release-ready +catalog until their PG18.4 blockers are gone. The only current non-stable row is +Apache AGE, because the tracked source still calls PostgreSQL APIs removed in +PG18. PostgreSQL 18.4 can build `uuid-ossp` only with +`--with-uuid=bsd`, `--with-uuid=e2fs`, or `--with-uuid=ossp`. Oliphaunt carries +a first-party portable UUID compatibility source for the e2fs API under +`src/runtimes/liboliphaunt/native/portable-uuid`; the WASIX, Linux/macOS native, +iOS, Android, and Windows native build scripts compile and link it for +`uuid-ossp`. `uuid-ossp` is stable in the generated WASIX plan; WASIX side-module builds and packages with matching archive +and module hashes, has host AOT metadata, and has direct, server, restart, and +dump-restore smoke evidence recorded for the package. diff --git a/docs/maintainers/mobile-stability-model.md b/docs/maintainers/mobile-stability-model.md new file mode 100644 index 00000000..932b6c00 --- /dev/null +++ b/docs/maintainers/mobile-stability-model.md @@ -0,0 +1,167 @@ +# Mobile Stability Model + +This document is the stability contract for the Swift, Kotlin, and React Native +SDKs over `liboliphaunt`. + +The deeper iOS process-model investigation is captured in +`docs/architecture/ios.md`. That document is the source of +truth for why iOS direct mode is the universal fast path and why any robust iOS +broker must be an ExtensionFoundation/AppExtensionProcess design rather than a +normal helper daemon. + +## Current Truth + +`NativeDirect` embeds one PostgreSQL backend in the host app process. Swift and +Kotlin serialize all direct calls through one actor/owner dispatcher. React +Native delegates to those SDKs and uses a New Architecture JSI ArrayBuffer +transport for protocol bytes. + +This is fast, but it is not process isolated. A PostgreSQL `ERROR`, protocol +error, cancellation, or controlled `proc_exit` path can be surfaced as an SDK +error. A native crash such as memory corruption, abort, or unhandled signal is a +host-process crash in direct mode. + +Direct mode's reliability claim is crash consistency, not crash isolation. If +the host process dies, the next app launch must reopen the same root and let +PostgreSQL perform WAL recovery. Direct mode must not be documented or surfaced +as host-app-survivable after native PostgreSQL crashes. + +`close()` is a logical detach in mobile direct mode. It releases the SDK handle, +rolls back any active transaction, runs `DISCARD ALL`, and keeps the resident +backend alive so the same root can be reopened in the same process. It is not a +full PostgreSQL shutdown. Direct mode cannot switch to a different root after +that resident backend exists. + +The SDK should make this resident-runtime model explicit with an app-scope +manager/container. Developers should not need to discover by accident that +`close()` does not make the process reusable for another root. + +Temporary direct roots are therefore process-resident too. The SDKs now reuse one +process-lifetime temporary root so `open(temporary)`, `close()`, and +`open(temporary)` do not accidentally ask the C ABI to switch roots. + +React Native `protocolStream` means true chunked native streaming through JSI. +If the installed JSI transport only has owned-response `execProtocolRaw`, +`execProtocolStream(...)` remains callable as a fallback but reports +`protocolStream=false` and emits one owned response chunk. + +## Platform Constraints + +React Native New Architecture is the correct JS/native boundary. The official +architecture replaces the old asynchronous bridge with JSI and allows JS to hold +references to native objects without serialization costs for database-like +objects. TurboModule Codegen remains the typed lifecycle/control surface; bulk +bytes should stay on JSI. + +Android can support a real mobile broker. A bound service is explicitly designed +for long-lived interaction over `IBinder`, and Android services can be declared +with a separate `android:process`. That gives us a credible crash-isolated +database process for Android apps. + +iOS does not have the same general app-owned daemon model. Normal apps receive +only a short background window before suspension. App extensions do run in +separate processes, and ExtensionFoundation exposes host-launched app-extension +processes with XPC connections on iOS 26+, but this is not the same thing as a +macOS helper service or Android service process. It likely starts as a +single-root broker because one app-extension identity maps to one running +process and the embedded PostgreSQL runtime is still process-wide. Until that +feasibility track passes on real devices, iOS direct mode must be honest: fast +and ergonomic, not crash isolated. + +PostgreSQL's normal robustness assumes a supervisor process and WAL recovery. In +server mode, an immediate shutdown or crash leads to WAL replay on restart. In +direct mode there is no external supervisor around the embedded backend thread; +only a broker/server process can make backend death survivable for the app +process. + +## Product Direction + +### iOS + +Default to `NativeDirect` for the first shippable iOS SDK. Make the contract +explicit: + +- one resident backend per app process; +- one physical session; +- serialized requests; +- same-root logical reopen only; +- no true independent concurrent sessions; +- no crash isolation; +- backgrounding is handled by checkpoint/cancel/close guidance, not by keeping + arbitrary work alive while suspended. + +The robust iOS direction is an opt-in `NativeExtensionBroker` built on +ExtensionFoundation/AppExtensionProcess only if device/App Store testing proves +the extension lifecycle, crash/reconnect behavior, memory ceiling, background +behavior, App Group storage model, and XPC throughput are acceptable. If that +feasibility fails, iOS remains direct plus server unavailable. Even if it +succeeds, it must not advertise desktop-style multi-root broker semantics until +worker multiplicity is proven. + +The product must fail closed here: if the iOS broker extension target, App Group, +minimum OS, or device evidence is missing, `NativeExtensionBroker` is +unavailable. It must not silently alias to direct mode. + +### Android + +Add `NativeBroker` as the recommended robust Android mode: + +- a bound service in `:liboliphaunt` owns roots and direct C ABI handles; +- app/RN process talks to it over Binder; +- one worker per root, serialized per root; +- service crash is observed through binder death, then the SDK reconnects and + reopens the root after WAL recovery; +- in-flight requests fail with unknown commit state unless the SDK has an + explicit idempotent replay envelope; +- `NativeServer` is separate and only for true PostgreSQL client sessions. + +The direct Android mode remains the fastest single-session path and the fallback +where apps do not want a service process. + +### React Native + +React Native should remain an adapter over Swift/Kotlin, but the hot path should +eventually move one layer lower: + +- TurboModule Codegen for typed lifecycle, capabilities, open, close, cancel, + backup/restore metadata, and package-size reporting; +- JSI HostObject/ArrayBuffer for database handles and protocol bytes; +- native chunked JSI streaming for large protocol responses; +- no base64 and no bridge byte transport; +- no private RN database runtime divergent from Swift/Kotlin semantics. + +On Android broker, RN should talk to Kotlin broker handles. On iOS direct, RN +talks to Swift direct handles. If an iOS broker becomes viable, RN should inherit +it through Swift. + +## Required Follow-Up Changes + +1. Add Android `NativeBroker` with a remote-process bound service and binder + death/reconnect tests. +2. Run an iOS 26+ ExtensionFoundation broker feasibility spike on real devices + and document whether App Store-safe single-root process isolation is viable. +3. Add mobile crash drills: direct-mode controlled backend exit, Android broker + service kill/reconnect, app background/foreground with long query + cancellation, and WAL recovery after process death. +4. Keep lifecycle APIs around foreground/background transitions covered on every + SDK surface: `prepareForBackground`, `resumeFromBackground`, bounded + checkpoint, and explicit cancellation policy. + +## References + +- React Native New Architecture: https://reactnative.dev/architecture/landing-page +- React Native Turbo Native Modules: https://reactnative.dev/docs/turbo-native-modules-android +- Android services and bound services: + https://developer.android.com/develop/background-work/services +- Android service manifest attributes: + https://developer.android.com/guide/topics/manifest/service-element +- Apple background execution: + https://developer.apple.com/documentation/uikit/extending-your-app-s-background-execution-time +- Apple ExtensionFoundation: + https://developer.apple.com/documentation/extensionfoundation +- Apple AppExtensionProcess: + https://developer.apple.com/documentation/ExtensionFoundation/AppExtensionProcess +- PostgreSQL server shutdown and recovery: + https://www.postgresql.org/docs/current/server-shutdown.html +- PostgreSQL WAL: + https://www.postgresql.org/docs/current/wal-intro.html diff --git a/docs/maintainers/native-runtime-contract.md b/docs/maintainers/native-runtime-contract.md new file mode 100644 index 00000000..f945e7c9 --- /dev/null +++ b/docs/maintainers/native-runtime-contract.md @@ -0,0 +1,201 @@ +# Native Runtime Guide + +This guide describes the native `oliphaunt` Rust SDK and `liboliphaunt` +runtime. WASIX runtime behavior is documented separately in +[`WASM runtime`](/sdk/wasm/runtime). + +## Choose A Mode + +`NativeDirect` is the lowest-latency embedded mode. It loads `liboliphaunt` in +the host process and owns one resident PostgreSQL backend for the process +lifetime. + +Use it when the Rust SDK owns the database calls and the application wants one +fast embedded PostgreSQL session: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_direct() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .open() + .await?; + +let rows = db.query("SELECT 1::text AS value").await?; +assert_eq!(rows.get_text(0, "value")?, Some("1")); + +db.close().await?; +# Ok(()) +# } +``` + +`NativeBroker` runs the same direct engine in a helper process. It is the robust +desktop/app mode for process isolation and multiple roots managed by one Rust +SDK runtime. Each broker-owned root still has one serialized physical +PostgreSQL backend session. + +Use it when process isolation and multi-root ownership matter more than absolute +minimum call overhead: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_broker() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_broker() + .broker_max_roots(4) + .open() + .await?; + +db.execute("CREATE TABLE IF NOT EXISTS events(id bigint PRIMARY KEY)").await?; +db.close().await?; +# Ok(()) +# } +``` + +`NativeServer` starts a real local PostgreSQL-compatible server process. It is +the only SDK mode for independent client sessions, connection pools, `psql`, +`pg_dump`, ORMs, and libraries that expect a PostgreSQL connection string: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_server() -> oliphaunt::Result { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_server() + .max_client_sessions(8) + .open() + .await?; + +Ok(db.connection_string().expect("server mode exposes a URL").to_owned()) +# } +``` + +## Runtime Semantics + +The three modes are intentionally different. The SDK must not fake server +semantics in direct or broker mode. + +| Mode | Process model | Session model | Root model | Reopen/crash behavior | +| --- | --- | --- | --- | --- | +| `NativeDirect` | in-process | one serialized physical session | one resident root per process | same-root logical reopen only; no crash isolation | +| `NativeBroker` | helper process per active root | one serialized physical session per root | multiple roots bounded by `broker_max_roots` | helper crash can be restarted; app process remains alive | +| `NativeServer` | PostgreSQL server process | independent PostgreSQL client sessions | one server root per opened handle | use normal server restart/recovery flows | + +`Oliphaunt` is cloneable as an SDK handle. Clones share the same owner executor, +FIFO queue, session pin, cancellation handle, and close state. Cloning is not a connection pool. +Direct and broker mode reject `max_client_sessions` values other than `1`; +server mode is the independent-session mode. + +Transactions and explicit session pins reserve the single SDK-owned physical +session. Unpinned database work, backup, restore-adjacent work, and checkpoints +are rejected while a pin is active so direct and broker calls cannot interleave +inside one transaction-sensitive PostgreSQL session. + +## Direct Lifecycle + +Direct mode is process-resident: + +- one resident backend per process; +- one physical session; +- serialized requests through the SDK owner executor; +- one root per process after the resident backend exists; +- `close()` is a logical detach, not full PostgreSQL shutdown; +- reopening is same-root only inside the same process; +- native PostgreSQL crashes terminate the host process. + +On desktop Unix, `oliphaunt_init` promotes the loaded `liboliphaunt` image to +process-global dynamic-loader scope before PostgreSQL starts. Ordinary +PostgreSQL extension DSOs resolve backend globals from that image, including +when an FFI host initially loaded it locally. SDK loaders that expose flags also +request `RTLD_GLOBAL` directly. This makes one embedded PostgreSQL image a hard +process contract: co-loading another embedded PostgreSQL symbol provider or +placing `liboliphaunt` in a caller-created `dlmopen` namespace is unsupported. +Use broker or server mode when isolation from another PostgreSQL image is +required. + +The reliability contract is crash consistency, not crash isolation. If the host +process dies, the next launch reopens the same root and PostgreSQL performs WAL +recovery. Applications that need app-process survival after database-process +death should use broker/server modes where the target platform supports them. + +## Storage + +Native live storage is a PostgreSQL root directory, not a single file. A root +contains PGDATA, Oliphaunt metadata, lock metadata, extension metadata, and +recovery state. + +Persistent roots use exclusive locking in direct mode. Broker and server modes +own their roots through the helper/server process. A second unsafe owner fails +instead of sharing a data directory. + +Use SDK backup/restore APIs for ergonomic export/import: + +- direct and broker support same-version physical archives; +- server supports same-version physical archives and SQL dumps through packaged + PostgreSQL tooling; +- physical archives are for same-version restore, not cross-version upgrades. + +## Startup Configuration + +`OliphauntBuilder::runtime_footprint(...)` selects the startup footprint before +PostgreSQL starts: + +- `RuntimeFootprintProfile::Throughput`: throughput defaults; +- `RuntimeFootprintProfile::BalancedMobile`: lower slot counts, smaller shared + buffers/WAL footprint, and PG18 sync I/O for resident mobile apps; +- `RuntimeFootprintProfile::SmallMobile`: the smallest supported resident + profile for memory-pressure experiments. + +`OliphauntBuilder::startup_guc(name, value)` and `startup_gucs(...)` append +validated PostgreSQL `-c name=value` overrides after durability and footprint +profiles. Later overrides win, matching PostgreSQL startup behavior. Server mode +then appends its configured `max_connections` from `max_client_sessions(...)` +because independent session count is the server-mode contract. + +## Extensions + +Extensions are opt-in. Select exact PostgreSQL extension names before opening: + +```rust,no_run +use oliphaunt::{Extension, Oliphaunt}; + +# async fn open_with_vector() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + +db.execute("CREATE EXTENSION IF NOT EXISTS vector").await?; +db.close().await?; +# Ok(()) +# } +``` + +`CREATE EXTENSION` succeeds only when the selected runtime resources contain +the extension assets and, on mobile, when the required static registry entries +are present. Desktop runtimes load only the selected, package-validated +extension modules from that resource set; arbitrary system modules and a second +embedded PostgreSQL symbol provider remain outside the supported contract. + +## Capabilities + +Use capabilities instead of assuming a mode can do everything: + +- `session_concurrency` distinguishes serialized SDK sessions from independent + server sessions; +- `multi_root` is broker-only today; +- `same_root_logical_reopen`, `root_switchable`, and `crash_restartable` + describe lifecycle semantics explicitly; +- `backup_formats` and `restore_formats` gate backup/restore UI before work is + queued. + +Swift, Kotlin, and React Native expose the same product concepts with +platform-native naming. Unsupported platform modes should report explicit +unsupported reasons rather than aliasing to direct mode. diff --git a/docs/maintainers/performance-evidence.md b/docs/maintainers/performance-evidence.md new file mode 100644 index 00000000..198ea7d7 --- /dev/null +++ b/docs/maintainers/performance-evidence.md @@ -0,0 +1,642 @@ +# Performance + +`oliphaunt-wasix` is built to stay close to native Postgres while keeping the +database embedded in the Rust process. + +This page tracks the repo benchmark matrix. The main comparison uses SQLx on +each wire-protocol path: + +- native Postgres with SQLx; +- `oliphaunt-wasix + SQLx`; +- vanilla `@electric/wasm` persisted with NodeFS and reached through + `@electric/wasm-socket`, then measured with SQLx. + +The native `oliphaunt` track has its own matrix for PostgreSQL 18 direct, +broker, and server modes. That matrix is the release gate for the native SDK and +must be used before claiming native parity: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh +``` + +Native server mode keeps the public PostgreSQL-compatible TCP connection string, +but SDK-owned protocol traffic uses Unix-domain sockets on Unix by default. Set +`OLIPHAUNT_SERVER_SDK_TRANSPORT=tcp` only when explicitly diagnosing TCP +transport behavior. + +It records p50/p90/p95/p99 latency, suite totals, throughput, `/usr/bin/time` +CPU/RSS/footprint metrics, child-process RSS for broker/server modes, artifact +sizes, native PostgreSQL controls, a SQLite embedded speed control, +prepared-update rows, and backup/restore timings for native PostgreSQL, SQLite, +NativeDirect, NativeBroker, and NativeServer. The speed and backup/restore +sections report p50 elapsed time, p90 elapsed time, p95 elapsed time, median +throughput, tail throughput, p99 tail latency, native-PostgreSQL p90 ratios, +and command-level CPU/RSS/footprint p90/p99 so transport and persistence +regressions are visible without opening the raw JSON files. + +When NativeDirect misses a native PostgreSQL gate, the generated report includes +a `Native Direct Regression Diagnostics` section with the missed gate, the +matching focused matrix command, and a repeated speed-case diagnostic wrapper +that runs NativeDirect as one fresh process per case/repeat before comparing it +with the native PostgreSQL control. The lower-level `perf diagnose-speed-cases` +commands remain available for one-off inspection. + +The native matrix is native-only by default. The script builds `xtask` with +the `perf` feature explicitly enabled, avoiding the legacy `oliphaunt-wasix` +runtime-control path while still building the native broker helper: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh \ + --rtt-repeats 1 \ + --speed-repeats 1 +``` + +For an even faster no-build sanity check of the benchmark plan: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh --quick --plan-only +tools/perf/matrix/run_native_oliphaunt_matrix.sh \ + --quick --plan-only --engines broker --suites streaming +tools/perf/check-native-perf-harness.sh +``` + +Use `--engines direct|broker|server|all` and +`--suites rtt|speed|streaming|prepared|backup|all` for focused diagnostic runs. +Focused runs still include the relevant native PostgreSQL control for the +selected suite, but the generated report marks them as partial coverage. They +are not release evidence. + +Use `--runtime-footprint throughput|balanced-mobile|small-mobile` and repeated +`--startup-guc name=value` flags for mobile footprint experiments. The same +tuning is passed to NativeDirect, NativeBroker, NativeServer, and the native +PostgreSQL control, and the JSON/report/provenance files record the effective +profile and overrides: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh \ + --quick \ + --durability balanced \ + --runtime-footprint balanced-mobile \ + --startup-guc shared_buffers=32MB \ + --startup-guc wal_buffers=-1 +``` + +The default PostgreSQL 18 template uses 16MB WAL segments, so +`min_wal_size=8MB` and `min_wal_size=16MB` are invalid for the default mobile +cluster. WAL segment size is an `initdb`/template-cluster property, not a +startup GUC. For the small-WAL mobile experiments, run the Expo matrix with a +matching template segment size: + +```sh +tools/perf/matrix/run_mobile_footprint_matrix.sh --quick --platform android \ + --wal-segsize 4 \ + --min-wal-size 8MB,16MB \ + --max-wal-size 32MB,64MB \ + --durability balanced \ + --crash-recovery off +``` + +The harness passes `OLIPHAUNT_EXPO_MOBILE_WAL_SEGSIZE_MB` into the Android/iOS +dev-client scripts, regenerates the packaged template PGDATA with +`initdb --wal-segsize`, records `walSegmentSizeMB` in the template manifest, and +captures PostgreSQL's effective read-only `wal_segment_size` setting in the +benchmark report. + +For Android/iOS device sweeps, use the Expo dev-client matrix wrapper. It emits +or runs the same runtime-footprint, shared-buffer, WAL-buffer, WAL-minimum, +WAL-maximum, and Safe/Balanced combinations against the installed React Native +app. The default profile is `balancedMobile`; pass `--runtime-footprint all` to +run `throughput`, `balancedMobile`, and `smallMobile` under the same GUC axes. +Non-plan runs store every case in its own scratch directory and write +`summary.json` plus `summary.md` under `target/perf/mobile-footprint-/` +with open time, warm query p50/p90/p95/p99, bulk insert/update, background +checkpoint latency, Android PSS/RSS, and iOS resident memory where the platform +harness can collect them. Package footprint is reported at three separate +levels: the Oliphaunt embedded payload reported by the app, the built Android +APK or iOS app bundle, and the local React Native package tarball used by the +dev-client app. Benchmark reports also include a same-device Expo SQLite WAL +baseline, including simple-query, parameterized-query, indexed lookup, indexed +aggregate, update, checkpoint, large-result, and insert-throughput measurements +using the same durability label, so mobile SQLite comparison is device evidence +instead of inferred from the host matrix. Each native benchmark report also +records effective PostgreSQL settings through `current_setting(..., true)`, and +the matrix summary surfaces the core effective GUCs next to the intended startup +overrides. Treat measurements without those effective settings as incomplete +tuning evidence. React Native benchmark reports include app-reported process +memory via `Oliphaunt.processMemory()`: iOS records Mach task resident and +physical-footprint bytes, and Android records `Debug.MemoryInfo` PSS plus heap +fields. The matrix summary prefers this in-app report and uses `devicectl` or +`adb` process scraping only as additional harness evidence. Missing process +memory data leaves iOS resident memory blank rather than recording a false zero. +By default safe-durability matrix cases +also run the installed-app +process-death recovery lane. Balanced cases keep `synchronous_commit=off`, so +they remain latency/footprint evidence rather than last-commit survival gates. +Use `--crash-recovery off` only for a diagnostic latency-only sweep: + +```sh +tools/perf/matrix/run_mobile_footprint_matrix.sh --plan-only --platform android +tools/perf/matrix/run_mobile_footprint_matrix.sh --plan-only --platform android --runtime-footprint all +tools/perf/matrix/run_mobile_footprint_matrix.sh --quick --platform android \ + --shared-buffers 8MB,32MB,128MB \ + --wal-buffers -1 \ + --min-wal-size 32MB \ + --max-wal-size 64MB \ + --durability balanced \ + --crash-recovery off +tools/perf/matrix/run_mobile_footprint_matrix.sh --quick --platform ios --crash-recovery off +tools/perf/matrix/run_mobile_footprint_matrix.sh --platform ios +``` + +`--quick` keeps the same GUC/profile axes but passes +`OLIPHAUNT_EXPO_MOBILE_BENCHMARK_PRESET=quick` into the Expo dev-client app so +the installed-app workload uses fewer warmup, latency, checkpoint, insert, and +large-result iterations. Use it for harness validation and emulator/simulator +sanity checks; use the default full preset for reportable numbers. +Use `--shared-buffers`, `--wal-buffers`, `--min-wal-size`, `--max-wal-size`, +`--wal-segsize`, and `--durability` to run a small slice with the same +installed-app harness before committing to the full device matrix. + +Current diagnostic Android emulator slice: + +- run id: `android-guc-slice-20260524T1750` +- report: `target/perf/mobile-footprint-android-guc-slice-20260524T1750/summary.md` +- platform: Android API 34 emulator through the Expo dev-client harness +- benchmark preset: `quick` +- fixed settings: `balancedMobile`, `balanced`, + `wal_buffers=-1`, `min_wal_size=32MB`, `max_wal_size=64MB` + +| shared_buffers | Android PSS | Android RSS | Open ms | Param p90 ms | Insert rows/s | Checkpoint p90 ms | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 8MB | 253.7 MB | 383.1 MB | 7695.68 | 39.38 | 57 | 1457.90 | +| 32MB | 256.6 MB | 386.3 MB | 6347.87 | 41.92 | 77 | 1480.58 | + +This is diagnostic emulator evidence, not a release claim. It does show that +lowering `shared_buffers` from 32MB to 8MB does not currently buy a proportional +resident-memory reduction in the React Native app process; fixed mappings, +runtime/template assets, extension registry, or other PostgreSQL/React Native +process costs are still dominating the measured PSS/RSS. Keep the full device +matrix and source/build-cut investigations separate from this quick slice. + +Current diagnostic Android emulator small-WAL slice: + +- run id: `android-small-wal-20260524T1833` +- report: `target/perf/mobile-footprint-android-small-wal-20260524T1833/summary.md` +- platform: Android API 34 emulator through the Expo dev-client harness +- benchmark preset: `quick` +- fixed settings: `balancedMobile`, `balanced`, `shared_buffers=32MB`, + `wal_buffers=-1`, `max_wal_size=32MB`, `--wal-segsize 4` + +| min_wal_size | Effective wal_segment_size | Android PSS | Open ms | Param p90 ms | Lookup p90 ms | Aggregate p90 ms | SQLite param p90 ms | SQLite lookup p90 ms | SQLite aggregate p90 ms | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 8MB | 4MB | 257.9 MB | 11223.12 | 45.02 | 40.72 | 26.98 | 79.90 | 67.94 | 733.46 | +| 16MB | 4MB | 260.5 MB | 22116.32 | 71.35 | 37.20 | 47.34 | 78.57 | 34.45 | 31.58 | + +This proves the harness can build and run an installed Android package against +template clusters with 4MB WAL segments and verify the effective PostgreSQL +`wal_segment_size`. The open-time spread is too noisy to treat this quick +emulator slice as a tuning decision; use it as harness evidence and run the full +physical-device matrix before picking the mobile default. + +Latest Android emulator retry caveat: + +- run id: `android-emulator-shared-minwal-slice-20260525T0325` +- report: + `target/perf/mobile-footprint-android-emulator-shared-minwal-slice-20260525T0325/summary.md` +- result: one quick `shared_buffers=8MB,min_wal_size=8MB` case passed with + app-reported `android-debug-memory-info`; the matching `min_wal_size=32MB` + case did not produce benchmark evidence. +- passed case: `271,565 KB` app PSS, `396,424 KB` host RSS, `41,415.89 ms` + open, `286.18 ms` parameterized p90, `7.98 rows/s` insert throughput, + `126.77 ms` checkpoint p90, and `34.1 MB` embedded payload. + +A focused `min_wal_size=32MB` retry after adding a bounded +`Linking.getInitialURL()` path in the Expo example still failed before the +React Native app attached: Android killed the app process for `failed to attach` +/ `start timeout`, and Metro never served a bundle. Treat this as local AVD +instability, not PostgreSQL tuning evidence. Physical Android device evidence is +still required before Android defaults can be selected. + +Current diagnostic iOS simulator small-WAL slice: + +- run id: `ios-small-wal-20260524T1855` +- report: `target/perf/mobile-footprint-ios-small-wal-20260524T1855/summary.md` +- platform: iOS 18.0 simulator through the Expo dev-client harness +- benchmark preset: `quick` +- fixed settings: `balancedMobile`, `balanced`, `shared_buffers=32MB`, + `wal_buffers=-1`, `max_wal_size=32MB`, `--wal-segsize 4` + +| min_wal_size | Effective wal_segment_size | iOS RSS | Open ms | Param p90 ms | Lookup p90 ms | Aggregate p90 ms | SQLite param p90 ms | SQLite lookup p90 ms | SQLite aggregate p90 ms | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 8MB | 4MB | 380.1 MB | 1109.52 | 0.50 | 0.67 | 0.95 | 0.94 | 0.84 | 0.94 | +| 16MB | 4MB | 382.2 MB | 1534.13 | 0.98 | 1.67 | 1.91 | 1.78 | 1.66 | 1.47 | + +This proves the iOS harness can package the same 4MB-WAL template and capture +effective GUCs, package size, resident memory, and same-device SQLite baselines. +It is simulator evidence only. Physical iOS benchmark runs additionally require +a valid Apple Development signing identity or a working Xcode account that can +create one through automatic provisioning. + +Current iPhoneOS build-only device-artifact evidence: + +- scratch: `target/oliphaunt-expo-ios-device-buildonly-20260524T1615` +- mode: `OLIPHAUNT_EXPO_IOS_SDK=iphoneos`, + `OLIPHAUNT_EXPO_IOS_BUILD_ONLY=1`, + `OLIPHAUNT_EXPO_IOS_CODE_SIGNING_ALLOWED=NO` +- result: Xcode `Debug-iphoneos` build succeeded using the local + `liboliphaunt.xcframework` iPhoneOS slice +- bundled Oliphaunt resources: 1,874 files, 35,800,256 bytes +- iOS app bundle: 184,075,464 bytes +- packed React Native package: 14,015,379 bytes + +This is compile/package evidence only. It proves the iPhoneOS artifact, +resource bundle, React Native local iOS pod integration, and New Architecture +generated code compile without relying on a runnable device. It is not runtime +performance evidence; physical install/launch still requires Developer Mode, +Developer Disk Image services, and valid signing on the paired phone. + +Current physical iPhone install/runtime/benchmark evidence: + +- scratch: `target/oliphaunt-expo-ios-device-crash-safe-smallwal-20260524T174847` +- runtime smoke scratch: + `target/oliphaunt-expo-ios-device-smoke-autolifecycle-20260524T0018` +- latest reuse-installed runtime smoke: + `target/oliphaunt-expo-ios-smoke/reports/smoke-report.json` +- quick footprint matrix scratch: + `target/perf/mobile-footprint-ios-physical-memory-retry-20260525T0230` +- full candidate footprint matrix scratch: + `target/perf/mobile-footprint-ios-physical-full-candidate-20260525T0200` +- device: iPhone 14 Pro, UDID `7C01EC26-8B01-56E6-872D-82BB72421567` +- mode: `OLIPHAUNT_EXPO_IOS_SDK=iphoneos`, + `OLIPHAUNT_EXPO_MOBILE_DURABILITY=safe`, + `OLIPHAUNT_EXPO_MOBILE_RUNTIME_FOOTPRINT=balancedMobile`, + `OLIPHAUNT_EXPO_MOBILE_WAL_SEGSIZE_MB=4` +- startup GUCs: + `shared_buffers=32MB,wal_buffers=-1,min_wal_size=8MB,max_wal_size=32MB` +- result: Xcode `Debug-iphoneos` build succeeded and `devicectl device install + app` installed bundle ID `dev.oliphaunt.reactnative.example` +- bundled Oliphaunt resources: 1,871 files, 35,799,044 bytes +- selected extension: `vector`, 38 files, 63,478 bytes +- iOS app bundle: 183,420,535 bytes +- packed React Native package: 14,008,184 bytes +- crash recovery: passed on the physical iPhone with app-private + `app-support://...` storage; verify reopened the recovered root in + `146.99 ms` and read back `crash-ios-12452656` +- smoke/runtime: passed on the physical iPhone after the harness automatically + backgrounded the app through Safari and foregrounded it again. The smoke + covered `SELECT 1`, parameterized query, DDL, DDL event triggers, pgvector, + extension selection, transaction/savepoint recovery, constraint error + recovery, JSONB/arrays, recursive CTE/window functions, raw protocol + streaming, query cancellation/recovery, checkpoint/physical backup, and + background/foreground resume SQL. +- smoke timings: open `1360.33 ms`, select p50/p90/p99 + `0.23/0.25/0.57 ms`, backup payload `33,425,920` bytes, lifecycle SQL + after foreground `27.43 ms` +- latest reuse-installed smoke after the bounded launch-URL change opened in + `1357.67 ms`, reported select p90 `0.245 ms`, passed the + `active -> inactive -> background -> active` lifecycle SQL check, and reported + an embedded payload of `35,799,044` bytes. +- full candidate footprint matrix: passed two physical-device cases with + `shared_buffers=32MB`, `wal_buffers=-1`, `min_wal_size=8MB`, + `max_wal_size=32MB`, 4MB WAL segments, and the full benchmark preset. Safe + durability reported open `1386.29 ms`, raw p90 `0.04 ms`, typed p90 + `0.08 ms`, parameterized p90 `0.09 ms`, insert throughput `9686 rows/s`, + checkpoint p90 `1.13 ms`, large-result p90 `0.81 ms`, process-death + recovery elapsed `127.00 ms`, and recovery open `102.61 ms`. Balanced + durability reported open `1466.92 ms`, raw p90 `0.04 ms`, + typed p90 `0.09 ms`, parameterized p90 `0.10 ms`, insert throughput + `9726 rows/s`, checkpoint p90 `1.15 ms`, and large-result p90 `0.80 ms`. +- same-device SQLite baseline in that full candidate matrix: Safe open + `6.30 ms`, parameterized p90 `0.17 ms`, insert throughput `6855 rows/s`, + large-result p90 `4.90 ms`; Balanced open `6.16 ms`, parameterized p90 + `0.16 ms`, insert throughput `6910 rows/s`, large-result p90 `4.97 ms`. +- app-reported iOS process memory source: `ios-task-vm-info`. Safe reported + `253.3 MB` resident and `153.3 MB` physical footprint; Balanced reported + `199.8 MB` resident and `137.5 MB` physical footprint. + +Current physical iPhone shared-buffer/min-WAL tuning slice: + +- run id: `ios-physical-shared-minwal-slice-20260525T0300` +- report: + `target/perf/mobile-footprint-ios-physical-shared-minwal-slice-20260525T0300/summary.md` +- device: same iPhone 14 Pro physical dev-client install +- platform: iPhoneOS through the Expo dev-client harness +- benchmark preset: `quick` +- fixed settings: `balancedMobile`, `balanced`, `wal_buffers=-1`, + `max_wal_size=32MB`, `--wal-segsize 4`, process-death recovery off +- varied settings: `shared_buffers=8/16/32/64/128MB`, + `min_wal_size=8/16/32MB` +- result: 15 cases passed, 0 failed; every row recorded PostgreSQL effective + GUCs and app-reported `ios-task-vm-info` memory. + +| shared_buffers | effective wal_buffers | footprint median MB | footprint min-max MB | RSS median MB | open median ms | param p90 median ms | insert median rows/s | +| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 8MB | 256kB | 135.5 | 135.3-136.2 | 202.7 | 1341.12 | 0.10 | 8710 | +| 16MB | 512kB | 136.1 | 135.9-136.3 | 198.4 | 1331.04 | 0.10 | 8972 | +| 32MB | 1MB | 137.0 | 136.9-137.3 | 199.4 | 1310.48 | 0.10 | 8907 | +| 64MB | 2MB | 139.5 | 139.3-141.0 | 205.4 | 1346.28 | 0.10 | 8923 | +| 128MB | 4MB | 142.8 | 142.4-146.3 | 208.5 | 1363.92 | 0.10 | 8858 | + +This quick physical-device slice shows that `min_wal_size=8/16/32MB` does not +materially move app physical footprint for this small workload. Lowering +`shared_buffers` from 128MB to the 8-32MB band saves roughly 6-11MB of physical +footprint on this iPhone, but it does not collapse total process footprint +because the embedded Postgres/runtime/template baseline still dominates. Treat +this as tuning evidence for the mobile default shape, not as full release +evidence; Safe durability, additional `wal_buffers` values, runtime footprint +profiles, and physical Android still need corresponding device evidence. + +The iPhoneOS `liboliphaunt.xcframework` used for this run also has a stricter +artifact gate: the device and simulator slices are rejected if they import +mobile-forbidden SysV/POSIX shared-memory or semaphore APIs (`shm*`, +`shm_open`, or external `sem*`). This was added after a real-device `SIGSYS` +crash report showed PostgreSQL reaching `shmget` during embedded startup. + +By default the wrapper skips `min_wal_size` values below two WAL segments. With +the default `--wal-segsize 16`, that skips 8MB and 16MB. With +`--wal-segsize 4`, both become valid and are included. Pass +`--include-invalid-wal-min` only for negative validation. The wrapper also skips +impossible WAL ranges such as `max_wal_size=32MB` with `min_wal_size=80MB`, +while preserving a `max_wal_size=default` baseline for the current +throughput-sized WAL ceiling. + +Crash recovery after process death is measured by the installed-app crash lanes, +which write to a persistent app-private root, terminate the app without closing +the direct-mode database, relaunch, and verify committed data through +PostgreSQL recovery. The default crash invocation uses safe durability; do not +interpret balanced/synchronous-commit-off runs as committed-row survival +evidence: + +```sh +pnpm --dir src/sdks/react-native/examples/expo run crash:android +pnpm --dir src/sdks/react-native/examples/expo run crash:ios +``` + +Use the default script invocation for release evidence. Native release gates are +read against native PostgreSQL controls. The matrix plan labels runs with +`releaseEvidence`, `partialReport`, and `diagnosticRun` before any expensive +work starts. Default runs must meet the current release minimums: 100 RTT +samples, 10 fresh-process RTT repeats, 25,000 prepared-update rows, 10 +fresh-process prepared repeats, 20 fresh-process speed repeats, and 10 +fresh-process backup/restore repeats across the default direct/broker/server and +rtt/speed/streaming/prepared/backup matrix. Quick or focused runs are diagnostic +evidence only, even when they are useful for investigating a regression. + +Each native matrix run writes `provenance.json` next to `report.md`. The +provenance file records the benchmark source set, PostgreSQL patch/build inputs, +Rust SDK sources, `xtask`, and native artifacts by SHA-256. Verify an existing +run before using it as release evidence: + +```sh +OLIPHAUNT_PERF_RUN_DIR="$PWD/target/perf/native-liboliphaunt-" \ +tools/perf/check-native-perf-report.sh +``` + +This validation rejects diagnostic and partial reports by default. To verify +only the source/artifact provenance of a focused diagnostic run, set +`OLIPHAUNT_PERF_ALLOW_DIAGNOSTIC=1`; do not use that mode for release +claims or updates to the latest complete matrix section. + +Use the focused native diagnostic when a specific speed case misses the native +control and needs repeat evidence: + +```sh +tools/perf/matrix/run_native_speed_diagnostics.sh --ids 1,2,2.1 --repeats 10 --skip-build +``` + +It writes `summary.json` and `summary.md` under +`target/perf/native-speed-diagnostics-/`. Use the lower-level command +when you need a single raw diagnostic case: + +```sh +LIBOLIPHAUNT_PATH="$PWD/target/liboliphaunt-pg18/out/liboliphaunt.dylib" \ +OLIPHAUNT_INSTALL_DIR="$PWD/target/liboliphaunt-pg18/install" \ +cargo run -p oliphaunt-perf -- \ + diagnose-speed-cases --engine native-liboliphaunt --ids 3 +``` + +Native direct diagnostics run one case per process because the embedded backend +has a single safe process lifetime. Diagnostic output includes the engine +process model and key PostgreSQL GUCs so direct-mode misses can be separated +from control mismatch. The same command supports `--engine native-postgres`; it +uses `OLIPHAUNT_POSTGRES` / `OLIPHAUNT_INITDB` or the repo's +`target/liboliphaunt-pg18/install/bin` tools when present, with `--postgres-bin` +and `--initdb-bin` available for explicit overrides. PGDATA template hydration +defaults to physical byte-copy because local matrix evidence showed better p90 +stability than APFS clone-on-write. Set +`OLIPHAUNT_PGDATA_COPY_MODE=prefer-clone` only when investigating +clone-on-write behavior explicitly. + +The SQLite control is part of the same matrix by default and can be run +directly for a quick embedded baseline: + +```sh +cargo run -p oliphaunt-perf -- \ + sqlite --suite speed --speed-source oliphaunt --durability safe +``` + +`safe`, `balanced`, and `fast-dev` map to explicit SQLite PRAGMAs inside `oliphaunt-perf`, +so SQLite numbers are recorded as product comparison data rather than inferred +from a separate tool. + +Most recent recorded complete native track matrix: + +- run id: `20260524T090412Z` +- report: `target/perf/native-liboliphaunt-20260524T090412Z/report.md` +- provenance: `target/perf/native-liboliphaunt-20260524T090412Z/provenance.json` +- PostgreSQL control: `postgres (PostgreSQL) 18.4` +- native durability profile: `safe` +- native runtime footprint profile: `throughput` +- PGDATA template hydration: `copy` +- RTT samples: `100` +- RTT repeats: `10` +- prepared-update repeats: `10` +- speed repeats: `20` +- backup/restore repeats: `10` +- provenance verification: passed for that recorded source/artifact set; rerun + the full matrix before making current-checkout release claims after the + later backup ABI/tar-writer changes. + +Key results from that run: + +| Metric | NativeDirect | NativeBroker | NativeServer | Native Postgres control | SQLite embedded | +| --- | ---: | ---: | ---: | ---: | ---: | +| RTT repeat gate p90 | 107 us | 124 us | 127 us | 112 us | n/a | +| Speed suite p90 | 2.668 s | 2.629 s | 2.452 s | 2.419 s | 3.871 s | +| Backup/restore physical p90 | 0.558 s | 0.62 s | 0.567 s | 0.344 s | 0.005 s | +| Backup payload p50 | 56.17 MB | 56.17 MB | 56.17 MB | 56.17 MB | 1.31 MB | +| Backup tail throughput p10 | 201.3 MB/s | 181.1 MB/s | 198.1 MB/s | 326.5 MB/s | 483.9 MB/s | +| Open p90 | 440.28 ms | 384.11 ms | 423.18 ms | 576.4 ms | 0.89 ms | +| p90 RSS | 123.5 MB | 107.9 MB process / 109.5 MB observed helper | 93.1 MB process / 138.5 MB observed server | 90.6 MB process / 128.7 MB observed server | 36.9 MB | + +The packaged-template bootstrap path still fixes the prior native open-time +miss: direct open p90 is now `0.764x` native PostgreSQL control open p90, with +broker and server also below the native control. PGDATA template hydration +defaults to byte-copy because same-source evidence showed better p90 stability +than `prefer-clone`. + +This run is more defensible than the older one-shot RTT snapshots because the +harness now runs 10 fresh-process RTT repeats and gates RTT on p90 across +repeated median-p90 summaries. It also still requires at least 20 fresh-process +speed repeats before classifying speed tail stability as release-grade. Under +the speed-quality rule, direct, broker, native PostgreSQL tokio, and SQLite are +`stable`; server is also `stable` on this host run. + +Native direct passes the repeated RTT, open p90, and RSS gates, but it does not +yet pass the speed-suite or physical-backup gates. RTT gate p90 is `107 us` +versus native PostgreSQL tokio at `112 us` (`0.955x`). Speed-suite p90 is +`2.668 s` versus native PostgreSQL tokio at `2.419 s` (`1.103x`), and speed +tail throughput p10 is `63,420.4 ops/s` versus `69,938.9 ops/s` (`0.907x`). +Backup/restore now has a same-semantics native PostgreSQL physical control: +direct p90 is `0.558 s` versus native PostgreSQL physical at `0.344 s` +(`1.622x`), with equal `56.17 MB` p50 payloads. Direct backup tail throughput +p10 is `201.3 MB/s` versus native PostgreSQL physical at `326.5 MB/s` +(`0.617x`). The logical `pg_dump`/`pg_restore -Fc` control still appears in the +backup table as portability comparison data; its p90 is `0.149 s` with a +`1.27 MB` p50 payload. + +A focused post-report backup diagnostic at +`target/perf/native-liboliphaunt-20260524Tbackup-final-direct/report.md` +uses matching current-source provenance but is intentionally partial. It covers +only NativeDirect plus native PostgreSQL backup/restore with 10 repeats after +the C ABI gained `oliphaunt_backup_ex`, in-archive SDK metadata append, direct +`read(2)` file copying, and per-entry tar buffer reservation. That run improves +the direct physical backup/restore p90 to `0.534 s`, but native PostgreSQL +physical remains `0.324 s`, so the backup gate is still a real miss. Opt-in +`OLIPHAUNT_TRACE_BACKUP=1` phase tracing shows the remaining direct cost is +concentrated in PostgreSQL `pg_backup_start` and PGDATA archive generation, not +Rust-side metadata annotation or FFI response copying. + +Individual speed cases above the 5% tolerance in the complete matrix are `1`, +`2`, `2.1`, `3`, `3.1`, `4`, `5`, `10`, and `13`; the generated report includes +focused diagnostic commands for those ids. A follow-up fresh-process diagnostic run +stored at +`target/perf/native-speed-diagnostics-20260524T090412Z-speed-misses/summary.md` +reproduced stable misses for `1`, `2.1`, `3`, `4`, `10`, and `13`. Cases `2`, +`3.1`, and `5` did not reproduce above tolerance in that isolated per-case run +and should be rechecked only if they recur in the next complete matrix. +Compared with SQLite, native direct wins the total speed suite in this run +(`0.689x`) but still has much higher open p90 (`493.034x`) and RSS (`3.343x`). + +Prepared-update p90 rows now include sequential and pipelined direct, broker, +server, and native PostgreSQL controls. Direct sequential prepared p90 is +`0.775 s` numeric and `0.768 s` text versus native PostgreSQL tokio at +`0.867 s` and `0.879 s`. Direct pipelined prepared p90 is `0.337 s` numeric and +`0.34 s` text versus native PostgreSQL tokio at `0.341 s` and `0.359 s`. + +Artifact rows from the same run: + +| Artifact | Size | +| --- | ---: | +| `liboliphaunt.dylib` | 11.14 MB | +| Embedded extension modules | 2.32 MB | +| Native PostgreSQL install | 33.62 MB | +| Native PostgreSQL install tree | 33.42 MB | + +## Snapshot + +Snapshot run: `20260507T113000Z` + +Environment: + +- OS: `macOS 26.4.1 (Darwin 25.4.0 arm64)` +- CPU: `Apple M1 Pro` +- RAM: `16 GB` +- Logical cores: `10` +- Node: `v24.13.0` +- Node packages: `@electric/wasm@0.4.5`, + `@electric/wasm-socket@0.1.5` +- Native Postgres: `18.3 (Homebrew)` +- RTT iterations: `100` +- Speed source: exact upstream SQL from + `target/oliphaunt-sources/checkouts/oliphaunt/packages/benchmark/src` + +Every mode was run serially. + +## Representative Operations + +Lower is better. + +| Operation | native pg + SQLx | oliphaunt-wasix + SQLx | vanilla Oliphaunt + SQLx | +|---|---:|---:|---:| +| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | +| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | + +## Full Operation Table + +| ID | Test | native pg + SQLx | oliphaunt-wasix + SQLx | vanilla Oliphaunt + SQLx | +|---|---|---:|---:|---:| +| 1 | Test 1: 1000 INSERTs | 9.13 ms | 19.76 ms | 15.66 ms | +| 2 | Test 2: 25000 INSERTs in a transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 2.1 | Test 2.1: 25000 INSERTs in single statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 3 | Test 3: 25000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 3.1 | Test 3.1: 25000 INSERTs into an indexed table in single statement | 66.41 ms | 95.12 ms | 93.88 ms | +| 4 | Test 4: 100 SELECTs without an index | 107.63 ms | 162.89 ms | 242.03 ms | +| 5 | Test 5: 100 SELECTs on a string comparison | 305.38 ms | 338.01 ms | 434.63 ms | +| 6 | Test 6: Creating indexes | 9.94 ms | 13.08 ms | 17.12 ms | +| 7 | Test 7: 5000 SELECTs with an index | 81.39 ms | 125.31 ms | 203.05 ms | +| 8 | Test 8: 1000 UPDATEs without an index | 47.91 ms | 74.42 ms | 103.66 ms | +| 9 | Test 9: 25000 UPDATEs with an index | 351.05 ms | 578.96 ms | 720.63 ms | +| 10 | Test 10: 25000 text UPDATEs with an index | 471.74 ms | 712.38 ms | 858.95 ms | +| 11 | Test 11: INSERTs from a SELECT | 65.64 ms | 97.43 ms | 112.87 ms | +| 12 | Test 12: DELETE without an index | 7.54 ms | 9.74 ms | 11.69 ms | +| 13 | Test 13: DELETE with an index | 9.31 ms | 26.58 ms | 27.7 ms | +| 14 | Test 14: A big INSERT after a big DELETE | 53 ms | 71.6 ms | 87.72 ms | +| 15 | Test 15: A big DELETE followed by 12000 small INSERTs | 58.98 ms | 74.49 ms | 112.18 ms | +| 16 | Test 16: DROP TABLE | 3.43 ms | 10.17 ms | 6.74 ms | + +## Reproduce + +Run the native matrix plan locally: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh --plan-only +``` + +Run measured native results when the native runtime artifacts are present: + +```sh +tools/perf/matrix/run_native_oliphaunt_matrix.sh --engines direct,broker,server +``` + +That command covers: + +1. native direct, broker, and server Oliphaunt paths; +2. native PostgreSQL control runs; +3. SQLite embedded control runs for the speed suite; +4. p50/p90/p95 latency, throughput, RSS, CPU, and footprint report generation. + +The WASM product lane has its own perf smoke target: + +```sh +moon run oliphaunt-wasix:bench +``` + +Outputs land under `target/perf/`: + +- `bench-native-postgres-sqlx-.json` +- `bench-oliphaunt-native-direct-.json` +- `bench-oliphaunt-native-broker-.json` +- `bench-oliphaunt-native-server-.json` +- `bench-sqlite-.json` +- `bench-comparison-.md` + +Override the native Postgres binaries when needed: + +```sh +OLIPHAUNT_POSTGRES=/path/to/postgres \ +OLIPHAUNT_INITDB=/path/to/initdb \ +tools/perf/matrix/run_native_oliphaunt_matrix.sh --engines direct,broker,server +``` + +## Reading The Matrix + +- `oliphaunt-wasix + SQLx` is the product-style path for apps that connect through + standard Postgres clients. +- `vanilla Oliphaunt + SQLx` keeps upstream Oliphaunt on NodeFS, but uses the same Rust + SQLx client path as the other wire-protocol rows. +- These are machine-local numbers. Re-run the matrix before quoting them in a + release note or public comparison. diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md new file mode 100644 index 00000000..1e207c18 --- /dev/null +++ b/docs/maintainers/release-setup.md @@ -0,0 +1,431 @@ +# Release setup + +Status: normative external-setup guide. Last verified: 2026-07-20. Owner: repository maintainers. + +This document covers state that cannot live in the repository. The executable +contract is the least-privilege dispatcher in `.github/workflows/release.yml`, +the shared implementation in `.github/workflows/release-execute.yml`, and +`tools/release/check_publish_environment.mjs`; update this guide when any of +them changes. + +## GitHub controls + +Protect `main` before the first public release: + +- require pull requests and the repository's aggregate `Required` check; +- allow squash merges only and require linear history; +- enforce the rules for administrators, and block force-push and deletion; +- require conversations to be resolved and dismiss stale approvals; +- require at least one independent approval in team governance, but set the + approval count to zero when the repository has only one collaborator so a + self-authored generated release PR remains mergeable; +- do not enable `cancel-in-progress` for main qualification or release workflows. + +`Required` is the branch merge gate. After a merge, the non-cancelled CI run on +the resulting `main` commit must also produce the exact-SHA `Qualified` record; +that record is publication evidence, not a pull-request branch-protection check. + +Create these environments: + +| Environment | Purpose | Secrets | Protection | +| --- | --- | --- | --- | +| `release-pr` | Create/update the generated release PR | `RELEASE_PR_TOKEN` | main only | +| `release-dry-run` | Exact-SHA artifact assembly and dry-run | none | main only | +| `release-bootstrap` | Creation of npm/crates identities that do not exist yet | Only the short-lived, registry-scoped `CRATES_IO_BOOTSTRAP_TOKEN` and/or `NPM_BOOTSTRAP_TOKEN` required by the approved lock | main only; independent approval when available | +| `release-publish` | Normal trusted publication | Maven Central credentials and signing key | main only; independent approval when available | + +Use a GitHub App or narrowly scoped bot token for `RELEASE_PR_TOKEN`; PRs created by the default workflow token do not trigger the normal PR workflow. Keep bootstrap tokens out of repository secrets and out of `release-publish`. Delete/revoke them immediately after trusted publishers are configured. + +Every release environment must use a `main`-only deployment branch policy. +Environment approval is optional for dry-run and recommended for the +irreversible bootstrap and publish operations when a second maintainer is +available. In that case, require the independent reviewer and prevent +self-review. A solo-maintained repository must leave self-review prevention +disabled so publication remains possible; manual dispatch plus exact-SHA +qualification, current-main revalidation, and the frozen lock are the viable +solo controls. + +Actions must allow OIDC and artifact attestations. Normal Cargo publication's +in-process broker follows crates.io's documented OIDC exchange and revocation +protocol: it requests audience `crates.io`, exchanges the GitHub JWT for a +30-minute registry token, masks it before use, starts at most 20 carriers and +no work after 20 minutes on that token, and revokes it in `finally`. A fresh +batch receives a fresh token; `id-token: write` alone does not authorize a +registry upload. The frozen Cargo uploader sends the lock-matching `.crate` +through crates.io's Registry Web API instead of asking `cargo publish` to +repackage it. + +Bootstrap's OIDC permission is only for npm provenance on the +token-authenticated first publish; it is not npm or crates.io trusted-publisher +authorization. After package identities and their external publisher settings +exist, normal Cargo/npm publication uses the `release-publish` environment and +short-lived OIDC credentials without bootstrap tokens. + +### Trusted-publisher identity through the reusable workflow + +GitHub's standard OIDC claims describe the top-level caller, while +`job_workflow_ref` describes the called reusable workflow. Consequently, +crates.io and npm must be configured with the dispatcher filename +`release.yml`, **not** `release-execute.yml`. The protected environment belongs +to the called job and is still emitted as the `environment` claim. The workflow +performs a read-only live-token check of all three values before either +mutating operation. + +| Registry | Exact external configuration | Branch binding | +| --- | --- | --- | +| crates.io | owner `f0rr0`, repository `oliphaunt`, workflow filename `release.yml`, environment `release-publish` | crates.io has no branch field; the GitHub `release-publish` environment must allow only `main` | +| npm | owner `f0rr0`, repository `oliphaunt`, workflow filename `release.yml`, environment `release-publish`, allowed action `npm publish` | npm has no branch field; the GitHub `release-publish` environment must allow only `main` | +| JSR | link `@oliphaunt/ts` to GitHub repository `f0rr0/oliphaunt` | JSR has no workflow filename, environment, or branch publisher field; the workflow and GitHub environment enforce `main` | + +This distinction follows GitHub's [OIDC behavior for reusable +workflows](https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-with-reusable-workflows), +the [crates.io trusted-publishing setup](https://crates.io/docs/trusted-publishing), +npm's [trusted-publisher fields and reusable-workflow +behavior](https://docs.npmjs.com/trusted-publishers/), and JSR's +[repository-link publishing model](https://jsr.io/docs/publishing-packages). +Registry settings are external state: the OIDC preflight proves what GitHub +emits, not what a registry operator entered. Audit the table after bootstrap. +The crates.io exchange then proves its matching configuration before normal +registry mutation; npm has no non-publishing trusted-auth probe, so its package +settings must be checked directly. + +Use the exact publication lock to manage that external state; never maintain a +second handwritten package list. This command validates the lock against the +checked-out catalog and prints a plan without credentials, network access, or +mutation: + +```sh +lock=target/release/publication-lock.json +tools/dev/bun.sh tools/release/trusted-publisher-config.mjs --lock "$lock" +``` + +Authenticated `--audit` remains read-only. Mutation exists only behind the +literal `--apply` flag *and* an exact `--confirm-lock-digest`; the tool first +audits the entire selected batch, refuses every mutation if it finds a wrong or +extra configuration, creates only missing configurations, and re-audits after +creation. It never revokes or replaces a registry configuration. A failed or +expired-auth run is resumable by rerunning the same lock and batch: exact +configurations are skipped, while conflicts still fail closed. + +The dispatcher callers intentionally omit both named secrets and +`secrets: inherit`. GitHub resolves only the environment secrets selected by +the called job (`release-pr`, `release-bootstrap`, or `release-publish`) and +automatically provides the scoped `GITHUB_TOKEN`. GitHub validates every job +in a called reusable workflow before evaluating its `if`, so all four caller +jobs must declare the union ceiling needed by all nested jobs. The called jobs' +explicit `permissions` remain the effective grants: dry-run is read-only, +bootstrap adds OIDC for npm provenance, preparation gets release-PR writes, +and normal publish gets only its required write scopes. Dry-run and publish +share one YAML-anchored step list but remain separate permission and +environment boundaries. Policy regressions reject an operation-only caller +ceiling, a write-capable dry-run, or detached step lists because any of those +would respectively fail startup, weaken least privilege, or invite drift. + +Audit the live controls without changing them: + +```sh +tools/dev/bun.sh tools/release/audit-github-release-controls.mjs \ + --governance solo \ + --bootstrap-state idle +``` + +Use `--governance team` only when an independent maintainer is actually +available. Bootstrap state is an explicit credential lifecycle, not an +authorization shortcut: + +- `idle` is the default before first-identity bootstrap, including history + repair, qualification, release-PR preparation, and dry-run; it requires both + bootstrap tokens to be absent; +- `ready` is valid only after every reviewed short-lived Cargo/npm token + required by the approved lock has been installed for an imminent + `publish-bootstrap` dispatch; it accepts either registry token or both, and + requires at least one. The current first release requires both registries; + a later lock that introduces identities in only one registry must not force + provisioning an unrelated credential; and +- `retired` is valid only after bootstrap sealed, trusted publishers were + configured, and both tokens were revoked and removed; it also requires the + token names to be absent. + +The `publish-bootstrap` workflow independently derives the registries required +by the approved lock and rejects each missing credential immediately before +mutation, so an `idle` audit cannot authorize bootstrap publication. The +auditor reads the canonical repository through `gh api`, prints deterministic +`PASS`/`WARN`/`FAIL` findings, and exits nonzero only for hard release-safety +findings (usage, authentication, or API errors exit `2`). It reads environment +secret names, never values; the authenticated account therefore needs +permission to inspect repository and environment settings. Warnings cover +optional team governance and repository hygiene and do not block a solo +release. + +## Registry ownership + +The publication catalog is the identity inventory. Generate/query it rather than maintaining a package list in this document. Bootstrap accepts exactly two registry states: the locked first version is already public with lock-matching bytes (a recovery skip), or the package name is wholly absent (a first-version mutation). An existing name without the locked exact version is a hard blocker and must use a later normal release; it is never treated as bootstrap work. A resumed run reconciles and checkpoints every matching public version, then invokes publishers only for names that remain absent. A conflicting public identity is a blocker, not a reason to rename an artifact silently. + +### crates.io + +1. Create the maintainer account/team. +2. Inventory the exact first-release lock. Crates.io's documented per-user new-name limit is a burst of 5 followed by one new crate every 10 minutes. Do not copy a carrier count from this document: the publication catalog is the stable identity model, while oversized payloads add generated `*-part-NNN` carriers only when the candidate artifacts and publication lock are assembled. For `C` missing Cargo names, the untouched-default rate-limit floor is `max(0, C - 5) * 10 minutes`. Crates.io support may grant exceptional capacity, but no API exposes that account state, so the workflow never treats an operator-entered number as proof. A valid `429 Retry-After` response and the next read-only registry inventory are authoritative. +3. Use the protected `publish-bootstrap` operation for only the missing first versions. The prior dry-run must have emitted one approved lock and its manifest-bound Cargo/npm capsule in the same workflow run. The slim bootstrap job verifies and atomically installs those exact bytes; it does not rebuild them. Before initializing its ledger or sending any npm/Cargo mutation, the workflow queries crates.io read-only and reports exact selected/existing/missing counts and the official-default duration floor. It admits only a dependency-closed batch that fits the bounded job window. Independent Cargo and npm mutations overlap; each registry remains strictly sequential, and a cross-registry dependency remains a barrier. + + When the exact lock cannot finish in one six-hour hosted job, successful + partial progress is not a failure and does not require a maintainer rerun. + The job flushes its content-addressed checkpoint, uploads it, and a separate + credential-free job dispatches the next `release.yml` run for the same + release commit. The continuation carries the exact parent artifact ID, + digest, size, checkpoint identity, and bounded generation. It must prove at + least one new receipt before another continuation is allowed. A timeout, + ambiguous upload, integrity conflict, malformed retry response, or + checkpoint failure remains a hard failure. + + Bootstrap's protected `release-bootstrap` environment may increase, but + never decrease, four calibrated admission variables: 30 seconds per missing + Cargo name (`REGISTRY_BOOTSTRAP_CARGO_SECONDS_PER_CARRIER`), 30 seconds per + missing npm name (`REGISTRY_BOOTSTRAP_NPM_SECONDS_PER_CARRIER`), 6 seconds + per already-public version reconciled at fixed concurrency eight + (`REGISTRY_BOOTSTRAP_RECONCILIATION_SECONDS_PER_CARRIER`), and a 600-second + non-publication reserve (`REGISTRY_BOOTSTRAP_RESERVE_SECONDS`). Leave them + unset to use these defaults. Increase a value only from measured registry or + runner evidence; the parser rejects smaller values so an operator cannot + make a workload appear to fit by weakening the admission model. For `C` + missing Cargo names and `N` missing npm names, the basic aggregate + estimate is `30*C + 30*N` seconds and the independent-lane lower bound is + `max(30*C, 30*N)` seconds before reserve and dependency barriers. These + formulas are explanatory, not admission evidence: the gate recomputes the + exact dependency-DAG critical path from the frozen lock, including every + generated Cargo part carrier. + + These values are not upper bounds on third-party latency. A Cargo carrier + can use up to twelve visibility probes separated by ten-second waits, and an + npm carrier has its own bounded publish and visibility loops; either can + exceed the 30-second estimate. The absolute registry/job deadline is the + hard bound. Completed carrier IDs are reconciled and appended through one + serialized checkpoint writer in canonical order. If either lane fails, no + new mutation starts after the shared abort, the peer's one in-flight + immutable operation drains, and a final checkpoint attempt covers every + successful operation. An exact-lock rerun inventories and byte-proves public + partial mutations, including an ambiguous upload accepted before failure, + without blindly repeating that immutable upload. +4. Give the revocable scoped API token the `publish-new` and `trusted-publishing` endpoint scopes and only the `oliphaunt*` and `liboliphaunt*` crate scopes. After bootstrap seals, expose that same bootstrap token to the local process as `CRATES_IO_TRUST_CONFIG_TOKEN`, then run the lock-derived read-only audit and explicit apply below. The crates.io API is queried per exact crate; any wrong or additional configuration blocks the whole apply before it creates another one. + + ```sh + lock=target/release/publication-lock.json + digest="$(jq -er .lockDigest "$lock")" + tools/dev/bun.sh tools/release/trusted-publisher-config.mjs \ + --audit --ecosystem cargo --lock "$lock" + tools/dev/bun.sh tools/release/trusted-publisher-config.mjs \ + --apply --confirm-lock-digest "$digest" --ecosystem cargo --lock "$lock" + ``` + + The first audit exits `1` while configurations are missing; that is an + expected read-only finding, not permission to weaken the apply guard. Run it + once more after apply and retain its zero-missing, zero-conflict JSON report + with the release evidence. `CRATES_IO_TRUST_CONFIG_TOKEN` is only the local + process alias for the already-protected bootstrap token: do not duplicate it + as another GitHub secret or put its value in a file, command argument, log, + or shell history. +5. Revoke `CRATES_IO_BOOTSTRAP_TOKEN` and remove it from `release-bootstrap`. +6. Run normal `publish`; its bounded in-process broker must acquire, mask, use, + and revoke fresh OIDC-exchanged tokens for at most 20 Cargo carriers and 20 + minutes per batch. + +Crates.io returns `429` with an HTTP-date `Retry-After` when a publish bucket is +empty. The frozen publisher retries only that explicit non-mutating rejection, +using the exact same locked bytes and only while the bounded mutation deadline +can accommodate the server delay. Ambiguous transport and other server errors +are never blindly replayed: registry state is checked, matching bytes resume, +and otherwise the immutable checkpoint chain is used by the next run. + +Normal publication has a separate per-user version-update bucket: 30 versions +immediately, then one version per minute. For `C` pending Cargo versions, the +untouched-default rate-limit floor after the initial burst is +`max(0, C - 30) * 1 minute`; generated part carriers increase `C`. Publication +work overlaps bucket refill, so admission models each Cargo operation as +`start = max(previous_finish, (index - burst) * refill)` rather than adding all +wait and upload durations. No capacity secret is accepted: a support-side +exception naturally permits earlier uploads, while `429 Retry-After` remains +authoritative. The normal-publish gate +inventories every exact `name@version` before tags or packages are mutated and +rejects missing names that should have gone through bootstrap. +It uses 30 seconds per pending Cargo carrier as a calibrated admission estimate +inside the Cargo lane, models independent npm/Maven/JSR lanes and explicit +cross-registry dependencies, and adds a separate executor reserve. A late run +fails before mutation instead of assuming that a support exception also makes +uploads instantaneous. This estimate is not a worst-case visibility bound; +the absolute deadline and exact-lock recovery contract remain authoritative. +Trusted-publishing credentials are also bounded: crates.io issues each token +for 30 minutes, so the Cargo executor refreshes and revokes tokens in batches +well inside that lifetime rather than retaining the workflow's first token. +Every OIDC, token-exchange, and revoke request is clamped to the shared registry +deadline. A batch cannot acquire a token unless two bounded exchanges plus the +mandatory revoke budget remain, and publication receives a deadline that +excludes that revoke budget even when a carrier fails. + +Generated Cargo `*-part-NNN` crates are allowed only when a `.crate` would exceed crates.io's package-size limit. They are carriers in the frozen lock, publish before their aggregator, and are not independent release products. Capacity and duration decisions must use the frozen-lock count, never the catalog count alone. + +### npm + +1. Create/claim the `@oliphaunt` scope and require public access/provenance in package metadata. +2. Bootstrap only identities whose settings page cannot exist before a first + publish. Immediately before that run, create `NPM_BOOTSTRAP_TOKEN` as a + short-lived **granular access token** owned by an actor whose npm account has + 2FA enabled and write access to `@oliphaunt`. Its package/scopes permission + must be **Read and write**, its selected scope must explicitly include + `@oliphaunt`, and **Bypass two-factor authentication** must be enabled. + Limit its lifetime to the bootstrap window, store it only in the + `release-bootstrap` environment, and revoke it as soon as the identity chain + seals. An ordinary token, a read-only token, an organization-only grant, or + a granular token without 2FA bypass may authenticate successfully but will + fail the noninteractive first publish with `EOTP`; do not discover that + distinction through a sacrificial package mutation. +3. For this operator-side configuration step, use npm CLI 11.15.0 or newer and + an npm authentication method supported by `npm trust`; the account must have + 2FA and every package must already exist. The release workflow separately + installs and verifies its exact `NPM_VERSION` through + `setup-npm-publisher`; do not substitute the operator's ambient CLI for that + workflow pin. + The setup helper retains npm's documented two-second spacing and divides the + exact lock into deterministic batches of 25, leaving room inside each + five-minute 2FA skip window for pre-audit, creation, and post-audit. For each + `npmBatches[].number` printed by the no-network plan, run the read-only audit + and then the explicit apply: + + ```sh + lock=target/release/publication-lock.json + digest="$(jq -er .lockDigest "$lock")" + tools/dev/bun.sh tools/release/trusted-publisher-config.mjs \ + --audit --ecosystem npm --batch 1 --lock "$lock" + tools/dev/bun.sh tools/release/trusted-publisher-config.mjs \ + --apply --confirm-lock-digest "$digest" \ + --ecosystem npm --batch 1 --lock "$lock" + ``` + + Repeat with the next batch number, completing npm's 2FA prompt when a new + window is needed. The first audit exits `1` for missing configurations; its + JSON is still the required pre-mutation inventory. Every exact package receives repository + `f0rr0/oliphaunt`, caller workflow `release.yml`, environment + `release-publish`, and only `npm publish`; staged publishing is never + authorized. After all batches, rerun `--audit` for every batch and retain + the zero-missing, zero-conflict reports. + + Run each npm `--apply` command directly in an interactive terminal. Read and + audit commands remain captured and bounded, while only `npm trust github` + inherits the terminal so npm can complete web or classic OTP authentication; + each such interactive mutation has a five-minute process deadline. Do not + pipe or redirect an apply command that may need a 2FA prompt. +4. Revoke `NPM_BOOTSTRAP_TOKEN`; normal publishing uses npm trusted publishing and no `NODE_AUTH_TOKEN`. + +Target-specific npm packages are intentional carriers. Routine payload-splitting packages are forbidden; an npm tarball contains the target payload directly and stays within the registry's documented limits. + +### JSR + +Create the `@oliphaunt` scope and `@oliphaunt/ts` package, then link it to `f0rr0/oliphaunt`. The normal workflow uses JSR's GitHub Actions OIDC path. Its read-only readiness gate queries JSR's management API and fails before qualification downloads or release mutations unless the package exists and exposes that exact repository link. Do not configure `JSR_TOKEN` unless JSR's documented bootstrap process explicitly requires one, and never retain it for normal releases. + +JSR requires the actor who dispatches the GitHub workflow to be a member of the +JSR scope by default. Keep that safer default and make every release operator a +scope member; if the scope setting is deliberately relaxed, record that +decision in the external controls audit. This actor-membership policy is not +visible through the repository's unauthenticated readiness query. + +### Maven Central + +Verify in Central Portal that `dev.oliphaunt` is visibly **Verified** for the +account represented by the portal token. This is a one-time external control: +the documented Publisher API has no read-only namespace-status endpoint, so an +empty deployment listing must not be represented as proof of namespace +verification. Store the portal username/password and in-memory GPG +key/id/passphrase only in `release-publish`. Before any mutation, the normal +workflow authenticates against the read-only deployment endpoint, binds its +query to `dev.oliphaunt`, and rejects catalog groups outside that namespace. It +also imports the key into an isolated temporary GPG home, signs fixed preflight +bytes with the configured key and passphrase, requires the signature to use the +primary key rather than an incompatible signing subkey, verifies the detached +signature and fingerprint, confirms that exact primary fingerprint is +retrievable from at least one Central-supported keyserver, deletes the +temporary keyring, and repeats that proof in the registry job immediately +before mutation. The earlier staging job additionally constructs the complete +lock-selected Central bundle without upload and requires Central metadata, +exact primary/sources/javadoc files, nonempty payloads, signatures, checksums, +safe paths, and a total archive size strictly below 1 GB. Placeholder +sources/javadoc JARs are deterministic and intentional for binary carrier +coordinates. `MAVEN_GPG_KEY_ID` must be an 8-64 character hexadecimal +primary-key ID or fingerprint, with an optional `0x` prefix. Publish the public +primary key to `keyserver.ubuntu.com`, `keys.openpgp.org`, or `pgp.mit.edu` +before release. Publish the identities declared by the catalog: the Android +AAR, Gradle plugin and marker, runtime/extension ABI carriers. Do not publish an +undeclared Kotlin Multiplatform/JVM root module. + +### SwiftPM and GitHub Releases + +Product tags use `-v`. SwiftPM additionally consumes an unscoped semantic tag; because legacy unscoped tags occupy versions through `0.5.1`, the first Oliphaunt Swift version is `0.6.0`. + +Release Please owns product versions, changelogs, and the generated release PR. +After final current-main validation, the protected publish workflow stages each +selected product tag and draft GitHub release directly at the qualified SHA. +Before staging, the lock-derived SwiftPM preflight creates no tag and performs +no push: it accepts only an absent semantic tag or an existing tag that resolves +to the exact deterministic manifest commit for this release. Any conflicting +or ambiguous remote tag fails before the first mutation. +The workflow then uploads checksum-covered assets, completes registries, runs +the receipt-bound anonymous public-consumer probes, preserves their immutable +evidence, and promotes drafts. A failed publish must leave drafts unpromoted. +The Swift probe can resolve the public source tag and evaluate its manifest at +this point; draft binary-target assets are not anonymously public until +promotion and remain covered by the exact GitHub asset/attestation receipt. + +## First release sequence + +1. Confirm selected registry identities and product tags do not conflict. +2. Merge the introduction tree through the qualified path. Its parent must be + the full `bootstrap-sha` boundary recorded in `release-please-config.json`, + so legacy release commits are excluded from the first product releases. Do + not rewrite history after any affected identity is public. +3. Run `prepare-release-pr` from current `main`; review the single generated release-bump commit. + A large Release Please update may arrive internally as multiple transport + chunks; the prepare job validates and collapses the exact canonical PR tree + to this single commit before it is reviewable or mergeable. +4. Merge it and wait for that exact commit's non-cancelled `Qualified` CI run. +5. Run `publish-dry-run`. It must download that run's artifacts, create/freeze the publication lock, create the deterministic lock-bound Cargo/npm bootstrap capsule, and perform clean package/install checks without credentials. Preserve the successful run ID that contains both approval artifacts. +6. If npm/crates first identities are missing, run `publish-bootstrap`. It selects one same-SHA dry-run containing both the lock and capsule, verifies their byte binding, and installs the capsule without rebuilding. It writes a genesis checkpoint before the first registry mutation and appends immutable byte receipts throughout the run. An incomplete but progressing job uploads the chain and self-dispatches the next bounded generation, which restores only the exact parent artifact ID/digest/size and validates the continuation contract before resuming. Manual recovery can inventory compatible same-SHA evidence, but an automatic child never selects a mutable “latest” artifact. After the chain seals, use the exact lock's `trusted-publisher-config.mjs` plan, audit, and explicit apply flow above; retain the final reports and revoke the bootstrap tokens. Bootstrap does not promote GitHub releases or publish unrelated registries. +7. Run normal `publish` on the same current `main` SHA. One dispatch executes three sequential jobs with fresh deadlines: GitHub staging, exact registry publication, and consumer verification/final promotion. The jobs exchange only manifest-validated immutable Actions artifacts: the approved dry-run capsule carries Cargo/npm bytes once, the stage handoff carries only required non-capsule registry inputs and evidence, and the registry handoff carries receipts rather than package payloads. Each downstream job revalidates current main, source/tree, products, lock, approved artifact IDs/digests/sizes, and remote staged state before its mutation. Finalization runs bounded parallel public Cargo/npm/Maven/JSR and Git/Swift probes from fresh anonymous caches, uploads deterministic receipt-bound evidence, and promotes drafts as its literal last step. The macOS host probe complements, but does not replace, same-SHA platform CI and the exhaustive per-carrier receipts. +8. Preserve the publication lock, ledger, provenance, and workflow URL with the release. + +The first generated release PR consumes the one-time `bootstrap-sha` boundary. +`sync-release-pr.mjs` removes it on that PR once any manifest entry advances +from `0.0.0`; the release-bump commit must contain that removal. Never delete +the boundary on the unreleased introduction tree, and never restore it after +the first release bump. + +`release_commit` is only an equality assertion for the workflow commit; it cannot select an older commit. Release tooling fixes create a new candidate SHA and require new qualification. There is no temporary Release Please target branch. + +## Recovery + +Publishing is resumable but not cross-registry atomic. On failure, preserve and validate the complete content-addressed checkpoint chain, then inventory all selected identities against the frozen lock. Matching immutable versions may be skipped only after registry bytes are proved; a mismatched version/tag/asset or ledger checkpoint stops the release. Repository changes require a new version and new exact-SHA qualification. Use `.codex/skills/release-oliphaunt/references/recovery.md` for the recovery and pre-publication history-repair procedure. + +## External readiness checklist + +- the read-only GitHub controls audit has no `FAIL` findings for the applicable + solo/team governance and `idle`, `ready`, or `retired` bootstrap lifecycle; +- main requires `Required`, squash-only merges, linear history, resolved + conversations, stale-approval dismissal, administrator enforcement, and no + force-push or deletion; +- every release environment is restricted to `main`; bootstrap and publish use + independent approval and prevent-self-review when a second maintainer exists, + while solo operation keeps self-review prevention disabled; +- `release-pr` can create a PR that triggers normal CI; +- dry-run has no write credentials; +- bootstrap tokens are absent unless a reviewed first-identity run is imminent; +- every Cargo and npm identity uses dispatcher `release.yml` and environment + `release-publish` (never reusable implementation `release-execute.yml`), npm + allows `npm publish`, and neither registry is expected to bind a branch; +- the exact-lock trusted-publisher audit reports every selected Cargo/npm + identity exact, with zero missing and zero conflicting/extra configurations; +- JSR `@oliphaunt/ts` links to `f0rr0/oliphaunt`, and each workflow dispatcher + is a JSR scope member while the default actor restriction is enabled; +- Central Portal visibly marks `dev.oliphaunt` Verified, the deployment API + credentials authenticate, and the primary signing key preflight validates; +- registry owners and GitHub maintainers can recover/revoke credentials; +- staged/local clean consumers pass before publication, and the normal publish + can resolve/install every applicable exact-lock public ecosystem entry from + anonymous endpoints before draft promotion; Swift's pre-promotion proof is + intentionally source-tag/manifest-only because draft binary assets are not + public yet. diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md new file mode 100644 index 00000000..974908c7 --- /dev/null +++ b/docs/maintainers/release.md @@ -0,0 +1,600 @@ +# Release process + +Windows publishers must also follow the [Visual C++ runtime release +contract](./windows-vc-runtime.md); it defines redistributable provenance, +extension-provider ownership, app-local placement, and receipt evidence. + +Status: normative operation guide. Last verified: 2026-07-16. Owner: repository maintainers. + +Oliphaunt releases independent products from one monorepo. There is no repository-wide product version. + +## Model + +A product owns its SemVer, changelog, source identity, Release Please component, +product tag, and GitHub release. A carrier is one published representation of +that product for an ecosystem, OS/ABI, or payload limit. All carriers for a +release use their product's version. + +The canonical model is composed from: + +- Moon project/release metadata for ownership and dependency impact; +- `release-please-config.json` and `.release-please-manifest.json` for product + versions, changelogs, components, and tag naming; +- the protected release workflow for exact-SHA tag and draft-release creation; +- product `release.toml` and explicit target manifests for publish surfaces; +- `tools/release/release-semantic-inputs.toml` for shared byte producers and + public target contracts that do not live under one product root; +- `tools/release/publication-catalog.mjs` for the normalized Product → Carrier inventory; +- the frozen publication lock for the actual files produced by one candidate. + +Do not maintain a second hand-written package matrix. Query the catalog and inspect the lock. Dynamic package identities are forbidden except crates.io payload `part-N` carriers whose parent is declared and whose size requires splitting. + +## Version rules + +- New products remain `0.0.0` in source until their first generated release PR. The global first version is `0.1.0`. +- Swift also remains `0.0.0`, but its per-product initial version is `0.6.0` because legacy unscoped SwiftPM tags occupy `0.1.0` through `0.5.1`. +- Release Please creates changelog headings. A brand-new tracked `CHANGELOG.md` is empty; pre-seeding `# Changelog` creates a duplicate heading. +- PostgreSQL 18 contrib members are owned by the single + `oliphaunt-extension-contrib-pg18` product. That product is `runtime-bound` + and shares the runtime linked-version group; its 32 SQL members are not 32 + separately versioned release products. +- external extensions are `upstream-bound` and own independent packaging SemVer. Their upstream version/commit and compatible runtime versions are separate metadata. +- `feat`, `fix`, `perf`, `refactor`, and `revert` are release-impacting types because the Release Please `changelog-sections` catalog says so. A Conventional Commit `!` is breaking. Release-intent checks derive this set from config. +- Product source PRs never edit versions. The generated release PR owns all version, compatibility, lockfile, and changelog changes. +- While every product is still `0.0.0`, top-level `bootstrap-sha` is the full + legacy-history boundary `07a9054faa03d5737dc0193f7a77ed4a71920c05`. + Release Please considers only commits after that exclusive boundary. Derived + release-PR sync removes the key in the first generated release-bump change; + do not remove it before Release Please has consumed it or retain it after the + first bump. + +`tools/dev/bun.sh tools/release/sync-release-pr.mjs --check` verifies derived release files. Pure version/changelog changes alter package envelopes but do not alter the committed WASIX binary-semantic fingerprint. + +Shared code that can change published bytes or a declared public target must +have one exact ownership rule in `release-semantic-inputs.toml`. The generated +`.release-semantic-inputs.json` file under each affected product root gives +Release Please a content-addressed trigger without treating workflow, +validator, registry-transport, test, or documentation edits as product +releases. After changing an owned shared input or its ownership, run +`tools/dev/bun.sh tools/release/sync-release-semantic-inputs.mjs --write` and +then `--check`; do not hand-edit the fingerprints. + +### Generated dependent candidates + +Release Please remains the sole authority for direct candidates and their +version policy. After the canonical generated PR is normalized, derived sync +computes the deterministic downstream fixed point from three graph-owned +relationships: + +- Moon `production` and `peer` edges, traversed from dependency to consumer; +- compatibility metadata, traversed only from `source_product` to the product + that owns the compatibility field; and +- the runtime/contrib-bundle linked-version group. + +Moon `build`, development, and test edges never expand a release. Dependency +edges are never traversed backwards. Consequently a directly changed external +extension remains an independent one-product release unless another real +consumer depends on it; a runtime change selects external extension products +whose own compatibility fields must advance. + +The Release Please linked-versions plugin must create a complete, same-version +runtime group. Sync rejects incomplete output rather than reproducing that +plugin. For another missing dependent that has already shipped, sync creates a +patch candidate, updates only the canonical and extra version files already +declared in `release-please-config.json`, and inserts a deterministic +`Dependencies` changelog entry naming the exact graph reason. A missing +dependent still at `0.0.0` is a hard failure: Release Please must choose its +first version. The sync command reloads the expanded manifest before updating +compatibility fields and package/lock dependencies, then `--check`, structured +release-commit verification, and release-PR coverage independently require the +fixed point to be closed. Do not hand-edit a missing candidate into a source PR +or weaken dependency scopes to make this gate pass. + +PR CI recognizes generated `chore(release):` changes only on the generated +Release Please branch. Before merge it requires the release commit's parent to +equal the exact base SHA, derives the product set from the manifest diff, and +runs the same structured release-commit verifier used by publish. A multi-commit +or source-bearing release PR therefore fails before it can reach `main`. +Release Please may transport a large generated change as multiple file-chunk +commits. The protected prepare job accepts those chunks only from the exact +open canonical Release Please PR, requires a linear exact-main descendant with +the configured release title on every chunk, collapses their tree to one commit, +reapplies derived release synchronization, runs the structured verifier, and +pushes with a lease against the inspected PR head. The normalized tree must be +byte-identical before derived synchronization; a moved PR head is never replaced. + +## Qualification contract + +Publication accepts only a current-main candidate with one non-cancelled CI run whose `head_sha` is exact and whose `Qualified` gate succeeded. That record covers required checks, tests, builds, policy, selected E2E, and named build artifacts. A successful `Builds` job alone is insufficient. + +The `macos-26` publication runner is ARM64, but its current runner-image +contract exposes the installed Java 17 path as `JAVA_HOME_17_arm64` (including +that lowercase suffix). Release setup uses that exact variable first, permits +`JAVA_HOME_17_X64` only as an Intel-image fallback, and fails before release +work unless the selected path contains an executable `bin/java`. Do not invent +the variable name from the architecture or rely on the image's moving default +Java version for Maven or Gradle publication. + +The publish workflow downloads artifacts by that run id and SHA, verifies their attestations/qualification record, assembles the selected product carriers, then freezes a publication lock containing: + +- source commit/tree and catalog digest; +- product/version and every actual registry identity; +- ecosystem, target, role, dependency/order, file hash, and size; +- corresponding GitHub release assets and bootstrap/trust state. + +Missing and extra identities both fail. Publish commands reverify the lock immediately before writes. + +## Operations + +Run local metadata gates before dispatching: + +```sh +tools/dev/bun.sh tools/release/release-check.mjs +cargo run -p xtask -- assets verify-committed +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check +``` + +The default `release-check.mjs` invocation includes the release mutation unit +suite. `release-metadata-check.mjs` is a distinct internal replay surface that +still executes every live release-policy, Release Please, target, version, +changelog, synchronization, and consumer-shape check. The workflow may call it +only after the structured generated-release commit verifier succeeds, or after +the exact hosted qualification record is reverified against a clean checkout +at `RELEASE_HEAD_SHA`. Do not substitute it for the full local pre-dispatch +gate. + +The `Release` workflow has four operations: + +1. `prepare-release-pr` — run from current `main`; creates/updates the single generated release PR and syncs derived files. +2. `publish-dry-run` — downloads exact-SHA CI artifacts, performs package/registry preflight and clean-consumer checks, freezes/verifies the lock, and emits the lock-bound Cargo/npm bootstrap capsule without write credentials. +3. `publish-bootstrap` — creation of missing npm/crates identities only, from the already-approved capsule in bounded resumable Linux jobs. npm requires a short-lived granular `@oliphaunt` read/write token with 2FA bypass for this noninteractive first publication; the exact operator checklist is in `release-setup.md`. Configure trusted publishers and revoke every provisioned bootstrap token immediately after the chain seals. The current first release needs both Cargo and npm credentials, while a future single-registry identity addition must provision only that registry's token. +4. `publish` — normal trusted release. It uses short-lived Cargo/npm/JSR credentials, Maven protected secrets, the frozen lock, and idempotent publication checks. + +Only a successful `publish-dry-run` uploads the canonical +`oliphaunt-publication-lock` and `oliphaunt-bootstrap-capsule` approval +artifacts. Bootstrap selects one successful same-SHA dry-run that contains both +artifacts, downloads both by that one run ID, verifies the embedded lock is +byte-identical to the separately downloaded lock, and atomically installs only +the exact locked Cargo/npm bytes. It does not rebuild packages. Normal publish +independently reassembles the complete candidate from the same exact-SHA CI +artifacts, downloads the approved lock, and byte-compares the two locks before +publication. A mutating run therefore cannot +approve itself or silently combine artifacts from different dry-runs. + +The manual workflow is a small dispatcher. Each operation calls one shared +execution workflow. GitHub validates every nested job's permission request +before it evaluates the nested `if` conditions, so every caller declares the +same union permission ceiling required to compile the reusable workflow. This +does not give that union to the running operation: explicit permissions on the +called jobs reduce the actual token to repository-read-only for dry-run, OIDC +plus repository read for bootstrap, release-PR writes for preparation, or the +normal publish scopes. Dry-run and normal staging are separate jobs over one +YAML-anchored step list, so this separation does not create two release +implementations that can drift. Secrets come only from the operation's protected +environment; callers do not inherit repository or organization secrets. +Trusted publishers match the top-level caller filename `release.yml` because +GitHub exposes that file through `workflow_ref`; the shared +`release-execute.yml` implementation appears separately through +`job_workflow_ref` and must not be entered as the npm or crates.io publisher. +Before any reusable caller is compiled, an unconditional two-minute, +repository-read-only job validates the operation, optional exact commit, and +continuation pointer together. Malformed or contradictory manual inputs +therefore fail as input errors instead of opaque reusable-workflow startup +failures. +After bootstrap, derive the complete configuration inventory from the exact +publication lock with `tools/release/trusted-publisher-config.mjs`. Its default +plan is offline/read-only; authenticated inspection requires `--audit`, and +creation requires both `--apply` and confirmation of the exact lock digest. +Wrong or extra configurations are blockers and are never automatically +replaced. + +Normal release execution reuses the verified Node and pnpm payloads installed +by `setup-moon`; the slim bootstrap job uses the same digest-pinned Node runtime +through `setup-node-runtime`. Neither path relies on Corepack. When the selected +carrier set includes npm, both normal and bootstrap jobs use +`setup-npm-publisher` to install the workflow-pinned npm archive. That action +verifies the canonical URL, compressed digests and byte count, safe archive +shape, executable modes, and complete extracted-tree digest before exporting +the CLI. It then compares the observed npm version exactly and validates the +observed Node/npm trusted-publishing runtime. This local tool setup neither +requests an OIDC token nor changes the frozen registry mutation logic. + +Installer fault-injection suites are owned by the exact-SHA +`ci-workflows:check` gate. Publication does not execute those download, cache, +and rollback suites again. Release-PR preparation runs one full release check +before generation and the live metadata checker only after structured commit +verification. The protected GitHub-staging job runs one full release check before +qualification, then uses the live metadata checker only after the same-SHA +`Qualified` record is verified. +`--qualified-ci` is not a trusted Boolean bypass: the publisher rejects dirty +or non-hosted use, binds HEAD to `RELEASE_HEAD_SHA`, and reruns the fixed +candidate/plan/WASIX-evidence verifier before omitting mutation tests. Workflow +policy rejects extra full invocations or replay before candidate verification. + +`release_commit`, when supplied, is an assertion that must equal the workflow commit. It cannot select historical code. A tooling fix is a new candidate and must pass new qualification. + +## Publish order + +Cross-registry publication cannot be atomic, so the workflow is resumable and state-driven: + +1. validate names, ownership, versions, auth mode, exact SHA, and registry collisions; +2. create exact product tags and draft GitHub releases; +3. upload and attest selected GitHub assets and create the frozen Swift source tag outside the registry executor; +4. execute the publication lock's dependency topology: payload parts/leaves before aggregators/façades and runtime carriers before dependent SDK carriers; +5. prove public registry bytes, then run the exact public-consumer gate once and preserve its lock-bound evidence; +6. promote verified GitHub release drafts last. + +Normal publish realizes that order as three successful jobs, never as one +runner carrying mutable filesystem state: `publish` leaves only tags, drafts, +assets, and attestations staged; `publish-registry` installs an immutable +handoff and publishes the exact registry topology; `publish-finalize` installs +the receipt-only handoff, probes public consumers, and promotes drafts. Each +handoff binds the source commit/tree, selected products, +lock/catalog/package-envelope digests, approved dry-run artifact +IDs/digests/sizes, and every transported file digest/size. The downstream job +downloads a current-run handoff by immutable artifact ID and rejects missing, +extra, changed, executable, or path-unsafe payloads. + +The workflow does not encode a second product/ecosystem publish order. Before +the first mutation it writes `normal-publication-plan.json` directly from the +approved lock. The normal registry executor consumes that same plan and rejects +an omitted selected dependency, unknown carrier, cycle, or non-contiguous +operation order. It runs one sequential Cargo, npm, Maven, and JSR lane, +overlaps independent lanes, and awaits every explicit cross-registry dependency +barrier. Cargo (including dynamic payload parts), npm, and JSR consume their +exact frozen carrier bytes. All selected Maven coordinates form one +signed, atomic Central deployment because Maven Central validates and publishes +that bundle as a unit. Before the first GitHub write, the workflow constructs +the complete selected bundle without upload and verifies every coordinate, +POM, primary artifact, sources JAR, javadoc JAR, signature, checksum, nonempty +file, and the strict sub-1-GB archive ceiling. A rerun skips an immutable carrier only after proving its +public bytes match the lock; a partially published Maven product fails closed. + +JSR publication resolves the exact lock-installed CLI owned by +`src/sdks/js`, validates its package identity, lock integrity, and executable, +and invokes that absolute executable while retaining the frozen source as the +working directory. POSIX runs the package executable directly; Windows invokes +that same file through the absolute Node executable already verified by release +setup, because Windows does not execute JavaScript shebangs. Never replace this +with an ambient `jsr` or a `pnpm exec` lookup from `target/`: the frozen source +intentionally has no workspace `node_modules` tree. +The registry runner performs a frozen, script-disabled install filtered to +`@oliphaunt/ts` before the mutation gate, using the digest-pinned Node and pnpm +toolchains. This installs the one lock-owned JSR publisher without relying on +global state or spending the registry deadline on unrelated workspaces. + +The dry-run assembles the exact candidate Cargo registry before any public +write. It copies each registry-neutral Cargo example into scratch space, adds +exact `[patch.crates-io]` entries only for candidate packages, and generates a +fresh lock there. The validator compares every candidate lock checksum with the +registry index and actual `.crate` SHA-256, enforces dependency policy, and +performs a full `cargo fetch --locked`. Source examples never commit candidate +patches or nested locks, so exact-SHA qualification does not create a +build-commit-build cycle. In later partial releases, unchanged packages resolve +normally from crates.io. + +npm is the deliberate exception to a separate moving-tag promotion phase. +Trusted-publishing OIDC authenticates `npm publish`, but npm does not authorize +`npm dist-tag` with that credential. Each frozen npm version is therefore +published with its normal tag only after every non-mutating preflight has +passed; target leaves precede the user-facing façade. The workflow does not +reintroduce a long-lived token merely to move `latest`. This registry constraint +is documented by npm's [trusted-publishing limitations](https://docs.npmjs.com/trusted-publishers/). + +An existing immutable identity is skipped only when its version/integrity matches the lock. A conflict stops publication. Never replace a public artifact or reuse a version. + +Identity bootstrap is checkpointed before and during publication. The genesis +checkpoint freezes the source SHA/tree, publication-lock and catalog digests, +selected products, and complete expected registry envelope before the first +write. The slim bootstrap job consumes only the canonical dry-run's +manifest-bound Cargo/npm capsule; it neither reconstructs packages nor repeats +the macOS build ceremony. Bootstrap preserves the lock's Cargo/npm dependency +edges and executes one sequential lane per registry, overlapping only +independent operations. Its one serialized checkpoint writer receives newly +completed carrier IDs in canonical lock order and appends content-addressed, +hash-chained checkpoints containing normalized registry byte receipts. A lane +failure stops new starts, lets the peer's one in-flight immutable mutation +drain, and triggers a final reconciliation/checkpoint attempt; publication and +checkpoint failures are reported together rather than masking either. The +workflow uploads the chain even when a later phase fails and restores the newest +compatible chain for the same workflow and exact SHA on a rerun attempt or a +fresh recovery dispatch. Sealing succeeds only after +every expected Cargo and npm identity exists with lock-matching bytes; an +altered, missing, reordered, or conflicting checkpoint fails closed. + +Before that genesis checkpoint, bootstrap performs a read-only crates.io name +inventory from the exact frozen lock. Crates.io's default per-user new-name +bucket (5 immediately, then one every 10 minutes) cannot accommodate the full +first-release catalog in one hosted job. No API exposes support-granted account +capacity, so an operator-entered number is not accepted as a correctness gate. +The planner uses the documented bucket, treats a valid `429 Retry-After` as +authoritative, and admits only a dependency-closed batch that fits the current +window. After progress, it uploads the immutable checkpoint before a separate +credential-free job dispatches an exact-parent continuation. The continuation +is bound to the release SHA/tree, lock and package-envelope digests, products, +root and parent run identities, artifact ID/digest/size, checkpoint identity, +and a bounded generation. Zero-progress recursion is forbidden; ambiguous +uploads and integrity or checkpoint failures remain hard failures. +For normal publication, every continuation artifact also carries the latest +content-write pacer and core-request journal. A child verifies their hashes, +sizes, schemas, counters, and root-run identity, merges any reads it performed +while locating that exact parent artifact, and seals the monotonically extended +state into the next generation. No child may reset rolling limits by falling +back to its own run ID or to the original stage snapshot. + +Every mutating phase records its own hard deadline rather than inheriting time +left on a previous runner. Bootstrap and normal registry publication each have +an independent six-hour hosted-job envelope. The normal GitHub-staging job has +a 350-minute hard window plus ten minutes for cleanup; the registry job also +has a 350-minute hard window plus ten minutes for cleanup; finalization has a +114-minute hard window plus six minutes for cleanup. The executable phase-budget table accounts for setup, exact-ID +transfer, validation, mutation, evidence/handoff, and cleanup and requires a +strictly positive margin in every phase. Bootstrap starts its registry window only after +qualification, capsule/lock verification, checkpoint restoration, and +current-main revalidation, and clamps it to the earlier of 5.5 hours from that +point or the job hard deadline. Normal registry mutation starts only after the +approved capsule and GitHub-stage handoff have been independently verified, +the exact staged releases and recovery checkpoint have been revalidated, and +current `main` still equals the release SHA. It requires its complete +rate-aware mutation allowance plus positive margin before the protected +15-minute receipt/recovery handoff; a shortened residual window cannot admit a +partial planned run unless the executor can close a dependency-safe checkpoint +and issue an exact-parent continuation. Finalization refuses to start unless at least 48 minutes +remain on its fresh deadline. Bootstrap additionally proves that the exact pending Cargo/npm +inventory plus its reserve fits before mutation begins. + +Normal publication applies the same immutable-version rule to every supported +registry: crates.io checks the published checksum, npm checks `dist.integrity`, +Maven Central streams and hashes each frozen payload at its exact coordinate, +and JSR compares the complete published file manifest with the frozen explicit +`publish.include` set. A complete, same-lock bootstrap ledger is verified in +bounded parallel before mutation and its Cargo/npm receipts are reused by the +normal executor; already-proven public identities are not queried serially +again. Each registry publisher returns the exact receipt it proves; the +executor assembles those results in canonical operation order, rejects missing, +extra, duplicate, or replaced receipts, and writes one exhaustive lock-bound +receipt file (including a valid empty receipt set for a source-only release). +It also atomically checkpoints the dependency-closed completed operation set +after every successful registry operation. Mutually exclusive success/failure +evidence steps retain exactly one `normal-publication-recovery-` artifact +per run. On registry success, a separate immutable receipt-only handoff feeds +finalization without retransferring package payloads; on failure, the +dependency-maximal checkpoint remains recoverable without authorizing the next +job. A retry fully +paginates bounded exact-workflow/exact-SHA inventories, selects the unique +dependency-maximal checkpoint, and reuses it only after strict source, lock, +catalog, package-envelope, plan, and receipt validation. +Final verification validates that evidence locally instead of downloading and +reproving every registry package. + +The post-publication consumer gate is an actual anonymous public-endpoint +probe, not the static consumer-shape policy check used during qualification. +It derives its products, ecosystem lanes, dependency roots, full carrier +closure, versions, Maven coordinates, and Git tags from the same frozen lock. +In parallel clean temporary homes/caches it resolves each Cargo consumer root +in an independent scratch manifest without compiling payloads, installs each +npm dependency root in an independent project, resolves each Maven entry in an +isolated Gradle configuration without an Android build, caches each exact JSR +entry import, and anonymously fetches every product tag. Each lane requires +the resolver's platform-independent lock graphs to cover every carrier in the +corresponding frozen dependency closure; a missing carrier cannot be silently +relabelled as receipt-only. It never invents one all-platform consumer graph. +Evidence separately identifies npm carriers not installed on the macOS host +and Cargo payloads intentionally not fetched/compiled; immutable receipts +prove those bytes. When Swift is +selected it also fetches the unscoped source tag, +requires its synthetic commit to have the release SHA as its only parent, and +evaluates that tagged `Package.swift` with `swift package dump-package`. + +Known registry/CDN not-yet-visible and transient network responses are retried +only within one shared deadline. Every retry uses a new workspace and package +cache so a partial npm install or Gradle negative cache cannot authorize a +result. Exact-version, exact-source, closure, tag, and receipt mismatches are +terminal. The gate emits one deterministic immutable evidence file bound to +the registry receipt hash, GitHub receipt digest, lock digest, source SHA/tree, +and selected products; that file is uploaded before draft promotion. The +finalization job runs on macOS, so npm's `installedCarrierIds` proves only the host +subset actually installed there. The exact lock/receipt set and same-SHA CI, +not this host probe, prove the complete supported OS/ABI matrix. + +Draft release assets are intentionally not claimed to be anonymously public. +In particular SwiftPM binary-target downloads cannot be a genuine public +pre-promotion test; the gate proves the public source tag and manifest while +the already-validated GitHub receipt proves every frozen binary-target asset. +Drafts remain unpromoted if either half fails. + +GitHub release assets use the same immutable phase handoff. Each pinned provenance +action emits one signed bundle covering its complete subject set. Before the +registry clock starts, the workflow verifies each nonempty bundle once against +the frozen local lock bytes, checks the exact remote release asset set for +every selected product (including an explicit empty set for source-only +products), and freezes a lock/source/release-ID/digest receipt. Finalization +rechecks only immutable GitHub release IDs plus asset names, sizes, and SHA-256 +metadata against that receipt. It never serially downloads and re-attests the +hundreds of extension assets after registry publication. +The CI extension artifact is intentionally broad, but an extension attestation +is not: its subjects are re-hashed paths derived from only the selected +exact-extension products' frozen GitHub asset and metadata rows. A subset +release therefore cannot attest an unselected exact-extension product merely +because both products were present in the downloaded CI artifact. + +GitHub's primary `GITHUB_TOKEN` allowance and secondary content-creation +allowance are separate release constraints. The GitHub-staging request model is +derived from the frozen publication lock: selected products, exact per-product +asset counts, attestation subjects, qualification transfers, and live release +pagination all feed the admission calculation. The total may intentionally +span more than one rolling hour, so it is not compared directly with the hourly +allowance. The admission gate uses the current rolling request journal, a +361-request maximum paced hour, and a 100-attempt retry reserve, and rejects a +projected rolling count of 900 or more. Release-list pages are derived from the +live count plus every selected draft that may be created, and every product +uses the dedicated paginated asset endpoint; embedded release asset arrays +never authorize publication. +Every REST collection follows validated `Link` relations one page at a time; +opaque `gh api --paginate`/`--slurp` reads are forbidden so each physical page +attempt, including a retry of the same page, receives its own durable core-request +reservation before transport. Exact 100- and 200-row boundaries stop from the +response metadata without issuing an unbudgeted empty trailing request. +Snapshot reads distinguish ordinary authorization failures from GitHub primary +and secondary limits, honor bounded `Retry-After` or `X-RateLimit-Reset` waits, +and otherwise use GitHub's one-minute exponential backoff guidance inside a +five-minute phase deadline. +Completed workflow metadata and artifact +identities are captured once per immutable run and reused; artifact ZIPs are +downloaded only by exact ID and verified against GitHub's size and SHA-256. + +Every product tag, draft, asset upload, SwiftPM source-tag push, attestation +bundle, and promotion shares one 10-second content-write pacer. The exact write +count is lock-derived, with at most 361 in any rolling hour and seven in any +rolling minute. Asset-backed products execute in bounded waves of at most five +uploader processes; products with an exact empty asset set are proven by the +pre-mutation and final receipts without consuming an uploader lane. A new +runner starts a conservative +rolling-hour cooldown at the beginning of the GitHub-staging job; read-only +qualification and artifact preparation overlap that window. The first actual +write still waits for the window to mature, and every reservation is persisted +before its request. Every core REST attempt, including retries and the API call +inside each attestation action, is also reserved in a durable run-identity-bound +journal before transport; attempt 901 inside one rolling hour is refused. +Each sealed handoff carries both journals, and a downstream runner accepts them +only when their repository/root-run/source identity and manifest bytes still +match. A runner that cannot prove continuity fails closed; exact remote-state +reconciliation still prevents immutable mutation replay. Before the first +write, the lock-derived admission gate proves the complete selected-product/ +exact-asset pacing and bounded upload-wave plan plus the immutable stage +handoff fit the stage hard deadline, and simulates a conservative full registry +window for the capacity preflight. The workflow repeats the live +page/clock/rolling-request admission +immediately before the cold-start reservation. A paced mutation receives its +complete configured transport timeout only when that timeout still fits the +absolute operation deadline after pacing and core-request journal admission. +Read transports recompute and clamp their attempt timeout after journal admission, +and never start if the overall read deadline was exhausted while acquiring the +journal lock. The SwiftPM source-tag push is likewise +noninteractive and bounded; success, rejection, disconnect, and timeout are all +resolved by an exact remote tag/SHA read before the result is accepted. Before +any release mutation, a lock-derived SwiftPM preflight constructs the exact +manifest commit without creating a local tag and accepts only an absent remote +semantic tag or one already pointing at that exact commit; a conflicting or +ambiguous tag blocks the release before GitHub drafts are staged. Both +durable journals and the complete asset-wave report cross the validated +receipt handoff before public promotion. Promotion is the literal final step +of `publish-finalize` and the only remaining public +mutation; an interrupted partial promotion is resumable because the next run +accepts only exact already-public releases, promotes the remaining exact draft +IDs, and finishes with one exact tag/release snapshot. + +The normal lane has one all-registry pre-mutation admission contract. It +inventories every selected frozen Cargo and npm `name@version`, routes absent +names back to identity bootstrap, budgets pending Cargo and npm publication, +the atomic Maven deployment, JSR, uncovered public-version reconciliation, and +executor contingency. It computes the critical path from explicit dependency +edges plus implicit same-ecosystem serialization, then compares that path and +reserve with the authoritative mutation window. When the full remaining graph +does not fit, it chooses the canonical first-fit maximal dependency-closed +operation subset. This deliberately preserves frozen plan priority rather than +maximizing the count of cheap reconciliation rows; independent registry lanes +still fill otherwise idle critical-path capacity. Every successful partial run +checkpoints at least one operation, so an N-operation frozen plan needs at most +N invocations. Admission also checks every remaining indivisible operation +against an empty fresh window before it authorizes any mutation. An +intrinsically oversized later operation therefore fails immediately instead of +being discovered after earlier progress or creating an unbounded continuation +chain. A verified bootstrap-ledger receipt removes its public Cargo/npm carrier +from the reconciliation budget. For Cargo rate admission, the gate uses +crates.io's documented default burst of 30 followed by one version per minute. +It overlaps publication work with bucket refill using the recurrence +`start = max(previous_finish, (index - burst) * refill)` and never trusts an +unverifiable capacity secret. A support-side exception naturally allows an +earlier accepted upload; a valid `429 Retry-After` is authoritative. If the +exact dependency graph still cannot fit, the registry checkpoint is uploaded +and an exact-parent continuation resumes it; finalization cannot run until the +receipt set is exhaustive. +Temporary trusted-publishing tokens expire after 30 minutes, so the executor +refreshes them in bounded batches and revokes each token; one early job token is +never treated as a release-long credential. The per-carrier values are +calibrated admission estimates, not worst-case bounds on registry visibility; +the absolute deadline stops pathological latency. A rerun of the same frozen +lock reclassifies matching public partial mutations, proves their checksum/SRI +or payload manifest, and never blindly replays an ambiguous immutable upload. + +## Artifact and OS policy + +Target packages are required where package managers select by OS/CPU/libc/ABI or where a registry has a real file-size limit. They are not separate products. + +- Desktop native: the complete Linux x64/arm64 GNU carrier floor is glibc + 2.38 and `GLIBCXX_3.4.30`. Every ELF file in the native runtime, client + tools, broker, Node addon, and exact-extension payload must stay at or below + those symbol-version ceilings; an individual file may require an older + version. The direct macOS arm64 binaries declare macOS 11.0 in Mach-O load + commands; Windows publishes x64 MSVC carriers. +- Android: arm64-v8a and x86_64 AAR/native extension carriers with API 24 as + the minimum Android level. +- Apple: the release XCFramework contains macOS arm64, iOS device arm64, and + iOS simulator arm64 slices. iOS binaries target iOS 17; the Swift SDK + declares macOS 14 and iOS 17 even though the direct macOS binary carrier has + the lower macOS 11.0 floor. SwiftPM consumes the XCFramework with runtime + resources. Every native exact-extension and native-dependency XCFramework + carries the same three Apple platform slices and is rejected before packaging + if any slice is missing. The base Swift package remains extension-free; + exact-extension products are generated from their separately released, + checksum-covered carrier assets. Every extension release publishes one + immutable `*-swift-extension-carrier.json`: the contrib bundle carrier owns + exactly 32 SQL-member rows, while each independently versioned external + extension carrier owns one. The carrier pins its compatible native base, + direct extension dependency release identities, and member asset digests, + and is covered by the product's canonical release checksum manifest. Swift's + repeatable `--extension-carrier` composition lets an external-only release be + consumed without a Swift version bump; base mismatch, dependency skew, + duplicate SQL ownership, or native-dependency byte conflicts fail closed. + Hosted macOS qualification final-links and + runs a generated native-extension Swift executable against the exact candidate + assets. +- WASIX: portable runtime/extension carriers plus native AOT carriers for Linux + x64/arm64 GNU, macOS arm64, and Windows x64 MSVC. +- SDK façades: Rust/Cargo, npm, Maven/Gradle, and SwiftPM entry points select + only the needed target carriers. JSR is deliberately protocol/query-only and + does not claim native runtime carriers. + +The first release is fail-closed: it does not publish macOS x64, Windows ARM64, +Linux musl, Android 32-bit, or additional Apple architectures. A target becomes +supported only when the product's explicit target manifest declares it, the +publication catalog selects a corresponding carrier, and the frozen lock contains +that carrier with required evidence. A broader runtime matrix or package-manager +fallback must never imply that an undeclared target exists. + +Release qualification must inspect the binaries themselves: Mach-O +`LC_BUILD_VERSION`, Android API/ELF metadata, and Linux ELF symbol-version +requirements must satisfy these floors. Package labels, runner versions, and +successful builds are not compatibility evidence by themselves. + +Rust 1.93.1's Linux `std::process` implementation contains weak +`pidfd_getpid`/`pidfd_spawnp` references. Linking the broker directly on an +Ubuntu 24.04 runner binds those otherwise optional references to +`GLIBC_2.39`, which raises the load-time floor even when the fast path is never +used. Linux broker release assets are therefore linked from a clean target +directory in the exact, digest-pinned `rust:1.93.1-slim-bookworm` container. +After any bounded digest-pinned image acquisition, only Cargo's locked +dependency-fetch phase may use the network inside the container; package code +and build scripts run in a read-only, networkless, capability-free container. +The resulting broker is then executed in that baseline before staging. + +Linux release stages also run in the exact digest-pinned Fedora 39 fixture +after verifying that `getconf GNU_LIBC_VERSION` is exactly `glibc 2.38`. +The rehearsal resolves every staged dynamic ELF dependency and executes the +safe version/argument probes available in the carrier. Fedora 39 is retained +solely as a reproducible ABI fixture: it is end-of-life and this check is not a +claim that Fedora 39 is a security-supported production OS. Oliphaunt's public +contract is the GNU architecture and symbol-version floor, not a distro name. + +Every exact extension has its own product and stable ecosystem façades. Each product's `targets/artifacts.toml` explicitly declares supported/unpublished targets and evidence. The runtime target matrix bounds possible values but never creates extension support by default. + +## Recovery and history repair + +On a failed publish, preserve the candidate SHA, run id, lock, complete checkpoint chain, draft releases, and registry responses. Inventory every selected identity as absent, matching, or conflicting; restore and validate the exact-SHA chain, then resume only missing phases. Repository changes require a new version and candidate. + +History repair is allowed only before any affected product tag/package is public. Freeze main, archive the old tip, qualify the replacement tree, use an exact `--force-with-lease` only with explicit maintainer authorization, and immediately restore force-push protection. The desired bootstrap history is one tree-identical introduction commit followed by one generated release-bump commit. See `.codex/skills/release-oliphaunt/references/recovery.md`. + +## Handoff evidence + +Record the candidate SHA/tree, exact CI run, selected product versions, catalog/lock digests, artifact attestations, registry bootstrap/trust status, publication ledger, promoted release URLs, and clean-install results. “The workflow passed” is not sufficient release evidence without those identities. diff --git a/docs/maintainers/repo-structure.md b/docs/maintainers/repo-structure.md new file mode 100644 index 00000000..96e12760 --- /dev/null +++ b/docs/maintainers/repo-structure.md @@ -0,0 +1,285 @@ +# Repository Structure + +This repository is organized as a multi-product workspace, not as one Rust crate +with adjacent experiments. + +## Evidence + +- Cargo supports a virtual workspace when the root `Cargo.toml` has + `[workspace]` and no `[package]`. Cargo documents this as useful when there + is no primary package or packages should be kept in separate directories: + https://doc.rust-lang.org/cargo/reference/workspaces.html +- Cargo workspaces share one lockfile and one target directory, which keeps + cross-crate Rust development coherent while letting each package own its own + manifest and public boundary: + https://doc.rust-lang.org/cargo/reference/workspaces.html +- Swift Package Manager expects each package to own a `Package.swift`, products, + targets, and target-scoped resources. The Swift SDK therefore lives under + `src/sdks/swift` as a normal Swift package instead of as ad hoc root files: + https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html +- Gradle's multi-project model uses a root build plus isolated subprojects + declared from settings, which maps to the Kotlin/Android SDK under + `src/sdks/kotlin`: + https://docs.gradle.org/current/userguide/multi_project_builds.html +- moon provides the product graph, affected-CI selection, and task discovery. + It does not replace package-native tools; Cargo, SwiftPM/Xcode, Gradle, + pnpm/Expo, PostgreSQL build scripts, and shell harnesses remain authoritative: + https://moonrepo.dev/docs + +## Top-Level Policy + +The repository root should contain shared metadata and entrypoints only. Product +source lives under `src//`. + +- `src/runtimes/liboliphaunt/native/` owns the C ABI and PostgreSQL patch stack. +- `src/sdks/rust/` owns the Rust SDK and Cargo package. +- `src/sdks/swift/`, `src/sdks/kotlin/`, + `src/sdks/react-native/`, and `src/sdks/js/` own platform and + runtime SDKs. +- `src/bindings/wasix-rust/` owns the first-class WASM/WASIX product lane. +- `src/*/moon.yml` is the canonical product graph. `tools/policy/sdk-manifest.toml` + is a small SDK parity ownership registry and must agree with Moon metadata. +- Tooling lives under `tools/`. +- Benchmarks live under `benchmarks/`. +- `src/docs/` is the public documentation product. It owns public SDK + docs under `src/docs/content/sdk`, generated matrices, tested + snippets, API-reference stubs, and LLM docs rendered into + `target/docs`. +- Cross-product architecture, performance, release, and maintainer source docs + live under `docs/`. +- Shared fixture corpora consumed by at least two product-native test suites + live under `src/shared/fixtures/` and are governed by + `src/shared/contracts/test-matrix.toml`. +- Pinned PostgreSQL source metadata, runtime-level third-party source pins, + toolchain pins, extension-owned source pins, and generated extension catalogs + live under `src/postgres/versions/18`, `src/sources/third-party`, + `src/sources/toolchains`, and `src/extensions`. + +There should be no tracked product source under retired roots such as +`crates/`, `sdks/`, root `liboliphaunt/`, or root product examples. + +Tests, fixtures, and benchmarks follow the consumer surface instead of a single +synthetic root: + +- Product-native tests live in each product's package-native test root: + `src/sdks/rust/tests/`, `src/sdks/swift/Tests/`, + `src/sdks/kotlin/oliphaunt/src/*Test/`, + `src/sdks/react-native/src/__tests__/`, + `src/sdks/js/src/__tests__/`, and + `src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/`. +- Rust SDK release-shape tests are split by contract: config and mode + capability contracts stay in `src/sdks/rust/tests/sdk_config_modes.rs`, + handle lifecycle behavior stays in `src/sdks/rust/tests/sdk_shape.rs`, + native-environment smokes stay in + `src/sdks/rust/tests/sdk_native_smoke.rs`, extension catalog and + release-ready extension selection stays in + `src/sdks/rust/tests/sdk_extensions.rs`, and shared backend protocol + fixtures stay in `src/sdks/rust/tests/protocol_query_fixtures.rs`. +- Product-private fixtures stay beside those tests. Shared fixtures move to + `src/shared/fixtures/` only when the contract is consumed by multiple + products, or when a product-specific boundary fixture needs central policy + enforcement. `src/shared/fixtures/protocol/query-response-cases.json` is the + current shared PostgreSQL backend-response corpus consumed from product-native + Rust, Swift, Kotlin, TypeScript, React Native, and WASM parser tests. +- Benchmark plans, datasets, and published reports live in `benchmarks/`. + Executable benchmark harnesses live in `tools/perf/` unless the harness is a + deliberate product API. + +## Product Boundaries + +- `liboliphaunt` is the native C boundary. It owns PostgreSQL source pins, patches, + exported headers, and native build harnesses. +- `src/sdks/rust` is the Rust-native SDK for Tauri and Rust desktop + apps. It should depend on `liboliphaunt` artifacts through explicit + runtime/build configuration, not on `oliphaunt-wasix` internals. +- `docs/maintainers/rust-sdk-policy.md` is the Rust SDK policy entrypoint. The package + source, tests, and release metadata live in `src/sdks/rust`. +- `src/bindings/wasix-rust/crates/oliphaunt-wasix` is the existing WASIX package. It stays intact as a + release lane and comparison target. It should not expose native engine + selection or link/load `liboliphaunt`; native Rust work belongs in + `src/sdks/rust`. +- `src/runtimes/liboliphaunt/wasix/assets/build` is source-only: scripts, patches, + Docker inputs, and shims. Generated WASIX build and work trees live under + `target/oliphaunt-wasix/wasix-build`. +- `src/sdks/swift` is a normal Swift package for iOS and macOS apps. It owns + `Oliphaunt`, a C header target, and Swift tests. +- `src/sdks/kotlin` is a Gradle multi-project Kotlin Multiplatform development + build for the Android SDK. It owns the common suspend API, host-native + conformance targets, Android wrapper, and Kotlin tests; only its Android + AAR/plugin/ABI surfaces are published. +- `src/sdks/react-native` is a React Native New Architecture package. It owns the + TypeScript DX layer and TurboModule Codegen spec. Platform runtime behavior + belongs to the Swift and Kotlin SDKs; React Native native code should be + adapter glue, not a parallel PostgreSQL lifecycle implementation. +- `src/sdks/js` is the SDK for Node.js, Bun, and Deno. Tauri apps currently + use the Rust SDK behind narrow app-owned commands; direct JavaScript/webview + integration is planned. The TypeScript SDK owns JavaScript runtime FFI adapters, npm/JSR package metadata, and + broker/server client orchestration. Its broker implementation depends on the + published `oliphaunt-broker` runtime and the shared `PGOB` protocol, + so that dependency must remain modeled in Moon and product-local release + metadata. + +All SDKs are product peers over the same native PostgreSQL boundary. They should +have parity wherever the target platform can support the behavior honestly; any +gap must be represented as an explicit unsupported error and justified in +`docs/maintainers/sdk-parity-policy.md`. + +## Internal Organization Rules + +- Product crates own their own runtime code. `oliphaunt-wasix` may depend on the + WASIX asset crates; `oliphaunt` may load `liboliphaunt`; neither crate + should call into the other's private modules. +- `tools/policy/check-native-boundaries.sh` enforces the native/legacy split: + the Rust-native SDK and Swift/Kotlin/React Native package manifests must not + depend on `oliphaunt-wasix`, WASIX AOT payload crates, or Wasmer runtime + packages. +- `tools/xtask` is shared repo automation for WASIX assets, release staging, + and optional performance diagnostics. Its default feature set is intentionally + empty; legacy WASIX runtime controls, perf harnesses, template running, and + AOT serializers must be enabled with explicit feature flags. +- `tools/xtask/src/main.rs` is the command router plus shared helpers. WASIX + asset build, packaging, generated manifest, AOT packaging, and staged metadata + orchestration lives in `tools/xtask/src/asset_pipeline.rs`. Source-controlled + asset verification, canonical generated-asset layout checks, asset input + fingerprinting, AOT target catalog checks, and upstream-fix audits live in + `tools/xtask/src/asset_checks.rs`. Generated asset manifest DTOs, AOT + manifest DTOs, asset packaging descriptors, and WASM link-metadata parsing + live in + `tools/xtask/src/asset_manifest.rs`. Asset download/install code lives in + `tools/xtask/src/asset_io.rs`, shared filesystem/archive/hash helpers live in + `tools/xtask/src/fs_utils.rs`, + release workspace assembly lives in `tools/xtask/src/release_workspace.rs`, + source-pin and source-spine handling lives in + `tools/xtask/src/source_spine.rs`, PostgreSQL source/patch-surface guards + live in `tools/xtask/src/postgres_guard.rs`, template execution lives in + `tools/xtask/src/template_runner.rs`, and AOT serialization lives in + `tools/xtask/src/aot_serializer.rs`. Performance benchmark workload/result + construction lives in `tools/perf/runner/src/benchmarks.rs`, report DTOs live + in `tools/perf/runner/src/report.rs`, and legacy WASIX cold/warm probes live + in `tools/perf/runner/src/legacy_wasix.rs`. Native liboliphaunt execution, + child-process entrypoints, and SDK-backed diagnostics live in + `tools/perf/runner/src/native_liboliphaunt.rs`. Native PostgreSQL process, + protocol, and backup/restore controls live in + `tools/perf/runner/src/native_postgres.rs`. Prepared-update benchmark + parsing, transport variants, gates, and native comparison live in + `tools/perf/runner/src/prepared_updates.rs`. Indexed-update, speed-hotspot, + and buffer-cache diagnostics live in `tools/perf/runner/src/diagnostics.rs`. + Benchmark execution should continue to split under `tools/perf/runner/src/` + by collection, aggregation, transport family, diagnostics, and report + rendering. +- Native C ABI concerns are split by layer: + - `src/runtimes/liboliphaunt/native/` for C, PostgreSQL patches, and platform build scripts. + - `src/sdks/rust/src/runtimes/liboliphaunt/native/ffi.rs` for Rust symbol loading and + ABI structs. + - `src/sdks/rust/src/runtimes/liboliphaunt/native/root.rs` for native root locking, + runtime materialization, and opt-in extension asset copying. + - `src/sdks/rust/src/runtimes/liboliphaunt/native/mod.rs` for the Rust runtime/session + implementation. +- Native runtime-resource packaging is split by release artifact concern: + - `src/sdks/rust/src/runtime_resources.rs` for the public resource + package API and selected extension resolution. + - `src/sdks/rust/src/runtime_resources/manifest.rs` for portable + manifest parsing, identifier validation, and runtime artifact path rules. + - `src/sdks/rust/src/runtime_resources/package.rs` for resource-tree + writing, portable tree copying, package manifests, and size reports. + - `src/sdks/rust/src/runtime_resources/extension_artifact.rs` for exact + prebuilt extension artifact creation, archive extraction, and artifact + manifest writing. + - `src/sdks/rust/src/runtime_resources/extension_index.rs` for external + extension artifact index creation, resolution, signing, download, and + checksum verification. + - `src/sdks/rust/src/runtime_resources/static_registry.rs` for iOS and + Android static extension registry metadata, generated C source, and mobile + static archive staging. +- WASM/WASIX runtime internals should keep VM orchestration separate from + reusable host adapters: + - `src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base.rs` for + install/root preparation, runtime layout selection, archive validation, and + PGDATA template orchestration. + - `src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base/template_clone.rs` + for PGDATA template copy/clone mechanics, runtime-state exclusion, reflink + fallback, and symlink handling. + - `src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs` + for PostgreSQL WASIX module lifecycle, exported function wiring, startup + protocol, and split-initdb command orchestration. + - `src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs` + for WASIX virtual stdio adapters, protocol stream attachment, and bounded + process-output capture. + - `src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs` + for host filesystem wrapping, `/dev` mounting, eager-copy PGDATA overlays, + and optional filesystem tracing. +- Public runtime and build controls should use `OLIPHAUNT_*`. Use + `LIBOLIPHAUNT_PATH` only for the literal native C library artifact path. +- Large files should have a reason. Once a module mixes lifecycle, packaging, + protocol, and CLI orchestration, split it along those responsibilities before + adding more behavior. + +## Tooling Rules + +- `.moon/` owns product graph, affected task selection, and shared toolchain + pins. Do not duplicate release dependency rules in ad hoc scripts when moon + metadata can express them. +- `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, and the generated release + PR. Bun release entrypoints under `tools/release/*.mjs` own the public and + protected check, dry-run, publish, exact-SHA tag, and draft-release command + surface. +- Cargo publication runs through `tools/dev/bun.sh + tools/release/release-publish.mjs publish` in the protected Release workflow. + Release packaging freezes each `.crate`, and the exact-byte registry uploader + sends that lock-matching file through crates.io's Registry Web API. Do not + replace it with `cargo publish`, which would repackage the source and break + the frozen-byte contract, or 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 + orchestration over Moon tasks and the release CLI. +- `sdk-contracts:check` owns generated API, SDK registry, C ABI header-copy, + native-boundary, and README-example contracts. The small + `tools/policy/check-sdk-parity.sh` entry point is a local convenience + aggregate. Exact extension catalogs belong to `extension-model:check`, while + SDK behavior, React Native delegation, package contents, and installed-app + evidence belong to product-local Moon tasks. Stable CI does not infer those + contracts from prose, test names, or implementation-source spellings. +- `prek` owns Git hooks as a language-neutral runner for whitespace, format, + and commit-message guards. Heavy asset, lockfile, and workspace checks belong + in Moon tasks, product-local tools, release CLI subcommands, and CI, not + automatic pre-push hooks. +- `actionlint` and `zizmor` are intentionally paired: actionlint validates + GitHub Actions syntax and expression semantics; zizmor audits workflow + security posture. Do not add a third workflow linter without removing overlap. +- Package-native tools stay native: Cargo for Rust, SwiftPM/Xcode tooling for + Swift, Gradle for Kotlin/Android, and React Native's own Codegen/build flow + for React Native. + +## Current Tree + +```text +. +├── Cargo.toml +├── package.json +├── benchmarks/ +├── docs/ +├── examples/ +│ └── integration/ +├── src/ +│ ├── shared/ +│ │ ├── contracts/ +│ │ └── fixtures/ +│ ├── liboliphaunt/ +│ ├── oliphaunt-rust/ +│ ├── oliphaunt-swift/ +│ ├── oliphaunt-kotlin/ +│ ├── oliphaunt-react-native/ +│ ├── oliphaunt-js/ +│ ├── oliphaunt-wasix/ +│ └── docs/ +└── tools/ + ├── dev/ + ├── perf/ + ├── policy/ + ├── release/ + └── xtask/ +``` diff --git a/docs/maintainers/rust-sdk-policy.md b/docs/maintainers/rust-sdk-policy.md new file mode 100644 index 00000000..d1b41e27 --- /dev/null +++ b/docs/maintainers/rust-sdk-policy.md @@ -0,0 +1,59 @@ +# oliphaunt Rust SDK Policy + +The Rust SDK is a peer product SDK for Tauri and Rust desktop apps. Its package +source lives in `src/sdks/rust` rather than root docs so Cargo workspace +ownership, release metadata, examples, benches, and tests stay idiomatic. + +Target users: + +- Tauri desktop apps; +- Rust desktop apps that want embedded PostgreSQL without sidecars in direct + mode; +- Rust services or developer tools that want broker/server modes with local + PostgreSQL compatibility. + +Validate the Rust SDK with: + +```bash +moon run oliphaunt-rust:check +``` + +Other SDKs should match the shared Oliphaunt concepts where the platform allows it: + +- engine modes: native direct, native broker, native server; +- raw PostgreSQL protocol boundary; +- typed query helpers layered above raw protocol; +- transaction helpers that keep one physical session pinned and reject + unpinned interleaving, including backup/checkpoint work, while still allowing + pinned raw and streaming protocol calls. Use `transaction()` when you want an + explicit handle, or `with_transaction(async |tx| { ... })` for commit/rollback + closure ergonomics; +- `checkpoint()` for explicit PostgreSQL checkpoint requests through the opened + engine; +- startup identity through builder-level `username(...)` and `database(...)` + options that feed direct, broker, and server-owned PostgreSQL sessions; +- SDK-owned executable/tooling paths such as `initdb_tooling_only(...)`, + `broker_executable(...)`, and `server_executable(...)` are rejected when + empty or NUL-containing before process startup; +- structured PostgreSQL errors with SQLSTATE and raw `ErrorResponse` fields; +- exact extensions selected before open; +- physical backup/restore for same-version archives; +- capability reporting for raw and streaming protocol, cancellation, + backup/restore, simple-query execution, extensions, and session + semantics, including concrete backup and restore format support through + capability and opened-handle `supports_backup_format` and + `supports_restore_format` helpers; +- `max_client_sessions(...)` is an honest concurrency knob: direct and broker mode reject values other than `1`; server mode is the mode for independent + PostgreSQL client sessions and pools; +- SDK-boundary rejection for unsupported backup formats before work is queued + onto the engine executor, and unsupported restore formats before a target + root is materialized; +- explicit mode support discovery through + `EngineCapabilities::rust_sdk_support()`; +- cancellation and close semantics; +- packaged runtime/template resources. + +Swift, Kotlin, TypeScript, React Native, and WASM may expose platform-native +naming, async, and packaging conventions, but deviations from the shared +Oliphaunt contract should be documented and justified rather than allowed to +drift silently. diff --git a/docs/maintainers/sdk-api-surface.md b/docs/maintainers/sdk-api-surface.md new file mode 100644 index 00000000..338a6f09 --- /dev/null +++ b/docs/maintainers/sdk-api-surface.md @@ -0,0 +1,788 @@ + +# SDK API Surface Inventory + +This no-build inventory makes public SDK drift visible in review. It is a symbol-level guard, not a replacement for full language reference documentation. + +Regenerate with: + +```sh +node tools/policy/generate-sdk-api-surface.mjs --write +``` + +## Rust: oliphaunt + +- `oliphaunt::BackgroundCheckpointSkipReason` +- `oliphaunt::BackgroundPreparationOptions` +- `oliphaunt::BackgroundPreparationResult` +- `oliphaunt::BackupArtifact` +- `oliphaunt::BackupFormat` +- `oliphaunt::BackupRequest` +- `oliphaunt::BenchmarkMetric` +- `oliphaunt::BenchmarkTarget` +- `oliphaunt::BootstrapStrategy` +- `oliphaunt::DEFAULT_DATABASE` +- `oliphaunt::DEFAULT_USERNAME` +- `oliphaunt::DatabaseRoot` +- `oliphaunt::DurabilityProfile` +- `oliphaunt::EngineCancel` +- `oliphaunt::EngineCapabilities` +- `oliphaunt::EngineMode` +- `oliphaunt::EngineModeSupport` +- `oliphaunt::EngineSession` +- `oliphaunt::Error` +- `oliphaunt::Extension` +- `oliphaunt::ExtensionArtifactPolicy` +- `oliphaunt::ExtensionCoverage` +- `oliphaunt::ExtensionManifestEntry` +- `oliphaunt::ExtensionModuleAsset` +- `oliphaunt::ExtensionRedistribution` +- `oliphaunt::ExtensionSizeReport` +- `oliphaunt::ExtensionSmokeCoverage` +- `oliphaunt::ExtensionSmokePlan` +- `oliphaunt::ExtensionSourceKind` +- `oliphaunt::ExtensionSqlAsset` +- `oliphaunt::MobileStaticLinkStatus` +- `oliphaunt::MobileStaticRegistryMetadata` +- `oliphaunt::MobileStaticRegistryState` +- `oliphaunt::NATIVE_EXTENSION_MANIFEST` +- `oliphaunt::NativeBrokerConfig` +- `oliphaunt::NativeBrokerRuntime` +- `oliphaunt::NativeDirectConfig` +- `oliphaunt::NativeExtensionArtifact` +- `oliphaunt::NativeExtensionArtifactFormat` +- `oliphaunt::NativeExtensionArtifactIndex` +- `oliphaunt::NativeExtensionArtifactIndexArtifact` +- `oliphaunt::NativeExtensionArtifactIndexCatalog` +- `oliphaunt::NativeExtensionArtifactIndexCatalogEntry` +- `oliphaunt::NativeExtensionArtifactIndexCreateOptions` +- `oliphaunt::NativeExtensionArtifactIndexOptions` +- `oliphaunt::NativeExtensionArtifactIndexResolution` +- `oliphaunt::NativeExtensionArtifactIndexSignature` +- `oliphaunt::NativeExtensionArtifactIndexSigningOptions` +- `oliphaunt::NativeExtensionArtifactIndexTrustRoot` +- `oliphaunt::NativeExtensionArtifactOptions` +- `oliphaunt::NativeExtensionMobileStaticArchive` +- `oliphaunt::NativeExtensionMobileStaticDependencyArchive` +- `oliphaunt::NativeExtensionStaticSymbolAlias` +- `oliphaunt::NativePrebuiltExtensionArtifact` +- `oliphaunt::NativeRuntime` +- `oliphaunt::NativeRuntimeFeature` +- `oliphaunt::NativeRuntimeResourceOptions` +- `oliphaunt::NativeRuntimeResourceSizeReport` +- `oliphaunt::NativeRuntimeResources` +- `oliphaunt::NativeServerConfig` +- `oliphaunt::NativeServerRuntime` +- `oliphaunt::Oliphaunt` +- `oliphaunt::OliphauntBuilder` +- `oliphaunt::OliphauntRuntime` +- `oliphaunt::OliphauntRuntimeSource` +- `oliphaunt::OpenConfig` +- `oliphaunt::PerformanceGate` +- `oliphaunt::PerformanceGateSet` +- `oliphaunt::PerformanceOperator` +- `oliphaunt::PostgresError` +- `oliphaunt::PostgresErrorField` +- `oliphaunt::PostgresStartupGuc` +- `oliphaunt::ProtocolRequest` +- `oliphaunt::ProtocolResponse` +- `oliphaunt::QueryField` +- `oliphaunt::QueryFormat` +- `oliphaunt::QueryParam` +- `oliphaunt::QueryResult` +- `oliphaunt::QueryRow` +- `oliphaunt::RestoreRequest` +- `oliphaunt::RestoreTargetPolicy` +- `oliphaunt::Result` +- `oliphaunt::RootLockPolicy` +- `oliphaunt::RuntimeFootprintProfile` +- `oliphaunt::RuntimeUnavailable` +- `oliphaunt::SessionConcurrency` +- `oliphaunt::SessionPin` +- `oliphaunt::StorageConfig` +- `oliphaunt::Transaction` +- `oliphaunt::build_native_runtime_resources` +- `oliphaunt::create_prebuilt_extension_artifact` +- `oliphaunt::create_prebuilt_extension_artifact_index` +- `oliphaunt::list_prebuilt_extension_artifact_index_catalog` +- `oliphaunt::parse_query_response` +- `oliphaunt::register_build_resources!` +- `oliphaunt::register_build_resources_dir` +- `oliphaunt::required_shared_preload_libraries` +- `oliphaunt::resolve_extension_selection` +- `oliphaunt::resolve_prebuilt_extension_artifacts_from_indexes` +- `oliphaunt::sign_prebuilt_extension_artifact_index` + +## Swift: Oliphaunt + +- `OliphauntBackgroundPreparationOptions.cancelActiveWork` +- `OliphauntBackgroundPreparationOptions.checkpointWhenIdle` +- `OliphauntBackgroundPreparationOptions.init` +- `OliphauntBackgroundPreparationResult.cancelledActiveWork` +- `OliphauntBackgroundPreparationResult.checkpointed` +- `OliphauntBackgroundPreparationResult.init` +- `OliphauntBackgroundPreparationResult.skippedCheckpointReason` +- `OliphauntBackupArtifact.bytes` +- `OliphauntBackupArtifact.format` +- `OliphauntBackupArtifact.init` +- `OliphauntBackupRequest.format` +- `OliphauntBackupRequest.init` +- `OliphauntCapabilities.backupFormats` +- `OliphauntCapabilities.backupRestore` +- `OliphauntCapabilities.connectionString` +- `OliphauntCapabilities.crashRestartable` +- `OliphauntCapabilities.extensions` +- `OliphauntCapabilities.independentSessions` +- `OliphauntCapabilities.init` +- `OliphauntCapabilities.maxClientSessions` +- `OliphauntCapabilities.mode` +- `OliphauntCapabilities.multiRoot` +- `OliphauntCapabilities.processIsolated` +- `OliphauntCapabilities.protocolRaw` +- `OliphauntCapabilities.protocolStream` +- `OliphauntCapabilities.queryCancel` +- `OliphauntCapabilities.reopenable` +- `OliphauntCapabilities.restoreFormats` +- `OliphauntCapabilities.rootSwitchable` +- `OliphauntCapabilities.sameRootLogicalReopen` +- `OliphauntCapabilities.simpleQuery` +- `OliphauntCapabilities.supportsBackupFormat()` +- `OliphauntCapabilities.supportsRestoreFormat()` +- `OliphauntConfiguration.database` +- `OliphauntConfiguration.durability` +- `OliphauntConfiguration.extensions` +- `OliphauntConfiguration.init` +- `OliphauntConfiguration.mode` +- `OliphauntConfiguration.root` +- `OliphauntConfiguration.runtimeFootprint` +- `OliphauntConfiguration.startupGUCs` +- `OliphauntConfiguration.username` +- `OliphauntDatabase.backup()` +- `OliphauntDatabase.cancel()` +- `OliphauntDatabase.capabilities()` +- `OliphauntDatabase.checkpoint()` +- `OliphauntDatabase.close()` +- `OliphauntDatabase.connectionString()` +- `OliphauntDatabase.execProtocolRaw()` +- `OliphauntDatabase.execProtocolStream()` +- `OliphauntDatabase.execute()` +- `OliphauntDatabase.open()` +- `OliphauntDatabase.prepareForBackground()` +- `OliphauntDatabase.query()` +- `OliphauntDatabase.restore()` +- `OliphauntDatabase.resumeFromBackground()` +- `OliphauntDatabase.supportedModes()` +- `OliphauntDatabase.supportsBackupFormat()` +- `OliphauntDatabase.supportsRestoreFormat()` +- `OliphauntDatabase.transaction()` +- `OliphauntDefaultEngine.brokerUnavailableReason` +- `OliphauntDefaultEngine.init` +- `OliphauntDefaultEngine.open()` +- `OliphauntDefaultEngine.restore()` +- `OliphauntDefaultEngine.serverUnavailableReason` +- `OliphauntDefaultEngine.supportedModes` +- `OliphauntEngineModeSupport.available` +- `OliphauntEngineModeSupport.capabilities` +- `OliphauntEngineModeSupport.init` +- `OliphauntEngineModeSupport.mode` +- `OliphauntEngineModeSupport.unavailableReason` +- `OliphauntError.description` +- `OliphauntExtensionSizeReport.bytes` +- `OliphauntExtensionSizeReport.fileCount` +- `OliphauntExtensionSizeReport.init` +- `OliphauntExtensionSizeReport.name` +- `OliphauntNativeDirectEngine.database` +- `OliphauntNativeDirectEngine.init` +- `OliphauntNativeDirectEngine.libraryURL` +- `OliphauntNativeDirectEngine.open()` +- `OliphauntNativeDirectEngine.restore()` +- `OliphauntNativeDirectEngine.runtimeDirectory` +- `OliphauntNativeDirectEngine.runtimeResources` +- `OliphauntNativeDirectEngine.supportedModes` +- `OliphauntNativeDirectEngine.username` +- `OliphauntPostgresError.columnName` +- `OliphauntPostgresError.constraintName` +- `OliphauntPostgresError.dataTypeName` +- `OliphauntPostgresError.description` +- `OliphauntPostgresError.detail` +- `OliphauntPostgresError.fallback()` +- `OliphauntPostgresError.fields` +- `OliphauntPostgresError.hint` +- `OliphauntPostgresError.init` +- `OliphauntPostgresError.message` +- `OliphauntPostgresError.position` +- `OliphauntPostgresError.schemaName` +- `OliphauntPostgresError.severity` +- `OliphauntPostgresError.sqlstate` +- `OliphauntPostgresError.tableName` +- `OliphauntPostgresError.whereText` +- `OliphauntPostgresErrorField.code` +- `OliphauntPostgresErrorField.init` +- `OliphauntPostgresErrorField.value` +- `OliphauntProtocol.extendedQuery()` +- `OliphauntProtocol.simpleQuery()` +- `OliphauntQueryField.format` +- `OliphauntQueryField.name` +- `OliphauntQueryField.tableAttribute` +- `OliphauntQueryField.tableOID` +- `OliphauntQueryField.typeModifier` +- `OliphauntQueryField.typeOID` +- `OliphauntQueryField.typeSize` +- `OliphauntQueryParam.binary()` +- `OliphauntQueryResult.commandTag` +- `OliphauntQueryResult.fieldIndex()` +- `OliphauntQueryResult.fields` +- `OliphauntQueryResult.getText()` +- `OliphauntQueryResult.rowCount` +- `OliphauntQueryResult.rows` +- `OliphauntQueryRow.text()` +- `OliphauntQueryRow.values` +- `OliphauntRestoreRequest.artifact` +- `OliphauntRestoreRequest.init` +- `OliphauntRestoreRequest.replaceExisting()` +- `OliphauntRestoreRequest.root` +- `OliphauntRestoreRequest.targetPolicy` +- `OliphauntRuntimeResourceSizeReport.extensions` +- `OliphauntRuntimeResourceSizeReport.init` +- `OliphauntRuntimeResourceSizeReport.mobileStaticRegistryPending` +- `OliphauntRuntimeResourceSizeReport.mobileStaticRegistryRegistered` +- `OliphauntRuntimeResourceSizeReport.mobileStaticRegistryState` +- `OliphauntRuntimeResourceSizeReport.nativeModuleStems` +- `OliphauntRuntimeResourceSizeReport.packageBytes` +- `OliphauntRuntimeResourceSizeReport.runtimeBytes` +- `OliphauntRuntimeResourceSizeReport.runtimeFeatures` +- `OliphauntRuntimeResourceSizeReport.selectedExtensionBytes` +- `OliphauntRuntimeResourceSizeReport.staticRegistryBytes` +- `OliphauntRuntimeResourceSizeReport.templatePgdataBytes` +- `OliphauntRuntimeResources.bundled()` +- `OliphauntRuntimeResources.cacheRoot` +- `OliphauntRuntimeResources.defaultCacheRoot()` +- `OliphauntRuntimeResources.init` +- `OliphauntRuntimeResources.materializeRuntime()` +- `OliphauntRuntimeResources.packageSizeReport()` +- `OliphauntRuntimeResources.preparePgdata()` +- `OliphauntRuntimeResources.resourceRoot` +- `OliphauntSDKSupport.allModes` +- `OliphauntSDKSupport.capabilities()` +- `OliphauntSDKSupport.nativeDirectOnly()` +- `OliphauntSDKSupport.unavailable()` +- `OliphauntSession.execProtocolStream()` +- `OliphauntStartupGUC.init` +- `OliphauntStartupGUC.name` +- `OliphauntStartupGUC.value` +- `OliphauntTransaction.execProtocolRaw()` +- `OliphauntTransaction.execProtocolStream()` +- `OliphauntTransaction.execute()` +- `OliphauntTransaction.query()` +- `RuntimeUnavailableEngine.init` +- `RuntimeUnavailableEngine.open()` +- `RuntimeUnavailableEngine.restore()` +- `RuntimeUnavailableEngine.supportedModes` +- `actor OliphauntDatabase` +- `enum OliphauntBackgroundCheckpointSkipReason` +- `enum OliphauntBackupFormat` +- `enum OliphauntDurability` +- `enum OliphauntEngineMode` +- `enum OliphauntError` +- `enum OliphauntProtocol` +- `enum OliphauntQueryFormat` +- `enum OliphauntQueryParam` +- `enum OliphauntRestoreTargetPolicy` +- `enum OliphauntRuntimeFootprintProfile` +- `enum OliphauntSDKSupport` +- `extension OliphauntDatabase` +- `extension OliphauntSession` +- `extension OliphauntTransaction` +- `parseOliphauntQueryResponse()` +- `protocol OliphauntEngine` +- `protocol OliphauntEngineSupportProvider` +- `protocol OliphauntSession` +- `registerPackagedExtensionResource()` +- `struct OliphauntBackgroundPreparationOptions` +- `struct OliphauntBackgroundPreparationResult` +- `struct OliphauntBackupArtifact` +- `struct OliphauntBackupRequest` +- `struct OliphauntCapabilities` +- `struct OliphauntConfiguration` +- `struct OliphauntDefaultEngine` +- `struct OliphauntEngineModeSupport` +- `struct OliphauntExtensionSizeReport` +- `struct OliphauntNativeDirectEngine` +- `struct OliphauntPostgresError` +- `struct OliphauntPostgresErrorField` +- `struct OliphauntQueryField` +- `struct OliphauntQueryResult` +- `struct OliphauntQueryRow` +- `struct OliphauntRestoreRequest` +- `struct OliphauntRuntimeResourceSizeReport` +- `struct OliphauntRuntimeResources` +- `struct OliphauntStartupGUC` +- `struct OliphauntTransaction` +- `struct RuntimeUnavailableEngine` +- `unregisterPackagedExtensionResource()` + +## Kotlin: oliphaunt + +### commonMain + +- `EngineCapabilities.supportsBackupFormat()` +- `EngineCapabilities.supportsRestoreFormat()` +- `OliphauntDatabase.backup()` +- `OliphauntDatabase.cancel()` +- `OliphauntDatabase.capabilities()` +- `OliphauntDatabase.checkpoint()` +- `OliphauntDatabase.close()` +- `OliphauntDatabase.connectionString()` +- `OliphauntDatabase.execProtocolRaw()` +- `OliphauntDatabase.execProtocolStream()` +- `OliphauntDatabase.execute()` +- `OliphauntDatabase.open()` +- `OliphauntDatabase.prepareForBackground()` +- `OliphauntDatabase.query()` +- `OliphauntDatabase.restore()` +- `OliphauntDatabase.resumeFromBackground()` +- `OliphauntDatabase.supportedModes()` +- `OliphauntDatabase.supportsBackupFormat()` +- `OliphauntDatabase.supportsRestoreFormat()` +- `OliphauntDatabase.transaction()` +- `OliphauntEngine.open()` +- `OliphauntEngine.restore()` +- `OliphauntEngine.supportedModes()` +- `OliphauntRuntimeSupport.allModes` +- `OliphauntRuntimeSupport.capabilitiesFor()` +- `OliphauntRuntimeSupport.nativeDirectOnly()` +- `OliphauntRuntimeSupport.unavailable()` +- `OliphauntSession.backup()` +- `OliphauntSession.cancel()` +- `OliphauntSession.capabilities()` +- `OliphauntSession.close()` +- `OliphauntSession.execProtocolRaw()` +- `OliphauntSession.execProtocolStream()` +- `OliphauntTransaction.execProtocolRaw()` +- `OliphauntTransaction.execProtocolStream()` +- `OliphauntTransaction.execute()` +- `OliphauntTransaction.query()` +- `PostgresError.fallback()` +- `PostgresError.fromFields()` +- `PostgresException.postgresError` +- `ProtocolRequest.Companion.extendedQuery()` +- `ProtocolRequest.simpleQuery()` +- `QueryFormat.fromCode()` +- `QueryParam.binary()` +- `QueryParam.text()` +- `QueryParam.value` +- `QueryResult.fieldIndex()` +- `QueryResult.getText()` +- `QueryResult.rowCount` +- `QueryRow.text()` +- `QueryRow.values` +- `RestoreRequest.replaceExisting()` +- `class BackgroundPreparationOptions` +- `class BackgroundPreparationResult` +- `class BackupArtifact` +- `class BackupRequest` +- `class EngineCapabilities` +- `class EngineModeSupport` +- `class OliphauntConfig` +- `class OliphauntDatabase` +- `class OliphauntException` +- `class OliphauntTransaction` +- `class PostgresError` +- `class PostgresErrorField` +- `class PostgresException` +- `class PostgresStartupGuc` +- `class ProtocolRequest` +- `class ProtocolResponse` +- `class QueryField` +- `class QueryFormat` +- `class QueryFormat.Other` +- `class QueryParam` +- `class QueryParam.Binary` +- `class QueryParam.Text` +- `class QueryResult` +- `class QueryRow` +- `class RestoreRequest` +- `class RuntimeUnavailableEngine` +- `defaultOliphauntEngine()` +- `enum class BackgroundCheckpointSkipReason` +- `enum class BackupFormat` +- `enum class DurabilityProfile` +- `enum class EngineMode` +- `enum class RestoreTargetPolicy` +- `enum class RuntimeFootprintProfile` +- `interface OliphauntEngine` +- `interface OliphauntSession` +- `object OliphauntRuntimeSupport` +- `object QueryFormat.Binary` +- `object QueryFormat.Text` +- `object QueryParam.Null` +- `parseQueryResponse()` + +### androidMain + +- `AndroidNativeDirectEngine.packageSizeReport()` +- `OliphauntAndroid.open()` +- `OliphauntAndroid.packageSizeReport()` +- `OliphauntAndroid.restore()` +- `OliphauntAndroid.supportedModes()` +- `class AndroidNativeDirectEngine` +- `class OliphauntExtensionSizeReport` +- `class OliphauntPackageSizeReport` +- `defaultOliphauntEngine()` +- `object OliphauntAndroid` + +### jvmMain + +- `defaultOliphauntEngine()` + +### nativeMain + +- `class NativeDirectEngine` +- `defaultOliphauntEngine()` + +## React Native: @oliphaunt/react-native + +### Types + +- `BackgroundPreparationOptions` +- `BackgroundPreparationResult` +- `BackupArtifact` +- `BackupFormat` +- `BinaryInput` +- `DurabilityProfile` +- `EngineCapabilities` +- `EngineMode` +- `EngineModeSupport` +- `ExtensionSizeReport` +- `JsiRawProtocolTransport` +- `LatencySummary` +- `MobileReleaseExtensionProof` +- `MobileReleasePlatform` +- `NativeCapabilities` +- `NativeEngineModeSupport` +- `NativeExtensionSizeReport` +- `NativeOpenConfig` +- `NativePackageSizeReport` +- `NativeProcessMemoryReport` +- `NativeResourceConfig` +- `OliphauntClient` +- `OliphauntTransaction` +- `OpenConfig` +- `PackageSizeReport` +- `PackageSizeReportOptions` +- `PostgresErrorField` +- `PostgresSettings` +- `PostgresStartupGUC` +- `ProcessMemoryReport` +- `ProtocolChunkCallback` +- `QueryBinaryInput` +- `QueryField` +- `QueryFormat` +- `QueryParam` +- `QueryResult` +- `QueryRow` +- `RawProtocolTransport` +- `ReactNativeBenchmarkOptions` +- `ReactNativeBenchmarkReport` +- `ReactNativeBenchmarkWorkload` +- `ReactNativeSmokeOptions` +- `ReactNativeSmokeReport` +- `RestoreOptions` +- `RuntimeFootprintProfile` +- `Spec` +- `ThroughputSummary` + +### Values + +- `MOBILE_RELEASE_EXTENSION_CATALOG_SHA256` +- `MOBILE_RELEASE_EXTENSION_PROOF_COUNT` +- `Oliphaunt` +- `OliphauntDatabase` +- `PostgresError` +- `createOliphauntClient` +- `extendedQuery` +- `mobileReleaseExtensionProofPlan` +- `parseQueryResponse` +- `runInstalledOliphauntReactNativeBenchmark` +- `runInstalledOliphauntReactNativeSmoke` +- `runOliphauntReactNativeBenchmark` +- `runOliphauntReactNativeSmoke` +- `simpleQuery` +- `supportsBackupFormat` +- `supportsRestoreFormat` + +### Members + +- `BackgroundPreparationOptions.cancelActiveWork` +- `BackgroundPreparationOptions.checkpointWhenIdle` +- `BackgroundPreparationResult.cancelledActiveWork` +- `BackgroundPreparationResult.checkpointed` +- `BackgroundPreparationResult.skippedCheckpointReason` +- `BackupArtifact.bytes` +- `BackupArtifact.format` +- `EngineCapabilities.backupFormats` +- `EngineCapabilities.backupRestore` +- `EngineCapabilities.connectionString` +- `EngineCapabilities.crashRestartable` +- `EngineCapabilities.engine` +- `EngineCapabilities.extensions` +- `EngineCapabilities.independentSessions` +- `EngineCapabilities.maxClientSessions` +- `EngineCapabilities.multiRoot` +- `EngineCapabilities.processIsolated` +- `EngineCapabilities.protocolRaw` +- `EngineCapabilities.protocolStream` +- `EngineCapabilities.queryCancel` +- `EngineCapabilities.rawProtocolTransport` +- `EngineCapabilities.reopenable` +- `EngineCapabilities.restoreFormats` +- `EngineCapabilities.rootSwitchable` +- `EngineCapabilities.sameRootLogicalReopen` +- `EngineCapabilities.simpleQuery` +- `EngineModeSupport.available` +- `EngineModeSupport.capabilities` +- `EngineModeSupport.engine` +- `EngineModeSupport.unavailableReason` +- `ExtensionSizeReport.bytes` +- `ExtensionSizeReport.fileCount` +- `ExtensionSizeReport.name` +- `OliphauntClient.open()` +- `OliphauntClient.packageSizeReport()` +- `OliphauntClient.processMemory()` +- `OliphauntClient.restore()` +- `OliphauntClient.supportedModes()` +- `OliphauntDatabase.backup()` +- `OliphauntDatabase.cancel()` +- `OliphauntDatabase.capabilities()` +- `OliphauntDatabase.checkpoint()` +- `OliphauntDatabase.close()` +- `OliphauntDatabase.connectionString()` +- `OliphauntDatabase.execProtocolRaw()` +- `OliphauntDatabase.execProtocolStream()` +- `OliphauntDatabase.execute()` +- `OliphauntDatabase.handle` +- `OliphauntDatabase.prepareForBackground()` +- `OliphauntDatabase.query()` +- `OliphauntDatabase.resumeFromBackground()` +- `OliphauntDatabase.supportsBackupFormat()` +- `OliphauntDatabase.supportsRestoreFormat()` +- `OliphauntTransaction.execProtocolRaw()` +- `OliphauntTransaction.execProtocolStream()` +- `OliphauntTransaction.execute()` +- `OliphauntTransaction.query()` +- `OpenConfig.database` +- `OpenConfig.durability` +- `OpenConfig.engine` +- `OpenConfig.extensions` +- `OpenConfig.libraryPath` +- `OpenConfig.resourceRoot` +- `OpenConfig.root` +- `OpenConfig.runtimeDirectory` +- `OpenConfig.runtimeFootprint` +- `OpenConfig.startupGUCs` +- `OpenConfig.temporary` +- `OpenConfig.username` +- `PackageSizeReport.extensions` +- `PackageSizeReport.mobileStaticRegistryPending` +- `PackageSizeReport.mobileStaticRegistryRegistered` +- `PackageSizeReport.mobileStaticRegistryState` +- `PackageSizeReport.nativeModuleStems` +- `PackageSizeReport.packageBytes` +- `PackageSizeReport.runtimeBytes` +- `PackageSizeReport.runtimeFeatures` +- `PackageSizeReport.selectedExtensionBytes` +- `PackageSizeReport.staticRegistryBytes` +- `PackageSizeReport.templatePgdataBytes` +- `PackageSizeReportOptions.resourceRoot` +- `PostgresErrorField.code` +- `PostgresErrorField.value` +- `ProcessMemoryReport.nativeHeapAllocatedBytes` +- `ProcessMemoryReport.nativeHeapSizeBytes` +- `ProcessMemoryReport.peakResidentBytes` +- `ProcessMemoryReport.physicalFootprintBytes` +- `ProcessMemoryReport.residentBytes` +- `ProcessMemoryReport.runtimeFreeBytes` +- `ProcessMemoryReport.runtimeTotalBytes` +- `ProcessMemoryReport.source` +- `ProcessMemoryReport.totalPrivateDirtyKb` +- `ProcessMemoryReport.totalPssKb` +- `ProcessMemoryReport.totalSharedDirtyKb` +- `ProcessMemoryReport.virtualBytes` +- `QueryField.format` +- `QueryField.name` +- `QueryField.tableAttribute` +- `QueryField.tableOid` +- `QueryField.typeModifier` +- `QueryField.typeOid` +- `QueryField.typeSize` +- `QueryResult.commandTag` +- `QueryResult.fieldIndex()` +- `QueryResult.fields` +- `QueryResult.getText()` +- `QueryResult.rowCount` +- `QueryResult.rows` +- `QueryRow.text()` +- `QueryRow.values` +- `RestoreOptions.artifact` +- `RestoreOptions.libraryPath` +- `RestoreOptions.replaceExisting` +- `RestoreOptions.root` +- `createOliphauntClient()` +- `extendedQuery()` +- `parseQueryResponse()` +- `simpleQuery()` +- `supportsBackupFormat()` +- `supportsRestoreFormat()` + +## TypeScript: @oliphaunt/ts + +### Types + +- `BackgroundPreparationOptions` +- `BackgroundPreparationResult` +- `BackupArtifact` +- `BackupFormat` +- `BinaryInput` +- `BrokerTransport` +- `DurabilityProfile` +- `EngineCapabilities` +- `EngineMode` +- `EngineModeSupport` +- `JavaScriptRuntime` +- `MaybePromise` +- `NativeBinding` +- `NativeBindingOptions` +- `NativeHandle` +- `NativeOpenConfig` +- `NativeRestoreOptions` +- `NormalizedOpenConfig` +- `OliphauntClient` +- `OliphauntTransaction` +- `OpenConfig` +- `PostgresStartupGUC` +- `ProtocolChunkCallback` +- `RawProtocolTransport` +- `RestoreOptions` +- `RuntimeBinding` +- `RuntimeFootprintProfile` +- `RuntimeHandle` +- `SupportedModesOptions` + +### Values + +- `Oliphaunt` +- `OliphauntDatabase` +- `PostgresError` +- `assertSuccessfulQueryResponse` +- `createBunNativeBinding` +- `createDefaultNativeBinding` +- `createDenoNativeBinding` +- `createNodeNativeBinding` +- `createOliphauntClient` +- `extendedQuery` +- `nativeDirectCapabilities` +- `parseQueryResponse` +- `simpleQuery` +- `supportsBackupFormat` +- `supportsRestoreFormat` +- `toUint8Array` +- `type NativeBindingFactory` +- `type PostgresErrorField` +- `type QueryBinaryInput` +- `type QueryField` +- `type QueryFormat` +- `type QueryParam` +- `type QueryResult` +- `type QueryRow` + +### Members + +- `BackgroundPreparationOptions.cancelActiveWork` +- `BackgroundPreparationOptions.checkpointWhenIdle` +- `BackgroundPreparationResult.cancelledActiveWork` +- `BackgroundPreparationResult.checkpointed` +- `BackgroundPreparationResult.skippedCheckpointReason` +- `BackupArtifact.bytes` +- `BackupArtifact.format` +- `EngineCapabilities.backupFormats` +- `EngineCapabilities.backupRestore` +- `EngineCapabilities.connectionString` +- `EngineCapabilities.crashRestartable` +- `EngineCapabilities.engine` +- `EngineCapabilities.extensions` +- `EngineCapabilities.independentSessions` +- `EngineCapabilities.maxClientSessions` +- `EngineCapabilities.multiRoot` +- `EngineCapabilities.processIsolated` +- `EngineCapabilities.protocolRaw` +- `EngineCapabilities.protocolStream` +- `EngineCapabilities.queryCancel` +- `EngineCapabilities.rawProtocolTransport` +- `EngineCapabilities.reopenable` +- `EngineCapabilities.restoreFormats` +- `EngineCapabilities.rootSwitchable` +- `EngineCapabilities.sameRootLogicalReopen` +- `EngineCapabilities.simpleQuery` +- `EngineModeSupport.available` +- `EngineModeSupport.capabilities` +- `EngineModeSupport.engine` +- `EngineModeSupport.unavailableReason` +- `OliphauntClient.open()` +- `OliphauntClient.restore()` +- `OliphauntClient.supportedModes()` +- `OliphauntDatabase.backup()` +- `OliphauntDatabase.cancel()` +- `OliphauntDatabase.capabilities()` +- `OliphauntDatabase.checkpoint()` +- `OliphauntDatabase.close()` +- `OliphauntDatabase.connectionString()` +- `OliphauntDatabase.execProtocolRaw()` +- `OliphauntDatabase.execProtocolStream()` +- `OliphauntDatabase.execute()` +- `OliphauntDatabase.handle` +- `OliphauntDatabase.prepareForBackground()` +- `OliphauntDatabase.query()` +- `OliphauntDatabase.resumeFromBackground()` +- `OliphauntDatabase.supportsBackupFormat()` +- `OliphauntDatabase.supportsRestoreFormat()` +- `OliphauntTransaction.execProtocolRaw()` +- `OliphauntTransaction.execProtocolStream()` +- `OliphauntTransaction.execute()` +- `OliphauntTransaction.query()` +- `OpenConfig.brokerExecutable` +- `OpenConfig.brokerMaxRoots` +- `OpenConfig.brokerTransport` +- `OpenConfig.database` +- `OpenConfig.durability` +- `OpenConfig.engine` +- `OpenConfig.extensions` +- `OpenConfig.libraryPath` +- `OpenConfig.maxClientSessions` +- `OpenConfig.root` +- `OpenConfig.runtimeDirectory` +- `OpenConfig.runtimeFootprint` +- `OpenConfig.serverExecutable` +- `OpenConfig.serverPort` +- `OpenConfig.serverToolDirectory` +- `OpenConfig.startupGUCs` +- `OpenConfig.temporary` +- `OpenConfig.username` +- `RestoreOptions.artifact` +- `RestoreOptions.brokerExecutable` +- `RestoreOptions.engine` +- `RestoreOptions.libraryPath` +- `RestoreOptions.replaceExisting` +- `RestoreOptions.root` +- `SupportedModesOptions.brokerExecutable` +- `SupportedModesOptions.brokerTransport` +- `SupportedModesOptions.libraryPath` +- `SupportedModesOptions.runtimeDirectory` +- `SupportedModesOptions.serverExecutable` +- `SupportedModesOptions.serverToolDirectory` +- `assertSuccessfulQueryResponse()` +- `createOliphauntClient()` +- `extendedQuery()` +- `parseQueryResponse()` +- `simpleQuery()` +- `supportsBackupFormat()` +- `supportsRestoreFormat()` +- `toUint8Array()` diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md new file mode 100644 index 00000000..ac572e44 --- /dev/null +++ b/docs/maintainers/sdk-parity-policy.md @@ -0,0 +1,299 @@ +# SDK Parity + +`oliphaunt` is a native PostgreSQL product with peer SDK surfaces: + +- Rust: SDK for Tauri and Rust desktop apps; +- Swift: Apple SDK for iOS and macOS apps; +- Kotlin: Android SDK; +- React Native: TypeScript/TurboModule SDK over Swift and Kotlin. +- TypeScript: desktop JavaScript SDK for Node.js, Bun, and Deno. A direct + Tauri JavaScript/webview adapter is planned. +- 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 +of truth for SDK classification, target platforms, runtime ownership, artifact +resolution, and React Native delegation. The prose below explains the contract; +CI parses the registry and product graph directly. This document is maintainer +guidance and review context, not an input to source-text assertions. + +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. 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 +backend-response corpus consumed by Rust, Swift, Kotlin, React Native, +TypeScript, and the WASM wire parser. Additional shared contracts live under +`src/shared/fixtures/sdk-capabilities/`, `src/shared/fixtures/runtime-resources/`, +`src/shared/fixtures/backup/`, and `src/shared/fixtures/lifecycle/`; RN-specific binary transport +fixtures live under `src/shared/fixtures/react-native-jsi/`. + +Mobile crash/reopen/concurrency semantics are tracked separately in +[`Mobile Stability`](/learn/mobile-stability) because they differ by platform +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 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 + +SDK ownership is product ownership, not just source layout: + +- Rust is the Tauri/Rust desktop SDK. Its Cargo crate lives under + `src/sdks/rust`; its public docs live under + `src/docs/content/sdk/rust`. +- Swift owns iOS and macOS runtime behavior. +- Kotlin owns Android runtime behavior. +- React Native owns TypeScript DX and TurboModule transport, while delegating + runtime behavior to Swift on Apple platforms and Kotlin on Android. +- TypeScript owns desktop JavaScript runtime behavior for Node.js, Bun, and + Deno. Tauri apps currently use narrow app-owned Rust commands; a direct + JavaScript/webview adapter is planned. 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 +Swift and Kotlin SDKs so platform bugs, packaging, extension checks, +backup/restore behavior, and lifecycle semantics are fixed once in the platform +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-package `layout`, full `selectedExtensions`, createable `extensions`, +`runtimeFeatures`, `sharedPreloadLibraries`, and mobile static-registry +metadata. The registered SQL-name and native-module-stem domains must exactly +match the selected native subset, and all domains are sorted and duplicate-free. +Swift and Kotlin validate those fields before using generated resources, React +Native inherits the same checks through those platform SDKs, and every +availability check uses `selectedExtensions` so module-only selections are not +lost. + +## 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` | `oliphaunt-tools` Cargo facade selecting split `oliphaunt-tools-*` payload crates for the runtime cache | the PostgreSQL 18 contrib bundle or independently versioned external extension Cargo carriers, selected by exact SQL name | `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 | contrib-bundle or external WASIX/AOT carriers selected by exact SQL name | `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR` | +| TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*`; JSR is protocol/query-only | 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 | checksum-covered release carriers composed over the embedded base snapshot; contrib has one 32-row carrier and each external release has one row | `--carrier`, repeatable `--extension-carrier`, `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` | + +Swift and React Native resolve logical extensions by exact SQL name even when +the physical release owner is the contrib bundle. Carrier product/tag/version +identify the owning release; generated Swift products remain SQL-member +specific. Multi-carrier composition requires one identical native base, +dependency-closed member rows, and non-conflicting checksums. This distinction +allows an independently versioned external extension to publish usable Apple +assets without forcing a Swift SDK release. + +## Parity Bar + +Rust is classified as an SDK, not an internal implementation detail. Its release +contract matters in the same way as Swift, Kotlin, TypeScript, React Native, +and WASM contracts. It owns Rust/Tauri ergonomics, direct/broker/server APIs, +and the broker helper used by TypeScript; it is not the only proof layer for +shared semantics. + +Parity is required where the target platform can support the behavior without +lying about PostgreSQL semantics or degrading developer experience. A feature is +allowed to differ only when the difference is documented with a product reason: + +- impossible or inappropriate for the platform sandbox; +- better expressed through a platform-native API shape; +- intentionally not implemented yet because a fake implementation would be + worse than an explicit unsupported error. + +Unsupported does not mean undefined. Each SDK must expose a clear error for +unsupported modes or backup formats, and the parity matrix must explain why the +gap is acceptable. + +Mode support is part of the public contract, not tribal knowledge. Each SDK +must expose a `supportedModes`-style API that lists `nativeDirect`, +`nativeBroker`, and `nativeServer`, marks whether the current platform adapter +can open each mode, and carries the canonical capability shape plus the product +reason for any unavailable mode. + +## Required Concepts + +| Concept | Rust | Swift | Kotlin | React Native | +| --- | --- | --- | --- | --- | +| Native direct mode | yes | yes | yes | via Swift/Kotlin | +| Native broker mode | yes | future platform adapter | future platform adapter | via Swift/Kotlin | +| Native server mode | yes | future platform adapter | future platform adapter | via Swift/Kotlin | +| Raw protocol API | `exec_protocol_raw` | `execProtocolRaw` | `execProtocolRaw` | `execProtocolRaw` | +| Streaming protocol API | `exec_protocol_raw_stream` | `execProtocolStream` | `execProtocolStream` | `execProtocolStream` over the selected raw transport; New Architecture builds use `jsi-array-buffer` | +| Typed query helpers | yes | yes, simple and parameterized result parser | yes, simple and parameterized result parser | yes, JS simple and parameterized result parser | +| Simple-query SQL validation | simple-query builders reject NUL-containing SQL before frontend frame construction | simple-query builders reject NUL-containing SQL before frontend frame construction | simple-query builders reject NUL-containing SQL before frontend frame construction | simple-query builders reject NUL-containing SQL before frontend frame construction | +| Extended-query input validation | extended-query builders reject NUL-containing SQL and parameter lists above the PostgreSQL protocol `Int16` limit before frontend frame construction | extended-query builders reject NUL-containing SQL and parameter lists above the PostgreSQL protocol `Int16` limit before frontend frame construction | extended-query builders reject NUL-containing SQL and parameter lists above the PostgreSQL protocol `Int16` limit before frontend frame construction | extended-query builders reject NUL-containing SQL and parameter lists above the PostgreSQL protocol `Int16` limit before frontend frame construction | +| Backend UTF-8 parsing | backend C-strings and text accessors reject malformed UTF-8 instead of replacement decoding | backend C-strings and text accessors reject malformed UTF-8 instead of replacement decoding | backend C-strings and text accessors reject malformed UTF-8 instead of replacement decoding | backend C-strings and text accessors reject malformed UTF-8 instead of replacement decoding | +| Backend response validation | typed query parsers accept known simple/extended-query control tags, validate async backend control-message framing and `ReadyForQuery` transaction status, and reject unexpected backend tags instead of ignoring them | typed query parsers accept known simple/extended-query control tags, validate async backend control-message framing and `ReadyForQuery` transaction status, and reject unexpected backend tags instead of ignoring them | typed query parsers accept known simple/extended-query control tags, validate async backend control-message framing and `ReadyForQuery` transaction status, and reject unexpected backend tags instead of ignoring them | typed query parsers accept known simple/extended-query control tags, validate async backend control-message framing and `ReadyForQuery` transaction status, and reject unexpected backend tags instead of ignoring them | +| Transaction helper | `transaction()` returns an explicit pinned handle; `with_transaction(...)` commits or rolls back an async closure; unpinned work is rejected | `transaction {}` uses the actor-owned session for raw and streaming work and rejects database work outside the active transaction handle | `transaction {}` uses the serialized session for raw and streaming work and rejects database work outside the active transaction handle | `transaction(async tx => ...)` preserves the platform session boundary for raw and streaming work and rejects database work outside the active transaction handle | +| Structured PostgreSQL errors | `Error::Postgres(Box)` with SQLSTATE and raw ErrorResponse fields | `OliphauntError.postgres(OliphauntPostgresError)` with SQLSTATE and raw ErrorResponse fields | `PostgresException(PostgresError)` with SQLSTATE and raw ErrorResponse fields | `PostgresError` with SQLSTATE and raw ErrorResponse fields | +| Capability reporting | raw, stream, cancel, backup/restore, simple query, extensions, session model, multi-root support | same C ABI capability bits surfaced as Swift properties, including `multiRoot` | same C ABI capability bits surfaced as Kotlin properties, including `multiRoot` | same capability fields delegated from Swift/Kotlin, including `multiRoot` | +| Backup/restore format discovery | direct/broker: physical archive; server: SQL and physical archive backup; restore: physical archive; capability and handle `supports_backup_format`/`supports_restore_format` helpers | `backupFormats`, `restoreFormats`, and capability/database `supportsBackupFormat`/`supportsRestoreFormat` helpers | `backupFormats`, `restoreFormats`, and capability/database `supportsBackupFormat`/`supportsRestoreFormat` helpers | delegated `backupFormats` and `restoreFormats` capability fields plus TypeScript `supportsBackupFormat`/`supportsRestoreFormat` helpers and matching database methods | +| Backup format enforcement | `EngineExecutor::backup` rejects unsupported formats before the owner queue | `OliphauntDatabase.backup` rejects unsupported formats before the native session call | `OliphauntDatabase.backup` rejects unsupported formats before the platform session call | `OliphauntDatabase.backup` rejects unsupported formats before the TurboModule backup call | +| Checkpoint | `checkpoint()` sends PostgreSQL `CHECKPOINT` through the opened engine and rejects while a session pin is active | `checkpoint()` sends PostgreSQL `CHECKPOINT` through the actor-owned session and rejects while a transaction is active | `checkpoint()` sends PostgreSQL `CHECKPOINT` through the serialized session and rejects while a transaction is active | `checkpoint()` sends PostgreSQL `CHECKPOINT` through the delegated platform session and rejects while a transaction is active | +| Restore format enforcement | `Oliphaunt::restore` rejects non-physical artifacts before target materialization | `OliphauntDatabase.restore` rejects non-physical artifacts before the engine call | `OliphauntDatabase.restore` rejects non-physical artifacts before the platform engine call | `Oliphaunt.restore` rejects non-physical artifacts before the TurboModule restore call | +| Root validation | persistent roots are rejected when empty or NUL-containing before runtime selection; restore targets are rejected before materialization | roots must be file URLs and are rejected when empty or NUL-containing before engine calls | blank or NUL-containing open and restore roots are rejected before platform engine calls | blank or NUL-containing open and restore roots are rejected before TurboModule calls | +| 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 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 | +| Package-size evidence | `NativeRuntimeResources::size_report` and `oliphaunt/package-size.tsv` producer | `OliphauntRuntimeResources.packageSizeReport()` parses the shared TSV | `OliphauntAndroid.packageSizeReport(context)` and `OliphauntAndroid.packageSizeReport(resourceRoot)` parse the shared TSV | `Oliphaunt.packageSizeReport(...)` delegates to Swift/Kotlin and returns the same typed report | +| Packaged native library | host library path today | XCFramework target | Android `jniLibs` | Swift/Kotlin package artifacts | +| Physical backup/restore | yes | yes | yes | via Swift/Kotlin | +| Cancellation | yes | yes | yes | via Swift/Kotlin | +| 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, and Deno, but it is not a +separate mobile runtime layer. Direct Tauri JavaScript/webview integration is +planned; current Tauri apps use the Rust SDK behind app-owned commands. 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. 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 + +`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 metadata and consumer-readiness checks validate the WASIX package and +proof-task contracts. The WASIX Rust product `release-check` owns executable +behavior: 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 | Host-native compilations are development/parity evidence and are not published; JVM runtime is explicitly unavailable; Android common defaults require the `OliphauntAndroid` Context facade; 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 | + +## React Native Ownership + +React Native should not own a separate database runtime. It owns: + +- TypeScript types and ergonomic JS handles; +- TurboModule Codegen; +- versioned JSI ArrayBuffer transport installers for protocol, backup, and restore bytes; +- JS protocol/query helpers, including chunked JSI streaming when the installed + transport provides it and explicit `protocolStream=false` when a custom or + stale transport can only return one owned response; +- a typed `packageSizeReport(...)` facade over the Swift/Kotlin resource + package readers; +- error normalization for JS callers. + +Swift owns Apple runtime behavior for iOS and macOS. Kotlin owns Android runtime +behavior. The React Native native modules are adapters over those SDKs. RN iOS +delegates open, protocol execution, backup, restore, cancellation, and close to +`Oliphaunt`; any future RN macOS target must use the same Swift SDK boundary. +RN Android delegates the same operations to the Kotlin SDK through the +`OliphauntAndroid` facade, not by constructing a private native-direct runtime. + +### React Native Installed-App Harness + +The Expo dev-client example is the installed-app validation harness. The default +combined lane is `moon run oliphaunt-react-native:smoke-mobile`; +platform-specific local lanes are backed by +`src/sdks/react-native/tools/expo-android-runner.sh` and +`src/sdks/react-native/tools/expo-ios-runner.sh`. + +Local Expo MCP validation must run with `EXPO_UNSTABLE_MCP_SERVER=1` so the +example can be driven through the same dev-client app surface that developers +use during iteration. + +## Defensible Deviations + +- React Native keeps TurboModule Codegen for lifecycle/control calls while + requiring a New Architecture JSI ArrayBuffer transport for binary protocol, + backup, and restore traffic. +- Swift and Kotlin use platform-native async/actor/coroutine shapes rather than + copying Rust names exactly. +- Android requires packaged template PGDATA for new roots because mobile apps + cannot rely on executing `initdb` from writable app storage. + +## Release Rule + +An SDK feature is complete only when its SDK-specific tests prove the behavior +and the parity matrix either marks it present or documents a justified platform +deviation. Green Rust tests do not prove Swift, Kotlin, or React Native parity. +Green Swift/Kotlin tests do not prove React Native parity unless the RN adapter +tests demonstrate that calls route through those SDKs rather than through a +private runtime. + +Evidence is deliberately split by ownership: + +- `sdk-contracts:check` verifies the generated public API inventory, SDK + registry schema, C ABI header copies, native boundary model, and tracked + README examples; +- `extension-model:check` verifies exact extension availability, release + identity, platform support, and generated SDK catalogs; +- each SDK's `check`, `test`, `package`, and `release-check` targets own + compilation, behavior, clean-consumer shape, and registry candidate proof; +- installed-app and native lifecycle targets own platform/runtime behavior. + +Stable CI does not grep this document, implementation spellings, or test names. +When a contract needs enforcement, express it in generated data, a parsed +schema/package shape, or a product-owned behavioral test. + +The local cross-SDK contract aggregate is: + +```sh +moon run sdk-contracts:check +``` + +The compatibility shell entry point runs the same lightweight validators: + +```sh +tools/policy/check-sdk-parity.sh +``` + +The full product SDK aggregate is: + +```sh +src/runtimes/liboliphaunt/native/tools/check-track.sh sdks +``` diff --git a/docs/maintainers/sdk-products-policy.md b/docs/maintainers/sdk-products-policy.md new file mode 100644 index 00000000..2fcd175f --- /dev/null +++ b/docs/maintainers/sdk-products-policy.md @@ -0,0 +1,132 @@ +# SDK Products + +SDK source lives under `src/` with the product it releases. This document is +the cross-SDK policy and parity contract. + +These are product SDKs, not auxiliary bindings. Rust, Swift, Kotlin, React +Native, and TypeScript should expose the same product concepts where the target +platform can do so honestly: + +- Rust is the SDK for Tauri and Rust desktop apps. +- Swift is the SDK for iOS and macOS apps. +- Kotlin is the SDK for Android apps. Only the Android AAR, Gradle plugin and + marker, and declared Android ABI carriers are public release surfaces. +- React Native is the TypeScript/TurboModule SDK over the Swift and Kotlin SDKs. +- TypeScript is the SDK for Node.js, Bun, and Deno. A direct Tauri + JavaScript/webview adapter is planned. + +`tools/policy/sdk-manifest.toml` is the repo-level SDK registry. The canonical +product graph lives in `src/*/moon.yml`; `sdk-contracts:check` parses both and +rejects ownership or package-identity drift. Product tests and package checks, +not source-text assertions, prove runtime delegation and consumer behavior. + +- `src/sdks/rust/`: canonical Rust SDK for Tauri and Rust desktop apps. +- `src/sdks/swift/`: Swift package with an actor-first `Oliphaunt` API and a + native-direct C ABI engine over `liboliphaunt`; `.nativeDirect` uses that engine + by default and can materialize packaged runtime/template resources for iOS and + macOS apps. +- `src/sdks/kotlin/`: Kotlin Multiplatform source/build project with a + suspend-first common API, host-native conformance targets, and the Android + native-direct JNI engine. The host-native compilations are development and + parity evidence only; Maven publication is deliberately limited to the + Android consumer surface. +- `src/sdks/react-native/`: React Native New Architecture package. Its product contract + is a typed TypeScript/TurboModule layer over the Swift and Kotlin SDKs, with + no independent database semantics. +- `src/sdks/js/`: desktop JavaScript SDK for Node.js, Bun, and Deno. + Tauri apps currently expose narrow app-owned commands from the Rust SDK; a + direct JavaScript/webview adapter is planned. `nativeDirect` is the default across supported JavaScript + runtimes; Node.js uses the package-owned prebuilt Node direct adapter, and Bun + and Deno use their runtime FFI surfaces. TypeScript broker mode consumes the + published `oliphaunt-broker` runtime and the shared `PGOB` protocol + instead of inventing another broker runtime; app developers get verified + release assets by default instead of building Rust locally. The npm package + is the native-runtime distribution for Node, Bun, and Deno; the JSR package + intentionally exposes protocol/query helpers only and must not advertise a + native runtime. + +The Rust SDK is canonical for now; Swift, Kotlin, React Native, and TypeScript +mirror its mode, raw protocol, typed query, transaction, checkpoint, structured PostgreSQL error, capabilities, backup, restore, exact extension, and resource packaging terminology unless a platform restriction is documented. +React Native must not duplicate database runtime behavior: iOS calls flow +through `Oliphaunt`, and Android calls flow through the `oliphaunt` +`OliphauntAndroid` facade. +Every SDK-facing feature must either be implemented with equivalent semantics or +fail with an explicit unsupported error that is justified in +[`sdk-parity-policy.md`](sdk-parity-policy.md). Silent drift between SDKs is a +release blocker. + +Validation is package-native: + +```sh +moon run oliphaunt-rust:check +moon run oliphaunt-swift:check +moon run oliphaunt-kotlin:check +moon run oliphaunt-react-native:check +moon run oliphaunt-js:check +moon run sdk-contracts:check +moon run extension-model:check +``` + +The Kotlin and React Native Android validation scripts opt into Gradle +configuration cache by default. Set `OLIPHAUNT_GRADLE_CONFIGURATION_CACHE=0` +when debugging Gradle task configuration itself. + +When a local `target/liboliphaunt-pg18` build exists, the Swift and Kotlin lanes +automatically run their native-direct C ABI tests against that library and +runtime tree. + +Build app-bundle resources from the Rust/native track with: + +```sh +cargo run -p oliphaunt --bin oliphaunt-resources -- \ + --output target/oliphaunt-resources \ + --extension vector \ + --force +``` + +Extension selection is exact-name only. SDKs accept exact PostgreSQL extension +names; `vector` means only the SQL extension `vector`, and names like `core`, +`search`, or `geo` must not resolve to hidden extension sets. + +The generated `target/oliphaunt-resources/oliphaunt` directory is the resource +root consumed by Swift bundles, Android assets, and React Native apps. Android +Gradle builds also accept the parent directory through +`-PoliphauntRuntimeResourcesDir=target/oliphaunt-resources`. + +For iOS and Android release artifacts, build runtime resources with +`--require-mobile-static-registry` once the selected extension modules have +platform static registry rows. Swift, Kotlin, and React Native reject requested +extensions whose packaged runtime advertises pending mobile registry work. +The platform resource build must also pass each linked registry module stem with +`--mobile-static-module `; the Rust runtime-resource CLI rejects stems +that are not selected by the runtime resources. Those stems are declarations for +validation; mobile-ready output includes +`oliphaunt/static-registry/oliphaunt_static_registry.c`, which exports +`liboliphaunt_selected_static_extensions`. Platform bridges discover that symbol +and register the returned rows through `oliphaunt_register_static_extensions` +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`, +the full dependency-closed `selectedExtensions` domain, its exact +`creates-extension=true` subset in `extensions`, `runtimeFeatures`, and +`sharedPreloadLibraries`. Mobile manifests additionally bind the exact native +SQL-name domain in `mobileStaticRegistryRegistered` and its exact module stems +in `nativeModuleStems`; the static-registry manifest must agree. All domains +are sorted and duplicate-free. SDK resource-availability checks use +`selectedExtensions`, including for selected module-only extensions, 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. +The resource root also carries `package-size.tsv`. Swift exposes it through +`OliphauntRuntimeResources.packageSizeReport()`, Kotlin Android exposes it through +`OliphauntAndroid.packageSizeReport(context)` or +`OliphauntAndroid.packageSizeReport(resourceRoot)`, and React Native exposes the +same typed report through `Oliphaunt.packageSizeReport(...)` while still delegating +the actual resource lookup to Swift/Kotlin. + +Android packages the native C ABI library separately from runtime resources. +Pass a `jniLibs`-style directory with ABI subdirectories through +`-PoliphauntAndroidJniLibsDir=/path/to/jniLibs`; each packaged ABI must include +`liboliphaunt.so`. diff --git a/docs/maintainers/testing.md b/docs/maintainers/testing.md new file mode 100644 index 00000000..199a8ef8 --- /dev/null +++ b/docs/maintainers/testing.md @@ -0,0 +1,397 @@ +# Testing Policy + +Status: normative testing policy. Last verified: 2026-07-15. Owner: repository maintainers. + +Oliphaunt is a polyglot product repo. Product-native tests stay in product-native test roots. +Each SDK is validated with the same tools its consumers use: + +- Rust SDK: `src/sdks/rust/tests/` +- WASM crate: `src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/` +- Swift SDK: `src/sdks/swift/Tests/` +- Kotlin SDK: `src/sdks/kotlin/oliphaunt/src/commonTest/`, + `src/sdks/kotlin/oliphaunt/src/androidUnitTest/`, and + `src/sdks/kotlin/oliphaunt/src/nativeTest/` +- React Native package: `src/sdks/react-native/src/__tests__/` +- Installed React Native app smoke and benchmark coverage: + `src/sdks/react-native/examples/expo/` + +Use the tier model below when deciding whether a check belongs in PR fast +feedback, affected integration, an explicit full manual run, release dry-run, or post-publish +validation. + +- PR: Moon-affected `check` and `test` tasks, release intent, and the selected + package, artifact, and E2E jobs. Measured `coverage` is an explicit + local/manual lane; it is not part of the `Required` PR gate. +- Main: the PR gate plus selected runtime smokes and regressions for changed + products; a successful non-cancelled run emits the exact-SHA `Qualified` + release record. +- Full manual: full regressions, extension matrix, installed mobile app + smokes, lifecycle drills, and measured benchmark reports. +- Release: package-native dry-runs, artifact manifests, checksums, + attestations, registry checks, exact-extension evidence, binary + compatibility-floor inspection, and selected regression/performance gates. + +Target-scoped consumer diagnostics do not inherit an implicit success barrier +from a multi-platform producer matrix. Mobile extension packaging and Android +app/E2E rows, the per-target JavaScript candidate consumers, the Linux native +lifecycle/Rust candidate consumers, and the Linux WASIX regression continue +after an unrelated producer row fails, provided the plan and every shared +prerequisite succeeded and the run was not cancelled. Each consumer still +downloads its exact same-run artifact and fails when that target's input is +absent. The JavaScript matrix waits directly for the desktop producer matrix, +not the all-platform native aggregate. Its portable ICU candidate is packaged +once by the macOS desktop row into a separate exact same-run artifact, so a +failed Windows, Android, or iOS row cannot skip otherwise usable Linux/macOS +consumer diagnostics. The all-platform native aggregate remains a separately +selected mandatory build and still validates the complete release-asset set. +`Builds`, `E2E`, `Required`, and `Qualified` retain every producer and consumer +result and therefore remain fail-closed for release evidence. + +Linux producer lanes prove compatibility twice. The format-independent ELF +inspector rejects any `GLIBC` requirement above 2.38 or `GLIBCXX` requirement +above 3.4.30, including objects inside static archives. The packaged dynamic +trees then run through `tools/release/check-linux-consumer-baseline.sh` in an +immutable Fedora 39/glibc 2.38 container with no network, writable root, or +Linux capabilities. The fixture is an ABI test appliance, not a supported-OS +or security-lifecycle assertion. The broker is additionally built and started +in its pinned older linker baseline so the rehearsal cannot merely document a +runner-induced floor regression. + +### Candidate Cargo example locks + +Cargo examples are registry-neutral source fixtures. Their manifests use +ordinary crates.io dependencies with exact Oliphaunt versions; they do not +commit `registry = "oliphaunt-local"`, a candidate `[patch.crates-io]` table, or +a nested `Cargo.lock`. Exact candidate archives are outputs of cross-platform +CI, so a source commit cannot also contain their final checksums without +creating a build-commit-build cycle. + +The static source check rejects registry coupling, stale product versions, +committed nested locks, and missing ignore rules: + +```sh +tools/dev/bun.sh tools/release/example-cargo-policy.mjs --check +``` + +Release qualification first assembles an exact local registry from the +qualified artifacts. It then copies each example to `target/`, injects exact +`[patch.crates-io]` entries for candidate packages, generates a lock, validates +candidate versions and checksums against both the registry index and `.crate` +bytes, enforces the canonical stable Wasmer/WASIX set and advisory floors, and +runs a full `cargo fetch --locked`. The generated evidence records each lock +digest. Run the same proof locally with: + +```sh +tools/dev/bun.sh tools/release/validate-example-cargo-candidates.mjs \ + --index target/release-work/candidate-registries/cargo/index +``` + +For a later partial release, only packages present in that candidate registry +are patched; unchanged published packages and all third-party dependencies +resolve from crates.io. Candidate manifests and locks must never be copied back +into source. + +Cross-product behavior belongs in `docs/maintainers/sdk-parity-policy.md` and executable parity +checks. Do not centralize platform tests into a fake shared test harness when a +native package manager, simulator, Gradle target, SwiftPM target, Cargo target, +or React Native Codegen path is the actual consumer contract. + +## Fixtures + +Product-private fixtures stay inside the product test root that consumes them. +Create a shared fixture root only after the same contract is consumed by at +least two products without platform-specific setup. Until then, colocated +fixtures are clearer and cheaper to maintain. + +Shared fixture domains are small, semantic contracts consumed by +product-native tests or policy checks: + +- `src/shared/fixtures/protocol/query-response-cases.json`: PostgreSQL backend-response + corpus consumed by Rust, Swift, Kotlin, React Native, TypeScript, and WASM + protocol tests. +- `src/shared/fixtures/sdk-capabilities/mode-support.json`: direct, broker, and server + capability expectations used to keep mode support assertions aligned. +- `src/shared/fixtures/runtime-resources/manifest.properties`, + `src/shared/fixtures/runtime-resources/template-pgdata-manifest.properties`, and + `src/shared/fixtures/runtime-resources/package-size.tsv`: runtime-resource and + exact-extension package-size contracts used by Rust, Swift, Kotlin, + TypeScript, and React Native packaging/resource tests. +- `src/shared/fixtures/backup/physical-archive-manifest.json`: physical archive metadata + expectations for backup/restore contract tests. +- `src/shared/fixtures/lifecycle/session-lifecycle.json`: close, cancel, + background/foreground, and transaction-pinning lifecycle expectations. +- `src/shared/fixtures/react-native-jsi/binary-transport.json`: RN-only JSI ArrayBuffer, + typed-array offset, stream chunk, callback, and handle-validation cases. + +Reusable benchmark datasets, benchmark plans, and published reports belong in +`benchmarks/`. Executable benchmark harnesses belong in `tools/perf/` unless +the harness is intentionally part of a product's public developer API. + +## Moon Tasks + +Moon task names are intentionally narrow: + +- `check`: static checks, typecheck, codegen, lint, or build-only validation. +- `test`: real unit or contract tests in the product-native runner. +- `package`: package-shape checks and publish dry-runs. +- `smoke`: one runtime happy path for that product. +- `regression`: broader SQL, protocol, extension, lifecycle, or runtime + regression suites. +- `bench`: benchmark plan/report validation only. +- `bench-run`: measured benchmark execution. +- `coverage`: runs product-native measured line coverage and writes + machine-readable reports under `target/coverage//`. + +`check` and `test` must not call the same command for SDK products. `test` +must run tests, not metadata-only checks. `smoke` targets must be explicit +runtime probes and must be run with `--cache off` in CI/release evidence lanes +where current device/simulator/runtime state matters. + +Runtime prerequisites are centralized in `tools/runtime/preflight.sh`. Rust, +Swift, Kotlin, TypeScript, and WASM smoke/regression lanes use that helper for +host liboliphaunt, Android liboliphaunt, iOS simulator probe, and WASIX +asset/AOT checks. Static, package, unit, and coverage lanes remain +artifact-light; they may warn about missing local runtimes but must not claim +runtime evidence. React Native installed-app smokes delegate runtime +materialization to the Expo platform scripts and hard-fail there if native +artifacts cannot be built or located. + +React Native installed-app smoke is split by platform: + +```sh +moon run oliphaunt-react-native:smoke-android +moon run oliphaunt-react-native:smoke-ios +moon run oliphaunt-react-native:smoke-mobile +``` + +PR jobs run RN static, unit, Codegen, JSI, config-plugin, and package checks. +Affected PR, main, and explicit manual lanes run the installed Android/iOS app +smokes selected by the CI plan. + +Installed-app E2E runner choice is closed, not a recurring research task. +Decision (2026-06-08): Oliphaunt uses the pinned open-source Maestro CLI +through GitHub-hosted emulator/simulator jobs. This is not an open research loop. +Reopen that decision only when a written implementation proposal names an +installed-app E2E requirement that the pinned open-source Maestro CLI cannot +satisfy. Do not keep re-checking Maestro, Detox, Appium, EAS, Firebase Test +Lab, BrowserStack, Sauce, AWS Device Farm, or other hosted-device services while +implementing this plan. Routine maintenance verifies the pinned installer, flow +files, app artifacts, runner behavior, and CI logs for the selected Maestro +lanes; it does not revisit provider selection. + +`tools/dev/setup-maestro.sh` installs only the exact versioned release asset and +SHA-256 recorded in `src/sources/toolchains/maestro.toml`; that manifest is the +single release pin. It does not execute the vendor's network installer. Version +upgrades change the reviewed manifest metadata and must keep the staged +archive/layout/version and atomic-promotion regression tests green; incomplete +or inconsistent metadata fails before any download. + +The Node direct addon likewise treats `src/sources/toolchains/node.toml` as the +single source for fallback header and Windows import-library release metadata. +`build-node-addon.sh` continues to prefer an explicit or installed local header +or `node.lib` candidate. Only a missing candidate activates the fallback, which +then requires the manifest's exact Node runtime, HTTPS-only bounded transfer, +SHA-256 verification, safe staged header extraction, and atomic cache promotion. +The fault suite covers invalid metadata, corrupt caches, unsafe or truncated +archives, transport interruption, checksum failure, and promotion rollback. +Node upgrades update that manifest's reviewed digests together with +`.prototools` and each CI `NODE_VERSION`; source-toolchain policy rejects any +runtime/manifest drift before a release build. + +Prior provider research is historical context, not a standing checklist. Maestro +pin upgrades are dependency maintenance; they do not reopen the runner decision +unless they expose a concrete installed-app E2E requirement this path cannot +meet. + +The default installed-app path must remain free and public-checkout +reproducible. Paid hosted-device providers, SaaS-only runners, and required +private runner infrastructure are not part of the default proof path. When +mobile E2E breaks, inspect the selected implementation first: app artifact shape, +simulator/emulator setup, Maestro flow files, logs, and CI runner assumptions. +Debug the chosen implementation first. Do not restart provider research unless +the failure proves a concrete requirement this model cannot satisfy. + +## Coverage + +Coverage is measured evidence, not a policy-only check. Product tasks run the +native reporter for their ecosystem: `cargo-llvm-cov` for Rust and WASM library +coverage, `swift test --enable-code-coverage` for Swift, Kover for Kotlin, and +Vitest V8 coverage for TypeScript and React Native TypeScript code. Each product writes +`target/coverage//summary.json` plus its native report formats, and +`moon run repo:coverage` aggregates those summaries into `target/coverage/summary.json` +and `target/coverage/summary.md`. + +Rust and WASM executable unit tests run through `cargo nextest` with the `ci` +profile. Unit lanes still run doctests through `cargo test --doc` because +nextest does not own doctest execution. Coverage lanes measure line coverage +through `cargo llvm-cov nextest` and then run `cargo test --doc` as stable-Rust +correctness evidence. Doctest coverage itself requires nightly rustdoc flags, so +it is not part of the default stable LCOV gate. +WASM library unit coverage intentionally uses `--no-default-features`, while +WASM doctests run with default features because the README extension examples +exercise the default extension surface. Runtime Postgres/WASIX execution stays +in `smoke` and `regression`, where missing runtime assets must fail or skip +explicitly according to the lane policy. + +TypeScript and React Native unit tests use the shared Vitest discovery runner +in `tools/test/run-js-tests.mjs`. Coverage calls the same runner with Vitest V8 +coverage enabled, so test discovery and coverage discovery cannot drift. React +Native native adapter compile checks, Codegen checks, Expo prebuild/app wiring, +and installed-device smokes remain separate package or runtime lanes; Vitest +coverage is only evidence for TypeScript API/config/JSI contract code. + +`coverage/baseline.toml` records product-owned `source_globs`, precise +`exclude_globs`, explicit waivers, the aggregate gate, and an initial per-file +floor. Every owned source file must be measured or waived with a reason and +replacement evidence; every waiver also carries an owner and expiry/review +horizon. Generated code, vendored code, PostgreSQL sources, native build +outputs, package `lib/` output, Gradle build directories, Xcode DerivedData, +and Codegen output are excluded from SDK wrapper coverage gates. +`measured_line_coverage` is an audit snapshot, not an exact equality gate. The +initial aggregate floor is 80 percent for SDK wrapper code, with a two-point +per-release ratchet until each SDK wrapper reaches 85 percent line coverage. +Use `moon run repo:coverage-policy` when you only need to validate the +coverage policy shape. + +The root coverage commands are: + +```sh +moon run :coverage +moon run :coverage --affected +``` + +## WASM Runtime Tests + +`oliphaunt-wasix` is intended for tests that need real Postgres semantics without +Docker. + +Use `Oliphaunt::temporary()` when the code under test can call the direct Rust +API: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +#[test] +fn stores_rows() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + + db.exec("CREATE TABLE items (id int primary key, name text)", None)?; + db.exec("INSERT INTO items VALUES (1, 'alpha')", None)?; + + let rows = db.query("SELECT name FROM items WHERE id = 1", &[], None)?; + assert_eq!(rows.rows[0].get("name").unwrap(), "alpha"); + + db.close()?; + Ok(()) +} +``` + +Use `fresh_temporary()` only when the test must validate fresh-cluster +initialization behavior: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +#[test] +fn fresh_cluster_path() -> Result<(), Box> { + let mut db = Oliphaunt::builder().fresh_temporary().open()?; + db.close()?; + Ok(()) +} +``` + +## Server Tests + +Use `OliphauntServer` when the application already talks to Postgres through a +client library: + +```rust,no_run +use oliphaunt_wasix::OliphauntServer; +use sqlx::{Connection, Row}; + +#[tokio::test] +async fn sqlx_query() -> Result<(), Box> { + let server = OliphauntServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + let row = sqlx::query("SELECT $1::int4 + 1 AS n") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("n")?, 42); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} +``` + +Keep client pools at one connection. + +## Extension Tests + +Enable bundled extensions through the builder: + +```rust,no_run +use oliphaunt_wasix::{Oliphaunt, extensions}; + +#[test] +fn vector_query() -> Result<(), Box> { + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + + db.exec("CREATE TABLE items (embedding vector(3))", None)?; + db.exec("INSERT INTO items VALUES ('[1,2,3]')", None)?; + db.exec("SELECT embedding <-> '[1,2,4]' FROM items", None)?; + + db.close()?; + Ok(()) +} +``` + +When an extension has bundled dependencies, prefer the builder path over +post-open `enable_extension(...)`. + +## Snapshot Setup + +Use physical data-dir archives or `try_clone()` when a test suite needs a +pre-populated same-version fixture: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +#[test] +fn clone_fixture() -> Result<(), Box> { + let mut seed = Oliphaunt::temporary()?; + seed.exec("CREATE TABLE items(value TEXT)", None)?; + seed.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let mut clone = seed.try_clone()?; + clone.exec("SELECT * FROM items", None)?; + + clone.close()?; + seed.close()?; + Ok(()) +} +``` + +Use logical dumps, not physical archives, when you need a portable export. + +## Cross-Language Clients + +Use `oliphaunt-wasix-proxy` when the test process lives outside Rust: + +```sh +oliphaunt-wasix-proxy --temporary --tcp 127.0.0.1:0 --print-uri +``` + +Pass the printed URI to Python `psycopg`, Go `pgx`, Node `pg`, or another +standard Postgres client. + +## COPY And Raw Protocol Tests + +Direct `Oliphaunt` supports `/dev/blob` for `COPY TO` and `COPY FROM`. Server +mode supports ordinary client-driven `COPY FROM STDIN` and other standard wire +protocol flows through the local Postgres endpoint. diff --git a/docs/maintainers/tooling.md b/docs/maintainers/tooling.md new file mode 100644 index 00000000..5ee39721 --- /dev/null +++ b/docs/maintainers/tooling.md @@ -0,0 +1,351 @@ +# Tooling Decisions + +Status: normative tooling decision record. Last verified: 2026-07-16. Owner: repository maintainers. + +Oliphaunt is a polyglot product monorepo. Tooling has to make product work +predictable without hiding ecosystem-native behavior. + +## Roles + +- Moon is the product/task graph, affectedness engine, local cache, and CI task + executor. +- Release Please manifest mode owns release PRs, versions, and changelogs. +- The protected release workflow owns exact-SHA product tags and draft GitHub + releases. +- Product-local `release.toml` files own package metadata release-please does + not model: owner, kind, publish targets, registry packages, release + artifacts, compatibility-version files, and derived version files. +- Runtime products select published target presets in Moon + `project.release.artifactTargets`; exact extension products own explicit + support and evidence in `targets/artifacts.toml`. +- Product-native build tools own product behavior: Cargo, SwiftPM/Xcode, + Gradle, npm/JSR, Expo, React Native Codegen, and PostgreSQL build scripts. +- Bun helpers under `tools/release/*.mjs` and `.github/scripts/*.mjs` own the + public and protected release check, dry-run, publish, tag, and draft-release + command surface. + +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. + +## Moon + +Install Moon through proto from `.prototools` and run `moon` directly: + +```sh +moon query projects +moon query tasks +moon query affected --upstream none --downstream deep +moon run :check +moon run :test +moon run :coverage +``` + +Moon task names carry stable intent: + +- `check`: static, typecheck, lint, codegen, or build-only validation. +- `test`: product-native unit or contract tests. +- `package`: package-shape checks and publish dry-runs. +- `smoke`: one runtime happy path. +- `regression`: broader SQL, protocol, extension, lifecycle, or runtime + regressions. +- `bench`: benchmark plan/report validation. +- `bench-run`: measured benchmark execution. +- `coverage`: measured product-native line coverage. + +Every task must declare explicit inputs. Tasks with deterministic output that +other tasks consume must declare outputs. Use Moon tags for CI lanes and ad-hoc +selection; do not create root script aliases for new lanes. + +Moon dependency scopes are meaningful: + +- `production` and `peer` are release-affecting compatibility edges. +- `build` is for tests, fixtures, generated metadata, package-shape checks, and + other non-release coupling. + +## pnpm + +pnpm is not the global build orchestrator. Its repo-level role is: + +- install JavaScript-family workspace dependencies from `pnpm-lock.yaml`; +- provide JavaScript package-manager commands for docs, TypeScript, and React + Native packages. + +The root `package.json` intentionally has no scripts. Run the corresponding +Moon target or the product-native package command; a second alias layer makes +affectedness, cache behavior, and ownership harder to inspect. + +Cargo, Gradle, SwiftPM, Xcode, npm/JSR publish, Expo, and PostgreSQL build +scripts stay product-owned and are invoked through Moon tasks where repository +or CI orchestration is needed. `node_modules/` directories are normal ignored +local install state; they must never be tracked. + +## Scripts + +Use shell for setup, process orchestration, platform packaging glue, and thin +CI wrappers. Policy code that parses repository files and asserts invariants +should live under `tools/policy/assertions/assert-*.mjs` and run with Bun. Keep +`check-*` scripts as Moon/CI entrypoints when they aggregate checks or wrap +ecosystem-native tools. + +## Bootstrap toolchain sources + +The manifests in `src/sources/toolchains/` are the source of truth for +downloaded maintainer toolchains. `bun.toml` and `deno.toml` pin both the +official archive SHA-256 and the extracted executable SHA-256 for every +supported macOS, Linux, and Windows host target. `android-sdk.toml` pins the +Linux and macOS command-line-tools archives and the exact SDK package +identities used by builds. Version inputs in composite actions are compatibility +guards: they must agree with the manifest; they do not select arbitrary bytes. + +CI does not delegate Node, Moon, or pnpm acquisition to `actions/setup-node`, +Corepack, or `moonrepo/setup-toolchain`. `node-runtime.toml` pins the official +Node archive and extracted runtime binary for every supported host. +`moon-cli.toml` and `pnpm.toml` pin their archives, component hashes, executable +modes, and extracted-tree identities. `moon-plugins.toml` pins both each OCI +manifest digest and the manifest-bound WASM blob. `npm-publisher.toml` applies +the same archive and complete-tree contract to the npm CLI used by publication +and exact-candidate consumer checks. The proto version in Moon configuration is +a compatibility contract only; CI does not hydrate tools through proto. + +Bootstrap downloads are HTTPS-only, bounded, checksum-verified, validated for +archive layout and entry type, extracted into private staging directories, and +promoted by a same-filesystem rename. A valid local executable is preserved. A +corrupt or wrong-version cache is repaired, and an interrupted replacement +restores the prior directory. Do not add `continue-on-error` setup-action or +unchecked `curl | unzip` fallbacks. + +GitHub cache entries are acceleration only. Every restored binary, wrapper, +plugin, component, mode inventory, and complete package tree is revalidated +before its path is exported. Moon plugins are copied into a fresh private +`MOON_HOME`, and `MOON_TOOLCHAIN_FORCE_GLOBALS=true` prevents Moon from +silently hydrating another runtime. On Windows, composite actions convert +Git-Bash paths back to native paths before writing `GITHUB_PATH`; pnpm store +creation converts the native path to POSIX only for the Bash filesystem call. + +Android command-line-tools are byte-pinned. Packages installed through +`sdkmanager` are not immutable repository blobs: the bootstrap requests the +exact NDK, CMake, build-tools, and platform package identities, then validates +their installed `source.properties` and build-critical executables/resources. +`platform-tools` is an intentionally +unversioned moving Android repository package and is validated by the presence +of an executable `adb`; do not describe it as byte-reproducible. + +The Android setup action enables the Gradle cache only for Gradle/Expo +consumers. Native-only Android artifact jobs pass `gradle-cache: "false"`, so +`actions/setup-java` does not register an irrelevant Gradle post-cache step. +When native ccache is enabled, its directory is created before cache restore so +an empty first run does not emit a cache path-validation warning. + +The Linux Kotlin/Native test lane has a dedicated setup-java Gradle cache +identity selected by +`src/sdks/kotlin/gradle/cache-scopes/linux-native-tests.txt`. The shared +dependency inputs are not sufficient for this lane: another Gradle consumer +can populate the same immutable setup-java key without the host-native test +variants, and a successful job is required before setup-java can save a more +complete entry. Change the marker only when intentionally invalidating this +lane's cache, and qualify that change with a cold `linuxX64Test` run. + +Kotlin plugin and dependency resolution try Google Cloud's fixed, hosted Maven +Central mirror before canonical Maven Central. The mirror is an availability +path for valid Central coordinates when a shared GitHub-hosted runner IP is +temporarily refused; canonical Central remains the missing-module fallback. +Do not add retries for an HTTP 403, a mutable repository override, or another +uncontrolled repository. A mirror or cache change must be proven from a fresh +Gradle home, and mirror payloads used as evidence must match canonical Central +by checksum. Gradle dependency verification should be introduced only as one +complete, reviewed rollout covering every supported host and configuration; +an incomplete host-generated metadata file is not a release safeguard. + +## CI + +GitHub Actions owns runners, credentials, artifact upload, and platform matrix +fan-out. Moon owns which tasks are affected and how tasks depend on each other. +Every GitHub-hosted runner uses an explicit OS-version label. Mutable +`ubuntu-latest`, `macos-latest`, and `windows-latest` aliases are forbidden by +workflow policy, including suffixed variants. A runner-image upgrade is an +intentional dependency change: inspect the image/toolchain delta, rerun the +platform binary compatibility contract, and qualify every affected release +target before changing the pin. +The repository build toolchain is exact Rust 1.93.1 in +`rust-toolchain.toml`; crate `rust-version = "1.93"` fields are the distinct +consumer MSRV contract and must not be used as a mutable CI toolchain selector. +Linux broker packaging also pins the official Rust 1.93.1 Bookworm OCI index +by digest because the final glibc symbol bindings are a linker-input contract, +not a property guaranteed by `rust-toolchain.toml`. Its sealed build is +networkless and read-only. The separate digest-pinned Fedora 39 image is used +only to rehearse the glibc 2.38 ABI after verifying the container's observed +glibc version; Fedora 39's end-of-life security status is not a production OS +support claim. +Windows jobs configure the runner-owned Visual Studio developer shell through +`.github/scripts/setup-msvc.ps1`; they do not depend on a third-party Node +action for compiler environment discovery. The setup accepts only Visual Studio +installation major 18, selects x64 tools from `HostX64/x64`, and verifies the +VC145 redistributable closure. It records the observed Visual Studio, VC tools, +Windows SDK, compiler/linker, and runner-image versions without patch-pinning a +runner-owned toolchain that GitHub may service in place. Apple jobs select the +exact `/Applications/Xcode_26.5.app/Contents/Developer` bundle and require the +observed Xcode minor to remain 26.5. The Xcode build identifier, Apple SDK +versions, and runner-image version are evidence rather than patch pins. macOS +setup removes the unused runner-provided `aws/tap` before formula lookup instead +of disabling Homebrew's tap-trust enforcement. + +CI flow: + +1. The affected job uses Moon queries to select stable job names from task tags + named `ci-` and to emit the exact Moon task targets for each job. +2. The affected job emits dynamic `Checks / ` and `Tests / ` + matrices plus one compact `Policy` task batch from Moon-selected targets. + `Checks` are normal static/lint/typecheck-style package or tool checks. + Policy targets are invariant assertions that parse repository files, + workflow YAML, release metadata, generated graphs, or package topology. + Package checks and tests keep task inheritance; the policy batch runs its + selected targets with `--upstream none` so it does not re-run package + prerequisites that already have their own visible jobs. A task that truly + needs the Android SDK declares the `ci-android-sdk` Moon tag. The planner + projects that capability into runner setup, so unrelated check, policy, and + test shards do not install Android tooling. +3. Product build jobs call `.github/scripts/run-planned-moon-job.sh `. +4. The planned-job wrapper reads the affected job target map, then delegates to + `.github/scripts/run-moon-targets.sh`, which runs + `moon run` with the selected targets. This is for planned artifact targets + whose producer jobs may be selected by release-product implications rather + than by direct file affectedness. Jobs that consume downloaded artifacts pass + `OLIPHAUNT_MOON_UPSTREAM=none`; other build jobs keep Moon upstream task + inheritance enabled. SDK `package-artifacts` tasks depend on the product + `package` task and consume its package-shape outputs instead of rerunning + package assertions inside the artifact staging script. +5. GitHub matrix fans out only target dimensions such as OS, CPU, ABI, native + runtime target, broker target, Node direct target, WASIX AOT target, Android + emulator, and iOS simulator. + +The required PR gate is thin: visible `Checks / `, `Policy`, +`Tests / `, `Builds / `, and installed-app +`E2E` jobs all fan out from the affected plan, while Moon models package-local +prerequisites. The final `Required` job aggregates the `Checks`, `Tests`, +`Builds`, and `E2E` phase gates plus `release-intent`; the selected `Policy` +batch is included in the `Checks` gate. Mobile installed-app `E2E` +consumes built app artifacts from the same CI run and does not rebuild runtimes, +SDKs, or extension packages. + +Mobile CI target fan-out is derived from published +`liboliphaunt-native` artifact rows generated from the product's Moon +`artifactTargets` declaration and the release target preset. Android jobs use +rows whose surfaces include `react-native-android`; iOS jobs use rows whose +surfaces include `react-native-ios`. Do not hardcode mobile ABI target lists in +CI planners. + +Keep workflow names and job names product-oriented. Put implementation details +in step names. + +## Moon Cache Policy + +Moon is allowed to cache task results when inputs, dependency task outputs, +toolchain-sensitive files, environment variables, and outputs represent the +work. It does not know about simulator/device state, installed apps, local +ports, Docker daemon state, code-signing identities, registry state, or copied +runtime artifacts unless those are modeled as inputs. + +Cache deterministic static checks, package-shape checks, generated freshness, +docs builds, unit tests, and coverage reports when they declare inputs and +outputs. + +Use `cache: local` for developer smoke tasks that are useful to replay when +local source inputs have not changed. + +Force live execution for CI/mobile/device proof with `MOON_CACHE=off`; those +lanes prove the current runner, simulator/device, signing environment, app +artifact, and runtime artifact. + +Cache benchmark plan checks, never measured benchmark runs. `bench` validates +matrix and report shape; `bench-run` measures current hardware and runtime +state. + +Use `runInCI: skip` for expensive dependency-only tasks that must stay valid in +CI action graphs but must not run as broad CI work. Use `runInCI: false` only +for tasks CI must never invoke. + +## Release Tooling + +Release Please owns the generated release PR, product-version bumps, and +changelogs without forcing non-JavaScript products into fake `package.json` +files. It supplies the reviewed component/version state used to derive tag +names, but it does not create tags or GitHub releases. + +What release-please does not own: + +- platform binary builds; +- extension artifact builds; +- checksums and attestations; +- registry credential checks; +- package-native publish commands; +- verifying already-published GitHub release assets; +- exact-SHA product tags and draft GitHub releases. + +Those stay behind the Bun release entrypoints, the protected workflow, and +product-native release tasks. + +Do not reintroduce release-plz, git-cliff product changelog ownership, a central +release graph, or broad clean-registry reinstall gates as routine CI policy. + +## Debugging + +Use Moon's graph and cache diagnostics before adding scripts: + +```sh +moon project-graph +moon action-graph oliphaunt-react-native:package-artifacts +moon hash +moon run --cache off --log trace +``` + +If a task is slow, first check whether its inputs are too broad, outputs are +missing, dependency scopes are wrong, or CI is proving runner state that cannot +be safely cached. + +Graph policy fixtures are split by contract: + +- `tools/graph/synthetic/affected.toml` checks Moon owner/downstream behavior. +- `tools/graph/synthetic/release.toml` checks release product selection. +- `tools/graph/synthetic/coverage.toml` checks coverage routing. + +Do not add mixed synthetic cases that assert unrelated contracts in one table. + +### Advisory reference audits + +Low textual reference counts are cleanup leads, not correctness failures. Agents +may inspect them with `moon run dev-tools:helper-reference-audit` and +`moon run dev-tools:source-reference-audit`. Both tasks are intentionally local, +uncached, and excluded from CI because public entrypoints and generated or +indirect references require human or agent review before removal. + +## Policy Design + +Policy checks protect externally meaningful contracts, not the current spelling +of an implementation. Prefer these forms, in order: + +1. parse a manifest, package, workflow, lock, checksum, or evidence record and + assert a stable invariant; +2. execute a package-shape, clean-consumer, failure-path, or runtime test; +3. use a narrow security scan when the unsafe behavior is itself textual. + +Do not assert function names, step display names, source line order, prose +fragments, or exhaustive file inventories. Refactoring should fail policy only +when it changes a contract. `repository-semantics.mjs` owns the small set of +cross-repository layout and toolchain invariants; focused checkers own release, +SDK, extension, workflow, dependency, and evidence behavior. + +The repository-local skills under `.codex/skills/` are the operational runbooks +for agent-built changes. Use `qualify-oliphaunt-change` for selecting proof, +`add-oliphaunt-extension` for extension metadata and carriers, and +`release-oliphaunt` for candidate qualification and publication. Update the +relevant skill when an operational workflow changes; do not encode a tutorial +as source-text assertions in CI. + +## Tool Ownership + +Keep code in the narrowest owning domain: product behavior beside the product, +shared source/asset operations in `tools/xtask`, performance behavior in +`tools/perf`, graph selection in `tools/graph`, release contracts in +`tools/release`, and repository invariants in `tools/policy`. Split a module +when it has independent inputs, outputs, or failure modes. File names and helper +boundaries are implementation details, not policy APIs. diff --git a/docs/maintainers/wasm-usage-legacy.md b/docs/maintainers/wasm-usage-legacy.md new file mode 100644 index 00000000..56e0c280 --- /dev/null +++ b/docs/maintainers/wasm-usage-legacy.md @@ -0,0 +1,449 @@ +# WASIX Usage Guide + +This legacy maintainer note describes the preserved `oliphaunt-wasix` crate. +Native Rust SDK work should start with the public native runtime docs and +`src/sdks/rust/README.md`. + +`oliphaunt-wasix` has two primary entry points: + +- `Oliphaunt` for direct embedded queries from Rust; +- `OliphauntServer` for libraries that need a PostgreSQL connection URI. + +Prefer `Oliphaunt` unless you specifically need a Postgres client connection. + +## Install Mode + +Projects use the SDK crate and the package-manager-resolved WASIX artifact +products selected by the language build integration. After the first public +release, the initial dependency line is: + +```toml +oliphaunt-wasix = "0.1" +``` + +Enable extension APIs explicitly: + +```toml +oliphaunt-wasix = { version = "0.1", features = ["extensions"] } +``` + +The `0.5.x` repository-wide tags are legacy history, not versions of this +independently tagged product. Source remains at `0.0.0` until the generated +first-release PR moves it to `0.1.0`. + +The crate has no `bundled` feature and no public runtime/AOT archive env-var +install mode. Normal database opens require staged package-manager runtime and +AOT artifacts. + +## Opening Databases + +Persistent database under an explicit path: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::open("./.oliphaunt")?; + db.close()?; + Ok(()) +} +``` + +Persistent database under the platform app-data directory: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::builder() + .app("com", "example", "desktop-app") + .open()?; + db.close()?; + Ok(()) +} +``` + +Fast temporary database for tests: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + db.close()?; + Ok(()) +} +``` + +Explicit fresh-cluster temporary database: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::builder().fresh_temporary().open()?; + db.close()?; + Ok(()) +} +``` + +`temporary()` uses the cached template path. `fresh_temporary()` disables that +cache and runs the packaged `initdb` path instead. Use it when a test needs a +brand-new cluster, not for the common fast path. + +The direct builder also exposes: + +- `path(...)`, `app(...)`, and `app_id(...)` for persistent roots; +- `temporary()`, `template_cache(bool)`, and `fresh_temporary()` for ephemeral + roots; +- `load_data_dir_archive(...)` for restoring a physical data-dir archive before + open. + +## Startup Configuration + +Use builder methods for startup-time database settings: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::builder() + .temporary() + .postgres_config("synchronous_commit", "off") + .postgres_config("work_mem", "8MB") + .username("postgres") + .database("template1") + .relaxed_durability(true) + .open()?; + db.close()?; + Ok(()) +} +``` + +Relevant direct and server builder methods: + +- `postgres_config(name, value)` and `postgres_configs(...)`; +- `username(...)` and `database(...)`; +- `debug_level(level)` with PostgreSQL levels `0..=5`; +- `relaxed_durability(true)` for cacheable local workloads; +- `startup_arg(...)` and `startup_args(...)` for advanced PostgreSQL arguments. + +Use `postgres_config` for ordinary GUCs. It follows PostgreSQL's normal +`-c name=value` startup behavior, and explicit values override the default +startup profile. + +For `OliphauntServer`, the same startup methods are available on +`OliphauntServer::builder()`. The `oliphaunt-wasix-proxy` CLI exposes startup GUCs with +`--postgres-config NAME=VALUE`. + +## Queries + +`exec` runs SQL without parameters. `query` runs the extended protocol with +JSON parameters. + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; +use serde_json::json; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + + db.exec("CREATE TABLE items(id INT PRIMARY KEY, value TEXT)", None)?; + db.query( + "INSERT INTO items(id, value) VALUES ($1, $2)", + &[json!(1), json!("alpha")], + None, + )?; + + let result = db.query("SELECT value FROM items WHERE id = $1", &[json!(1)], None)?; + println!("{:?}", result.rows); + + db.close()?; + Ok(()) +} +``` + +Parameters are `serde_json::Value`. Default parsers and serializers cover +common Postgres scalar types, JSON, bytea, UUIDs, timestamps, and built-in +arrays. + +When you add runtime-created array types such as arrays of enums, domains, or +composites, `oliphaunt-wasix` usually discovers them lazily. If you want to refresh +that state explicitly, call `refresh_array_types()`. + +## Query Options + +`QueryOptions` controls result parsing and protocol behavior: + +- `row_mode` switches between object rows and positional arrays; +- `parsers` and `serializers` override type handling for specific OIDs; +- `blob` attaches bytes to `/dev/blob` for `COPY FROM`; +- `param_types` pins parameter OIDs for cases where PostgreSQL cannot infer + them cleanly; +- `on_notice` handles backend notices on a query-by-query basis. + +Example: + +```rust,no_run +use oliphaunt_wasix::{Oliphaunt, QueryOptions, RowMode}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + let options = QueryOptions { + row_mode: Some(RowMode::Array), + ..QueryOptions::default() + }; + + let result = db.query("SELECT 1, 2", &[], Some(&options))?; + println!("{:?}", result.rows); + + db.close()?; + Ok(()) +} +``` + +Use `describe_query(...)` when you need parameter and result type metadata +without executing the query. + +## Transactions + +Use `transaction` when several direct calls should commit or roll back together. + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; +use serde_json::json; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + + db.transaction(|tx| { + tx.query("INSERT INTO items(value) VALUES ($1)", &[json!("alpha")], None)?; + tx.query("INSERT INTO items(value) VALUES ($1)", &[json!("beta")], None)?; + Ok(()) + })?; + + db.close()?; + Ok(()) +} +``` + +The `Transaction` handle also exposes `exec`, `query`, `refresh_array_types`, +`commit`, and `rollback`. + +## Notifications + +Use `listen` when you want channel-specific `LISTEN/NOTIFY` callbacks, and +`on_notification` when you want to observe every notification. + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + + let specific = db.listen("events", |payload| { + println!("events payload: {payload}"); + })?; + let global = db.on_notification(|channel, payload| { + println!("{channel}: {payload}"); + }); + + db.exec("NOTIFY events, 'hello'", None)?; + + db.unlisten(specific)?; + db.off_notification(global); + db.close()?; + Ok(()) +} +``` + +`unlisten_channel(...)` removes all listeners for a specific channel. + +## `/dev/blob` and COPY + +Direct `Oliphaunt` can send and receive bytes through the virtual `/dev/blob` +device. + +```rust,no_run +use oliphaunt_wasix::{Oliphaunt, QueryOptions}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + + let import = QueryOptions { + blob: Some(b"alpha\nbeta\n".to_vec()), + ..QueryOptions::default() + }; + db.exec("COPY items FROM '/dev/blob'", Some(&import))?; + + let exported = db.exec("COPY items TO '/dev/blob'", None)?; + let blob = exported[0].blob.clone().expect("COPY TO blob"); + println!("{}", String::from_utf8(blob)?); + + db.close()?; + Ok(()) +} +``` + +If you already use a standard Postgres client, `OliphauntServer` also supports +client-driven `COPY FROM STDIN` through the normal wire protocol. + +## SQL Helpers + +`format_query` asks Postgres to quote parameter values. `QueryTemplate` and +`quote_identifier` help build SQL while keeping identifiers and values separate. + +```rust,no_run +use oliphaunt_wasix::{Oliphaunt, QueryTemplate, format_query, quote_identifier}; +use serde_json::json; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + + let sql = format_query(&mut db, "SELECT $1::int", &[json!(42)])?; + assert_eq!(sql, "SELECT '42'::int"); + + let mut template = QueryTemplate::new(); + template.push_sql("SELECT * FROM "); + template.push_identifier("items"); + template.push_sql(" WHERE value = "); + template.push_param(json!("alpha")); + let built = template.build(); + + assert_eq!(built.query, "SELECT * FROM \"items\" WHERE value = $1"); + assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\""); + + db.close()?; + Ok(()) +} +``` + +## Server Mode + +Use `OliphauntServer` when another crate expects a PostgreSQL URL. + +```rust,no_run +use oliphaunt_wasix::OliphauntServer; +use sqlx::{Connection, Row}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server = OliphauntServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} +``` + +`OliphauntServer::builder()` supports: + +- `path(...)`, `temporary()`, and `fresh_temporary()`; +- `tcp(...)`, and on Unix hosts `unix(...)`; +- the same startup configuration methods as `OliphauntBuilder`; +- bundled extensions with `extension(...)` and `extensions(...)`. + +Use `connection_uri()` or `database_url()` to hand a URI to a client library. +Generated URLs include `sslmode=disable`. + +Server mode still exposes one embedded backend. Configure SQLx, Diesel, +SeaORM, `tokio-postgres`, and framework pools with one connection. + +## Raw Protocol + +`exec_protocol` is the safest low-level wire-protocol entry point. It returns +parsed backend messages and still handles notices and notifications. + +```rust,no_run +use oliphaunt_wasix::{ExecProtocolOptions, Oliphaunt}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + let mut query = vec![b'Q']; + query.extend_from_slice(&13_i32.to_be_bytes()); + query.extend_from_slice(b"SELECT 1\0"); + + let result = db.exec_protocol(&query, ExecProtocolOptions::default())?; + assert!(!result.messages.is_empty()); + + db.close()?; + Ok(()) +} +``` + +Use `exec_protocol_raw(...)` when you need raw bytes, and +`exec_protocol_raw_stream(...)` when you want to forward backend bytes as they +arrive. + +## Physical Data-Dir Archives + +Use physical archives for same-version restore and fast cloning. They are not a +cross-version backup protocol. + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + let mut source = Oliphaunt::temporary()?; + source.exec("CREATE TABLE items(value TEXT)", None)?; + source.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let archive = source.dump_data_dir()?; + + let mut restored = Oliphaunt::builder() + .temporary() + .load_data_dir_archive(archive) + .open()?; + let mut cloned = restored.try_clone()?; + + cloned.exec("SELECT * FROM items", None)?; + + restored.close()?; + cloned.close()?; + source.close()?; + Ok(()) +} +``` + +Use `dump_data_dir_with_format(...)` when you want an explicit +`DataDirArchiveFormat::Tar` or `DataDirArchiveFormat::TarGz`. + +## Logical Dumps + +With the default feature set, both direct and server APIs expose logical dumps +through `PgDumpOptions`. + +```rust,no_run +use oliphaunt_wasix::{PgDumpOptions, Oliphaunt}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + db.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let sql = db.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + println!("{sql}"); + + db.close()?; + Ok(()) +} +``` + +CLI: + +```sh +oliphaunt-wasix-dump --root ./.oliphaunt +oliphaunt-wasix-dump --root ./.oliphaunt -- --schema-only +``` + +See [Dump and restore](../../src/docs/content/sdk/wasm/dump-restore.mdx) for +dump/restore and upgrade guidance. diff --git a/docs/maintainers/windows-vc-runtime.md b/docs/maintainers/windows-vc-runtime.md new file mode 100644 index 00000000..2d9c09ae --- /dev/null +++ b/docs/maintainers/windows-vc-runtime.md @@ -0,0 +1,70 @@ +# Windows Visual C++ runtime release contract + +Oliphaunt's Windows release carriers are self-contained on a clean supported +Windows x64 host. They do not assume that a system-wide Visual C++ +Redistributable has already been installed. + +## Redistribution source and terms + +Release builds may copy production runtime DLLs only from the developer shell's +exact `VCToolsRedistDir/x64/Microsoft.VC145.CRT` directory. The closure tool +rejects another CRT generation, non-x64 or PE32 files, symlinks, debug runtime +imports, and unapproved future CRT families. The DLL bytes are copied unchanged; +the binary-strip step explicitly preserves Microsoft redistributables and closure +verification runs after stripping. + +Microsoft permits licensed Visual Studio users to redistribute unmodified files +from the Visual Studio `VC/redist` directory subject to the Visual Studio license. +Maintainers must review the current [Visual Studio 2026 redistribution +terms](https://learn.microsoft.com/visualstudio/releases/2026/redistribution) +before publishing. Debug and `debug_nonredist` files must never be published. +Application-local placement follows Microsoft's [application-local deployment +guidance](https://learn.microsoft.com/cpp/windows/walkthrough-deploying-a-visual-cpp-application-to-an-application-local-folder?view=msvc-170). + +The installed `Microsoft.VC145.CRT` directory normally contains redistributable +payload files rather than a separate notice. If a future installed toolchain +ships a notice or license that its terms require distributors to carry, the +release must stop until that file is preserved in every derived carrier and its +presence is added to the package contract. Repository license metadata does not +relicense Microsoft's DLLs. + +## Carrier model + +- `liboliphaunt-native` is the one shared provider for native extensions. Its + `provider` profile is the exact union required by the base runtime and every + supported Windows extension: `msvcp140.dll`, `vcruntime140.dll`, and + `vcruntime140_1.dll`. Separately versioned extension packages depend on the + exact native-runtime product/version and do not duplicate these bytes. +- `oliphaunt-tools` and `oliphaunt-broker` are independently installable. Each + carries only the transitive CRT closure derived from its own normal and + delay-load PE imports. +- `oliphaunt-node-direct` currently imports no app-local VC runtime. Adding such + an import requires its independently installed npm carrier to add and verify + the corresponding exact closure. + +Every carrier places required DLLs in the executable/DLL search directory and +ships `windows-vc-runtime.sha256`. Receipt lines are lowercase, bytewise sorted, +LF-terminated SHA-256 bindings. Repackagers read the receipt, copy exactly those +members, and rerun the PE/import and digest verifier; they must not maintain an +independent blind list. The native provider is the deliberate exception: its +named profile requires the full supported-extension union even when an affected +PR builds only a subset of extensions. + +## Release evidence + +The MSVC setup step fails before a long build unless `VCToolsRedistDir` resolves +the exact x64 VC145 directory and all audited production files. Windows build +and packaging then: + +1. inspect normal and delay-import tables without relying on host PATH state; +2. strip Oliphaunt-produced binaries before staging redistributables; +3. atomically copy unmodified source bytes and write the digest receipt; +4. verify the exact carrier/profile closure and receipt after optimization; +5. preserve the receipt and DLLs through GitHub, npm, and Cargo carriers; and +6. run a clean-host smoke test with no preinstalled redistributable assumption. + +For native extensions, the aggregate extension build is evidence for the +provider union: every supported Windows module import must be covered by the +provider profile, while the package graph enforces the exact native-runtime +dependency. A new CRT import fails policy until the audited profile, legal +review, carrier checks, and clean-host evidence are deliberately updated. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..95be55ae --- /dev/null +++ b/examples/README.md @@ -0,0 +1,120 @@ +# 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 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. Native examples +load `postgres`, `initdb`, and `pg_ctl` from `liboliphaunt-native-*`, while +`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 a successful exact-commit CI run +can be staged transactionally. Set `QUALIFIED_SHA` to the commit that passed +the required CI qualification; the helper selects a matching successful run +or verifies an explicitly supplied `--run-id`: + +```sh +tools/dev/bun.sh tools/release/local-registry-publish.mjs download --sha "$QUALIFIED_SHA" --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 \ + --target linux-x64-gnu +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 +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 +tools/dev/bun.sh tools/release/local-registry-publish.mjs publish \ + --surface cargo \ + --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 +``` + +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-*`. + +Run npm examples through the local registry helper so pnpm reads the staged +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 +``` + +The committed Cargo example manifests use ordinary crates.io dependencies with +exact Oliphaunt product versions. They do not name the private +`oliphaunt-local` registry, commit `[patch.crates-io]`, or track a nested +`Cargo.lock`. + +`examples/tools/with-local-registries.sh` generates the candidate patch table +inside its isolated Cargo home. This keeps the commands in each example README +usable against staged packages without changing an example manifest or lock. + +Candidate validation happens only after exact-SHA CI has produced every +selected `.crate`. The release dry-run copies each example into scratch space, +adds exact `[patch.crates-io]` entries for packages in the candidate registry, +generates a fresh lock, and validates every candidate source, version, index +checksum, and archive SHA-256 before running `cargo fetch --locked`. Public +dependencies—and unchanged Oliphaunt packages in a later partial +release—continue to resolve from crates.io. Both the scratch manifests and +locks live under `target/`; they never change the qualified commit. + +Run the static source policy with: + +```sh +tools/dev/bun.sh tools/release/example-cargo-policy.mjs --check +``` + +Given an assembled candidate registry, run the exact-byte consumer proof with: + +```sh +tools/dev/bun.sh tools/release/validate-example-cargo-candidates.mjs \ + --index target/release-work/candidate-registries/cargo/index +``` + +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. + +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/build_pgdata_template.rs b/examples/build_pgdata_template.rs deleted file mode 100644 index 87fe75de..00000000 --- a/examples/build_pgdata_template.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::env; -use std::path::PathBuf; - -use anyhow::Result; -use pglite_oxide::build_pgdata_template; - -fn main() -> Result<()> { - let output_dir = env::args_os().nth(1).map(PathBuf::from).unwrap_or_else(|| { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/pglite-oxide/assets/prepopulated") - }); - - let template = build_pgdata_template(&output_dir)?; - println!("archive: {}", template.archive_path.display()); - println!("manifest: {}", template.manifest_path.display()); - Ok(()) -} 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/.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 new file mode 100644 index 00000000..361db07b --- /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 +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 +`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..ec66302a --- /dev/null +++ b/examples/electron-wasix/package.json @@ -0,0 +1,22 @@ +{ + "name": "oliphaunt-example-electron-wasix", + "private": true, + "version": "0.0.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": { + "kysely": "^0.29.2", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "electron": "^39.2.5", + "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.toml b/examples/electron-wasix/src-wasix/Cargo.toml new file mode 100644 index 00000000..c0383054 --- /dev/null +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "oliphaunt-electron-wasix-sidecar" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +anyhow = "1" +oliphaunt-wasix = { version = "=0.1.0", features = [ + "tools", + "extension-hstore", + "extension-pg-trgm", + "extension-unaccent", +] } +oliphaunt-wasix-tools = { version = "=0.1.0" } +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" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0" } 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..92b053c9 --- /dev/null +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -0,0 +1,89 @@ +use std::env; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::thread; + +use anyhow::{bail, Context, Result}; +use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions, PsqlOptions}; +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([ + extensions::HSTORE, + extensions::PG_TRGM, + extensions::UNACCENT, + ]) + .start() + .context("start oliphaunt-wasix server")?; + validate_wasix_tools(&server)?; + Ok(server) +} + +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"), + "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(()) +} + +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 ") +} + +#[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/electron-wasix/src/main-process.ts b/examples/electron-wasix/src/main-process.ts new file mode 100644 index 00000000..b62be467 --- /dev/null +++ b/examples/electron-wasix/src/main-process.ts @@ -0,0 +1,81 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import { dirname, join } from "node:path"; +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.cjs"), + 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")); + } + 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( + "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)); + +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(); +}); + +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.cts b/examples/electron-wasix/src/preload.cts new file mode 100644 index 00000000..0cebe053 --- /dev/null +++ b/examples/electron-wasix/src/preload.cts @@ -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..40ce9e83 --- /dev/null +++ b/examples/electron-wasix/src/todos.ts @@ -0,0 +1,191 @@ +import { join } from "node:path"; + +import { Kysely, PostgresDialect, sql, type Generated } from "kysely"; +import pg from "pg"; + +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", + "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)", +]; + +type Store = { + db: Kysely; + 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 db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: sidecar.databaseUrl, + max: 1, + }), + }), + }); + for (const statement of schemaStatements) { + await sql.raw(statement).execute(db); + } + return { db, sidecar }; +} + +export async function listTodos( + userData: string, + filter: { search: string; status: StatusFilter }, +) { + 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 { 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 { 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 { 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.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 statusPredicate(status: StatusFilter) { + return sql`( + ${status}::text = 'all' + OR (${status}::text = 'open' AND NOT done) + OR (${status}::text = 'done' AND done) + )`; +} + +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) { + 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..4e16471e --- /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.cts", "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..27152134 --- /dev/null +++ b/examples/electron-wasix/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: ".", + base: "./", + 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/.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 new file mode 100644 index 00000000..dbf5cebe --- /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 `nativeServer` +mode with a persistent root under Electron's user data directory. + +```sh +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/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..81353c84 --- /dev/null +++ b/examples/electron/package.json @@ -0,0 +1,24 @@ +{ + "name": "oliphaunt-example-electron", + "private": true, + "version": "0.0.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": { + "@oliphaunt/extension-contrib-pg18": "0.0.0", + "@oliphaunt/ts": "0.0.0", + "kysely": "^0.29.2", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "electron": "^39.2.5", + "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/electron/src/main-process.ts b/examples/electron/src/main-process.ts new file mode 100644 index 00000000..6d608529 --- /dev/null +++ b/examples/electron/src/main-process.ts @@ -0,0 +1,81 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import { dirname, join } from "node:path"; +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.cjs"), + 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")); + } + 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( + "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)); + +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(); +}); + +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.cts b/examples/electron/src/preload.cts new file mode 100644 index 00000000..0cebe053 --- /dev/null +++ b/examples/electron/src/preload.cts @@ -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..117a5e9d --- /dev/null +++ b/examples/electron/src/todos.ts @@ -0,0 +1,209 @@ +import { join } from "node:path"; + +import { Oliphaunt, type OliphauntDatabase } from "@oliphaunt/ts"; +import { Kysely, PostgresDialect, sql, type Generated } from "kysely"; +import pg from "pg"; + +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; +}; + +type Store = { + native: OliphauntDatabase; + db: Kysely; +}; + +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)", +]; + +let storePromise: Promise | undefined; + +export function getDatabase(userData: string) { + storePromise ??= openDatabase(userData); + return storePromise; +} + +async function openDatabase(userData: string): Promise { + const native = await Oliphaunt.open({ + 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 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 }, +) { + 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 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 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.deleteFrom("todos").where("id", "=", String(id)).execute(); +} + +export async function closeDatabase() { + if (!storePromise) return; + const store = await storePromise; + await store.db.destroy(); + await store.native.close(); + 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 statusPredicate(status: StatusFilter) { + return sql`( + ${status}::text = 'all' + OR (${status}::text = 'open' AND NOT done) + OR (${status}::text = 'done' AND done) + )`; +} + +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) { + 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..5d26d54a --- /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.cts", "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..f822c83a --- /dev/null +++ b/examples/electron/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: ".", + base: "./", + clearScreen: false, + server: { + port: 5174, + strictPort: true, + }, + build: { + outDir: "dist/renderer", + emptyOutDir: false, + }, +}); diff --git a/examples/moon.yml b/examples/moon.yml new file mode 100644 index 00000000..88a7db75 --- /dev/null +++ b/examples/moon.yml @@ -0,0 +1,51 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "integration-examples" +language: "typescript" +layer: "application" +stack: "frontend" +tags: ["examples", "integration"] + +project: + title: "Integration Examples" + description: "Cross-product examples and installed-app validation entrypoints." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/core" + paths: + "**/*": ["@oliphaunt/core"] + +tasks: + check: + tags: ["quality", "static"] + command: "bash tools/dev/bun.sh examples/tools/check-examples.mjs" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/examples/**/*" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + - "/release-please-config.json" + - "/src/sdks/react-native/examples/**/*" + - "!/src/sdks/react-native/examples/**/node_modules" + - "!/src/sdks/react-native/examples/**/node_modules/**" + - "/src/bindings/wasix-rust/examples/**/*" + - "/src/extensions/contrib/VERSION" + - "/src/sdks/js/package.json" + - "/src/sources/toolchains/wasix.toml" + - "/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" + - "/examples/tools/check-examples.mjs" + - "/examples/tools/check-examples.sh" + - "/tools/release/example-cargo-policy.mjs" + - "/tools/release/example-cargo-registry.mjs" + - "/tools/release/publication-catalog.mjs" + - "/tools/release/release-graph.mjs" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/examples/tauri-sqlx-vanilla/README.md b/examples/tauri-sqlx-vanilla/README.md deleted file mode 100644 index d3dca54c..00000000 --- a/examples/tauri-sqlx-vanilla/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# pglite-oxide Tauri SQLx example - -This is a Tauri v2 example that keeps `pglite-oxide` in Rust state and talks to -it through a real one-connection `sqlx::PgPool`. - -## Run the desktop app - -```sh -npm install -npm run tauri dev -``` - -The app opens first and runs the database profile only when the profile command -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/pglite-profile-release.json -``` - -Use `--fresh` to remove the profile data directory before the run. Omit it to -measure a warm start with an existing cluster. - -## What it demonstrates - -- storing the database in managed Rust state; -- using `PgliteServer` to hand SQLx a PostgreSQL URI; -- configuring the SQLx pool with `max_connections(1)`; -- creating schema, seeding rows, and profiling real SQL queries. diff --git a/examples/tauri-sqlx-vanilla/index.html b/examples/tauri-sqlx-vanilla/index.html deleted file mode 100644 index c2ea9cff..00000000 --- a/examples/tauri-sqlx-vanilla/index.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - pglite SQLx profile - - - - -
-
-
-

pglite-oxide / Tauri / SQLx

-

Embedded Postgres profile

-
-
Idle
-
- -
- - - - - -
-
- Startup - - -
-
- Workload - - -
-
- Rows - - -
-
- Proxy - - -
-
- -
-
-
-

Startup phases

- - -
-
-
- -
-
-

Workload phases

- - -
-
-
-
- -
-
-

SQLx query timings

- P50 / P95 / max -
-
-
- - - - - - - - - - - - -
QueryRunsRowsMeanP50P95Max
- - - -
-
- - diff --git a/examples/tauri-sqlx-vanilla/package-lock.json b/examples/tauri-sqlx-vanilla/package-lock.json deleted file mode 100644 index 273d9d7e..00000000 --- a/examples/tauri-sqlx-vanilla/package-lock.json +++ /dev/null @@ -1,1361 +0,0 @@ -{ - "name": "tauri-sqlx-vanilla", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tauri-sqlx-vanilla", - "version": "0.1.0", - "dependencies": { - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2" - }, - "devDependencies": { - "@tauri-apps/cli": "^2", - "typescript": "~5.6.2", - "vite": "^6.0.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", - "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", - "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", - "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", - "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", - "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", - "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", - "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", - "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", - "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", - "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", - "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", - "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", - "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", - "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", - "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", - "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", - "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", - "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", - "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", - "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", - "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", - "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", - "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", - "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", - "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tauri-apps/api": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", - "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", - "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.10.1", - "@tauri-apps/cli-darwin-x64": "2.10.1", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", - "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", - "@tauri-apps/cli-linux-arm64-musl": "2.10.1", - "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", - "@tauri-apps/cli-linux-x64-gnu": "2.10.1", - "@tauri-apps/cli-linux-x64-musl": "2.10.1", - "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", - "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", - "@tauri-apps/cli-win32-x64-msvc": "2.10.1" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", - "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", - "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", - "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", - "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", - "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", - "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", - "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", - "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", - "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", - "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", - "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", - "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.8.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.60.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", - "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.2", - "@rollup/rollup-android-arm64": "4.60.2", - "@rollup/rollup-darwin-arm64": "4.60.2", - "@rollup/rollup-darwin-x64": "4.60.2", - "@rollup/rollup-freebsd-arm64": "4.60.2", - "@rollup/rollup-freebsd-x64": "4.60.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", - "@rollup/rollup-linux-arm-musleabihf": "4.60.2", - "@rollup/rollup-linux-arm64-gnu": "4.60.2", - "@rollup/rollup-linux-arm64-musl": "4.60.2", - "@rollup/rollup-linux-loong64-gnu": "4.60.2", - "@rollup/rollup-linux-loong64-musl": "4.60.2", - "@rollup/rollup-linux-ppc64-gnu": "4.60.2", - "@rollup/rollup-linux-ppc64-musl": "4.60.2", - "@rollup/rollup-linux-riscv64-gnu": "4.60.2", - "@rollup/rollup-linux-riscv64-musl": "4.60.2", - "@rollup/rollup-linux-s390x-gnu": "4.60.2", - "@rollup/rollup-linux-x64-gnu": "4.60.2", - "@rollup/rollup-linux-x64-musl": "4.60.2", - "@rollup/rollup-openbsd-x64": "4.60.2", - "@rollup/rollup-openharmony-arm64": "4.60.2", - "@rollup/rollup-win32-arm64-msvc": "4.60.2", - "@rollup/rollup-win32-ia32-msvc": "4.60.2", - "@rollup/rollup-win32-x64-gnu": "4.60.2", - "@rollup/rollup-win32-x64-msvc": "4.60.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - } - } -} diff --git a/examples/tauri-sqlx-vanilla/package.json b/examples/tauri-sqlx-vanilla/package.json deleted file mode 100644 index 035457ce..00000000 --- a/examples/tauri-sqlx-vanilla/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "tauri-sqlx-vanilla", - "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", - "@tauri-apps/plugin-opener": "^2" - }, - "devDependencies": { - "@tauri-apps/cli": "^2", - "vite": "^6.0.3", - "typescript": "~5.6.2" - } -} diff --git a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock deleted file mode 100644 index 38ee4d1d..00000000 --- a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ /dev/null @@ -1,7905 +0,0 @@ -# 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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[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.117", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[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.11.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - -[[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.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -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 = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[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.117", -] - -[[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.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "bytesize" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" -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.11.1", - "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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" -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.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" -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 = "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.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -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.117", -] - -[[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.11.1", - "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.11.1", - "core-foundation", - "libc", -] - -[[package]] -name = "corosensei" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" -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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" -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 0.13.1", - "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.117", -] - -[[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.117", -] - -[[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.117", -] - -[[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.117", -] - -[[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.117", -] - -[[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.117", -] - -[[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.117", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -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.0.1", -] - -[[package]] -name = "defmt" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" -dependencies = [ - "defmt-parser", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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 = [ - "powerfmt", - "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.117", -] - -[[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.117", -] - -[[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.117", - "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.0", - "const-oid", - "crypto-common 0.2.1", -] - -[[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.11.1", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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.117", -] - -[[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.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -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 = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[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.117", -] - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "enumset" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" -dependencies = [ - "enumset_derive", -] - -[[package]] -name = "enumset_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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 = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "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.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[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.117", -] - -[[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-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[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.117", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.1", - "wasip2", - "wasip3", - "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.11.1", - "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.117", -] - -[[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.117", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -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.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -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.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" -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.0", - "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.47.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" -dependencies = [ - "console", - "once_cell", - "regex", - "serde", - "similar", - "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 = "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[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.117", -] - -[[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.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "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.11.1", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "leb128" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" - -[[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.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "bitflags 2.11.1", - "libc", - "plain", - "redox_syscall 0.7.5", -] - -[[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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lz4_flex" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -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.11.1", - "itoa", -] - -[[package]] -name = "muda" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" -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.117", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.11.1", - "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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[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.117", -] - -[[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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.11.1", - "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.0", - "indexmap 2.14.0", - "memchr", - "ruzstd", -] - -[[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 = "open" -version = "5.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" -dependencies = [ - "dunce", - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[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 = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[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 = "pglite-oxide" -version = "0.5.1" -dependencies = [ - "anyhow", - "async-trait", - "directories", - "dunce", - "filetime", - "flate2", - "hex", - "pglite-oxide-aot-aarch64-apple-darwin", - "pglite-oxide-aot-aarch64-unknown-linux-gnu", - "pglite-oxide-aot-x86_64-pc-windows-msvc", - "pglite-oxide-aot-x86_64-unknown-linux-gnu", - "pglite-oxide-assets", - "regex", - "serde", - "serde_json", - "sha2 0.10.9", - "tar", - "tempfile", - "tokio", - "tracing", - "wasmer", - "wasmer-config", - "wasmer-types", - "wasmer-wasix", - "webc", - "zstd", -] - -[[package]] -name = "pglite-oxide-aot-aarch64-apple-darwin" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-x86_64-pc-windows-msvc" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" -version = "0.5.1" - -[[package]] -name = "pglite-oxide-assets" -version = "0.5.1" -dependencies = [ - "serde", - "serde_json", -] - -[[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", - "serde", -] - -[[package]] -name = "phf_codegen" -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", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -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", -] - -[[package]] -name = "phf_macros" -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", - "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", -] - -[[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.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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 = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[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.11.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - -[[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.117", -] - -[[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.11+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.117", -] - -[[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.117", -] - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -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.2", - "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.11.1", -] - -[[package]] -name = "redox_syscall" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" -dependencies = [ - "bitflags 2.11.1", -] - -[[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.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -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.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" -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.0", - "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.117", -] - -[[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.11.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" -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.117", -] - -[[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.117", -] - -[[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.11.1", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf 0.13.1", - "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.117", -] - -[[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.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -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.117", -] - -[[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.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" -dependencies = [ - "base64 0.22.1", - "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.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[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.117", -] - -[[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 = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -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.117", -] - -[[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.117", - "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.11.1", - "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 0.13.1", - "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 0.13.1", - "phf_shared 0.13.1", - "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 = "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.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c30da69ccd7ab2780ce5309791f3cd2ef9716262c07a0a29096226d4235a979" -dependencies = [ - "debugid", - "memmap2 0.9.10", - "stable_deref_trait", - "uuid", -] - -[[package]] -name = "symbolic-demangle" -version = "13.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1245acf80236b4a0d99e9216532102a1670950e79c70b980b607c2040966e83d" -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.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -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.117", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" -dependencies = [ - "bitflags 2.11.1", - "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.117", -] - -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" -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.117", - "tauri-utils", - "thiserror 2.0.18", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "walkdir", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit", - "objc2-foundation", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.18", - "url", - "windows", - "zbus", -] - -[[package]] -name = "tauri-runtime" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" -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-sqlx-vanilla" -version = "0.1.0" -dependencies = [ - "anyhow", - "pglite-oxide", - "serde", - "serde_json", - "sqlx", - "tauri", - "tauri-build", - "tauri-plugin-opener", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "tauri-utils" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf 0.11.3", - "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.2", - "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.117", -] - -[[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.117", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" -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.117", -] - -[[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.2", -] - -[[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.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.2", -] - -[[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.2", -] - -[[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.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.1", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[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.117", -] - -[[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.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" -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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "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.2", - "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 = "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.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[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.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -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" -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", -] - -[[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.10", - "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 0.250.0", - "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.117", -] - -[[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.2", - "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 0.250.0", -] - -[[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.2", - "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 0.250.0", - "wasmer", - "wasmer-config", - "wasmer-journal", - "wasmer-package", - "wasmer-types", - "wasmer-wasix-types", - "wasmparser 0.250.0", - "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.11.1", - "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.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", - "indexmap 2.14.0", -] - -[[package]] -name = "web-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" -dependencies = [ - "phf 0.13.1", - "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.7", -] - -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -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.117", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" -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.117", -] - -[[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.117", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -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.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" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -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.117", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 1.0.2", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" -dependencies = [ - "serde", - "winnow 1.0.2", - "zvariant", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -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.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[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.117", -] - -[[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", -] - -[[package]] -name = "zvariant" -version = "5.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 1.0.2", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.117", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.117", - "winnow 1.0.2", -] diff --git a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml b/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml deleted file mode 100644 index 40a271da..00000000 --- a/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "tauri-sqlx-vanilla" -version = "0.1.0" -description = "Vanilla Tauri v2 SQLx profiler for pglite-oxide" -authors = ["pglite-oxide contributors"] -edition = "2021" -publish = false - -[workspace] - -[lib] -name = "tauri_sqlx_vanilla_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - -[build-dependencies] -tauri-build = { version = "2", features = [] } - -[dependencies] -anyhow = "1" -pglite-oxide = { path = "../../.." } -sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } -tauri = { version = "2", features = [] } -tauri-plugin-opener = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -thiserror = "2" -tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } diff --git a/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs b/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs deleted file mode 100644 index f871d8d0..00000000 --- a/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -pub mod bench; - -use serde::ser::Serializer; -use tauri::Manager; - -use bench::{BenchReport, BenchState}; - -#[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:#}")) - } -} - -#[tauri::command] -async fn profile_queries( - state: tauri::State<'_, BenchState>, - fresh: bool, - row_count: u32, -) -> Result { - state - .profile_queries(fresh, row_count) - .await - .map_err(CommandError::from) -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .setup(|app| { - let root = app - .path() - .app_data_dir() - .map(|dir| dir.join("pglite-sqlx-profile"))?; - app.manage(BenchState::new(root)); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![profile_queries]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json b/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json deleted file mode 100644 index 49ec5917..00000000 --- a/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "pglite SQLx Profile", - "version": "0.1.0", - "identifier": "com.pgliteoxide.sqlxbench", - "build": { - "beforeDevCommand": "npm run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" - }, - "app": { - "withGlobalTauri": true, - "windows": [ - { - "title": "pglite SQLx Profile", - "width": 1080, - "height": 760 - } - ], - "security": { - "csp": null - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } -} 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/.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 new file mode 100644 index 00000000..f0bd0d3b --- /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 +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/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..83019e14 --- /dev/null +++ b/examples/tauri-wasix/package.json @@ -0,0 +1,20 @@ +{ + "name": "oliphaunt-example-tauri-wasix", + "private": true, + "version": "0.0.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": "^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.toml b/examples/tauri-wasix/src-tauri/Cargo.toml new file mode 100644 index 00000000..17d33d3a --- /dev/null +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "oliphaunt-example-tauri-wasix" +version = "0.0.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 = { version = "=0.1.0", features = [ + "tools", + "extension-hstore", + "extension-pg-trgm", + "extension-unaccent", +] } +oliphaunt-wasix-tools = { version = "=0.1.0" } +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 = ["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" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0" } 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..0cfd3f15 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -0,0 +1,310 @@ +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::{Context, Result}; +use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions, PsqlOptions}; +use serde::ser::Serializer; +use serde::{Deserialize, Serialize}; +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()) + } +} + +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([ + extensions::HSTORE, + extensions::PG_TRGM, + extensions::UNACCENT, + ]) + .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)) + .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(()) +} + +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"), + "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(()) +} + +#[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 = 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"); +} + +#[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-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/.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 new file mode 100644 index 00000000..0e529721 --- /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 +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/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..04d89c83 --- /dev/null +++ b/examples/tauri/package.json @@ -0,0 +1,20 @@ +{ + "name": "oliphaunt-example-tauri", + "private": true, + "version": "0.0.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": "^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.toml b/examples/tauri/src-tauri/Cargo.toml new file mode 100644 index 00000000..76dbd92b --- /dev/null +++ b/examples/tauri/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "oliphaunt-example-tauri" +version = "0.0.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 = { version = "=0.1.0" } +tauri-build = { version = "2", features = [] } + +[dependencies] +anyhow = "1" +oliphaunt = { version = "=0.1.0" } +oliphaunt-tools = { version = "=0.1.0" } +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" } +oliphaunt-broker-linux-x64-gnu = { version = "=0.1.0" } +oliphaunt-extension-contrib-pg18-linux-x64-gnu = { version = "=0.1.0" } 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..d9721966 --- /dev/null +++ b/examples/tauri/src-tauri/src/lib.rs @@ -0,0 +1,275 @@ +use std::path::PathBuf; + +use oliphaunt::{BackupRequest, Extension, Oliphaunt, QueryResult}; +use serde::ser::Serializer; +use serde::{Deserialize, Serialize}; +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 { + oliphaunt::register_build_resources!()?; + let db = Oliphaunt::builder() + .path(root) + .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>, + 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"); +} + +#[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/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.mjs b/examples/tools/check-examples.mjs new file mode 100644 index 00000000..31362485 --- /dev/null +++ b/examples/tools/check-examples.mjs @@ -0,0 +1,273 @@ +#!/usr/bin/env bun +import { existsSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +import { electronReleaseDependencies } from "./example-release-dependencies.mjs"; + +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 readJsonObject(path) { + const value = JSON.parse(readFileSync(path, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${path} must contain a JSON object`); + } + return value; +} + +function requireDependencyVersion(path, packageName, expectedVersion) { + const data = readJsonObject(path); + const dependencies = data.dependencies; + if (dependencies === null || Array.isArray(dependencies) || typeof dependencies !== "object") { + fail(`${path} must declare dependencies`); + } + const actual = dependencies[packageName]; + if (actual !== expectedVersion) { + fail( + `${path} dependency ${packageName} must match current release product version ${expectedVersion}, got ${actual ?? ""}`, + ); + } +} + +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"`); +requireText("examples/tools/with-local-registries.sh", "--patch-candidates"); +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 --dir "\$app_dir" install --no-frozen-lockfile`, +); +requireText( + "examples/tools/run-electron-driver-smoke.sh", + String.raw`pnpm --dir "\$app_dir" install --no-frozen-lockfile`, +); +requireText( + "examples/tools/run-electron-driver-smoke.sh", + String.raw`example_package_version "@oliphaunt/tools-linux-x64-gnu"`, +); +rejectText( + "examples/tools/run-electron-driver-smoke.sh", + String.raw`assert_npm_package\s+"@oliphaunt/[^"]+"\s+"[0-9]+\.[0-9]+\.[0-9]+"`, +); +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"); +requireFile("tools/release/example-cargo-policy.mjs"); +requireFile("tools/release/prepare-example-cargo-candidate.mjs"); +requireFile("tools/release/validate-example-cargo-candidates.mjs"); +for (const { packageName, version } of electronReleaseDependencies(ROOT)) { + requireDependencyVersion("examples/electron/package.json", packageName, version); +} +requireText("examples/electron/package.json", String.raw`"pg": "\^8\.16\.3"`); +rejectFile("examples/electron/src/oliphaunt-kysely.ts"); +rejectText("examples/tauri/src-tauri/Cargo.toml", 'registry\\s*=\\s*"oliphaunt-local"'); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-tools ="); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-extension-contrib-pg18-linux-x64-gnu"); +rejectText("examples/tauri-wasix/src-tauri/Cargo.toml", 'registry\\s*=\\s*"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"); +requireWasixToolsSmoke("examples/tauri-wasix/src-tauri/src/lib.rs"); +rejectText("examples/electron-wasix/src-wasix/Cargo.toml", 'registry\\s*=\\s*"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"); +requireWasixToolsSmoke("examples/electron-wasix/src-wasix/src/main.rs"); +rejectText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + 'registry\\s*=\\s*"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/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'); +rejectText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + 'path = "../../../crates/oliphaunt-wasix"', +); +for (const lockfile of [ + "examples/tauri/src-tauri/Cargo.lock", + "examples/tauri-wasix/src-tauri/Cargo.lock", + "examples/electron-wasix/src-wasix/Cargo.lock", + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock", +]) { + rejectFile(lockfile); +} + +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 new file mode 100755 index 00000000..bd58f036 --- /dev/null +++ b/examples/tools/check-examples.sh @@ -0,0 +1,9 @@ +#!/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" +exec tools/dev/bun.sh examples/tools/check-examples.mjs "$@" diff --git a/examples/tools/check-lockfiles.sh b/examples/tools/check-lockfiles.sh new file mode 100755 index 00000000..42391b57 --- /dev/null +++ b/examples/tools/check-lockfiles.sh @@ -0,0 +1,10 @@ +#!/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" + +tools/dev/bun.sh tools/release/example-cargo-policy.mjs --check 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/example-release-dependencies.mjs b/examples/tools/example-release-dependencies.mjs new file mode 100644 index 00000000..01df6b08 --- /dev/null +++ b/examples/tools/example-release-dependencies.mjs @@ -0,0 +1,110 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const ELECTRON_RELEASE_DEPENDENCIES = [ + { + packageName: "@oliphaunt/ts", + versionSource: { type: "json", path: "src/sdks/js/package.json", keys: ["version"] }, + }, + { + packageName: "@oliphaunt/extension-contrib-pg18", + versionSource: { type: "text", path: "src/extensions/contrib/VERSION" }, + }, +]; + +const ELECTRON_SMOKE_PACKAGES = [ + { + packageName: "@oliphaunt/liboliphaunt-linux-x64-gnu", + versionSource: { + type: "json", + path: "src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json", + keys: ["version"], + }, + }, + { + packageName: "@oliphaunt/tools-linux-x64-gnu", + versionSource: { + type: "json", + path: "src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json", + keys: ["version"], + }, + }, +]; + +function fail(message) { + console.error(`example-release-dependencies.mjs: ${message}`); + process.exit(2); +} + +function readJsonObject(root, relativePath) { + const file = path.join(root, relativePath); + const value = JSON.parse(readFileSync(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + throw new Error(`${relativePath} must contain a JSON object`); + } + return value; +} + +function readVersion(root, source, context) { + if (source.type === "text") { + const version = readFileSync(path.join(root, source.path), "utf8").trim(); + if (!version) { + throw new Error(`${source.path} does not define a version for ${context}`); + } + return version; + } + if (source.type === "json") { + let current = readJsonObject(root, source.path); + for (const key of source.keys) { + if (current === null || Array.isArray(current) || typeof current !== "object") { + throw new Error(`${source.path} has no JSON object at ${source.keys.join(".")} for ${context}`); + } + current = current[key]; + } + if (typeof current !== "string" || !current) { + throw new Error(`${source.path} does not define a string version for ${context}`); + } + return current; + } + throw new Error(`${context} uses unsupported version source ${JSON.stringify(source.type)}`); +} + +export function electronReleaseDependencies(root) { + return ELECTRON_RELEASE_DEPENDENCIES.map((entry) => ({ + packageName: entry.packageName, + version: readVersion(root, entry.versionSource, entry.packageName), + })); +} + +export function electronPackageVersion(root, packageName) { + const entry = [...ELECTRON_RELEASE_DEPENDENCIES, ...ELECTRON_SMOKE_PACKAGES].find( + (candidate) => candidate.packageName === packageName, + ); + if (entry === undefined) { + throw new Error(`unknown Electron example package ${JSON.stringify(packageName)}`); + } + return readVersion(root, entry.versionSource, packageName); +} + +function printElectronPackageVersion(argv) { + const packageName = argv[0]; + if (typeof packageName !== "string" || packageName.length === 0) { + fail("usage: example-release-dependencies.mjs electron-package-version "); + } + try { + process.stdout.write(`${electronPackageVersion(process.cwd(), packageName)}\n`); + } catch (error) { + fail(error.message); + } +} + +const mainUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; +if (import.meta.url === mainUrl) { + const [command, ...args] = process.argv.slice(2); + if (command === "electron-package-version") { + printElectronPackageVersion(args); + } else { + fail("usage: example-release-dependencies.mjs electron-package-version "); + } +} diff --git a/examples/tools/run-electron-driver-smoke.sh b/examples/tools/run-electron-driver-smoke.sh new file mode 100755 index 00000000..3f2ab725 --- /dev/null +++ b/examples/tools/run-electron-driver-smoke.sh @@ -0,0 +1,188 @@ +#!/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" + +assert_npm_package() { + local package_name="$1" + local expected_version="$2" + 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, 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}`); +} +const normalized = packageJson.split(path.sep).join('/'); +if (!normalized.includes('/node_modules/')) { + throw new Error(`${packageName} resolved outside node_modules: ${packageJson}`); +} +NODE +} + +example_package_version() { + local package_name="$1" + node "$root/examples/tools/example-release-dependencies.mjs" electron-package-version "$package_name" +} + +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 after example install" +fi +if [ "$app_dir" = "examples/electron" ]; then + typescript_version="$(example_package_version "@oliphaunt/ts")" + liboliphaunt_linux_version="$(example_package_version "@oliphaunt/liboliphaunt-linux-x64-gnu")" + tools_linux_version="$(example_package_version "@oliphaunt/tools-linux-x64-gnu")" + hstore_version="$(example_package_version "@oliphaunt/extension-hstore")" + + assert_npm_package "@oliphaunt/ts" "$typescript_version" + assert_npm_package "@oliphaunt/liboliphaunt-linux-x64-gnu" "$liboliphaunt_linux_version" "@oliphaunt/ts" + assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "$tools_linux_version" "@oliphaunt/ts" + assert_npm_package "@oliphaunt/extension-hstore" "$hstore_version" +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" +) + +if command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a "${run_smoke[@]}" +else + "${run_smoke[@]}" +fi diff --git a/examples/tools/run-tauri-webdriver-smoke.sh b/examples/tools/run-tauri-webdriver-smoke.sh new file mode 100755 index 00000000..273ac8a9 --- /dev/null +++ b/examples/tools/run-tauri-webdriver-smoke.sh @@ -0,0 +1,86 @@ +#!/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 +} + +source_app_dir="${1:-}" +if [ -z "$source_app_dir" ]; then + fail "usage: examples/tools/run-tauri-webdriver-smoke.sh " +fi +if [[ "$source_app_dir" = /* ]]; then + source_app_path="$(realpath -m "$source_app_dir")" +else + source_app_path="$(realpath -m "$root/$source_app_dir")" +fi +case "$source_app_path" in + "$root"/*) ;; + *) fail "example path must remain inside the repository: $source_app_dir" ;; +esac +if [ ! -f "$source_app_path/src-tauri/Cargo.toml" ]; then + fail "$source_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 + +source_app_relative="${source_app_path#"$root"/}" +scratch="$root/target/e2e/tauri-apps/${source_app_relative//\//-}/$$" +app_dir="$scratch/app" +trap 'rm -rf "$scratch"' EXIT +rm -rf "$scratch" +mkdir -p "$app_dir" +rsync -a --delete \ + --exclude node_modules \ + --exclude src-tauri/gen \ + --exclude src-tauri/target \ + "$source_app_path/" "$app_dir/" +rm -f "$app_dir/src-tauri/Cargo.lock" + +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="$( + 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="$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)); +} diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh new file mode 100755 index 00000000..0ece3eaa --- /dev/null +++ b/examples/tools/with-local-registries.sh @@ -0,0 +1,45 @@ +#!/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" +cargo_home="$root/target/local-registries/cargo-home" +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/dev/bun.sh tools/release/local-registry-publish.mjs before running examples" >&2 + exit 1 +fi + +mkdir -p "$cargo_home" +export CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX="$({ + tools/dev/bun.sh tools/release/example-cargo-registry.mjs configure \ + --cargo-home "$cargo_home" \ + --index "$cargo_index" \ + --patch-candidates +})" +export OLIPHAUNT_LOCAL_CARGO_INDEX="$cargo_index" +# 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 +# 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 +export electron_config_cache="$root/target/local-registries/electron-cache" + +exec "$@" diff --git a/moon.yml b/moon.yml new file mode 100644 index 00000000..90022760 --- /dev/null +++ b/moon.yml @@ -0,0 +1,411 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "repo" +language: "unknown" +layer: "configuration" +stack: "infrastructure" +tags: ["repo", "hygiene", "monorepo"] + +project: + title: "Repository" + description: "Root repository hygiene, workspace metadata, and cross-cutting checks." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/core" + paths: + "*": ["@oliphaunt/core"] + +tasks: + check: + tags: ["policy", "aggregate", "quality", "static"] + command: "true" + deps: + - "repo:structure" + - "repo:tooling" + - "repo:ci-policy" + - "repo:docs-policy" + - "repo:release-policy" + - "repo:release-metadata" + - "repo:moon-graph" + - "repo:test-policy" + - "repo:test-strategy" + - "repo:prek" + options: + cache: true + runFromWorkspaceRoot: true + structure: + tags: ["policy", "assertion", "quality", "static"] + command: "bash tools/policy/check-repo-structure.sh" + inputs: + - "/.github/**/*" + - "/.gitignore" + - "/.moon/workspace.yml" + - "/.moon/toolchains.yml" + - "/benchmarks/**/*" + - "/Cargo.lock" + - "/Cargo.toml" + - "/README.md" + - "/coverage/**/*" + - "/docs/**/*" + - "/src/shared/fixtures/**/*" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + - "/src/**/*" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/src/**/.build" + - "!/src/**/.build/**" + - "!/src/**/.gradle/**" + - "!/src/**/.cxx/**" + - "!/src/**/.next/**" + - "!/src/**/.source/**" + - "!/src/**/build/**" + - "!/src/**/out/**" + - "!/src/**/Pods/**" + - "!/src/**/DerivedData/**" + - "/tools/**/*" + options: + cache: true + runFromWorkspaceRoot: true + tooling: + tags: ["policy", "assertion", "quality", "static"] + command: "bash tools/policy/check-tooling-stack.sh" + inputs: + - "/.github/**/*" + - "/.moon/workspace.yml" + - "/.moon/toolchains.yml" + - "/docs/maintainers/tooling.md" + - "/moon.yml" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + - "/src/**/moon.yml" + - "/tools/**/*" + options: + cache: true + runFromWorkspaceRoot: true + ci-policy: + tags: ["policy", "assertion", "quality", "static"] + script: | + tools/dev/bun.sh test tools/policy/assertions/workflow-contract-core.test.mjs tools/policy/assertions/workflow-semantics.test.mjs + bun tools/policy/assertions/assert-ci-workflows.mjs + inputs: + - "/.github/**/*" + - "/docs/architecture/final-product-source-architecture.md" + - "/docs/maintainers/tooling.md" + - "/tools/policy/assertions/assert-ci-workflows.mjs" + - "/tools/policy/assertions/workflow-contract-core.mjs" + - "/tools/policy/assertions/workflow-contract-core.test.mjs" + - "/tools/policy/assertions/workflow-semantics.mjs" + - "/tools/policy/assertions/workflow-semantics.test.mjs" + - "/tools/policy/check-release-policy.mjs" + - "/tools/release/**/*" + options: + cache: true + runFromWorkspaceRoot: true + docs-policy: + tags: ["policy", "assertion", "quality", "static"] + command: "bash tools/policy/check-docs.sh" + inputs: + - "/docs/**/*" + - "/src/docs/**/*" + - "!/src/docs/node_modules" + - "!/src/docs/node_modules/**" + - "!/src/docs/.next/**" + - "/tools/policy/check-docs.sh" + options: + cache: true + runFromWorkspaceRoot: true + release-policy: + tags: ["policy", "assertion", "quality", "static"] + command: "tools/dev/bun.sh tools/policy/check-release-policy.mjs" + inputs: + - "/.github/**/*" + - "/.moon/workspace.yml" + - "/.moon/toolchains.yml" + - "/Cargo.lock" + - "/Cargo.toml" + - "/Package.swift" + - "/package.json" + - "/pnpm-lock.yaml" + - "/src/**/*" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/src/**/.build" + - "!/src/**/.build/**" + - "!/src/**/.gradle/**" + - "!/src/**/.cxx/**" + - "!/src/**/.next/**" + - "!/src/**/.source/**" + - "!/src/**/build/**" + - "!/src/**/out/**" + - "!/src/**/Pods/**" + - "!/src/**/DerivedData/**" + - "/tools/release/**/*" + - "/tools/policy/check-release-policy.mjs" + options: + cache: true + runFromWorkspaceRoot: true + # release-tools:check owns this exact proof in hosted CI. + runInCI: skip + release-metadata: + tags: ["policy", "assertion", "quality", "static"] + command: "tools/dev/bun.sh tools/release/check-release-metadata.mjs" + inputs: + - "/README.md" + - "/docs/**/*" + - "/Package.swift" + - "/src/**/*" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/src/**/.build" + - "!/src/**/.build/**" + - "!/src/**/.gradle/**" + - "!/src/**/.cxx/**" + - "!/src/**/.next/**" + - "!/src/**/.source/**" + - "!/src/**/build/**" + - "!/src/**/out/**" + - "!/src/**/Pods/**" + - "!/src/**/DerivedData/**" + - "/tools/dev/bun.sh" + - "/tools/release/**/*" + options: + cache: true + runFromWorkspaceRoot: true + # release-tools:check owns this exact proof in hosted CI. + runInCI: skip + moon-graph: + tags: ["policy", "assertion", "quality", "static"] + command: "tools/policy/check-moon-product-graph.mjs" + inputs: + - "/.moon/workspace.yml" + - "/.moon/toolchains.yml" + - "/.release-please-manifest.json" + - "/benchmarks/moon.yml" + - "/coverage/**/*" + - "/examples/**/package.json" + - "/moon.yml" + - "/package.json" + - "/pnpm-workspace.yaml" + - "/release-please-config.json" + - "/src/**/package.json" + - "/src/extensions/generated/sdk/rust.json" + - "/src/**/release.toml" + - "/src/**/moon.yml" + - "/src/**/targets/*.toml" + - "/tools/**/moon.yml" + - "/tools/graph/**/*" + - "/tools/policy/check-moon-product-graph.mjs" + - "/tools/release/**/*" + options: + cache: true + runFromWorkspaceRoot: true + prek: + tags: ["policy", "assertion", "quality", "static", "ci-maintainer-tools"] + command: "bash tools/policy/check-prek.sh" + inputs: + - "/.config/nextest.toml" + - "/.lychee.toml" + - "/.markdownlint-cli2.jsonc" + - "/.typos.toml" + - "/biome.json" + - "/deny.toml" + - "/package.json" + - "/pnpm-lock.yaml" + - "/prek.toml" + - "/renovate.json" + - "/rust-toolchain.toml" + - "/src/**/*" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/src/**/.build" + - "!/src/**/.build/**" + - "!/src/**/.gradle/**" + - "!/src/**/.cxx/**" + - "!/src/**/.next/**" + - "!/src/**/.source/**" + - "!/src/**/build/**" + - "!/src/**/out/**" + - "!/src/**/Pods/**" + - "!/src/**/DerivedData/**" + - "/tools/policy/check-prek.sh" + options: + cache: true + runFromWorkspaceRoot: true + test-policy: + tags: ["policy", "assertion", "quality", "static"] + command: "bash tools/policy/check-rust-test-topology.sh" + inputs: + - "/.config/nextest.toml" + - "/Cargo.lock" + - "/Cargo.toml" + - "/src/**/*.rs" + - "/src/**/Cargo.toml" + - "/src/**/moon.yml" + - "/src/**/tools/*.sh" + - "/tools/perf/runner/**/*.rs" + - "/tools/perf/runner/Cargo.toml" + - "/tools/xtask/**/*.rs" + - "/tools/xtask/Cargo.toml" + - "/tools/xtask/moon.yml" + - "/tools/policy/check-rust-test-topology.sh" + options: + cache: true + runFromWorkspaceRoot: true + smoke: + tags: ["runtime", "smoke"] + command: "bash tools/dev/bun.sh examples/tools/check-examples.mjs" + inputs: + - "/examples/**/*" + - "/src/**/*" + - "!/src/**/node_modules" + - "!/src/**/node_modules/**" + - "!/src/**/.build" + - "!/src/**/.build/**" + - "!/src/**/.gradle/**" + - "!/src/**/.cxx/**" + - "!/src/**/.next/**" + - "!/src/**/.source/**" + - "!/src/**/build/**" + - "!/src/**/out/**" + - "!/src/**/Pods/**" + - "!/src/**/DerivedData/**" + - "/tools/**/*" + - "/Cargo.lock" + - "/Cargo.toml" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + - "/rust-toolchain.toml" + options: + cache: local + runFromWorkspaceRoot: true + runInCI: false + test-strategy: + tags: ["policy", "assertion", "quality", "static"] + command: "tools/policy/check-test-strategy.mjs" + inputs: + - "/.moon/**/*" + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/**/moon.yml" + - "/src/**/release.toml" + - "/src/shared/contracts/**/*" + - "/src/shared/fixtures/**/*" + - "/src/sdks/js/package.json" + - "/src/sdks/react-native/package.json" + - "/src/sdks/react-native/src/specs/NativeOliphaunt.ts" + - "/tools/policy/check-test-strategy.mjs" + - "/tools/policy/moon.mjs" + - "/tools/release/release-graph.mjs" + options: + cache: true + runFromWorkspaceRoot: true + package: + tags: ["package"] + command: "bash tools/policy/check-crate-package.sh --allow-dirty" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/src/**/Cargo.toml" + - "/src/**/*.rs" + - "/tools/policy/check-crate-size.sh" + options: + cache: true + runFromWorkspaceRoot: true + runInCI: false + coverage: + tags: ["coverage", "quality"] + command: "tools/coverage/summarize" + deps: + - target: "oliphaunt-rust:coverage" + cacheStrategy: "outputs" + - target: "oliphaunt-swift:coverage" + cacheStrategy: "outputs" + - target: "oliphaunt-kotlin:coverage" + cacheStrategy: "outputs" + - target: "oliphaunt-js:coverage" + cacheStrategy: "outputs" + - target: "oliphaunt-react-native:coverage" + cacheStrategy: "outputs" + - target: "oliphaunt-wasix-rust:coverage" + cacheStrategy: "outputs" + inputs: + - "/coverage/baseline.toml" + - "/tools/coverage/**/*" + outputs: + - "/target/coverage/summary.json" + - "/target/coverage/summary.md" + options: + cache: true + runFromWorkspaceRoot: true + coverage-policy: + tags: ["coverage", "policy"] + command: "bash tools/policy/check-coverage.sh all" + inputs: + - "/coverage/baseline.toml" + - "/src/**/moon.yml" + - "/src/bindings/*/moon.yml" + - "/tools/coverage/**/*" + - "/tools/policy/check-coverage.sh" + options: + cache: true + runFromWorkspaceRoot: true + release-check: + tags: ["release", "package"] + command: "true" + deps: + - "release-tools:check" + options: + cache: local + runFromWorkspaceRoot: true + runInCI: false + bench-run: + tags: ["bench", "measured"] + command: "bash tools/perf/matrix/run_native_oliphaunt_matrix.sh" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/src/shared/fixtures/**/*" + - "/src/runtimes/liboliphaunt/native/**/*" + - "/src/sdks/rust/**/*" + - "/src/postgres/versions/18/**/*" + - "/src/sources/toolchains/**/*" + - "/src/sources/third-party/**/*" + - "/src/extensions/**/*" + - "/tools/perf/**/*" + - "/benchmarks/**/*" + - "/release-please-config.json" + - "/.release-please-manifest.json" + - "/src/**/release.toml" + - "/rust-toolchain.toml" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: false + bench: + tags: ["bench", "plan"] + command: "bash tools/perf/check-native-perf-harness.sh" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/src/runtimes/liboliphaunt/native/**/*" + - "/src/sdks/rust/**/*" + - "/src/bindings/wasix-rust/**/*" + - "/src/postgres/versions/18/**/*" + - "/src/sources/toolchains/**/*" + - "/src/sources/third-party/**/*" + - "/src/extensions/**/*" + - "/tools/perf/**/*" + - "/benchmarks/**/*" + - "/release-please-config.json" + - "/.release-please-manifest.json" + - "/src/**/release.toml" + - "/rust-toolchain.toml" + options: + cache: true + runFromWorkspaceRoot: true + runInCI: false diff --git a/package.json b/package.json new file mode 100644 index 00000000..25195de1 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "oliphaunt-monorepo", + "private": true, + "packageManager": "pnpm@11.5.0", + "engines": { + "node": ">=22.13 <25", + "pnpm": "11.5.0" + }, + "scripts": {} +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..b2ba6273 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,13572 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: false + excludeLinksFromLockfile: false + +catalogs: + default: + '@vitest/coverage-v8': + specifier: ^4.1.8 + version: 4.1.8 + tsx: + specifier: ^4.20.6 + version: 4.22.3 + typedoc: + specifier: ^0.28.16 + version: 0.28.19 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.8 + version: 4.1.8 + +overrides: + esbuild: 0.28.1 + js-yaml: 4.3.0 + postcss: 8.5.15 + uuid: 11.1.1 + +importers: + + .: {} + + src/bindings/wasix-rust/examples/tauri-sqlx-vanilla: + dependencies: + '@tauri-apps/api': + specifier: ^2 + version: 2.11.0 + '@tauri-apps/plugin-opener': + specifier: ^2 + version: 2.5.4 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.11.2 + typescript: + specifier: ~5.6.2 + version: 5.6.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/docs: + dependencies: + '@mdx-js/react': + specifier: ^3.1.0 + version: 3.1.1(@types/react@19.2.16)(react@19.2.7) + '@oliphaunt/react-native': + specifier: workspace:* + version: link:../sdks/react-native + '@oliphaunt/ts': + specifier: workspace:* + version: link:../sdks/js + clsx: + specifier: ^2.1.1 + version: 2.1.1 + fumadocs-core: + specifier: 16.9.3 + version: 16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + fumadocs-mdx: + specifier: 15.0.10 + version: 15.0.10(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.16)(fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + fumadocs-ui: + specifier: 16.9.3 + version: 16.9.3(@tailwindcss/oxide@4.3.0)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.0) + lucide-react: + specifier: ^1.17.0 + version: 1.17.0(react@19.2.7) + next: + specifier: 16.2.7 + version: 16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + smol-toml: + specifier: ^1.4.2 + version: 1.6.1 + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + devDependencies: + '@biomejs/biome': + specifier: ^2.4.16 + version: 2.4.16 + '@tailwindcss/postcss': + specifier: ^4.3.0 + version: 4.3.0 + '@types/mdx': + specifier: ^2.0.13 + version: 2.0.13 + '@types/node': + specifier: 22.19.19 + version: 22.19.19 + '@types/react': + specifier: 19.2.16 + version: 19.2.16 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.16) + postcss: + specifier: 8.5.15 + version: 8.5.15 + tailwindcss: + specifier: 4.3.0 + version: 4.3.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + src/runtimes/broker/packages/darwin-arm64: {} + + src/runtimes/broker/packages/linux-arm64-gnu: {} + + src/runtimes/broker/packages/linux-x64-gnu: {} + + src/runtimes/broker/packages/win32-x64-msvc: {} + + src/runtimes/liboliphaunt/native/icu-npm: {} + + src/runtimes/liboliphaunt/native/packages/darwin-arm64: {} + + src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu: {} + + src/runtimes/liboliphaunt/native/packages/linux-x64-gnu: {} + + 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: + specifier: 1.9.0 + version: 1.9.0 + + src/runtimes/node-direct/packages/darwin-arm64: {} + + src/runtimes/node-direct/packages/linux-arm64-gnu: {} + + src/runtimes/node-direct/packages/linux-x64-gnu: {} + + src/runtimes/node-direct/packages/win32-x64-msvc: {} + + src/sdks/js: + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.12.4 + '@vitest/coverage-v8': + specifier: 'catalog:' + version: 4.1.8(vitest@4.1.8) + jsr: + specifier: ^0.14.3 + version: 0.14.3 + tsx: + specifier: 'catalog:' + version: 4.22.3 + typedoc: + specifier: 'catalog:' + version: 0.28.19(typescript@6.0.3) + typescript: + specifier: 'catalog:' + version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@types/node@24.12.4)(@vitest/coverage-v8@4.1.8)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + optionalDependencies: + '@oliphaunt/broker-darwin-arm64': + specifier: workspace:0.0.0 + version: link:../../runtimes/broker/packages/darwin-arm64 + '@oliphaunt/broker-linux-arm64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/broker/packages/linux-arm64-gnu + '@oliphaunt/broker-linux-x64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/broker/packages/linux-x64-gnu + '@oliphaunt/broker-win32-x64-msvc': + specifier: workspace:0.0.0 + version: link:../../runtimes/broker/packages/win32-x64-msvc + '@oliphaunt/liboliphaunt-darwin-arm64': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/packages/darwin-arm64 + '@oliphaunt/liboliphaunt-linux-arm64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/packages/linux-arm64-gnu + '@oliphaunt/liboliphaunt-linux-x64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/packages/linux-x64-gnu + '@oliphaunt/liboliphaunt-win32-x64-msvc': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/packages/win32-x64-msvc + '@oliphaunt/node-direct-darwin-arm64': + specifier: workspace:0.0.0 + version: link:../../runtimes/node-direct/packages/darwin-arm64 + '@oliphaunt/node-direct-linux-arm64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/node-direct/packages/linux-arm64-gnu + '@oliphaunt/node-direct-linux-x64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/node-direct/packages/linux-x64-gnu + '@oliphaunt/node-direct-win32-x64-msvc': + specifier: workspace:0.0.0 + version: link:../../runtimes/node-direct/packages/win32-x64-msvc + '@oliphaunt/tools-darwin-arm64': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/darwin-arm64 + '@oliphaunt/tools-linux-arm64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu + '@oliphaunt/tools-linux-x64-gnu': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu + '@oliphaunt/tools-win32-x64-msvc': + specifier: workspace:0.0.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc + + src/sdks/react-native: + devDependencies: + '@react-native/codegen': + specifier: ^0.85.3 + version: 0.85.3 + '@react-native/typescript-config': + specifier: ^0.85.0 + version: 0.85.3 + '@types/node': + specifier: ^24.10.1 + version: 24.12.4 + '@vitest/coverage-v8': + specifier: 'catalog:' + version: 4.1.8(vitest@4.1.8) + react: + specifier: ^19.2.0 + version: 19.2.3 + react-native: + specifier: ^0.85.0 + version: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + tsx: + specifier: 'catalog:' + version: 4.22.3 + typedoc: + specifier: 'catalog:' + version: 0.28.19(typescript@6.0.3) + typescript: + specifier: 'catalog:' + version: 6.0.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@types/node@24.12.4)(@vitest/coverage-v8@4.1.8)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + + src/sdks/react-native/examples/expo: + dependencies: + '@expo/ui': + specifier: ~56.0.21 + version: 56.0.21(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@oliphaunt/react-native': + specifier: workspace:* + version: link:../.. + expo: + specifier: ~56.0.15 + version: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: + specifier: ~56.0.20 + version: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-dev-client: + specifier: ~56.0.22 + version: 56.0.22(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-device: + specifier: ~56.0.4 + version: 56.0.4(expo@56.0.15) + expo-font: + specifier: ~56.0.7 + version: 56.0.7(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-glass-effect: + specifier: ~56.0.4 + version: 56.0.4(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-image: + specifier: ~56.0.11 + version: 56.0.11(expo@56.0.15)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-linking: + specifier: ~56.0.15 + version: 56.0.15(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-router: + specifier: ~56.2.14 + version: 56.2.14(b629021f94eaa4102b0df896a774cd24) + expo-splash-screen: + specifier: ~56.0.12 + version: 56.0.12(expo@56.0.15)(typescript@6.0.3) + expo-sqlite: + specifier: ~56.0.5 + version: 56.0.5(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-status-bar: + specifier: ~56.0.4 + version: 56.0.4(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-symbols: + specifier: ~56.0.6 + version: 56.0.6(expo-font@56.0.7)(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-system-ui: + specifier: ~56.0.5 + version: 56.0.5(expo@56.0.15)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-web-browser: + specifier: ~56.0.5 + version: 56.0.5(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + react-native: + specifier: 0.85.3 + version: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + react-native-gesture-handler: + specifier: ~2.31.1 + version: 2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-reanimated: + specifier: 4.3.1 + version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-safe-area-context: + specifier: ~5.7.0 + version: 5.7.0(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-screens: + specifier: 4.25.2 + version: 4.25.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-web: + specifier: ~0.21.0 + version: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-native-worklets: + specifier: 0.8.3 + version: 0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + devDependencies: + '@react-native/metro-config': + specifier: 0.85.3 + version: 0.85.3 + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@types/react': + specifier: 19.2.16 + version: 19.2.16 + eslint: + specifier: ^9.0.0 + version: 9.39.4(jiti@2.7.0) + eslint-config-expo: + specifier: ~56.0.4 + version: 56.0.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + expo-doctor: + specifier: ^1.19.7 + version: 1.19.8 + expo-mcp: + specifier: ~0.2.1 + version: 0.2.4 + typescript: + specifier: ~6.0.3 + version: 6.0.3 + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-export-default-from@7.27.1': + resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-default-from@7.28.6': + resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.28.6': + resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.27.1': + resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.7': + resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@biomejs/biome@2.4.16': + resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.4.16': + resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.4.16': + resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.4.16': + resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.4.16': + resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.4.16': + resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.4.16': + resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.4.16': + resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.4.16': + resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@egjs/hammerjs@2.0.17': + resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} + engines: {node: '>=0.8.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@expo-google-fonts/material-symbols@0.4.38': + resolution: {integrity: sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==} + + '@expo/cli@56.1.19': + resolution: {integrity: sha512-k+oipwbu83WFnACtybvGZvDbbItarY4lfjkKgP9F5lml2ilvL/VvzsPhbhT7nRyFB3zPJq9SbNNKa45VDLoihA==} + hasBin: true + peerDependencies: + expo: '*' + expo-router: '*' + react-native: '*' + peerDependenciesMeta: + expo-router: + optional: true + react-native: + optional: true + + '@expo/code-signing-certificates@0.0.6': + resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} + + '@expo/config-plugins@56.0.12': + resolution: {integrity: sha512-UKjPEOkvxBzTvjghmjUECURDoJLPJIFIevB0JQCe8l9Fg4yfy2fabU5LU3Kfrmmu1/8et/93bucCnABEmoxYEw==} + + '@expo/config-types@56.0.7': + resolution: {integrity: sha512-V7bxawNsNned/yMppAHdisIOxniZXgPKRWpIUiQOQBs45/A5MBd2gfDM4Ecq5gnbilnQUTaI6Zxn6JcW7L3TAA==} + + '@expo/config@56.0.11': + resolution: {integrity: sha512-Rt3U9Rqr6midz/sDeubFN5fefR0KzHpcEEgAUnV98XsLSSTnfzIxoC1blICb7pUUscKc8TtlVyn7/Gita2wnOQ==} + + '@expo/devcert@1.2.1': + resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==} + + '@expo/devtools@56.0.2': + resolution: {integrity: sha512-ANl4kPdbe0/HQYWkDEN79S6bQhI+i/ZCnPxuC853pPsB4svhINC7Ku9lmGOKPsUUWWnrHg1spkDGQBZ4sD6JxQ==} + peerDependencies: + react: '*' + react-native: '*' + peerDependenciesMeta: + react: + optional: true + react-native: + optional: true + + '@expo/dom-webview@56.0.6': + resolution: {integrity: sha512-DQY3Tj5nrPbpIfEQiD9xbyEFTu25yEORa02Eyzl5rXszDzxDT4VkZDHkyErNQOyE1qjublJHNFhE5bDm4tRfqA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + '@expo/env@2.3.1': + resolution: {integrity: sha512-JvBdZa5OkTd+dGEtt3fLW9OF6RlIp9SNu9VyMSiWPV5szMDTSAYbX8Uj3cRvZcU1zzaRbqnTSsHk2AE8WXHfxQ==} + engines: {node: '>=20.12.0'} + + '@expo/env@2.4.1': + resolution: {integrity: sha512-3c9Mg9x0HmGPEsVrGAGyEDJsNUOZ55cZvZ47/HLmXh7MHV9Zv7My73wThklKrObaBBoMfE4YqpKjYKDRzojpjQ==} + engines: {node: '>=20.12.0'} + + '@expo/expo-modules-macros-plugin@0.2.2': + resolution: {integrity: sha512-4IMzPDIo/VOXREQjsJtliSfqYVZvfzU2SLFS/9sKMWF848S8CHx+e/E+Vf0TcMvpWCCKX5umyqxb13KJJ+YUzg==} + + '@expo/fingerprint@0.19.7': + resolution: {integrity: sha512-Q04NyJE0E7qKGXepBjI8e0p983RrQGBWJcSICKyyLczsr5JhNuSmqw604aL7koaXG2ctrUL36qd332XiMS/s6w==} + hasBin: true + + '@expo/image-utils@0.10.2': + resolution: {integrity: sha512-qQUGaacqXduoFTCUQAMceIWYzlKU0xX4/BKTyh8TdVj0uHv9/W3MfHOy5yXoOqBVrSccXk++Gm4D/NfS5HnCNA==} + + '@expo/inline-modules@0.0.13': + resolution: {integrity: sha512-26RllWesRmYsAAo70cRcR9DaqXPKJct9MIGxZteS+Tkg25ljOkFeG7fYEsSazZOLpAslKBiilqtZykVYrxSzCw==} + + '@expo/json-file@10.2.0': + resolution: {integrity: sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==} + + '@expo/json-file@11.0.0': + resolution: {integrity: sha512-pHJCETqFL5x5BzNV6cEPwjwuECgGmnl0bNmfHIJ6LM1tlh2eVXi5HEdit3zby/JO/B8Otk5cgcqtJXgvvUat3A==} + + '@expo/local-build-cache-provider@56.0.9': + resolution: {integrity: sha512-VMJC5ul7dXPfD2OO16ObPi6ym4H8vlnCBUuCM/KeKh0OvLRub35X8OB2uYrFQ+vbWkPemAEOWvk0oOptpPxbzA==} + + '@expo/log-box@56.0.14': + resolution: {integrity: sha512-3Eadcxz0J2NESG2iKe8DnAggsRCWqY0mBLhgQPNzClPVjS6o4NyD0F8kuOvWNngFwpJBC08HhY2o8P1mgSgeJg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + '@expo/mcp-tunnel@0.2.4': + resolution: {integrity: sha512-hxFzqdUNKCt+8pbGV3oGcd/aBNA1mmhwh3DSeXoHReypxzsiLYLITJs1OctglaPecfMA9qFb+6z/RIkRSf5S4g==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.26.0 + + '@expo/metro-config@56.0.16': + resolution: {integrity: sha512-mxPZ23exC6kkEpPYQOteamaiWYER3uDk2IqKys7EJwtIKc3F1207Xtsdko/DNNG04DLwpN/WM2UlNl+Ak8uPRg==} + peerDependencies: + expo: '*' + peerDependenciesMeta: + expo: + optional: true + + '@expo/metro-file-map@56.0.3': + resolution: {integrity: sha512-5OGW3z8LgEYgMJOR7F3pC8llFLkb1fVqwAewbCl6S4Vkha8AFQMwOjT+9Wbka+V4rmpljpGqOnMhF4xZbD961w==} + + '@expo/metro-runtime@56.0.16': + resolution: {integrity: sha512-UsKrBLOCa+kAVHW1S0HdGkM0hkiQXeh3fZIpjcriOmoNg/CVRgdKQhUxUkNoXAoPfT85uW1x2fzNbiYhPQO1SA==} + peerDependencies: + expo: '*' + react: '*' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + + '@expo/metro@56.0.0': + resolution: {integrity: sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==} + + '@expo/osascript@2.6.0': + resolution: {integrity: sha512-QvqDBlJXa8CS2vRORJ4wEflY1m0vVI07uSJdIRgBrLxRPBcsrXxrtU7+wXRXMqfq9zLwNP9XbvRsXF2omoDylg==} + engines: {node: '>=12'} + + '@expo/package-manager@1.13.0': + resolution: {integrity: sha512-s3W3eZafJDEyVL7W/jxj2Nz3eONKxSCU604S5xj8ijrVaRz83x0DnZznLf/UXQEI1w+FyibH68nHeQyk767b1A==} + + '@expo/plist@0.7.0': + resolution: {integrity: sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q==} + + '@expo/prebuild-config@56.0.19': + resolution: {integrity: sha512-aP/7kGDPMmwY9C9cYMK4MfUiZk23KhN4AfnKz0eeRyg3Izw0OGdtzNDLIcchnqpDAwHsI0+LUHZhucGl1jQJ5A==} + + '@expo/require-utils@56.1.4': + resolution: {integrity: sha512-IX7XXg9obnrH3ni0TClBbVKgqk0nT0bjTEPTBtbD+WgIeZ0hpcgwxMJH3j5uBsUqEAq1jqUAkJWrPcg7ZRSV7g==} + peerDependencies: + typescript: ^5.0.0 || ^5.0.0-0 || ^6.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@expo/router-server@56.0.16': + resolution: {integrity: sha512-df5BI5GFnSKqAhuALTPIbuHisd4CUhuYam7pwF9Dt1D6BinPctsteWWDm/sEGXdLVhsub4NPI7tvUHo/HWL7aA==} + peerDependencies: + '@expo/metro-runtime': ^56.0.16 + expo: '*' + expo-constants: ^56.0.20 + expo-font: ^56.0.7 + expo-router: '*' + expo-server: ^56.0.5 + react: '*' + react-dom: '*' + react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 + peerDependenciesMeta: + '@expo/metro-runtime': + optional: true + expo-router: + optional: true + react-dom: + optional: true + react-server-dom-webpack: + optional: true + + '@expo/schema-utils@56.0.2': + resolution: {integrity: sha512-WcOH1E6rwxRqNwBzPOVhd5GBbMlS04+5n+ZMdhdwKOg/Kt3suV+F8F6An+z8mUJ8eSuMM3HD9Sj09t7vc/Xjkw==} + + '@expo/sdk-runtime-versions@1.0.0': + resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==} + + '@expo/spawn-async@1.8.0': + resolution: {integrity: sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==} + engines: {node: '>=12'} + + '@expo/sudo-prompt@9.3.2': + resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} + + '@expo/ui@56.0.21': + resolution: {integrity: sha512-DosrgEiDqxc/tPH39/Nf5BmVxdeJ616xk/ihu7CJ7Xuzkr79E6/fp4IA15KzBiTdJeH3id1adhIwj8RX1jnh6A==} + peerDependencies: + '@babel/core': '*' + expo: '*' + react: '*' + react-dom: '*' + react-native: '*' + react-native-worklets: '*' + peerDependenciesMeta: + '@babel/core': + optional: true + react-dom: + optional: true + react-native-worklets: + optional: true + + '@expo/ws-tunnel@2.0.0': + resolution: {integrity: sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==} + peerDependencies: + ws: ^8.0.0 + + '@expo/xcpretty@4.4.4': + resolution: {integrity: sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==} + hasBin: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fumadocs/tailwind@0.0.5': + resolution: {integrity: sha512-ENKPWUDRmriccsrUDE4bDBq3FNr/ms3BP2rWlsAEMV1yP23pcCaan+ceGfeBUsAQjw7sj9Q3R4Kl3g/TCStPzQ==} + peerDependencies: + '@tailwindcss/oxide': ^4.0.0 + tailwindcss: ^4.0.0 + peerDependenciesMeta: + '@tailwindcss/oxide': + optional: true + tailwindcss: + optional: true + + '@gerrit0/mini-shiki@3.23.0': + resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + + '@isaacs/ttlcache@1.4.1': + resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} + engines: {node: '>=12'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@next/env@16.2.7': + resolution: {integrity: sha512-tMJizPlj6ZYpBMMdK8S0LJufrP4QTdR6pcv9KQ/bVETPAmg0j1mlHE9G2c38UyGHxoBapgwuj7XjbGJ2RcDFOg==} + + '@next/swc-darwin-arm64@16.2.7': + resolution: {integrity: sha512-vm1EDI/pVaBNNiychmxk3fft+OhQPVD9cIM/tReLZIQ3TfQ4kqI9DwKk00dzuS1ulC7icbrzCFrmRRlk9PfNdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.2.7': + resolution: {integrity: sha512-O3IRSv1ZBL1zs0WrIgefTEcTKFVn+ryxBNe54erJ6KsD+2f/Mmt7g2jOYh8PSBdUwPtKQJuCsTMlZ7tIu2AcsQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.2.7': + resolution: {integrity: sha512-Re6PZtjBDd0aMU+VcZcC/PrIvj4WhrjDYtMhhCVQamWN4L90EVP0pcEOBQD25prSlw7OzNw5QpHLWMilRLsRNw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-arm64-musl@16.2.7': + resolution: {integrity: sha512-qyogG9QtBzWxgJfeGBvOEHI3851gTfCF3wLZ5RDLTBJGAmE9p1qDwKCOdrBrvBzRvYDT+gUDp72pzlSEfAXgNA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@next/swc-linux-x64-gnu@16.2.7': + resolution: {integrity: sha512-Vhe4ZDuBpmMogrGi5D4R2Kq4JAQlj6+wvgaFYy31zfES0zPmt6TLA+cuYpM/OLrPZjo2MYQTHVqNUSCR6+fDZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@next/swc-linux-x64-musl@16.2.7': + resolution: {integrity: sha512-srvian89JahFLw1YLBEuhvPJ0DO5lpUeJQMXy4xYo7g628ZlNgXdNkqoxSAv9OYrBfByh6vxISMwW/mRbzCY+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@next/swc-win32-arm64-msvc@16.2.7': + resolution: {integrity: sha512-GX3wvLpULFuRFJzwHaKfm7QZJ18F4ZSuxlPJ96BoBglCzBmdSjyeBKF+ZhWhvL/ckxNfLnNa7bsObO2ipYpszw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.2.7': + resolution: {integrity: sha512-J4WlM72NMk076Qsg0jTdK3SNXatlSdnjW7L7oNGLst1tAGjHrJh/FYi+pw9wyIjEtGRKDNzD0zuiY16oWYWVaw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@orama/orama@3.1.18': + resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} + engines: {node: '>= 20.0.0'} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@react-native-masked-view/masked-view@0.3.2': + resolution: {integrity: sha512-XwuQoW7/GEgWRMovOQtX3A4PrXhyaZm0lVUiY8qJDvdngjLms9Cpdck6SmGAUNqQwcj2EadHC1HwL0bEyoa/SQ==} + peerDependencies: + react: '>=16' + react-native: '>=0.57' + + '@react-native/assets-registry@0.85.3': + resolution: {integrity: sha512-u9ZiYP23vA2IFtdFQFmetzSmk6SM0xgKIoiOsr1hXNHjHaLhOm+/Ph1ud57wX6+Dbwdzx8coJgnzSKL3W21PCg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/babel-plugin-codegen@0.85.3': + resolution: {integrity: sha512-Wc94zGfeFG8Njf9SHMPfYZP04kjigkOps6F1TYTvd7ZVXuGxqseCDgxc50LWcOhOCLypI9n3oVVqz81C3p44ZA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/babel-preset@0.85.3': + resolution: {integrity: sha512-fD7fxEhkJB/aF57tWoXjaAWpklfrExYZS3k6aXPP3BQ77DZY7gvf/b7dbirwjID6NVnP1JDRJyTuPBGr0K/vlw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/codegen@0.85.3': + resolution: {integrity: sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/community-cli-plugin@0.85.3': + resolution: {integrity: sha512-fs85dmbIqNmtzEixDb0g+q6R3Vt4H9eAt8/inIZdDKfjN76+sUJA2r1nxODQ76bU23MrIbz8sI7KFBPaWk/zQw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@react-native-community/cli': '*' + '@react-native/metro-config': 0.85.3 + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/metro-config': + optional: true + + '@react-native/debugger-frontend@0.85.3': + resolution: {integrity: sha512-uAu7rM5o/Np1zgp6fi5zM1sP1aB8DcS7DdOLcj/TkSutOAjkMqqd2lWt1/+3S7qXexRHVK5XcP+o3VXo4L/V0A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/debugger-shell@0.85.3': + resolution: {integrity: sha512-/jRAaT9boiCttIcEwS02WPwYkUihqsjSaK/TMtHz05vT6uMgac9PaQt5kzBQLIABv5aEIa5gtrMmKVz49MjkjQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/dev-middleware@0.85.3': + resolution: {integrity: sha512-JYzBiT4A8w+KQt+dOD5v+ti+tDrGoPnsSTuApq3Ls4RB5sfWbDlYMyz3dbc8qBIHz9tv0sQ5+eOu6Xwqzr5AQA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/gradle-plugin@0.85.3': + resolution: {integrity: sha512-39dY2j50Q1pntejzwt3XL7vwXtrj8jcIfHq6E+gyu3jzYxZJVvMkMutQ39vSg6zinIQOX36oQDhidXUbCXzgoA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/js-polyfills@0.85.3': + resolution: {integrity: sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/metro-babel-transformer@0.85.3': + resolution: {integrity: sha512-omuKq+r7jM4XvCMIlNMPP7Up3SyB8o5EAdZtF7YXniKyq7UOMBqhYHFqgsdOXr0lT+3ADf7VCJG3sb82jlBrrQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/metro-config@0.85.3': + resolution: {integrity: sha512-sVo6HepUmCcpdfozEf91lA0FjpLNNZYu/Zi9FiYiAQTK8pzATXDVTqhvdxpFrQn435p5eUTSbllvbH/KN+bnyA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/normalize-colors@0.74.89': + resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==} + + '@react-native/normalize-colors@0.85.3': + resolution: {integrity: sha512-hj0PScZEhIbcOvQV5yMKX3ha4XEIOy/SVE1Rrpp0beW0dpNLOgSC7KDxGewmDnIHK9YdQUXGY9eMEfShUMIaZw==} + + '@react-native/typescript-config@0.85.3': + resolution: {integrity: sha512-F2Ign3lv/99R5HMDiaQE6NpRdopn87VuXgfHABSk0iwzouLFk1fcwaMkJUmjhnxrQagsUwxOWp4WTPwEvRRazQ==} + + '@react-native/virtualized-lists@0.85.3': + resolution: {integrity: sha512-dsCjI//OIPEUJMyNHp4l7zNLVjCx7bcaRUceOCkU+IB17hkbtbGWvi7HjGFSzy7FJGmS/MOlcfpb72xXiy1Oig==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + peerDependencies: + '@types/react': ^19.2.0 + react: '*' + react-native: 0.85.3 + peerDependenciesMeta: + '@types/react': + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@shikijs/core@4.1.0': + resolution: {integrity: sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.1.0': + resolution: {integrity: sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/engine-oniguruma@4.1.0': + resolution: {integrity: sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==} + engines: {node: '>=20'} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/langs@4.1.0': + resolution: {integrity: sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.1.0': + resolution: {integrity: sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==} + engines: {node: '>=20'} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/themes@4.1.0': + resolution: {integrity: sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==} + engines: {node: '>=20'} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/types@4.1.0': + resolution: {integrity: sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@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==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.0': + resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==} + + '@tauri-apps/api@2.11.0': + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} + engines: {node: '>= 10'} + hasBin: true + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} + + '@types/node@24.12.4': + resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-test-renderer@19.1.0': + resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} + + '@types/react@19.2.16': + resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@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} + peerDependencies: + '@typescript-eslint/parser': ^8.59.4 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/coverage-v8@4.1.8': + resolution: {integrity: sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==} + peerDependencies: + '@vitest/browser': 4.1.8 + vitest: 4.1.8 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agent-cli-detector@0.1.2: + resolution: {integrity: sha512-qdZ/9JFORtTKJNhT/IczMeEfEUbUU0K5umYeiIQHX+AjHs+Y9SXVzSgaYlpZeyNMrvuh2HpZiOTpvS57iPfBkQ==} + engines: {node: '>=18.18'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + anser@1.4.10: + resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.3: + resolution: {integrity: sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + babel-plugin-react-native-web@0.21.2: + resolution: {integrity: sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==} + + babel-plugin-syntax-hermes-parser@0.33.3: + resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==} + + babel-plugin-transform-flow-enums@0.0.2: + resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} + + babel-preset-expo@56.0.17: + resolution: {integrity: sha512-yanAlbzaMMNqnju/uMnZsf3ltgssG71UubkfD+2iMaoXGNIj+TODEs9hF12jrTX2lzq/zH0HWSuRLZLNalf8fw==} + peerDependencies: + '@babel/runtime': ^7.20.0 + expo: '*' + expo-widgets: ^56.0.22 + react-refresh: '>=0.14.0 <1.0.0' + peerDependenciesMeta: + '@babel/runtime': + optional: true + expo: + optional: true + expo-widgets: + optional: true + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + engines: {node: '>=6.0.0'} + hasBin: true + + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bplist-creator@0.1.0: + resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} + + bplist-parser@0.3.1: + resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==} + engines: {node: '>= 5.10.0'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chrome-launcher@0.15.2: + resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} + engines: {node: '>=12.13.0'} + hasBin: true + + chromium-edge-launcher@0.3.0: + resolution: {integrity: sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@2.1.0: + resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} + engines: {node: '>=4'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + compute-scroll-into-view@3.1.1: + resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dnssd-advertise@1.1.6: + resolution: {integrity: sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-expo@56.0.4: + resolution: {integrity: sha512-1OD7rJMxCchKHxq+U+OQsAxVtzAxeUb9875g6+15KsSD9fqKTgq7DEEWYwunzU9r9E8kYJ+mh7+j86vF9m9NMw==} + peerDependencies: + eslint: '>=8.10' + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-expo@1.0.3: + resolution: {integrity: sha512-C1v9NPvpDET36+7Klpp/+53Jl+VzOfpbDxpKtL/pAPhCDwTX0kW6Swo425PT0uc4AMT5jpQbB7hSKFjKOGMl4A==} + engines: {node: '>=18.0.0'} + peerDependencies: + eslint: '>=8.10' + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.5.0: + resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + expo-asset@56.0.19: + resolution: {integrity: sha512-huGY0bVfYUivNOir+iUEjjW9IbNHzLFNo8d6FGh22u1OsXcFR7Za3vpu5V1gMp0dc31wiqmsXkTazjm+Rro3vA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-constants@56.0.20: + resolution: {integrity: sha512-4HgoVvUiMvcqujr/CUj7L1tumLWj8WX0PLM77OriDPoaJrMEwUHd841m4o4IoUyMPVT8XTiTiPFwJtGali8+9Q==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-dev-client@56.0.22: + resolution: {integrity: sha512-LKhgIlMu8DkmhTtfurpX9YMPsnv4QI1hCcDZqcFn+jlB+s6yE1THB9UWZ+BVtef8kYMzsw+BH3ClXvNkRd0vXw==} + peerDependencies: + expo: '*' + + expo-dev-launcher@56.0.23: + resolution: {integrity: sha512-OKgLAn6m15Lu+Qyg8WA0HPurFR9pxhsrHUvvafv8pTZHspB0BbOvBwhOJdxgiMfR2LHq4032cnMf8164Vzqeng==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-dev-menu-interface@56.0.1: + resolution: {integrity: sha512-odATx0ZL/Kis10sKSBiKiGQxAB6coSi/KQtKcMhnQVNno6FkRh5/4e5BqcEvpq2rNMTiQp4ytNAQHtdwbPXvGA==} + peerDependencies: + expo: '*' + + expo-dev-menu@56.0.19: + resolution: {integrity: sha512-sm6dfnkq3lgbn39Kd4yjFFm7UQAZqm6Hn3OpjagGtWBoWByQae3nKh1MF5K4Peh172TqUPJTOhjRasnZSkUkOQ==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-device@56.0.4: + resolution: {integrity: sha512-ucVcGPkvBrl2QHuy7XcYex2Y6BETvJ6TREutZrwLGUDnlvbpKS8KfQoNZOpvkyo5Nmm9RrasYQ0CrXmBHho2mg==} + peerDependencies: + expo: '*' + + expo-doctor@1.19.8: + resolution: {integrity: sha512-ZHpQM+BfJe1DNaA+/ObtLYazC2x78tIV3kkfoSGR46Tj1EvzOFh1p9gFHsILI9TOyXPEcgxCkFw9AVvf8C4c1g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + + expo-file-system@56.0.8: + resolution: {integrity: sha512-NrH41/8snGIBSbYicwVLB4txPdgCATd7ZYhMAGS3YJZ9GbnduhlAoV4/YCbGayjrbpE9bJb/6wegPL/zmvRMnQ==} + peerDependencies: + expo: '*' + react-native: '*' + + expo-font@56.0.7: + resolution: {integrity: sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-glass-effect@56.0.4: + resolution: {integrity: sha512-xI9rXtDwi7RW82uAlfyaXO6+k21ApWJ2tHAWYqPr/FjfmZbKsgNJ4Q0iZzGPCwboqjTGxaRZ61SZxBl8hDt5iA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-image@56.0.11: + resolution: {integrity: sha512-k2xwxGk14xi6zxmEGAU4rUTb1lK5qf0y0Qb8+Jaggnul0KaJJxcq9qvyDp9iyJBW35cp9isONAUnNtIiooZ/Pw==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + + expo-json-utils@56.0.0: + resolution: {integrity: sha512-lUqyv9aIGDbYTQ5Nux2FnH2/Dz0w5uJ8Pr080eS0StXi2jr5OmuMNErpzUnpfnYOU55xKotd4AHv68PfV/ludg==} + + expo-keep-awake@56.0.3: + resolution: {integrity: sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA==} + peerDependencies: + expo: '*' + react: '*' + + expo-linking@56.0.15: + resolution: {integrity: sha512-V3tkCxHIWO624ayw2L5Nfi0/bPgYy/0MgmoQR7g3FAKF4nyAayK8IhgAInz4MZ5Ao5Bb+5uZWDdUaS4yyEb+Gw==} + peerDependencies: + react: '*' + react-native: '*' + + expo-manifests@56.0.4: + resolution: {integrity: sha512-Fokawl2UkiExIF0bqGoblRFA8lYpROVD+EpvDwSW4LgqQyPwNua1gLSgHZjdl5GsVugfRMMWE3LHaibDyX93hw==} + peerDependencies: + expo: '*' + + expo-mcp@0.2.4: + resolution: {integrity: sha512-rBomlm+085wNa+UF9YC3bXGZR6LlYPfOlUXwKBB5R7+dnASk0VjWFETuxyApdtXw9OItmOsAXolUxrAlEYfqSA==} + hasBin: true + + expo-modules-autolinking@56.0.19: + resolution: {integrity: sha512-ztzTzS21fbq4oJQItSgT+WWJMZXGsC0GdX/cDN/GBhCTPiZz8foHkVa14DiLa5CWd7pEZ9EeSMdHDcAEc2dZ5Q==} + hasBin: true + + expo-modules-core@56.0.20: + resolution: {integrity: sha512-Xagwt/gC6sV1jiSnlFrYxLooZr90adLrpV64YlkXZjgZ6Kot89FF62e457RyejLtyDm2pOdy6FZphF1ReyK2Jw==} + peerDependencies: + react: '*' + react-native: '*' + react-native-worklets: ^0.7.4 || ^0.8.0 + peerDependenciesMeta: + react-native-worklets: + optional: true + + expo-modules-jsi@56.0.12: + resolution: {integrity: sha512-OnXiNbXzYybZPh5QnJyIIwQjQfN7PP4Di/2Hz0xQHKLgQmCfn/zAQziOjbUXVeL3cAyZ8+uDnTDYmDAc3B2qhQ==} + peerDependencies: + react-native: '*' + + expo-router@56.2.14: + resolution: {integrity: sha512-zhJ0YgMxzosxPVTqPJTzTwOCvY06+uFJSW93vFYpF+AyMSZ/DCT+iQew0bwGky6dmWrrIjLBbZ/bv/CdaEdAEg==} + peerDependencies: + '@testing-library/react-native': '>= 13.2.0' + expo: '*' + expo-constants: ^56.0.20 + expo-linking: ^56.0.15 + react: '*' + react-dom: '*' + react-native: '*' + react-native-gesture-handler: '*' + react-native-reanimated: '*' + react-native-safe-area-context: '>= 5.4.0' + react-native-screens: ^4.25.2 + react-native-web: '*' + react-server-dom-webpack: ~19.0.4 || ~19.1.5 || ~19.2.4 + peerDependenciesMeta: + '@testing-library/react-native': + optional: true + react-dom: + optional: true + react-native-gesture-handler: + optional: true + react-native-reanimated: + optional: true + react-native-web: + optional: true + react-server-dom-webpack: + optional: true + + expo-server@56.0.5: + resolution: {integrity: sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ==} + engines: {node: '>=20.16.0'} + + expo-splash-screen@56.0.12: + resolution: {integrity: sha512-RvEtJ78aNpuxdVD8TCe9viKtlYGzS9hW20fbvqvkeXDN6WAaYo1CRitPdjWNkaNNSPKOolp3zfha4EGdhXOtxg==} + peerDependencies: + expo: '*' + + expo-sqlite@56.0.5: + resolution: {integrity: sha512-wHYRVLS5nUFEtli45wHaO+RjlRY8sQXyOSgENVk6I4zq7+FgySqjOk3YOYW6IKIMwhj5XzjMJO+pY8xKUy73Kw==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-status-bar@56.0.4: + resolution: {integrity: sha512-IGs/fDfkHXofy2ZQrGiXayhFK04HB85FZXorhcEhDZEcqASKgSqpak+HwUtAaR0MeTJwWyHNF7I6VmVbbp8EcA==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + + expo-symbols@56.0.6: + resolution: {integrity: sha512-BrA81DjcNafdj7gXVhdrExb9LtUiSVyOf/NavyMmDAHgHMY1GqeR5cnn1PSAZeYKnSgQhee/H89XUpAxtog5hg==} + peerDependencies: + expo: '*' + expo-font: '*' + react: '*' + react-native: '*' + + expo-system-ui@56.0.5: + resolution: {integrity: sha512-n1MmnUArV4cc3gVed9fGtluPme00PE9axKVx+NHbKxHFMam5l4GcOI7PxbYKFNx8o7WA1LRD7eLW33agmZrxGg==} + peerDependencies: + expo: '*' + react-native: '*' + react-native-web: '*' + peerDependenciesMeta: + react-native-web: + optional: true + + expo-updates-interface@56.0.2: + resolution: {integrity: sha512-eWTwSZ9y8vrULG2oBn2TQSSIwBGSq/TxGJ3jY6tuVS2FWH/ASRIiKs3zkUZTRoC3ZuV2alz0mUClYV7nNrFx8g==} + peerDependencies: + expo: '*' + + expo-web-browser@56.0.5: + resolution: {integrity: sha512-kaN+wcR5lHwPCH1IgrU1XyPUQvBRzdF1TMp65uAF9iUCyipqYnmrvV87eqAmrdkFFopWVgU7FcxPu1UZw+gvUQ==} + peerDependencies: + expo: '*' + react-native: '*' + + expo@56.0.15: + resolution: {integrity: sha512-Tnas9Sq1fDY865rhSQ4266Kd4GULEUyBEBEchbNLQsiY6U+AAt4MgCKJUsRvUkNHdaZwnGzexy6hdnOjsgvNcA==} + hasBin: true + peerDependencies: + '@expo/metro-runtime': '*' + react: '*' + react-dom: '*' + react-native: '*' + react-native-web: '*' + react-native-webview: '*' + peerDependenciesMeta: + '@expo/metro-runtime': + optional: true + react-dom: + optional: true + react-native-web: + optional: true + react-native-webview: + optional: true + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fb-dotslash@0.5.8: + resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} + engines: {node: '>=20'} + hasBin: true + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fbjs-css-vars@1.0.2: + resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} + + fbjs@3.0.5: + resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-nodeshim@0.4.10: + resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + flow-enums-runtime@0.0.6: + resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} + + fontfaceobserver@2.3.0: + resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + framer-motion@12.40.0: + resolution: {integrity: sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fumadocs-core@16.9.3: + resolution: {integrity: sha512-8RVzKnzBJR5o+tJCccY28ntekfMQYBoYiz7alnYb/d9YJc+XpnsINzTl63lQ1eBMZ9gdhm2MqRtgUjh/8rUrbw==} + peerDependencies: + '@mdx-js/mdx': '*' + '@mixedbread/sdk': 0.x.x + '@orama/core': 1.x.x + '@oramacloud/client': 2.x.x + '@tanstack/react-router': 1.x.x + '@types/estree-jsx': '*' + '@types/hast': '*' + '@types/mdast': '*' + '@types/react': '*' + algoliasearch: 5.x.x + flexsearch: '*' + lucide-react: '*' + next: 16.x.x + react: ^19.2.0 + react-dom: ^19.2.0 + react-router: 7.x.x + waku: '*' + zod: 4.x.x + peerDependenciesMeta: + '@mdx-js/mdx': + optional: true + '@mixedbread/sdk': + optional: true + '@orama/core': + optional: true + '@oramacloud/client': + optional: true + '@tanstack/react-router': + optional: true + '@types/estree-jsx': + optional: true + '@types/hast': + optional: true + '@types/mdast': + optional: true + '@types/react': + optional: true + algoliasearch: + optional: true + flexsearch: + optional: true + lucide-react: + optional: true + next: + optional: true + react: + optional: true + react-dom: + optional: true + react-router: + optional: true + waku: + optional: true + zod: + optional: true + + fumadocs-mdx@15.0.10: + resolution: {integrity: sha512-kH3S7ESS9yXTAaCkA8dDugsCK/MbnpgyZ5qBEL7cWoavV0O/T4+4YTYFkvNknz7cw+T/r+OG0p2BvlVhkk4fww==} + hasBin: true + peerDependencies: + '@types/mdast': '*' + '@types/mdx': '*' + '@types/react': '*' + fumadocs-core: ^16.7.0 + mdast-util-directive: '*' + next: ^15.3.0 || ^16.0.0 + react: ^19.2.0 + rolldown: '*' + vite: 7.x.x || 8.x.x + peerDependenciesMeta: + '@types/mdast': + optional: true + '@types/mdx': + optional: true + '@types/react': + optional: true + mdast-util-directive: + optional: true + next: + optional: true + react: + optional: true + rolldown: + optional: true + vite: + optional: true + + fumadocs-ui@16.9.3: + resolution: {integrity: sha512-eoVKj1H+ATut0su+WIoPWBLRqzPMGD0hekIBr4GopWvUg1lS997HL4kP+Leyf+3CYlZtFgyXb6ylbvRLFtEj6Q==} + peerDependencies: + '@takumi-rs/image-response': '*' + '@types/mdx': '*' + '@types/react': '*' + fumadocs-core: 16.9.3 + next: 16.x.x + react: ^19.2.0 + react-dom: ^19.2.0 + peerDependenciesMeta: + '@takumi-rs/image-response': + optional: true + '@types/mdx': + optional: true + '@types/react': + optional: true + next: + optional: true + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + getenv@2.0.0: + resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} + engines: {node: '>=6'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + 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} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hermes-compiler@250829098.0.10: + resolution: {integrity: sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-estree@0.33.3: + resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} + + hermes-estree@0.35.0: + resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hermes-parser@0.33.3: + resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} + + hermes-parser@0.35.0: + resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hono@4.12.22: + resolution: {integrity: sha512-7fvVPbB92zNRsQke+uiRGwtTuef0tB2Dg4hWxYfFNvkQhIltWoyi0ONReM5LWA+jJWS3nfT5lTq+qbsIpX0IQw==} + engines: {node: '>=16.9.0'} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-size@1.2.1: + resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} + engines: {node: '>=16.x'} + hasBin: true + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jimp-compact@0.16.1: + resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsc-safe-url@0.2.4: + resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsr@0.14.3: + resolution: {integrity: sha512-PGxnDepx7vwJoZQe2SHbyBiFfpGwsOKmX4kn/wZZqfMafV7fjXqTxSaX6lp9QHYkSTLKkER+P/wmrZY3gVJNzg==} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lan-network@0.2.1: + resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==} + hasBin: true + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lighthouse-logger@1.4.2: + resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.1: + resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.throttle@4.1.1: + resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} + + log-symbols@2.2.0: + resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} + engines: {node: '>=4'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.17.0: + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-it@14.2.0: + resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} + hasBin: true + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + metro-babel-transformer@0.84.4: + resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-cache-key@0.84.4: + resolution: {integrity: sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-cache@0.84.4: + resolution: {integrity: sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-config@0.84.4: + resolution: {integrity: sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-core@0.84.4: + resolution: {integrity: sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-file-map@0.84.4: + resolution: {integrity: sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-minify-terser@0.84.4: + resolution: {integrity: sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-resolver@0.84.4: + resolution: {integrity: sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-runtime@0.84.4: + resolution: {integrity: sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-source-map@0.84.4: + resolution: {integrity: sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-symbolicate@0.84.4: + resolution: {integrity: sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + + metro-transform-plugins@0.84.4: + resolution: {integrity: sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-transform-worker@0.84.4: + resolution: {integrity: sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro@0.84.4: + resolution: {integrity: sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@1.2.0: + resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} + engines: {node: '>=4'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + motion-dom@12.40.0: + resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.40.0: + resolution: {integrity: sha512-yjrHUrBFW6kQvjJwRsoiPSAhC5tRwRqNGJWmiJ4CrGnbKp0V88AdzkhBmDoqIsIPfarOe0Uddd37Xq43/gIocA==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multitars@1.0.0: + resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@16.2.7: + resolution: {integrity: sha512-eMJxgjRzBaj3olkP4cBamHDXL79A8FC6u1GcsO1D1Tsx8bw/LLXUJCaoajVxtnhD3A1IJqIT8IcRJjgBIPJq4w==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-api-headers@1.9.0: + resolution: {integrity: sha512-2oNILP4jXwRB4ywnYKjVk1YyJ96n2D4EOVJO6S3oYZ5PtbJrw3Yt9TpAuX3nBLMuzn74rnfGQrv13pS9vC+YiA==} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + node-stream-zip@1.15.0: + resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} + engines: {node: '>=0.12.0'} + + npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + + nullthrows@1.1.1: + resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} + + ob1@0.84.4: + resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@2.0.1: + resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} + engines: {node: '>=4'} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + open@7.4.2: + resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} + engines: {node: '>=8'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@3.4.0: + resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} + engines: {node: '>=6'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-png@2.1.0: + resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} + engines: {node: '>=10'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + plist@3.1.1: + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} + + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + react-devtools-core@6.1.5: + resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} + + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + peerDependencies: + react: ^19.2.3 + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-freeze@1.0.4: + resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} + engines: {node: '>=10'} + peerDependencies: + react: '>=17.0.0' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.6: + resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + + react-native-drawer-layout@4.2.4: + resolution: {integrity: sha512-l1Le5HcVidobnJm8xqFZo46Rs8FDHdxbTZhkjxpNSRgU+QMoQXilOfzTHAeNjEGiKVGgIs9cW3ctXeHqgp5jJg==} + peerDependencies: + react: '>= 18.2.0' + react-native: '*' + react-native-gesture-handler: '>= 2.0.0' + react-native-reanimated: '>= 2.0.0' + + react-native-gesture-handler@2.31.2: + resolution: {integrity: sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-is-edge-to-edge@1.3.1: + resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-reanimated@4.3.1: + resolution: {integrity: sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q==} + peerDependencies: + react: '*' + react-native: 0.81 - 0.85 + react-native-worklets: 0.8.x + + react-native-safe-area-context@5.7.0: + resolution: {integrity: sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==} + peerDependencies: + react: '*' + react-native: '*' + + react-native-screens@4.25.2: + resolution: {integrity: sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg==} + peerDependencies: + react: '*' + react-native: '>=0.82.0' + + react-native-web@0.21.2: + resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + react-native-worklets@0.8.3: + resolution: {integrity: sha512-oCBJROyLU7yG/1R8s0INMflygTH71bx+5XcYkH0CM938TlhSoVbiunE1WVW5FZa51vwYqfLie/IXMX2s1Kh3eg==} + peerDependencies: + '@babel/core': '*' + '@react-native/metro-config': '*' + react: '*' + react-native: 0.81 - 0.85 + + react-native@0.85.3: + resolution: {integrity: sha512-HN/fGC+3nZVcDNcw7gfbM/DuqZAvI9Mz+/SxuhODaua4JY0BPzhfTzWXRyTR4mRgMHmShTPpH2PYMTxvZrsdZA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + peerDependencies: + '@react-native/jest-preset': 0.85.3 + '@types/react': ^19.1.1 + react: ^19.2.3 + peerDependenciesMeta: + '@react-native/jest-preset': + optional: true + '@types/react': + optional: true + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + hasBin: true + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-workspace-root@2.0.1: + resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@2.0.0: + resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} + engines: {node: '>=4'} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + semiver@1.1.0: + resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==} + engines: {node: '>=6'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serialize-error@2.1.0: + resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} + engines: {node: '>=0.10.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sf-symbols-typescript@2.2.0: + resolution: {integrity: sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==} + engines: {node: '>=10'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + shiki@4.1.0: + resolution: {integrity: sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==} + engines: {node: '>=20'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-plist@1.3.1: + resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slugify@1.6.9: + resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} + engines: {node: '>=8.0.0'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-parser@0.1.11: + resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} + engines: {node: '>=6'} + + standard-navigation@0.0.5: + resolution: {integrity: sha512-YAmzwAiiQVocZxO/VGPFiQHcu5pKiz09QIGC0MK6aRMoa3E0QkoTQgcqJr7ZZ3OMiNhu4DkaGElFI5htjOIDbw==} + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stream-buffers@2.2.0: + resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} + engines: {node: '>= 0.10.0'} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + structured-headers@0.4.1: + resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + styleq@0.1.3: + resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + throat@5.0.0: + resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + toqr@0.1.1: + resolution: {integrity: sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.3: + resolution: {integrity: sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typedoc@0.28.19: + resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==} + engines: {node: '>= 18', pnpm: '>= 10'} + hasBin: true + peerDependencies: + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@0.7.41: + resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} + hasBin: true + + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest-callback@0.2.6: + resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} + peerDependencies: + react: '>=16.8' + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vlq@1.0.1: + resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + warn-once@0.1.1: + resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + + whatwg-url-minimum@0.1.2: + resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@7.5.11: + resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xcode@3.0.1: + resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} + engines: {node: '>=10.0.0'} + + xml2js@0.6.0: + resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} + engines: {node: '>=4.0.0'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + + zx@8.8.5: + resolution: {integrity: sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==} + engines: {node: '>= 12.17.0'} + hasBin: true + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@alloc/quick-lru@5.2.0': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.29.7 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} + + '@biomejs/biome@2.4.16': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.4.16 + '@biomejs/cli-darwin-x64': 2.4.16 + '@biomejs/cli-linux-arm64': 2.4.16 + '@biomejs/cli-linux-arm64-musl': 2.4.16 + '@biomejs/cli-linux-x64': 2.4.16 + '@biomejs/cli-linux-x64-musl': 2.4.16 + '@biomejs/cli-win32-arm64': 2.4.16 + '@biomejs/cli-win32-x64': 2.4.16 + + '@biomejs/cli-darwin-arm64@2.4.16': + optional: true + + '@biomejs/cli-darwin-x64@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-arm64@2.4.16': + optional: true + + '@biomejs/cli-linux-x64-musl@2.4.16': + optional: true + + '@biomejs/cli-linux-x64@2.4.16': + optional: true + + '@biomejs/cli-win32-arm64@2.4.16': + optional: true + + '@biomejs/cli-win32-x64@2.4.16': + optional: true + + '@egjs/hammerjs@2.0.17': + dependencies: + '@types/hammerjs': 2.0.46 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@expo-google-fonts/material-symbols@0.4.38': {} + + '@expo/cli@56.1.19(@expo/metro-runtime@56.0.16)(expo-constants@56.0.20)(expo-font@56.0.7)(expo-router@56.2.14)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)': + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/config': 56.0.11(typescript@6.0.3) + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + '@expo/devcert': 1.2.1 + '@expo/env': 2.3.1 + '@expo/image-utils': 0.10.2(typescript@6.0.3) + '@expo/inline-modules': 0.0.13(typescript@6.0.3) + '@expo/json-file': 10.2.0 + '@expo/log-box': 56.0.14(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/metro': 56.0.0 + '@expo/metro-config': 56.0.16(expo@56.0.15)(typescript@6.0.3) + '@expo/metro-file-map': 56.0.3 + '@expo/osascript': 2.6.0 + '@expo/package-manager': 1.13.0 + '@expo/plist': 0.7.0 + '@expo/prebuild-config': 56.0.19(typescript@6.0.3) + '@expo/require-utils': 56.1.4(typescript@6.0.3) + '@expo/router-server': 56.0.16(@expo/metro-runtime@56.0.16)(expo-constants@56.0.20)(expo-font@56.0.7)(expo-router@56.2.14)(expo-server@56.0.5)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@expo/schema-utils': 56.0.2 + '@expo/spawn-async': 1.8.0 + '@expo/ws-tunnel': 2.0.0(ws@8.21.0) + '@expo/xcpretty': 4.4.4 + '@react-native/dev-middleware': 0.85.3 + accepts: 1.3.8 + agent-cli-detector: 0.1.2 + arg: 5.0.2 + bplist-creator: 0.1.0 + bplist-parser: 0.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + compression: 1.8.1 + connect: 3.7.0 + debug: 4.4.3 + dnssd-advertise: 1.1.6 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-server: 56.0.5 + fetch-nodeshim: 0.4.10 + getenv: 2.0.0 + glob: 13.0.6 + lan-network: 0.2.1 + multitars: 1.0.0 + node-forge: 1.4.0 + npm-package-arg: 11.0.3 + ora: 3.4.0 + picomatch: 4.0.4 + pretty-format: 29.7.0 + progress: 2.0.3 + prompts: 2.4.2 + resolve-from: 5.0.0 + semver: 7.8.1 + send: 0.19.2 + slugify: 1.6.9 + stacktrace-parser: 0.1.11 + structured-headers: 0.4.1 + terminal-link: 2.1.1 + toqr: 0.1.1 + wrap-ansi: 7.0.0 + ws: 8.21.0 + zod: 3.25.76 + optionalDependencies: + expo-router: 56.2.14(b629021f94eaa4102b0df896a774cd24) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + transitivePeerDependencies: + - '@expo/metro-runtime' + - bufferutil + - expo-constants + - expo-font + - react + - react-dom + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + + '@expo/code-signing-certificates@0.0.6': + dependencies: + node-forge: 1.4.0 + + '@expo/config-plugins@56.0.12(typescript@6.0.3)': + dependencies: + '@expo/config-types': 56.0.7 + '@expo/json-file': 10.2.0 + '@expo/plist': 0.7.0 + '@expo/require-utils': 56.1.4(typescript@6.0.3) + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.6 + semver: 7.8.1 + slugify: 1.6.9 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/config-types@56.0.7': {} + + '@expo/config@56.0.11(typescript@6.0.3)': + dependencies: + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + '@expo/config-types': 56.0.7 + '@expo/json-file': 10.2.0 + '@expo/require-utils': 56.1.4(typescript@6.0.3) + deepmerge: 4.3.1 + getenv: 2.0.0 + glob: 13.0.6 + resolve-workspace-root: 2.0.1 + semver: 7.8.1 + slugify: 1.6.9 + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/devcert@1.2.1': + dependencies: + '@expo/sudo-prompt': 9.3.2 + debug: 3.2.7 + transitivePeerDependencies: + - supports-color + + '@expo/devtools@56.0.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + chalk: 4.1.2 + optionalDependencies: + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + '@expo/dom-webview@56.0.6(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + '@expo/env@2.3.1': + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + transitivePeerDependencies: + - supports-color + + '@expo/env@2.4.1': + dependencies: + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + transitivePeerDependencies: + - supports-color + + '@expo/expo-modules-macros-plugin@0.2.2': {} + + '@expo/fingerprint@0.19.7': + dependencies: + '@expo/env': 2.4.1 + '@expo/spawn-async': 1.8.0 + arg: 5.0.2 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.6 + ignore: 5.3.2 + minimatch: 10.2.5 + resolve-from: 5.0.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + '@expo/image-utils@0.10.2(typescript@6.0.3)': + dependencies: + '@expo/require-utils': 56.1.4(typescript@6.0.3) + '@expo/spawn-async': 1.8.0 + chalk: 4.1.2 + getenv: 2.0.0 + jimp-compact: 0.16.1 + parse-png: 2.1.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/inline-modules@0.0.13(typescript@6.0.3)': + dependencies: + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/json-file@10.2.0': + dependencies: + '@babel/code-frame': 7.29.0 + json5: 2.2.3 + + '@expo/json-file@11.0.0': + dependencies: + '@babel/code-frame': 7.29.0 + json5: 2.2.3 + + '@expo/local-build-cache-provider@56.0.9(typescript@6.0.3)': + dependencies: + '@expo/config': 56.0.11(typescript@6.0.3) + chalk: 4.1.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/log-box@56.0.14(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + '@expo/dom-webview': 56.0.6(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + anser: 1.4.10 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + stacktrace-parser: 0.1.11 + + '@expo/mcp-tunnel@0.2.4(@modelcontextprotocol/sdk@1.29.0)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0 + ws: 8.21.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@expo/metro-config@56.0.16(expo@56.0.15)(typescript@6.0.3)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@expo/config': 56.0.11(typescript@6.0.3) + '@expo/env': 2.3.1 + '@expo/json-file': 10.2.0 + '@expo/metro': 56.0.0 + '@expo/require-utils': 56.1.4(typescript@6.0.3) + '@expo/spawn-async': 1.8.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + browserslist: 4.28.2 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.6 + hermes-parser: 0.33.3 + jsc-safe-url: 0.2.4 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + resolve-from: 5.0.0 + optionalDependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - typescript + - utf-8-validate + + '@expo/metro-file-map@56.0.3': + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + '@expo/metro-runtime@56.0.16(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + '@expo/log-box': 56.0.14(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + anser: 1.4.10 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + pretty-format: 29.7.0 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + stacktrace-parser: 0.1.11 + whatwg-fetch: 3.6.20 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + '@expo/metro@56.0.0': + dependencies: + metro: 0.84.4 + metro-babel-transformer: 0.84.4 + metro-cache: 0.84.4 + metro-cache-key: 0.84.4 + metro-config: 0.84.4 + metro-core: 0.84.4 + metro-file-map: 0.84.4 + metro-minify-terser: 0.84.4 + metro-resolver: 0.84.4 + metro-runtime: 0.84.4 + metro-source-map: 0.84.4 + metro-symbolicate: 0.84.4 + metro-transform-plugins: 0.84.4 + metro-transform-worker: 0.84.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@expo/osascript@2.6.0': + dependencies: + '@expo/spawn-async': 1.8.0 + + '@expo/package-manager@1.13.0': + dependencies: + '@expo/json-file': 11.0.0 + '@expo/spawn-async': 1.8.0 + chalk: 4.1.2 + npm-package-arg: 11.0.3 + ora: 3.4.0 + resolve-workspace-root: 2.0.1 + + '@expo/plist@0.7.0': + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + '@expo/prebuild-config@56.0.19(typescript@6.0.3)': + dependencies: + '@expo/config': 56.0.11(typescript@6.0.3) + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + '@expo/config-types': 56.0.7 + '@expo/image-utils': 0.10.2(typescript@6.0.3) + '@expo/json-file': 10.2.0 + '@react-native/normalize-colors': 0.85.3 + debug: 4.4.3 + expo-modules-autolinking: 56.0.19(typescript@6.0.3) + resolve-from: 5.0.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@expo/require-utils@56.1.4(typescript@6.0.3)': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@expo/router-server@56.0.16(@expo/metro-runtime@56.0.16)(expo-constants@56.0.20)(expo-font@56.0.7)(expo-router@56.2.14)(expo-server@56.0.5)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + debug: 4.4.3 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-font: 56.0.7(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-server: 56.0.5 + react: 19.2.3 + optionalDependencies: + '@expo/metro-runtime': 56.0.16(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-router: 56.2.14(b629021f94eaa4102b0df896a774cd24) + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - supports-color + + '@expo/schema-utils@56.0.2': {} + + '@expo/sdk-runtime-versions@1.0.0': {} + + '@expo/spawn-async@1.8.0': + dependencies: + cross-spawn: 7.0.6 + + '@expo/sudo-prompt@9.3.2': {} + + '@expo/ui@56.0.21(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + sf-symbols-typescript: 2.2.0 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + optionalDependencies: + '@babel/core': 7.29.0 + react-dom: 19.2.3(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + '@expo/ws-tunnel@2.0.0(ws@8.21.0)': + dependencies: + ws: 8.21.0 + + '@expo/xcpretty@4.4.4': + dependencies: + '@babel/code-frame': 7.29.0 + chalk: 4.1.2 + js-yaml: 4.3.0 + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + + '@fumadocs/tailwind@0.0.5(@tailwindcss/oxide@4.3.0)(tailwindcss@4.3.0)': + optionalDependencies: + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + + '@gerrit0/mini-shiki@3.23.0': + dependencies: + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@hono/node-server@1.19.14(hono@4.12.22)': + dependencies: + hono: 4.12.22 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@isaacs/cliui@9.0.0': {} + + '@isaacs/ttlcache@1.4.1': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.10 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.12.4 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + acorn: 8.16.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.16.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/react@3.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.16 + react: 19.2.7 + + '@modelcontextprotocol/sdk@1.29.0': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.22) + ajv: 8.20.0 + ajv-formats: 3.0.1 + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.22 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@next/env@16.2.7': {} + + '@next/swc-darwin-arm64@16.2.7': + optional: true + + '@next/swc-darwin-x64@16.2.7': + optional: true + + '@next/swc-linux-arm64-gnu@16.2.7': + optional: true + + '@next/swc-linux-arm64-musl@16.2.7': + optional: true + + '@next/swc-linux-x64-gnu@16.2.7': + optional: true + + '@next/swc-linux-x64-musl@16.2.7': + optional: true + + '@next/swc-win32-arm64-msvc@16.2.7': + optional: true + + '@next/swc-win32-x64-msvc@16.2.7': + optional: true + + '@nolyfill/is-core-module@1.0.39': {} + + '@orama/orama@3.1.18': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + aria-hidden: 1.2.6 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-id@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/rect': 1.1.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.16)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.16)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.16)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + '@types/react-dom': 19.2.3(@types/react@19.2.16) + + '@radix-ui/rect@1.1.1': {} + + '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + '@react-native/assets-registry@0.85.3': {} + + '@react-native/babel-plugin-codegen@0.85.3': + dependencies: + '@babel/traverse': 7.29.0 + '@react-native/codegen': 0.85.3 + transitivePeerDependencies: + - supports-color + + '@react-native/babel-preset@0.85.3': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@react-native/babel-plugin-codegen': 0.85.3 + babel-plugin-syntax-hermes-parser: 0.33.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + react-refresh: 0.14.2 + transitivePeerDependencies: + - supports-color + + '@react-native/codegen@0.85.3': + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + hermes-parser: 0.33.3 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.16 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3)': + dependencies: + '@react-native/dev-middleware': 0.85.3 + debug: 4.4.3 + invariant: 2.2.4 + metro: 0.84.4 + metro-config: 0.84.4 + metro-core: 0.84.4 + semver: 7.8.1 + optionalDependencies: + '@react-native/metro-config': 0.85.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.85.3': {} + + '@react-native/debugger-shell@0.85.3': + dependencies: + cross-spawn: 7.0.6 + debug: 4.4.3 + fb-dotslash: 0.5.8 + transitivePeerDependencies: + - supports-color + + '@react-native/dev-middleware@0.85.3': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.85.3 + '@react-native/debugger-shell': 0.85.3 + chrome-launcher: 0.15.2 + chromium-edge-launcher: 0.3.0 + connect: 3.7.0 + debug: 4.4.3 + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + serve-static: 1.16.3 + ws: 7.5.11 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/gradle-plugin@0.85.3': {} + + '@react-native/js-polyfills@0.85.3': {} + + '@react-native/metro-babel-transformer@0.85.3': + dependencies: + '@babel/core': 7.29.0 + '@react-native/babel-preset': 0.85.3 + hermes-parser: 0.33.3 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + '@react-native/metro-config@0.85.3': + dependencies: + '@react-native/js-polyfills': 0.85.3 + '@react-native/metro-babel-transformer': 0.85.3 + metro-config: 0.84.4 + metro-runtime: 0.84.4 + transitivePeerDependencies: + - supports-color + + '@react-native/normalize-colors@0.74.89': {} + + '@react-native/normalize-colors@0.85.3': {} + + '@react-native/typescript-config@0.85.3': {} + + '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@shikijs/core@4.1.0': + dependencies: + '@shikijs/primitive': 4.1.0 + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/engine-oniguruma@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/langs@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + + '@shikijs/primitive@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@4.1.0': + dependencies: + '@shikijs/types': 4.1.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/types@4.1.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@sinclair/typebox@0.27.10': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/postcss@4.3.0': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + postcss: 8.5.15 + tailwindcss: 4.3.0 + + '@tauri-apps/api@2.11.0': {} + + '@tauri-apps/cli-darwin-arm64@2.11.2': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.2': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + optional: true + + '@tauri-apps/cli@2.11.2': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/hammerjs@2.0.46': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/ms@2.1.0': {} + + '@types/node@22.19.19': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.12.4': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.16)': + dependencies: + '@types/react': 19.2.16 + + '@types/react-test-renderer@19.1.0': + dependencies: + '@types/react': 19.2.16 + + '@types/react@19.2.16': + dependencies: + csstype: 3.2.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@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 + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.4': {} + + '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.1': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/coverage-v8@4.1.8(vitest@4.1.8)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.8 + ast-v8-to-istanbul: 1.0.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.8(@types/node@24.12.4)(@vitest/coverage-v8@4.1.8)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(vite@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))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 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) + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.8': {} + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@xmldom/xmldom@0.8.13': {} + + '@xmldom/xmldom@0.9.10': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@7.1.4: {} + + agent-cli-detector@0.1.2: {} + + ajv-formats@3.0.1: + dependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + anser@1.4.10: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asap@2.0.6: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.3: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + astring@1.9.0: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + await-lock@2.2.2: {} + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.29.0 + + babel-plugin-react-native-web@0.21.2: {} + + babel-plugin-syntax-hermes-parser@0.33.3: + dependencies: + hermes-parser: 0.33.3 + + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + dependencies: + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + + babel-preset-expo@56.0.17(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@56.0.15)(react-refresh@0.14.2): + dependencies: + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@react-native/babel-plugin-codegen': 0.85.3 + babel-plugin-react-compiler: 1.0.0 + babel-plugin-react-native-web: 0.21.2 + babel-plugin-syntax-hermes-parser: 0.33.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + debug: 4.4.3 + react-refresh: 0.14.2 + optionalDependencies: + '@babel/runtime': 7.29.2 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.32: {} + + big-integer@1.6.52: {} + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bplist-creator@0.1.0: + dependencies: + stream-buffers: 2.2.0 + + bplist-parser@0.3.1: + dependencies: + big-integer: 1.6.52 + + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001793: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chrome-launcher@0.15.2: + dependencies: + '@types/node': 24.12.4 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + transitivePeerDependencies: + - supports-color + + chromium-edge-launcher@0.3.0: + dependencies: + '@types/node': 24.12.4 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 1.4.2 + mkdirp: 1.0.4 + transitivePeerDependencies: + - supports-color + + ci-info@2.0.0: {} + + ci-info@3.9.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@2.1.0: + dependencies: + restore-cursor: 2.0.0 + + cli-spinners@2.9.2: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + comma-separated-tokens@2.0.3: {} + + commander@12.1.0: {} + + commander@2.20.3: {} + + commander@7.2.0: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + compute-scroll-into-view@3.1.1: {} + + concat-map@0.0.1: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dnssd-advertise@1.1.6: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.361: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@4.5.0: {} + + entities@6.0.1: {} + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.2: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.3 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.16.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-config-expo@56.0.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@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) + '@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) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-expo: 1.0.3(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) + globals: 16.5.0 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + - typescript + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.2 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + get-tsconfig: 4.14.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.16 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@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) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-expo@1.0.3(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3): + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + - typescript + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.7.0) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + hasown: 2.0.3 + is-core-module: 2.16.2 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 9.39.4(jiti@2.7.0) + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 9.39.4(jiti@2.7.0) + estraverse: 5.3.0 + hasown: 2.0.3 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-value-to-estree@3.5.0: + dependencies: + '@types/estree': 1.0.9 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + expect-type@1.3.0: {} + + expo-asset@56.0.19(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + dependencies: + '@expo/image-utils': 0.10.2(typescript@6.0.3) + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + transitivePeerDependencies: + - supports-color + - typescript + + expo-constants@56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + '@expo/env': 2.3.1 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + transitivePeerDependencies: + - supports-color + + expo-dev-client@56.0.22(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-launcher: 56.0.23(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-dev-menu: 56.0.19(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-dev-menu-interface: 56.0.1(expo@56.0.15) + expo-manifests: 56.0.4(expo@56.0.15) + expo-updates-interface: 56.0.2(expo@56.0.15) + transitivePeerDependencies: + - react-native + + expo-dev-launcher@56.0.23(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + '@expo/schema-utils': 56.0.2 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-menu: 56.0.19(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-manifests: 56.0.4(expo@56.0.15) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-dev-menu-interface@56.0.1(expo@56.0.15): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + + expo-dev-menu@56.0.19(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-menu-interface: 56.0.1(expo@56.0.15) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-device@56.0.4(expo@56.0.15): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + ua-parser-js: 0.7.41 + + expo-doctor@1.19.8: {} + + expo-file-system@56.0.8(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-font@56.0.7(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + fontfaceobserver: 2.3.0 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-glass-effect@56.0.4(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-image@56.0.11(expo@56.0.15)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + sf-symbols-typescript: 2.2.0 + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + + expo-json-utils@56.0.0: {} + + expo-keep-awake@56.0.3(expo@56.0.15)(react@19.2.3): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + + expo-linking@56.0.15(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + expo-constants: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + transitivePeerDependencies: + - expo + - supports-color + + expo-manifests@56.0.4(expo@56.0.15): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-json-utils: 56.0.0 + + expo-mcp@0.2.4: + dependencies: + '@expo/mcp-tunnel': 0.2.4(@modelcontextprotocol/sdk@1.29.0) + '@modelcontextprotocol/sdk': 1.29.0 + debug: 4.4.3 + glob: 11.1.0 + jimp-compact: 0.16.1 + resolve-from: 5.0.0 + ws: 8.21.0 + xml2js: 0.6.2 + zod: 3.25.76 + zx: 8.8.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + + expo-modules-autolinking@56.0.19(typescript@6.0.3): + dependencies: + '@expo/require-utils': 56.1.4(typescript@6.0.3) + '@expo/spawn-async': 1.8.0 + chalk: 4.1.2 + commander: 7.2.0 + transitivePeerDependencies: + - supports-color + - typescript + + expo-modules-core@56.0.20(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + '@expo/expo-modules-macros-plugin': 0.2.2 + expo-modules-jsi: 56.0.12(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + react-native-worklets: 0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + + expo-modules-jsi@56.0.12(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-router@56.2.14(b629021f94eaa4102b0df896a774cd24): + dependencies: + '@expo/log-box': 56.0.14(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/metro-runtime': 56.0.16(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/schema-utils': 56.0.2 + '@expo/ui': 56.0.21(@babel/core@7.29.0)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@testing-library/jest-dom': 6.9.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + client-only: 0.0.1 + color: 4.2.3 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-glass-effect: 56.0.4(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-linking: 56.0.15(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-server: 56.0.5 + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.12 + query-string: 7.1.3 + react: 19.2.3 + react-fast-compare: 3.2.2 + react-is: 19.2.6 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + react-native-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-screens: 4.25.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + server-only: 0.0.1 + sf-symbols-typescript: 2.2.0 + shallowequal: 1.1.0 + standard-navigation: 0.0.5 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - '@babel/core' + - '@testing-library/dom' + - '@types/react' + - '@types/react-dom' + - expo-font + - react-native-worklets + - supports-color + + expo-server@56.0.5: {} + + expo-splash-screen@56.0.12(expo@56.0.15)(typescript@6.0.3): + dependencies: + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + '@expo/image-utils': 0.10.2(typescript@6.0.3) + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + - typescript + + expo-sqlite@56.0.5(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + await-lock: 2.2.2 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-status-bar@56.0.4(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + '@expo-google-fonts/material-symbols': 0.4.38 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-font: 56.0.7(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + sf-symbols-typescript: 2.2.0 + + expo-system-ui@56.0.5(expo@56.0.15)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + '@react-native/normalize-colors': 0.85.3 + debug: 4.4.3 + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - supports-color + + expo-updates-interface@56.0.2(expo@56.0.15): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + + expo-web-browser@56.0.5(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)): + dependencies: + expo: 56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + expo@56.0.15(@babel/core@7.29.0)(@expo/metro-runtime@56.0.16)(expo-router@56.2.14)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + dependencies: + '@babel/runtime': 7.29.2 + '@expo/cli': 56.1.19(@expo/metro-runtime@56.0.16)(expo-constants@56.0.20)(expo-font@56.0.7)(expo-router@56.2.14)(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + '@expo/config': 56.0.11(typescript@6.0.3) + '@expo/config-plugins': 56.0.12(typescript@6.0.3) + '@expo/devtools': 56.0.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/dom-webview': 56.0.6(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/fingerprint': 0.19.7 + '@expo/local-build-cache-provider': 56.0.9(typescript@6.0.3) + '@expo/log-box': 56.0.14(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + '@expo/metro': 56.0.0 + '@expo/metro-config': 56.0.16(expo@56.0.15)(typescript@6.0.3) + '@ungap/structured-clone': 1.3.1 + babel-preset-expo: 56.0.17(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@56.0.15)(react-refresh@0.14.2) + expo-asset: 56.0.19(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.20(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-file-system: 56.0.8(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3)) + expo-font: 56.0.7(expo@56.0.15)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + expo-keep-awake: 56.0.3(expo@56.0.15)(react@19.2.3) + expo-modules-autolinking: 56.0.19(typescript@6.0.3) + expo-modules-core: 56.0.20(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + pretty-format: 29.7.0 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + react-refresh: 0.14.2 + whatwg-url-minimum: 0.1.2 + optionalDependencies: + '@expo/metro-runtime': 56.0.16(expo@56.0.15)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-dom: 19.2.3(react@19.2.3) + react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + transitivePeerDependencies: + - '@babel/core' + - bufferutil + - expo-router + - expo-widgets + - react-native-worklets + - react-server-dom-webpack + - supports-color + - typescript + - utf-8-validate + + exponential-backoff@3.1.3: {} + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-uri@3.1.2: {} + + fb-dotslash@0.5.8: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fbjs-css-vars@1.0.2: {} + + fbjs@3.0.5: + dependencies: + cross-fetch: 3.2.0 + fbjs-css-vars: 1.0.2 + loose-envify: 1.4.0 + object-assign: 4.1.1 + promise: 7.3.1 + setimmediate: 1.0.5 + ua-parser-js: 1.0.41 + transitivePeerDependencies: + - encoding + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fetch-nodeshim@0.4.10: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@1.1.0: {} + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + flow-enums-runtime@0.0.6: {} + + fontfaceobserver@2.3.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forwarded@0.2.0: {} + + framer-motion@12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + motion-dom: 12.40.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + dependencies: + '@orama/orama': 3.1.18 + estree-util-value-to-estree: 3.5.0 + github-slugger: 2.0.0 + hast-util-to-estree: 3.1.3 + hast-util-to-jsx-runtime: 2.3.6 + js-yaml: 4.3.0 + mdast-util-mdx: 3.0.0 + mdast-util-to-markdown: 2.1.2 + remark: 15.0.1 + remark-gfm: 4.0.1 + remark-rehype: 11.1.2 + scroll-into-view-if-needed: 3.1.0 + shiki: 4.1.0 + tinyglobby: 0.2.16 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + optionalDependencies: + '@mdx-js/mdx': 3.1.1 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.16 + lucide-react: 1.17.0(react@19.2.7) + next: 16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + zod: 4.4.3 + transitivePeerDependencies: + - supports-color + + fumadocs-mdx@15.0.10(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.16)(fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7): + dependencies: + '@mdx-js/mdx': 3.1.1 + '@standard-schema/spec': 1.1.0 + chokidar: 5.0.0 + esbuild: 0.28.1 + estree-util-value-to-estree: 3.5.0 + fumadocs-core: 16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + js-yaml: 4.3.0 + mdast-util-mdx: 3.0.0 + picocolors: 1.1.1 + picomatch: 4.0.4 + tinyexec: 1.2.4 + tinyglobby: 0.2.16 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + zod: 4.4.3 + optionalDependencies: + '@types/mdast': 4.0.4 + '@types/mdx': 2.0.13 + '@types/react': 19.2.16 + next: 16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + transitivePeerDependencies: + - supports-color + + fumadocs-ui@16.9.3(@tailwindcss/oxide@4.3.0)(@types/mdx@2.0.13)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(fumadocs-core@16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.0): + dependencies: + '@fumadocs/tailwind': 0.0.5(@tailwindcss/oxide@4.3.0)(tailwindcss@4.3.0) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.7) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + class-variance-authority: 0.7.1 + fumadocs-core: 16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.16)(lucide-react@1.17.0(react@19.2.7))(next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + lucide-react: 1.17.0(react@19.2.7) + motion: 12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.16)(react@19.2.7) + rehype-raw: 7.0.0 + scroll-into-view-if-needed: 3.1.0 + shiki: 4.1.0 + tailwind-merge: 3.6.0 + unist-util-visit: 5.1.0 + optionalDependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.16 + next: 16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@tailwindcss/oxide' + - '@types/react-dom' + - tailwindcss + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.3 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + getenv@2.0.0: {} + + github-slugger@2.0.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globals@16.5.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.9 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + hermes-compiler@250829098.0.10: {} + + hermes-estree@0.25.1: {} + + hermes-estree@0.33.3: {} + + hermes-estree@0.35.0: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hermes-parser@0.33.3: + dependencies: + hermes-estree: 0.33.3 + + hermes-parser@0.35.0: + dependencies: + hermes-estree: 0.35.0 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hono@4.12.22: {} + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + html-escaper@2.0.2: {} + + html-void-elements@3.0.0: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + hyphenate-style-name@1.1.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-size@1.2.1: + dependencies: + queue: 6.0.2 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + inline-style-parser@0.2.7: {} + + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.3 + side-channel: 1.1.0 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.3.4: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.1 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jest-get-type@29.6.3: {} + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 24.12.4 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.2 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-worker@29.7.0: + dependencies: + '@types/node': 24.12.4 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jimp-compact@0.16.1: {} + + jiti@2.7.0: {} + + jose@6.2.3: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsc-safe-url@0.2.4: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsr@0.14.3: + dependencies: + node-stream-zip: 1.15.0 + semiver: 1.1.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + lan-network@0.2.1: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lighthouse-logger@1.4.2: + dependencies: + debug: 2.6.9 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + linkify-it@5.0.1: + dependencies: + uc.micro: 2.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.debounce@4.0.8: {} + + lodash.merge@4.6.2: {} + + lodash.throttle@4.1.1: {} + + log-symbols@2.2.0: + dependencies: + chalk: 2.4.2 + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@11.5.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.17.0(react@19.2.7): + dependencies: + react: 19.2.7 + + lunr@2.3.9: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.1 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + markdown-extensions@2.0.0: {} + + markdown-it@14.2.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.1 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-table@3.0.4: {} + + marky@1.3.0: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdurl@2.0.0: {} + + media-typer@1.1.0: {} + + memoize-one@5.2.1: {} + + memoize-one@6.0.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + metro-babel-transformer@0.84.4: + dependencies: + '@babel/core': 7.29.0 + flow-enums-runtime: 0.0.6 + hermes-parser: 0.35.0 + metro-cache-key: 0.84.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-cache-key@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-cache@0.84.4: + dependencies: + exponential-backoff: 3.1.3 + flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6 + metro-core: 0.84.4 + transitivePeerDependencies: + - supports-color + + metro-config@0.84.4: + dependencies: + connect: 3.7.0 + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.84.4 + metro-cache: 0.84.4 + metro-core: 0.84.4 + metro-runtime: 0.84.4 + yaml: 2.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.84.4 + + metro-file-map@0.84.4: + dependencies: + debug: 4.4.3 + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-minify-terser@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.48.0 + + metro-resolver@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.84.4: + dependencies: + '@babel/runtime': 7.29.2 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.84.4: + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.84.4 + nullthrows: 1.1.1 + ob1: 0.84.4 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.84.4 + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.84.4: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.84.4: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + flow-enums-runtime: 0.0.6 + metro: 0.84.4 + metro-babel-transformer: 0.84.4 + metro-cache: 0.84.4 + metro-cache-key: 0.84.4 + metro-minify-terser: 0.84.4 + metro-source-map: 0.84.4 + metro-transform-plugins: 0.84.4 + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.84.4: + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0 + debug: 4.4.3 + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.35.0 + image-size: 1.2.1 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.84.4 + metro-cache: 0.84.4 + metro-cache-key: 0.84.4 + metro-config: 0.84.4 + metro-core: 0.84.4 + metro-file-map: 0.84.4 + metro-resolver: 0.84.4 + metro-runtime: 0.84.4 + metro-source-map: 0.84.4 + metro-symbolicate: 0.84.4 + metro-transform-plugins: 0.84.4 + metro-transform-worker: 0.84.4 + mime-types: 3.0.2 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.11 + yargs: 17.7.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.9 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.9 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mimic-fn@1.2.0: {} + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp@1.0.4: {} + + motion-dom@12.40.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + framer-motion: 12.40.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + ms@2.0.0: {} + + ms@2.1.3: {} + + multitars@1.0.0: {} + + nanoid@3.3.12: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + next-themes@0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + next@16.2.7(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@next/env': 16.2.7 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + postcss: 8.5.15 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + styled-jsx: 5.1.6(react@19.2.7) + optionalDependencies: + '@next/swc-darwin-arm64': 16.2.7 + '@next/swc-darwin-x64': 16.2.7 + '@next/swc-linux-arm64-gnu': 16.2.7 + '@next/swc-linux-arm64-musl': 16.2.7 + '@next/swc-linux-x64-gnu': 16.2.7 + '@next/swc-linux-x64-musl': 16.2.7 + '@next/swc-win32-arm64-msvc': 16.2.7 + '@next/swc-win32-x64-msvc': 16.2.7 + babel-plugin-react-compiler: 1.0.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-api-headers@1.9.0: {} + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-forge@1.4.0: {} + + node-int64@0.4.0: {} + + node-releases@2.0.46: {} + + node-stream-zip@1.15.0: {} + + npm-package-arg@11.0.3: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.8.1 + validate-npm-package-name: 5.0.1 + + nullthrows@1.1.1: {} + + ob1@0.84.4: + dependencies: + flow-enums-runtime: 0.0.6 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + obug@2.1.1: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@2.0.1: + dependencies: + mimic-fn: 1.2.0 + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + open@7.4.2: + dependencies: + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@3.4.0: + dependencies: + chalk: 2.4.2 + cli-cursor: 2.1.0 + cli-spinners: 2.9.2 + log-symbols: 2.2.0 + strip-ansi: 5.2.0 + wcwidth: 1.0.1 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-png@2.1.0: + dependencies: + pngjs: 3.4.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.0 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pkce-challenge@5.0.1: {} + + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pngjs@3.4.0: {} + + possible-typed-array-names@1.1.0: {} + + postcss-value-parser@4.2.0: {} + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + proc-log@4.2.0: {} + + progress@2.0.3: {} + + promise@7.3.1: + dependencies: + asap: 2.0.6 + + promise@8.3.0: + dependencies: + asap: 2.0.6 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@7.1.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue@6.0.2: + dependencies: + inherits: 2.0.4 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + react-devtools-core@6.1.5: + dependencies: + shell-quote: 1.8.4 + ws: 7.5.11 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + react-dom@19.2.3(react@19.2.3): + dependencies: + react: 19.2.3 + scheduler: 0.27.0 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-fast-compare@3.2.2: {} + + react-freeze@1.0.4(react@19.2.3): + dependencies: + react: 19.2.3 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-is@19.2.6: {} + + react-native-drawer-layout@4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + color: 4.2.3 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + use-latest-callback: 0.2.6(react@19.2.3) + + react-native-gesture-handler@2.31.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + '@egjs/hammerjs': 2.0.17 + '@types/react-test-renderer': 19.1.0 + hoist-non-react-statics: 3.3.2 + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + semver: 7.8.1 + + react-native-safe-area-context@5.7.0(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + + react-native-screens@4.25.2(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-freeze: 1.0.4(react@19.2.3) + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + warn-once: 0.1.1 + + react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.29.2 + '@react-native/normalize-colors': 0.74.89 + fbjs: 3.0.5 + inline-style-prefixer: 7.0.1 + memoize-one: 6.0.0 + nullthrows: 1.1.1 + postcss-value-parser: 4.2.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styleq: 0.1.3 + transitivePeerDependencies: + - encoding + + react-native-worklets@0.8.3(@babel/core@7.29.0)(@react-native/metro-config@0.85.3)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@react-native/metro-config': 0.85.3 + convert-source-map: 2.0.0 + react: 19.2.3 + react-native: 0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3) + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3): + dependencies: + '@react-native/assets-registry': 0.85.3 + '@react-native/codegen': 0.85.3 + '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3) + '@react-native/gradle-plugin': 0.85.3 + '@react-native/js-polyfills': 0.85.3 + '@react-native/normalize-colors': 0.85.3 + '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@react-native/metro-config@0.85.3)(@types/react@19.2.16)(react@19.2.3))(react@19.2.3) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.33.3 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.10 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.84.4 + metro-source-map: 0.84.4 + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.3 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.1 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.16 + whatwg-fetch: 3.6.20 + ws: 7.5.11 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 19.2.16 + transitivePeerDependencies: + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + + react-refresh@0.14.2: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + react-remove-scroll-bar@2.3.8(@types/react@19.2.16)(react@19.2.7): + dependencies: + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + react-remove-scroll@2.7.2(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.16)(react@19.2.3) + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.3) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.16)(react@19.2.3) + use-sidecar: 1.1.3(@types/react@19.2.16)(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.16 + + react-remove-scroll@2.7.2(@types/react@19.2.16)(react@19.2.7): + dependencies: + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.16)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.16)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.16)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.16)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.16 + + react-style-singleton@2.2.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + get-nonce: 1.0.1 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + react-style-singleton@2.2.3(@types/react@19.2.16)(react@19.2.7): + dependencies: + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + react@19.2.3: {} + + react@19.2.7: {} + + readdirp@5.0.0: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.9 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.9 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.1: + dependencies: + jsesc: 3.1.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve-workspace-root@2.0.1: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@2.0.0: + dependencies: + onetime: 2.0.1 + signal-exit: 3.0.7 + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + sax@1.6.0: {} + + scheduler@0.27.0: {} + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.1 + + semiver@1.1.0: {} + + semver@6.3.1: {} + + semver@7.8.1: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-error@2.1.0: {} + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sf-symbols-typescript@2.2.0: {} + + shallowequal@1.1.0: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.4: {} + + shiki@4.1.0: + dependencies: + '@shikijs/core': 4.1.0 + '@shikijs/engine-javascript': 4.1.0 + '@shikijs/engine-oniguruma': 4.1.0 + '@shikijs/langs': 4.1.0 + '@shikijs/themes': 4.1.0 + '@shikijs/types': 4.1.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-plist@1.3.1: + dependencies: + bplist-creator: 0.1.0 + bplist-parser: 0.3.1 + plist: 3.1.1 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + sisteransi@1.0.5: {} + + slugify@1.6.9: {} + + smol-toml@1.6.1: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + split-on-first@1.1.0: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + stackframe@1.3.4: {} + + stacktrace-parser@0.1.11: + dependencies: + type-fest: 0.7.1 + + standard-navigation@0.0.5: {} + + statuses@1.5.0: {} + + statuses@2.0.2: {} + + std-env@4.1.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stream-buffers@2.2.0: {} + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + structured-headers@0.4.1: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + styled-jsx@5.1.6(react@19.2.7): + dependencies: + client-only: 0.0.1 + react: 19.2.7 + + styleq@0.1.3: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-hyperlinks@2.3.0: + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + terminal-link@2.1.1: + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + throat@5.0.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + toqr@0.1.1: {} + + tr46@0.0.3: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.22.3: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.21.3: {} + + type-fest@0.7.1: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typedoc@0.28.19(typescript@6.0.3): + dependencies: + '@gerrit0/mini-shiki': 3.23.0 + lunr: 2.3.9 + markdown-it: 14.2.0 + minimatch: 10.2.5 + typescript: 6.0.3 + yaml: 2.9.0 + + typescript@5.6.3: {} + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + ua-parser-js@0.7.41: {} + + ua-parser-js@1.0.41: {} + + uc.micro@2.1.0: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + undici-types@7.16.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unpipe@1.0.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + use-callback-ref@1.3.3(@types/react@19.2.16)(react@19.2.7): + dependencies: + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + use-latest-callback@0.2.6(react@19.2.3): + dependencies: + react: 19.2.3 + + use-sidecar@1.1.3(@types/react@19.2.16)(react@19.2.3): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.3 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + use-sidecar@1.1.3(@types/react@19.2.16)(react@19.2.7): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.16 + + utils-merge@1.0.1: {} + + uuid@11.1.1: {} + + validate-npm-package-name@5.0.1: {} + + vary@1.1.2: {} + + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@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): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.60.4 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.4 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + terser: 5.48.0 + tsx: 4.22.3 + yaml: 2.9.0 + + vitest@4.1.8(@types/node@24.12.4)(@vitest/coverage-v8@4.1.8)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@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)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 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) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.4 + '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + + vlq@1.0.1: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + warn-once@0.1.1: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-namespaces@2.0.1: {} + + webidl-conversions@3.0.1: {} + + whatwg-fetch@3.6.20: {} + + whatwg-url-minimum@0.1.2: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@7.5.11: {} + + ws@8.21.0: {} + + xcode@3.0.1: + dependencies: + simple-plist: 1.3.1 + uuid: 11.1.1 + + xml2js@0.6.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xml2js@0.6.2: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@3.25.76: {} + + zod@4.4.3: {} + + zwitch@2.0.4: {} + + zx@8.8.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..86b41582 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,46 @@ +packages: + - "src/docs" + - "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/*" + - "src/sdks/react-native" + - "src/sdks/react-native/examples/expo" + - "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla" + +catalog: + "@vitest/coverage-v8": ^4.1.8 + tsx: ^4.20.6 + typedoc: ^0.28.16 + typescript: ^6.0.3 + vitest: ^4.1.8 + +minimumReleaseAge: 1440 +nodeLinker: isolated +# The source-only ICU workspace has no payload until packaging. Do not let +# pnpm's hidden hoist make it appear installed in ordinary SDK consumers. +hoistPattern: + - "*" + - "!@oliphaunt/icu" +confirmModulesPurge: false +autoInstallPeers: false +saveWorkspaceProtocol: rolling +updateNotifier: false +verifyDepsBeforeRun: false + +overrides: + esbuild: 0.28.1 + js-yaml: 4.3.0 + postcss: 8.5.15 + uuid: 11.1.1 + +allowBuilds: + core-js: false + electron: true + esbuild: true + msgpackr-extract: true + sharp: true + unrs-resolver: true diff --git a/prek.toml b/prek.toml index 941f4eb5..ea8c689a 100644 --- a/prek.toml +++ b/prek.toml @@ -1,11 +1,11 @@ -minimum_prek_version = "0.3.10" -default_install_hook_types = ["pre-commit", "commit-msg", "pre-push"] +minimum_prek_version = "0.4.3" +default_install_hook_types = ["pre-commit", "commit-msg"] fail_fast = true exclude = { glob = [ "target/**", - "examples/tauri-sqlx-vanilla/node_modules/**", - "examples/tauri-sqlx-vanilla/dist/**", - "examples/tauri-sqlx-vanilla/src-tauri/target/**", + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/node_modules/**", + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/dist/**", + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/target/**", ] } [[repos]] @@ -14,7 +14,7 @@ hooks = [ { id = "trailing-whitespace", args = ["--markdown-linebreak-ext=md"], stages = ["pre-commit"], types = ["text"] }, { id = "end-of-file-fixer", stages = ["pre-commit"], types = ["text"] }, { id = "check-toml", stages = ["pre-commit"] }, - { id = "check-yaml", stages = ["pre-commit"] }, + { id = "check-yaml", args = ["--allow-multiple-documents"], stages = ["pre-commit"] }, { id = "check-json", stages = ["pre-commit"], exclude = "(^|/)tsconfig\\.json$" }, { id = "check-merge-conflict", stages = ["pre-commit"] }, { id = "check-case-conflict", stages = ["pre-commit"] }, @@ -34,10 +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 examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --check", pass_filenames = false, files = "^examples/tauri-sqlx-vanilla/src-tauri/.*\\.(rs|toml)$", stages = ["pre-commit"] }, - { id = "git-diff-check", name = "git diff --check", language = "system", entry = "git diff --check", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "pre-push-fmt", name = "pre-push cargo fmt", language = "system", entry = "cargo fmt --all --check", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "asset-input-fingerprint", name = "asset input fingerprint", language = "system", entry = "scripts/check-asset-input-fingerprint.sh", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "example-lockfiles", name = "example lockfiles", language = "system", entry = "scripts/check-example-lockfiles.sh", pass_filenames = false, always_run = true, stages = ["pre-push"] }, - { id = "pre-push-check", name = "pre-push cargo check", language = "system", entry = "cargo check --workspace --locked", pass_filenames = false, always_run = true, stages = ["pre-push"] }, + { 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/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..bd3d004e --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,374 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "bootstrap-sha": "07a9054faa03d5737dc0193f7a77ed4a71920c05", + "include-v-in-tag": true, + "separate-pull-requests": false, + "tag-separator": "-", + "pull-request-title-pattern": "chore${scope}: release${component} ${version}", + "group-pull-request-title-pattern": "chore(release): prepare ${branch} releases", + "initial-version": "0.1.0", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "changelog-sections": [ + { + "type": "feat", + "section": "Features", + "hidden": false + }, + { + "type": "fix", + "section": "Bug Fixes", + "hidden": false + }, + { + "type": "perf", + "section": "Performance Improvements", + "hidden": false + }, + { + "type": "refactor", + "section": "Code Refactoring", + "hidden": false + }, + { + "type": "revert", + "section": "Reverts", + "hidden": false + } + ], + "plugins": [ + { + "type": "node-workspace", + "merge": false + }, + { + "type": "linked-versions", + "groupName": "liboliphaunt-runtime", + "components": [ + "liboliphaunt-native", + "liboliphaunt-wasix", + "oliphaunt-extension-contrib-pg18" + ] + } + ], + "packages": { + "src/runtimes/liboliphaunt/native": { + "release-type": "simple", + "component": "liboliphaunt-native", + "package-name": "liboliphaunt-native", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "json", + "path": "packages/darwin-arm64/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-arm64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-x64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "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": "toml", + "path": "crates/tools/Cargo.toml", + "jsonpath": "$.package.version" + }, + { + "type": "json", + "path": "icu-npm/package.json", + "jsonpath": "$.version" + }, + { + "type": "generic", + "path": "icu-npm/OliphauntICU.podspec" + }, + { + "type": "generic", + "path": "src/liboliphaunt_native.c" + } + ] + }, + "src/sdks/rust": { + "release-type": "rust", + "component": "oliphaunt-rust", + "package-name": "oliphaunt", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "toml", + "path": "crates/oliphaunt-build/Cargo.toml", + "jsonpath": "$.package.version" + } + ] + }, + "src/runtimes/broker": { + "release-type": "rust", + "component": "oliphaunt-broker", + "package-name": "oliphaunt-broker", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "json", + "path": "packages/darwin-arm64/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-arm64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-x64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/win32-x64-msvc/package.json", + "jsonpath": "$.version" + }, + { + "type": "toml", + "path": "crates/linux-arm64-gnu/Cargo.toml", + "jsonpath": "$.package.version" + }, + { + "type": "toml", + "path": "crates/linux-x64-gnu/Cargo.toml", + "jsonpath": "$.package.version" + }, + { + "type": "toml", + "path": "crates/macos-arm64/Cargo.toml", + "jsonpath": "$.package.version" + }, + { + "type": "toml", + "path": "crates/windows-x64-msvc/Cargo.toml", + "jsonpath": "$.package.version" + } + ] + }, + "src/runtimes/node-direct": { + "release-type": "node", + "component": "oliphaunt-node-direct", + "package-name": "@oliphaunt/node-direct", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "json", + "path": "packages/darwin-arm64/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-arm64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/linux-x64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/win32-x64-msvc/package.json", + "jsonpath": "$.version" + } + ] + }, + "src/sdks/swift": { + "release-type": "simple", + "component": "oliphaunt-swift", + "package-name": "oliphaunt-swift", + "initial-version": "0.6.0", + "bump-patch-for-minor-pre-major": false, + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/sdks/kotlin": { + "release-type": "simple", + "component": "oliphaunt-kotlin", + "package-name": "dev.oliphaunt:oliphaunt-android", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "generic", + "path": "gradle.properties" + } + ] + }, + "src/sdks/react-native": { + "release-type": "node", + "component": "oliphaunt-react-native", + "package-name": "@oliphaunt/react-native", + "changelog-path": "CHANGELOG.md" + }, + "src/sdks/js": { + "release-type": "node", + "component": "oliphaunt-js", + "package-name": "@oliphaunt/ts", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "json", + "path": "jsr.json", + "jsonpath": "$.version" + } + ] + }, + "src/extensions/contrib": { + "release-type": "simple", + "component": "oliphaunt-extension-contrib-pg18", + "package-name": "oliphaunt-extension-contrib-pg18", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/pg_hashids": { + "release-type": "simple", + "component": "oliphaunt-extension-pg-hashids", + "package-name": "oliphaunt-extension-pg-hashids", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/pg_ivm": { + "release-type": "simple", + "component": "oliphaunt-extension-pg-ivm", + "package-name": "oliphaunt-extension-pg-ivm", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/pg_textsearch": { + "release-type": "simple", + "component": "oliphaunt-extension-pg-textsearch", + "package-name": "oliphaunt-extension-pg-textsearch", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/pg_uuidv7": { + "release-type": "simple", + "component": "oliphaunt-extension-pg-uuidv7", + "package-name": "oliphaunt-extension-pg-uuidv7", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/pgtap": { + "release-type": "simple", + "component": "oliphaunt-extension-pgtap", + "package-name": "oliphaunt-extension-pgtap", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/postgis": { + "release-type": "simple", + "component": "oliphaunt-extension-postgis", + "package-name": "oliphaunt-extension-postgis", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/extensions/external/vector": { + "release-type": "simple", + "component": "oliphaunt-extension-vector", + "package-name": "oliphaunt-extension-vector", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md" + }, + "src/runtimes/liboliphaunt/wasix": { + "release-type": "simple", + "component": "liboliphaunt-wasix", + "package-name": "liboliphaunt-wasix", + "version-file": "VERSION", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + { + "type": "toml", + "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" + } + ] + }, + "src/bindings/wasix-rust/crates/oliphaunt-wasix": { + "release-type": "rust", + "component": "oliphaunt-wasix-rust", + "package-name": "oliphaunt-wasix", + "changelog-path": "CHANGELOG.md" + } + } +} diff --git a/release-plz.toml b/release-plz.toml deleted file mode 100644 index 8b2b7164..00000000 --- a/release-plz.toml +++ /dev/null @@ -1,91 +0,0 @@ -[workspace] -allow_dirty = true -changelog_update = true -dependencies_update = true -features_always_increment_minor = false -git_release_enable = false -git_release_name = "{{ package }} v{{ version }}" -git_tag_enable = false -git_tag_name = "{{ version }}" -pr_branch_prefix = "release-plz-" -pr_labels = ["release"] -pr_name = "chore(release): {{ version }}" -publish = true -publish_allow_dirty = true -publish_timeout = "30m" -release_always = true -repo_url = "https://github.com/f0rr0/oliphaunt" -semver_check = true - -[changelog] -protect_breaking_commits = true -sort_commits = "oldest" -tag_pattern = '^[0-9]+\.[0-9]+\.[0-9]+.*$' -commit_parsers = [ - { message = "^feat", group = "Added" }, - { message = "^fix", group = "Fixed" }, - { message = "^perf", group = "Performance" }, - { message = "^refactor", group = "Changed" }, - { message = "^revert", group = "Changed" }, - { message = "^.*!", group = "Breaking" }, - { message = "^docs", skip = true }, - { message = "^test", skip = true }, - { message = "^ci", skip = true }, - { message = "^chore", skip = true }, - { message = "^style", skip = true }, -] - -[[package]] -name = "pglite-oxide" -version_group = "pglite-oxide" -changelog_path = "CHANGELOG.md" -changelog_include = [ - "pglite-oxide-assets", - "pglite-oxide-aot-aarch64-apple-darwin", - "pglite-oxide-aot-x86_64-unknown-linux-gnu", - "pglite-oxide-aot-aarch64-unknown-linux-gnu", - "pglite-oxide-aot-x86_64-pc-windows-msvc", -] -git_release_enable = true -git_tag_enable = true -git_tag_name = "{{ version }}" - -[[package]] -name = "pglite-oxide-assets" -version_group = "pglite-oxide" -changelog_update = false -git_release_enable = false -git_tag_enable = false -semver_check = false - -[[package]] -name = "pglite-oxide-aot-aarch64-apple-darwin" -version_group = "pglite-oxide" -changelog_update = false -git_release_enable = false -git_tag_enable = false -semver_check = false - -[[package]] -name = "pglite-oxide-aot-x86_64-unknown-linux-gnu" -version_group = "pglite-oxide" -changelog_update = false -git_release_enable = false -git_tag_enable = false -semver_check = false - -[[package]] -name = "pglite-oxide-aot-aarch64-unknown-linux-gnu" -version_group = "pglite-oxide" -changelog_update = false -git_release_enable = false -git_tag_enable = false -semver_check = false - -[[package]] -name = "pglite-oxide-aot-x86_64-pc-windows-msvc" -version_group = "pglite-oxide" -changelog_update = false -git_release_enable = false -git_tag_enable = false -semver_check = false diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..eb3c4ea5 --- /dev/null +++ b/renovate.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", ":semanticCommits"], + "dependencyDashboard": true, + "labels": ["dependencies"], + "minimumReleaseAge": "3 days", + "rangeStrategy": "bump", + "enabledManagers": [ + "cargo", + "cocoapods", + "dockerfile", + "github-actions", + "gradle", + "gradle-wrapper", + "npm", + "pep621", + "regex", + "swift" + ], + "packageRules": [ + { + "matchManagers": ["github-actions"], + "groupName": "GitHub Actions" + }, + { + "matchManagers": ["cargo"], + "groupName": "Rust crates" + }, + { + "matchManagers": ["gradle", "gradle-wrapper"], + "groupName": "Android and Kotlin" + }, + { + "matchManagers": ["npm"], + "groupName": "JavaScript and React Native" + }, + { + "matchManagers": ["swift", "cocoapods"], + "groupName": "Apple SDK" + } + ], + "customManagers": [ + { + "customType": "regex", + "description": "Pinned Moon CLI", + "managerFilePatterns": ["/^\\.prototools$/"], + "matchStrings": ["moon = \"(?[^\"]+)\""], + "datasourceTemplate": "npm", + "depNameTemplate": "@moonrepo/cli" + }, + { + "customType": "regex", + "description": "Pinned Node runtime", + "managerFilePatterns": ["/^\\.prototools$/"], + "matchStrings": ["node = \"(?[^\"]+)\""], + "datasourceTemplate": "node-version", + "depNameTemplate": "node" + }, + { + "customType": "regex", + "description": "Pinned pnpm runtime", + "managerFilePatterns": ["/^\\.prototools$/"], + "matchStrings": ["pnpm = \"(?[^\"]+)\""], + "datasourceTemplate": "npm", + "depNameTemplate": "pnpm" + } + ] +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 36e244e4..9efad5be 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.93" -components = ["rustfmt", "clippy"] +channel = "1.93.1" +components = ["rustfmt", "clippy", "llvm-tools-preview"] profile = "minimal" diff --git a/scripts/bootstrap-tools.sh b/scripts/bootstrap-tools.sh deleted file mode 100755 index 6c8fcca6..00000000 --- a/scripts/bootstrap-tools.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -PREK_VERSION="${PREK_VERSION:-0.3.10}" -CARGO_BINSTALL_VERSION="${CARGO_BINSTALL_VERSION:-1.19.1}" -CARGO_DENY_VERSION="${CARGO_DENY_VERSION:-0.19.4}" -CARGO_HACK_VERSION="${CARGO_HACK_VERSION:-0.6.44}" -CARGO_SEMVER_CHECKS_VERSION="${CARGO_SEMVER_CHECKS_VERSION:-0.47.0}" -ZIZMOR_VERSION="${ZIZMOR_VERSION:-1.24.1}" -ACTIONLINT_VERSION="${ACTIONLINT_VERSION:-1.7.12}" - -cargo_bin_dir="${CARGO_HOME:-$HOME/.cargo}/bin" -mkdir -p "$cargo_bin_dir" -PATH="$cargo_bin_dir:$PATH" -export PATH - -has_command() { - command -v "$1" >/dev/null 2>&1 -} - -installed_tool_version() { - binary="$1" - case "$binary" in - cargo-hack) cargo hack --version 2>/dev/null || true ;; - cargo-semver-checks) cargo semver-checks --version 2>/dev/null || true ;; - *) "$binary" --version 2>/dev/null || true ;; - esac -} - -version_output_matches() { - output="$1" - version="$2" - escaped_version="$(printf '%s' "$version" | sed 's/[][\\.^$*+?{}|()]/\\&/g')" - printf '%s\n' "$output" | grep -Eq "(^|[^0-9.])${escaped_version}([^0-9.]|$)" -} - -require_pinned_version() { - binary="$1" - version="$2" - output="$3" - if ! version_output_matches "$output" "$version"; then - cat >&2 </dev/null - return - fi - echo "cargo-binstall could not install $package@$version from a binary; falling back to cargo install" >&2 - fi - cargo install "$package" --version "$version" --locked - installed_pinned_tool_version "$binary" "$version" >/dev/null -} - -install_cargo_binstall() { - if has_command cargo-binstall; then - output="$(cargo-binstall -V 2>/dev/null || true)" - require_pinned_version cargo-binstall "$CARGO_BINSTALL_VERSION" "$output" - echo "cargo-binstall already installed: $output" - return - fi - - os="$(uname -s | tr '[:upper:]' '[:lower:]')" - arch="$(uname -m)" - case "$os:$arch" in - darwin:arm64) asset="cargo-binstall-aarch64-apple-darwin.zip"; extract=zip ;; - darwin:x86_64) asset="cargo-binstall-x86_64-apple-darwin.zip"; extract=zip ;; - linux:aarch64 | linux:arm64) asset="cargo-binstall-aarch64-unknown-linux-musl.tgz"; extract=tgz ;; - linux:x86_64) asset="cargo-binstall-x86_64-unknown-linux-musl.tgz"; extract=tgz ;; - *) - echo "unsupported cargo-binstall platform: $os/$arch" >&2 - echo "falling back to source-built cargo-installed tools" >&2 - return 0 - ;; - esac - - 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" - 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 - ;; - tgz) - tar -xzf "$archive" -C "$tmp" - ;; - esac - binstall_bin="$(find "$tmp" -type f -name cargo-binstall | head -n 1)" - if [ -z "$binstall_bin" ]; then - echo "cargo-binstall archive did not contain a cargo-binstall binary" >&2 - find "$tmp" -maxdepth 3 -type f -print >&2 - return 1 - fi - install "$binstall_bin" "$cargo_bin_dir/cargo-binstall" - rm -rf "$tmp" - output="$(cargo-binstall -V 2>/dev/null || true)" - require_pinned_version cargo-binstall "$CARGO_BINSTALL_VERSION" "$output" -} - -install_actionlint() { - if has_command actionlint; then - output="$(actionlint -version 2>/dev/null || true)" - require_pinned_version actionlint "$ACTIONLINT_VERSION" "$output" - echo "actionlint already installed: $output" - return - fi - - os="$(uname -s | tr '[:upper:]' '[:lower:]')" - arch="$(uname -m)" - case "$os:$arch" in - darwin:arm64) asset_os=darwin; asset_arch=arm64 ;; - darwin:x86_64) asset_os=darwin; asset_arch=amd64 ;; - linux:aarch64 | linux:arm64) asset_os=linux; asset_arch=arm64 ;; - linux:x86_64) asset_os=linux; asset_arch=amd64 ;; - *) - echo "unsupported actionlint platform: $os/$arch" >&2 - echo "install actionlint manually from https://github.com/rhysd/actionlint/releases" >&2 - return 1 - ;; - esac - - tmp="$(mktemp -d)" - trap 'rm -rf "$tmp"' EXIT - archive="$tmp/actionlint.tar.gz" - curl -L --fail --retry 3 \ - --output "$archive" \ - "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_${asset_os}_${asset_arch}.tar.gz" - tar -xzf "$archive" -C "$tmp" - install "$tmp/actionlint" "$cargo_bin_dir/actionlint" - output="$(actionlint -version 2>/dev/null || true)" - require_pinned_version actionlint "$ACTIONLINT_VERSION" "$output" -} - -install_cargo_binstall -install_cargo_tool prek prek "$PREK_VERSION" -install_cargo_tool cargo-deny cargo-deny "$CARGO_DENY_VERSION" -install_cargo_tool cargo-hack cargo-hack "$CARGO_HACK_VERSION" -install_cargo_tool cargo-semver-checks cargo-semver-checks "$CARGO_SEMVER_CHECKS_VERSION" -install_cargo_tool zizmor zizmor "$ZIZMOR_VERSION" -install_actionlint - -echo -echo "Tool bootstrap complete. Ensure $cargo_bin_dir is on PATH." diff --git a/scripts/check-asset-input-fingerprint.sh b/scripts/check-asset-input-fingerprint.sh deleted file mode 100755 index 6581f210..00000000 --- a/scripts/check-asset-input-fingerprint.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -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" -- \ - assets/sources.toml \ - assets/extensions.promoted.toml \ - assets/extensions.smoke.toml \ - assets/wasix-build \ - crates/assets/Cargo.toml \ - crates/assets/build.rs \ - crates/assets/src \ - crates/aot \ - xtask/src/main.rs \ - xtask/src/extension_catalog.rs \ - 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/scripts/check-dependency-invariants.sh b/scripts/check-dependency-invariants.sh deleted file mode 100755 index 225f2331..00000000 --- a/scripts/check-dependency-invariants.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$root" - -python3 <<'PY' -import pathlib -import sys -import tomllib - -root = pathlib.Path.cwd() -root_manifest_path = root / "Cargo.toml" -root_manifest = tomllib.loads(root_manifest_path.read_text(encoding="utf-8")) -root_version = root_manifest["package"]["version"] -expected_req = f"={root_version}" - - -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_internal_payload_crate(name): - return name == "pglite-oxide-assets" or name.startswith("pglite-oxide-aot-") - - -errors = [] -root_deps = {} -for table_name, deps in dependency_tables(root_manifest): - for dep_key, spec in deps.items(): - name = dependency_name(dep_key, spec) - if not is_internal_payload_crate(name): - continue - if name in root_deps: - errors.append(f"{name} is declared more than once in root dependencies") - root_deps[name] = (table_name, spec) - -internal_manifest_paths = [root / "crates/assets/Cargo.toml"] -internal_manifest_paths.extend(sorted((root / "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_internal_payload_crate(name): - errors.append(f"{manifest_path}: unexpected internal crate name {name!r}") - continue - if version != root_version: - errors.append( - f"{manifest_path}: {name} version {version} does not match root version {root_version}" - ) - - dep = root_deps.get(name) - if dep is None: - errors.append(f"root Cargo.toml does not depend on internal crate {name}") - continue - table_name, spec = dep - version_req = dependency_version(spec) - if version_req != expected_req: - errors.append( - f"root Cargo.toml {table_name}.{name} uses version {version_req!r}; expected {expected_req!r}" - ) - path = dependency_path(spec) - if path is None: - errors.append(f"root Cargo.toml {table_name}.{name} must keep a path dependency") - continue - expected_path = manifest_path.parent.resolve() - actual_path = (root / path).resolve() - if actual_path != expected_path: - errors.append( - f"root Cargo.toml {table_name}.{name} points at {path!r}; expected {manifest_path.parent}" - ) - -extra_deps = sorted(set(root_deps) - {tomllib.loads(path.read_text(encoding="utf-8"))["package"]["name"] for path in internal_manifest_paths}) -for name in extra_deps: - errors.append(f"root Cargo.toml depends on unknown internal crate {name}") - -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 - -blocked='wasm''time|wasm''time-wasi|wasmer-compiler-(llvm|cranelift|singlepass)|llvm-sys|cranelift-|singlepass' - -if cargo tree -p pglite-oxide --features extensions --locked | rg -n "$blocked"; then - cat >&2 <<'MSG' -blocked runtime dependency found in the normal user dependency tree. - -The production path must stay on headless Wasmer AOT loading. Backend compiler -crates such as LLVM, Cranelift, Singlepass, and Wasmtime must not enter the -normal user build. -MSG - exit 1 -fi - -if cargo tree -p xtask --features aot-serializer --locked | rg -n 'wasmer-compiler-(cranelift|singlepass)|cranelift-|singlepass|wasm''time'; then - cat >&2 <<'MSG' -blocked maintainer serializer dependency found. - -The AOT serializer may use Wasmer LLVM only. Cranelift, Singlepass, and Wasmtime -belong in isolated maintainer experiments, not in release/AOT tooling. -MSG - exit 1 -fi - -echo "dependency invariants ok" diff --git a/scripts/check-example-lockfiles.sh b/scripts/check-example-lockfiles.sh deleted file mode 100755 index e36fa645..00000000 --- a/scripts/check-example-lockfiles.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$root" - -base_ref="${EXAMPLE_LOCK_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 "example lockfile check skipped: ${base_ref} is not available" >&2 - exit 0 -fi - -changed="$( - git diff --name-only "${base_ref}...HEAD" -- \ - Cargo.toml \ - Cargo.lock \ - crates/assets/Cargo.toml \ - crates/aot \ - examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock \ - scripts/check-example-lockfiles.sh \ - scripts/sync-example-lockfiles.py -)" - -if [[ -z "$changed" ]]; then - echo "example lockfile check skipped: no package version or lockfile changes" - exit 0 -fi - -scripts/sync-example-lockfiles.py --check diff --git a/scripts/ci-scope.sh b/scripts/ci-scope.sh deleted file mode 100755 index 936925af..00000000 --- a/scripts/ci-scope.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -base_ref="${1:-}" -head_ref="${2:-HEAD}" - -root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$root" - -all_true=false -if [[ -z "$base_ref" ]] || ! git rev-parse --verify -q "$base_ref^{commit}" >/dev/null; then - all_true=true -fi -if ! git rev-parse --verify -q "$head_ref^{commit}" >/dev/null; then - all_true=true -fi - -if [[ "$all_true" == true ]]; then - changed_files="*" -else - changed_files="$(git diff --name-only "$base_ref...$head_ref" --)" -fi - -repo=false -rust=false -examples=false -package=false -assets=false -ci=false -docs=false - -set_all_true() { - repo=true - rust=true - examples=true - package=true - assets=true - ci=true - docs=true -} - -if [[ "$changed_files" == "*" ]]; then - set_all_true -else - while IFS= read -r file; do - [[ -z "$file" ]] && continue - - case "$file" in - .github/workflows/* | .github/scripts/* | .github/actions/* | .github/zizmor.yml | scripts/* | prek.toml | deny.toml | clippy.toml | rust-toolchain.toml) - repo=true - ci=true - ;; - .github/*) - repo=true - docs=true - ;; - README.md | CHANGELOG.md | docs/*) - repo=true - docs=true - ;; - esac - - case "$file" in - Cargo.toml | build.rs | crates/*/Cargo.toml | crates/aot/*/Cargo.toml) - repo=true - rust=true - package=true - ;; - Cargo.lock | src/* | tests/*) - repo=true - rust=true - ;; - xtask/*) - repo=true - rust=true - assets=true - ;; - esac - - case "$file" in - assets/* | crates/assets/* | crates/aot/*) - repo=true - rust=true - assets=true - ;; - esac - - case "$file" in - examples/*) - repo=true - examples=true - ;; - esac - done <<< "$changed_files" -fi - -if [[ "$assets" == true ]]; then - rust=true - package=true -fi - -docs_only=false -if [[ "$docs" == true && "$rust" == false && "$examples" == false && "$package" == false && "$assets" == false && "$ci" == false ]]; then - docs_only=true -fi - -emit() { - local key="$1" - local value="$2" - printf '%s=%s\n' "$key" "$value" - if [[ -n "${GITHUB_OUTPUT:-}" ]]; then - printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT" - fi -} - -emit repo "$repo" -emit rust "$rust" -emit examples "$examples" -emit package "$package" -emit assets "$assets" -emit ci "$ci" -emit docs "$docs" -emit docs_only "$docs_only" diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh deleted file mode 100755 index 91182b2e..00000000 --- a/scripts/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/scripts/perf/build_bench_matrix.mjs b/scripts/perf/build_bench_matrix.mjs deleted file mode 100644 index e97e7538..00000000 --- a/scripts/perf/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 nodeRttNodefsSqlx = collectRun(node, 'rtt', 'pglite_nodefs_sqlx') - const nodeSpeedNodefsSqlx = collectRun(node, 'speed', 'pglite_nodefs_sqlx') - - const headlineModes = [ - { - label: 'native pg + SQLx', - rttRun: nativeRttSqlx, - speedRun: nativeSpeedSqlx, - openMicros: nativeRttSqlx.openMicros, - connectMicros: nativeRttSqlx.connectMicros, - setupMicros: nativeRttSqlx.setupMicros, - }, - { - label: 'pglite-oxide + SQLx', - rttRun: oxideRttSqlx, - speedRun: oxideSpeedSqlx, - openMicros: oxideRttSqlx.openMicros, - connectMicros: oxideRttSqlx.connectMicros, - setupMicros: oxideRttSqlx.setupMicros, - }, - { - label: 'vanilla PGlite + SQLx', - rttRun: nodeRttNodefsSqlx, - speedRun: nodeSpeedNodefsSqlx, - openMicros: nodeRttNodefsSqlx.openMicros, - connectMicros: nodeRttNodefsSqlx.connectMicros, - setupMicros: nodeRttNodefsSqlx.setupMicros, - }, - ] - - const speedMaps = { - oxideSqlx: indexTestsById(oxideSpeedSqlx), - nativeSqlx: indexTestsById(nativeSpeedSqlx), - nodeNodefsSqlx: indexTestsById(nodeSpeedNodefsSqlx), - } - - 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( - `- npm 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 \`assets/checkouts/pglite/packages/benchmark/src\``) - lines.push('') - lines.push('## Headline') - lines.push('') - lines.push('| Metric | native pg + SQLx | pglite-oxide + SQLx | vanilla PGlite + 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(`- pglite-oxide + SQLx RTT vs vanilla PGlite + SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(nodeRttNodefsSqlx))}`) - lines.push(`- pglite-oxide + SQLx RTT vs native pg + SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(nativeRttSqlx))}`) - lines.push(`- pglite-oxide + SQLx speed total vs vanilla PGlite + SQLx: ${formatRatio(speedTotalMicros(oxideSpeedSqlx), speedTotalMicros(nodeSpeedNodefsSqlx))}`) - lines.push(`- pglite-oxide + 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 | pglite-oxide + SQLx | vanilla PGlite + 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 nodeNodefsSqlx = speedMaps.nodeNodefsSqlx.get(test.id).elapsedMicros - lines.push( - `| ${test.id} | ${test.label} | ${formatMillis(nativeSqlx / 1000)} | ${formatMillis(oxideSqlx / 1000)} | ${formatMillis(nodeNodefsSqlx / 1000)} |`, - ) - } - - lines.push('') - lines.push('## Notes') - lines.push('') - lines.push('- This matrix is meant for local reproducibility, not universal absolute claims. Different CPUs, filesystems, Node 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 `pglite-oxide + SQLx` vs `vanilla PGlite + SQLx`.') - lines.push('') - - await fs.writeFile(output, `${lines.join('\n')}\n`) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/perf/node-bench/package-lock.json b/scripts/perf/node-bench/package-lock.json deleted file mode 100644 index 7b3b8512..00000000 --- a/scripts/perf/node-bench/package-lock.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "pglite-oxide-node-bench", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pglite-oxide-node-bench", - "dependencies": { - "@electric-sql/pglite": "0.4.5", - "@electric-sql/pglite-socket": "0.1.5" - } - }, - "node_modules/@electric-sql/pglite": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.5.tgz", - "integrity": "sha512-aGG2zGEyZzGWKy8P+9ZoNUV0jxt1+hgbeTf+bVAYyxVZZLXg3/9aFlfLxb08AYZVAfAkQlQIysmWjhc5hwDG8g==", - "license": "Apache-2.0", - "peer": true - }, - "node_modules/@electric-sql/pglite-socket": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.5.tgz", - "integrity": "sha512-/RAye+3EPKfO9nY4tljzxXmkT7yIpFDm0L3F+c28b+Z6uxPOjy/Zz/QEHYHXcrfuUC88/a9S72EO0+3E0j97wQ==", - "license": "Apache-2.0", - "bin": { - "pglite-server": "dist/scripts/server.js" - }, - "peerDependencies": { - "@electric-sql/pglite": "0.4.5" - } - } - } -} diff --git a/scripts/perf/node-bench/package.json b/scripts/perf/node-bench/package.json deleted file mode 100644 index 9cffc3a9..00000000 --- a/scripts/perf/node-bench/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "pglite-oxide-node-bench", - "private": true, - "type": "module", - "dependencies": { - "@electric-sql/pglite": "0.4.5", - "@electric-sql/pglite-socket": "0.1.5" - } -} diff --git a/scripts/perf/node-bench/start_nodefs_socket.mjs b/scripts/perf/node-bench/start_nodefs_socket.mjs deleted file mode 100644 index 0b1db3e6..00000000 --- a/scripts/perf/node-bench/start_nodefs_socket.mjs +++ /dev/null @@ -1,115 +0,0 @@ -import { performance } from 'node:perf_hooks' -import fs from 'node:fs/promises' -import path from 'node:path' -import process from 'node:process' -import { fileURLToPath } from 'node:url' - -import { PGlite } from '@electric-sql/pglite' -import { PGLiteSocketServer } from '@electric-sql/pglite-socket' - -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 nowMicros() { - return Math.round(performance.now() * 1000) -} - -function elapsedMicros(startMicros) { - return nowMicros() - startMicros -} - -async function main() { - const args = parseArgs(process.argv.slice(2)) - const readyPath = requireArg(args, '--ready') - const runId = requireArg(args, '--run-id') - - const scriptDir = path.dirname(fileURLToPath(import.meta.url)) - const repoRoot = path.resolve(scriptDir, '../../..') - const dataDir = - args['--data-dir'] ?? - path.join(repoRoot, 'target/perf/node-bench/runtime', runId, 'pglite_nodefs_sqlx') - - await fs.mkdir(path.dirname(readyPath), { recursive: true }) - await fs.rm(readyPath, { force: true }) - await fs.rm(dataDir, { recursive: true, force: true }) - await fs.mkdir(dataDir, { recursive: true }) - - const openStarted = nowMicros() - const db = new PGlite(dataDir) - await db.waitReady - const server = new PGLiteSocketServer({ - db, - host: '127.0.0.1', - port: 0, - maxConnections: 1, - }) - await server.start() - const openMicros = elapsedMicros(openStarted) - - const [host, port] = server.getServerConn().split(':') - const databaseUrl = `postgresql://postgres:postgres@${host}:${port}/postgres?sslmode=disable` - const ready = { - databaseUrl, - host, - port: Number(port), - dataDir, - openMicros, - node: process.version, - package: '@electric-sql/pglite', - version: '0.4.5', - socketPackage: '@electric-sql/pglite-socket', - socketVersion: '0.1.5', - } - await fs.writeFile(readyPath, `${JSON.stringify(ready, null, 2)}\n`) - console.log(`PGlite NodeFS socket ready at ${host}:${port}`) - - let shuttingDown = false - const stop = async () => { - if (shuttingDown) { - return - } - shuttingDown = true - await server.stop() - await db.close() - } - - await new Promise((resolve) => { - for (const signal of ['SIGINT', 'SIGTERM']) { - process.once(signal, () => { - resolve() - }) - } - process.once('disconnect', () => { - resolve() - }) - }) - await stop() -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/scripts/perf/run_bench_matrix.sh b/scripts/perf/run_bench_matrix.sh deleted file mode 100755 index e40f0428..00000000 --- a/scripts/perf/run_bench_matrix.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -TARGET_DIR="$REPO_ROOT/target/perf" -NODE_BENCH_DIR="$SCRIPT_DIR/node-bench" - -RUN_ID="${1:-$(date -u +%Y%m%dT%H%M%SZ)}" - -POSTGRES_BIN="${PGLITE_OXIDE_NATIVE_POSTGRES:-/opt/homebrew/opt/postgresql@18/bin/postgres}" -INITDB_BIN="${PGLITE_OXIDE_NATIVE_INITDB:-/opt/homebrew/opt/postgresql@18/bin/initdb}" - -if [[ ! -x "$POSTGRES_BIN" ]]; then - POSTGRES_BIN="$(command -v postgres)" -fi - -if [[ ! -x "$INITDB_BIN" ]]; then - INITDB_BIN="$(command -v initdb)" -fi - -mkdir -p "$TARGET_DIR" - -if [[ ! -d "$NODE_BENCH_DIR/node_modules" ]]; then - ( - cd "$NODE_BENCH_DIR" - npm install --no-fund --no-audit - ) -fi - -OXIDE_JSON="$TARGET_DIR/bench-oxide-$RUN_ID.json" -NATIVE_JSON="$TARGET_DIR/bench-native-postgres-sqlx-$RUN_ID.json" -NODE_JSON="$TARGET_DIR/bench-pglite-nodefs-sqlx-$RUN_ID.json" -NODE_READY_JSON="$TARGET_DIR/bench-pglite-nodefs-sqlx-ready-$RUN_ID.json" -NODE_LOG="$TARGET_DIR/bench-pglite-nodefs-sqlx-$RUN_ID.log" -REPORT_MD="$TARGET_DIR/bench-comparison-$RUN_ID.md" - -NATIVE_VERSION="$("$POSTGRES_BIN" --version | sed 's/^postgres (PostgreSQL) //')" -OS_LABEL="$(uname -smr)" -if command -v sw_vers >/dev/null 2>&1; then - OS_LABEL="$(sw_vers -productName) $(sw_vers -productVersion) (${OS_LABEL})" -fi -CPU_LABEL="$(sysctl -n machdep.cpu.brand_string 2>/dev/null || uname -m)" -RAM_LABEL="$( - python3 - <<'PY' -import os -try: - mem = int(os.popen('sysctl -n hw.memsize').read().strip()) - print(f"{mem/1024/1024/1024:.0f} GB") -except Exception: - print("unknown") -PY -)" -CORES_LABEL="$(sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN || echo unknown)" - -echo "Running oxide benchmark suite..." -cargo run --release -p xtask -- perf bench \ - --suite all \ - --mode server-sqlx \ - --iterations 100 \ - --speed-source pglite \ - > "$OXIDE_JSON" - -echo "Running native Postgres SQLx benchmark suite..." -cargo run --release -p xtask -- perf native-postgres \ - --suite all \ - --iterations 100 \ - --speed-source pglite \ - --client sqlx \ - --postgres-bin "$POSTGRES_BIN" \ - --initdb-bin "$INITDB_BIN" \ - > "$NATIVE_JSON" - -echo "Starting PGlite NodeFS socket server..." -node "$NODE_BENCH_DIR/start_nodefs_socket.mjs" \ - --ready "$NODE_READY_JSON" \ - --run-id "$RUN_ID" \ - > "$NODE_LOG" 2>&1 & -NODE_PID="$!" -cleanup_node_server() { - if kill -0 "$NODE_PID" >/dev/null 2>&1; then - kill "$NODE_PID" >/dev/null 2>&1 || true - wait "$NODE_PID" >/dev/null 2>&1 || true - fi -} -trap cleanup_node_server EXIT - -for _ in $(seq 1 300); do - if [[ -s "$NODE_READY_JSON" ]]; then - break - fi - if ! kill -0 "$NODE_PID" >/dev/null 2>&1; then - cat "$NODE_LOG" >&2 || true - echo "PGlite NodeFS socket server exited before becoming ready" >&2 - exit 1 - fi - sleep 0.1 -done - -if [[ ! -s "$NODE_READY_JSON" ]]; then - cat "$NODE_LOG" >&2 || true - echo "Timed out waiting for PGlite NodeFS socket server" >&2 - exit 1 -fi - -NODE_DATABASE_URL="$(node -e "const fs=require('fs'); console.log(JSON.parse(fs.readFileSync(process.argv[1], 'utf8')).databaseUrl)" "$NODE_READY_JSON")" -NODE_OPEN_MICROS="$(node -e "const fs=require('fs'); console.log(JSON.parse(fs.readFileSync(process.argv[1], 'utf8')).openMicros)" "$NODE_READY_JSON")" - -echo "Running PGlite NodeFS SQLx benchmark suite..." -cargo run --release -p xtask -- perf pglite-nodefs-sqlx \ - --suite all \ - --iterations 100 \ - --speed-source pglite \ - --database-url "$NODE_DATABASE_URL" \ - --open-micros "$NODE_OPEN_MICROS" \ - > "$NODE_JSON" - -cleanup_node_server -trap - EXIT - -echo "Building comparison markdown..." -node "$SCRIPT_DIR/build_bench_matrix.mjs" \ - --output "$REPORT_MD" \ - --oxide "$OXIDE_JSON" \ - --native "$NATIVE_JSON" \ - --node "$NODE_JSON" \ - --node-server "$NODE_READY_JSON" \ - --run-id "$RUN_ID" \ - --native-version "$NATIVE_VERSION" \ - --machine-os "$OS_LABEL" \ - --machine-cpu "$CPU_LABEL" \ - --machine-ram "$RAM_LABEL" \ - --machine-cores "$CORES_LABEL" - -echo "$REPORT_MD" diff --git a/scripts/sync-example-lockfiles.py b/scripts/sync-example-lockfiles.py deleted file mode 100755 index ff269725..00000000 --- a/scripts/sync-example-lockfiles.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import pathlib -import re -import sys -import tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -LOCKFILES = [ - ROOT / "examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock", -] -INTERNAL_PACKAGE_MANIFESTS = [ - ROOT / "Cargo.toml", - ROOT / "crates/assets/Cargo.toml", - ROOT / "crates/aot/aarch64-apple-darwin/Cargo.toml", - ROOT / "crates/aot/aarch64-unknown-linux-gnu/Cargo.toml", - ROOT / "crates/aot/x86_64-pc-windows-msvc/Cargo.toml", - ROOT / "crates/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_path_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 - } - missing = sorted(set(versions) - present) - if missing: - raise SystemExit( - f"{lockfile.relative_to(ROOT)} is missing internal path packages: {', '.join(missing)}" - ) - - -def sync_lockfile(lockfile: pathlib.Path, versions: dict[str, str]) -> list[str]: - check_lockfile_contains_path_packages(lockfile, versions) - lines = lockfile.read_text(encoding="utf-8").splitlines(keepends=True) - 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: - lines[version_idx] = replace_version_line(lines[version_idx], expected_version) - changes.append( - f"{lockfile.relative_to(ROOT)}: {name} {current_version} -> {expected_version}" - ) - - 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) - 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 `scripts/sync-example-lockfiles.py`", file=sys.stderr) - return 1 - - print("updated example lockfiles") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/validate.sh b/scripts/validate.sh deleted file mode 100755 index eda7ba03..00000000 --- a/scripts/validate.sh +++ /dev/null @@ -1,451 +0,0 @@ -#!/usr/bin/env sh -set -eu - -mode="${1:-pre-push}" -shift || true - -if [ "${PGLITE_OXIDE_RELEASE_STAGED:-0}" = "1" ]; then - root="$(pwd)" -else - root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -fi -cd "$root" - -cargo_bin="${CARGO_HOME:-$HOME/.cargo}/bin" -if [ -d "$cargo_bin" ]; then - PATH="$cargo_bin:$PATH" - export PATH -fi - -allow_dirty=0 -for arg in "$@"; do - case "$arg" in - --allow-dirty) - allow_dirty=1 - ;; - --*) - echo "unknown flag for $mode: $arg" >&2 - exit 2 - ;; - *) - ;; - esac -done - -run() { - printf '\n==> %s\n' "$*" - "$@" -} - -require() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - echo "run scripts/bootstrap-tools.sh to install the pinned local toolchain" >&2 - exit 1 - fi -} - -run_xtask() { - if [ -n "${PGLITE_OXIDE_XTASK:-}" ]; then - xtask="$PGLITE_OXIDE_XTASK" - if command -v cygpath >/dev/null 2>&1; then - xtask="$(cygpath -u "$xtask" 2>/dev/null || printf '%s\n' "$xtask")" - fi - run "$xtask" "$@" - else - require cargo - run cargo run -p xtask -- "$@" - fi -} - -xtask_output() { - if [ -n "${PGLITE_OXIDE_XTASK:-}" ]; then - xtask="$PGLITE_OXIDE_XTASK" - if command -v cygpath >/dev/null 2>&1; then - xtask="$(cygpath -u "$xtask" 2>/dev/null || printf '%s\n' "$xtask")" - fi - "$xtask" "$@" - else - require cargo - cargo run --quiet -p xtask -- "$@" - fi -} - -run_prek() { - require prek - stage="${1:?run_prek requires a stage}" - shift - run prek run --all-files --stage "$stage" "$@" -} - -run_prek_tracked_files() { - require prek - stage="${1:?run_prek_tracked_files requires a stage}" - printf '\n==> prek run --tracked-files --stage %s\n' "$stage" - git ls-files | - while IFS= read -r file; do - [ -e "$file" ] && printf '%s\0' "$file" - done | - xargs -0 prek run --stage "$stage" --files -} - -cargo_publish_args() { - if [ "$allow_dirty" -eq 1 ]; then - printf '%s\n' --allow-dirty - fi -} - -cargo_package_args() { - if [ "$allow_dirty" -eq 1 ]; then - printf '%s\n' --allow-dirty - fi -} - -clean_package_artifacts() { - rm -f target/package/*.crate -} - -internal_packages() { - xtask_output assets internal-packages -} - -aot_targets() { - xtask_output assets aot-targets -} - -host_aot_manifest() { - host="$1" - if [ -f "target/pglite-oxide/aot/$host/manifest.json" ]; then - printf '%s\n' "target/pglite-oxide/aot/$host/manifest.json" - elif [ -f "crates/aot/$host/artifacts/manifest.json" ]; then - printf '%s\n' "crates/aot/$host/artifacts/manifest.json" - else - return 1 - fi -} - -run_root_publish_dry_run() { - tmp="$(mktemp)" - if cargo publish -p pglite-oxide --dry-run --locked $(cargo_publish_args) >"$tmp" 2>&1; then - cat "$tmp" - rm -f "$tmp" - return 0 - fi - - status=$? - if grep -Eq 'no matching package named `pglite-oxide-(assets|aot-[^`]+)` found|failed to select a version for the requirement `pglite-oxide-(assets|aot-[^`]+) = "=' "$tmp"; then - cat >&2 <<'MSG' -warning: root crate publish dry-run could not resolve exact internal crate -versions from crates.io. - -This is expected for same-release internal asset/AOT versions. release-plz owns -the actual publish order; this validation dry-runs every internal crate before -release-plz publish/dry-run is invoked. -MSG - rm -f "$tmp" - return 0 - fi - - cat "$tmp" >&2 - rm -f "$tmp" - return "$status" -} - -validate_repo() { - require prek - run prek validate-config prek.toml - run_prek_tracked_files pre-commit -} - -validate_artifacts() { - run_xtask assets verify-committed -} - -validate_workflows() { - require actionlint - require zizmor - run actionlint - run zizmor --config .github/zizmor.yml --min-severity medium --persona auditor .github/workflows .github/actions -} - -validate_lint() { - require cargo - run scripts/check-dependency-invariants.sh - run cargo clippy --workspace --all-targets --locked -- -D warnings -} - -validate_tests() { - require cargo - run cargo check --workspace --locked - run cargo check --workspace --no-default-features --all-targets --locked - run cargo test --doc --workspace --locked - run cargo test --workspace --all-targets --locked --no-run -} - -validate_dev() { - validate_repo - validate_artifacts - validate_lint - validate_tests -} - -require_host_runtime_artifacts() { - require cargo - host="$(rustc -vV | awk '/^host:/{print $2}')" - if ! host_aot_manifest "$host" >/dev/null 2>&1; then - cat >&2 < --target-triple $host - cargo run -p xtask -- assets download --latest-compatible --target-triple $host - cargo run -p xtask -- assets install-local --target-triple $host -MSG - exit 1 - fi - if [ ! -f "target/pglite-oxide/assets/manifest.json" ]; then - cat >&2 < --target-triple $host - cargo run -p xtask -- assets download --latest-compatible --target-triple $host - cargo run -p xtask -- assets install-local --target-triple $host -MSG - exit 1 - fi - run_xtask assets install-local --target-triple "$host" - export PGLITE_OXIDE_GENERATED_ASSETS_DIR="$root/target/pglite-oxide/assets" - export PGLITE_OXIDE_GENERATED_AOT_DIR="$root/target/pglite-oxide/aot" -} - -validate_runtime_smoke() { - require_host_runtime_artifacts - export RUST_BACKTRACE="${RUST_BACKTRACE:-full}" - run cargo test -p pglite-oxide --locked \ - --test runtime_smoke \ - --test proxy_smoke \ - --test cli_smoke \ - --test performance_smoke \ - --test extensions_smoke \ - --test postgres_regression \ - -- --nocapture - run cargo test -p pglite-oxide --locked --lib pg_dump -- --nocapture -} - -validate_runtime() { - require_host_runtime_artifacts - run cargo test --workspace --all-targets --locked -} - -validate_examples() { - require cargo - require npm - run scripts/sync-example-lockfiles.py --check - run cargo check --manifest-path examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --locked - run npm --prefix examples/tauri-sqlx-vanilla ci - run npm --prefix examples/tauri-sqlx-vanilla run build -} - -validate_package() { - require cargo - clean_package_artifacts - run cargo package --workspace --exclude xtask --locked --no-verify $(cargo_package_args) - run scripts/check-crate-size.sh --enforce -} - -validate_feature_powerset() { - require cargo-hack - run cargo hack check --workspace --feature-powerset --no-dev-deps --exclude-features aot-serializer,template-runner -} - -validate_semver() { - require cargo-semver-checks - run cargo semver-checks check-release --package pglite-oxide --manifest-path Cargo.toml -} - -validate_supply_chain() { - require cargo-deny - run cargo deny check -} - -require_release_aot_artifacts() { - for target in $(aot_targets); do - manifest="$(host_aot_manifest "$target" 2>/dev/null || true)" - if [ -z "$manifest" ]; then - manifest="target/pglite-oxide/aot/$target/manifest.json" - fi - if [ ! -f "$manifest" ]; then - echo "missing release AOT artifacts for $target; download them from the successful Assets workflow before release validation" >&2 - exit 1 - fi - python3 - "$manifest" <<'PY' -import json -import sys -path = sys.argv[1] -with open(path, encoding="utf-8") as f: - manifest = json.load(f) -if not manifest.get("artifacts"): - raise SystemExit(f"{path} does not contain generated AOT artifacts") -PY - done -} - -require_release_portable_assets() { - if [ ! -f "target/pglite-oxide/assets/manifest.json" ]; then - echo "missing release portable assets; download or build Assets workflow outputs before release validation" >&2 - exit 1 - fi -} - -validate_release_aot_artifacts() { - for target in $(aot_targets); do - run_xtask assets check-aot --target-triple "$target" - done -} - -validate_release() { - require cargo - if [ "${PGLITE_OXIDE_RELEASE_STAGED:-0}" != "1" ]; then - require_release_portable_assets - require_release_aot_artifacts - run_xtask release stage - ( - cd target/pglite-oxide/release/workspace - PGLITE_OXIDE_RELEASE_STAGED=1 scripts/validate.sh release --allow-dirty - ) - return 0 - fi - require_release_portable_assets - require_release_aot_artifacts - run_xtask assets check --strict-generated - validate_release_aot_artifacts - validate_package - for package in $(internal_packages); do - run cargo publish -p "$package" --dry-run --locked $(cargo_publish_args) - done - printf '\n==> cargo publish -p pglite-oxide --dry-run --locked\n' - run_root_publish_dry_run -} - -case "$mode" in - commit-msg) - require prek - run prek run --stage commit-msg --commit-msg-filename "${1:?commit-msg mode requires a message file}" - ;; - - pre-commit) - run_prek pre-commit - ;; - - pre-push) - run_prek pre-push - ;; - - repo) - validate_repo - ;; - - artifacts) - validate_artifacts - ;; - - lint) - validate_lint - ;; - - test) - validate_tests - ;; - - workflows) - validate_workflows - ;; - - dev) - validate_dev - ;; - - runtime) - validate_runtime - ;; - - runtime-smoke) - validate_runtime_smoke - ;; - - examples) - validate_examples - ;; - - package) - validate_package - ;; - - feature-powerset) - validate_feature_powerset - ;; - - semver) - validate_semver - ;; - - supply-chain) - validate_supply_chain - ;; - - dev-ci) - validate_dev - validate_examples - ;; - - ci) - validate_dev - validate_workflows - validate_examples - validate_package - validate_feature_powerset - validate_semver - validate_supply_chain - ;; - - release) - validate_release - ;; - - *) - cat >&2 <<'MSG' -usage: scripts/validate.sh [--allow-dirty] - -modes: - commit-msg validate a Conventional Commit message with prek - pre-commit run all pre-commit prek hooks - pre-push run all pre-push prek hooks - repo repository hygiene and formatting - workflows actionlint and zizmor GitHub Actions checks - lint dependency invariants and clippy - test source-only checks, doctests, and test compilation - dev repo, source-only asset checks, lint, and tests/compile gate - runtime require host generated assets and run runtime tests - runtime-smoke require host generated assets and run runtime smoke tests only - examples Tauri/Rust/frontend example checks - package package all published crates and enforce size limits - feature-powerset cargo-hack feature combination checks - semver cargo-semver-checks public API compatibility - supply-chain cargo-deny dependency checks - dev-ci repo, artifacts, lint, test, and examples - ci full local CI parity lane - release package generated release workspace and publish-dry-run internals - artifacts verify source-controlled asset inputs and AOT crate templates -MSG - exit 2 - ;; -esac diff --git a/src/bin/pglite_dump.rs b/src/bin/pglite_dump.rs deleted file mode 100644 index 243c50a4..00000000 --- a/src/bin/pglite_dump.rs +++ /dev/null @@ -1,63 +0,0 @@ -use anyhow::Result; -#[cfg(feature = "extensions")] -use pglite_oxide::{PgDumpOptions, PgliteServer}; -#[cfg(feature = "extensions")] -use std::env; -#[cfg(feature = "extensions")] -use std::path::PathBuf; - -#[cfg(feature = "extensions")] -#[derive(Debug)] -struct Args { - root: PathBuf, - passthrough: Vec, -} - -fn main() -> Result<()> { - #[cfg(not(feature = "extensions"))] - { - anyhow::bail!("pglite-dump requires the `extensions` feature"); - } - #[cfg(feature = "extensions")] - { - let Args { root, passthrough } = parse_args()?; - let server = PgliteServer::builder().path(root).start()?; - let sql = server.dump_sql(PgDumpOptions::new().args(passthrough))?; - print!("{sql}"); - server.shutdown()?; - Ok(()) - } -} - -#[cfg(feature = "extensions")] -fn parse_args() -> Result { - let mut root = PathBuf::from("./.pglite"); - let mut passthrough = Vec::new(); - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--root" => { - root = PathBuf::from( - args.next() - .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?, - ); - } - "--help" | "-h" => { - print_usage(); - std::process::exit(0); - } - "--" => { - passthrough.extend(args); - break; - } - other => passthrough.push(other.to_string()), - } - } - Ok(Args { root, passthrough }) -} - -#[cfg(feature = "extensions")] -fn print_usage() { - eprintln!("Usage: pglite-dump --root PATH -- [pg_dump args]"); - eprintln!("Example: pglite-dump --root ./.pglite -- --schema-only"); -} diff --git a/src/bin/pglite_proxy.rs b/src/bin/pglite_proxy.rs deleted file mode 100644 index 8f9d9511..00000000 --- a/src/bin/pglite_proxy.rs +++ /dev/null @@ -1,149 +0,0 @@ -use anyhow::{Context, Result, bail}; -use pglite_oxide::PgliteServer; -#[cfg(feature = "extensions")] -use pglite_oxide::extensions; -use std::env; -use std::net::SocketAddr; -use std::path::PathBuf; - -#[derive(Debug)] -enum Bind { - Tcp(SocketAddr), - #[cfg(unix)] - Unix(PathBuf), -} - -#[derive(Debug)] -struct Args { - root: Option, - temporary: bool, - bind: Bind, - print_uri: bool, - postgres_config: Vec<(String, String)>, - extensions: Vec, -} - -fn main() -> Result<()> { - let args = parse_args()?; - let mut builder = if args.temporary { - PgliteServer::builder().temporary() - } else if let Some(root) = args.root { - PgliteServer::builder().path(root) - } else { - PgliteServer::builder().path("./.pglite") - }; - - builder = match args.bind { - Bind::Tcp(addr) => builder.tcp(addr), - #[cfg(unix)] - Bind::Unix(path) => builder.unix(path), - }; - builder = builder.postgres_configs(args.postgres_config); - - #[cfg(feature = "extensions")] - { - for name in &args.extensions { - let extension = extensions::by_sql_name(name) - .ok_or_else(|| anyhow::anyhow!("unknown bundled extension: {name}"))?; - builder = builder.extension(extension); - } - } - #[cfg(not(feature = "extensions"))] - if !args.extensions.is_empty() { - bail!("this pglite-proxy build was compiled without bundled extension support"); - } - - let server = builder.start()?; - if args.print_uri { - println!("{}", server.database_url()); - } else { - eprintln!("listening: {}", server.database_url()); - } - - loop { - std::thread::park(); - } -} - -fn parse_args() -> Result { - let mut root = None; - let mut temporary = false; - let mut print_uri = false; - let mut postgres_config = Vec::new(); - let mut extensions = Vec::new(); - let mut bind = Bind::Tcp("127.0.0.1:5432".parse().expect("valid default TCP addr")); - - let mut args = env::args().skip(1); - while let Some(arg) = args.next() { - match arg.as_str() { - "--temporary" => temporary = true, - "--root" => { - let value = args - .next() - .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?; - root = Some(PathBuf::from(value)); - temporary = false; - } - "--tcp" => { - let value = args.next().unwrap_or_else(|| "127.0.0.1:5432".to_string()); - bind = Bind::Tcp( - value - .parse() - .with_context(|| format!("parse TCP bind address {value}"))?, - ); - } - #[cfg(unix)] - "--unix" | "--uds" => { - let value = args - .next() - .unwrap_or_else(|| "/tmp/.s.PGSQL.5432".to_string()); - bind = Bind::Unix(PathBuf::from(value)); - } - "--print-uri" => print_uri = true, - "--postgres-config" => { - let value = args - .next() - .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; - let (name, value) = value - .split_once('=') - .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; - postgres_config.push((name.to_owned(), value.to_owned())); - } - "--extension" => { - let value = args - .next() - .ok_or_else(|| anyhow::anyhow!("--extension requires a name"))?; - extensions.push(value); - } - "--help" | "-h" => { - print_usage(); - std::process::exit(0); - } - other => bail!("unknown argument: {other}"), - } - } - - Ok(Args { - root, - temporary, - bind, - print_uri, - postgres_config, - extensions, - }) -} - -fn print_usage() { - eprintln!( - "Usage: pglite-proxy [--temporary | --root PATH] [--tcp ADDR | --unix PATH] [--print-uri] [--postgres-config NAME=VALUE] [--extension NAME]" - ); - eprintln!(" --temporary Use an ephemeral database removed on exit"); - eprintln!(" --root PATH Runtime and cluster root. Default: ./.pglite"); - eprintln!(" --tcp ADDR Listen on TCP. Use 127.0.0.1:0 for a random port"); - #[cfg(unix)] - eprintln!(" --unix PATH Listen on a Unix socket path"); - eprintln!(" --print-uri Print the PostgreSQL connection URI to stdout"); - eprintln!(" --postgres-config NAME=VALUE"); - eprintln!(" Set a PostgreSQL startup GUC on the embedded backend"); - eprintln!(" --extension NAME Enable a bundled extension that passed the smoke suite"); -} diff --git a/src/bindings/wasix-rust/THIRD_PARTY_NOTICES.md b/src/bindings/wasix-rust/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..17a90be2 --- /dev/null +++ b/src/bindings/wasix-rust/THIRD_PARTY_NOTICES.md @@ -0,0 +1,17 @@ +# oliphaunt-wasix Third-Party Notices + +`oliphaunt-wasix` ships WASIX PostgreSQL runtime assets, selected SQL extensions, +and target-specific Wasmer AOT artifacts. + +The PostgreSQL runtime is derived from PostgreSQL 18 source pinned under +`src/postgres/versions/18/` and built with the WASM/WASIX patch stack owned by +`src/runtimes/liboliphaunt/wasix/assets/build/postgres/patches/`. + +- PostgreSQL license: https://www.postgresql.org/about/licence/ +- ICU / Unicode License v3: https://github.com/unicode-org/icu/blob/main/LICENSE + +Third-party source pins for optional external extensions are maintained in +`src/sources/third-party/`, and WASIX toolchain inputs are maintained in +`src/sources/toolchains/`. Exact SQL extension selection is modeled in +`src/extensions/`; generated WASM assets must include only the +extension artifacts explicitly selected for the release payload. diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/.release-semantic-inputs.json b/src/bindings/wasix-rust/crates/oliphaunt-wasix/.release-semantic-inputs.json new file mode 100644 index 00000000..5f08b775 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/.release-semantic-inputs.json @@ -0,0 +1,79 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-wasix-rust", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "sdk-package-producers", + "paths": [ + "tools/release/build-sdk-ci-artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/build-sdk-ci-artifacts.mjs", + "sha256": "470978efc3dbe66b4fd438b05f9b55d5492a8c89ff1f163fb6db4c5bb95d42e4" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "wasix-rust-sdk-source-producer", + "paths": [ + "tools/release/package_oliphaunt_wasix_sdk_crate.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_oliphaunt_wasix_sdk_crate.mjs", + "sha256": "a3352f7a2254395b722c571097ab065cfdc33f0c9b79af97b42db4ebb86deacb" + } + ] + } + ], + "sha256": "ce869e8f5be57f8477447f18370d9442eda54688ef781f791c21a015771068d2" +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/CHANGELOG.md b/src/bindings/wasix-rust/crates/oliphaunt-wasix/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml new file mode 100644 index 00000000..54317c33 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -0,0 +1,150 @@ +[package] +name = "oliphaunt-wasix" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Embedded Postgres for Rust tests and local apps. No Docker, works with SQLx and any Postgres client." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +documentation = "https://docs.rs/oliphaunt-wasix" +keywords = ["postgres", "oliphaunt", "wasm", "database", "embedded"] +categories = ["database-implementations", "wasm", "development-tools::testing"] +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_wasix_relay" +build = "build.rs" +exclude = [ + "Cargo.toml.orig", + "release.toml", +] + +[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"] +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] +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 = "38" +oliphaunt-npm-version-checked = "0.4.5" +runtime-archive-sha256 = "7dccedb08fdc32b0092ff92a0882d911230e0361d0f4fdf228d6a6cb7d981178" +oliphaunt-wasix-sha256 = "da58c392818149789b8ca9824952abf20ed1a084e7b580369a5478e6db280b05" +pgdata-template-archive-sha256 = "6155909517d8e5e8979a49fbd635d980474fccf7f5124e77316d213655f6235a" +initdb-wasix-sha256 = "8c2b936abfd01ba7d7272897a1719ce2a0e2bfaa4835bea3458f462afe74f8fc" + +[dependencies] +anyhow = "1" +async-trait = "0.1" +tar = "0.4" +zstd = { version = "0.13", default-features = false } +directories = "6" +tracing = "0.1" +flate2 = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +regex = "1" +tempfile = "3" +hex = "0.4" +sha2 = "0.10" +dunce = "1" +filetime = "0.2" +liboliphaunt-wasix-portable = { version = "=0.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } +oliphaunt-wasix-tools = { version = "=0.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools", 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", default-features = false, features = [ + "sys", + "headless", + "compiler", + "wasmer-artifact-load", +] } +wasmer-config = "=0.702.0" +wasmer-types = "=7.2.0" +wasmer-wasix = { version = "=0.702.0", default-features = false, features = [ + "sys-minimal", + "sys-poll", + "host-vnet", + "time", +] } +webc = "=12.0.0" + +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] +liboliphaunt-wasix-aot-aarch64-apple-darwin = { version = "=0.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin" } +oliphaunt-wasix-tools-aot-aarch64-apple-darwin = { version = "=0.0.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.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.0.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.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu = { version = "=0.0.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.0.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc" } +oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc = { version = "=0.0.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 = [ + "postgres", + "runtime-tokio", +] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +tokio-postgres = "0.7" + +[[bin]] +name = "oliphaunt-wasix-dump" +path = "src/bin/oliphaunt_wasix_dump.rs" +required-features = ["tools"] + +[[bin]] +name = "oliphaunt-wasix-proxy" +path = "src/bin/oliphaunt_wasix_proxy.rs" diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md b/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md new file mode 100644 index 00000000..f6aee5e8 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md @@ -0,0 +1,118 @@ +

oliphaunt-wasix

+ +

+ Embedded Postgres for Rust tests and local apps.
+ Real PostgreSQL. Direct Rust API or a local Postgres URL. +

+ +

+ Guide + · + Performance + · + Extensions + · + Dump & Upgrade + · + Runtime + · + Tauri +

+ +

+ CI + crates.io + docs.rs + MSRV + License +

+ +`oliphaunt-wasix` brings the WASIX Oliphaunt/Postgres runtime to Rust with a +small API. Open a database directly with `Oliphaunt`, or hand `OliphauntServer` +to SQLx and any standard Postgres client. The release-built runtime is +PostgreSQL 18.4. Cargo resolves the matching WASIX runtime and AOT artifact +crates; applications do not download runtime assets at first database open. + +## Add Postgres In One Minute ⚡ + +Already using SQLx or another Postgres client? The WASIX API shape is: + +```sh +cargo add oliphaunt-wasix +``` + +```rust,no_run +use oliphaunt_wasix::OliphauntServer; +use sqlx::{Connection, Row}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let server = OliphauntServer::temporary_tcp()?; + // For a persistent TCP server: + // let server = OliphauntServer::builder().path("./.oliphaunt").start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()).await?; + + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} +``` + +That's it. Real PostgreSQL, no service setup. + +## Why oliphaunt-wasix ✨ + +Postgres should be as easy to add to a Rust project as SQLite. + +- ⚡ **No service tax**: no Docker, no local Postgres, no testcontainers. +- 🔌 **Use your real stack**: SQLx, `tokio-postgres`, CLIs, and other clients + connect through a normal local URL. +- 🌉 **Proxy included**: expose an embedded database to non-Rust tools with + `oliphaunt-wasix-proxy`. +- 🧪 **Clean tests**: temporary databases are isolated, fast, and removed on + drop. +- 💾 **Persistent apps**: keep local app data across restarts when you want it. +- 🧩 **Extensions available**: install exact extension release assets owned by + your application. +- 📦 **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 🚀 + +Current local snapshot on `Apple M1 Pro`, `16 GB RAM`, and `macOS 26.4.1`. +Full numbers and reproduction steps live in the +[performance guide](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/reference/performance.md). Lower is better. + +| Operation | native pg + SQLx | oliphaunt-wasix + SQLx | vanilla Oliphaunt + SQLx | +|---|---:|---:|---:| +| 25,000 INSERTs in one transaction | 132.36 ms | 149.54 ms | 257.02 ms | +| 25,000 INSERTs in one statement | 46.14 ms | 59.39 ms | 117.19 ms | +| 25,000 INSERTs into an indexed table | 188.72 ms | 253.38 ms | 352.64 ms | +| 5,000 indexed SELECTs | 81.39 ms | 125.31 ms | 203.05 ms | +| 25,000 indexed UPDATEs | 351.05 ms | 578.96 ms | 720.63 ms | + +`oliphaunt-wasix` stays close to native Postgres while running entirely embedded +and consistently performs better than vanilla Oliphaunt. + +## Extensions 🧩 + +WASIX extensions are exact package artifacts. The base runtime does not include +optional extension payloads. Applications select only the extension packages +they use. + +## Docs + +- [WASM guide](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/sdk/wasm/guide.mdx) +- [Extensions](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/reference/extensions.mdx) +- [Performance guide](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/reference/performance.md) +- [Dump and upgrade guide](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/sdk/wasm/dump-restore.md) +- [Tauri usage](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/learn/tauri.md) +- [WASIX runtime guide](https://github.com/f0rr0/oliphaunt/blob/main/src/docs/content/sdk/wasm/runtime.md) diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/build.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/build.rs new file mode 100644 index 00000000..2428c093 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/build.rs @@ -0,0 +1,100 @@ +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_WASIX_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-wasix 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_runtime_and_aot_manifests() { + let instructions = relay_manifest_instructions([ + ( + "DEP_OLIPHAUNT_ARTIFACT_LIBOLIPHAUNT_WASIX_RUNTIME_MANIFEST".to_owned(), + "/tmp/runtime.toml".to_owned(), + ), + ( + "DEP_OLIPHAUNT_ARTIFACT_LIBOLIPHAUNT_WASIX_AOT_LINUX_X64_GNU_MANIFEST".to_owned(), + "/tmp/aot.toml".to_owned(), + ), + ]) + .unwrap(); + assert!(instructions.contains( + &"cargo::metadata=liboliphaunt_wasix_runtime_manifest=/tmp/runtime.toml".to_owned() + )); + assert!( + instructions.contains( + &"cargo::metadata=liboliphaunt_wasix_aot_linux_x64_gnu_manifest=/tmp/aot.toml" + .to_owned() + ) + ); + } + + #[test] + fn ignores_own_downstream_metadata() { + let instructions = relay_manifest_instructions([( + "DEP_OLIPHAUNT_ARTIFACT_WASIX_RELAY_LIBOLIPHAUNT_WASIX_RUNTIME_MANIFEST".to_owned(), + "/tmp/runtime.toml".to_owned(), + )]) + .unwrap(); + assert!(instructions.is_empty()); + } +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml new file mode 100644 index 00000000..23a406a2 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml @@ -0,0 +1,6 @@ +id = "oliphaunt-wasix-rust" +owner = "@oliphaunt/wasix-rust" +kind = "sdk" +publish_targets = ["crates-io"] +registry_packages = ["crates:oliphaunt-wasix"] +release_artifacts = ["cargo-crate"] 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 new file mode 100644 index 00000000..29aa3698 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_dump.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +#[cfg(feature = "tools")] +use oliphaunt_wasix::{OliphauntServer, PgDumpOptions}; +#[cfg(feature = "tools")] +use std::env; +#[cfg(feature = "tools")] +use std::path::PathBuf; + +#[cfg(feature = "tools")] +#[derive(Debug)] +struct Args { + root: PathBuf, + passthrough: Vec, +} + +fn main() -> Result<()> { + #[cfg(not(feature = "tools"))] + { + anyhow::bail!("oliphaunt-wasix-dump requires the `tools` feature"); + } + #[cfg(feature = "tools")] + { + let Args { root, passthrough } = parse_args()?; + let server = OliphauntServer::builder().path(root).start()?; + let sql = server.dump_sql(PgDumpOptions::new().args(passthrough))?; + print!("{sql}"); + server.shutdown()?; + Ok(()) + } +} + +#[cfg(feature = "tools")] +fn parse_args() -> Result { + let mut root = PathBuf::from("./.oliphaunt"); + let mut passthrough = Vec::new(); + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--root" => { + root = PathBuf::from( + args.next() + .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?, + ); + } + "--help" | "-h" => { + print_usage(); + std::process::exit(0); + } + "--" => { + passthrough.extend(args); + break; + } + other => passthrough.push(other.to_string()), + } + } + Ok(Args { root, passthrough }) +} + +#[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/bin/oliphaunt_wasix_proxy.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_proxy.rs new file mode 100644 index 00000000..e7415cc2 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_proxy.rs @@ -0,0 +1,149 @@ +use anyhow::{Context, Result, bail}; +use oliphaunt_wasix::OliphauntServer; +#[cfg(feature = "extensions")] +use oliphaunt_wasix::extensions; +use std::env; +use std::net::SocketAddr; +use std::path::PathBuf; + +#[derive(Debug)] +enum Bind { + Tcp(SocketAddr), + #[cfg(unix)] + Unix(PathBuf), +} + +#[derive(Debug)] +struct Args { + root: Option, + temporary: bool, + bind: Bind, + print_uri: bool, + postgres_config: Vec<(String, String)>, + extensions: Vec, +} + +fn main() -> Result<()> { + let args = parse_args()?; + let mut builder = if args.temporary { + OliphauntServer::builder().temporary() + } else if let Some(root) = args.root { + OliphauntServer::builder().path(root) + } else { + OliphauntServer::builder().path("./.oliphaunt") + }; + + builder = match args.bind { + Bind::Tcp(addr) => builder.tcp(addr), + #[cfg(unix)] + Bind::Unix(path) => builder.unix(path), + }; + builder = builder.postgres_configs(args.postgres_config); + + #[cfg(feature = "extensions")] + { + for name in &args.extensions { + let extension = extensions::by_sql_name(name) + .ok_or_else(|| anyhow::anyhow!("unknown extension: {name}"))?; + builder = builder.extension(extension); + } + } + #[cfg(not(feature = "extensions"))] + if !args.extensions.is_empty() { + bail!("this oliphaunt-wasix-proxy build was compiled without extension support"); + } + + let server = builder.start()?; + if args.print_uri { + println!("{}", server.database_url()); + } else { + eprintln!("listening: {}", server.database_url()); + } + + loop { + std::thread::park(); + } +} + +fn parse_args() -> Result { + let mut root = None; + let mut temporary = false; + let mut print_uri = false; + let mut postgres_config = Vec::new(); + let mut extensions = Vec::new(); + let mut bind = Bind::Tcp("127.0.0.1:5432".parse().expect("valid default TCP addr")); + + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--temporary" => temporary = true, + "--root" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--root requires a path"))?; + root = Some(PathBuf::from(value)); + temporary = false; + } + "--tcp" => { + let value = args.next().unwrap_or_else(|| "127.0.0.1:5432".to_string()); + bind = Bind::Tcp( + value + .parse() + .with_context(|| format!("parse TCP bind address {value}"))?, + ); + } + #[cfg(unix)] + "--unix" | "--uds" => { + let value = args + .next() + .unwrap_or_else(|| "/tmp/.s.PGSQL.5432".to_string()); + bind = Bind::Unix(PathBuf::from(value)); + } + "--print-uri" => print_uri = true, + "--postgres-config" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; + let (name, value) = value + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("--postgres-config requires name=value"))?; + postgres_config.push((name.to_owned(), value.to_owned())); + } + "--extension" => { + let value = args + .next() + .ok_or_else(|| anyhow::anyhow!("--extension requires a name"))?; + extensions.push(value); + } + "--help" | "-h" => { + print_usage(); + std::process::exit(0); + } + other => bail!("unknown argument: {other}"), + } + } + + Ok(Args { + root, + temporary, + bind, + print_uri, + postgres_config, + extensions, + }) +} + +fn print_usage() { + eprintln!( + "Usage: oliphaunt-wasix-proxy [--temporary | --root PATH] [--tcp ADDR | --unix PATH] [--print-uri] [--postgres-config NAME=VALUE] [--extension NAME]" + ); + eprintln!(" --temporary Use an ephemeral database removed on exit"); + eprintln!(" --root PATH Runtime and cluster root. Default: ./.oliphaunt"); + eprintln!(" --tcp ADDR Listen on TCP. Use 127.0.0.1:0 for a random port"); + #[cfg(unix)] + eprintln!(" --unix PATH Listen on a Unix socket path"); + eprintln!(" --print-uri Print the PostgreSQL connection URI to stdout"); + eprintln!(" --postgres-config NAME=VALUE"); + eprintln!(" Set a PostgreSQL startup GUC on the embedded backend"); + eprintln!(" --extension NAME Enable an installed extension by SQL name"); +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs new file mode 100644 index 00000000..0122fe47 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs @@ -0,0 +1,31 @@ +#![doc = include_str!("../README.md")] +#![deny(unsafe_code)] + +mod oliphaunt; +mod protocol; + +#[cfg(feature = "extensions")] +pub use oliphaunt::extensions; + +pub use oliphaunt::{ + DataDirArchiveFormat, DataTransferContainer, DescribeQueryParam, DescribeQueryResult, + DescribeResultField, EngineCapabilities, ExecProtocolOptions, ExecProtocolResult, FieldInfo, + GlobalListenerHandle, ListenerHandle, NoticeCallback, Oliphaunt, OliphauntBuilder, + OliphauntError, OliphauntServer, OliphauntServerBuilder, ParserMap, PostgresConfig, + QueryOptions, QueryTemplate, Results, RowMode, Serializer, SerializerMap, TemplatedQuery, + Transaction, TypeParser, format_query, quote_identifier, +}; +#[cfg(feature = "tools")] +pub use oliphaunt::{PgDumpOptions, PsqlOptions, preflight_wasix_tools}; +pub use protocol::messages::{BackendMessage, DatabaseError, NoticeMessage}; + +#[doc(hidden)] +pub use oliphaunt::{ + AssetManifestMetadata, DebugLevel, FsTraceSnapshot, InstallOptions, InstallOutcome, MountInfo, + OliphauntPaths, OliphauntProxy, PgDataTemplate, PgDataTemplateManifest, PhaseTiming, + ProtocolStatsSnapshot, asset_manifest_metadata, build_pgdata_template, capture_phase_timings, + disable_protocol_stats, ensure_cluster, fs_trace_snapshot, install_and_init, + install_and_init_in, install_default, install_extension_archive, install_extension_bytes, + install_into, install_with_options, measure_phase, preload_runtime_module, + protocol_stats_snapshot, record_phase_timing, reset_fs_trace, reset_protocol_stats, +}; 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 new file mode 100644 index 00000000..84edf678 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -0,0 +1,1131 @@ +use std::collections::{BTreeSet, HashMap}; +use std::fs; +use std::io::{Cursor, Read}; +use std::path::{Path, PathBuf}; +use std::sync::{Mutex, OnceLock}; + +use anyhow::{Context, Result, bail, ensure}; +use directories::ProjectDirs; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use wasmer::sys::{EngineBuilder, Features, NativeEngineExt}; +use wasmer::{Engine, Module}; +use zstd::stream::read::Decoder as ZstdDecoder; + +use super::assets; +#[cfg(feature = "extensions")] +use super::extensions::Extension; +use super::timing; + +const RUNTIME_ARTIFACT: &str = "runtime:oliphaunt"; +const EXPECTED_AOT_ENGINE: &str = "llvm-opta"; +const EXPECTED_WASMER_VERSION: &str = "7.2.0"; +const EXPECTED_WASMER_WASIX_VERSION: &str = "0.702.0"; +const AOT_ENGINE_ID: &str = concat!( + "engine=", + "llvm-opta", + ";wasmer=", + "7.2.0", + ";wasmer-wasix=", + "0.702.0", + ";cpu=generic-baseline" +); +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(); +static MODULE_CACHE: OnceLock>> = OnceLock::new(); + +#[derive(Debug, Clone)] +struct InstalledArtifact { + path: PathBuf, + sha256: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum AotVerifyMode { + Fast, + Full, +} + +pub(crate) fn headless_engine() -> Engine { + HEADLESS_ENGINE + .get_or_init(|| { + let _phase = timing::phase("wasmer.headless_engine"); + let mut features = Features::new(); + features.exceptions(true); + EngineBuilder::headless() + .set_features(Some(features)) + .engine() + .into() + }) + .clone() +} + +pub(crate) fn load_runtime_module() -> Result<(Engine, Module)> { + let engine = headless_engine(); + let module = load_artifact_module(&engine, RUNTIME_ARTIFACT)?; + Ok((engine, module)) +} + +pub(crate) fn engine_identity() -> &'static str { + AOT_ENGINE_ID +} + +pub(crate) fn preload_runtime_artifact() -> Result<()> { + let _ = load_runtime_module()?; + Ok(()) +} + +#[cfg(feature = "extensions")] +pub(crate) fn preload_extension_artifact(extension: Extension) -> Result<()> { + let engine = headless_engine(); + for module in extension.native_support_modules() { + if let Some(aot_name) = module.aot_name() { + let _ = load_artifact_module(&engine, aot_name)?; + } + } + let _ = load_extension_module(&engine, extension)?; + Ok(()) +} + +#[cfg(feature = "extensions")] +pub(crate) fn load_extension_module( + engine: &Engine, + extension: Extension, +) -> Result> { + let Some(aot_name) = extension.aot_name() else { + return Ok(None); + }; + load_artifact_module(engine, aot_name).map(Some) +} + +pub(crate) fn load_artifact_module(engine: &Engine, artifact_name: &str) -> Result { + let artifact = install_artifact(artifact_name)?; + let cache_key = format!("{artifact_name}:{}", artifact.sha256); + let module_cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let mut modules = module_cache.lock().expect("AOT module cache poisoned"); + if let Some(module) = modules.get(&cache_key) { + return Ok(module.clone()); + } + + let module = match deserialize_headless(engine, &artifact.path) { + Ok(module) => module, + Err(err) if aot_verify_mode()? == AotVerifyMode::Fast => { + let _phase = timing::phase("aot.rebuild_after_deserialize_failure"); + forget_installed_artifact(artifact_name); + remove_cached_artifact(&artifact.path)?; + let artifact = rebuild_artifact(artifact_name).with_context(|| { + format!("rebuild AOT artifact '{artifact_name}' after deserialize failure") + })?; + deserialize_headless(engine, &artifact.path).with_context(|| { + format!( + "deserialize rebuilt Wasmer AOT artifact '{}' after initial failure: {err:#}", + artifact.path.display() + ) + })? + } + Err(err) => return Err(err), + }; + modules.insert(cache_key, module.clone()); + Ok(module) +} + +#[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 { + load_artifact_module(engine, "tool:initdb") +} + +fn install_artifact(name: &str) -> Result { + if let Some(artifact) = installed_artifact(name) { + return Ok(artifact); + } + + let _guard = AOT_INSTALL_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("AOT install lock poisoned"); + if let Some(artifact) = installed_artifact(name) { + return Ok(artifact); + } + + let manifest_artifact = { + let _phase = timing::phase("aot.manifest_validation"); + target_manifest_artifact(name)? + }; + let verify_mode = aot_verify_mode()?; + + if let Some(artifact) = cached_raw_artifact(name, &manifest_artifact, verify_mode)? { + remember_installed_artifact(name, artifact.clone()); + return Ok(artifact); + } + + let artifact = materialize_artifact(name, &manifest_artifact, verify_mode)?; + remember_installed_artifact(name, artifact.clone()); + Ok(artifact) +} + +fn rebuild_artifact(name: &str) -> Result { + let _guard = AOT_INSTALL_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("AOT install lock poisoned"); + forget_installed_artifact(name); + let manifest_artifact = { + let _phase = timing::phase("aot.manifest_validation"); + target_manifest_artifact(name)? + }; + let artifact = materialize_artifact(name, &manifest_artifact, aot_verify_mode()?)?; + remember_installed_artifact(name, artifact.clone()); + Ok(artifact) +} + +fn materialize_artifact( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result { + let _phase = timing::phase("aot.materialize"); + let raw = artifact_raw_bytes(name, manifest_artifact, verify_mode)?; + let hash = expected_raw_hash(name, manifest_artifact, &raw, verify_mode)?; + let cache_path = cache_path(name, &hash)?; + remove_cached_artifact(&cache_path)?; + + if let Some(parent) = cache_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create AOT cache directory {}", parent.display()))?; + } + let tmp_path = + cache_path.with_extension(format!("bin.{}.{}.tmp", std::process::id(), tmp_suffix())); + fs::write(&tmp_path, raw) + .with_context(|| format!("write AOT artifact {}", tmp_path.display()))?; + if let Err(err) = fs::rename(&tmp_path, &cache_path) { + remove_file_if_exists(&tmp_path).ok(); + return Err(err).with_context(|| { + format!( + "promote AOT artifact {} -> {}", + tmp_path.display(), + cache_path.display() + ) + }); + } + + write_cache_receipt(name, manifest_artifact, &cache_path, &hash)?; + Ok(InstalledArtifact { + path: cache_path, + sha256: hash, + }) +} + +fn cached_raw_artifact( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result> { + let Some(raw_sha256) = manifest_artifact.raw_sha256.as_deref() else { + return Ok(None); + }; + let cache_path = cache_path(name, raw_sha256)?; + if !cache_path.exists() { + return Ok(None); + } + + match verify_mode { + AotVerifyMode::Fast => { + let _phase = timing::phase("aot.cache_receipt_verify"); + if !cache_receipt_matches(name, manifest_artifact, &cache_path, raw_sha256)? { + remove_cached_artifact(&cache_path)?; + return Ok(None); + } + } + AotVerifyMode::Full => { + let _phase = timing::phase("aot.raw_cache_verify"); + let (actual, actual_size) = sha256_file_with_len(&cache_path)?; + if !actual.eq_ignore_ascii_case(raw_sha256) { + remove_cached_artifact(&cache_path)?; + return Ok(None); + } + if let Some(raw_size) = manifest_artifact.raw_size { + ensure!( + actual_size == raw_size, + "cached AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", + actual_size + ); + } + } + } + Ok(Some(InstalledArtifact { + path: cache_path, + sha256: raw_sha256.to_owned(), + })) +} + +fn installed_artifact(name: &str) -> Option { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .get(name) + .filter(|artifact| artifact.path.exists()) + .cloned() +} + +fn remember_installed_artifact(name: &str, artifact: InstalledArtifact) { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .insert(name.to_string(), artifact); +} + +fn forget_installed_artifact(name: &str) { + INSTALLED_ARTIFACTS + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .expect("installed AOT artifact cache poisoned") + .remove(name); +} + +fn remove_cached_artifact(path: &Path) -> Result<()> { + remove_file_if_exists(path)?; + remove_file_if_exists(&receipt_path(path)) +} + +fn remove_file_if_exists(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(err).with_context(|| format!("remove {}", path.display())), + } +} + +fn tmp_suffix() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default() +} + +fn artifact_raw_bytes( + name: &str, + manifest_artifact: &AotManifestArtifact, + verify_mode: AotVerifyMode, +) -> Result> { + let bytes = if let Some(bytes) = target_artifact_bytes(name) { + bytes.to_vec() + } else { + bail!( + "no package-manager-resolved Wasmer LLVM AOT artifact named '{name}' is available for target {}; publish and stage the matching liboliphaunt-wasix AOT artifact crate with the application", + target_triple() + ) + }; + if verify_mode == AotVerifyMode::Full { + validate_compressed_artifact_manifest(name, manifest_artifact, &bytes)?; + } + + if bytes.starts_with(ZSTD_MAGIC) { + let _phase = timing::phase("aot.decompress"); + let mut decoder = ZstdDecoder::new(Cursor::new(bytes)) + .with_context(|| format!("decode compressed AOT artifact '{name}'"))?; + let mut raw = Vec::new(); + decoder + .read_to_end(&mut raw) + .with_context(|| format!("decompress AOT artifact '{name}'"))?; + ensure!( + !raw.is_empty(), + "AOT artifact '{name}' decompressed to zero bytes" + ); + Ok(raw) + } else { + Ok(bytes.to_vec()) + } +} + +fn expected_raw_hash( + name: &str, + manifest_artifact: &AotManifestArtifact, + raw: &[u8], + verify_mode: AotVerifyMode, +) -> Result { + if let Some(raw_size) = manifest_artifact.raw_size { + ensure!( + raw.len() as u64 == raw_size, + "AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", + raw.len() + ); + } + + let Some(raw_sha256) = &manifest_artifact.raw_sha256 else { + ensure!( + verify_mode == AotVerifyMode::Full, + "AOT artifact '{name}' is missing raw-sha256 metadata; rebuild assets or set OLIPHAUNT_WASM_AOT_VERIFY=full for strict hash-derived cache keys" + ); + return Ok(sha256_hex(raw)); + }; + if verify_mode == AotVerifyMode::Full { + let actual = sha256_hex(raw); + ensure!( + actual.eq_ignore_ascii_case(raw_sha256), + "AOT artifact '{name}' raw hash mismatch: manifest={raw_sha256} actual={actual}" + ); + } + Ok(raw_sha256.clone()) +} + +fn target_manifest_artifact(name: &str) -> Result { + let manifest = target_aot_manifest()?; + ensure!( + manifest.target_triple == target_triple(), + "AOT manifest target mismatch: manifest={} actual={}", + manifest.target_triple, + target_triple() + ); + ensure!( + manifest.engine == EXPECTED_AOT_ENGINE, + "AOT manifest engine mismatch: manifest={} expected={EXPECTED_AOT_ENGINE}", + manifest.engine + ); + ensure!( + manifest.wasmer_version == EXPECTED_WASMER_VERSION, + "AOT manifest Wasmer version mismatch: manifest={} expected={EXPECTED_WASMER_VERSION}", + manifest.wasmer_version + ); + ensure!( + manifest.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION, + "AOT manifest wasmer-wasix version mismatch: manifest={} expected={EXPECTED_WASMER_WASIX_VERSION}", + manifest.wasmer_wasix_version + ); + let metadata = assets::asset_manifest_metadata()?; + if let Some(expected) = metadata.source_fingerprint.as_deref() { + ensure!( + manifest.source_fingerprint.as_deref() == Some(expected), + "AOT manifest source fingerprint mismatch: manifest={} assets={expected}", + manifest + .source_fingerprint + .as_deref() + .unwrap_or("") + ); + } + let postgres_version = manifest + .postgres_version + .as_deref() + .ok_or_else(|| anyhow::anyhow!("AOT manifest is missing postgres-version metadata"))?; + if !metadata.postgres_version.trim().is_empty() { + ensure!( + postgres_version == metadata.postgres_version, + "AOT manifest postgres-version mismatch: manifest={} assets={}", + postgres_version, + metadata.postgres_version + ); + } + + let artifact = manifest + .artifacts + .into_iter() + .find(|artifact| artifact.name == name) + .ok_or_else(|| anyhow::anyhow!("AOT manifest does not list artifact '{name}'"))?; + + Ok(artifact) +} + +fn validate_compressed_artifact_manifest( + name: &str, + artifact: &AotManifestArtifact, + bytes: &[u8], +) -> Result<()> { + let actual_hash = sha256_hex(bytes); + ensure!( + actual_hash.eq_ignore_ascii_case(&artifact.sha256), + "AOT artifact '{name}' hash mismatch: manifest={} actual={actual_hash}", + artifact.sha256 + ); + Ok(()) +} + +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); + } + 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", + target_triple() + ) +} + +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" + ); + 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")] + { + let manifest = _manifest; + for sql_name in liboliphaunt_wasix_portable::SELECTED_EXTENSION_AOT_SQL_NAMES { + 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!( + "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") + .context("could not resolve oliphaunt-wasix cache directory")?; + Ok(dirs + .cache_dir() + .join("wasmer-aot") + .join(target_triple()) + .join(format!("{safe_name}-{hash}.bin"))) +} + +fn receipt_path(cache_path: &Path) -> PathBuf { + cache_path.with_extension("receipt.json") +} + +fn cache_receipt_matches( + name: &str, + manifest_artifact: &AotManifestArtifact, + cache_path: &Path, + raw_sha256: &str, +) -> Result { + let Some(raw_size) = manifest_artifact.raw_size else { + return Ok(false); + }; + let metadata = match fs::metadata(cache_path) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("stat {}", cache_path.display())), + }; + if metadata.len() != raw_size { + return Ok(false); + } + + let receipt_path = receipt_path(cache_path); + let receipt = match fs::read(&receipt_path) { + Ok(bytes) => match serde_json::from_slice::(&bytes) { + Ok(receipt) => receipt, + Err(_) => return Ok(false), + }, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("read {}", receipt_path.display())), + }; + + Ok(receipt.format_version == CACHE_RECEIPT_FORMAT_VERSION + && receipt.artifact_name == name + && receipt.target_triple == target_triple() + && receipt.engine == EXPECTED_AOT_ENGINE + && receipt.wasmer_version == EXPECTED_WASMER_VERSION + && receipt.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION + && receipt.raw_sha256.eq_ignore_ascii_case(raw_sha256) + && receipt.raw_size == raw_size + && receipt + .compressed_sha256 + .eq_ignore_ascii_case(&manifest_artifact.sha256) + && receipt + .module_sha256 + .eq_ignore_ascii_case(&manifest_artifact.module_sha256)) +} + +fn write_cache_receipt( + name: &str, + manifest_artifact: &AotManifestArtifact, + cache_path: &Path, + raw_sha256: &str, +) -> Result<()> { + let Some(raw_size) = manifest_artifact.raw_size else { + return Ok(()); + }; + let receipt = AotCacheReceipt { + format_version: CACHE_RECEIPT_FORMAT_VERSION, + artifact_name: name.to_owned(), + target_triple: target_triple().to_owned(), + engine: EXPECTED_AOT_ENGINE.to_owned(), + wasmer_version: EXPECTED_WASMER_VERSION.to_owned(), + wasmer_wasix_version: EXPECTED_WASMER_WASIX_VERSION.to_owned(), + raw_sha256: raw_sha256.to_owned(), + raw_size, + compressed_sha256: manifest_artifact.sha256.clone(), + module_sha256: manifest_artifact.module_sha256.clone(), + }; + + let path = receipt_path(cache_path); + let tmp_path = path.with_extension(format!( + "receipt.{}.{}.tmp", + std::process::id(), + tmp_suffix() + )); + let bytes = serde_json::to_vec(&receipt).context("serialize AOT cache receipt")?; + fs::write(&tmp_path, bytes).with_context(|| format!("write {}", tmp_path.display()))?; + if let Err(err) = fs::rename(&tmp_path, &path) { + remove_file_if_exists(&tmp_path).ok(); + return Err(err).with_context(|| format!("promote AOT cache receipt {}", path.display())); + } + Ok(()) +} + +fn sha256_file_with_len(path: &Path) -> Result<(String, u64)> { + let mut file = fs::File::open(path).with_context(|| format!("open {}", path.display()))?; + let mut hasher = Sha256::new(); + let mut len = 0u64; + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file + .read(&mut buffer) + .with_context(|| format!("read {}", path.display()))?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + len += read as u64; + } + Ok((format!("{:x}", hasher.finalize()), len)) +} + +fn aot_verify_mode() -> Result { + let Some(value) = std::env::var_os("OLIPHAUNT_WASM_AOT_VERIFY") else { + return Ok(AotVerifyMode::Fast); + }; + let value = value.to_string_lossy().to_ascii_lowercase(); + match value.as_str() { + "" | "fast" | "metadata" | "receipt" | "0" | "false" | "off" => Ok(AotVerifyMode::Fast), + "full" | "sha" | "sha256" | "strict" | "1" | "true" | "on" => Ok(AotVerifyMode::Full), + other => bail!("unsupported OLIPHAUNT_WASM_AOT_VERIFY={other}; use `fast` or `full`"), + } +} + +#[allow(unsafe_code)] +fn deserialize_headless(engine: &Engine, path: &Path) -> Result { + let _phase = timing::phase("aot.deserialize"); + deserialize_headless_mmap(engine, path) +} + +#[allow(unsafe_code)] +fn deserialize_headless_mmap(engine: &Engine, path: &Path) -> Result { + let _phase = timing::phase("aot.deserialize.mmap"); + // SAFETY: same artifact ownership and cache-key constraints as the file + // deserializer below. This path avoids reading the complete native artifact + // into a Rust Vec before Wasmer deserializes it. + unsafe { + engine + .deserialize_from_mmapped_file(path) + .with_context(|| format!("mmap-deserialize Wasmer AOT artifact {}", path.display())) + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + +fn target_triple() -> &'static str { + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + return "aarch64-apple-darwin"; + } + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] + { + return "x86_64-unknown-linux-gnu"; + } + #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] + { + return "aarch64-unknown-linux-gnu"; + } + #[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] + { + return "x86_64-pc-windows-msvc"; + } + #[allow(unreachable_code)] + "unsupported" +} + +fn target_artifact_bytes(name: &str) -> Option<&'static [u8]> { + 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")] + { + 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 !liboliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { + return None; + } + 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> { + liboliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_aarch64_apple_darwin::MANIFEST_JSON) +} + +#[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; + } + oliphaunt_wasix_tools_aot_aarch64_apple_darwin::artifact_bytes(name) +} + +#[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) +} + +#[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 { + return None; + } + 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> { + 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( + 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; + } + oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::artifact_bytes(name) +} + +#[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) +} + +#[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 { + return None; + } + 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> { + liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) +} + +#[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; + } + oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::artifact_bytes(name) +} + +#[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) +} + +#[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 { + return None; + } + 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> { + liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) +} + +#[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; + } + oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::artifact_bytes(name) +} + +#[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) +} + +#[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_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { + None +} + +#[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 +} + +#[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_aot_manifest_json_for_crate() -> Option<&'static str> { + None +} + +#[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 +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AotManifest { + source_fingerprint: Option, + postgres_version: Option, + target_triple: String, + engine: String, + wasmer_version: String, + wasmer_wasix_version: String, + artifacts: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +struct AotManifestArtifact { + name: String, + sha256: String, + #[allow(dead_code)] + module_sha256: String, + raw_sha256: Option, + raw_size: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +struct AotCacheReceipt { + format_version: u32, + artifact_name: String, + target_triple: String, + engine: String, + wasmer_version: String, + wasmer_wasix_version: String, + raw_sha256: String, + raw_size: u64, + compressed_sha256: String, + module_sha256: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + const WASIX_TOOLCHAIN: &str = include_str!("../../../../../../sources/toolchains/wasix.toml"); + + #[test] + fn runtime_aot_versions_match_asset_toolchain() { + assert_eq!( + EXPECTED_WASMER_VERSION, + toolchain_value("wasmer"), + "runtime AOT Wasmer expectation must match src/sources/toolchains/wasix.toml" + ); + assert_eq!( + EXPECTED_WASMER_WASIX_VERSION, + toolchain_value("wasmer-wasix"), + "runtime AOT WASIX expectation must match src/sources/toolchains/wasix.toml" + ); + } + + #[test] + fn engine_identity_matches_runtime_aot_versions() { + assert!( + AOT_ENGINE_ID.contains(EXPECTED_AOT_ENGINE), + "engine identity must include the validated AOT engine" + ); + assert!( + AOT_ENGINE_ID.contains(EXPECTED_WASMER_VERSION), + "engine identity must include the validated Wasmer version" + ); + assert!( + AOT_ENGINE_ID.contains(EXPECTED_WASMER_WASIX_VERSION), + "engine identity must include the validated WASIX version" + ); + } + + #[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]") + .expect("WASIX toolchain manifest has a [toolchain] section") + .1; + let section = rest.split_once("\n[").map_or(rest, |(section, _)| section); + + for line in section.lines() { + let Some((line_key, value)) = line.trim().split_once('=') else { + continue; + }; + if line_key.trim() == key { + return value.trim().trim_matches('"'); + } + } + panic!("WASIX toolchain manifest has toolchain.{key}"); + } +} 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 new file mode 100644 index 00000000..f53cb893 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs @@ -0,0 +1,103 @@ +#[cfg(feature = "extensions")] +use anyhow::anyhow; +use anyhow::{Context, Result}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AssetManifestMetadata { + pub source_lane: Option, + pub source_fingerprint: Option, + pub postgres_version: String, + pub pgdata_template_source_lane: Option, + pub pgdata_template_source_fingerprint: Option, + pub pgdata_template_postgres_version: Option, +} + +pub fn asset_manifest_metadata() -> Result { + let manifest = + liboliphaunt_wasix_portable::manifest().context("parse oliphaunt-wasix asset manifest")?; + Ok(AssetManifestMetadata { + source_lane: manifest.source_lane, + source_fingerprint: manifest.source_fingerprint, + postgres_version: manifest.runtime.postgres_version, + pgdata_template_source_lane: manifest + .pgdata_template + .as_ref() + .and_then(|template| template.source_lane.clone()), + pgdata_template_source_fingerprint: manifest + .pgdata_template + .as_ref() + .and_then(|template| template.source_fingerprint.clone()), + pgdata_template_postgres_version: manifest + .pgdata_template + .as_ref() + .map(|template| template.postgres_version.clone()), + }) +} + +pub(crate) fn runtime_archive() -> Option<&'static [u8]> { + liboliphaunt_wasix_portable::runtime_archive() +} + +pub(crate) fn expected_runtime_archive_sha256() -> Result { + let manifest = + liboliphaunt_wasix_portable::manifest().context("parse oliphaunt-wasix asset manifest")?; + Ok(manifest.runtime.sha256) +} + +pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { + liboliphaunt_wasix_portable::pgdata_template_archive() +} + +pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { + liboliphaunt_wasix_portable::pgdata_template_manifest() +} + +#[cfg(feature = "tools")] +pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { + oliphaunt_wasix_tools::pg_dump_wasm() +} + +#[cfg(feature = "tools")] +pub(crate) fn psql_wasm() -> Option<&'static [u8]> { + oliphaunt_wasix_tools::psql_wasm() +} + +#[allow(dead_code)] +pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { + liboliphaunt_wasix_portable::initdb_wasm() +} + +pub(crate) fn icu_data_archive() -> Option<&'static [u8]> { + #[cfg(feature = "icu")] + { + oliphaunt_icu::icu_data_archive() + } + #[cfg(not(feature = "icu"))] + { + None + } +} + +#[cfg(feature = "extensions")] +pub(crate) fn extension_archive(sql_name: &str) -> Option<&'static [u8]> { + liboliphaunt_wasix_portable::extension_archive(sql_name) +} + +#[cfg(feature = "extensions")] +pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result { + 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") + }) +} + +#[cfg(feature = "extensions")] +pub(crate) fn extension_aot_manifest_json(target: &str, sql_name: &str) -> Option<&'static str> { + 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]> { + liboliphaunt_wasix_portable::extension_aot_artifact_bytes(target, name) +} 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 new file mode 100644 index 00000000..675fae76 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/backend.rs @@ -0,0 +1,474 @@ +#[cfg(feature = "extensions")] +use anyhow::Context; +use anyhow::{Result, ensure}; +use std::sync::{Mutex, MutexGuard, OnceLock}; + +use crate::oliphaunt::base::InstallOutcome; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; +use crate::oliphaunt::engine::EngineCapabilities; +#[cfg(feature = "extensions")] +use crate::oliphaunt::extensions::{Extension, extension_session_setup_sql, extension_setup_sql}; +use crate::oliphaunt::interface::DataTransferContainer; +use crate::oliphaunt::postgres_mod::{ + PostgresMod, ProtocolPumpOutcome, ProtocolStream, StartupProtocolResponse, +}; +use crate::oliphaunt::timing; +use crate::oliphaunt::transport::Transport; +use crate::oliphaunt::wire::raw_protocol_message_len; +#[cfg(feature = "extensions")] +use crate::oliphaunt::wire::{response_contains_error, simple_query_message}; + +static WASIX_BACKEND_OPEN_LOCK: OnceLock> = OnceLock::new(); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BackendOpenKind { + Direct, + Proxy, +} + +pub(crate) struct BackendSession(Box); + +pub(crate) struct WasixBackendSession { + pg: PostgresMod, + transport: Transport, + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + #[cfg(feature = "extensions")] + preinstalled_extensions: Vec, + #[cfg(feature = "extensions")] + preloaded_extensions: Vec, +} + +impl WasixBackendSession { + pub(crate) fn open( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + #[cfg(feature = "extensions")] + { + Self::open_with_extension_preload(outcome, postgres_config, startup_config, kind, &[]) + } + #[cfg(not(feature = "extensions"))] + { + Self::open_without_extension_preload(outcome, postgres_config, startup_config, kind) + } + } + + #[cfg(feature = "extensions")] + pub(crate) fn open_with_extension_preload( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + extensions: &[Extension], + ) -> Result { + Self::open_inner(outcome, postgres_config, startup_config, kind, extensions) + } + + #[cfg(not(feature = "extensions"))] + fn open_without_extension_preload( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + Self::open_inner(outcome, postgres_config, startup_config, kind) + } + + #[cfg(feature = "extensions")] + fn open_inner( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + extensions: &[Extension], + ) -> Result { + let _open_guard = wasix_backend_open_guard(); + let preinstalled_extensions = outcome.preinstalled_extensions.clone(); + let pg = Self::new_postgres( + outcome.clone(), + postgres_config.clone(), + startup_config.clone(), + kind, + )?; + for extension in extensions { + pg.preload_extension_module(*extension)?; + } + let (pg, transport) = Self::finish_open(pg, kind)?; + Ok(Self { + pg, + transport, + outcome, + postgres_config, + startup_config, + kind, + preinstalled_extensions, + preloaded_extensions: extensions.to_vec(), + }) + } + + #[cfg(not(feature = "extensions"))] + fn open_inner( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + let _open_guard = wasix_backend_open_guard(); + let pg = Self::new_postgres( + outcome.clone(), + postgres_config.clone(), + startup_config.clone(), + kind, + )?; + let (pg, transport) = Self::finish_open(pg, kind)?; + Ok(Self { + pg, + transport, + outcome, + postgres_config, + startup_config, + kind, + }) + } + + fn new_postgres( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + let pg = { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "oliphaunt.postgres_new", + BackendOpenKind::Proxy => "proxy.backend_postgres_new", + }); + PostgresMod::new_prepared_with_config( + outcome.paths, + outcome.runtime_layout, + postgres_config, + startup_config, + )? + }; + Ok(pg) + } + + fn finish_open(mut pg: PostgresMod, kind: BackendOpenKind) -> Result<(PostgresMod, Transport)> { + { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "oliphaunt.ensure_cluster", + BackendOpenKind::Proxy => "proxy.backend_ensure_cluster", + }); + pg.ensure_cluster()?; + } + let transport = { + let _phase = timing::phase(match kind { + BackendOpenKind::Direct => "oliphaunt.transport_prepare", + BackendOpenKind::Proxy => "proxy.transport_prepare", + }); + Transport::prepare(&mut pg)? + }; + Ok((pg, transport)) + } + + pub(crate) fn paths(&self) -> &crate::oliphaunt::base::OliphauntPaths { + self.pg.paths() + } + + pub(crate) fn pgdata_template_root(&self) -> Option<&std::path::Path> { + self.pg.pgdata_template_root() + } + + pub(crate) fn startup_config(&self) -> &StartupConfig { + &self.startup_config + } + + #[cfg(debug_assertions)] + pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.pg.guest_bridge_allocation_counts() + } + + pub(crate) fn send_buffered( + &mut self, + message: &[u8], + requested: Option, + ) -> Result> { + self.transport.send(&mut self.pg, message, requested) + } + + pub(crate) fn send_framed_raw_stream( + &mut self, + message: &[u8], + requested: Option, + mut on_data: F, + ) -> Result<()> + where + F: FnMut(&[u8]) -> Result<()>, + { + let mut cursor = 0usize; + while cursor < message.len() { + let frame_len = raw_protocol_message_len(&message[cursor..])?; + let end = cursor + frame_len; + let data = self.send_buffered(&message[cursor..end], requested)?; + if !data.is_empty() { + on_data(&data)?; + } + cursor = end; + } + Ok(()) + } + + pub(crate) fn startup_with_packet( + &mut self, + message: &[u8], + ) -> Result { + self.pg.start_protocol_with_startup_packet(message) + } + + #[cfg(feature = "tools")] + pub(crate) fn existing_startup_response(&self) -> Option> { + self.pg.existing_startup_response() + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_extension_module(&mut self, extension: Extension) -> Result<()> { + self.pg.preload_extension_module(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_installed_extension(&mut self, extension: Extension) -> Result<()> { + self.preload_extension_module(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn enable_extensions(&mut self, extensions: &[Extension]) -> Result<()> { + for extension in extensions { + let setup_sql = if self.has_preinstalled_extension(*extension) { + self.preload_installed_extension(*extension)?; + extension_session_setup_sql(*extension) + } else { + extension_setup_sql(*extension) + }; + for sql in setup_sql { + let response = self + .send_buffered(&simple_query_message(&sql), None) + .with_context(|| { + format!("enable bundled extension '{}'", extension.sql_name()) + })?; + if response_contains_error(&response) { + anyhow::bail!( + "enable bundled extension '{}' returned a Postgres error", + extension.sql_name() + ); + } + } + } + Ok(()) + } + + #[cfg(feature = "extensions")] + pub(crate) fn has_preinstalled_extension(&self, extension: Extension) -> bool { + self.preinstalled_extensions + .iter() + .any(|sql_name| sql_name == extension.sql_name()) + } + + pub(crate) fn supports_protocol_pump(&self) -> bool { + self.pg.supports_streaming_protocol() + } + + pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + self.pg.attach_protocol_stream(stream) + } + + pub(crate) fn send_with_protocol_pump( + &mut self, + message: &[u8], + continuation_prefix: impl FnOnce() -> Vec, + ) -> Result { + ensure!( + self.supports_protocol_pump(), + "WASIX runtime is missing backend-owned protocol pump exports" + ); + self.pg.send_protocol_pump(message, continuation_prefix) + } + + pub(crate) fn shutdown(&mut self) -> Result<()> { + self.pg.shutdown_backend() + } + + pub(crate) fn restart(&mut self) -> Result<()> { + let _open_guard = wasix_backend_open_guard(); + let pg = Self::new_postgres( + self.outcome.clone(), + self.postgres_config.clone(), + self.startup_config.clone(), + self.kind, + )?; + #[cfg(feature = "extensions")] + for extension in &self.preloaded_extensions { + pg.preload_extension_module(*extension)?; + } + let (pg, transport) = Self::finish_open(pg, self.kind)?; + self.pg = pg; + self.transport = transport; + Ok(()) + } +} + +impl BackendSession { + pub(crate) fn open( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + ) -> Result { + WasixBackendSession::open(outcome, postgres_config, startup_config, kind) + .map(Box::new) + .map(Self) + } + + #[cfg(feature = "extensions")] + pub(crate) fn open_with_extension_preload( + outcome: InstallOutcome, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + kind: BackendOpenKind, + extensions: &[Extension], + ) -> Result { + WasixBackendSession::open_with_extension_preload( + outcome, + postgres_config, + startup_config, + kind, + extensions, + ) + .map(Box::new) + .map(Self) + } + + pub(crate) fn capabilities(&self) -> EngineCapabilities { + EngineCapabilities::wasix_legacy(self.0.supports_protocol_pump()) + } + + pub(crate) fn paths(&self) -> &crate::oliphaunt::base::OliphauntPaths { + self.0.paths() + } + + pub(crate) fn pgdata_template_root(&self) -> Option<&std::path::Path> { + self.0.pgdata_template_root() + } + + pub(crate) fn startup_config(&self) -> &StartupConfig { + self.0.startup_config() + } + + #[cfg(debug_assertions)] + pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.0.guest_bridge_allocation_counts() + } + + pub(crate) fn send_buffered( + &mut self, + message: &[u8], + requested: Option, + ) -> Result> { + self.0.send_buffered(message, requested) + } + + pub(crate) fn with_buffered( + &mut self, + message: &[u8], + requested: Option, + f: F, + ) -> Result + where + F: FnOnce(&[u8]) -> Result, + { + let data = self.0.send_buffered(message, requested)?; + f(&data) + } + + pub(crate) fn send_framed_raw_stream( + &mut self, + message: &[u8], + requested: Option, + on_data: F, + ) -> Result<()> + where + F: FnMut(&[u8]) -> Result<()>, + { + self.0.send_framed_raw_stream(message, requested, on_data) + } + + pub(crate) fn startup_with_packet( + &mut self, + message: &[u8], + ) -> Result { + self.0.startup_with_packet(message) + } + + #[cfg(feature = "tools")] + pub(crate) fn existing_startup_response(&self) -> Option> { + self.0.existing_startup_response() + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_extension_module(&mut self, extension: Extension) -> Result<()> { + self.0.preload_extension_module(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_installed_extension(&mut self, extension: Extension) -> Result<()> { + self.0.preload_installed_extension(extension) + } + + #[cfg(feature = "extensions")] + pub(crate) fn enable_extensions(&mut self, extensions: &[Extension]) -> Result<()> { + self.0.enable_extensions(extensions) + } + + pub(crate) fn supports_protocol_pump(&self) -> bool { + self.0.supports_protocol_pump() + } + + pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + self.0.attach_protocol_stream(stream) + } + + pub(crate) fn send_with_protocol_pump( + &mut self, + message: &[u8], + continuation_prefix: impl FnOnce() -> Vec, + ) -> Result { + self.0.send_with_protocol_pump(message, continuation_prefix) + } + + pub(crate) fn shutdown(&mut self) -> Result<()> { + self.0.shutdown() + } + + pub(crate) fn restart(&mut self) -> Result<()> { + self.0.restart() + } +} + +fn wasix_backend_open_guard() -> MutexGuard<'static, ()> { + // Wasmer/WASIX backend startup uses process-wide runtime and module-cache + // state. Serialize creation and `_start`; already-open backends still run + // independently after startup. + WASIX_BACKEND_OPEN_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("WASIX backend open lock poisoned") +} diff --git a/src/pglite/base.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base.rs similarity index 76% rename from src/pglite/base.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base.rs index 5d7c17c3..b24f3f42 100644 --- a/src/pglite/base.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base.rs @@ -1,12 +1,9 @@ use std::collections::BTreeSet; -use std::ffi::OsStr; use std::fs::{self, File, OpenOptions}; use std::io::{Cursor, Read}; #[cfg(windows)] use std::os::windows::fs::OpenOptionsExt; use std::path::{Component, Path, PathBuf}; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use std::process::Command; use std::sync::{Arc, Mutex, OnceLock}; use anyhow::{Context, Result, anyhow, bail, ensure}; @@ -20,26 +17,31 @@ use zstd::stream::read::Decoder as ZstdDecoder; use super::postgres_mod::PostgresMod; use super::timing; -use crate::pglite::assets; +use crate::oliphaunt::assets; #[cfg(feature = "extensions")] -use crate::pglite::client::Pglite; +use crate::oliphaunt::client::Oliphaunt; #[cfg(feature = "extensions")] -use crate::pglite::config::{PostgresConfig, StartupConfig}; -use crate::pglite::data_dir::unpack_pgdata_archive; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; +use crate::oliphaunt::data_dir::unpack_pgdata_archive; #[cfg(feature = "extensions")] -use crate::pglite::extensions::Extension; +use crate::oliphaunt::extensions::Extension; use tempfile::TempDir; -const RUNTIME_ARCHIVE_NAME: &str = "pglite.wasix.tar.zst"; +mod template_clone; + +use template_clone::clone_pgdata_template_dir; + +const RUNTIME_ARCHIVE_NAME: &str = "oliphaunt.wasix.tar.zst"; const PGDATA_TEMPLATE_ARCHIVE_NAME: &str = "pgdata-template.tar.zst"; -const MOUNTFS_RUNTIME_MARKER: &str = ".pglite-oxide-mountfs-runtime"; -const RUNTIME_LAYOUT_MANIFEST_NAME: &str = ".pglite-oxide-runtime-layout.json"; -const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".pglite-oxide-pgdata-overlay.json"; +const MOUNTFS_RUNTIME_MARKER: &str = ".oliphaunt-wasix-mountfs-runtime"; +const RUNTIME_LAYOUT_MANIFEST_NAME: &str = ".oliphaunt-wasix-runtime-layout.json"; +const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".oliphaunt-wasix-pgdata-overlay.json"; +const ICU_DATA_MARKER_NAME: &str = ".oliphaunt-icu-data.sha256"; // Bump these when cache materialization semantics change; old mutable PGDATA // template caches may have been modified by earlier clone strategies. const PGDATA_TEMPLATE_CACHE_FORMAT: &str = "v2"; #[cfg(feature = "extensions")] -const EXTENSION_PGDATA_TEMPLATE_CACHE_FORMAT: &str = "v4"; +const EXTENSION_PGDATA_TEMPLATE_CACHE_FORMAT: &str = "v5"; const DEFAULT_PASSWORD_FILE: &[u8] = b"password\n"; static RUNTIME_CACHE: OnceLock, String>> = OnceLock::new(); @@ -52,6 +54,13 @@ static EXTENSION_TEMPLATE_CACHE_LOCK: OnceLock> = OnceLock::new(); static ROOT_LOCKED_PATHS: OnceLock>> = OnceLock::new(); const TEMPLATE_RUNTIME_STATE_FILES: &[&str] = &["postmaster.pid", "postmaster.opts"]; +#[cfg(feature = "extensions")] +fn extension_template_cache_enabled() -> bool { + std::env::var_os("OLIPHAUNT_WASM_EXTENSION_TEMPLATE_CACHE") + .and_then(|value| value.into_string().ok()) + .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "on")) +} + #[derive(Debug)] struct CachedRuntime { runtime_root: PathBuf, @@ -70,7 +79,7 @@ struct CachedExtensionPgDataTemplate { } #[derive(Debug, Clone)] -pub struct PglitePaths { +pub struct OliphauntPaths { pub pgroot: PathBuf, pub pgdata: PathBuf, } @@ -123,6 +132,10 @@ struct PgDataOverlayManifest { template_archive_sha256: String, postgres_version: String, #[serde(default)] + source_lane: Option, + #[serde(default)] + source_fingerprint: Option, + #[serde(default)] extension_sql_names: Vec, } @@ -215,7 +228,7 @@ impl RootPrepareOptions { impl RuntimeLayout { pub(crate) fn module_path(&self) -> PathBuf { - self.module_root.join("bin/pglite") + self.module_root.join("bin/oliphaunt") } pub(crate) fn uses_shared_overlay(&self) -> bool { @@ -230,12 +243,16 @@ pub struct PgDataTemplate { pub manifest_path: PathBuf, } -/// Manifest that binds a PGDATA template to the PGlite WASIX runtime it was +/// Manifest that binds a PGDATA template to the Oliphaunt WASIX runtime it was /// created with. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PgDataTemplateManifest { pub postgres_version: String, + #[serde(default)] + pub source_lane: Option, + #[serde(default)] + pub source_fingerprint: Option, pub wasm_sha256: String, pub archive_sha256: String, #[serde(default)] @@ -248,6 +265,10 @@ pub struct PgDataTemplateManifest { struct ExtensionPgDataTemplateManifest { version: u32, postgres_version: String, + #[serde(default)] + source_lane: Option, + #[serde(default)] + source_fingerprint: Option, base_template_archive_sha256: String, base_template_wasm_sha256: String, extension_sql_names: Vec, @@ -256,7 +277,7 @@ struct ExtensionPgDataTemplateManifest { cache_key: String, } -impl PglitePaths { +impl OliphauntPaths { pub fn new(app_qual: (&str, &str, &str)) -> Result { let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2) .context("could not resolve app data dir")?; @@ -267,7 +288,7 @@ impl PglitePaths { pub fn with_root(root: impl Into) -> Self { let base = root.into(); let pgroot = base.join("tmp"); - let pgdata = pgroot.join("pglite").join("base"); + let pgdata = pgroot.join("oliphaunt").join("base"); Self { pgroot, pgdata } } @@ -287,7 +308,7 @@ impl PglitePaths { } pub(crate) fn runtime_root(&self) -> PathBuf { - self.pgroot.join("pglite") + self.pgroot.join("oliphaunt") } pub fn with_temp_dir() -> Result<(TempDir, Self)> { @@ -312,7 +333,7 @@ impl PglitePaths { impl RootLock { pub(crate) fn acquire(root: &Path) -> Result { fs::create_dir_all(root) - .with_context(|| format!("create PGlite root {}", root.display()))?; + .with_context(|| format!("create Oliphaunt root {}", root.display()))?; let canonical_root = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); { let mut locked = ROOT_LOCKED_PATHS @@ -321,18 +342,18 @@ impl RootLock { .expect("root lock path set poisoned"); ensure!( locked.insert(canonical_root.clone()), - "PGlite root is already in use: {}", + "Oliphaunt root is already in use: {}", root.display() ); } - let path = root.join(".pglite-oxide.lock"); + let path = root.join(".oliphaunt-wasix.lock"); let file = match open_root_lock_file(&path) { Ok(file) => file, Err(err) => { release_root_lock_path(&canonical_root); return Err(err).with_context(|| { format!( - "PGlite root is already in use or unavailable: {}", + "Oliphaunt root is already in use or unavailable: {}", root.display() ) }); @@ -341,7 +362,7 @@ impl RootLock { if let Err(err) = file.try_lock() { release_root_lock_path(&canonical_root); return Err(err) - .with_context(|| format!("PGlite root is already in use: {}", root.display())); + .with_context(|| format!("Oliphaunt root is already in use: {}", root.display())); } Ok(Self { path: canonical_root, @@ -349,7 +370,7 @@ impl RootLock { }) } - pub(crate) fn acquire_for_paths(paths: &PglitePaths) -> Result { + pub(crate) fn acquire_for_paths(paths: &OliphauntPaths) -> Result { Self::acquire(paths.install_root()) } } @@ -399,18 +420,18 @@ impl CacheLock { } } -fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> { - let pglite_dir = paths.pgroot.join("pglite"); - if !pglite_dir.exists() { +fn locate_runtime_module(paths: &OliphauntPaths) -> Option<(PathBuf, PathBuf)> { + let oliphaunt_dir = paths.pgroot.join("oliphaunt"); + if !oliphaunt_dir.exists() { return None; } - let pglite_bin_dir = pglite_dir.join("bin"); - let module = pglite_bin_dir.join("pglite"); + let oliphaunt_bin_dir = oliphaunt_dir.join("bin"); + let module = oliphaunt_bin_dir.join("oliphaunt"); if !module.exists() { return None; } - let share = pglite_dir.join("share").join("postgresql"); + let share = oliphaunt_dir.join("share").join("postgresql"); let required_share_files = [ "postgres.bki", "timezonesets/Default", @@ -424,28 +445,31 @@ fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> { { return None; } - Some((module, pglite_bin_dir)) + Some((module, oliphaunt_bin_dir)) } -fn ensure_full_runtime(paths: &PglitePaths) -> Result { +fn ensure_full_runtime(paths: &OliphauntPaths) -> Result { let _phase = timing::phase("runtime.ensure"); + let source_key = runtime_cache_key()?; let existing_runtime = { let _phase = timing::phase("runtime.locate_existing"); locate_runtime_module(paths) }; if existing_runtime.is_some() { - let repaired_runtime = if runtime_support_files_need_repair(paths)? { + let source_key_matches = full_runtime_layout_matches_current(paths, &source_key)?; + let repaired_runtime = if !source_key_matches || runtime_support_files_need_repair(paths)? { install_runtime_from_tar(paths)? } else { false }; + let repaired_icu = install_optional_icu_data(&paths.runtime_root())?; write_runtime_layout_manifest( &paths.runtime_root(), RuntimeLayoutKind::FullLocal, - &runtime_cache_key()?, + &source_key, )?; ensure_runtime_password_file(&paths.runtime_root())?; - return Ok(repaired_runtime); + return Ok(repaired_runtime || repaired_icu); } if let Some(parent) = paths.pgroot.parent() { @@ -456,6 +480,7 @@ fn ensure_full_runtime(paths: &PglitePaths) -> Result { } install_runtime_from_tar(paths)?; + install_optional_icu_data(&paths.runtime_root())?; locate_runtime_module(paths).ok_or_else(|| { anyhow!( "runtime missing: could not locate module under {} after archive install", @@ -465,14 +490,24 @@ fn ensure_full_runtime(paths: &PglitePaths) -> Result { write_runtime_layout_manifest( &paths.runtime_root(), RuntimeLayoutKind::FullLocal, - &runtime_cache_key()?, + &source_key, )?; ensure_runtime_password_file(&paths.runtime_root())?; Ok(true) } -fn runtime_support_files_need_repair(paths: &PglitePaths) -> Result { +fn full_runtime_layout_matches_current( + paths: &OliphauntPaths, + expected_source_key: &str, +) -> Result { + let Some(manifest) = read_runtime_layout_manifest(&paths.runtime_root())? else { + return Ok(false); + }; + Ok(manifest.kind == RuntimeLayoutKind::FullLocal && manifest.source_key == expected_source_key) +} + +fn runtime_support_files_need_repair(paths: &OliphauntPaths) -> Result { for relative in [ "password", "share/postgresql/postgres.bki", @@ -490,29 +525,13 @@ fn runtime_support_files_need_repair(paths: &PglitePaths) -> Result { Ok(false) } -fn runtime_tar_path() -> Option { - if let Ok(path) = std::env::var("PGLITE_OXIDE_RUNTIME_ARCHIVE") - .or_else(|_| std::env::var("PGLITE_OXIDE_RUNTIME_TAR")) - { - let candidate = PathBuf::from(path); - if candidate.exists() { - return Some(candidate); - } - } - - None -} - -fn install_runtime_from_tar(paths: &PglitePaths) -> Result { +fn install_runtime_from_tar(paths: &OliphauntPaths) -> Result { let _phase = timing::phase("runtime.archive_install"); - if let Some(tar_path) = runtime_tar_path() { - info!("installing runtime from tar archive {}", tar_path.display()); - let file = fs::File::open(&tar_path) - .with_context(|| format!("open runtime archive {}", tar_path.display()))?; - unpack_runtime_archive_reader(file, &tar_path, &paths.pgroot)?; - } else if let Some(runtime_archive) = assets::runtime_archive() { + if let Some(runtime_archive) = assets::runtime_archive() { info!("installing embedded runtime archive"); - maybe_validate_embedded_runtime_archive(runtime_archive)?; + if strict_asset_verification()? { + validate_embedded_runtime_archive_strict(runtime_archive)?; + } unpack_runtime_archive_reader( Cursor::new(runtime_archive), Path::new(RUNTIME_ARCHIVE_NAME), @@ -520,38 +539,142 @@ fn install_runtime_from_tar(paths: &PglitePaths) -> Result { )?; } else { bail!( - "no embedded PGlite runtime assets are available; enable the `bundled` feature or set PGLITE_OXIDE_RUNTIME_ARCHIVE" + "Oliphaunt WASIX runtime assets are unavailable; publish and stage package-manager-resolved liboliphaunt-wasix runtime artifacts with the application" ); } Ok(true) } -#[cfg(feature = "bundled")] -fn maybe_validate_embedded_runtime_archive(bytes: &[u8]) -> Result<()> { - if strict_asset_verification()? { - validate_embedded_runtime_archive_strict(bytes)?; +pub(crate) fn install_optional_icu_data(runtime_root: &Path) -> Result { + let _phase = timing::phase("runtime.icu_data_install"); + let icu_dir = runtime_root.join("share/icu"); + let marker = runtime_root.join(ICU_DATA_MARKER_NAME); + let Some(archive) = assets::icu_data_archive() else { + let mut changed = false; + if icu_dir.exists() { + fs::remove_dir_all(&icu_dir) + .with_context(|| format!("remove unselected ICU data {}", icu_dir.display()))?; + changed = true; + } + match fs::remove_file(&marker) { + Ok(()) => changed = true, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err).with_context(|| format!("remove {}", marker.display())), + } + return Ok(changed); + }; + + let expected = sha256_hex(archive); + if icu_data_root_contains_data(&icu_dir)? + && fs::read_to_string(&marker) + .map(|value| value.trim() == expected) + .unwrap_or(false) + { + return Ok(false); } - Ok(()) -} -#[cfg(feature = "bundled")] -fn validate_embedded_runtime_archive_strict(bytes: &[u8]) -> Result<()> { - let expected = assets::expected_runtime_archive_sha256()?; - let actual = sha256_hex(bytes); + if icu_dir.exists() { + fs::remove_dir_all(&icu_dir) + .with_context(|| format!("remove stale ICU data {}", icu_dir.display()))?; + } + unpack_icu_data_archive_reader(Cursor::new(archive), runtime_root)?; ensure!( - actual.eq_ignore_ascii_case(&expected), - "embedded runtime archive hash mismatch: manifest={expected} actual={actual}" + icu_data_root_contains_data(&icu_dir)?, + "embedded ICU data archive did not install icudt data under {}", + icu_dir.display() ); - Ok(()) + fs::write(&marker, format!("{expected}\n")) + .with_context(|| format!("write {}", marker.display()))?; + Ok(true) } -#[cfg(not(feature = "bundled"))] -fn maybe_validate_embedded_runtime_archive(_bytes: &[u8]) -> Result<()> { +fn unpack_icu_data_archive_reader(reader: R, destination: &Path) -> Result<()> { + let decoder = ZstdDecoder::new(reader).context("decode ICU data archive")?; + let mut archive = Archive::new(decoder); + for entry in archive.entries().context("read ICU data archive entries")? { + let mut entry = entry.context("read ICU data archive entry")?; + let path = entry + .path() + .context("read ICU data archive entry path")? + .into_owned(); + let relative = icu_archive_relative_path(&path)?; + let entry_type = entry.header().entry_type(); + let dest = archive_destination(destination, relative)?; + if entry_type.is_dir() { + fs::create_dir_all(&dest) + .with_context(|| format!("create ICU data directory {}", dest.display()))?; + continue; + } + if !entry_type.is_file() { + bail!( + "unsafe ICU data archive entry {} has unsupported type {:?}", + path.display(), + entry_type + ); + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create ICU data directory {}", parent.display()))?; + } + entry + .unpack(&dest) + .with_context(|| format!("unpack ICU data archive entry {}", path.display()))?; + } Ok(()) } -fn unpack_runtime_archive_reader( +fn icu_archive_relative_path(path: &Path) -> Result<&Path> { + let mut without_dot = path; + if let Ok(stripped) = without_dot.strip_prefix(".") { + without_dot = stripped; + } + let relative = without_dot + .strip_prefix("tmp/oliphaunt") + .unwrap_or(without_dot); + let mut components = relative.components(); + let under_share_icu = matches!(components.next(), Some(Component::Normal(part)) if part == "share") + && matches!(components.next(), Some(Component::Normal(part)) if part == "icu"); + if !under_share_icu { + bail!( + "ICU data archive entry {} must stay under share/icu", + path.display() + ); + } + Ok(relative) +} + +fn icu_data_root_contains_data(root: &Path) -> Result { + let entries = match fs::read_dir(root) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(err) => return Err(err).with_context(|| format!("read {}", root.display())), + }; + for entry in entries { + let entry = entry.with_context(|| format!("read entry in {}", root.display()))?; + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if path.is_file() && name.starts_with("icudt") && name.ends_with(".dat") { + return Ok(true); + } + if path.is_dir() && name.starts_with("icudt") && directory_contains_file(&path)? { + return Ok(true); + } + } + Ok(false) +} + +fn directory_contains_file(path: &Path) -> Result { + for entry in fs::read_dir(path).with_context(|| format!("read {}", path.display()))? { + let entry = entry.with_context(|| format!("read entry in {}", path.display()))?; + if entry.path().is_file() { + return Ok(true); + } + } + Ok(false) +} + +pub(crate) fn unpack_runtime_archive_reader( reader: R, archive_path: &Path, destination: &Path, @@ -587,7 +710,7 @@ fn archive_destination(root: &Path, archive_path: &Path) -> Result { Ok(dest) } -fn install_extension_reader(paths: &PglitePaths, mut reader: R) -> Result<()> { +fn install_extension_reader(paths: &OliphauntPaths, mut reader: R) -> Result<()> { let _phase = timing::phase("extension.archive_install"); let mut bytes = Vec::new(); reader @@ -601,7 +724,7 @@ fn install_extension_reader(paths: &PglitePaths, mut reader: R) -> Resu Box::new(Cursor::new(bytes)) }; let mut ar = Archive::new(archive_reader); - let target = paths.pgroot.join("pglite"); + let target = paths.pgroot.join("oliphaunt"); std::fs::create_dir_all(&target) .with_context(|| format!("create extension target {}", target.display()))?; unpack_archive_entries(&mut ar, &target) @@ -609,19 +732,19 @@ fn install_extension_reader(paths: &PglitePaths, mut reader: R) -> Resu Ok(()) } -pub fn install_extension_archive(paths: &PglitePaths, archive_path: &Path) -> Result<()> { +pub fn install_extension_archive(paths: &OliphauntPaths, archive_path: &Path) -> Result<()> { let file = std::fs::File::open(archive_path) .with_context(|| format!("open extension archive {}", archive_path.display()))?; install_extension_reader(paths, file) } -pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> { +pub fn install_extension_bytes(paths: &OliphauntPaths, bytes: &[u8]) -> Result<()> { install_extension_reader(paths, std::io::Cursor::new(bytes)) } #[cfg(feature = "extensions")] pub(crate) fn install_bundled_extension_bytes( - paths: &PglitePaths, + paths: &OliphauntPaths, sql_name: &str, bytes: &[u8], ) -> Result<()> { @@ -642,6 +765,16 @@ fn validate_bundled_extension_archive_strict(sql_name: &str, bytes: &[u8]) -> Re Ok(()) } +fn validate_embedded_runtime_archive_strict(bytes: &[u8]) -> Result<()> { + let expected = assets::expected_runtime_archive_sha256()?; + let actual = sha256_hex(bytes); + ensure!( + actual.eq_ignore_ascii_case(&expected), + "embedded runtime archive hash mismatch: manifest={expected} actual={actual}" + ); + Ok(()) +} + pub fn build_pgdata_template(output_dir: impl AsRef) -> Result { let output_dir = output_dir.as_ref(); fs::create_dir_all(output_dir) @@ -670,7 +803,10 @@ pub fn build_pgdata_template(output_dir: impl AsRef) -> Result Result { +fn try_install_embedded_pgdata_template( + paths: &OliphauntPaths, + module_path: &Path, +) -> Result { let _phase = timing::phase("pgdata.embedded_template_install"); if cluster_is_complete(paths) { return Ok(false); @@ -700,7 +836,7 @@ fn try_install_embedded_pgdata_template(paths: &PglitePaths, module_path: &Path) } fn try_prepare_pgdata_template_overlay( - paths: &PglitePaths, + paths: &OliphauntPaths, module_path: &Path, runtime_layout: &mut RuntimeLayout, ) -> Result { @@ -771,7 +907,7 @@ fn install_extension_template_into_outcome( for extension in &normalized { let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { anyhow!( - "extension asset '{}' is not bundled in this pglite-oxide build", + "extension asset '{}' is not bundled in this oliphaunt-wasix build", extension.sql_name() ) })?; @@ -783,7 +919,7 @@ fn install_extension_template_into_outcome( #[cfg(feature = "extensions")] fn install_pgdata_template_overlay_from_extension_template( - paths: &PglitePaths, + paths: &OliphauntPaths, runtime_layout: &mut RuntimeLayout, template: &CachedExtensionPgDataTemplate, ) -> Result<()> { @@ -807,6 +943,8 @@ fn install_pgdata_template_overlay_from_extension_template( paths, &template.manifest.cache_key, &template.manifest.postgres_version, + template.manifest.source_lane.as_deref(), + template.manifest.source_fingerprint.as_deref(), &template.manifest.extension_sql_names, )?; remove_template_runtime_state(&paths.pgdata)?; @@ -816,7 +954,7 @@ fn install_pgdata_template_overlay_from_extension_template( #[cfg(feature = "extensions")] fn install_pgdata_template_clone_from_extension_template( - paths: &PglitePaths, + paths: &OliphauntPaths, template: &CachedExtensionPgDataTemplate, ) -> Result<()> { let _phase = timing::phase("pgdata.extension_template_clone"); @@ -837,15 +975,15 @@ fn install_pgdata_template_clone_from_extension_template( Ok(()) } -fn pgdata_overlay_manifest_path(paths: &PglitePaths) -> PathBuf { +fn pgdata_overlay_manifest_path(paths: &OliphauntPaths) -> PathBuf { paths.pgdata.join(PGDATA_OVERLAY_MANIFEST_NAME) } -fn pgdata_overlay_is_installed(paths: &PglitePaths) -> bool { +fn pgdata_overlay_is_installed(paths: &OliphauntPaths) -> bool { pgdata_overlay_manifest_path(paths).is_file() } -fn read_pgdata_overlay_manifest(paths: &PglitePaths) -> Result> { +fn read_pgdata_overlay_manifest(paths: &OliphauntPaths) -> Result> { let path = pgdata_overlay_manifest_path(paths); match fs::read(&path) { Ok(bytes) => { @@ -859,26 +997,32 @@ fn read_pgdata_overlay_manifest(paths: &PglitePaths) -> Result Result<()> { write_pgdata_overlay_manifest_values( paths, &manifest.archive_sha256, &manifest.postgres_version, + manifest.source_lane.as_deref(), + manifest.source_fingerprint.as_deref(), &[], ) } fn write_pgdata_overlay_manifest_values( - paths: &PglitePaths, + paths: &OliphauntPaths, template_archive_sha256: &str, postgres_version: &str, + source_lane: Option<&str>, + source_fingerprint: Option<&str>, extension_sql_names: &[String], ) -> Result<()> { let overlay = PgDataOverlayManifest { template_archive_sha256: template_archive_sha256.to_owned(), postgres_version: postgres_version.to_owned(), + source_lane: source_lane.map(str::to_owned), + source_fingerprint: source_fingerprint.map(str::to_owned), extension_sql_names: extension_sql_names.to_vec(), }; fs::write( @@ -899,15 +1043,6 @@ fn ensure_module_matches_template( manifest: &PgDataTemplateManifest, ) -> Result<()> { if !strict_asset_verification()? { - #[cfg(feature = "bundled")] - if runtime_tar_path().is_none() { - let expected = assets::expected_module_sha256("runtime:pglite")?; - ensure!( - expected.eq_ignore_ascii_case(&manifest.wasm_sha256), - "embedded PGDATA template wasm hash mismatch: manifest={} assets={expected}", - manifest.wasm_sha256 - ); - } return Ok(()); } @@ -939,6 +1074,8 @@ fn validated_embedded_pgdata_template_manifest() -> Result Result Result<()> { + let metadata = assets::asset_manifest_metadata()?; + let asset_source_lane = metadata + .source_lane + .as_deref() + .context("asset manifest is missing source-lane metadata")?; + let template_source_lane = manifest + .source_lane + .as_deref() + .context("embedded PGDATA template manifest is missing source-lane metadata")?; + ensure!( + template_source_lane == asset_source_lane, + "embedded PGDATA template source lane mismatch: template={} assets={asset_source_lane}", + template_source_lane + ); + if let Some(pgdata_source_lane) = metadata.pgdata_template_source_lane.as_deref() { + ensure!( + template_source_lane == pgdata_source_lane, + "embedded PGDATA template source lane mismatch: template={} asset-entry={pgdata_source_lane}", + template_source_lane + ); + } + + if let Some(expected) = metadata.pgdata_template_postgres_version.as_deref() { + ensure!( + manifest.postgres_version == expected, + "embedded PGDATA template PostgreSQL version mismatch: template={} asset-entry={expected}", + manifest.postgres_version + ); + } + + let expected_fingerprint = metadata + .pgdata_template_source_fingerprint + .as_deref() + .or(metadata.source_fingerprint.as_deref()); + if let Some(expected) = expected_fingerprint { + ensure!( + manifest.source_fingerprint.as_deref() == Some(expected), + "embedded PGDATA template source fingerprint mismatch: template={} assets={expected}", + manifest + .source_fingerprint + .as_deref() + .unwrap_or("") + ); + } + + Ok(()) +} + fn pgdata_template_cache() -> Result> { PGDATA_TEMPLATE_CACHE .get_or_init(|| { @@ -975,8 +1161,8 @@ fn build_pgdata_template_cache() -> Result { bail!("embedded PGDATA template archive is unavailable"); }; - let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") - .context("could not resolve pglite-oxide cache directory")?; + let dirs = ProjectDirs::from("dev", "oliphaunt-wasix", "oliphaunt-wasix") + .context("could not resolve oliphaunt-wasix cache directory")?; let cache_root = dirs .cache_dir() .join("pgdata-template") @@ -1050,8 +1236,8 @@ fn build_extension_pgdata_template_cache( ensure_module_matches_template(module_path, &base_manifest)?; let manifest = extension_pgdata_template_manifest(&base_manifest, extensions, postgres_config)?; - let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") - .context("could not resolve pglite-oxide cache directory")?; + let dirs = ProjectDirs::from("dev", "oliphaunt-wasix", "oliphaunt-wasix") + .context("could not resolve oliphaunt-wasix cache directory")?; let cache_root = dirs .cache_dir() .join("pgdata-extension-template") @@ -1086,7 +1272,7 @@ fn build_extension_pgdata_template_cache( let _ = fs::remove_dir_all(&staging_root); return Err(err); } - let staging_pgdata = PglitePaths::with_root(&staging_root).pgdata; + let staging_pgdata = OliphauntPaths::with_root(&staging_root).pgdata; validate_pgdata_template_dir(&staging_pgdata, &base_manifest)?; remove_template_runtime_state(&staging_pgdata)?; fs::rename(&staging_pgdata, &pgdata).with_context(|| { @@ -1125,7 +1311,7 @@ fn build_extension_pgdata_template_staging( fs::create_dir_all(staging_root) .with_context(|| format!("create build dir {}", staging_root.display()))?; - let paths = PglitePaths::with_root(staging_root); + let paths = OliphauntPaths::with_root(staging_root); let (runtime_layout, unpacked_runtime) = prepare_runtime_layout(&paths, RuntimeLayoutPolicy::FullLocal)?; let base_template = pgdata_template_cache()?; @@ -1138,7 +1324,7 @@ fn build_extension_pgdata_template_staging( runtime_layout, preinstalled_extensions: Vec::new(), }; - let mut db = Pglite::new_prepared_with_config( + let mut db = Oliphaunt::new_prepared_with_config( outcome, postgres_config.clone(), StartupConfig::default(), @@ -1189,9 +1375,19 @@ fn extension_pgdata_template_manifest( let postgres_config_entries = postgres_config.stable_entries(); let mut hasher = Sha256::new(); - hasher.update(b"pglite-oxide-extension-pgdata-template-v4-startup-config\n"); + hasher.update(b"oliphaunt-wasix-extension-pgdata-template-v5-source-metadata\n"); hasher.update(base_manifest.postgres_version.as_bytes()); hasher.update(b"\n"); + if let Some(source_lane) = &base_manifest.source_lane { + hasher.update(b"source-lane="); + hasher.update(source_lane.as_bytes()); + hasher.update(b"\n"); + } + if let Some(source_fingerprint) = &base_manifest.source_fingerprint { + hasher.update(b"source-fingerprint="); + hasher.update(source_fingerprint.as_bytes()); + hasher.update(b"\n"); + } hasher.update(base_manifest.archive_sha256.as_bytes()); hasher.update(b"\n"); hasher.update(base_manifest.wasm_sha256.as_bytes()); @@ -1214,8 +1410,10 @@ fn extension_pgdata_template_manifest( let cache_key = format!("{:x}", hasher.finalize()); Ok(ExtensionPgDataTemplateManifest { - version: 3, + version: 5, postgres_version: base_manifest.postgres_version.clone(), + source_lane: base_manifest.source_lane.clone(), + source_fingerprint: base_manifest.source_fingerprint.clone(), base_template_archive_sha256: base_manifest.archive_sha256.clone(), base_template_wasm_sha256: base_manifest.wasm_sha256.clone(), extension_sql_names, @@ -1313,12 +1511,77 @@ fn remove_template_runtime_state(pgdata: &Path) -> Result<()> { Ok(()) } -fn cluster_is_complete(paths: &PglitePaths) -> bool { +fn cluster_is_complete(paths: &OliphauntPaths) -> bool { (paths.marker_cluster().is_file() && paths.marker_control_file().is_file()) || pgdata_overlay_is_installed(paths) } -fn remove_interrupted_pgdata(paths: &PglitePaths) -> Result<()> { +fn ensure_existing_pgdata_matches_runtime(paths: &OliphauntPaths) -> Result<()> { + let Some(expected_major) = runtime_postgres_major()? else { + return Ok(()); + }; + ensure_pgdata_postgres_major_matches(paths, &expected_major) +} + +fn runtime_postgres_major() -> Result> { + let metadata = assets::asset_manifest_metadata()?; + if metadata.postgres_version.trim().is_empty() { + return Ok(None); + } + Ok(Some(postgres_major_from_version( + &metadata.postgres_version, + ))) +} + +fn postgres_major_from_version(version: &str) -> String { + version + .trim() + .split('.') + .next() + .filter(|major| !major.is_empty()) + .unwrap_or(version.trim()) + .to_owned() +} + +fn ensure_pgdata_postgres_major_matches( + paths: &OliphauntPaths, + expected_major: &str, +) -> Result<()> { + ensure!( + !expected_major.trim().is_empty(), + "expected PostgreSQL major version must not be empty" + ); + + if let Some(overlay) = read_pgdata_overlay_manifest(paths)? { + let actual_major = postgres_major_from_version(&overlay.postgres_version); + ensure!( + actual_major == expected_major, + "existing PGDATA overlay at {} is PostgreSQL {}, but current Oliphaunt runtime is PostgreSQL {}; use a separate root or migrate the database before reusing this root", + paths.pgdata.display(), + actual_major, + expected_major + ); + return Ok(()); + } + + let pg_version_path = paths.marker_cluster(); + if !pg_version_path.is_file() { + return Ok(()); + } + let pg_version = fs::read_to_string(&pg_version_path) + .with_context(|| format!("read {}", pg_version_path.display()))?; + let actual_major = postgres_major_from_version(&pg_version); + ensure!( + actual_major == expected_major, + "existing PGDATA at {} is PostgreSQL {}, but current Oliphaunt runtime is PostgreSQL {}; use a separate root or migrate the database before reusing this root", + paths.pgdata.display(), + actual_major, + expected_major + ); + Ok(()) +} + +fn remove_interrupted_pgdata(paths: &OliphauntPaths) -> Result<()> { if paths.pgdata.exists() && !cluster_is_complete(paths) { fs::remove_dir_all(&paths.pgdata).with_context(|| { format!( @@ -1338,14 +1601,14 @@ fn tmp_suffix() -> u128 { } fn strict_asset_verification() -> Result { - let Some(value) = std::env::var_os("PGLITE_OXIDE_AOT_VERIFY") else { + let Some(value) = std::env::var_os("OLIPHAUNT_WASM_AOT_VERIFY") else { return Ok(false); }; let value = value.to_string_lossy().to_ascii_lowercase(); match value.as_str() { "" | "fast" | "metadata" | "receipt" | "0" | "false" | "off" => Ok(false), "full" | "sha" | "sha256" | "strict" | "1" | "true" | "on" => Ok(true), - other => bail!("unsupported PGLITE_OXIDE_AOT_VERIFY={other}; use `fast` or `full`"), + other => bail!("unsupported OLIPHAUNT_WASM_AOT_VERIFY={other}; use `fast` or `full`"), } } @@ -1360,26 +1623,26 @@ fn sha256_hex(bytes: &[u8]) -> String { format!("{:x}", hasher.finalize()) } -pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> { +pub fn ensure_cluster(paths: &OliphauntPaths) -> Result<()> { ensure_cluster_with_template(paths, true) } -fn ensure_cluster_with_template(paths: &PglitePaths, use_template: bool) -> Result<()> { +fn ensure_cluster_with_template(paths: &OliphauntPaths, use_template: bool) -> Result<()> { let outcome = prepare_database_root(paths.clone(), prepare_options_for_template(use_template))?; let mut pg = PostgresMod::new_prepared(outcome.paths.clone(), outcome.runtime_layout.clone())?; pg.ensure_cluster() } -pub fn preload_runtime_module(paths: &PglitePaths) -> Result<()> { +pub fn preload_runtime_module(paths: &OliphauntPaths) -> Result<()> { let _ = paths; let cached_runtime = runtime_cache()?; - let module_path = cached_runtime.runtime_root.join("bin/pglite"); + let module_path = cached_runtime.runtime_root.join("bin/oliphaunt"); PostgresMod::preload_module(&module_path) } #[derive(Debug, Clone)] pub struct InstallOutcome { - pub paths: PglitePaths, + pub paths: OliphauntPaths, pub unpacked_runtime: bool, pub(crate) runtime_layout: RuntimeLayout, #[cfg_attr(not(feature = "extensions"), allow(dead_code))] @@ -1396,7 +1659,7 @@ impl InstallOutcome { } fn prepare_root_from_paths( - paths: PglitePaths, + paths: OliphauntPaths, root: PathBuf, temp_dir: Option, root_lock: Option, @@ -1414,7 +1677,7 @@ fn prepare_root_from_paths( pub(crate) fn prepare_root(plan: RootPlan) -> Result { let (paths, root, temp_dir, root_lock, temporary) = match plan.target { RootTarget::Path(root) => { - let paths = PglitePaths::with_root(&root); + let paths = OliphauntPaths::with_root(&root); let root_lock = RootLock::acquire(&root)?; (paths, root, None, Some(root_lock), false) } @@ -1423,7 +1686,7 @@ pub(crate) fn prepare_root(plan: RootPlan) -> Result { organization, application, } => { - let paths = PglitePaths::new(( + let paths = OliphauntPaths::new(( qualifier.as_str(), organization.as_str(), application.as_str(), @@ -1433,9 +1696,9 @@ pub(crate) fn prepare_root(plan: RootPlan) -> Result { (paths, root, None, Some(root_lock), false) } RootTarget::Temporary => { - let temp_dir = TempDir::new().context("create temporary pglite directory")?; + let temp_dir = TempDir::new().context("create temporary oliphaunt directory")?; let root = temp_dir.path().to_path_buf(); - let paths = PglitePaths::with_root(&root); + let paths = OliphauntPaths::with_root(&root); (paths, root, Some(temp_dir), None, true) } }; @@ -1450,7 +1713,7 @@ pub(crate) fn prepare_root(plan: RootPlan) -> Result { #[cfg(feature = "extensions")] { let mut prepared = prepared; - if temporary && use_template { + if temporary && use_template && extension_template_cache_enabled() { install_extension_template_into_outcome( &mut prepared.outcome, &plan.extensions, @@ -1469,7 +1732,7 @@ pub(crate) fn prepare_root(plan: RootPlan) -> Result { } fn prepare_root_from_data_dir_archive( - paths: PglitePaths, + paths: OliphauntPaths, root: PathBuf, temp_dir: Option, root_lock: Option, @@ -1490,6 +1753,7 @@ fn prepare_root_from_data_dir_archive( paths.marker_cluster().is_file() && paths.marker_control_file().is_file(), "loaded PGDATA archive did not contain PG_VERSION and global/pg_control" ); + ensure_existing_pgdata_matches_runtime(&paths)?; Ok(PreparedRoot { root, temp_dir, @@ -1514,7 +1778,7 @@ pub(crate) fn install_missing_extension_archives( } let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { anyhow!( - "extension asset '{}' is not bundled in this pglite-oxide build", + "extension asset '{}' is not bundled in this oliphaunt-wasix build", extension.sql_name() ) })?; @@ -1539,12 +1803,12 @@ impl Default for InstallOptions { #[derive(Debug, Clone)] pub struct MountInfo { mount: PathBuf, - paths: PglitePaths, + paths: OliphauntPaths, reused_existing: bool, } impl MountInfo { - pub fn into_paths(self) -> PglitePaths { + pub fn into_paths(self) -> OliphauntPaths { self.paths } @@ -1552,7 +1816,7 @@ impl MountInfo { &self.mount } - pub fn paths(&self) -> &PglitePaths { + pub fn paths(&self) -> &OliphauntPaths { &self.paths } @@ -1562,17 +1826,17 @@ impl MountInfo { } pub fn install_default(app_id: (&str, &str, &str)) -> Result { - let paths = PglitePaths::new(app_id)?; + let paths = OliphauntPaths::new(app_id)?; prepare_database_root(paths, RootPrepareOptions::template()) } pub fn install_into(root: &Path) -> Result { - let paths = PglitePaths::with_root(root); + let paths = OliphauntPaths::with_root(root); prepare_database_root(paths, RootPrepareOptions::template()) } pub(crate) fn prepare_database_root( - paths: PglitePaths, + paths: OliphauntPaths, options: RootPrepareOptions, ) -> Result { let (mut runtime_layout, unpacked_runtime) = prepare_runtime_layout(&paths, options.runtime)?; @@ -1586,7 +1850,7 @@ pub(crate) fn prepare_database_root( } fn prepare_pgdata( - paths: &PglitePaths, + paths: &OliphauntPaths, cluster_policy: ClusterPolicy, runtime_layout: &mut RuntimeLayout, ) -> Result<()> { @@ -1606,6 +1870,7 @@ fn prepare_pgdata( } } if cluster_is_complete(paths) { + ensure_existing_pgdata_matches_runtime(paths)?; remove_template_runtime_state(&paths.pgdata)?; return Ok(()); } @@ -1674,7 +1939,7 @@ pub fn install_and_init_in>(root: P) -> Result { }) } -pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result { +pub fn install_with_options(paths: OliphauntPaths, options: InstallOptions) -> Result { let outcome = prepare_database_root(paths, RootPrepareOptions::template())?; if options.ensure_cluster && !cluster_is_complete(&outcome.paths) { let mut pg = @@ -1708,7 +1973,7 @@ pub(crate) fn pgdata_overlay_enabled() -> bool { } fn prepare_runtime_layout( - paths: &PglitePaths, + paths: &OliphauntPaths, policy: RuntimeLayoutPolicy, ) -> Result<(RuntimeLayout, bool)> { match resolve_runtime_layout_kind(paths, policy)? { @@ -1754,7 +2019,7 @@ fn prepare_runtime_layout( } fn resolve_runtime_layout_kind( - paths: &PglitePaths, + paths: &OliphauntPaths, policy: RuntimeLayoutPolicy, ) -> Result { match policy { @@ -1819,12 +2084,12 @@ fn build_runtime_cache() -> Result { let _phase = timing::phase("runtime.cache_key"); runtime_cache_key()? }; - let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") - .context("could not resolve pglite-oxide cache directory")?; + let dirs = ProjectDirs::from("dev", "oliphaunt-wasix", "oliphaunt-wasix") + .context("could not resolve oliphaunt-wasix cache directory")?; let cache_root = dirs.cache_dir().join("runtime"); let _cache_lock = CacheLock::acquire(&cache_root.join(".locks").join(format!("{key}.lock")))?; let root = cache_root.join(&key); - let paths = PglitePaths::with_root(root); + let paths = OliphauntPaths::with_root(root); { let _phase = timing::phase("runtime.cache_ensure_full"); ensure_full_runtime(&paths)?; @@ -1895,42 +2160,24 @@ fn ensure_runtime_password_file(runtime_root: &Path) -> Result<()> { } fn runtime_cache_key() -> Result { - if assets::runtime_archive().is_some() { - return embedded_runtime_archive_sha256(); - } - if let Some(path) = runtime_tar_path() { - if strict_asset_verification()? { - return sha256_file(&path); + if let Some(runtime_archive) = assets::runtime_archive() { + let mut hasher = Sha256::new(); + hasher.update(b"oliphaunt-wasix-runtime-cache-v2\nruntime="); + hasher.update(sha256_hex(runtime_archive).as_bytes()); + hasher.update(b"\nicu="); + if let Some(icu_archive) = assets::icu_data_archive() { + hasher.update(sha256_hex(icu_archive).as_bytes()); + } else { + hasher.update(b"absent"); } - return file_metadata_cache_key(&path); + return Ok(format!("{:x}", hasher.finalize())); } bail!( - "no embedded PGlite runtime assets are available; enable the `bundled` feature or set PGLITE_OXIDE_RUNTIME_ARCHIVE" + "Oliphaunt WASIX runtime assets are unavailable; package-manager-resolved runtime artifacts were not staged" ) } -#[cfg(feature = "bundled")] -fn embedded_runtime_archive_sha256() -> Result { - assets::expected_runtime_archive_sha256() -} - -#[cfg(not(feature = "bundled"))] -fn embedded_runtime_archive_sha256() -> Result { - bail!("embedded runtime archive is unavailable without the `bundled` feature") -} - -fn file_metadata_cache_key(path: &Path) -> Result { - let metadata = fs::metadata(path).with_context(|| format!("stat {}", path.display()))?; - let modified_nanos = metadata - .modified() - .ok() - .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) - .map(|duration| duration.as_nanos()) - .unwrap_or_default(); - Ok(format!("external-{}-{modified_nanos}", metadata.len())) -} - -fn prepare_shared_runtime_upper_root(src_runtime: &Path, paths: &PglitePaths) -> Result<()> { +fn prepare_shared_runtime_upper_root(src_runtime: &Path, paths: &OliphauntPaths) -> Result<()> { let _phase = timing::phase("runtime.mountfs_upper_root"); let dest_runtime = paths.runtime_root(); @@ -1993,320 +2240,10 @@ fn copy_runtime_file_if_exists(src: PathBuf, dest: PathBuf) -> Result<()> { Ok(()) } -#[cfg(test)] -fn copy_template_pgdata(template_root: &Path, dest_root: &Path) -> Result<()> { - let source_pgdata = template_root.join("tmp/pglite/base"); - clone_pgdata_template_dir(&source_pgdata, &dest_root.join("tmp/pglite/base")) -} - -fn clone_pgdata_template_dir(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { - if try_clone_dir(source_pgdata, dest_pgdata)? { - return Ok(()); - } - copy_pgdata_template_dir_inner(source_pgdata, dest_pgdata) -} - -fn copy_pgdata_template_dir_inner(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { - fs::create_dir_all(dest_pgdata) - .with_context(|| format!("create directory {}", dest_pgdata.display()))?; - - for entry in fs::read_dir(source_pgdata) - .with_context(|| format!("read directory {}", source_pgdata.display()))? - { - let entry = - entry.with_context(|| format!("read entry under {}", source_pgdata.display()))?; - let file_name = entry.file_name(); - if should_skip_template_entry(&file_name) { - continue; - } - - let src_path = entry.path(); - let dest_path = dest_pgdata.join(&file_name); - let file_type = entry - .file_type() - .with_context(|| format!("stat {}", src_path.display()))?; - - if file_type.is_dir() { - copy_pgdata_template_dir_inner(&src_path, &dest_path)?; - } else if file_type.is_file() { - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - clone_mutable_template_file(&src_path, &dest_path)?; - } else if file_type.is_symlink() { - copy_symlink(&src_path, &dest_path)?; - } - } - - Ok(()) -} - -fn clone_mutable_template_file(src: &Path, dest: &Path) -> Result<()> { - if std::env::var_os("PGLITE_OXIDE_TEMPLATE_REFLINK").is_some() && try_reflink_file(src, dest)? { - return Ok(()); - } - copy_template_file(src, dest) -} - -fn try_clone_dir(src: &Path, dest: &Path) -> Result { - if dest.exists() { - fs::remove_dir_all(dest).with_context(|| format!("remove {}", dest.display()))?; - } - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; - } - - let status = clone_dir_command(src, dest); - match status { - Ok(status) if status.success() && dest.exists() => Ok(true), - Ok(_) | Err(_) => { - if dest.exists() { - fs::remove_dir_all(dest).with_context(|| { - format!("remove failed cloned directory {}", dest.display()) - })?; - } - Ok(false) - } - } -} - -#[cfg(target_os = "linux")] -fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { - Command::new("cp") - .arg("-a") - .arg("--reflink=auto") - .arg("--") - .arg(src) - .arg(dest) - .status() -} - -#[cfg(target_os = "macos")] -fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { - Command::new("cp").arg("-cR").arg(src).arg(dest).status() -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn clone_dir_command(_src: &Path, _dest: &Path) -> std::io::Result { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "directory clone is unsupported on this platform", - )) -} - -fn copy_template_file(src: &Path, dest: &Path) -> Result<()> { - fs::copy(src, dest).with_context(|| format!("copy {} to {}", src.display(), dest.display()))?; - Ok(()) -} - -#[cfg(target_os = "linux")] -fn try_reflink_file(src: &Path, dest: &Path) -> Result { - let status = Command::new("cp") - .arg("--reflink=always") - .arg("--") - .arg(src) - .arg(dest) - .status(); - match status { - Ok(status) if status.success() && dest.exists() => Ok(true), - Ok(_) | Err(_) => { - let _ = fs::remove_file(dest); - Ok(false) - } - } -} - -#[cfg(target_os = "macos")] -fn try_reflink_file(src: &Path, dest: &Path) -> Result { - let status = Command::new("cp").arg("-c").arg(src).arg(dest).status(); - match status { - Ok(status) if status.success() && dest.exists() => Ok(true), - Ok(_) | Err(_) => { - let _ = fs::remove_file(dest); - Ok(false) - } - } -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -fn try_reflink_file(_src: &Path, _dest: &Path) -> Result { - Ok(false) -} - -fn should_skip_template_entry(file_name: &OsStr) -> bool { - let name = file_name.to_string_lossy(); - name.starts_with(".s.PGSQL.") || TEMPLATE_RUNTIME_STATE_FILES.contains(&name.as_ref()) -} - -#[cfg(unix)] -fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; - std::os::unix::fs::symlink(&target, dest) - .with_context(|| format!("create symlink {} -> {}", dest.display(), target.display()))?; - Ok(()) -} - -#[cfg(not(unix))] -fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { - let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; - let target_path = if target.is_absolute() { - target - } else { - src.parent().unwrap_or_else(|| Path::new(".")).join(target) - }; - - if target_path.is_dir() { - copy_pgdata_template_dir_inner(&target_path, dest) - } else { - if let Some(parent) = dest.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create directory {}", parent.display()))?; - } - fs::copy(&target_path, dest) - .with_context(|| format!("copy {} to {}", target_path.display(), dest.display()))?; - Ok(()) - } -} - #[cfg(test)] mod tests { use super::*; - #[test] - fn template_copy_keeps_cluster_files_and_skips_runtime_state() -> Result<()> { - let source = TempDir::new()?; - let pgdata = source.path().join("tmp/pglite/base"); - fs::create_dir_all(&pgdata)?; - fs::write(pgdata.join("PG_VERSION"), b"17\n")?; - fs::write(pgdata.join("postmaster.pid"), b"stale pid")?; - fs::write(pgdata.join("postmaster.opts"), b"stale opts")?; - fs::write(pgdata.join(".s.PGSQL.5432"), b"socket")?; - fs::write(pgdata.join(".s.PGSQL.5432.lock"), b"lock")?; - - let dest = TempDir::new()?; - let dest_pgdata = dest.path().join("tmp/pglite/base"); - copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; - - assert!( - dest_pgdata.join("PG_VERSION").exists(), - "destination entries: {}", - list_test_entries(dest.path())? - ); - assert!(!dest_pgdata.join("postmaster.pid").exists()); - assert!(!dest_pgdata.join("postmaster.opts").exists()); - assert!(!dest_pgdata.join(".s.PGSQL.5432").exists()); - assert!(!dest_pgdata.join(".s.PGSQL.5432.lock").exists()); - Ok(()) - } - - #[cfg(unix)] - #[test] - fn template_clone_does_not_hardlink_mutable_pgdata_files() -> Result<()> { - use std::os::unix::fs::MetadataExt; - - let source = TempDir::new()?; - let pgdata = source.path().join("tmp/pglite/base"); - fs::create_dir_all(&pgdata)?; - fs::write(pgdata.join("PG_VERSION"), b"17\n")?; - - let dest = TempDir::new()?; - let dest_pgdata = dest.path().join("tmp/pglite/base"); - copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; - - let source_pg_version = pgdata.join("PG_VERSION"); - let dest_pg_version = dest_pgdata.join("PG_VERSION"); - assert!( - source_pg_version.exists(), - "source PG_VERSION should exist at {}", - source_pg_version.display() - ); - assert!( - dest_pg_version.exists(), - "cloned PG_VERSION should exist at {}; destination entries: {}", - dest_pg_version.display(), - list_test_entries(dest.path())? - ); - let source_meta = fs::metadata(&source_pg_version)?; - let dest_meta = fs::metadata(&dest_pg_version)?; - assert_ne!( - (source_meta.dev(), source_meta.ino()), - (dest_meta.dev(), dest_meta.ino()), - "mutable PGDATA template files must be copied or reflinked, not hardlinked" - ); - Ok(()) - } - - #[cfg(unix)] - #[test] - fn fallback_template_pgdata_copy_does_not_hardlink_mutable_files() -> Result<()> { - use std::os::unix::fs::MetadataExt; - - let source = TempDir::new()?; - let pgdata = source.path().join("tmp/pglite/base"); - fs::create_dir_all(&pgdata)?; - fs::write(pgdata.join("PG_VERSION"), b"17\n")?; - - let dest = TempDir::new()?; - copy_template_pgdata(source.path(), dest.path())?; - - let source_pg_version = pgdata.join("PG_VERSION"); - let dest_pg_version = dest.path().join("tmp/pglite/base/PG_VERSION"); - assert!(dest_pg_version.exists()); - let source_meta = fs::metadata(&source_pg_version)?; - let dest_meta = fs::metadata(&dest_pg_version)?; - assert_ne!( - (source_meta.dev(), source_meta.ino()), - (dest_meta.dev(), dest_meta.ino()), - "fallback PGDATA template copy must not hardlink mutable files" - ); - Ok(()) - } - - #[test] - fn fallback_template_pgdata_copy_does_not_share_mutable_files() -> Result<()> { - let source = TempDir::new()?; - let pgdata = source.path().join("base"); - fs::create_dir_all(&pgdata)?; - fs::write(pgdata.join("PG_VERSION"), b"17\n")?; - - let dest = TempDir::new()?; - let cloned = dest.path().join("base"); - copy_pgdata_template_dir_inner(&pgdata, &cloned)?; - fs::write(cloned.join("PG_VERSION"), b"changed\n")?; - - assert_eq!( - fs::read(pgdata.join("PG_VERSION"))?, - b"17\n", - "fallback PGDATA template copy must not share mutable file storage with the source" - ); - Ok(()) - } - - fn list_test_entries(root: &Path) -> Result { - let mut entries = Vec::new(); - collect_test_entries(root, root, &mut entries)?; - entries.sort(); - Ok(entries.join(", ")) - } - - fn collect_test_entries(root: &Path, current: &Path, entries: &mut Vec) -> Result<()> { - for entry in fs::read_dir(current)? { - let entry = entry?; - let path = entry.path(); - let relative = path.strip_prefix(root).unwrap_or(&path); - entries.push(relative.display().to_string()); - if entry.file_type()?.is_dir() { - collect_test_entries(root, &path, entries)?; - } - } - Ok(()) - } - #[cfg(feature = "extensions")] #[test] fn embedded_pgdata_template_installs_valid_cluster() -> Result<()> { @@ -2315,7 +2252,7 @@ mod tests { } let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); ensure_full_runtime(&paths)?; let (module_path, _) = @@ -2336,7 +2273,7 @@ mod tests { } let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); ensure_full_runtime(&paths)?; fs::create_dir_all(paths.pgdata.join("global"))?; fs::write(paths.pgdata.join("postmaster.pid"), b"stale pid")?; @@ -2365,7 +2302,7 @@ mod tests { return Ok(()); } let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); fs::create_dir_all(&paths.pgdata)?; fs::write(paths.pgdata.join("postmaster.pid"), b"stale pid")?; fs::write(paths.pgdata.join("partial"), b"interrupted initdb")?; @@ -2389,7 +2326,7 @@ mod tests { return Ok(()); } let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); fs::create_dir_all(&paths.pgdata)?; fs::write(paths.pgdata.join("PG_VERSION"), b"17\n")?; fs::write( @@ -2412,11 +2349,11 @@ mod tests { fn root_lock_is_exclusive_until_dropped() -> Result<()> { let temp_dir = TempDir::new()?; let first = RootLock::acquire(temp_dir.path())?; - assert!(temp_dir.path().join(".pglite-oxide.lock").exists()); + assert!(temp_dir.path().join(".oliphaunt-wasix.lock").exists()); let err = RootLock::acquire(temp_dir.path()).expect_err("second root lock should be rejected"); - assert!(format!("{err:#}").contains("PGlite root is already in use")); + assert!(format!("{err:#}").contains("Oliphaunt root is already in use")); drop(first); let _second = RootLock::acquire(temp_dir.path())?; @@ -2430,6 +2367,73 @@ mod tests { assert!(err.to_string().contains("unsafe archive path")); } + #[test] + fn pgdata_major_guard_accepts_same_major_cluster() -> Result<()> { + let temp_dir = TempDir::new()?; + let paths = OliphauntPaths::with_root(temp_dir.path()); + fs::create_dir_all(paths.pgdata.join("global"))?; + fs::write(paths.pgdata.join("PG_VERSION"), b"18\n")?; + fs::write(paths.pgdata.join("global/pg_control"), b"control")?; + + ensure_pgdata_postgres_major_matches(&paths, "18")?; + Ok(()) + } + + #[test] + fn pgdata_major_guard_rejects_cross_major_cluster() -> Result<()> { + let temp_dir = TempDir::new()?; + let paths = OliphauntPaths::with_root(temp_dir.path()); + fs::create_dir_all(paths.pgdata.join("global"))?; + fs::write(paths.pgdata.join("PG_VERSION"), b"17\n")?; + fs::write(paths.pgdata.join("global/pg_control"), b"control")?; + + let err = ensure_pgdata_postgres_major_matches(&paths, "18") + .expect_err("cross-major PGDATA must be rejected"); + assert!( + format!("{err:#}").contains("existing PGDATA") + && format!("{err:#}").contains("PostgreSQL 17") + && format!("{err:#}").contains("PostgreSQL 18"), + "unexpected error: {err:#}" + ); + Ok(()) + } + + #[test] + fn pgdata_major_guard_rejects_cross_major_overlay() -> Result<()> { + let temp_dir = TempDir::new()?; + let paths = OliphauntPaths::with_root(temp_dir.path()); + fs::create_dir_all(&paths.pgdata)?; + fs::write( + pgdata_overlay_manifest_path(&paths), + br#"{ + "templateArchiveSha256": "old-template", + "postgresVersion": "17", + "extensionSqlNames": [] + }"#, + )?; + + let err = ensure_pgdata_postgres_major_matches(&paths, "18") + .expect_err("cross-major PGDATA overlay must be rejected"); + assert!( + format!("{err:#}").contains("existing PGDATA overlay") + && format!("{err:#}").contains("PostgreSQL 17") + && format!("{err:#}").contains("PostgreSQL 18"), + "unexpected error: {err:#}" + ); + Ok(()) + } + + #[test] + fn full_runtime_layout_requires_current_source_key() -> Result<()> { + let temp_dir = TempDir::new()?; + let paths = OliphauntPaths::with_root(temp_dir.path()); + write_runtime_layout_manifest(&paths.runtime_root(), RuntimeLayoutKind::FullLocal, "old")?; + + assert!(!full_runtime_layout_matches_current(&paths, "new")?); + assert!(full_runtime_layout_matches_current(&paths, "old")?); + Ok(()) + } + fn tar_bytes_with_entry(path: &[u8], entry_type: u8, body: &[u8], link_name: &[u8]) -> Vec { let mut header = [0u8; 512]; header[..path.len()].copy_from_slice(path); @@ -2462,7 +2466,7 @@ mod tests { fn extension_archive_rejects_parent_components() -> Result<()> { let bytes = tar_bytes_with_entry(b"../escape", b'0', b"nope", b""); let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); let err = install_extension_bytes(&paths, &bytes).expect_err("unsafe archive must fail"); assert!(err.to_string().contains("unpack extension")); Ok(()) @@ -2477,7 +2481,7 @@ mod tests { b"/tmp/attacker-owned-vector.so", ); let temp_dir = TempDir::new()?; - let paths = PglitePaths::with_root(temp_dir.path()); + let paths = OliphauntPaths::with_root(temp_dir.path()); let err = install_extension_bytes(&paths, &bytes).expect_err("symlink archive must fail"); assert!( err.chain() @@ -2502,17 +2506,8 @@ mod tests { #[cfg(feature = "extensions")] #[test] - fn bundled_extension_archive_hash_is_validated() -> Result<()> { - let mut bytes = assets::extension_archive("vector") - .expect("embedded vector archive") - .to_vec(); - bytes[0] ^= 0xff; - let err = validate_bundled_extension_archive_strict("vector", &bytes) - .expect_err("corrupted extension archive hash must fail"); - assert!( - err.to_string() - .contains("extension archive 'vector' hash mismatch") - ); - Ok(()) + fn base_wasix_assets_do_not_embed_extension_archives() { + assert!(assets::extension_archive("vector").is_none()); + assert!(assets::extension_archive("hstore").is_none()); } } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base/template_clone.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base/template_clone.rs new file mode 100644 index 00000000..d3400b47 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/base/template_clone.rs @@ -0,0 +1,326 @@ +use std::ffi::OsStr; +use std::fs; +use std::path::Path; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::process::Command; + +use anyhow::{Context, Result}; + +use super::TEMPLATE_RUNTIME_STATE_FILES; + +pub(super) fn clone_pgdata_template_dir(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { + if try_clone_dir(source_pgdata, dest_pgdata)? { + return Ok(()); + } + copy_pgdata_template_dir_inner(source_pgdata, dest_pgdata) +} + +fn copy_pgdata_template_dir_inner(source_pgdata: &Path, dest_pgdata: &Path) -> Result<()> { + fs::create_dir_all(dest_pgdata) + .with_context(|| format!("create directory {}", dest_pgdata.display()))?; + + for entry in fs::read_dir(source_pgdata) + .with_context(|| format!("read directory {}", source_pgdata.display()))? + { + let entry = + entry.with_context(|| format!("read entry under {}", source_pgdata.display()))?; + let file_name = entry.file_name(); + if should_skip_template_entry(&file_name) { + continue; + } + + let src_path = entry.path(); + let dest_path = dest_pgdata.join(&file_name); + let file_type = entry + .file_type() + .with_context(|| format!("stat {}", src_path.display()))?; + + if file_type.is_dir() { + copy_pgdata_template_dir_inner(&src_path, &dest_path)?; + } else if file_type.is_file() { + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + clone_mutable_template_file(&src_path, &dest_path)?; + } else if file_type.is_symlink() { + copy_symlink(&src_path, &dest_path)?; + } + } + + Ok(()) +} + +fn clone_mutable_template_file(src: &Path, dest: &Path) -> Result<()> { + if std::env::var_os("OLIPHAUNT_WASM_TEMPLATE_REFLINK").is_some() && try_reflink_file(src, dest)? + { + return Ok(()); + } + copy_template_file(src, dest) +} + +fn try_clone_dir(src: &Path, dest: &Path) -> Result { + if dest.exists() { + fs::remove_dir_all(dest).with_context(|| format!("remove {}", dest.display()))?; + } + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; + } + + let status = clone_dir_command(src, dest); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + if dest.exists() { + fs::remove_dir_all(dest).with_context(|| { + format!("remove failed cloned directory {}", dest.display()) + })?; + } + Ok(false) + } + } +} + +#[cfg(target_os = "linux")] +fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { + Command::new("cp") + .arg("-a") + .arg("--reflink=auto") + .arg("--") + .arg(src) + .arg(dest) + .status() +} + +#[cfg(target_os = "macos")] +fn clone_dir_command(src: &Path, dest: &Path) -> std::io::Result { + Command::new("cp").arg("-cR").arg(src).arg(dest).status() +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn clone_dir_command(_src: &Path, _dest: &Path) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "directory clone is unsupported on this platform", + )) +} + +fn copy_template_file(src: &Path, dest: &Path) -> Result<()> { + fs::copy(src, dest).with_context(|| format!("copy {} to {}", src.display(), dest.display()))?; + Ok(()) +} + +#[cfg(target_os = "linux")] +fn try_reflink_file(src: &Path, dest: &Path) -> Result { + let status = Command::new("cp") + .arg("--reflink=always") + .arg("--") + .arg(src) + .arg(dest) + .status(); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + let _ = fs::remove_file(dest); + Ok(false) + } + } +} + +#[cfg(target_os = "macos")] +fn try_reflink_file(src: &Path, dest: &Path) -> Result { + let status = Command::new("cp").arg("-c").arg(src).arg(dest).status(); + match status { + Ok(status) if status.success() && dest.exists() => Ok(true), + Ok(_) | Err(_) => { + let _ = fs::remove_file(dest); + Ok(false) + } + } +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +fn try_reflink_file(_src: &Path, _dest: &Path) -> Result { + Ok(false) +} + +fn should_skip_template_entry(file_name: &OsStr) -> bool { + let name = file_name.to_string_lossy(); + name.starts_with(".s.PGSQL.") || TEMPLATE_RUNTIME_STATE_FILES.contains(&name.as_ref()) +} + +#[cfg(unix)] +fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; + std::os::unix::fs::symlink(&target, dest) + .with_context(|| format!("create symlink {} -> {}", dest.display(), target.display()))?; + Ok(()) +} + +#[cfg(not(unix))] +fn copy_symlink(src: &Path, dest: &Path) -> Result<()> { + let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?; + let target_path = if target.is_absolute() { + target + } else { + src.parent().unwrap_or_else(|| Path::new(".")).join(target) + }; + + if target_path.is_dir() { + copy_pgdata_template_dir_inner(&target_path, dest) + } else { + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent) + .with_context(|| format!("create directory {}", parent.display()))?; + } + fs::copy(&target_path, dest) + .with_context(|| format!("copy {} to {}", target_path.display(), dest.display()))?; + Ok(()) + } +} + +#[cfg(test)] +fn copy_template_pgdata(template_root: &Path, dest_root: &Path) -> Result<()> { + let source_pgdata = template_root.join("tmp/oliphaunt/base"); + clone_pgdata_template_dir(&source_pgdata, &dest_root.join("tmp/oliphaunt/base")) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn template_copy_keeps_cluster_files_and_skips_runtime_state() -> Result<()> { + let source = TempDir::new()?; + let pgdata = source.path().join("tmp/oliphaunt/base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + fs::write(pgdata.join("postmaster.pid"), b"stale pid")?; + fs::write(pgdata.join("postmaster.opts"), b"stale opts")?; + fs::write(pgdata.join(".s.PGSQL.5432"), b"socket")?; + fs::write(pgdata.join(".s.PGSQL.5432.lock"), b"lock")?; + + let dest = TempDir::new()?; + let dest_pgdata = dest.path().join("tmp/oliphaunt/base"); + copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; + + assert!( + dest_pgdata.join("PG_VERSION").exists(), + "destination entries: {}", + list_test_entries(dest.path())? + ); + assert!(!dest_pgdata.join("postmaster.pid").exists()); + assert!(!dest_pgdata.join("postmaster.opts").exists()); + assert!(!dest_pgdata.join(".s.PGSQL.5432").exists()); + assert!(!dest_pgdata.join(".s.PGSQL.5432.lock").exists()); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn template_clone_does_not_hardlink_mutable_pgdata_files() -> Result<()> { + use std::os::unix::fs::MetadataExt; + + let source = TempDir::new()?; + let pgdata = source.path().join("tmp/oliphaunt/base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + + let dest = TempDir::new()?; + let dest_pgdata = dest.path().join("tmp/oliphaunt/base"); + copy_pgdata_template_dir_inner(&pgdata, &dest_pgdata)?; + + let source_pg_version = pgdata.join("PG_VERSION"); + let dest_pg_version = dest_pgdata.join("PG_VERSION"); + assert!( + source_pg_version.exists(), + "source PG_VERSION should exist at {}", + source_pg_version.display() + ); + assert!( + dest_pg_version.exists(), + "cloned PG_VERSION should exist at {}; destination entries: {}", + dest_pg_version.display(), + list_test_entries(dest.path())? + ); + let source_meta = fs::metadata(&source_pg_version)?; + let dest_meta = fs::metadata(&dest_pg_version)?; + assert_ne!( + (source_meta.dev(), source_meta.ino()), + (dest_meta.dev(), dest_meta.ino()), + "mutable PGDATA template files must be copied or reflinked, not hardlinked" + ); + Ok(()) + } + + #[cfg(unix)] + #[test] + fn fallback_template_pgdata_copy_does_not_hardlink_mutable_files() -> Result<()> { + use std::os::unix::fs::MetadataExt; + + let source = TempDir::new()?; + let pgdata = source.path().join("tmp/oliphaunt/base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + + let dest = TempDir::new()?; + copy_template_pgdata(source.path(), dest.path())?; + + let source_pg_version = pgdata.join("PG_VERSION"); + let dest_pg_version = dest.path().join("tmp/oliphaunt/base/PG_VERSION"); + assert!(dest_pg_version.exists()); + let source_meta = fs::metadata(&source_pg_version)?; + let dest_meta = fs::metadata(&dest_pg_version)?; + assert_ne!( + (source_meta.dev(), source_meta.ino()), + (dest_meta.dev(), dest_meta.ino()), + "fallback PGDATA template copy must not hardlink mutable files" + ); + Ok(()) + } + + #[test] + fn fallback_template_pgdata_copy_does_not_share_mutable_files() -> Result<()> { + let source = TempDir::new()?; + let pgdata = source.path().join("base"); + fs::create_dir_all(&pgdata)?; + fs::write(pgdata.join("PG_VERSION"), b"17\n")?; + + let dest = TempDir::new()?; + let cloned = dest.path().join("base"); + copy_pgdata_template_dir_inner(&pgdata, &cloned)?; + fs::write(cloned.join("PG_VERSION"), b"changed\n")?; + + assert_eq!( + fs::read(pgdata.join("PG_VERSION"))?, + b"17\n", + "fallback PGDATA template copy must not share mutable file storage with the source" + ); + Ok(()) + } + + fn list_test_entries(root: &Path) -> Result { + let mut entries = Vec::new(); + collect_test_entries(root, root, &mut entries)?; + entries.sort(); + Ok(entries.join(", ")) + } + + fn collect_test_entries(root: &Path, current: &Path, entries: &mut Vec) -> Result<()> { + for entry in fs::read_dir(current)? { + let entry = entry?; + let path = entry.path(); + let relative = path.strip_prefix(root).unwrap_or(&path); + entries.push(relative.display().to_string()); + if entry.file_type()?.is_dir() { + collect_test_entries(root, &path, entries)?; + } + } + Ok(()) + } +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/builder.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/builder.rs new file mode 100644 index 00000000..fbe8f26d --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/builder.rs @@ -0,0 +1,272 @@ +use std::path::PathBuf; + +use anyhow::{Result, bail}; + +use crate::oliphaunt::base::{PreparedRoot, RootPlan, RootSource, RootTarget, prepare_root}; +use crate::oliphaunt::client::Oliphaunt; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use crate::oliphaunt::extensions::{Extension, resolve_extension_set}; +use crate::oliphaunt::interface::DebugLevel; + +/// Builder for opening persistent or temporary [`Oliphaunt`] databases. +#[derive(Debug, Clone)] +pub struct OliphauntBuilder { + target: Option, + template_cache: bool, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + load_data_dir_archive: Option>, + #[cfg(feature = "extensions")] + extensions: Vec, +} + +#[derive(Debug, Clone)] +enum OliphauntTarget { + Path(PathBuf), + AppId { + qualifier: String, + organization: String, + application: String, + }, + Temporary, +} + +impl Default for OliphauntBuilder { + fn default() -> Self { + Self { + target: None, + template_cache: true, + postgres_config: PostgresConfig::default(), + startup_config: StartupConfig::default(), + load_data_dir_archive: None, + #[cfg(feature = "extensions")] + extensions: Vec::new(), + } + } +} + +impl OliphauntBuilder { + /// Create a builder. Call [`path`](Self::path), [`app_id`](Self::app_id), + /// or [`temporary`](Self::temporary) before [`open`](Self::open). + pub fn new() -> Self { + Self::default() + } + + /// Open a persistent database rooted at `root`. + pub fn path(mut self, root: impl Into) -> Self { + self.target = Some(OliphauntTarget::Path(root.into())); + self + } + + /// Open a persistent database under the platform data directory. + pub fn app( + mut self, + qualifier: impl Into, + organization: impl Into, + application: impl Into, + ) -> Self { + self.target = Some(OliphauntTarget::AppId { + qualifier: qualifier.into(), + organization: organization.into(), + application: application.into(), + }); + self + } + + /// Open a persistent database under the platform data directory. + pub fn app_id(self, app_id: (&str, &str, &str)) -> Self { + self.app(app_id.0, app_id.1, app_id.2) + } + + /// Open an ephemeral database removed when the instance is dropped. + /// + /// Temporary databases use the process-local template cluster cache by + /// default, avoiding repeated `initdb` work in test suites. + pub fn temporary(mut self) -> Self { + self.target = Some(OliphauntTarget::Temporary); + self + } + + /// Control whether new databases are cloned from the process-local or + /// embedded PGDATA template cache. + pub fn template_cache(mut self, enabled: bool) -> Self { + self.template_cache = enabled; + self + } + + /// Open an ephemeral database with a fresh `initdb`. + /// + /// This is a compatibility alias for + /// `temporary().template_cache(false)`. Fresh initdb uses the bundled split + /// WASIX `initdb` module; cached temporary databases remain the production + /// fast path. + pub fn fresh_temporary(self) -> Self { + self.temporary().template_cache(false) + } + + /// Set a PostgreSQL startup GUC for this embedded backend. + pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { + self.postgres_config.insert(name, value); + self + } + + /// Set multiple PostgreSQL startup GUCs for this embedded backend. + pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + for (name, value) in settings { + self.postgres_config.insert(name, value); + } + self + } + + /// Connect as a PostgreSQL role. The role must already exist in the + /// cluster. + pub fn username(mut self, username: impl Into) -> Self { + self.startup_config.username = username.into(); + self + } + + /// Connect to a PostgreSQL database. The database must already exist in the + /// cluster. + pub fn database(mut self, database: impl Into) -> Self { + self.startup_config.database = database.into(); + self + } + + /// Enable PostgreSQL debug logging level `0..=5` for the embedded backend. + pub fn debug_level(mut self, level: DebugLevel) -> Self { + self.startup_config.debug_level = Some(level); + self + } + + /// Use lower durability settings for ephemeral or cacheable local + /// workloads. + pub fn relaxed_durability(mut self, enabled: bool) -> Self { + self.startup_config.relaxed_durability = enabled; + self + } + + /// Append an advanced PostgreSQL startup argument. Prefer + /// [`postgres_config`](Self::postgres_config) for GUCs. + pub fn startup_arg(mut self, arg: impl Into) -> Self { + self.startup_config.extra_args.push(arg.into()); + self + } + + /// Append advanced PostgreSQL startup arguments. + pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { + self.startup_config + .extra_args + .extend(args.into_iter().map(Into::into)); + self + } + + /// Load a previously dumped PGDATA tar archive before opening the database. + pub fn load_data_dir_archive(mut self, archive: impl Into>) -> Self { + self.load_data_dir_archive = Some(archive.into()); + self + } + + /// Enable a bundled Postgres extension before returning the database. + #[cfg(feature = "extensions")] + pub fn extension(mut self, extension: Extension) -> Self { + self.extensions.push(extension); + self + } + + /// Enable bundled Postgres extensions before returning the database. + #[cfg(feature = "extensions")] + pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { + self.extensions.extend(extensions); + self + } + + /// Install, initialize, and start the selected database. + pub fn open(self) -> Result { + self.postgres_config.validate()?; + self.startup_config.validate()?; + let target = match self.target.clone() { + Some(OliphauntTarget::Path(root)) => RootTarget::Path(root), + Some(OliphauntTarget::AppId { + qualifier, + organization, + application, + }) => RootTarget::AppId { + qualifier, + organization, + application, + }, + Some(OliphauntTarget::Temporary) => RootTarget::Temporary, + None => { + bail!( + "OliphauntBuilder target is not set; call path, app_id, or temporary before open" + ) + } + }; + let source = if let Some(archive) = self.load_data_dir_archive.clone() { + RootSource::DataDirArchive(archive) + } else if self.template_cache { + RootSource::Template + } else { + RootSource::FreshInitdb + }; + #[cfg(feature = "extensions")] + let extensions = resolve_extension_set(&self.extensions)?; + let plan = RootPlan::new(target, source); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), self.postgres_config.clone()); + let prepared = prepare_root(plan)?; + #[cfg(feature = "extensions")] + { + self.open_prepared_root(prepared, extensions) + } + #[cfg(not(feature = "extensions"))] + { + self.open_prepared_root(prepared) + } + } + + fn open_prepared_root( + self, + prepared: PreparedRoot, + #[cfg(feature = "extensions")] extensions: Vec, + ) -> Result { + let PreparedRoot { + temp_dir, + root_lock, + outcome, + .. + } = prepared; + #[cfg(feature = "extensions")] + let preinstalled_extensions = outcome.preinstalled_extensions.clone(); + let mut instance = Oliphaunt::new_prepared_with_config( + outcome, + self.postgres_config, + self.startup_config, + )?; + if let Some(lock) = root_lock { + instance.attach_root_lock(lock); + } + if let Some(temp_dir) = temp_dir { + instance.attach_temp_dir(temp_dir); + } + #[cfg(feature = "extensions")] + let mut instance = instance; + #[cfg(feature = "extensions")] + for extension in extensions { + if preinstalled_extensions + .iter() + .any(|sql_name| sql_name == extension.sql_name()) + { + instance.enable_preinstalled_extension(extension)?; + } else { + instance.enable_extension(extension)?; + } + } + Ok(instance) + } +} diff --git a/src/pglite/client.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs similarity index 84% rename from src/pglite/client.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs index 4c7b7cb1..1f573611 100644 --- a/src/pglite/client.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs @@ -7,45 +7,49 @@ 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::pglite::aot; +use crate::oliphaunt::aot; #[cfg(feature = "extensions")] -use crate::pglite::assets; -use crate::pglite::backend::{BackendOpenKind, BackendSession}; +use crate::oliphaunt::assets; +use crate::oliphaunt::backend::{BackendOpenKind, BackendSession}; #[cfg(feature = "extensions")] -use crate::pglite::base::install_bundled_extension_bytes; -use crate::pglite::base::{InstallOutcome, PglitePaths, RootLock}; -use crate::pglite::builder::PgliteBuilder; -use crate::pglite::config::{PostgresConfig, StartupConfig}; -use crate::pglite::data_dir::{DataDirArchiveFormat, dump_pgdata_archive}; -use crate::pglite::errors::PgliteError; +use crate::oliphaunt::base::install_bundled_extension_bytes; +use crate::oliphaunt::base::{InstallOutcome, OliphauntPaths, RootLock}; +use crate::oliphaunt::builder::OliphauntBuilder; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; +use crate::oliphaunt::data_dir::{DataDirArchiveFormat, dump_pgdata_archive}; +use crate::oliphaunt::engine::EngineCapabilities; +use crate::oliphaunt::errors::OliphauntError; #[cfg(feature = "extensions")] -use crate::pglite::extensions::{ +use crate::oliphaunt::extensions::{ Extension, by_sql_name, extension_session_setup_sql, extension_setup_sql, resolve_extension_set, }; -use crate::pglite::interface::{ +use crate::oliphaunt::interface::{ DataTransferContainer, DescribeQueryParam, DescribeQueryResult, DescribeResultField, - ExecProtocolOptions, ExecProtocolResult, ParserMap, QueryOptions, Results, SerializerMap, + ExecProtocolOptions, ExecProtocolResult, NoticeCallback, ParserMap, QueryOptions, Results, + SerializerMap, }; -use crate::pglite::parse::{parse_describe_statement_results, parse_results}; -#[cfg(feature = "extensions")] -use crate::pglite::pg_dump::{PgDumpOptions, PgDumpVirtualSocket, dump_direct_sql}; +use crate::oliphaunt::parse::{ + command_tag_row_count, parse_describe_statement_results, parse_results, +}; +#[cfg(feature = "tools")] +use crate::oliphaunt::pg_dump::{PgDumpOptions, PgDumpVirtualSocket, dump_direct_sql}; #[cfg(feature = "extensions")] -use crate::pglite::postgres_mod::PostgresMod; -use crate::pglite::timing; -use crate::pglite::types::{ +use crate::oliphaunt::postgres_mod::PostgresMod; +use crate::oliphaunt::timing; +use crate::oliphaunt::types::{ ArrayTypeInfo, DEFAULT_PARSERS, DEFAULT_SERIALIZERS, TEXT, register_array_type, }; -#[cfg(feature = "extensions")] -use crate::pglite::wire::{FrontendFrameKind, FrontendFrameReader, classify_frontend_message}; +#[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; use crate::protocol::serializer::{BindConfig, BindValue, PortalTarget, Serialize}; @@ -91,8 +95,13 @@ struct GlobalListener { callback: GlobalCallback, } +enum ExecTransportResult { + CommandOnly(Vec), + Raw(Vec), +} + /// Primary entry point for interacting with the embedded Postgres runtime. -pub struct Pglite { +pub struct Oliphaunt { backend: BackendSession, _temp_dir: Option, _root_lock: Option, @@ -111,23 +120,23 @@ pub struct Pglite { next_global_listener_id: u64, } -impl Pglite { - /// Create a builder for opening persistent or temporary PGlite databases. - pub fn builder() -> PgliteBuilder { - PgliteBuilder::new() +impl Oliphaunt { + /// Create a builder for opening persistent or temporary Oliphaunt databases. + pub fn builder() -> OliphauntBuilder { + OliphauntBuilder::new() } - /// Open a persistent PGlite database rooted at `root`, installing and initializing it if needed. + /// Open a persistent Oliphaunt database rooted at `root`, installing and initializing it if needed. pub fn open(root: impl AsRef) -> Result { Self::builder().path(root.as_ref().to_path_buf()).open() } - /// Open a persistent PGlite database under the platform data directory for `app_id`. + /// Open a persistent Oliphaunt database under the platform data directory for `app_id`. pub fn open_app(app_id: (&str, &str, &str)) -> Result { Self::builder().app_id(app_id).open() } - /// Create an ephemeral PGlite database whose files are removed when the instance is dropped. + /// Create an ephemeral Oliphaunt database whose files are removed when the instance is dropped. pub fn temporary() -> Result { Self::builder().temporary().open() } @@ -136,11 +145,11 @@ impl Pglite { pub fn preload() -> Result<()> { let (temp_dir, paths) = { let _phase = timing::phase("preload.tempdir"); - PglitePaths::with_temp_dir()? + OliphauntPaths::with_temp_dir()? }; { let _phase = timing::phase("preload.runtime_module"); - crate::pglite::base::preload_runtime_module(&paths)?; + crate::oliphaunt::base::preload_runtime_module(&paths)?; } { let _phase = timing::phase("preload.aot_runtime"); @@ -158,17 +167,17 @@ impl Pglite { for extension in resolve_extension_set(&extensions)? { let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { anyhow!( - "extension asset '{}' is not bundled in this pglite-oxide build", + "extension asset '{}' is not bundled in this oliphaunt-wasix build", extension.sql_name() ) })?; let (temp_dir, paths) = { let _phase = timing::phase("preload.extension_tempdir"); - PglitePaths::with_temp_dir()? + OliphauntPaths::with_temp_dir()? }; { let _phase = timing::phase("preload.extension_runtime_module"); - crate::pglite::base::preload_runtime_module(&paths)?; + crate::oliphaunt::base::preload_runtime_module(&paths)?; } { let _phase = timing::phase("preload.extension_archive_install"); @@ -187,12 +196,12 @@ impl Pglite { Ok(()) } - /// Create a new Pglite instance backed by the provided runtime paths. + /// Create a new Oliphaunt instance backed by the provided runtime paths. #[doc(hidden)] - pub fn new(paths: PglitePaths) -> Result { - let outcome = crate::pglite::base::prepare_database_root( + pub fn new(paths: OliphauntPaths) -> Result { + let outcome = crate::oliphaunt::base::prepare_database_root( paths, - crate::pglite::base::RootPrepareOptions::template(), + crate::oliphaunt::base::RootPrepareOptions::template(), )?; Self::new_prepared(outcome) } @@ -206,7 +215,7 @@ impl Pglite { postgres_config: PostgresConfig, startup_config: StartupConfig, ) -> Result { - let _phase = timing::phase("pglite.open"); + let _phase = timing::phase("oliphaunt.open"); let session_startup_config = startup_config.clone(); let backend = BackendSession::open( outcome, @@ -216,7 +225,7 @@ impl Pglite { )?; let mut instance = { - let _phase = timing::phase("pglite.client_struct_init"); + let _phase = timing::phase("oliphaunt.client_struct_init"); Self { backend, _temp_dir: None, @@ -240,7 +249,7 @@ impl Pglite { if session_startup_config.username != "postgres" { let sql = format!( "SET ROLE {}", - crate::pglite::templating::quote_identifier(&session_startup_config.username) + crate::oliphaunt::templating::quote_identifier(&session_startup_config.username) ); instance .exec(&sql, None) @@ -256,7 +265,7 @@ impl Pglite { let _phase = timing::phase("extension.enable"); let bytes = assets::extension_archive(extension.sql_name()).ok_or_else(|| { anyhow!( - "extension asset '{}' is not bundled in this pglite-oxide build", + "extension asset '{}' is not bundled in this oliphaunt-wasix build", extension.sql_name() ) })?; @@ -280,7 +289,7 @@ impl Pglite { /// Refresh direct API array parser and serializer registrations. /// - /// This mirrors upstream PGlite's `refreshArrayTypes()` escape hatch. Most + /// This mirrors upstream Oliphaunt's `refreshArrayTypes()` escape hatch. Most /// applications should not need it because built-in arrays are registered /// statically and runtime custom arrays are discovered lazily when possible. pub fn refresh_array_types(&mut self) -> Result<()> { @@ -368,7 +377,8 @@ impl Pglite { if let Err(err) = result { match err.downcast::() { Ok(db_err) => { - let enriched = PgliteError::new(db_err, sql, params_snapshot, options_snapshot); + let enriched = + OliphauntError::new(db_err, sql, params_snapshot, options_snapshot); return Err(enriched.into()); } Err(err) => { @@ -388,9 +398,14 @@ impl Pglite { self.ready && !self.closing && !self.closed } + /// Return the capabilities of the active embedded engine. + pub fn engine_capabilities(&self) -> EngineCapabilities { + self.backend.capabilities() + } + /// Return the host-side runtime and data-directory paths backing this instance. #[doc(hidden)] - pub fn paths(&self) -> &PglitePaths { + pub fn paths(&self) -> &OliphauntPaths { self.backend.paths() } @@ -403,8 +418,8 @@ impl Pglite { /// Dump the physical PGDATA directory to a gzipped tar archive. /// - /// The archive is intended to be loaded back into pglite-oxide/PGlite with - /// the same PostgreSQL/PGlite version. Use [`dump_sql`](Self::dump_sql) for + /// The archive is intended to be loaded back into oliphaunt-wasix/Oliphaunt with + /// the same PostgreSQL/Oliphaunt version. Use [`dump_sql`](Self::dump_sql) for /// logical backups across versions. pub fn dump_data_dir(&mut self) -> Result> { self.dump_data_dir_with_format(DataDirArchiveFormat::TarGz) @@ -416,7 +431,7 @@ impl Pglite { self.archive_quiesced_pgdata("dump PGDATA archive", format) } - /// Clone this database into a new temporary [`Pglite`] instance. + /// Clone this database into a new temporary [`Oliphaunt`] instance. pub fn try_clone(&mut self) -> Result { #[cfg(feature = "extensions")] let extensions = self.bundled_extensions_in_database()?; @@ -428,7 +443,7 @@ impl Pglite { } /// 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()?; @@ -437,7 +452,7 @@ impl Pglite { } /// 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()) } @@ -496,7 +511,7 @@ impl Pglite { if username != "postgres" { let sql = format!( "SET ROLE {}", - crate::pglite::templating::quote_identifier(&username) + crate::oliphaunt::templating::quote_identifier(&username) ); self.exec(&sql, None).with_context(|| { format!("restore startup role {username} after backend restart") @@ -510,14 +525,14 @@ impl Pglite { .map(|(channel, _)| channel.clone()) .collect::>(); for channel in channels { - let quoted_channel = crate::pglite::templating::quote_identifier(&channel); + let quoted_channel = crate::oliphaunt::templating::quote_identifier(&channel); self.exec_internal(&format!("LISTEN {quoted_channel}"), None) .with_context(|| format!("restore LISTEN {channel} after backend restart"))?; } 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)); @@ -533,14 +548,14 @@ impl Pglite { } } - #[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(); @@ -636,7 +651,7 @@ impl Pglite { return Ok(()); } if self.closing { - bail!("Pglite is closing"); + bail!("Oliphaunt is closing"); } self.closing = true; @@ -681,11 +696,22 @@ impl Pglite { let mut collected_messages: Vec = Vec::new(); let message = Serialize::query(sql); - let ExecProtocolResult { messages, .. } = match self.exec_protocol(&message, exec_opts) { - Ok(result) => result, + let transport_result = { + let _phase = timing::phase("client.protocol_transport_send"); + self.backend + .with_buffered(&message, exec_opts.data_transfer_container, |data| { + if let Some(affected_rows) = parse_command_only_result_counts(data) { + Ok(ExecTransportResult::CommandOnly(affected_rows)) + } else { + Ok(ExecTransportResult::Raw(data.to_vec())) + } + }) + }; + let transport_result = match transport_result { + Ok(data) => data, Err(err) => match err.downcast::() { Ok(db_err) => { - let enriched = PgliteError::new(db_err, sql, Vec::new(), options_snapshot); + let enriched = OliphauntError::new(db_err, sql, Vec::new(), options_snapshot); return Err(enriched.into()); } Err(err) => { @@ -693,9 +719,33 @@ impl Pglite { } }, }; + + let data = match transport_result { + ExecTransportResult::CommandOnly(affected_rows) => { + return self.finish_exec_command_only(affected_rows, options); + } + ExecTransportResult::Raw(data) => data, + }; + let ExecProtocolResult { messages, .. } = + match self.parse_protocol_data(data, exec_opts.throw_on_error, exec_opts.on_notice) { + Ok(result) => result, + Err(err) => match err.downcast::() { + Ok(db_err) => { + let enriched = + OliphauntError::new(db_err, sql, Vec::new(), options_snapshot); + return Err(enriched.into()); + } + Err(err) => { + return Err(err.context(format!("failed to execute simple query: {sql}"))); + } + }, + }; + let has_row_description = messages + .iter() + .any(|message| matches!(message, BackendMessage::RowDescription(_))); collected_messages.extend(messages); - self.finish_exec(collected_messages, options) + self.finish_exec(collected_messages, options, has_row_description) } /// Register a listener for `LISTEN channel`. Returns a handle that can be used to unlisten. @@ -705,7 +755,7 @@ impl Pglite { { self.check_ready()?; - let quoted_channel = crate::pglite::templating::quote_identifier(channel); + let quoted_channel = crate::oliphaunt::templating::quote_identifier(channel); let normalized = channel.to_string(); let should_listen = match self.notify_listeners.get(&normalized) { Some(existing) => existing.is_empty(), @@ -735,7 +785,8 @@ impl Pglite { listeners.retain(|listener| listener.id != handle.id); if listeners.is_empty() { self.notify_listeners.remove(&handle.normalized_channel); - let quoted_channel = crate::pglite::templating::quote_identifier(&handle.channel); + let quoted_channel = + crate::oliphaunt::templating::quote_identifier(&handle.channel); self.exec_internal(&format!("UNLISTEN {quoted_channel}"), None)?; } } @@ -744,7 +795,7 @@ impl Pglite { /// Remove all listeners for the specified channel. pub fn unlisten_channel(&mut self, channel: &str) -> Result<()> { - let quoted_channel = crate::pglite::templating::quote_identifier(channel); + let quoted_channel = crate::oliphaunt::templating::quote_identifier(channel); let normalized = channel.to_string(); if self.notify_listeners.remove(&normalized).is_some() { self.exec_internal(&format!("UNLISTEN {quoted_channel}"), None)?; @@ -816,7 +867,7 @@ impl Pglite { if let Err(err) = result { match err.downcast::() { Ok(db_err) => { - let enriched = PgliteError::new(db_err, sql, Vec::new(), options_snapshot); + let enriched = OliphauntError::new(db_err, sql, Vec::new(), options_snapshot); return Err(enriched.into()); } Err(err) => { @@ -1026,6 +1077,7 @@ impl Pglite { &mut self, messages: Vec, options: Option<&QueryOptions>, + has_row_description: bool, ) -> Result> { let blob = { let _phase = timing::phase("client.finish.blob_read"); @@ -1039,7 +1091,7 @@ impl Pglite { let _phase = timing::phase("client.finish.sync_to_fs"); self.sync_to_fs()?; } - { + if has_row_description { let _phase = timing::phase("client.finish.ensure_array_types"); self.ensure_array_types_for_result_messages(&messages, options)?; } @@ -1050,6 +1102,45 @@ impl Pglite { Ok(parsed) } + fn finish_exec_command_only( + &mut self, + affected_rows: Vec, + options: Option<&QueryOptions>, + ) -> Result> { + let blob = { + let _phase = timing::phase("client.finish.blob_read"); + self.get_written_blob()? + }; + { + let _phase = timing::phase("client.finish.blob_cleanup"); + self.cleanup_blob()?; + } + if !self.in_transaction { + let _phase = timing::phase("client.finish.sync_to_fs"); + self.sync_to_fs()?; + } + + let _ = options; + let mut results = Vec::with_capacity(affected_rows.len().max(1)); + for count in affected_rows { + results.push(Results { + rows: Vec::new(), + fields: Vec::new(), + affected_rows: Some(count), + blob: blob.clone(), + }); + } + if results.is_empty() { + results.push(Results { + rows: Vec::new(), + fields: Vec::new(), + affected_rows: Some(0), + blob, + }); + } + Ok(results) + } + /// Execute raw PostgreSQL frontend protocol bytes and parse backend /// protocol messages. pub fn exec_protocol( @@ -1068,7 +1159,15 @@ impl Pglite { let _phase = timing::phase("client.protocol_roundtrip"); self.exec_protocol_raw_inner(message, sync_to_fs, data_transfer_container)? }; + self.parse_protocol_data(data, throw_on_error, on_notice) + } + fn parse_protocol_data( + &mut self, + data: Vec, + throw_on_error: bool, + on_notice: Option, + ) -> Result { let mut messages = Vec::new(); let on_notice_cb = on_notice.clone(); let parse_result = { @@ -1228,7 +1327,7 @@ impl Pglite { ORDER BY e.oid "; let results = { - let _phase = timing::phase("pglite.array_type_catalog_query"); + let _phase = timing::phase("oliphaunt.array_type_catalog_query"); self.exec_internal(sql, None)? }; let result_set = results @@ -1237,7 +1336,7 @@ impl Pglite { .ok_or_else(|| anyhow!("array type discovery returned no results"))?; { - let _phase = timing::phase("pglite.array_type_register"); + let _phase = timing::phase("oliphaunt.array_type_register"); for row in result_set.rows { if let Some(info) = array_type_info_from_row(&row) { self.register_array_type(info); @@ -1264,7 +1363,7 @@ impl Pglite { AND a.typelem <> 0" ); let results = { - let _phase = timing::phase("pglite.array_type_targeted_lookup"); + let _phase = timing::phase("oliphaunt.array_type_targeted_lookup"); self.exec_internal(&sql, None)? }; let Some(result_set) = results.into_iter().next() else { @@ -1351,19 +1450,19 @@ impl Pglite { fn check_ready(&self) -> Result<()> { if self.closing { - bail!("Pglite instance is closing"); + bail!("Oliphaunt instance is closing"); } if self.closed { - bail!("Pglite instance is closed"); + bail!("Oliphaunt instance is closed"); } if !self.ready { - bail!("Pglite instance is not ready"); + bail!("Oliphaunt instance is not ready"); } Ok(()) } } -impl Drop for Pglite { +impl Drop for Oliphaunt { fn drop(&mut self) { if !self.closed { let _ = self.close(); @@ -1371,7 +1470,7 @@ impl Drop for Pglite { } } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn ensure_direct_pg_dump_options_match_session( startup_config: &StartupConfig, options: &PgDumpOptions, @@ -1393,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, @@ -1419,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), @@ -1430,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), @@ -1440,6 +1539,43 @@ fn flush_direct_pg_dump_socket( .context("flush direct pg_dump virtual socket") } +fn parse_command_only_result_counts(data: &[u8]) -> Option> { + let mut offset = 0usize; + let mut affected_total = 0usize; + let mut affected_rows = Vec::new(); + while offset + 5 <= data.len() { + let tag = data[offset]; + let length = u32::from_be_bytes([ + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + ]) as usize; + if length < 4 { + return None; + } + let frame_len = 1 + length; + if frame_len > data.len() - offset { + return None; + } + let body_start = offset + 5; + let body_end = offset + frame_len; + match tag { + b'C' => { + let command_tag = data[body_start..body_end] + .strip_suffix(&[0]) + .unwrap_or(&data[body_start..body_end]); + affected_total = affected_total.saturating_add(command_tag_row_count(command_tag)); + affected_rows.push(affected_total); + } + b'Z' => {} + _ => return None, + } + offset += frame_len; + } + (offset == data.len()).then_some(affected_rows) +} + fn value_to_i32(value: Option<&Value>) -> Option { match value? { Value::Number(number) => number.as_i64().map(|value| value as i32), @@ -1468,14 +1604,14 @@ fn array_type_info_from_row(row: &Value) -> Option { Some(ArrayTypeInfo::new(element_oid, array_oid, delimiter)) } -/// Transaction handle used within [`Pglite::transaction`]. +/// Transaction handle used within [`Oliphaunt::transaction`]. pub struct Transaction<'a> { - client: &'a mut Pglite, + client: &'a mut Oliphaunt, closed: bool, } impl<'a> Transaction<'a> { - fn new(client: &'a mut Pglite) -> Self { + fn new(client: &'a mut Oliphaunt) -> Self { Self { client, closed: false, diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/config.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/config.rs new file mode 100644 index 00000000..130422e0 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/config.rs @@ -0,0 +1,157 @@ +use std::collections::BTreeMap; + +use anyhow::{Result, bail, ensure}; + +use crate::oliphaunt::interface::DebugLevel; + +/// PostgreSQL startup configuration applied through normal `postgres -c` GUC +/// handling before the embedded backend starts. +/// +/// Settings added here override `oliphaunt-wasix`'s default startup profile because +/// they are appended after the defaults in the generated PostgreSQL argv. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct PostgresConfig { + settings: BTreeMap, +} + +impl PostgresConfig { + /// Create an empty startup configuration. + pub fn new() -> Self { + Self::default() + } + + /// Set or replace one PostgreSQL GUC. + pub fn set(mut self, name: impl Into, value: impl Into) -> Self { + self.settings.insert(name.into(), value.into()); + self + } + + pub(crate) fn insert(&mut self, name: impl Into, value: impl Into) { + self.settings.insert(name.into(), value.into()); + } + + pub(crate) fn validate(&self) -> Result<()> { + for (name, value) in &self.settings { + validate_guc_name(name)?; + ensure!( + !value.contains('\0'), + "Postgres config value for '{name}' must not contain NUL bytes" + ); + } + Ok(()) + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.settings + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + } + + #[cfg(feature = "extensions")] + pub(crate) fn stable_entries(&self) -> Vec<(String, String)> { + self.settings + .iter() + .map(|(name, value)| (name.clone(), value.clone())) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct StartupConfig { + pub(crate) username: String, + pub(crate) database: String, + pub(crate) debug_level: Option, + pub(crate) relaxed_durability: bool, + pub(crate) extra_args: Vec, +} + +impl Default for StartupConfig { + fn default() -> Self { + Self { + username: "postgres".to_owned(), + database: "template1".to_owned(), + debug_level: None, + relaxed_durability: false, + extra_args: Vec::new(), + } + } +} + +impl StartupConfig { + pub(crate) fn validate(&self) -> Result<()> { + validate_startup_value("username", &self.username)?; + validate_startup_value("database", &self.database)?; + if let Some(level) = self.debug_level { + ensure!( + level <= 5, + "Postgres debug level must be between 0 and 5, got {level}" + ); + } + for arg in &self.extra_args { + ensure!( + !arg.contains('\0'), + "Postgres startup argument must not contain NUL bytes" + ); + } + Ok(()) + } +} + +fn validate_guc_name(name: &str) -> Result<()> { + ensure!(!name.is_empty(), "Postgres config name must not be empty"); + ensure!( + !name.contains('\0') && !name.contains('='), + "Postgres config name '{name}' must not contain NUL bytes or '='" + ); + + for part in name.split('.') { + if part.is_empty() { + bail!("Postgres config name '{name}' contains an empty identifier part"); + } + let mut chars = part.chars(); + let first = chars.next().expect("part is non-empty"); + if !(first == '_' || first.is_ascii_alphabetic()) { + bail!("Postgres config name '{name}' must start each identifier with a letter or '_'"); + } + if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) { + bail!("Postgres config name '{name}' may only contain letters, digits, '_', and '.'"); + } + } + + Ok(()) +} + +fn validate_startup_value(name: &str, value: &str) -> Result<()> { + ensure!( + !value.is_empty(), + "Postgres startup {name} must not be empty" + ); + ensure!( + !value.contains('\0'), + "Postgres startup {name} must not contain NUL bytes" + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::PostgresConfig; + + #[test] + fn validates_builtin_and_extension_guc_names() { + PostgresConfig::new() + .set("synchronous_commit", "off") + .set("pg_stat_statements.track", "all") + .validate() + .unwrap(); + } + + #[test] + fn rejects_invalid_guc_names_before_startup() { + let err = PostgresConfig::new() + .set("bad=name", "off") + .validate() + .expect_err("invalid GUC name should be rejected"); + assert!(err.to_string().contains("must not contain")); + } +} diff --git a/src/pglite/data_dir.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/data_dir.rs similarity index 99% rename from src/pglite/data_dir.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/data_dir.rs index 5376c0e4..f4c2ae3e 100644 --- a/src/pglite/data_dir.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/data_dir.rs @@ -9,7 +9,7 @@ use flate2::read::GzDecoder; use flate2::write::GzEncoder; use tar::{Archive, Builder, EntryType, Header}; -const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".pglite-oxide-pgdata-overlay.json"; +const PGDATA_OVERLAY_MANIFEST_NAME: &str = ".oliphaunt-wasix-pgdata-overlay.json"; const RUNTIME_STATE_FILES: &[&str] = &["postmaster.pid", "postmaster.opts"]; const OVERLAY_WHITEOUT_PREFIX: &str = ".wh."; diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/engine.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/engine.rs new file mode 100644 index 00000000..e19fae04 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/engine.rs @@ -0,0 +1,25 @@ +/// Capabilities advertised by the packaged WASIX runtime. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EngineCapabilities { + pub engine_name: &'static str, + pub version: String, + pub multi_instance: bool, + pub protocol_raw: bool, + pub protocol_stream: bool, + pub server_mode: bool, + pub extensions: bool, +} + +impl EngineCapabilities { + pub(crate) fn wasix_legacy(protocol_stream: bool) -> Self { + Self { + engine_name: "wasix-legacy", + version: crate::oliphaunt::aot::engine_identity().to_owned(), + multi_instance: true, + protocol_raw: true, + protocol_stream, + server_mode: true, + extensions: cfg!(feature = "extensions"), + } + } +} diff --git a/src/pglite/errors.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/errors.rs similarity index 80% rename from src/pglite/errors.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/errors.rs index 13e61232..3bcf0387 100644 --- a/src/pglite/errors.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/errors.rs @@ -3,19 +3,19 @@ use std::fmt; use serde_json::Value; -use crate::pglite::interface::QueryOptions; +use crate::oliphaunt::interface::QueryOptions; use crate::protocol::messages::DatabaseError; -/// Rich error type that mirrors the TypeScript `PGliteError` by carrying the +/// Rich error type that mirrors the TypeScript `OliphauntError` by carrying the /// original database error along with query context. -pub struct PgliteError { +pub struct OliphauntError { source: DatabaseError, query: String, params: Vec, query_options: Option, } -impl PgliteError { +impl OliphauntError { pub fn new( source: DatabaseError, query: impl Into, @@ -47,15 +47,15 @@ impl PgliteError { } } -impl fmt::Display for PgliteError { +impl fmt::Display for OliphauntError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.source) } } -impl fmt::Debug for PgliteError { +impl fmt::Debug for OliphauntError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PgliteError") + f.debug_struct("OliphauntError") .field("source", &self.source) .field("query", &self.query) .field("params", &self.params) @@ -64,7 +64,7 @@ impl fmt::Debug for PgliteError { } } -impl Error for PgliteError { +impl Error for OliphauntError { fn source(&self) -> Option<&(dyn Error + 'static)> { Some(&self.source) } 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 new file mode 100644 index 00000000..ccc2dbac --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/extensions.rs @@ -0,0 +1,870 @@ +use std::collections::BTreeSet; + +use anyhow::{Result, bail}; + +#[path = "generated_extensions.rs"] +mod generated; + +pub use generated::*; + +/// A native WASIX side module required by a bundled extension. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ExtensionNativeModule { + runtime_path: &'static str, + aot_name: Option<&'static str>, +} + +impl ExtensionNativeModule { + pub(crate) const fn new(runtime_path: &'static str, aot_name: Option<&'static str>) -> Self { + Self { + runtime_path, + aot_name, + } + } + + pub const fn runtime_path(self) -> &'static str { + self.runtime_path + } + + pub const fn aot_name(self) -> Option<&'static str> { + self.aot_name + } +} + +/// A bundled Postgres extension that can be installed into a Oliphaunt database. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Extension { + name: &'static str, + sql_name: &'static str, + archive_name: &'static str, + native_support_modules: &'static [ExtensionNativeModule], + native_module_file: Option<&'static str>, + aot_name: Option<&'static str>, + dependencies: &'static [&'static str], + setup: ExtensionSetup, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ExtensionSetup { + create_extension: bool, + create_schema: Option<&'static str>, + load_sql: &'static [&'static str], + post_create_sql: &'static [&'static str], +} + +impl ExtensionSetup { + pub(crate) const fn new( + create_extension: bool, + create_schema: Option<&'static str>, + load_sql: &'static [&'static str], + post_create_sql: &'static [&'static str], + ) -> Self { + Self { + create_extension, + create_schema, + load_sql, + post_create_sql, + } + } +} + +impl Extension { + #[allow(dead_code)] + pub(crate) const fn new( + name: &'static str, + sql_name: &'static str, + archive_name: &'static str, + native_support_modules: &'static [ExtensionNativeModule], + native_module_file: Option<&'static str>, + aot_name: Option<&'static str>, + dependencies: &'static [&'static str], + setup: ExtensionSetup, + ) -> Self { + Self { + name, + sql_name, + archive_name, + native_support_modules, + native_module_file, + aot_name, + dependencies, + setup, + } + } + + /// Human-facing extension name. + pub const fn name(self) -> &'static str { + self.name + } + + /// SQL extension name used in `CREATE EXTENSION`. + pub const fn sql_name(self) -> &'static str { + self.sql_name + } + + /// Archive path inside the asset manifest. + pub const fn archive_name(self) -> &'static str { + self.archive_name + } + + /// AOT artifact key for the extension side module. + pub const fn aot_name(self) -> Option<&'static str> { + self.aot_name + } + + /// Native side-module file installed into `/lib/postgresql`, when the + /// extension has one. + pub const fn native_module_file(self) -> Option<&'static str> { + self.native_module_file + } + + /// Support side modules that must be available before the extension module + /// is loaded. + pub const fn native_support_modules(self) -> &'static [ExtensionNativeModule] { + self.native_support_modules + } + + /// SQL extension names that must be installed before this extension. + pub const fn dependencies(self) -> &'static [&'static str] { + self.dependencies + } + + pub(crate) const fn setup(self) -> ExtensionSetup { + self.setup + } +} + +pub fn by_sql_name(sql_name: &str) -> Option { + ALL.iter() + .copied() + .find(|extension| extension.sql_name == sql_name) +} + +pub(crate) fn candidate_by_sql_name(sql_name: &str) -> Option { + generated::CANDIDATES + .iter() + .copied() + .find(|extension| extension.sql_name == sql_name) +} + +pub(crate) fn resolve_extension_set(extensions: &[Extension]) -> Result> { + let mut visiting = BTreeSet::new(); + let mut visited = BTreeSet::new(); + let mut resolved = Vec::new(); + let mut requested = extensions.to_vec(); + requested.sort_by_key(|extension| extension.sql_name()); + for extension in requested { + visit_extension(extension, &mut visiting, &mut visited, &mut resolved)?; + } + Ok(resolved) +} + +fn visit_extension( + extension: Extension, + visiting: &mut BTreeSet<&'static str>, + visited: &mut BTreeSet<&'static str>, + resolved: &mut Vec, +) -> Result<()> { + if visited.contains(extension.sql_name()) { + return Ok(()); + } + if !visiting.insert(extension.sql_name()) { + bail!( + "cyclic bundled extension dependency involving '{}'", + extension.sql_name() + ); + } + for dependency in extension.dependencies() { + let dependency_extension = candidate_by_sql_name(dependency).ok_or_else(|| { + anyhow::anyhow!( + "selected extension '{}' depends on missing catalog extension '{}'", + extension.sql_name(), + dependency + ) + })?; + visit_extension(dependency_extension, visiting, visited, resolved)?; + } + visiting.remove(extension.sql_name()); + visited.insert(extension.sql_name()); + resolved.push(extension); + Ok(()) +} + +pub(crate) fn extension_setup_sql(extension: Extension) -> Vec { + extension_setup_sql_with_schema_policy(extension) +} + +fn extension_setup_sql_with_schema_policy(extension: Extension) -> Vec { + let setup = extension.setup(); + let mut statements = Vec::new(); + if setup.create_extension { + let create_schema = setup.create_schema; + if let Some(schema) = create_schema.filter(|schema| *schema != "pg_catalog") { + statements.push(format!( + "CREATE SCHEMA IF NOT EXISTS {};", + crate::oliphaunt::templating::quote_identifier(schema) + )); + } + let mut sql = format!( + "CREATE EXTENSION IF NOT EXISTS {}", + crate::oliphaunt::templating::quote_identifier(extension.sql_name()) + ); + if let Some(schema) = create_schema { + sql.push_str(" WITH SCHEMA "); + sql.push_str(&crate::oliphaunt::templating::quote_identifier(schema)); + } + sql.push(';'); + statements.push(sql); + } + statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); + statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); + statements +} + +pub(crate) fn extension_session_setup_sql(extension: Extension) -> Vec { + let setup = extension.setup(); + let mut statements = Vec::new(); + statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); + statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); + statements +} + +#[cfg(all(test, feature = "extensions"))] +mod candidate_tests { + use super::*; + #[cfg(feature = "tools")] + use crate::PgDumpOptions; + use crate::{Oliphaunt, OliphauntServer}; + use anyhow::{Context, Result, ensure}; + use sqlx::{Connection, PgConnection}; + use std::collections::BTreeSet; + use std::path::{Path, PathBuf}; + + #[test] + fn public_extensions_pass_direct_and_restart_smoke() -> Result<()> { + run_direct_and_restart_smoke_set(generated::ALL) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn public_extensions_pass_server_smoke() -> Result<()> { + run_server_smoke_set(generated::ALL).await + } + + #[test] + fn public_extensions_materialize_only_requested_libraries() -> Result<()> { + run_lifecycle_materialization_set(generated::ALL) + } + + #[test] + #[cfg(feature = "tools")] + fn public_extensions_pass_direct_dump_restore_smoke() -> Result<()> { + run_direct_dump_restore_smoke_set(generated::ALL) + } + + #[test] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + fn packaged_candidate_extensions_pass_direct_and_restart_smoke() -> Result<()> { + run_direct_and_restart_smoke_set(generated::CANDIDATES) + } + + #[test] + fn uuid_ossp_candidate_passes_direct_and_restart_smoke() -> Result<()> { + run_direct_and_restart_smoke_set(&[generated::CANDIDATE_UUID_OSSP]) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + async fn packaged_candidate_extensions_pass_server_smoke() -> Result<()> { + run_server_smoke_set(generated::CANDIDATES).await + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn uuid_ossp_candidate_passes_server_smoke() -> Result<()> { + run_server_smoke_set(&[generated::CANDIDATE_UUID_OSSP]).await + } + + #[test] + #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + fn packaged_candidate_extensions_materialize_only_requested_libraries() -> Result<()> { + run_lifecycle_materialization_set(generated::CANDIDATES) + } + + #[test] + fn uuid_ossp_candidate_materializes_only_requested_libraries() -> Result<()> { + run_lifecycle_materialization_set(&[generated::CANDIDATE_UUID_OSSP]) + } + + #[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]) + } + + fn embedded_extension_archives(extensions: &[Extension]) -> Result> { + let embedded: Vec<_> = extensions + .iter() + .copied() + .filter(|extension| { + crate::oliphaunt::assets::extension_archive(extension.sql_name()).is_some() + }) + .collect(); + let embedded_names: BTreeSet<_> = embedded + .iter() + .map(|extension| extension.sql_name()) + .collect(); + let missing: Vec<_> = extensions + .iter() + .map(|extension| extension.sql_name()) + .filter(|name| !embedded_names.contains(name)) + .collect(); + ensure!( + missing.is_empty(), + "required WASIX extension archives are not embedded: {}", + missing.join(", ") + ); + Ok(embedded) + } + + fn run_direct_and_restart_smoke_set(extensions: &[Extension]) -> Result<()> { + let extensions = embedded_extension_archives(extensions)?; + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_direct_and_restart_smoke(extension) { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension direct/restart smoke failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + fn run_one_direct_and_restart_smoke(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + { + let mut db = Oliphaunt::builder() + .temporary() + .extension(extension) + .open() + .with_context(|| format!("open temporary database with extension {name}"))?; + run_direct_smoke(&mut db, extension)?; + db.close() + .with_context(|| format!("close temporary database with extension {name}"))?; + } + + let root = tempfile::TempDir::new() + .with_context(|| format!("create restart root for extension {name}"))?; + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| { + format!("open persistent database with extension {name} before restart") + })?; + run_direct_smoke(&mut db, extension)?; + assert_extension_catalog_state(&mut db, extension)?; + db.close() + .with_context(|| format!("close persistent database with extension {name}"))?; + } + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| { + format!("reopen persistent database with extension {name} after restart") + })?; + assert_extension_catalog_state(&mut db, extension)?; + db.close() + .with_context(|| format!("close restarted database with extension {name}"))?; + } + Ok(()) + } + + async fn run_server_smoke_set(extensions: &[Extension]) -> Result<()> { + let extensions = embedded_extension_archives(extensions)?; + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_server_smoke(extension).await { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension server smoke failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + async fn run_one_server_smoke(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + let server = OliphauntServer::builder() + .temporary() + .extension(extension) + .start() + .with_context(|| format!("start server with extension {name}"))?; + let mut conn = PgConnection::connect(&server.database_url()) + .await + .with_context(|| format!("connect server with extension {name}"))?; + run_server_smoke(&mut conn, extension).await?; + drop(conn); + server + .shutdown() + .with_context(|| format!("shutdown server with extension {name}"))?; + Ok(()) + } + + fn run_lifecycle_materialization_set(extensions: &[Extension]) -> Result<()> { + let extensions = embedded_extension_archives(extensions)?; + let mut failures = Vec::new(); + for extension in extensions { + if let Err(error) = run_one_lifecycle_materialization(extension) { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension lifecycle/materialization failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + fn run_one_lifecycle_materialization(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + let root = tempfile::TempDir::new() + .with_context(|| format!("create lifecycle root for extension {name}"))?; + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extension) + .open() + .with_context(|| format!("open lifecycle database with extension {name}"))?; + db.close() + .with_context(|| format!("close lifecycle database with extension {name}"))?; + } + 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(); + for extension in extensions { + if let Err(error) = run_one_direct_dump_restore_smoke(extension) { + failures.push(format!("{}: {error:?}", extension.sql_name())); + } + } + ensure!( + failures.is_empty(), + "extension direct dump/restore smoke failures:\n{}", + failures.join("\n\n") + ); + Ok(()) + } + + #[cfg(feature = "tools")] + fn run_one_direct_dump_restore_smoke(extension: Extension) -> Result<()> { + let name = extension.sql_name(); + let dump = { + let mut db = Oliphaunt::builder() + .temporary() + .extension(extension) + .open() + .with_context(|| format!("open dump source database with extension {name}"))?; + assert_extension_catalog_state(&mut db, extension)?; + db.exec( + "CREATE TABLE oxide_extension_dump_marker(value text); + INSERT INTO oxide_extension_dump_marker VALUES ('restored');", + None, + ) + .with_context(|| format!("seed dump source database with extension {name}"))?; + let dump = db + .dump_sql(PgDumpOptions::new()) + .with_context(|| format!("dump source database with extension {name}"))?; + db.close() + .with_context(|| format!("close dump source database with extension {name}"))?; + dump + }; + + if extension.setup().create_extension { + let unquoted_needle = + format!("CREATE EXTENSION IF NOT EXISTS {}", extension.sql_name()); + let quoted_needle = format!( + "CREATE EXTENSION IF NOT EXISTS {}", + crate::oliphaunt::templating::quote_identifier(extension.sql_name()) + ); + ensure!( + dump.contains(&unquoted_needle) || dump.contains("ed_needle), + "pg_dump for extension {} should contain {:?} or {:?}; dump was:\n{}", + extension.sql_name(), + unquoted_needle, + quoted_needle, + dump + ); + } + + let mut restored = Oliphaunt::builder() + .temporary() + .extension(extension) + .open() + .with_context(|| format!("open dump restore database with extension {name}"))?; + restored + .exec(&dump, None) + .with_context(|| format!("restore dump SQL with extension {name}"))?; + restored + .exec("SET search_path TO public, pg_catalog", None) + .with_context(|| { + format!("reset restore session search_path after pg_dump SQL for extension {name}") + })?; + assert_extension_catalog_state(&mut restored, extension)?; + let marker = restored.query( + "SELECT value FROM public.oxide_extension_dump_marker", + &[], + None, + )?; + ensure!( + marker.rows[0]["value"] == serde_json::json!("restored"), + "extension {} dump marker did not restore", + extension.sql_name() + ); + run_direct_smoke(&mut restored, extension)?; + restored + .close() + .with_context(|| format!("close dump restore database with extension {name}"))?; + Ok(()) + } + + fn run_direct_smoke(db: &mut Oliphaunt, extension: Extension) -> Result<()> { + for statement in smoke_sql(extension.sql_name()).statements() { + db.exec(statement, None).with_context(|| { + format!( + "direct smoke failed for extension {} while running:\n{}", + extension.sql_name(), + statement + ) + })?; + } + Ok(()) + } + + async fn run_server_smoke(conn: &mut PgConnection, extension: Extension) -> Result<()> { + for statement in smoke_sql(extension.sql_name()).statements() { + sqlx::query(statement) + .fetch_all(&mut *conn) + .await + .with_context(|| { + format!( + "server smoke failed for extension {} while running:\n{}", + extension.sql_name(), + statement + ) + })?; + } + Ok(()) + } + + fn assert_extension_catalog_state(db: &mut Oliphaunt, extension: Extension) -> Result<()> { + if extension.setup().create_extension { + let result = db.query( + "SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = $1", + &[serde_json::json!(extension.sql_name())], + None, + )?; + ensure!( + result.rows[0]["count"] == serde_json::json!(1), + "extension {} should survive restart in pg_extension", + extension.sql_name() + ); + } else { + let result = db.query("SELECT 1::int4 AS ok", &[], None)?; + ensure!( + result.rows[0]["ok"] == serde_json::json!(1), + "extension {} should reopen cleanly", + extension.sql_name() + ); + } + Ok(()) + } + + fn assert_only_resolved_extension_libraries_are_materialized( + root: &Path, + extension: Extension, + ) -> Result<()> { + let expected = resolve_extension_set(&[extension])? + .into_iter() + .flat_map(|extension| { + let mut modules = extension + .native_support_modules() + .iter() + .map(|module| { + PathBuf::from(module.runtime_path()) + .strip_prefix("lib/postgresql") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(module.runtime_path())) + }) + .collect::>(); + if let Some(module) = extension.native_module_file() { + modules.push(PathBuf::from(module)); + } + modules + }) + .collect::>(); + let actual = relative_files(&root.join("tmp/oliphaunt/lib/postgresql")) + .into_iter() + .collect::>(); + ensure!( + actual == expected, + "upper runtime library layer for {} should contain only resolved requested libraries; expected {:?}, got {:?}", + extension.sql_name(), + expected, + actual + ); + Ok(()) + } + + fn relative_files(root: &Path) -> Vec { + fn walk(base: &Path, current: &Path, files: &mut Vec) { + let Ok(entries) = std::fs::read_dir(current) else { + return; + }; + for entry in entries { + let entry = entry.expect("read runtime test directory entry"); + let path = entry.path(); + if path.is_dir() { + walk(base, &path, files); + } else if path.is_file() { + files.push( + path.strip_prefix(base) + .expect("relative extension library path") + .to_path_buf(), + ); + } + } + } + + let mut files = Vec::new(); + walk(root, root, &mut files); + files.sort(); + files + } + + const POSTGIS_SMOKE_SQL: &str = + include_str!("../../../../../../extensions/external/postgis/tests/smoke.sql"); + + enum SmokeSql { + Inline(&'static [&'static str]), + Recipe(&'static str), + } + + impl SmokeSql { + fn statements(&self) -> Vec<&'static str> { + match self { + Self::Inline(statements) => statements.to_vec(), + Self::Recipe(sql) => sql + .split("-- oliphaunt-statement") + .map(str::trim) + .filter(|statement| !statement.is_empty()) + .collect(), + } + } + } + + fn smoke_sql(sql_name: &str) -> SmokeSql { + if sql_name == "postgis" { + return SmokeSql::Recipe(POSTGIS_SMOKE_SQL); + } + SmokeSql::Inline(inline_smoke_sql(sql_name)) + } + + fn inline_smoke_sql(sql_name: &str) -> &'static [&'static str] { + // These are compact Rust ports of the Oliphaunt extension smoke tests in + // src/extensions tests. + match sql_name { + "age" => &[ + "SELECT ag_catalog.create_graph('oxide_graph')", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM ag_catalog.ag_graph WHERE name = 'oxide_graph') THEN RAISE EXCEPTION 'age graph was not created'; END IF; END $$", + "SELECT * FROM ag_catalog.cypher('oxide_graph', $$ RETURN 1 $$) AS (one agtype)", + ], + "amcheck" => &[ + "CREATE TEMP TABLE oxide_amcheck (id int PRIMARY KEY, value text)", + "INSERT INTO oxide_amcheck SELECT i, 'v' || i::text FROM generate_series(1, 8) AS i", + "SELECT bt_index_check('oxide_amcheck_pkey'::regclass)", + ], + "auto_explain" => &["EXPLAIN SELECT count(*) FROM pg_class"], + "bloom" => &[ + "CREATE TEMP TABLE oxide_bloom (id int, value int)", + "CREATE INDEX oxide_bloom_idx ON oxide_bloom USING bloom (id, value)", + "INSERT INTO oxide_bloom SELECT i, i % 3 FROM generate_series(1, 20) AS i", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_bloom WHERE id = 7 AND value = 1; IF n <> 1 THEN RAISE EXCEPTION 'bloom lookup failed: %', n; END IF; END $$", + ], + "btree_gin" => &[ + "CREATE TEMP TABLE oxide_btree_gin (id int)", + "CREATE INDEX oxide_btree_gin_idx ON oxide_btree_gin USING gin (id)", + "INSERT INTO oxide_btree_gin SELECT generate_series(1, 10)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gin WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gin lookup failed: %', n; END IF; END $$", + ], + "btree_gist" => &[ + "CREATE TEMP TABLE oxide_btree_gist (id int)", + "CREATE INDEX oxide_btree_gist_idx ON oxide_btree_gist USING gist (id)", + "INSERT INTO oxide_btree_gist SELECT generate_series(1, 10)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gist WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gist lookup failed: %', n; END IF; END $$", + ], + "citext" => &[ + "CREATE TEMP TABLE oxide_citext (value citext)", + "INSERT INTO oxide_citext VALUES ('Postgres')", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_citext WHERE value = 'postgres'; IF n <> 1 THEN RAISE EXCEPTION 'citext comparison failed: %', n; END IF; END $$", + ], + "cube" => &[ + "DO $$ DECLARE d float8; BEGIN SELECT cube(array[1,2,3]) <-> cube(array[1,2,4]) INTO d; IF d <> 1 THEN RAISE EXCEPTION 'cube distance failed: %', d; END IF; END $$", + ], + "dict_int" => &[ + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('intdict', '40865854'), ',') INTO lex; IF lex <> '408658' THEN RAISE EXCEPTION 'dict_int lexize failed: %', lex; END IF; END $$", + ], + "dict_xsyn" => &[ + "ALTER TEXT SEARCH DICTIONARY xsyn (RULES = 'xsyn_sample', KEEPORIG = true, MATCHORIG = true, KEEPSYNONYMS = true, MATCHSYNONYMS = false)", + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('xsyn', 'supernova'), ',') INTO lex; IF lex IS NULL OR lex !~ 'sn' THEN RAISE EXCEPTION 'dict_xsyn lexize failed: %', lex; END IF; END $$", + ], + "earthdistance" => &[ + "DO $$ DECLARE d float8; BEGIN SELECT earth_distance(ll_to_earth(0, 0), ll_to_earth(0, 1)) INTO d; IF d <= 0 THEN RAISE EXCEPTION 'earthdistance failed: %', d; END IF; END $$", + ], + "file_fdw" => &[ + "CREATE SERVER oxide_file_server FOREIGN DATA WRAPPER file_fdw", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_foreign_data_wrapper WHERE fdwname = 'file_fdw') THEN RAISE EXCEPTION 'file_fdw wrapper missing'; END IF; END $$", + ], + "fuzzystrmatch" => &[ + "DO $$ BEGIN IF levenshtein('kitten', 'sitting') <> 3 THEN RAISE EXCEPTION 'levenshtein failed'; END IF; IF soundex('kitten') <> 'K350' THEN RAISE EXCEPTION 'soundex failed'; END IF; END $$", + ], + "hstore" => &[ + "CREATE TEMP TABLE oxide_hstore (attrs hstore)", + "INSERT INTO oxide_hstore VALUES ('a=>1,b=>2'::hstore)", + "DO $$ DECLARE v text; BEGIN SELECT attrs -> 'b' INTO v FROM oxide_hstore; IF v <> '2' THEN RAISE EXCEPTION 'hstore lookup failed: %', v; END IF; END $$", + ], + "intarray" => &[ + "CREATE TEMP TABLE oxide_intarray (tags int[])", + "INSERT INTO oxide_intarray VALUES (ARRAY[1, 2, 5]), (ARRAY[3, 4])", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_intarray WHERE tags && ARRAY[2, 9]; IF n <> 1 THEN RAISE EXCEPTION 'intarray overlap failed: %', n; END IF; SELECT count(*) INTO n FROM oxide_intarray WHERE tags @@ '1 & (2|3)'::query_int; IF n <> 1 THEN RAISE EXCEPTION 'intarray query_int failed: %', n; END IF; END $$", + ], + "isn" => &[ + "DO $$ BEGIN IF isbn('978-0-393-04002-9')::text <> '0-393-04002-X' THEN RAISE EXCEPTION 'isbn failed'; END IF; IF isbn13('0901690546')::text <> '978-0-901690-54-8' THEN RAISE EXCEPTION 'isbn13 failed'; END IF; IF issn('1436-4522')::text <> '1436-4522' THEN RAISE EXCEPTION 'issn failed'; END IF; END $$", + ], + "lo" => &[ + "CREATE TEMP TABLE oxide_lo (id int, data oid)", + "CREATE TRIGGER oxide_lo_manage BEFORE UPDATE OR DELETE ON oxide_lo FOR EACH ROW EXECUTE FUNCTION lo_manage(data)", + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'oxide_lo_manage') THEN RAISE EXCEPTION 'lo trigger missing'; END IF; END $$", + ], + "ltree" => &[ + "CREATE TEMP TABLE oxide_ltree (path ltree)", + "INSERT INTO oxide_ltree VALUES ('Top.Science.Astronomy'), ('Top.Collections.Pictures')", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ltree WHERE path <@ 'Top.Science'; IF n <> 1 THEN RAISE EXCEPTION 'ltree ancestor query failed: %', n; END IF; END $$", + ], + "pageinspect" => &[ + "CREATE TEMP TABLE oxide_pageinspect (id int)", + "INSERT INTO oxide_pageinspect SELECT generate_series(1, 5)", + "SELECT * FROM page_header(get_raw_page('oxide_pageinspect', 0))", + ], + "pg_buffercache" => &[ + "SELECT * FROM pg_buffercache_summary()", + "SELECT * FROM pg_buffercache_usage_counts()", + ], + "pg_freespacemap" => &[ + "CREATE TEMP TABLE oxide_fsm (id int, value text)", + "INSERT INTO oxide_fsm SELECT i, repeat('x', 200) FROM generate_series(1, 20) AS i", + "DELETE FROM oxide_fsm WHERE id % 2 = 0", + "SELECT * FROM pg_freespace('oxide_fsm') LIMIT 1", + ], + "pg_hashids" => &[ + "DO $$ BEGIN IF id_encode(1001) <> 'jNl' THEN RAISE EXCEPTION 'pg_hashids encode failed'; END IF; IF id_decode_once('jNl') <> 1001 THEN RAISE EXCEPTION 'pg_hashids decode failed'; END IF; END $$", + ], + "pg_ivm" => &[ + "CREATE TABLE oxide_ivm_orders (id int, amount int)", + "INSERT INTO oxide_ivm_orders VALUES (1, 10), (2, 20)", + "SELECT pgivm.create_immv('oxide_ivm_summary', $$ SELECT id, amount FROM oxide_ivm_orders $$)", + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ivm_summary; IF n <> 2 THEN RAISE EXCEPTION 'pg_ivm initial count failed: %', n; END IF; END $$", + ], + "pg_surgery" => &[ + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'heap_force_kill') THEN RAISE EXCEPTION 'pg_surgery function missing'; END IF; END $$", + ], + "pg_textsearch" => &[ + "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_am WHERE amname = 'bm25') THEN RAISE EXCEPTION 'bm25 access method missing'; END IF; END $$", + "SELECT to_bm25query('postgres wasm')", + ], + "pg_trgm" => &[ + "DO $$ DECLARE score float8; BEGIN SELECT similarity('postgres', 'postgrex') INTO score; IF score <= 0 THEN RAISE EXCEPTION 'pg_trgm similarity failed: %', score; END IF; END $$", + ], + "pg_uuidv7" => &[ + "DO $$ DECLARE id uuid; ts timestamptz; BEGIN SELECT uuid_generate_v7() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuidv7 length failed'; END IF; SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') INTO ts; IF ts IS NULL THEN RAISE EXCEPTION 'uuidv7 timestamp failed'; END IF; END $$", + ], + "pg_visibility" => &[ + "CREATE TEMP TABLE oxide_visibility (id int)", + "INSERT INTO oxide_visibility SELECT generate_series(1, 5)", + "SELECT * FROM pg_visibility('oxide_visibility') LIMIT 1", + "SELECT * FROM pg_visibility_map('oxide_visibility') LIMIT 1", + ], + "pg_walinspect" => &[ + "CREATE TEMP TABLE oxide_walinspect (value text)", + "CREATE TEMP TABLE oxide_walinspect_lsn AS SELECT pg_current_wal_lsn() AS before_lsn", + "INSERT INTO oxide_walinspect SELECT 'row ' || i::text FROM generate_series(1, 5) AS i", + "SELECT * FROM pg_get_wal_block_info((SELECT before_lsn FROM oxide_walinspect_lsn), pg_current_wal_lsn()) ORDER BY start_lsn, block_id LIMIT 20", + ], + "pgcrypto" => &[ + "DO $$ DECLARE hashed text; encrypted bytea; BEGIN IF encode(digest('abc', 'sha256'), 'hex') <> 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' THEN RAISE EXCEPTION 'sha256 digest failed'; END IF; IF length(gen_random_bytes(16)) <> 16 THEN RAISE EXCEPTION 'random bytes length failed'; END IF; SELECT crypt('secret', gen_salt('bf', 4)) INTO hashed; IF crypt('secret', hashed) <> hashed THEN RAISE EXCEPTION 'password hash verify failed'; END IF; SELECT pgp_sym_encrypt('oliphaunt secret', 'passphrase') INTO encrypted; IF pgp_sym_decrypt(encrypted, 'passphrase') <> 'oliphaunt secret' THEN RAISE EXCEPTION 'PGP symmetric decrypt failed'; END IF; END $$", + "DO $$ BEGIN IF encode(hmac('test', 'key', 'sha1'), 'hex') <> '671f54ce0c540f78ffe1e26dcf9c2a047aea4fda' THEN RAISE EXCEPTION 'hmac failed'; END IF; IF gen_random_uuid()::text !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN RAISE EXCEPTION 'random uuid format failed'; END IF; END $$", + "DO $$ DECLARE armored text; header_count int; BEGIN SELECT armor(digest('test', 'sha1'), ARRAY['Version'], ARRAY['oliphaunt']) INTO armored; IF position('Version: oliphaunt' in armored) = 0 THEN RAISE EXCEPTION 'armor header failed'; END IF; SELECT count(*) INTO header_count FROM pgp_armor_headers(armored); IF header_count <> 1 THEN RAISE EXCEPTION 'armor header count failed: %', header_count; END IF; END $$", + "DO $$ DECLARE encrypted bytea; crypto_key bytea := decode('000102030405060708090a0b0c0d0e0f', 'hex'); crypto_iv bytea := decode('101112131415161718191a1b1c1d1e1f', 'hex'); BEGIN SELECT pgp_sym_encrypt('oliphaunt secret', 'passphrase') INTO encrypted; IF pgp_key_id(encrypted) <> 'SYMKEY' THEN RAISE EXCEPTION 'PGP symmetric key id failed'; END IF; SELECT encrypt(convert_to('oliphaunt raw cipher', 'UTF8'), crypto_key, 'aes') INTO encrypted; IF convert_from(decrypt(encrypted, crypto_key, 'aes'), 'UTF8') <> 'oliphaunt raw cipher' THEN RAISE EXCEPTION 'raw decrypt failed'; END IF; SELECT encrypt_iv(convert_to('oliphaunt iv cipher', 'UTF8'), crypto_key, crypto_iv, 'aes-cbc') INTO encrypted; IF convert_from(decrypt_iv(encrypted, crypto_key, crypto_iv, 'aes-cbc'), 'UTF8') <> 'oliphaunt iv cipher' THEN RAISE EXCEPTION 'raw iv decrypt failed'; END IF; END $$", + ], + "pgtap" => &[ + "BEGIN", + "SELECT plan(1)", + "SELECT pass('pgtap smoke')", + "SELECT * FROM finish()", + "ROLLBACK", + ], + "seg" => &[ + "DO $$ BEGIN IF '7(+-)1'::seg::text <> '6 .. 8' THEN RAISE EXCEPTION 'seg cast failed'; END IF; END $$", + ], + "tablefunc" => &[ + "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM normal_rand(10, 5, 3); IF n <> 10 THEN RAISE EXCEPTION 'normal_rand failed: %', n; END IF; END $$", + "SELECT * FROM crosstab('SELECT 1, 1, 10 UNION ALL SELECT 1, 2, 20') AS ct(rowid int, c1 int, c2 int)", + ], + "tcn" => &[ + "CREATE TEMP TABLE oxide_tcn (id int PRIMARY KEY, value text)", + "CREATE TRIGGER oxide_tcn_trigger AFTER INSERT OR UPDATE OR DELETE ON oxide_tcn FOR EACH ROW EXECUTE FUNCTION triggered_change_notification()", + "INSERT INTO oxide_tcn VALUES (1, 'one')", + ], + "tsm_system_rows" => &[ + "CREATE TEMP TABLE oxide_tsm_rows AS SELECT i FROM generate_series(1, 20) AS i", + "SELECT * FROM oxide_tsm_rows TABLESAMPLE SYSTEM_ROWS(5)", + ], + "tsm_system_time" => &[ + "CREATE TEMP TABLE oxide_tsm_time AS SELECT i FROM generate_series(1, 20) AS i", + "SELECT * FROM oxide_tsm_time TABLESAMPLE SYSTEM_TIME(50)", + ], + "unaccent" => &[ + "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('unaccent', 'Hôtel'), ',') INTO lex; IF lex <> 'Hotel' THEN RAISE EXCEPTION 'unaccent failed: %', lex; END IF; END $$", + ], + "uuid-ossp" => &[ + "DO $$ DECLARE id uuid; BEGIN SELECT uuid_generate_v1() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuid-ossp v1 length failed'; END IF; SELECT uuid_generate_v4() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuid-ossp v4 length failed'; END IF; END $$", + "DO $$ BEGIN IF uuid_generate_v3(uuid_ns_dns(), 'www.example.com')::text <> '5df41881-3aed-3515-88a7-2f4a814cf09e' THEN RAISE EXCEPTION 'uuid-ossp v3 failed'; END IF; IF uuid_generate_v5(uuid_ns_dns(), 'www.example.com')::text <> '2ed6657d-e927-568b-95e1-2665a8aea6a2' THEN RAISE EXCEPTION 'uuid-ossp v5 failed'; END IF; END $$", + "DO $$ BEGIN IF uuid_nil()::text <> '00000000-0000-0000-0000-000000000000' THEN RAISE EXCEPTION 'uuid-ossp nil failed'; END IF; IF uuid_ns_dns()::text <> '6ba7b810-9dad-11d1-80b4-00c04fd430c8' THEN RAISE EXCEPTION 'uuid-ossp dns namespace failed'; END IF; IF uuid_ns_oid()::text <> '6ba7b812-9dad-11d1-80b4-00c04fd430c8' THEN RAISE EXCEPTION 'uuid-ossp oid namespace failed'; END IF; END $$", + ], + "vector" => &[ + "CREATE TEMP TABLE oxide_vector (embedding vector(3))", + "INSERT INTO oxide_vector VALUES ('[1,2,3]')", + "DO $$ DECLARE d float8; BEGIN SELECT embedding <-> '[1,2,4]'::vector INTO d FROM oxide_vector; IF d <> 1 THEN RAISE EXCEPTION 'vector distance failed: %', d; END IF; END $$", + ], + other => panic!("missing smoke SQL for extension candidate {other}"), + } + } +} diff --git a/src/pglite/generated_extensions.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs similarity index 75% rename from src/pglite/generated_extensions.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs index a1332489..d4b4fcd9 100644 --- a/src/pglite/generated_extensions.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/generated_extensions.rs @@ -1,38 +1,21 @@ // @generated by `cargo run -p xtask -- extensions generate` -use super::{Extension, ExtensionSetup}; +use super::{Extension, ExtensionNativeModule, ExtensionSetup}; const EMPTY_SQL_NAMES: &[&str] = &[]; const EMPTY_SQL: &[&str] = &[]; - -const CANDIDATE_AGE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; -const CANDIDATE_AGE_LOAD_SQL: &[&str] = &["LOAD 'age';"]; -const CANDIDATE_AGE_POST_CREATE_SQL: &[&str] = - &["SET search_path = ag_catalog, \"$user\", public;"]; - -pub(crate) const CANDIDATE_AGE: Extension = Extension::new( - "Apache AGE", - "age", - "extensions/age.tar.zst", - Some("age.so"), - Some("extension:age"), - CANDIDATE_AGE_DEPENDENCIES, - ExtensionSetup::new( - true, - Some("ag_catalog"), - CANDIDATE_AGE_LOAD_SQL, - CANDIDATE_AGE_POST_CREATE_SQL, - ), -); +const EMPTY_NATIVE_MODULES: &[ExtensionNativeModule] = &[]; const CANDIDATE_AMCHECK_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_AMCHECK_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_AMCHECK_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_AMCHECK_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_AMCHECK: Extension = Extension::new( "amcheck", "amcheck", "extensions/amcheck.tar.zst", + CANDIDATE_AMCHECK_NATIVE_SUPPORT_MODULES, Some("amcheck.so"), Some("extension:amcheck"), CANDIDATE_AMCHECK_DEPENDENCIES, @@ -52,11 +35,14 @@ const CANDIDATE_AUTO_EXPLAIN_LOAD_SQL: &[&str] = &[ "SET auto_explain.log_level = 'NOTICE';", ]; const CANDIDATE_AUTO_EXPLAIN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_AUTO_EXPLAIN_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_AUTO_EXPLAIN: Extension = Extension::new( "auto_explain", "auto_explain", "extensions/auto_explain.tar.zst", + CANDIDATE_AUTO_EXPLAIN_NATIVE_SUPPORT_MODULES, Some("auto_explain.so"), Some("extension:auto_explain"), CANDIDATE_AUTO_EXPLAIN_DEPENDENCIES, @@ -71,11 +57,13 @@ pub(crate) const CANDIDATE_AUTO_EXPLAIN: Extension = Extension::new( const CANDIDATE_BLOOM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_BLOOM_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_BLOOM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BLOOM_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_BLOOM: Extension = Extension::new( "bloom", "bloom", "extensions/bloom.tar.zst", + CANDIDATE_BLOOM_NATIVE_SUPPORT_MODULES, Some("bloom.so"), Some("extension:bloom"), CANDIDATE_BLOOM_DEPENDENCIES, @@ -90,11 +78,13 @@ pub(crate) const CANDIDATE_BLOOM: Extension = Extension::new( const CANDIDATE_BTREE_GIN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_BTREE_GIN_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_BTREE_GIN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BTREE_GIN_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_BTREE_GIN: Extension = Extension::new( "btree_gin", "btree_gin", "extensions/btree_gin.tar.zst", + CANDIDATE_BTREE_GIN_NATIVE_SUPPORT_MODULES, Some("btree_gin.so"), Some("extension:btree_gin"), CANDIDATE_BTREE_GIN_DEPENDENCIES, @@ -109,11 +99,13 @@ pub(crate) const CANDIDATE_BTREE_GIN: Extension = Extension::new( const CANDIDATE_BTREE_GIST_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_BTREE_GIST_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_BTREE_GIST_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_BTREE_GIST_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_BTREE_GIST: Extension = Extension::new( "btree_gist", "btree_gist", "extensions/btree_gist.tar.zst", + CANDIDATE_BTREE_GIST_NATIVE_SUPPORT_MODULES, Some("btree_gist.so"), Some("extension:btree_gist"), CANDIDATE_BTREE_GIST_DEPENDENCIES, @@ -128,11 +120,13 @@ pub(crate) const CANDIDATE_BTREE_GIST: Extension = Extension::new( const CANDIDATE_CITEXT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_CITEXT_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_CITEXT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_CITEXT_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_CITEXT: Extension = Extension::new( "citext", "citext", "extensions/citext.tar.zst", + CANDIDATE_CITEXT_NATIVE_SUPPORT_MODULES, Some("citext.so"), Some("extension:citext"), CANDIDATE_CITEXT_DEPENDENCIES, @@ -147,11 +141,13 @@ pub(crate) const CANDIDATE_CITEXT: Extension = Extension::new( const CANDIDATE_CUBE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_CUBE_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_CUBE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_CUBE_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_CUBE: Extension = Extension::new( "cube", "cube", "extensions/cube.tar.zst", + CANDIDATE_CUBE_NATIVE_SUPPORT_MODULES, Some("cube.so"), Some("extension:cube"), CANDIDATE_CUBE_DEPENDENCIES, @@ -166,11 +162,13 @@ pub(crate) const CANDIDATE_CUBE: Extension = Extension::new( const CANDIDATE_DICT_INT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_DICT_INT_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_DICT_INT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_DICT_INT_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_DICT_INT: Extension = Extension::new( "dict_int", "dict_int", "extensions/dict_int.tar.zst", + CANDIDATE_DICT_INT_NATIVE_SUPPORT_MODULES, Some("dict_int.so"), Some("extension:dict_int"), CANDIDATE_DICT_INT_DEPENDENCIES, @@ -185,11 +183,13 @@ pub(crate) const CANDIDATE_DICT_INT: Extension = Extension::new( const CANDIDATE_DICT_XSYN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_DICT_XSYN_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_DICT_XSYN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_DICT_XSYN_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_DICT_XSYN: Extension = Extension::new( "dict_xsyn", "dict_xsyn", "extensions/dict_xsyn.tar.zst", + CANDIDATE_DICT_XSYN_NATIVE_SUPPORT_MODULES, Some("dict_xsyn.so"), Some("extension:dict_xsyn"), CANDIDATE_DICT_XSYN_DEPENDENCIES, @@ -204,11 +204,14 @@ pub(crate) const CANDIDATE_DICT_XSYN: Extension = Extension::new( const CANDIDATE_EARTHDISTANCE_DEPENDENCIES: &[&str] = &["cube"]; const CANDIDATE_EARTHDISTANCE_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_EARTHDISTANCE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_EARTHDISTANCE_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_EARTHDISTANCE: Extension = Extension::new( "earthdistance", "earthdistance", "extensions/earthdistance.tar.zst", + CANDIDATE_EARTHDISTANCE_NATIVE_SUPPORT_MODULES, Some("earthdistance.so"), Some("extension:earthdistance"), CANDIDATE_EARTHDISTANCE_DEPENDENCIES, @@ -223,11 +226,13 @@ pub(crate) const CANDIDATE_EARTHDISTANCE: Extension = Extension::new( const CANDIDATE_FILE_FDW_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_FILE_FDW_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_FILE_FDW_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_FILE_FDW_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_FILE_FDW: Extension = Extension::new( "file_fdw", "file_fdw", "extensions/file_fdw.tar.zst", + CANDIDATE_FILE_FDW_NATIVE_SUPPORT_MODULES, Some("file_fdw.so"), Some("extension:file_fdw"), CANDIDATE_FILE_FDW_DEPENDENCIES, @@ -242,11 +247,14 @@ pub(crate) const CANDIDATE_FILE_FDW: Extension = Extension::new( const CANDIDATE_FUZZYSTRMATCH_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_FUZZYSTRMATCH_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_FUZZYSTRMATCH_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_FUZZYSTRMATCH_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_FUZZYSTRMATCH: Extension = Extension::new( "fuzzystrmatch", "fuzzystrmatch", "extensions/fuzzystrmatch.tar.zst", + CANDIDATE_FUZZYSTRMATCH_NATIVE_SUPPORT_MODULES, Some("fuzzystrmatch.so"), Some("extension:fuzzystrmatch"), CANDIDATE_FUZZYSTRMATCH_DEPENDENCIES, @@ -261,11 +269,13 @@ pub(crate) const CANDIDATE_FUZZYSTRMATCH: Extension = Extension::new( const CANDIDATE_HSTORE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_HSTORE_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_HSTORE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_HSTORE_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_HSTORE: Extension = Extension::new( "hstore", "hstore", "extensions/hstore.tar.zst", + CANDIDATE_HSTORE_NATIVE_SUPPORT_MODULES, Some("hstore.so"), Some("extension:hstore"), CANDIDATE_HSTORE_DEPENDENCIES, @@ -280,11 +290,13 @@ pub(crate) const CANDIDATE_HSTORE: Extension = Extension::new( const CANDIDATE_INTARRAY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_INTARRAY_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_INTARRAY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_INTARRAY_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_INTARRAY: Extension = Extension::new( "intarray", "intarray", "extensions/intarray.tar.zst", + CANDIDATE_INTARRAY_NATIVE_SUPPORT_MODULES, Some("_int.so"), Some("extension:intarray"), CANDIDATE_INTARRAY_DEPENDENCIES, @@ -299,11 +311,13 @@ pub(crate) const CANDIDATE_INTARRAY: Extension = Extension::new( const CANDIDATE_ISN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_ISN_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_ISN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_ISN_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_ISN: Extension = Extension::new( "isn", "isn", "extensions/isn.tar.zst", + CANDIDATE_ISN_NATIVE_SUPPORT_MODULES, Some("isn.so"), Some("extension:isn"), CANDIDATE_ISN_DEPENDENCIES, @@ -318,11 +332,13 @@ pub(crate) const CANDIDATE_ISN: Extension = Extension::new( const CANDIDATE_LO_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_LO_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_LO_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_LO_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_LO: Extension = Extension::new( "lo", "lo", "extensions/lo.tar.zst", + CANDIDATE_LO_NATIVE_SUPPORT_MODULES, Some("lo.so"), Some("extension:lo"), CANDIDATE_LO_DEPENDENCIES, @@ -337,11 +353,13 @@ pub(crate) const CANDIDATE_LO: Extension = Extension::new( const CANDIDATE_LTREE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_LTREE_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_LTREE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_LTREE_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_LTREE: Extension = Extension::new( "ltree", "ltree", "extensions/ltree.tar.zst", + CANDIDATE_LTREE_NATIVE_SUPPORT_MODULES, Some("ltree.so"), Some("extension:ltree"), CANDIDATE_LTREE_DEPENDENCIES, @@ -356,11 +374,13 @@ pub(crate) const CANDIDATE_LTREE: Extension = Extension::new( const CANDIDATE_PAGEINSPECT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PAGEINSPECT_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PAGEINSPECT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PAGEINSPECT_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PAGEINSPECT: Extension = Extension::new( "pageinspect", "pageinspect", "extensions/pageinspect.tar.zst", + CANDIDATE_PAGEINSPECT_NATIVE_SUPPORT_MODULES, Some("pageinspect.so"), Some("extension:pageinspect"), CANDIDATE_PAGEINSPECT_DEPENDENCIES, @@ -375,11 +395,14 @@ pub(crate) const CANDIDATE_PAGEINSPECT: Extension = Extension::new( const CANDIDATE_PG_BUFFERCACHE_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_BUFFERCACHE_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_BUFFERCACHE_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_BUFFERCACHE_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_BUFFERCACHE: Extension = Extension::new( "pg_buffercache", "pg_buffercache", "extensions/pg_buffercache.tar.zst", + CANDIDATE_PG_BUFFERCACHE_NATIVE_SUPPORT_MODULES, Some("pg_buffercache.so"), Some("extension:pg_buffercache"), CANDIDATE_PG_BUFFERCACHE_DEPENDENCIES, @@ -394,11 +417,14 @@ pub(crate) const CANDIDATE_PG_BUFFERCACHE: Extension = Extension::new( const CANDIDATE_PG_FREESPACEMAP_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_FREESPACEMAP_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_FREESPACEMAP_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_FREESPACEMAP_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_FREESPACEMAP: Extension = Extension::new( "pg_freespacemap", "pg_freespacemap", "extensions/pg_freespacemap.tar.zst", + CANDIDATE_PG_FREESPACEMAP_NATIVE_SUPPORT_MODULES, Some("pg_freespacemap.so"), Some("extension:pg_freespacemap"), CANDIDATE_PG_FREESPACEMAP_DEPENDENCIES, @@ -413,11 +439,13 @@ pub(crate) const CANDIDATE_PG_FREESPACEMAP: Extension = Extension::new( const CANDIDATE_PG_HASHIDS_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_HASHIDS_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_HASHIDS_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_HASHIDS_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_HASHIDS: Extension = Extension::new( "pg_hashids", "pg_hashids", "extensions/pg_hashids.tar.zst", + CANDIDATE_PG_HASHIDS_NATIVE_SUPPORT_MODULES, Some("pg_hashids.so"), Some("extension:pg_hashids"), CANDIDATE_PG_HASHIDS_DEPENDENCIES, @@ -432,11 +460,13 @@ pub(crate) const CANDIDATE_PG_HASHIDS: Extension = Extension::new( const CANDIDATE_PG_IVM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_IVM_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_IVM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_IVM_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_IVM: Extension = Extension::new( "pg_ivm", "pg_ivm", "extensions/pg_ivm.tar.zst", + CANDIDATE_PG_IVM_NATIVE_SUPPORT_MODULES, Some("pg_ivm.so"), Some("extension:pg_ivm"), CANDIDATE_PG_IVM_DEPENDENCIES, @@ -451,11 +481,13 @@ pub(crate) const CANDIDATE_PG_IVM: Extension = Extension::new( const CANDIDATE_PG_SURGERY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_SURGERY_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_SURGERY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_SURGERY_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_SURGERY: Extension = Extension::new( "pg_surgery", "pg_surgery", "extensions/pg_surgery.tar.zst", + CANDIDATE_PG_SURGERY_NATIVE_SUPPORT_MODULES, Some("pg_surgery.so"), Some("extension:pg_surgery"), CANDIDATE_PG_SURGERY_DEPENDENCIES, @@ -470,11 +502,14 @@ pub(crate) const CANDIDATE_PG_SURGERY: Extension = Extension::new( const CANDIDATE_PG_TEXTSEARCH_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_TEXTSEARCH_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_TEXTSEARCH_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_TEXTSEARCH_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_TEXTSEARCH: Extension = Extension::new( "pg_textsearch", "pg_textsearch", "extensions/pg_textsearch.tar.zst", + CANDIDATE_PG_TEXTSEARCH_NATIVE_SUPPORT_MODULES, Some("pg_textsearch.so"), Some("extension:pg_textsearch"), CANDIDATE_PG_TEXTSEARCH_DEPENDENCIES, @@ -489,11 +524,13 @@ pub(crate) const CANDIDATE_PG_TEXTSEARCH: Extension = Extension::new( const CANDIDATE_PG_TRGM_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_TRGM_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_TRGM_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_TRGM_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_TRGM: Extension = Extension::new( "pg_trgm", "pg_trgm", "extensions/pg_trgm.tar.zst", + CANDIDATE_PG_TRGM_NATIVE_SUPPORT_MODULES, Some("pg_trgm.so"), Some("extension:pg_trgm"), CANDIDATE_PG_TRGM_DEPENDENCIES, @@ -508,11 +545,13 @@ pub(crate) const CANDIDATE_PG_TRGM: Extension = Extension::new( const CANDIDATE_PG_UUIDV7_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_UUIDV7_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_UUIDV7_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_UUIDV7_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_UUIDV7: Extension = Extension::new( "pg_uuidv7", "pg_uuidv7", "extensions/pg_uuidv7.tar.zst", + CANDIDATE_PG_UUIDV7_NATIVE_SUPPORT_MODULES, Some("pg_uuidv7.so"), Some("extension:pg_uuidv7"), CANDIDATE_PG_UUIDV7_DEPENDENCIES, @@ -527,11 +566,14 @@ pub(crate) const CANDIDATE_PG_UUIDV7: Extension = Extension::new( const CANDIDATE_PG_VISIBILITY_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_VISIBILITY_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_VISIBILITY_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_VISIBILITY_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_VISIBILITY: Extension = Extension::new( "pg_visibility", "pg_visibility", "extensions/pg_visibility.tar.zst", + CANDIDATE_PG_VISIBILITY_NATIVE_SUPPORT_MODULES, Some("pg_visibility.so"), Some("extension:pg_visibility"), CANDIDATE_PG_VISIBILITY_DEPENDENCIES, @@ -546,11 +588,14 @@ pub(crate) const CANDIDATE_PG_VISIBILITY: Extension = Extension::new( const CANDIDATE_PG_WALINSPECT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PG_WALINSPECT_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PG_WALINSPECT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PG_WALINSPECT_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PG_WALINSPECT: Extension = Extension::new( "pg_walinspect", "pg_walinspect", "extensions/pg_walinspect.tar.zst", + CANDIDATE_PG_WALINSPECT_NATIVE_SUPPORT_MODULES, Some("pg_walinspect.so"), Some("extension:pg_walinspect"), CANDIDATE_PG_WALINSPECT_DEPENDENCIES, @@ -562,14 +607,37 @@ pub(crate) const CANDIDATE_PG_WALINSPECT: Extension = Extension::new( ), ); +const CANDIDATE_PGCRYPTO_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_PGCRYPTO_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PGCRYPTO_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PGCRYPTO_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; + +pub(crate) const CANDIDATE_PGCRYPTO: Extension = Extension::new( + "pgcrypto", + "pgcrypto", + "extensions/pgcrypto.tar.zst", + CANDIDATE_PGCRYPTO_NATIVE_SUPPORT_MODULES, + Some("pgcrypto.so"), + Some("extension:pgcrypto"), + CANDIDATE_PGCRYPTO_DEPENDENCIES, + ExtensionSetup::new( + true, + None, + CANDIDATE_PGCRYPTO_LOAD_SQL, + CANDIDATE_PGCRYPTO_POST_CREATE_SQL, + ), +); + const CANDIDATE_PGTAP_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_PGTAP_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_PGTAP_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_PGTAP_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_PGTAP: Extension = Extension::new( "pgtap", "pgtap", "extensions/pgtap.tar.zst", + CANDIDATE_PGTAP_NATIVE_SUPPORT_MODULES, None, None, CANDIDATE_PGTAP_DEPENDENCIES, @@ -581,14 +649,41 @@ pub(crate) const CANDIDATE_PGTAP: Extension = Extension::new( ), ); +const CANDIDATE_POSTGIS_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_POSTGIS_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_POSTGIS_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_POSTGIS_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + &[ExtensionNativeModule::new( + "lib/postgresql/liboliphaunt_postgis_deps.so", + Some("extension:postgis:postgis_deps"), + )]; + +pub(crate) const CANDIDATE_POSTGIS: Extension = Extension::new( + "PostGIS", + "postgis", + "extensions/postgis.tar.zst", + CANDIDATE_POSTGIS_NATIVE_SUPPORT_MODULES, + Some("postgis-3.so"), + Some("extension:postgis"), + CANDIDATE_POSTGIS_DEPENDENCIES, + ExtensionSetup::new( + true, + None, + CANDIDATE_POSTGIS_LOAD_SQL, + CANDIDATE_POSTGIS_POST_CREATE_SQL, + ), +); + const CANDIDATE_SEG_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_SEG_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_SEG_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_SEG_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_SEG: Extension = Extension::new( "seg", "seg", "extensions/seg.tar.zst", + CANDIDATE_SEG_NATIVE_SUPPORT_MODULES, Some("seg.so"), Some("extension:seg"), CANDIDATE_SEG_DEPENDENCIES, @@ -603,11 +698,13 @@ pub(crate) const CANDIDATE_SEG: Extension = Extension::new( const CANDIDATE_TABLEFUNC_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_TABLEFUNC_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_TABLEFUNC_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TABLEFUNC_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_TABLEFUNC: Extension = Extension::new( "tablefunc", "tablefunc", "extensions/tablefunc.tar.zst", + CANDIDATE_TABLEFUNC_NATIVE_SUPPORT_MODULES, Some("tablefunc.so"), Some("extension:tablefunc"), CANDIDATE_TABLEFUNC_DEPENDENCIES, @@ -622,11 +719,13 @@ pub(crate) const CANDIDATE_TABLEFUNC: Extension = Extension::new( const CANDIDATE_TCN_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_TCN_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_TCN_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TCN_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_TCN: Extension = Extension::new( "tcn", "tcn", "extensions/tcn.tar.zst", + CANDIDATE_TCN_NATIVE_SUPPORT_MODULES, Some("tcn.so"), Some("extension:tcn"), CANDIDATE_TCN_DEPENDENCIES, @@ -641,11 +740,14 @@ pub(crate) const CANDIDATE_TCN: Extension = Extension::new( const CANDIDATE_TSM_SYSTEM_ROWS_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_TSM_SYSTEM_ROWS_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_TSM_SYSTEM_ROWS_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TSM_SYSTEM_ROWS_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_TSM_SYSTEM_ROWS: Extension = Extension::new( "tsm_system_rows", "tsm_system_rows", "extensions/tsm_system_rows.tar.zst", + CANDIDATE_TSM_SYSTEM_ROWS_NATIVE_SUPPORT_MODULES, Some("tsm_system_rows.so"), Some("extension:tsm_system_rows"), CANDIDATE_TSM_SYSTEM_ROWS_DEPENDENCIES, @@ -660,11 +762,14 @@ pub(crate) const CANDIDATE_TSM_SYSTEM_ROWS: Extension = Extension::new( const CANDIDATE_TSM_SYSTEM_TIME_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_TSM_SYSTEM_TIME_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_TSM_SYSTEM_TIME_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_TSM_SYSTEM_TIME_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = + EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_TSM_SYSTEM_TIME: Extension = Extension::new( "tsm_system_time", "tsm_system_time", "extensions/tsm_system_time.tar.zst", + CANDIDATE_TSM_SYSTEM_TIME_NATIVE_SUPPORT_MODULES, Some("tsm_system_time.so"), Some("extension:tsm_system_time"), CANDIDATE_TSM_SYSTEM_TIME_DEPENDENCIES, @@ -679,11 +784,13 @@ pub(crate) const CANDIDATE_TSM_SYSTEM_TIME: Extension = Extension::new( const CANDIDATE_UNACCENT_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_UNACCENT_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_UNACCENT_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_UNACCENT_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_UNACCENT: Extension = Extension::new( "unaccent", "unaccent", "extensions/unaccent.tar.zst", + CANDIDATE_UNACCENT_NATIVE_SUPPORT_MODULES, Some("unaccent.so"), Some("extension:unaccent"), CANDIDATE_UNACCENT_DEPENDENCIES, @@ -695,14 +802,37 @@ pub(crate) const CANDIDATE_UNACCENT: Extension = Extension::new( ), ); +const CANDIDATE_UUID_OSSP_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; +const CANDIDATE_UUID_OSSP_LOAD_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_UUID_OSSP_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_UUID_OSSP_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; + +pub(crate) const CANDIDATE_UUID_OSSP: Extension = Extension::new( + "uuid-ossp", + "uuid-ossp", + "extensions/uuid-ossp.tar.zst", + CANDIDATE_UUID_OSSP_NATIVE_SUPPORT_MODULES, + Some("uuid-ossp.so"), + Some("extension:uuid-ossp"), + CANDIDATE_UUID_OSSP_DEPENDENCIES, + ExtensionSetup::new( + true, + Some("pg_catalog"), + CANDIDATE_UUID_OSSP_LOAD_SQL, + CANDIDATE_UUID_OSSP_POST_CREATE_SQL, + ), +); + const CANDIDATE_VECTOR_DEPENDENCIES: &[&str] = EMPTY_SQL_NAMES; const CANDIDATE_VECTOR_LOAD_SQL: &[&str] = EMPTY_SQL; const CANDIDATE_VECTOR_POST_CREATE_SQL: &[&str] = EMPTY_SQL; +const CANDIDATE_VECTOR_NATIVE_SUPPORT_MODULES: &[ExtensionNativeModule] = EMPTY_NATIVE_MODULES; pub(crate) const CANDIDATE_VECTOR: Extension = Extension::new( "pgvector", "vector", "extensions/vector.tar.zst", + CANDIDATE_VECTOR_NATIVE_SUPPORT_MODULES, Some("vector.so"), Some("extension:vector"), CANDIDATE_VECTOR_DEPENDENCIES, @@ -714,7 +844,6 @@ pub(crate) const CANDIDATE_VECTOR: Extension = Extension::new( ), ); -pub const AGE: Extension = CANDIDATE_AGE; pub const AMCHECK: Extension = CANDIDATE_AMCHECK; pub const AUTO_EXPLAIN: Extension = CANDIDATE_AUTO_EXPLAIN; pub const BLOOM: Extension = CANDIDATE_BLOOM; @@ -743,17 +872,19 @@ pub const PG_TRGM: Extension = CANDIDATE_PG_TRGM; pub const PG_UUIDV7: Extension = CANDIDATE_PG_UUIDV7; pub const PG_VISIBILITY: Extension = CANDIDATE_PG_VISIBILITY; pub const PG_WALINSPECT: Extension = CANDIDATE_PG_WALINSPECT; +pub const PGCRYPTO: Extension = CANDIDATE_PGCRYPTO; pub const PGTAP: Extension = CANDIDATE_PGTAP; +pub const POSTGIS: Extension = CANDIDATE_POSTGIS; pub const SEG: Extension = CANDIDATE_SEG; pub const TABLEFUNC: Extension = CANDIDATE_TABLEFUNC; pub const TCN: Extension = CANDIDATE_TCN; pub const TSM_SYSTEM_ROWS: Extension = CANDIDATE_TSM_SYSTEM_ROWS; pub const TSM_SYSTEM_TIME: Extension = CANDIDATE_TSM_SYSTEM_TIME; pub const UNACCENT: Extension = CANDIDATE_UNACCENT; +pub const UUID_OSSP: Extension = CANDIDATE_UUID_OSSP; pub const VECTOR: Extension = CANDIDATE_VECTOR; pub const ALL: &[Extension] = &[ - AGE, AMCHECK, AUTO_EXPLAIN, BLOOM, @@ -782,17 +913,19 @@ pub const ALL: &[Extension] = &[ PG_UUIDV7, PG_VISIBILITY, PG_WALINSPECT, + PGCRYPTO, PGTAP, + POSTGIS, SEG, TABLEFUNC, TCN, TSM_SYSTEM_ROWS, TSM_SYSTEM_TIME, UNACCENT, + UUID_OSSP, VECTOR, ]; pub(crate) const CANDIDATES: &[Extension] = &[ - CANDIDATE_AGE, CANDIDATE_AMCHECK, CANDIDATE_AUTO_EXPLAIN, CANDIDATE_BLOOM, @@ -821,12 +954,15 @@ pub(crate) const CANDIDATES: &[Extension] = &[ CANDIDATE_PG_UUIDV7, CANDIDATE_PG_VISIBILITY, CANDIDATE_PG_WALINSPECT, + CANDIDATE_PGCRYPTO, CANDIDATE_PGTAP, + CANDIDATE_POSTGIS, CANDIDATE_SEG, CANDIDATE_TABLEFUNC, CANDIDATE_TCN, CANDIDATE_TSM_SYSTEM_ROWS, CANDIDATE_TSM_SYSTEM_TIME, CANDIDATE_UNACCENT, + CANDIDATE_UUID_OSSP, CANDIDATE_VECTOR, ]; diff --git a/src/pglite/interface.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/interface.rs similarity index 100% rename from src/pglite/interface.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/interface.rs 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 new file mode 100644 index 00000000..d1d1d5b3 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs @@ -0,0 +1,56 @@ +pub(crate) mod aot; +pub(crate) mod assets; +pub(crate) mod backend; +pub(crate) mod base; +pub(crate) mod builder; +pub(crate) mod client; +pub(crate) mod config; +pub(crate) mod data_dir; +pub(crate) mod engine; +pub(crate) mod errors; +#[cfg(feature = "extensions")] +pub mod extensions; +pub(crate) mod interface; +pub(crate) mod parse; +#[cfg(feature = "tools")] +pub mod pg_dump; +pub(crate) mod postgres_mod; +pub(crate) mod proxy; +pub(crate) mod server; +pub(crate) mod sync_host_fs; +pub(crate) mod templating; +pub(crate) mod timing; +pub(crate) mod transport; +pub(crate) mod types; +pub(crate) mod wire; + +#[doc(hidden)] +pub use assets::{AssetManifestMetadata, asset_manifest_metadata}; +pub use base::{ + InstallOptions, InstallOutcome, MountInfo, OliphauntPaths, PgDataTemplate, + PgDataTemplateManifest, build_pgdata_template, ensure_cluster, install_and_init, + install_and_init_in, install_default, install_extension_archive, install_extension_bytes, + install_into, install_with_options, preload_runtime_module, +}; +pub use builder::OliphauntBuilder; +pub use client::{GlobalListenerHandle, ListenerHandle, Oliphaunt, Transaction}; +pub use config::PostgresConfig; +pub use data_dir::DataDirArchiveFormat; +pub use engine::EngineCapabilities; +pub use errors::OliphauntError; +pub use interface::{ + DataTransferContainer, DebugLevel, DescribeQueryParam, DescribeQueryResult, + DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, NoticeCallback, + ParserMap, QueryOptions, Results, RowMode, Serializer, SerializerMap, TypeParser, +}; +#[cfg(feature = "tools")] +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::{ + OliphauntProxy, ProtocolStatsSnapshot, disable_protocol_stats, protocol_stats_snapshot, + reset_protocol_stats, +}; +pub use server::{OliphauntServer, OliphauntServerBuilder}; +pub use templating::{QueryTemplate, TemplatedQuery, format_query, quote_identifier}; +pub use timing::{PhaseTiming, capture_phase_timings, measure_phase, record_phase_timing}; diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/parse.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/parse.rs new file mode 100644 index 00000000..885284c4 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/parse.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; + +use serde_json::Value; + +use crate::oliphaunt::interface::{FieldInfo, ParserMap, QueryOptions, Results, RowMode}; +use crate::oliphaunt::types::ParserLookup; +use crate::protocol::messages::{ + BackendMessage, CommandCompleteMessage, DataRowMessage, RowDescriptionMessage, +}; + +pub fn parse_results( + messages: &[BackendMessage], + default_parsers: &ParserMap, + options: Option<&QueryOptions>, + blob: Option>, +) -> Vec { + let expected_result_sets = messages + .iter() + .filter(|message| matches!(message, BackendMessage::CommandComplete(_))) + .count() + .max(1); + let mut result_sets: Vec = Vec::with_capacity(expected_result_sets); + let mut current_fields: Vec = Vec::new(); + let mut current_rows: Vec = Vec::new(); + let mut affected_rows = 0usize; + + let empty_parsers = HashMap::new(); + let (row_mode, parsers_override) = options + .map(|opts| (opts.row_mode, &opts.parsers)) + .unwrap_or((None, &empty_parsers)); + + let parser_lookup = ParserLookup::new(default_parsers, parsers_override); + + for message in messages { + match message { + BackendMessage::RowDescription(desc) => { + current_fields = map_fields(desc); + } + BackendMessage::DataRow(row) => { + if current_fields.is_empty() { + continue; + } + let row_value = map_row(row, ¤t_fields, &parser_lookup, row_mode); + current_rows.push(row_value); + } + BackendMessage::CommandComplete(cmd) => { + affected_rows += retrieve_row_count(cmd); + result_sets.push(Results { + rows: std::mem::take(&mut current_rows), + fields: current_fields.clone(), + affected_rows: Some(affected_rows), + blob: blob.clone(), + }); + current_fields.clear(); + } + _ => {} + } + } + + if result_sets.is_empty() { + result_sets.push(Results { + rows: Vec::new(), + fields: Vec::new(), + affected_rows: Some(0), + blob, + }) + } + + result_sets +} + +pub fn parse_describe_statement_results(messages: &[BackendMessage]) -> Vec { + messages + .iter() + .find_map(|msg| match msg { + BackendMessage::ParameterDescription(desc) => Some(desc.data_type_ids.clone()), + _ => None, + }) + .unwrap_or_default() +} + +fn map_fields(desc: &RowDescriptionMessage) -> Vec { + desc.fields + .iter() + .map(|field| FieldInfo { + name: field.name.clone(), + data_type_id: field.data_type_id, + }) + .collect() +} + +fn map_row( + row: &DataRowMessage, + fields: &[FieldInfo], + parsers: &ParserLookup, + row_mode: Option, +) -> Value { + match row_mode { + Some(RowMode::Array) => { + let values: Vec = row + .fields + .iter() + .zip(fields.iter()) + .map(|(value, field)| parse_cell(value.as_deref(), field.data_type_id, parsers)) + .collect(); + Value::Array(values) + } + _ => { + let mut map = serde_json::Map::with_capacity(fields.len()); + for (value, field) in row.fields.iter().zip(fields.iter()) { + let parsed = parse_cell(value.as_deref(), field.data_type_id, parsers); + map.insert(field.name.clone(), parsed); + } + Value::Object(map) + } + } +} + +fn parse_cell(value: Option<&str>, type_id: i32, parsers: &ParserLookup) -> Value { + match value { + None => Value::Null, + Some(text) => parsers.apply(text, type_id), + } +} + +fn retrieve_row_count(msg: &CommandCompleteMessage) -> usize { + command_tag_row_count(msg.text.as_bytes()) +} + +pub(crate) fn command_tag_row_count(text: &[u8]) -> usize { + if text.starts_with(b"INSERT ") + || text.starts_with(b"UPDATE ") + || text.starts_with(b"DELETE ") + || text.starts_with(b"COPY ") + || text.starts_with(b"MERGE ") + { + parse_decimal_suffix(text).unwrap_or(0) + } else { + 0 + } +} + +fn parse_decimal_suffix(text: &[u8]) -> Option { + let mut start = text.len(); + while start > 0 && text[start - 1].is_ascii_digit() { + start -= 1; + } + if start == text.len() { + return None; + } + + let mut value = 0usize; + for digit in &text[start..] { + value = value + .checked_mul(10)? + .checked_add(usize::from(digit - b'0'))?; + } + Some(value) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn command(text: &str) -> CommandCompleteMessage { + CommandCompleteMessage { + length: text.len() + 5, + text: text.to_owned(), + } + } + + #[test] + fn retrieves_row_counts_from_command_tags() { + assert_eq!(retrieve_row_count(&command("INSERT 0 25")), 25); + assert_eq!(retrieve_row_count(&command("UPDATE 10")), 10); + assert_eq!(retrieve_row_count(&command("DELETE 3")), 3); + assert_eq!(retrieve_row_count(&command("COPY 42")), 42); + assert_eq!(retrieve_row_count(&command("MERGE 7")), 7); + assert_eq!(retrieve_row_count(&command("CREATE TABLE")), 0); + } +} 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 new file mode 100644 index 00000000..27328575 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs @@ -0,0 +1,1379 @@ +use std::fmt; +use std::io::{Cursor, Read, Seek, Write}; +use std::mem::MaybeUninit; +use std::net::Shutdown; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::Path; +use std::pin::Pin; +use std::sync::mpsc::{self, Receiver, SyncSender}; +use std::sync::{Arc, Mutex}; +use std::task::{Context as TaskContext, Poll}; +use std::thread; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result, anyhow, bail}; +use tempfile::TempDir; +use wasmer::Store; +use wasmer_types::ModuleHash; +use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; +use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; +use wasmer_wasix::virtual_fs::{self, AsyncRead, AsyncSeek, AsyncWrite}; +use wasmer_wasix::virtual_net::tcp_pair::TcpSocketHalf; +use wasmer_wasix::virtual_net::{ + self, InterestHandler, NetworkError, SocketStatus, VirtualConnectedSocket, VirtualIoSource, + VirtualNetworking, VirtualSocket, VirtualTcpSocket, +}; +use wasmer_wasix::{LocalNetworking, PluggableRuntime, VirtualFile}; + +use crate::oliphaunt::base::{install_optional_icu_data, unpack_runtime_archive_reader}; +use crate::oliphaunt::sync_host_fs::SyncHostFileSystem; +use crate::oliphaunt::timing; +use crate::oliphaunt::{aot, assets}; + +/// Options for the bundled WASIX `pg_dump` runner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PgDumpOptions { + args: Vec, + database: String, + username: String, +} + +impl Default for PgDumpOptions { + fn default() -> Self { + Self { + args: Vec::new(), + database: "template1".to_owned(), + username: "postgres".to_owned(), + } + } +} + +impl PgDumpOptions { + pub fn new() -> Self { + Self::default() + } + + /// Add one raw `pg_dump` argument. + pub fn arg(mut self, arg: impl Into) -> Self { + self.args.push(arg.into()); + self + } + + /// Add raw `pg_dump` arguments. + pub fn args(mut self, args: impl IntoIterator>) -> Self { + self.args.extend(args.into_iter().map(Into::into)); + self + } + + /// Select the database to dump. + pub fn database(mut self, database: impl Into) -> Self { + self.database = database.into(); + self + } + + /// Select the user passed to `pg_dump`. + 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'), + "pg_dump {name} must not be empty or contain NUL bytes" + ); + } + for arg in &self.args { + anyhow::ensure!( + !arg.contains('\0'), + "pg_dump argument must not contain NUL bytes" + ); + validate_passthrough_arg(arg)?; + } + Ok(()) + } + + pub(crate) fn database_ref(&self) -> &str { + &self.database + } + + pub(crate) fn username_ref(&self) -> &str { + &self.username + } +} + +/// 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!( + "pg_dump argument '{arg}' conflicts with oliphaunt-wasix's managed {flag}; use PgDumpOptions typed setters where available" + ); + } + Ok(()) +} + +fn disallowed_pg_dump_flag(arg: &str) -> Option<&'static str> { + const LONG_FLAGS: &[(&str, &str)] = &[ + ("--file", "output file"), + ("--format", "output format"), + ("--host", "host"), + ("--port", "port"), + ("--username", "username"), + ("--dbname", "database"), + ("--jobs", "job count"), + ]; + 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)] = &[ + ("-f", "output file"), + ("-F", "output format"), + ("-h", "host"), + ("-p", "port"), + ("-U", "username"), + ("-d", "database"), + ("-j", "job count"), + ]; + for (flag, label) in SHORT_FLAGS { + if arg == *flag || (arg.starts_with(*flag) && arg.len() > flag.len()) { + return Some(label); + } + } + 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()) +} + +/// 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 +where + F: FnOnce(PgDumpVirtualSocket) -> Result<()>, +{ + options.validate()?; + let (socket_tx, socket_rx) = mpsc::sync_channel(1); + let networking = DirectPgDumpNetworking::new(socket_tx); + let runner_options = options.clone(); + let runner = thread::spawn(move || { + dump_sql_with_networking(DIRECT_PG_DUMP_ADDR, &runner_options, networking) + }); + + let accepted = receive_direct_pg_dump_socket(&socket_rx, &runner) + .context("accept direct pg_dump virtual protocol connection"); + let serve_result = match accepted { + Ok(socket) => serve(socket), + Err(err) => Err(err), + }; + let dump_result = runner + .join() + .map_err(|_| anyhow!("direct pg_dump runner thread panicked"))?; + + match (serve_result, dump_result) { + (Ok(()), Ok(sql)) => Ok(sql), + (Err(err), Ok(_)) => Err(err), + (Ok(()), Err(err)) => Err(err), + (Err(err), Err(dump_err)) => { + Err(err.context(format!("direct pg_dump runner also failed: {dump_err:#}"))) + } + } +} + +fn dump_sql_with_networking( + addr: SocketAddr, + options: &PgDumpOptions, + networking: N, +) -> Result +where + N: VirtualNetworking + Sync, +{ + options.validate()?; + let _phase = timing::phase("pg_dump"); + let wasm = { + let _phase = timing::phase("pg_dump.load_embedded_module"); + 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) + .context("load pg_dump AOT artifact from oliphaunt-wasix-tools-aot-*")? + }; + let _store = Store::new(engine.clone()); + + let fs_root = TempDir::new().context("create pg_dump 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 pg_dump")?; + install_optional_icu_data(&fs_root.path().join("oliphaunt")) + .context("install WASIX ICU data for pg_dump")?; + } + let runtime = { + let _phase = timing::phase("pg_dump.tokio_runtime"); + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create Tokio runtime for WASIX pg_dump")? + }; + let (host_fs, wasix_runtime) = { + let _phase = timing::phase("pg_dump.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 output_path = "/host/out.sql"; + 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 = options.args.clone(); + args.extend([ + "-U".to_owned(), + options.username.clone(), + "-h".to_owned(), + host, + "-p".to_owned(), + port, + "--inserts".to_owned(), + "-j".to_owned(), + "1".to_owned(), + "-f".to_owned(), + output_path.to_owned(), + ]); + args.push(options.database.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("pg_dump.run_wasm"); + runner + .run_wasm( + RuntimeOrEngine::Runtime(Arc::new(wasix_runtime)), + "pg_dump", + 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}; pg_dump stderr: {stderr}") + } + }) + .context("run WASIX pg_dump")?; + } + + let sql = { + let _phase = timing::phase("pg_dump.read_output"); + match std::fs::read_to_string(fs_root.path().join("out.sql")) { + Ok(sql) => Ok(sql), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + let stdout = stdout.lock().expect("stdout capture poisoned"); + if stdout.is_empty() { + Err(err).with_context(|| { + format!( + "read pg_dump output {}", + fs_root.path().join("out.sql").display() + ) + }) + } else { + String::from_utf8(stdout.clone()).context("decode pg_dump stdout as UTF-8") + } + } + Err(err) => Err(err).with_context(|| { + format!( + "read pg_dump output {}", + fs_root.path().join("out.sql").display() + ) + }), + } + }?; + 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"); + psql_wasm_asset()? + }; + let engine = aot::headless_engine(); + let module = { + let _phase = timing::phase("psql.load_aot"); + aot::load_psql_module(&engine) + .context("load psql AOT artifact from oliphaunt-wasix-tools-aot-*")? + }; + 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') { + let body = line.trim_end_matches(['\r', '\n']); + if is_pg_dump_restrict_meta_command(body) { + continue; + } + stripped.push_str(line); + } + stripped +} + +fn is_pg_dump_restrict_meta_command(line: &str) -> bool { + let Some(key) = line + .strip_prefix("\\restrict ") + .or_else(|| line.strip_prefix("\\unrestrict ")) + else { + return false; + }; + !key.is_empty() && key.bytes().all(|byte| byte.is_ascii_alphanumeric()) +} + +const DIRECT_PG_DUMP_PORT: u16 = 65_432; +const DIRECT_PG_DUMP_SOCKET_BUFFER: usize = 8 * 1024 * 1024; +const DIRECT_PG_DUMP_LOCAL_PORT: u16 = 65_431; +const DIRECT_PG_DUMP_ADDR: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_PORT); +const DIRECT_PG_DUMP_LOCAL_ADDR: SocketAddr = + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_LOCAL_PORT); + +struct DirectPgDumpNetworking { + socket_tx: Mutex>>, +} + +impl DirectPgDumpNetworking { + fn new(socket_tx: SyncSender) -> Self { + Self { + socket_tx: Mutex::new(Some(socket_tx)), + } + } +} + +impl fmt::Debug for DirectPgDumpNetworking { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DirectPgDumpNetworking") + .finish_non_exhaustive() + } +} + +#[async_trait::async_trait] +impl VirtualNetworking for DirectPgDumpNetworking { + async fn connect_tcp( + &self, + addr: SocketAddr, + peer: SocketAddr, + ) -> virtual_net::Result> { + if peer != DIRECT_PG_DUMP_ADDR { + return Err(NetworkError::ConnectionRefused); + } + + let sender = self + .socket_tx + .lock() + .map_err(|_| NetworkError::IOError)? + .take() + .ok_or(NetworkError::ConnectionRefused)?; + let local = if addr.port() == 0 { + DIRECT_PG_DUMP_LOCAL_ADDR + } else { + addr + }; + let (guest, host) = TcpSocketHalf::channel(DIRECT_PG_DUMP_SOCKET_BUFFER, local, peer); + sender + .send(host) + .map_err(|_| NetworkError::ConnectionAborted)?; + Ok(Box::new(DirectPgDumpTcpSocket { + inner: guest, + first_write_ready_probe: true, + })) + } + + async fn resolve( + &self, + host: &str, + _port: Option, + _dns_server: Option, + ) -> virtual_net::Result> { + match host { + "localhost" | "127.0.0.1" => Ok(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]), + _ => Err(NetworkError::AddressNotAvailable), + } + } +} + +#[derive(Debug)] +struct DirectPgDumpTcpSocket { + inner: TcpSocketHalf, + // WASIX probes writability once while completing a blocking connect. + // `TcpSocketHalf` suppresses an immediate second write-ready poll until a + // write happens, but libpq polls again before its first StartupMessage. + // Keep the adapter level-triggered for that connect-to-first-write handoff. + first_write_ready_probe: bool, +} + +impl VirtualIoSource for DirectPgDumpTcpSocket { + fn remove_handler(&mut self) { + self.inner.remove_handler(); + } + + fn poll_read_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { + self.inner.poll_read_ready(cx) + } + + fn poll_write_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { + if self.first_write_ready_probe { + self.first_write_ready_probe = false; + return Poll::Ready(Ok(self.inner.send_buf_size().unwrap_or(1).max(1))); + } + self.inner.poll_write_ready(cx) + } +} + +impl VirtualSocket for DirectPgDumpTcpSocket { + fn set_ttl(&mut self, ttl: u32) -> virtual_net::Result<()> { + self.inner.set_ttl(ttl) + } + + fn ttl(&self) -> virtual_net::Result { + self.inner.ttl() + } + + fn addr_local(&self) -> virtual_net::Result { + self.inner.addr_local() + } + + fn status(&self) -> virtual_net::Result { + self.inner.status() + } + + fn set_handler( + &mut self, + handler: Box, + ) -> virtual_net::Result<()> { + self.inner.set_handler(handler) + } +} + +impl VirtualConnectedSocket for DirectPgDumpTcpSocket { + fn set_linger(&mut self, linger: Option) -> virtual_net::Result<()> { + self.inner.set_linger(linger) + } + + fn linger(&self) -> virtual_net::Result> { + self.inner.linger() + } + + fn try_send(&mut self, data: &[u8]) -> virtual_net::Result { + self.inner.try_send(data) + } + + fn try_flush(&mut self) -> virtual_net::Result<()> { + self.inner.try_flush() + } + + fn close(&mut self) -> virtual_net::Result<()> { + self.inner.close() + } + + fn try_recv(&mut self, buf: &mut [MaybeUninit], peek: bool) -> virtual_net::Result { + self.inner.try_recv(buf, peek) + } +} + +impl VirtualTcpSocket for DirectPgDumpTcpSocket { + fn set_recv_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { + self.inner.set_recv_buf_size(size) + } + + fn recv_buf_size(&self) -> virtual_net::Result { + self.inner.recv_buf_size() + } + + fn set_send_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { + self.inner.set_send_buf_size(size) + } + + fn send_buf_size(&self) -> virtual_net::Result { + self.inner.send_buf_size() + } + + fn set_nodelay(&mut self, reuse: bool) -> virtual_net::Result<()> { + self.inner.set_nodelay(reuse) + } + + fn nodelay(&self) -> virtual_net::Result { + self.inner.nodelay() + } + + fn set_keepalive(&mut self, keepalive: bool) -> virtual_net::Result<()> { + self.inner.set_keepalive(keepalive) + } + + fn keepalive(&self) -> virtual_net::Result { + self.inner.keepalive() + } + + fn set_dontroute(&mut self, keepalive: bool) -> virtual_net::Result<()> { + self.inner.set_dontroute(keepalive) + } + + fn dontroute(&self) -> virtual_net::Result { + self.inner.dontroute() + } + + fn addr_peer(&self) -> virtual_net::Result { + self.inner.addr_peer() + } + + fn shutdown(&mut self, how: Shutdown) -> virtual_net::Result<()> { + self.inner.shutdown(how) + } + + fn is_closed(&self) -> bool { + self.inner.is_closed() + } +} + +fn receive_direct_pg_dump_socket( + socket_rx: &Receiver, + runner: &thread::JoinHandle>, +) -> Result { + let started = Instant::now(); + loop { + match socket_rx.recv_timeout(Duration::from_millis(5)) { + Ok(socket) => return Ok(socket), + Err(mpsc::RecvTimeoutError::Timeout) => { + if runner.is_finished() { + bail!("pg_dump exited before opening the direct virtual protocol connection"); + } + if started.elapsed() > Duration::from_secs(30) { + bail!( + "timed out waiting for pg_dump to open the direct virtual protocol connection" + ); + } + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + bail!("pg_dump direct virtual networking channel closed before connect") + } + } + } +} + +#[derive(Debug)] +struct CaptureFile { + buffer: Arc>>, +} + +impl CaptureFile { + fn new(buffer: Arc>>) -> Self { + Self { buffer } + } +} + +impl VirtualFile for CaptureFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + self.buffer.lock().expect("capture lock poisoned").len() as u64 + } + + fn set_len(&mut self, _new_size: u64) -> Result<(), wasmer_wasix::FsError> { + Err(wasmer_wasix::FsError::PermissionDenied) + } + + fn unlink(&mut self) -> Result<(), wasmer_wasix::FsError> { + Ok(()) + } + + fn poll_read_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(8192)) + } +} + +impl AsyncRead for CaptureFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + _buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncWrite for CaptureFile { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + Poll::Ready(self.write(buf)) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl AsyncSeek for CaptureFile { + fn start_seek(self: Pin<&mut Self>, _position: std::io::SeekFrom) -> std::io::Result<()> { + Ok(()) + } + + fn poll_complete( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(0)) + } +} + +impl Read for CaptureFile { + fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { + Ok(0) + } +} + +impl Write for CaptureFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buffer + .lock() + .expect("capture lock poisoned") + .extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Seek for CaptureFile { + fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { + Ok(0) + } +} + +#[cfg(all(test, feature = "tools", feature = "extensions"))] +mod tests { + use super::*; + use crate::oliphaunt::Oliphaunt; + use crate::oliphaunt::extensions; + use crate::oliphaunt::server::OliphauntServer; + use serde_json::json; + use sqlx::{Connection, Executor, Row}; + + fn vector_extension_archive_is_embedded() -> bool { + assets::extension_archive(extensions::VECTOR.sql_name()).is_some() + } + + #[test] + fn pg_dump_options_reject_managed_args() { + for arg in [ + "-f", + "-f/tmp/out.sql", + "--file", + "--file=/tmp/out.sql", + "-F", + "-Fc", + "--format", + "--format=custom", + "-h", + "-hlocalhost", + "--host=localhost", + "-p", + "-p5432", + "--port=5432", + "-U", + "-Upostgres", + "--username=postgres", + "-d", + "-dpostgres", + "--dbname=postgres", + "-j", + "-j2", + "--jobs=2", + ] { + let err = PgDumpOptions::new() + .arg(arg) + .validate() + .expect_err("managed pg_dump arg should be rejected"); + assert!( + err.to_string().contains("conflicts with oliphaunt-wasix"), + "unexpected error for {arg}: {err:#}" + ); + } + } + + #[test] + fn pg_dump_options_allow_dump_shaping_args() -> Result<()> { + PgDumpOptions::new() + .args([ + "--schema-only", + "--quote-all-identifiers", + "-n", + "public", + "-t", + "dump_items", + ]) + .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 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\ + CREATE TABLE public.items(id integer);\n\ + \\copy public.items FROM stdin\n\ + \\unrestrict AbC123\n"; + assert_eq!( + strip_pg_dump_restrict_meta_commands(script.to_owned()), + "CREATE TABLE public.items(id integer);\n\\copy public.items FROM stdin\n" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn pg_dump_round_trip_plain_sql() -> Result<()> { + let server = OliphauntServer::temporary_tcp()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to Oliphaunt server")?; + conn.execute( + "CREATE TABLE dump_items(id INTEGER PRIMARY KEY, value TEXT); + CREATE INDEX dump_items_value_idx ON dump_items(value); + CREATE SEQUENCE dump_items_seq START WITH 10; + CREATE VIEW dump_item_values AS SELECT value FROM dump_items; + INSERT INTO dump_items(id, value) VALUES (1, 'alpha'), (2, 'beta'); + SELECT nextval('dump_items_seq');", + ) + .await + .context("seed pg_dump source data")?; + drop(conn); + + let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::default())?; + Ok((server, dump)) + }) + .await + .context("join pg_dump task")??; + + assert!(dump.contains("PostgreSQL database dump")); + assert!( + dump.contains("CREATE TABLE public.dump_items"), + "dump did not contain dump_items table DDL:\n{dump}" + ); + assert!(dump.contains("CREATE INDEX dump_items_value_idx")); + assert!(dump.contains("CREATE SEQUENCE public.dump_items_seq")); + assert!(dump.contains("CREATE VIEW public.dump_item_values")); + assert!(dump.contains("INSERT INTO")); + + let (server, schema_only) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + Ok((server, dump)) + }) + .await + .context("join schema-only pg_dump task")??; + assert!(schema_only.contains("CREATE TABLE public.dump_items")); + assert!( + !schema_only.contains("INSERT INTO public.dump_items"), + "schema-only dump unexpectedly contained data:\n{schema_only}" + ); + + let (server, quoted) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--quote-all-identifiers"))?; + Ok((server, dump)) + }) + .await + .context("join quoted pg_dump task")??; + assert!(quoted.contains("CREATE TABLE \"public\".\"dump_items\"")); + assert!(quoted.contains("INSERT INTO \"public\".\"dump_items\"")); + + let mut usable = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("reconnect after pg_dump")?; + let row = sqlx::query("SELECT count(*)::int4 AS count FROM public.dump_items") + .fetch_one(&mut usable) + .await + .context("server should remain usable after pg_dump")?; + assert_eq!(row.try_get::("count")?, 2); + usable.close().await?; + + server.shutdown()?; + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut restored = Oliphaunt::builder().temporary().open()?; + restored.exec(&dump, None).context("restore pg_dump SQL")?; + let result = restored.query( + "SELECT value FROM public.dump_items WHERE id = $1", + &[json!(2)], + None, + )?; + let value = result + .rows + .first() + .and_then(|row| row.get("value")) + .cloned(); + assert_eq!(value, Some(json!("beta"))); + let view = restored.query( + "SELECT count(*)::int AS count FROM public.dump_item_values", + &[], + None, + )?; + assert_eq!(view.rows[0]["count"], json!(2)); + let sequence = restored.query( + "SELECT nextval('public.dump_items_seq')::int AS next_value", + &[], + None, + )?; + assert_eq!(sequence.rows[0]["next_value"], json!(11)); + restored.close()?; + Ok(()) + }) + .await + .context("join restore task")??; + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn pg_dump_round_trip_vector_extension() -> Result<()> { + if !vector_extension_archive_is_embedded() { + eprintln!( + "skipping vector pg_dump smoke; base oliphaunt-wasix assets do not embed extension archives" + ); + return Ok(()); + } + + let server = OliphauntServer::builder() + .temporary() + .extension(extensions::VECTOR) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.database_url()) + .await + .context("connect to extension-enabled Oliphaunt server")?; + conn.execute( + "CREATE TABLE vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); + INSERT INTO vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", + ) + .await + .context("seed vector pg_dump source data")?; + drop(conn); + + let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { + let dump = server.dump_sql(PgDumpOptions::default())?; + Ok((server, dump)) + }) + .await + .context("join vector pg_dump task")??; + server.shutdown()?; + + assert!( + dump.contains("CREATE EXTENSION IF NOT EXISTS vector"), + "dump did not contain vector extension DDL:\n{dump}" + ); + assert!(dump.contains("CREATE TABLE public.vector_dump_items")); + assert!(dump.contains("'[1,2,3]'")); + + tokio::task::spawn_blocking(move || -> Result<()> { + let mut restored = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + restored + .exec(&dump, None) + .context("restore vector dump SQL")?; + let result = restored.query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance \ + FROM public.vector_dump_items WHERE id = $1", + &[json!(1)], + None, + )?; + let distance = result + .rows + .first() + .and_then(|row| row.get("distance")) + .and_then(|value| value.as_f64()); + assert_eq!(distance, Some(1.0)); + restored.close()?; + Ok(()) + }) + .await + .context("join vector restore task")??; + Ok(()) + } + + #[test] + fn direct_pg_dump_public_api_round_trip() -> Result<()> { + let mut db = Oliphaunt::temporary()?; + db.exec("CREATE TABLE direct_dump_items(value TEXT)", None)?; + db.exec("INSERT INTO direct_dump_items VALUES ('alpha')", None)?; + + let mismatched_database = db + .dump_sql(PgDumpOptions::new().database("other_database")) + .expect_err("direct pg_dump should reject database switching"); + assert!( + mismatched_database + .to_string() + .contains("already-open embedded backend database"), + "unexpected direct pg_dump database mismatch error: {mismatched_database:#}" + ); + + let dump = db.dump_sql(PgDumpOptions::new())?; + assert!(dump.contains("CREATE TABLE public.direct_dump_items")); + assert!(dump.contains("INSERT INTO")); + let source_still_usable = db.query( + "SELECT count(*)::int AS count FROM direct_dump_items", + &[], + None, + )?; + assert_eq!(source_still_usable.rows[0]["count"], json!(1)); + + let mut restored = Oliphaunt::temporary()?; + restored.exec(&dump, None)?; + let result = restored.query("SELECT value FROM public.direct_dump_items", &[], None)?; + assert_eq!(result.rows[0]["value"], json!("alpha")); + + restored.close()?; + db.close()?; + Ok(()) + } + + #[test] + fn direct_pg_dump_round_trip_vector_extension() -> Result<()> { + if !vector_extension_archive_is_embedded() { + eprintln!( + "skipping direct vector pg_dump smoke; base oliphaunt-wasix assets do not embed extension archives" + ); + return Ok(()); + } + + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.exec( + "CREATE TABLE direct_vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); + INSERT INTO direct_vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", + None, + )?; + + let dump = db.dump_sql(PgDumpOptions::new())?; + assert!(dump.contains("CREATE EXTENSION IF NOT EXISTS vector")); + assert!(dump.contains("CREATE TABLE public.direct_vector_dump_items")); + + let mut restored = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + restored.exec(&dump, None)?; + let result = restored.query( + "SELECT embedding <-> '[1,2,4]'::vector AS distance \ + FROM public.direct_vector_dump_items WHERE id = $1", + &[json!(1)], + None, + )?; + assert_eq!(result.rows[0]["distance"], json!(1.0)); + + restored.close()?; + db.close()?; + Ok(()) + } +} 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 new file mode 100644 index 00000000..1764a4e2 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs @@ -0,0 +1,2488 @@ +#[cfg(debug_assertions)] +use std::cell::Cell; +use std::collections::HashSet; +use std::fmt; +use std::fs; +use std::future::Future; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; + +use anyhow::{Context, Result, ensure}; +use sha2::{Digest, Sha256}; +use tokio::runtime::Runtime as TokioRuntime; +use tracing::{debug, warn}; +use wasmer::{Engine, Instance, Module, Store, TypedFunction, WasmTypeList}; +use wasmer_config::package::{PackageHash, PackageId}; +use wasmer_types::ModuleHash; +use wasmer_wasix::bin_factory::{BinaryPackage, BinaryPackageCommand, spawn_exec}; +use wasmer_wasix::fs::WasiFsRoot; +use wasmer_wasix::runners::wasi::{PackageOrHash, RuntimeOrEngine, WasiRunner}; +use wasmer_wasix::runtime::module_cache::ModuleCache; +use wasmer_wasix::runtime::module_cache::SharedCache; +use wasmer_wasix::runtime::task_manager::VirtualTaskManagerExt; +use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; +use wasmer_wasix::runtime::{PluggableRuntime, Runtime}; +use wasmer_wasix::virtual_fs::null_file::NullFile; +use wasmer_wasix::{WasiError, WasiFunctionEnv, virtual_fs}; +use webc::metadata::Command as WebcCommand; +use webc::metadata::annotations::{WASI_RUNNER_URI, Wasi}; + +use super::aot; +use super::base::{OliphauntPaths, RuntimeLayout}; +use super::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +use super::extensions::Extension; +use super::timing; + +mod stdio; +mod wasix_fs; + +pub(crate) use stdio::ProtocolStream; +use stdio::{ProtocolStdioAttachment, ProtocolStdioFile, TailCaptureFile, TailCaptureHandle}; +use wasix_fs::{ + EagerCopyOverlayFileSystem, host_filesystem, maybe_trace_filesystem, wasi_root_with_devices, +}; +pub use wasix_fs::{FsTraceSnapshot, fs_trace_snapshot, reset_fs_trace}; + +const OLIPHAUNT_EXE_PATH: &str = "/bin/oliphaunt"; +const PGDATA_DIR: &str = "/base"; +const ICU_DATA_DIR: &str = "/share/icu"; +const WASM_PREFIX: &str = "/"; +const RUNTIME_SIDE_MODULES: &[(&str, &str)] = &[ + ("plpgsql.so", "runtime-support:plpgsql"), + ("dict_snowball.so", "runtime-support:dict_snowball"), +]; +const OLIPHAUNT_EXIT_ALIVE: i32 = 99; +const POSTGRES_MAIN_LONGJMP: i32 = 100; + +const BACKEND_C_TIMINGS: &[(i32, &str)] = &[ + (1, "postgres.backend.c.main_pre"), + (2, "postgres.backend.c.restart_single_user_main"), + (3, "postgres.backend.c.async_single_user_main"), + (4, "postgres.backend.c.standalone_process"), + (5, "postgres.backend.c.guc_init"), + (6, "postgres.backend.c.switch_parse"), + (7, "postgres.backend.c.config_files"), + (8, "postgres.backend.c.data_dir_lock"), + (9, "postgres.backend.c.control_file"), + (10, "postgres.backend.c.preload_libraries"), + (11, "postgres.backend.c.shared_memory"), + (12, "postgres.backend.c.base_init"), + (13, "postgres.backend.c.init_postgres"), + (14, "postgres.backend.c.post_init"), + (15, "postgres.backend.c.message_contexts"), + (16, "postgres.backend.c.postmaster_environment"), + (17, "postgres.backend.c.init_proc_phase2"), + (18, "postgres.backend.c.startup_xlog"), + (19, "postgres.backend.c.relcache_catcache_init"), + (20, "postgres.backend.c.transaction_snapshot"), + (21, "postgres.backend.c.session_user"), + (22, "postgres.backend.c.database_lookup"), + (23, "postgres.backend.c.database_lock_recheck"), + (24, "postgres.backend.c.database_path"), + (25, "postgres.backend.c.relcache_phase3"), + (26, "postgres.backend.c.check_my_database"), + (27, "postgres.backend.c.startup_options"), + (28, "postgres.backend.c.process_settings"), + (29, "postgres.backend.c.session_initialization"), + (30, "postgres.backend.c.session_preload_libraries"), + (31, "postgres.backend.c.init_max_backends"), + (32, "postgres.backend.c.create_shared_memory"), + (33, "postgres.backend.c.init_process"), + (34, "postgres.backend.c.relation_cache_phase3"), + (35, "postgres.backend.c.initialize_acl"), + (36, "postgres.backend.c.exec_simple_query"), + (37, "postgres.backend.c.exec_start_xact"), + (38, "postgres.backend.c.exec_drop_unnamed"), + (39, "postgres.backend.c.exec_parse"), + (40, "postgres.backend.c.exec_snapshot"), + (41, "postgres.backend.c.exec_analyze_rewrite"), + (42, "postgres.backend.c.exec_plan"), + (43, "postgres.backend.c.exec_portal_start"), + (44, "postgres.backend.c.exec_dest_receiver"), + (45, "postgres.backend.c.exec_portal_run"), + (46, "postgres.backend.c.exec_finish_xact"), + (47, "postgres.backend.c.exec_command_counter"), + (48, "postgres.backend.c.exec_end_command"), + (49, "postgres.backend.c.heapam_tuple_update"), + (50, "postgres.backend.c.btree_doinsert"), + (51, "postgres.backend.c.xlog_insert_record"), + (52, "postgres.backend.c.btree_mkscankey"), + (53, "postgres.backend.c.btree_search_insert"), + (54, "postgres.backend.c.btree_check_unique"), + (55, "postgres.backend.c.btree_find_insertloc"), + (56, "postgres.backend.c.btree_insertonpg"), + (57, "postgres.backend.c.btree_split"), + (58, "postgres.backend.c.btree_binsrch_insert"), + (59, "postgres.backend.c.btree_compare"), + (60, "postgres.backend.c.heap_determine_columns"), + (61, "postgres.backend.c.heap_toast_update"), + (62, "postgres.backend.c.heap_get_buffer_for_tuple"), + (63, "postgres.backend.c.heap_put_tuple"), + (64, "postgres.backend.c.heap_log_update"), + (65, "postgres.backend.c.commit_record"), + (66, "postgres.backend.c.commit_procarray_end"), + (67, "postgres.backend.c.commit_callbacks"), + (68, "postgres.backend.c.commit_resource_before_locks"), + (69, "postgres.backend.c.commit_aio"), + (70, "postgres.backend.c.commit_buffers"), + (71, "postgres.backend.c.commit_relcache_typecache"), + (72, "postgres.backend.c.commit_inval"), + (73, "postgres.backend.c.commit_resource_locks"), + (74, "postgres.backend.c.commit_pending_deletes"), + (75, "postgres.backend.c.commit_notify"), + (76, "postgres.backend.c.commit_local_cleanup"), + (77, "postgres.backend.c.commit_memory"), + (78, "postgres.backend.c.commit_xlog_record"), + (79, "postgres.backend.c.commit_xlog_flush"), + (80, "postgres.backend.c.commit_clog_commit_tree"), + (81, "postgres.backend.c.commit_async_xact_lsn"), + (82, "postgres.backend.c.commit_async_commit_tree"), + (83, "postgres.backend.c.commit_sync_rep_wait"), + (84, "postgres.backend.c.xlog_write_pwrite"), + (85, "postgres.backend.c.xlog_write_pgstat_io"), + (86, "postgres.backend.c.xlog_flush_wait_insertions"), + (87, "postgres.backend.c.xlog_flush_wal_write_lock"), + (88, "postgres.backend.c.xlog_flush_xlog_write"), + (89, "postgres.backend.c.xlog_flush_walsnd_wakeup"), + (90, "postgres.backend.c.xlog_write_loop"), + (91, "postgres.backend.c.xlog_write_loop_scan"), + (92, "postgres.backend.c.xlog_write_before_pwrite"), + (93, "postgres.backend.c.xlog_write_after_pwrite"), + (94, "postgres.backend.c.xlog_write_fsync"), + (95, "postgres.backend.c.xlog_write_walsnd_request"), + (96, "postgres.backend.c.xlog_write_shared_status"), + (97, "postgres.backend.c.xlog_write_atomic_result"), + (98, "postgres.backend.c.xlog_write_loop_count"), + (99, "postgres.backend.c.xlog_write_group_count"), + (100, "postgres.backend.c.xlog_write_page_count"), + (101, "postgres.backend.c.xlog_write_pwrite_count"), + (102, "postgres.backend.c.xlog_write_pwrite_bytes"), + (103, "postgres.backend.c.xlog_write_request_bytes"), +]; + +static WASIX_PROCESS_RUNTIME: OnceLock, String>> = + OnceLock::new(); +static SEEDED_SIDE_MODULES: OnceLock>> = OnceLock::new(); + +struct WasixProcessRuntime { + tokio_runtime: Arc, + wasix_module_cache: Arc, + wasix_runtime: Arc, +} + +pub struct PostgresMod { + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + engine: Engine, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + tokio_runtime: Arc, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + wasix_module_cache: Arc, + _wasix_runtime: Arc, + store: Store, + _instance: Instance, + env: WasiFunctionEnv, + guest_allocator: GuestAllocator, + io: WasixOliphauntIo, + lifecycle: OliphauntLifecycleExports, + protocol: WasixProtocolExports, + protocol_stdio: Option, + protocol_stdio_file: ProtocolStdioFile, + wasi_stderr: TailCaptureHandle, + protocol_stdio_attachment: Option, + paths: OliphauntPaths, + pgdata_template_root: Option, + startup_config: StartupConfig, + startup_response: Option>, + cluster_ready: bool, + backend_started: bool, + started: bool, +} + +pub(crate) struct StartupProtocolResponse { + pub(crate) output: Vec, + pub(crate) accepted: bool, +} + +#[derive(Debug)] +pub(crate) struct StartupErrorResponse { + output: Vec, + summary: String, +} + +impl StartupErrorResponse { + fn new(output: Vec) -> Self { + let summary = summarize_protocol(&output); + Self { output, summary } + } + + pub(crate) fn output(&self) -> &[u8] { + &self.output + } +} + +impl fmt::Display for StartupErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Postgres startup returned a protocol ErrorResponse: {}", + self.summary + ) + } +} + +impl std::error::Error for StartupErrorResponse {} + +pub(crate) fn startup_error_response_output(err: &anyhow::Error) -> Option<&[u8]> { + err.downcast_ref::() + .map(StartupErrorResponse::output) +} + +pub(crate) enum ProtocolPumpOutcome { + Buffered(Vec), + Streamed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ProtocolTransportMode { + Buffered = 0, + Stream = 1, + Hybrid = 2, +} + +impl ProtocolTransportMode { + fn from_i32(value: i32) -> Result { + match value { + 0 => Ok(Self::Buffered), + 1 => Ok(Self::Stream), + 2 => Ok(Self::Hybrid), + other => anyhow::bail!("invalid WASIX protocol transport mode {other}"), + } + } +} + +struct OliphauntLifecycleExports { + wasi_start: TypedFunction<(), ()>, + set_force_host_error_recovery: Option>, + set_active: TypedFunction, + start_oliphaunt: TypedFunction<(), ()>, + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + run_atexit_funcs: Option>, + backend_timing_reset: Option>, + backend_timing_elapsed_us: Option>, +} + +struct WasixProtocolExports { + get_port: TypedFunction<(), i32>, + process_startup: TypedFunction<(i32, i32, i32), i32>, + send_conn_data: TypedFunction<(), ()>, + pq_flush: TypedFunction<(), ()>, + pq_buffer_remaining_data: TypedFunction<(), i32>, + main_loop: TypedFunction<(), ()>, + send_ready: TypedFunction<(), ()>, + recover_error: TypedFunction<(), ()>, +} + +#[derive(Clone)] +struct WasixProtocolStdioExports { + set_protocol_transport: TypedFunction, + protocol_stream_active: TypedFunction<(), i32>, +} + +struct WasixOliphauntIo { + input_reset: TypedFunction<(), i32>, + input_write: TypedFunction<(i32, i32), i32>, + input_available: TypedFunction<(), i32>, + output_reset: TypedFunction<(), i32>, + output_len: TypedFunction<(), i32>, + output_read: TypedFunction<(i32, i32), i32>, +} + +struct GuestAllocator { + malloc: TypedFunction, + free: TypedFunction, + #[cfg(debug_assertions)] + allocations: Cell, + #[cfg(debug_assertions)] + frees: Cell, +} + +impl PostgresMod { + pub(crate) fn preload_module(module_path: &std::path::Path) -> Result<()> { + let runtime_root = module_path + .parent() + .and_then(Path::parent) + .context("runtime module path must be under bin/oliphaunt")?; + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + runtime_root, + ) + } + + pub(crate) fn new_prepared( + paths: OliphauntPaths, + runtime_layout: RuntimeLayout, + ) -> Result { + Self::new_prepared_with_config( + paths, + runtime_layout, + PostgresConfig::default(), + StartupConfig::default(), + ) + } + + pub(crate) fn new_prepared_with_config( + paths: OliphauntPaths, + runtime_layout: RuntimeLayout, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + ) -> Result { + postgres_config.validate()?; + startup_config.validate()?; + ensure_runtime_dirs(&paths)?; + #[cfg(feature = "extensions")] + let runtime_root = runtime_layout.local_root.clone(); + let module_runtime_root = runtime_layout.module_root.clone(); + ensure!( + module_runtime_root.join("bin/oliphaunt").exists(), + "WASIX Oliphaunt executable not found at {}", + module_runtime_root.join("bin/oliphaunt").display() + ); + + let (engine, module) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + { + let _phase = timing::phase("wasix.preload_runtime_side_modules"); + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &module_runtime_root, + )?; + } + #[cfg(feature = "extensions")] + { + let _phase = timing::phase("wasix.preload_installed_extension_side_modules"); + preload_installed_extension_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_root, + )?; + } + let mut store = Store::new(engine.clone()); + + let _phase = timing::phase("wasix.instance_create"); + let (instance, env, protocol_stdio_file, wasi_stderr) = + instantiate_wasix_module(WasixInstantiateInput { + runtime: &process_runtime.tokio_runtime, + wasix_runtime: &process_runtime.wasix_runtime, + store: &mut store, + paths: &paths, + runtime_layout: &runtime_layout, + postgres_config: &postgres_config, + startup_config: &startup_config, + module: module.clone(), + })?; + seed_exported_c_string_value( + &mut store, + &instance, + &env, + "my_exec_path", + OLIPHAUNT_EXE_PATH, + )?; + + let (guest_allocator, io, lifecycle, protocol, protocol_stdio) = { + let _phase = timing::phase("wasix.export_load"); + let guest_allocator = GuestAllocator::load(&mut store, &instance)?; + let io = WasixOliphauntIo::new(&mut store, &instance)?; + ensure_integrated_oliphaunt_contract(&instance)?; + let lifecycle = OliphauntLifecycleExports::load(&mut store, &instance)?; + let protocol = WasixProtocolExports::load(&mut store, &instance)?; + let protocol_stdio = WasixProtocolStdioExports::load(&mut store, &instance)?; + (guest_allocator, io, lifecycle, protocol, protocol_stdio) + }; + + let pg = Self { + engine, + tokio_runtime: process_runtime.tokio_runtime.clone(), + wasix_module_cache: process_runtime.wasix_module_cache.clone(), + _wasix_runtime: process_runtime.wasix_runtime.clone(), + store, + _instance: instance, + env, + guest_allocator, + io, + lifecycle, + protocol, + protocol_stdio, + protocol_stdio_file, + wasi_stderr, + protocol_stdio_attachment: None, + paths, + pgdata_template_root: runtime_layout.pgdata_template_root.clone(), + startup_config, + startup_response: None, + cluster_ready: false, + backend_started: false, + started: false, + }; + Ok(pg) + } + + pub fn paths(&self) -> &OliphauntPaths { + &self.paths + } + + pub(crate) fn pgdata_template_root(&self) -> Option<&Path> { + self.pgdata_template_root.as_deref() + } + + #[cfg(debug_assertions)] + pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { + self.guest_allocator.allocation_counts() + } + + pub fn ensure_cluster(&mut self) -> Result<()> { + self.initialize_cluster()?; + self.start_backend() + } + + pub fn initialize_cluster(&mut self) -> Result<()> { + if self.cluster_ready { + return Ok(()); + } + + ensure!( + self.paths.is_cluster_initialized(), + "PGDATA is not initialized; install the WASIX runtime assets and PGDATA template before opening" + ); + self.cluster_ready = true; + Ok(()) + } + + fn start_backend(&mut self) -> Result<()> { + if self.backend_started { + return Ok(()); + } + let _phase = timing::phase("postgres.backend_start"); + self.reset_backend_c_timings()?; + self.configure_host_error_recovery()?; + { + let _phase = timing::phase("postgres.backend_start.set_active"); + self.lifecycle + .set_active + .call(&mut self.store, 1) + .context("oliphaunt_wasix_set_active(1)")?; + } + { + let _phase = timing::phase("postgres.backend_start.single_user_main"); + match self.lifecycle.wasi_start.call(&mut self.store) { + Ok(()) => {} + Err(err) if runtime_error_exit_code(&err) == Some(OLIPHAUNT_EXIT_ALIVE) => {} + Err(err) => { + return self.startup_failure(err, "_start Oliphaunt single-user backend"); + } + } + } + if let Err(err) = self.lifecycle.start_oliphaunt.call(&mut self.store) { + return self.startup_failure(err, "oliphaunt_wasix_start"); + } + self.record_backend_c_timings()?; + self.backend_started = true; + Ok(()) + } + + fn configure_host_error_recovery(&mut self) -> Result<()> { + let force = host_requires_process_exit_error_recovery(); + let Some(set_force) = &self.lifecycle.set_force_host_error_recovery else { + if force { + anyhow::bail!( + "WASIX runtime does not export oliphaunt_wasix_set_force_host_error_recovery required by this host" + ); + } + return Ok(()); + }; + + set_force + .call(&mut self.store, i32::from(force)) + .context("oliphaunt_wasix_set_force_host_error_recovery")?; + Ok(()) + } + + fn startup_failure(&mut self, err: wasmer::RuntimeError, context: &str) -> Result<()> { + if let Some(output) = self.take_startup_output_after_failure() { + if protocol_response_contains_error(&output) { + return Err(StartupErrorResponse::new(output).into()); + } + return Err(err).context(format!( + "{context}{}", + self.startup_failure_detail(Some(&output)) + )); + } + Err(err).context(format!("{context}{}", self.startup_failure_detail(None))) + } + + fn take_startup_output_after_failure(&mut self) -> Option> { + let _ = self.protocol.pq_flush.call(&mut self.store); + match self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator) + { + Ok(output) if !output.is_empty() => Some(output), + Ok(_) => None, + Err(err) => { + warn!("failed to read startup output after backend failure: {err}"); + None + } + } + } + + fn startup_failure_detail(&self, output: Option<&[u8]>) -> String { + let mut detail = String::new(); + let stderr = self.wasi_stderr.text(); + if !stderr.trim().is_empty() { + detail.push_str("\nWASIX stderr tail:\n"); + detail.push_str(stderr.trim_end()); + } + if let Some(output) = output { + detail.push_str("\nWASIX startup output tail:\n"); + detail.push_str(&format_output_tail(output)); + } + detail + } + + #[cfg_attr(not(feature = "extensions"), allow(dead_code))] + pub(crate) fn shutdown_backend(&mut self) -> Result<()> { + let _phase = timing::phase("postgres.backend_shutdown"); + self.lifecycle + .set_active + .call(&mut self.store, 0) + .context("oliphaunt_wasix_set_active(0)")?; + if let Some(run_atexit_funcs) = &self.lifecycle.run_atexit_funcs { + run_atexit_funcs + .call(&mut self.store) + .context("oliphaunt_wasix_run_atexit_funcs")?; + } + self.backend_started = false; + self.started = false; + self.startup_response = None; + self.cluster_ready = false; + Ok(()) + } + + fn record_backend_c_timings(&mut self) -> Result<()> { + let Some(elapsed) = &self.lifecycle.backend_timing_elapsed_us else { + return Ok(()); + }; + + for &(id, name) in BACKEND_C_TIMINGS { + let elapsed_micros = elapsed + .call(&mut self.store, id) + .with_context(|| format!("oliphaunt_wasix_backend_timing_elapsed_us({id})"))?; + if elapsed_micros > 0 { + timing::record_phase_timing(name, Duration::from_micros(elapsed_micros as u64)); + } + } + Ok(()) + } + + fn reset_backend_c_timings(&mut self) -> Result<()> { + let Some(reset) = &self.lifecycle.backend_timing_reset else { + return Ok(()); + }; + + reset + .call(&mut self.store) + .context("oliphaunt_wasix_backend_timing_reset")?; + Ok(()) + } + + #[cfg(feature = "extensions")] + pub fn preload_extension_module(&self, extension: Extension) -> Result<()> { + let runtime_root = self.paths.runtime_root(); + for module in extension.native_support_modules() { + seed_extension_side_module( + &self.tokio_runtime, + &self.engine, + &self.wasix_module_cache, + &runtime_root, + module.runtime_path(), + module.aot_name(), + &format!( + "extension '{}' support module '{}'", + extension.sql_name(), + module.runtime_path() + ), + )?; + } + + let Some(module_file) = extension.native_module_file() else { + return Ok(()); + }; + seed_extension_side_module( + &self.tokio_runtime, + &self.engine, + &self.wasix_module_cache, + &runtime_root, + &format!("lib/postgresql/{module_file}"), + extension.aot_name(), + &format!("extension '{}'", extension.sql_name()), + )?; + Ok(()) + } + + #[cfg(feature = "extensions")] + pub(crate) fn preload_extension_module_from_paths( + paths: &OliphauntPaths, + extension: Extension, + ) -> Result<()> { + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + let runtime_root = paths.runtime_root(); + for module in extension.native_support_modules() { + seed_extension_side_module( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_root, + module.runtime_path(), + module.aot_name(), + &format!( + "extension '{}' support module '{}'", + extension.sql_name(), + module.runtime_path() + ), + )?; + } + + let Some(module_file) = extension.native_module_file() else { + return Ok(()); + }; + seed_extension_side_module( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_root, + &format!("lib/postgresql/{module_file}"), + extension.aot_name(), + &format!("extension '{}'", extension.sql_name()), + ) + } + + pub(crate) fn run_split_initdb( + paths: &OliphauntPaths, + runtime_layout: &RuntimeLayout, + ) -> Result<()> { + run_split_initdb(paths, runtime_layout) + } + + pub fn send_protocol(&mut self, payload: &[u8]) -> Result> { + { + let _phase = timing::phase("postgres.protocol.ensure_started"); + self.start_protocol()?; + } + if payload.is_empty() { + return Ok(Vec::new()); + } + self.send_protocol_inner(payload) + } + + pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> + where + S: ProtocolStream + 'static, + { + ensure!( + self.protocol_stdio.is_some(), + "WASIX runtime does not export protocol stream transport" + ); + if self.protocol_stdio_attachment.is_none() { + let attachment = self.protocol_stdio_file.attach(stream)?; + self.protocol_stdio_attachment = Some(attachment); + } + Ok(()) + } + + pub(crate) fn set_protocol_stream_prefix(&mut self, prefix: Vec) -> Result<()> { + self.protocol_stdio_file.set_prefix(prefix) + } + + pub(crate) fn clear_protocol_stream_prefix(&mut self) -> Result<()> { + self.protocol_stdio_file.clear_prefix() + } + + pub(crate) fn send_protocol_pump( + &mut self, + payload: &[u8], + continuation_prefix: impl FnOnce() -> Vec, + ) -> Result { + { + let _phase = timing::phase("postgres.protocol.ensure_started"); + self.start_protocol()?; + } + if payload.is_empty() { + return Ok(ProtocolPumpOutcome::Buffered(Vec::new())); + } + ensure!( + self.protocol_stdio_attachment.is_some(), + "WASIX protocol pump requires an attached stream" + ); + let previous_mode = self.set_protocol_transport(ProtocolTransportMode::Hybrid)?; + ensure!( + previous_mode == ProtocolTransportMode::Buffered, + "WASIX protocol transport was not buffered before protocol pump" + ); + let result = self.send_protocol_inner(payload); + let active = self.protocol_stream_active().unwrap_or(false); + if active { + self.set_protocol_stream_prefix(continuation_prefix())?; + let stream_result = result.and_then(|_| self.serve_protocol_stream_inner()); + let restore_result = self.restore_protocol_transport(previous_mode); + let clear_result = self.clear_protocol_stream_prefix(); + stream_result.and(restore_result).and(clear_result)?; + Ok(ProtocolPumpOutcome::Streamed) + } else { + let output = result; + let restore_result = self.restore_protocol_transport(previous_mode); + restore_result?; + let output = output?; + Ok(ProtocolPumpOutcome::Buffered(output)) + } + } + + fn send_protocol_inner(&mut self, payload: &[u8]) -> Result> { + self.reset_backend_c_timings()?; + + { + let _phase = timing::phase("postgres.protocol.input_reset"); + self.io.reset(&mut self.store)?; + } + { + let _phase = timing::phase("postgres.protocol.input_write"); + self.io + .push_input(&mut self.store, &self.env, &self.guest_allocator, payload)?; + } + + { + let _phase = timing::phase("postgres.protocol.dispatch_buffer"); + let max_attempts = (payload.len() / 5).saturating_add(2).max(1); + let mut attempts = 0usize; + let mut recovered_protocol_error = false; + while self.protocol_input_remaining()? > 0 { + attempts += 1; + ensure!( + attempts <= max_attempts, + "Postgres protocol dispatch did not drain buffered input after {attempts} attempts" + ); + if let Err(err) = self.protocol.main_loop.call(&mut self.store) { + if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) { + debug!( + "PostgresMainLoopOnce used host longjmp fallback; recovering protocol error" + ); + self.recover_protocol_error(payload.len())?; + recovered_protocol_error = true; + } else if is_wasm_uncaught_exception(&err) { + debug!( + "PostgresMainLoopOnce trapped for PostgreSQL error; recovering protocol state: {err}" + ); + self.recover_protocol_error(payload.len())?; + recovered_protocol_error = true; + } else { + warn!("PostgresMainLoopOnce trapped; attempting protocol recovery: {err}"); + self.recover_protocol_error(payload.len())?; + recovered_protocol_error = true; + } + } + } + + { + let _phase = timing::phase("postgres.protocol.send_ready"); + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary")?; + } + { + let _phase = timing::phase("postgres.protocol.pq_flush"); + self.protocol + .pq_flush + .call(&mut self.store) + .context("oliphaunt_wasix_pq_flush after protocol buffer")?; + } + let output = { + let _phase = timing::phase("postgres.protocol.output_read"); + self.io + .take_output(&mut self.store, &self.env, &self.guest_allocator) + .context("take backend output after protocol buffer")? + }; + if !recovered_protocol_error && protocol_response_contains_error(&output) { + self.recover_non_trapping_protocol_error()?; + } + self.record_backend_c_timings()?; + Ok(output) + } + } + + pub(crate) fn supports_streaming_protocol(&self) -> bool { + self.protocol_stdio.is_some() + } + + fn serve_protocol_stream_inner(&mut self) -> Result<()> { + self.reset_backend_c_timings()?; + loop { + if let Err(err) = self.protocol.main_loop.call(&mut self.store) { + if runtime_error_exit_code(&err) == Some(OLIPHAUNT_EXIT_ALIVE) { + break; + } + if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) { + debug!( + "PostgresMainLoopOnce used host longjmp fallback while serving streaming protocol" + ); + self.protocol.recover_error.call(&mut self.store).context( + "recover Postgres main-loop error while serving streaming protocol", + )?; + } else if is_wasm_uncaught_exception(&err) { + debug!( + "PostgresMainLoopOnce trapped for PostgreSQL error while serving streaming protocol: {err}" + ); + self.protocol.recover_error.call(&mut self.store).context( + "recover Postgres main-loop error while serving streaming protocol", + )?; + } else { + return Err(err).context("PostgresMainLoopOnce streaming protocol"); + } + } + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary streaming protocol")?; + self.protocol + .pq_flush + .call(&mut self.store) + .context("oliphaunt_wasix_pq_flush streaming protocol")?; + } + self.record_backend_c_timings()?; + Ok(()) + } + + fn set_protocol_transport( + &mut self, + mode: ProtocolTransportMode, + ) -> Result { + let stdio = self + .protocol_stdio + .as_ref() + .context("WASIX runtime does not export protocol stdio switching")?; + let previous = stdio + .set_protocol_transport + .call(&mut self.store, mode as i32) + .context("oliphaunt_wasix_set_protocol_transport")?; + ProtocolTransportMode::from_i32(previous) + } + + fn restore_protocol_transport(&mut self, previous_mode: ProtocolTransportMode) -> Result<()> { + let current = self.set_protocol_transport(previous_mode)?; + ensure!( + current != previous_mode, + "oliphaunt_wasix_set_protocol_transport restore observed unchanged current mode" + ); + Ok(()) + } + + fn protocol_stream_active(&mut self) -> Result { + let stdio = self + .protocol_stdio + .as_ref() + .context("WASIX runtime does not export protocol stream state")?; + Ok(stdio + .protocol_stream_active + .call(&mut self.store) + .context("oliphaunt_wasix_protocol_stream_active")? + != 0) + } + + fn start_protocol(&mut self) -> Result<()> { + if self.started { + return Ok(()); + } + let startup = startup_packet(&self.startup_config.username, &self.startup_config.database); + let response = self.start_protocol_with_startup_packet(&startup)?; + ensure!( + response.accepted, + "Oliphaunt WASIX startup packet was rejected: {}", + summarize_protocol(&response.output) + ); + ensure!( + !protocol_response_contains_error(&response.output), + "Oliphaunt WASIX startup packet returned an error: {}", + summarize_protocol(&response.output) + ); + Ok(()) + } + + pub(crate) fn start_protocol_with_startup_packet( + &mut self, + startup: &[u8], + ) -> Result { + self.ensure_cluster()?; + ensure!( + !self.started, + "Oliphaunt WASIX protocol startup has already completed for this backend" + ); + + let _phase = timing::phase("postgres.startup_packet"); + { + let _phase = timing::phase("postgres.startup_packet.input_reset"); + self.io.reset(&mut self.store)?; + } + { + let _phase = timing::phase("postgres.startup_packet.input_write"); + self.io + .push_input(&mut self.store, &self.env, &self.guest_allocator, startup)?; + } + + // The upstream lifecycle is already running by this point. These calls + // open the Rust-owned direct wire-protocol transport on top of that + // lifecycle; they must not grow into a second backend lifecycle. + let port = { + let _phase = timing::phase("postgres.startup_packet.get_port"); + self.protocol + .get_port + .call(&mut self.store) + .context("oliphaunt_wasix_get_proc_port")? + }; + ensure!(port > 0, "oliphaunt_wasix_get_proc_port returned null"); + + let status = { + let _phase = timing::phase("postgres.startup_packet.process_startup"); + self.protocol + .process_startup + .call(&mut self.store, port, 1, 1) + .context("ProcessStartupPacket")? + }; + if status != 0 { + let _ = self.protocol.pq_flush.call(&mut self.store); + let output = self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator)?; + return Ok(StartupProtocolResponse { + output, + accepted: false, + }); + } + let output = { + let _phase = timing::phase("postgres.startup_packet.ready"); + { + let _phase = timing::phase("postgres.startup_packet.send_conn_data"); + self.protocol + .send_conn_data + .call(&mut self.store) + .context("oliphaunt_wasix_send_conn_data")?; + } + { + let _phase = timing::phase("postgres.startup_packet.pq_flush"); + self.protocol + .pq_flush + .call(&mut self.store) + .context("oliphaunt_wasix_pq_flush after startup")?; + } + { + let _phase = timing::phase("postgres.startup_packet.output_read"); + self.io + .take_output(&mut self.store, &self.env, &self.guest_allocator)? + } + }; + self.started = true; + self.startup_response = Some(output.clone()); + Ok(StartupProtocolResponse { + output, + accepted: true, + }) + } + + #[cfg(feature = "tools")] + pub(crate) fn existing_startup_response(&self) -> Option> { + self.startup_response.clone() + } + + fn recover_protocol_error(&mut self, payload_len: usize) -> Result<()> { + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp after protocol trap")?; + + // PostgreSQL extended-query errors skip messages until Sync. If Sync was + // already in this host buffer, re-enter the loop to drain it and produce + // ReadyForQuery from PostgreSQL rather than inventing one in Rust. + let max_drain_attempts = (payload_len / 5).saturating_add(2).max(1); + let mut drain_attempts = 0usize; + while self.protocol_input_remaining()? > 0 { + drain_attempts += 1; + ensure!( + drain_attempts <= max_drain_attempts, + "Postgres protocol recovery did not drain buffered input after {drain_attempts} attempts" + ); + if let Err(drain_err) = self.protocol.main_loop.call(&mut self.store) { + if runtime_error_exit_code(&drain_err) == Some(POSTGRES_MAIN_LONGJMP) + || is_wasm_uncaught_exception(&drain_err) + { + debug!( + "PostgresMainLoopOnce trapped while draining after PostgreSQL error recovery: {drain_err}" + ); + } else { + warn!( + "PostgresMainLoopOnce trapped while draining after recovery: {drain_err}" + ); + } + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp while draining after protocol trap")?; + } + } + Ok(()) + } + + fn recover_non_trapping_protocol_error(&mut self) -> Result<()> { + self.protocol + .recover_error + .call(&mut self.store) + .context("PostgresMainLongJmp after backend ErrorResponse")?; + self.protocol + .send_ready + .call(&mut self.store) + .context("PostgresSendReadyForQueryIfNecessary after backend ErrorResponse")?; + self.protocol + .pq_flush + .call(&mut self.store) + .context("oliphaunt_wasix_pq_flush after backend ErrorResponse recovery")?; + let _ = self + .io + .take_output(&mut self.store, &self.env, &self.guest_allocator)?; + Ok(()) + } + + fn protocol_input_remaining(&mut self) -> Result { + let host_remaining = self.io.available(&mut self.store)?; + if host_remaining > 0 { + return Ok(host_remaining); + } + self.protocol + .pq_buffer_remaining_data + .call(&mut self.store) + .context("pq_buffer_remaining_data") + } +} + +fn process_wasix_runtime(engine: &Engine) -> Result> { + WASIX_PROCESS_RUNTIME + .get_or_init(|| { + let _phase = timing::phase("wasix.runtime_construct"); + let tokio_runtime = { + let _phase = timing::phase("wasix.runtime_construct.tokio"); + Arc::new( + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create Tokio runtime for Wasmer/WASIX filesystem") + .map_err(|err| format!("{err:#}"))?, + ) + }; + let wasix_module_cache = { + let _phase = timing::phase("wasix.runtime_construct.module_cache"); + Arc::new(SharedCache::new()) + }; + let wasix_runtime = { + let _phase = timing::phase("wasix.runtime_construct.pluggable_runtime"); + build_wasix_runtime(&tokio_runtime, engine, wasix_module_cache.clone()) + }; + + Ok(Arc::new(WasixProcessRuntime { + tokio_runtime, + wasix_module_cache, + wasix_runtime, + })) + }) + .clone() + .map_err(|message| anyhow::anyhow!(message)) +} + +struct WasixInstantiateInput<'a> { + runtime: &'a TokioRuntime, + wasix_runtime: &'a Arc, + store: &'a mut Store, + paths: &'a OliphauntPaths, + runtime_layout: &'a RuntimeLayout, + postgres_config: &'a PostgresConfig, + startup_config: &'a StartupConfig, + module: Module, +} + +fn instantiate_wasix_module( + input: WasixInstantiateInput<'_>, +) -> Result<( + Instance, + WasiFunctionEnv, + ProtocolStdioFile, + TailCaptureHandle, +)> { + let _phase = timing::phase("wasix.instantiate"); + let _guard = input.runtime.enter(); + let root_fs = { + let _phase = timing::phase("wasix.instantiate.root_fs"); + if input.runtime_layout.uses_shared_overlay() { + mountfs_overlay_wasi_root(input.paths, input.runtime_layout)? + } else { + host_wasi_root(&input.paths.runtime_root())? + } + }; + + let mut runner = WasiRunner::new(); + runner.with_current_dir("/"); + let protocol_stdio_file = ProtocolStdioFile::new(); + let (stderr_file, stderr_capture) = TailCaptureFile::new(16 * 1024); + runner.with_stdin(Box::new(protocol_stdio_file.clone())); + runner.with_stdout(Box::new(protocol_stdio_file.clone())); + runner.with_stderr(Box::new(stderr_file)); + let wasi = Wasi::new(OLIPHAUNT_EXE_PATH); + let mut builder = { + let _phase = timing::phase("wasix.instantiate.prepare_env"); + runner + .prepare_webc_env( + OLIPHAUNT_EXE_PATH, + &wasi, + PackageOrHash::Hash(ModuleHash::random()), + RuntimeOrEngine::Runtime(input.wasix_runtime.clone()), + Some(root_fs), + ) + .context("prepare Wasmer/WASIX runner environment")? + }; + { + let _phase = timing::phase("wasix.instantiate.pgdata_preopen"); + add_pgdata_preopen(&mut builder)?; + } + add_oliphaunt_env( + &mut builder, + input.startup_config, + input.paths, + input.runtime_layout, + ); + add_oliphaunt_args(&mut builder, input.postgres_config, input.startup_config)?; + + { + let _phase = timing::phase("wasix.instantiate.module"); + builder + .instantiate(input.module, input.store) + .context("instantiate Oliphaunt WASIX module") + .map(|(instance, env)| (instance, env, protocol_stdio_file, stderr_capture)) + } +} + +fn add_pgdata_preopen(builder: &mut wasmer_wasix::WasiEnvBuilder) -> Result<()> { + builder + .add_preopen_build(|preopen| { + preopen + .directory(PGDATA_DIR) + .alias(PGDATA_DIR.trim_start_matches('/')) + .read(true) + .write(true) + .create(true) + }) + .context("preopen PGDATA directory for Wasmer/WASIX")?; + Ok(()) +} + +fn host_wasi_root(runtime_root: &Path) -> Result { + let root = maybe_trace_filesystem(host_filesystem(runtime_root)?); + Ok(WasiFsRoot::from_filesystem(wasi_root_with_devices(root)?)) +} + +fn mountfs_overlay_wasi_root( + paths: &OliphauntPaths, + runtime_layout: &RuntimeLayout, +) -> Result { + let _phase = timing::phase("wasix.mountfs_overlay_construct"); + let runtime_root = paths.runtime_root(); + let primary = + virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem(&runtime_root)?)); + let secondary = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &runtime_layout.module_root, + )?)); + let overlay = Arc::new(virtual_fs::OverlayFileSystem::new(primary, [secondary])); + let root: Arc = + if let Some(pgdata) = pgdata_overlay_filesystem(paths, runtime_layout)? { + wasi_root_with_pgdata_mount(overlay, pgdata)? + } else { + overlay + }; + + Ok(WasiFsRoot::from_filesystem(wasi_root_with_devices(root)?)) +} + +fn pgdata_overlay_filesystem( + paths: &OliphauntPaths, + runtime_layout: &RuntimeLayout, +) -> Result>> { + if let Some(pgdata_template_root) = &runtime_layout.pgdata_template_root { + let fs = + EagerCopyOverlayFileSystem::new(paths.pgdata.clone(), pgdata_template_root.clone())?; + return Ok(Some(maybe_trace_filesystem(Arc::new(fs)))); + } + Ok(None) +} + +fn wasi_root_with_pgdata_mount( + root: Arc, + pgdata: Arc, +) -> virtual_fs::Result> { + let mount = virtual_fs::MountFileSystem::new(); + mount.mount(Path::new("/"), root)?; + mount.mount(Path::new(PGDATA_DIR), pgdata)?; + Ok(Arc::new(mount)) +} + +fn build_wasix_runtime( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: Arc, +) -> Arc { + let _guard = runtime.enter(); + let task_manager = Arc::new(TokioTaskManager::new(runtime.handle().clone())); + let mut wasix_runtime = PluggableRuntime::new(task_manager); + wasix_runtime.set_engine(engine.clone()); + wasix_runtime.set_module_cache(module_cache); + Arc::new(wasix_runtime) +} + +fn run_split_initdb(paths: &OliphauntPaths, runtime_layout: &RuntimeLayout) -> Result<()> { + let _phase = timing::phase("initdb.split_wasix"); + let initdb_module = runtime_layout.module_root.join("bin/initdb"); + let postgres_module = runtime_layout.module_root.join("bin/postgres"); + ensure!( + initdb_module.exists(), + "split WASIX initdb module is not installed at {}; regenerate assets with `xtask assets template`", + initdb_module.display() + ); + ensure!( + postgres_module.exists(), + "WASIX postgres module is not installed at {}", + postgres_module.display() + ); + + fs::create_dir_all(&paths.pgdata) + .with_context(|| format!("create fresh PGDATA {}", paths.pgdata.display()))?; + + let (engine, _) = aot::load_runtime_module()?; + let process_runtime = process_wasix_runtime(&engine)?; + seed_wasix_module_cache( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &initdb_module, + "tool:initdb", + "split initdb command", + )?; + seed_wasix_module_cache( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &postgres_module, + "runtime:oliphaunt", + "initdb child postgres command", + )?; + preload_runtime_side_modules( + &process_runtime.tokio_runtime, + &engine, + &process_runtime.wasix_module_cache, + &runtime_layout.module_root, + )?; + // initdb execs child postgres commands; isolate that command process tree + // from concurrently running backends while keeping the module cache shared. + let initdb_runtime = build_wasix_runtime( + &process_runtime.tokio_runtime, + &engine, + process_runtime.wasix_module_cache.clone(), + ); + + let package = split_initdb_binary_package(&initdb_module, &postgres_module)?; + let root_fs = split_initdb_root_filesystem(paths, runtime_layout)?; + root_fs + .read_dir(Path::new(PGDATA_DIR)) + .with_context(|| format!("verify split initdb {PGDATA_DIR} mount"))?; + + let (stdout_file, stdout_capture) = TailCaptureFile::new(8 * 1024); + let (stderr_file, stderr_capture) = TailCaptureFile::new(8 * 1024); + + let mut runner = WasiRunner::new(); + runner + .with_current_dir("/") + .with_injected_package(package.clone()) + .with_args(split_initdb_args()) + .with_envs([ + ("PGDATA", PGDATA_DIR), + ("PGSYSCONFDIR", PGDATA_DIR), + ("HOME", "/home/postgres"), + ("USER", "postgres"), + ("LOGNAME", "postgres"), + ("PGCLIENTENCODING", "UTF8"), + ("PATH", "/bin"), + ("LC_CTYPE", "C.UTF-8"), + ("TZ", "UTC"), + ("PGTZ", "UTC"), + ("PG_COLOR", "never"), + ]) + .with_stdin(Box::::default()) + .with_stdout(Box::new(stdout_file)) + .with_stderr(Box::new(stderr_file)); + if wasix_icu_data_is_available(paths, runtime_layout) { + runner.with_envs([("ICU_DATA", ICU_DATA_DIR)]); + } + + { + let _phase = timing::phase("initdb.split_wasix.run_command"); + let result = + run_package_command_with_root(&runner, "initdb", &package, initdb_runtime, root_fs); + if let Err(err) = result { + let stdout = stdout_capture.text(); + let stderr = stderr_capture.text(); + let diagnostics = split_initdb_diagnostics(paths, runtime_layout); + return Err(err).with_context(|| { + format!( + "run split WASIX initdb\n{}\ninitdb stdout:\n{}\ninitdb stderr:\n{}", + diagnostics, + if stdout.trim().is_empty() { + "" + } else { + stdout.trim_end() + }, + if stderr.trim().is_empty() { + "" + } else { + stderr.trim_end() + } + ) + }); + } + } + Ok(()) +} + +fn split_initdb_root_filesystem( + paths: &OliphauntPaths, + runtime_layout: &RuntimeLayout, +) -> Result> { + let root: Arc = + if runtime_layout.uses_shared_overlay() { + let upper = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &paths.runtime_root(), + )?)); + let lower = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( + &runtime_layout.module_root, + )?)); + Arc::new(virtual_fs::OverlayFileSystem::new(upper, [lower])) + } else { + maybe_trace_filesystem(host_filesystem(&paths.runtime_root())?) + }; + + let pgdata = maybe_trace_filesystem(host_filesystem(&paths.pgdata)?); + // initdb execs a child postgres command during bootstrap. Keep PGDATA inside + // the root filesystem view so both commands inherit the same /base mount. + let root = wasi_root_with_pgdata_mount(root, pgdata)?; + // Wasmer's runner normally starts from a temporary root that provides WASIX + // device files. Keep the real runtime/PGDATA root mounted for database + // writes, but route device paths such as /dev/urandom to virtual devices. + Ok(wasi_root_with_devices(root)?) +} + +fn run_package_command_with_root( + runner: &WasiRunner, + command_name: &str, + package: &BinaryPackage, + runtime: Arc, + root_fs: Arc, +) -> Result<()> { + let cmd = package.get_command(command_name).with_context(|| { + format!("split initdb package does not contain command {command_name:?}") + })?; + let wasi = cmd + .metadata() + .annotation("wasi")? + .unwrap_or_else(|| Wasi::new(command_name)); + let exec_name = wasi.exec_name.as_deref().unwrap_or(command_name); + let mut builder = runner + .prepare_webc_env( + exec_name, + &wasi, + PackageOrHash::Package(package), + RuntimeOrEngine::Runtime(runtime), + Some(WasiFsRoot::from_filesystem(root_fs)), + ) + .with_context(|| format!("prepare WASIX command environment for {command_name:?}"))?; + add_pgdata_preopen(&mut builder)?; + + let env = builder.build()?; + let runtime = env.runtime.clone(); + let tasks = runtime.task_manager().clone(); + let package = package.clone(); + let command_name = command_name.to_owned(); + let exit_code = tasks.spawn_and_block_on(async move { + let mut task_handle = spawn_exec(package, &command_name, env, &runtime) + .await + .with_context(|| format!("spawn WASIX command {command_name:?}"))?; + task_handle + .wait_finished() + .await + .map_err(|err| anyhow::anyhow!("{err}")) + .with_context(|| format!("wait for WASIX command {command_name:?}")) + })??; + + ensure!(exit_code.raw() == 0, "WASI exited with code: {exit_code}"); + Ok(()) +} + +fn split_initdb_diagnostics(paths: &OliphauntPaths, runtime_layout: &RuntimeLayout) -> String { + let pgdata_parent = paths.pgdata.parent().unwrap_or(&paths.pgdata); + format!( + "initdb diagnostics:\n layout_kind={:?}\n pgdata_host={}\n pgdata_parent={}\n runtime_root={}\n module_root={}\n pgdata_entries={}", + runtime_layout.kind, + path_state(&paths.pgdata), + path_state(pgdata_parent), + path_state(&paths.runtime_root()), + path_state(&runtime_layout.module_root), + dir_entry_sample(&paths.pgdata), + ) +} + +fn path_state(path: &Path) -> String { + match fs::metadata(path) { + Ok(metadata) => format!( + "{} ({})", + path.display(), + if metadata.is_dir() { + "dir" + } else if metadata.is_file() { + "file" + } else { + "other" + } + ), + Err(err) => format!("{} ({})", path.display(), err), + } +} + +fn dir_entry_sample(path: &Path) -> String { + let entries = match fs::read_dir(path) { + Ok(entries) => entries, + Err(err) => return format!(""), + }; + let mut names = entries + .filter_map(|entry| { + entry + .ok() + .map(|entry| entry.file_name().to_string_lossy().into_owned()) + }) + .take(8) + .collect::>(); + names.sort(); + if names.is_empty() { + "".to_owned() + } else { + names.join(", ") + } +} + +fn split_initdb_args() -> Vec<&'static str> { + vec![ + "--allow-group-access", + "--encoding", + "UTF8", + "--locale", + "C.UTF-8", + "--locale-provider", + "libc", + "--auth", + "trust", + "-D", + PGDATA_DIR, + ] +} + +fn split_initdb_binary_package( + initdb_module: &Path, + postgres_module: &Path, +) -> Result { + let initdb_wasm = + fs::read(initdb_module).with_context(|| format!("read {}", initdb_module.display()))?; + let postgres_wasm = + fs::read(postgres_module).with_context(|| format!("read {}", postgres_module.display()))?; + + let mut package_hash = Sha256::new(); + package_hash.update(b"oliphaunt-wasix-split-initdb-package-v1\n"); + package_hash.update(&initdb_wasm); + package_hash.update(&postgres_wasm); + let package_hash: [u8; 32] = package_hash.finalize().into(); + let package_id = PackageId::Hash(PackageHash::from_sha256_bytes(package_hash)); + + Ok(BinaryPackage { + id: package_id.clone(), + package_ids: vec![package_id.clone()], + when_cached: None, + entrypoint_cmd: Some("initdb".to_owned()), + hash: Default::default(), + package_mounts: None, + commands: vec![ + split_initdb_command("initdb", initdb_wasm, &package_id), + split_initdb_command("postgres", postgres_wasm, &package_id), + ], + uses: Vec::new(), + file_system_memory_footprint: 0, + additional_host_mapped_directories: Vec::new(), + }) +} + +fn split_initdb_command(name: &str, wasm: Vec, package_id: &PackageId) -> BinaryPackageCommand { + let hash = ModuleHash::new(&wasm); + let atom: webc::compat::SharedBytes = wasm.into(); + BinaryPackageCommand::new( + name.to_owned(), + WebcCommand { + runner: WASI_RUNNER_URI.to_owned(), + annotations: Default::default(), + }, + atom, + hash, + None, + package_id.clone(), + package_id.clone(), + ) +} + +fn preload_runtime_side_modules( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + runtime_root: &Path, +) -> Result<()> { + let _phase = timing::phase("wasix.seed_runtime_side_modules"); + let lib_dir = runtime_root.join("lib/postgresql"); + for (file_name, artifact_name) in RUNTIME_SIDE_MODULES { + let library = lib_dir.join(file_name); + ensure!( + library.exists(), + "runtime support module '{}' is not installed at {}", + file_name, + library.display() + ); + + seed_side_module_cache( + runtime, + engine, + module_cache, + &library, + artifact_name, + &format!("runtime support module '{file_name}'"), + )?; + } + Ok(()) +} + +#[cfg(feature = "extensions")] +fn preload_installed_extension_side_modules( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + runtime_root: &Path, +) -> Result<()> { + let _phase = timing::phase("wasix.seed_extension_side_modules"); + for extension in super::extensions::ALL { + for module in extension.native_support_modules() { + let library = runtime_root.join(module.runtime_path()); + if !library.exists() { + continue; + } + let Some(aot_name) = module.aot_name() else { + continue; + }; + seed_side_module_cache( + runtime, + engine, + module_cache, + &library, + aot_name, + &format!( + "installed extension '{}' support module '{}'", + extension.sql_name(), + module.runtime_path() + ), + )?; + } + + let Some(module_file) = extension.native_module_file() else { + continue; + }; + let Some(aot_name) = extension.aot_name() else { + continue; + }; + let library = runtime_root + .join("lib") + .join("postgresql") + .join(module_file); + if !library.exists() { + continue; + } + seed_side_module_cache( + runtime, + engine, + module_cache, + &library, + aot_name, + &format!("installed extension '{}'", extension.sql_name()), + )?; + } + Ok(()) +} + +#[cfg(feature = "extensions")] +fn seed_extension_side_module( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + runtime_root: &Path, + runtime_path: &str, + aot_name: Option<&'static str>, + label: &str, +) -> Result<()> { + let Some(aot_name) = aot_name else { + return Ok(()); + }; + let library = runtime_root.join(runtime_path); + ensure!( + library.exists(), + "{label} is not installed at {}", + library.display() + ); + seed_side_module_cache(runtime, engine, module_cache, &library, aot_name, label) +} + +fn seed_side_module_cache( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + library: &Path, + artifact_name: &'static str, + label: &str, +) -> Result<()> { + seed_wasix_module_cache(runtime, engine, module_cache, library, artifact_name, label) +} + +fn seed_wasix_module_cache( + runtime: &TokioRuntime, + engine: &Engine, + module_cache: &Arc, + wasm_path: &Path, + artifact_name: &str, + label: &str, +) -> Result<()> { + let wasm = { + let _phase = timing::phase("wasix.seed_side_module.read_wasm"); + fs::read(wasm_path).with_context(|| format!("read WASIX module {}", wasm_path.display()))? + }; + let module_hash = { + let _phase = timing::phase("wasix.seed_side_module.module_hash"); + ModuleHash::new(&wasm) + }; + let seed_key = format!("{artifact_name}:{}:{module_hash}", aot::engine_identity()); + let mut seeded_side_modules = SEEDED_SIDE_MODULES + .get_or_init(|| Mutex::new(HashSet::new())) + .lock() + .expect("seeded side module cache poisoned"); + if seeded_side_modules.contains(&seed_key) { + return Ok(()); + } + + // Keep the process-wide seed check and SharedCache write atomic. Wasmer's + // shared cache is global to all concurrent Oliphaunt instances in this process. + let module = { + let _phase = timing::phase("wasix.seed_side_module.load_aot"); + aot::load_artifact_module(engine, artifact_name)? + }; + { + let _phase = timing::phase("wasix.seed_side_module.save_cache"); + block_on_tokio_runtime(runtime, module_cache.save(module_hash, engine, &module)) + .with_context(|| format!("seed Wasmer module cache for {label} ({module_hash})"))?; + } + seeded_side_modules.insert(seed_key); + Ok(()) +} + +fn block_on_tokio_runtime(runtime: &TokioRuntime, future: F) -> T +where + F: Future + Send, + T: Send, +{ + if tokio::runtime::Handle::try_current().is_ok() { + return std::thread::scope(|scope| { + scope + .spawn(move || runtime.block_on(future)) + .join() + .unwrap_or_else(|payload| std::panic::resume_unwind(payload)) + }); + } + + runtime.block_on(future) +} + +impl OliphauntLifecycleExports { + fn load(store: &mut Store, instance: &Instance) -> Result { + let wasi_start = typed_export(store, instance, "_start")?; + let set_force_host_error_recovery = optional_typed_export( + store, + instance, + "oliphaunt_wasix_set_force_host_error_recovery", + )?; + let set_active = typed_export(store, instance, "oliphaunt_wasix_set_active")?; + let start_oliphaunt = typed_export(store, instance, "oliphaunt_wasix_start")?; + let run_atexit_funcs = + optional_typed_export(store, instance, "oliphaunt_wasix_run_atexit_funcs")?; + let backend_timing_reset = + optional_typed_export(store, instance, "oliphaunt_wasix_backend_timing_reset")?; + let backend_timing_elapsed_us = + optional_typed_export(store, instance, "oliphaunt_wasix_backend_timing_elapsed_us")?; + + Ok(Self { + wasi_start, + set_force_host_error_recovery, + set_active, + start_oliphaunt, + run_atexit_funcs, + backend_timing_reset, + backend_timing_elapsed_us, + }) + } +} + +impl WasixProtocolExports { + fn load(store: &mut Store, instance: &Instance) -> Result { + let get_port = typed_export(store, instance, "oliphaunt_wasix_get_proc_port")?; + let process_startup = typed_export(store, instance, "ProcessStartupPacket")?; + let send_conn_data = typed_export(store, instance, "oliphaunt_wasix_send_conn_data")?; + let pq_flush = typed_export(store, instance, "oliphaunt_wasix_pq_flush")?; + let pq_buffer_remaining_data = typed_export(store, instance, "pq_buffer_remaining_data")?; + let main_loop = typed_export(store, instance, "PostgresMainLoopOnce")?; + let send_ready = typed_export(store, instance, "PostgresSendReadyForQueryIfNecessary")?; + let recover_error = typed_export(store, instance, "PostgresMainLongJmp")?; + + Ok(Self { + get_port, + process_startup, + send_conn_data, + pq_flush, + pq_buffer_remaining_data, + main_loop, + send_ready, + recover_error, + }) + } +} + +impl WasixProtocolStdioExports { + fn load(store: &mut Store, instance: &Instance) -> Result> { + let Some(set_protocol_transport) = optional_typed_export::( + store, + instance, + "oliphaunt_wasix_set_protocol_transport", + )? + else { + return Ok(None); + }; + let protocol_stream_active = + typed_export::<(), i32>(store, instance, "oliphaunt_wasix_protocol_stream_active")?; + Ok(Some(Self { + set_protocol_transport, + protocol_stream_active, + })) + } +} + +fn ensure_integrated_oliphaunt_contract(instance: &Instance) -> Result<()> { + for name in [ + "oliphaunt_wasix_start", + "oliphaunt_wasix_set_active", + "PostgresMainLongJmp", + ] { + ensure!( + instance.exports.get_function(name).is_ok() + || instance.exports.get_function(&format!("_{name}")).is_ok(), + "WASIX runtime is missing integrated Oliphaunt lifecycle export {name}" + ); + } + Ok(()) +} + +impl WasixOliphauntIo { + fn new(store: &mut Store, instance: &Instance) -> Result { + let io = Self { + input_reset: typed_export(store, instance, "oliphaunt_wasix_input_reset")?, + input_write: typed_export(store, instance, "oliphaunt_wasix_input_write")?, + input_available: typed_export(store, instance, "oliphaunt_wasix_input_available")?, + output_reset: typed_export(store, instance, "oliphaunt_wasix_output_reset")?, + output_len: typed_export(store, instance, "oliphaunt_wasix_output_len")?, + output_read: typed_export(store, instance, "oliphaunt_wasix_output_read")?, + }; + io.reset(store)?; + Ok(io) + } + + fn reset(&self, store: &mut Store) -> Result<()> { + ensure!( + self.input_reset + .call(&mut *store) + .context("oliphaunt_wasix_input_reset")? + == 0, + "oliphaunt_wasix_input_reset failed" + ); + ensure!( + self.output_reset + .call(&mut *store) + .context("oliphaunt_wasix_output_reset")? + == 0, + "oliphaunt_wasix_output_reset failed" + ); + Ok(()) + } + + fn push_input( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + allocator: &GuestAllocator, + bytes: &[u8], + ) -> Result<()> { + if bytes.is_empty() { + return Ok(()); + } + let written = allocator.with_bytes(store, env, bytes, |store, ptr| { + self.input_write + .call(&mut *store, ptr, bytes.len() as i32) + .context("oliphaunt_wasix_input_write") + })?; + ensure!( + written == bytes.len() as i32, + "oliphaunt_wasix_input_write wrote {written}, expected {}", + bytes.len() + ); + Ok(()) + } + + fn available(&self, store: &mut Store) -> Result { + let available = self + .input_available + .call(store) + .context("oliphaunt_wasix_input_available")?; + ensure!( + available >= 0, + "oliphaunt_wasix_input_available returned negative length {available}" + ); + Ok(available) + } + + fn take_output( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + allocator: &GuestAllocator, + ) -> Result> { + let len = self + .output_len + .call(&mut *store) + .context("oliphaunt_wasix_output_len")?; + ensure!( + len >= 0, + "oliphaunt_wasix_output_len returned negative length {len}" + ); + if len == 0 { + return Ok(Vec::new()); + } + let bytes = allocator.with_allocation(store, len, |store, ptr| { + let read = self + .output_read + .call(&mut *store, ptr, len) + .context("oliphaunt_wasix_output_read")?; + ensure!( + read >= 0 && read <= len, + "invalid oliphaunt_wasix_output_read length {read}" + ); + + let mut bytes = vec![0u8; read as usize]; + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.read(ptr as u64, &mut bytes) + .with_context(|| format!("read SQL output at 0x{ptr:x}"))?; + Ok(bytes) + })?; + ensure!( + self.output_reset + .call(&mut *store) + .context("oliphaunt_wasix_output_reset after read")? + == 0, + "oliphaunt_wasix_output_reset after read failed" + ); + Ok(bytes) + } +} + +impl GuestAllocator { + fn load(store: &mut Store, instance: &Instance) -> Result { + let malloc = typed_export::(store, instance, "malloc")?; + let free = typed_export::(store, instance, "pg_free") + .or_else(|_| typed_export::(store, instance, "free")) + .context("get pg_free/free export")?; + Ok(Self { + malloc, + free, + #[cfg(debug_assertions)] + allocations: Cell::new(0), + #[cfg(debug_assertions)] + frees: Cell::new(0), + }) + } + + #[cfg(debug_assertions)] + fn allocation_counts(&self) -> (u64, u64) { + (self.allocations.get(), self.frees.get()) + } + + fn with_bytes( + &self, + store: &mut Store, + env: &WasiFunctionEnv, + bytes: &[u8], + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let ptr = self.allocate(store, bytes.len() as i32)?; + self.run_and_free(store, ptr, |store, ptr| { + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.write(ptr as u64, bytes) + .with_context(|| format!("write guest bytes at 0x{ptr:x}"))?; + f(store, ptr) + }) + } + + fn with_allocation( + &self, + store: &mut Store, + len: i32, + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let ptr = self.allocate(store, len)?; + self.run_and_free(store, ptr, f) + } + + fn allocate(&self, store: &mut Store, len: i32) -> Result { + let ptr = self + .malloc + .call(&mut *store, len) + .context("malloc guest allocation")?; + ensure!(ptr > 0, "malloc returned null for guest allocation"); + #[cfg(debug_assertions)] + self.allocations.set(self.allocations.get() + 1); + Ok(ptr) + } + + fn run_and_free( + &self, + store: &mut Store, + ptr: i32, + f: impl FnOnce(&mut Store, i32) -> Result, + ) -> Result { + let result = f(store, ptr); + let free_result = self + .free + .call(&mut *store, ptr) + .with_context(|| format!("free guest allocation at 0x{ptr:x}")); + #[cfg(debug_assertions)] + if free_result.is_ok() { + self.frees.set(self.frees.get() + 1); + } + match (result, free_result) { + (Ok(value), Ok(())) => Ok(value), + (Ok(_), Err(err)) => Err(err), + (Err(err), Ok(())) => Err(err), + (Err(err), Err(free_err)) => Err(err.context(format!( + "failed to free guest allocation at 0x{ptr:x} after previous error: {free_err:#}" + ))), + } + } +} + +fn typed_export( + store: &mut Store, + instance: &Instance, + name: &str, +) -> Result> +where + Args: WasmTypeList, + Rets: WasmTypeList, +{ + instance + .exports + .get_typed_function::(&*store, name) + .or_else(|_| { + instance + .exports + .get_typed_function::(&*store, &format!("_{name}")) + }) + .with_context(|| format!("get {name} export")) +} + +fn optional_typed_export( + store: &mut Store, + instance: &Instance, + name: &str, +) -> Result>> +where + Args: WasmTypeList, + Rets: WasmTypeList, +{ + let underscored_name = format!("_{name}"); + if instance.exports.get_function(name).is_err() + && instance.exports.get_function(&underscored_name).is_err() + { + return Ok(None); + } + typed_export(store, instance, name).map(Some) +} + +fn runtime_error_exit_code(err: &wasmer::RuntimeError) -> Option { + err.downcast_ref::().and_then(|err| match err { + WasiError::Exit(code) => Some(code.raw()), + _ => None, + }) +} + +fn is_wasm_uncaught_exception(err: &wasmer::RuntimeError) -> bool { + // Wasmer reports an uncaught WebAssembly exception when PostgreSQL ERROR + // unwinds across the exported loop boundary. The C recovery export then + // performs the normal Postgres error cleanup and emits ErrorResponse. + err.message().contains("uncaught exception") +} + +fn host_requires_process_exit_error_recovery() -> bool { + // Wasmer does not implement nested WebAssembly exception throws on MSVC + // hosts. The WASIX bridge therefore routes PostgreSQL ERROR longjmps + // through the existing process-exit recovery boundary on that host + // capability, while preserving normal nested unwinding elsewhere. + cfg!(target_env = "msvc") +} + +fn wasix_icu_data_is_available(paths: &OliphauntPaths, runtime_layout: &RuntimeLayout) -> bool { + paths.runtime_root().join("share/icu").is_dir() + || runtime_layout.module_root.join("share/icu").is_dir() +} + +fn add_oliphaunt_env( + builder: &mut wasmer_wasix::WasiEnvBuilder, + startup_config: &StartupConfig, + paths: &OliphauntPaths, + runtime_layout: &RuntimeLayout, +) { + for (key, value) in [ + ("PREFIX", WASM_PREFIX), + ("PGDATA", PGDATA_DIR), + ("PGUSER", startup_config.username.as_str()), + ("PGDATABASE", startup_config.database.as_str()), + ("MODE", "REACT"), + ("REPL", "N"), + ("PGSYSCONFDIR", PGDATA_DIR), + ("PGCLIENTENCODING", "UTF8"), + ("LC_CTYPE", "C.UTF-8"), + ("TZ", "UTC"), + ("PGTZ", "UTC"), + ("PG_COLOR", "never"), + ("PROJ_DATA", "/share/proj"), + ] { + builder.add_env(key, value); + } + if wasix_icu_data_is_available(paths, runtime_layout) { + builder.add_env("ICU_DATA", ICU_DATA_DIR); + } +} + +fn add_oliphaunt_args( + builder: &mut wasmer_wasix::WasiEnvBuilder, + postgres_config: &PostgresConfig, + startup_config: &StartupConfig, +) -> Result<()> { + postgres_config.validate()?; + startup_config.validate()?; + for arg in ["--single", "-F", "-O", "-j"] { + builder.add_arg(arg); + } + if let Some(level) = startup_config.debug_level { + builder.add_arg("-d"); + builder.add_arg(level.to_string()); + } + for (name, value) in DEFAULT_STARTUP_GUCS { + builder.add_arg("-c"); + builder.add_arg(format!("{name}={value}")); + } + if startup_config.relaxed_durability { + builder.add_arg("-c"); + builder.add_arg("synchronous_commit=off"); + } + for (name, value) in postgres_config.iter() { + builder.add_arg("-c"); + builder.add_arg(format!("{name}={value}")); + } + for arg in &startup_config.extra_args { + builder.add_arg(arg); + } + for arg in ["-D", PGDATA_DIR, startup_config.database.as_str()] { + builder.add_arg(arg); + } + Ok(()) +} + +const DEFAULT_STARTUP_GUCS: &[(&str, &str)] = &[ + ("search_path", "public"), + ("exit_on_error", "false"), + ("log_checkpoints", "false"), + ("max_wal_senders", "0"), + ("max_worker_processes", "0"), + ("max_parallel_workers", "0"), + ("max_parallel_workers_per_gather", "0"), + // PostgreSQL 18 defaults io_method=worker, but the embedded WASIX + // single-user backend has no postmaster-managed IO worker process model. + ("io_method", "sync"), + ("wal_buffers", "4MB"), + ("min_wal_size", "80MB"), + ("shared_buffers", "128MB"), +]; + +fn ensure_runtime_dirs(paths: &OliphauntPaths) -> Result<()> { + for path in [ + paths.runtime_root(), + paths.pgdata.clone(), + paths.runtime_root().join("home"), + paths.runtime_root().join("dev"), + paths.runtime_root().join("dev/shm"), + paths.runtime_root().join("tmp"), + ] { + fs::create_dir_all(&path).with_context(|| format!("create {}", path.display()))?; + } + + Ok(()) +} + +fn startup_packet(user: &str, database: &str) -> Vec { + let mut body = Vec::new(); + body.extend_from_slice(&196608i32.to_be_bytes()); + for (key, value) in [ + ("user", user), + ("database", database), + ("client_encoding", "UTF8"), + ("DateStyle", "ISO, MDY"), + ("TimeZone", "UTC"), + ] { + body.extend_from_slice(key.as_bytes()); + body.push(0); + body.extend_from_slice(value.as_bytes()); + body.push(0); + } + body.push(0); + + let mut packet = Vec::with_capacity(body.len() + 4); + packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); + packet.extend_from_slice(&body); + packet +} + +fn protocol_response_contains_error(response: &[u8]) -> bool { + let mut cursor = 0usize; + while cursor + 5 <= response.len() { + let tag = response[cursor]; + let len = i32::from_be_bytes(response[cursor + 1..cursor + 5].try_into().unwrap()); + if len < 4 { + return false; + } + let total = 1usize.saturating_add(len as usize); + if cursor + total > response.len() { + return false; + } + if tag == b'E' { + return true; + } + cursor += total; + } + false +} + +fn format_output_tail(bytes: &[u8]) -> String { + const LIMIT: usize = 512; + let skipped = bytes.len().saturating_sub(LIMIT); + let tail = &bytes[skipped..]; + let mut hex = String::new(); + for (index, byte) in tail.iter().enumerate() { + if index > 0 { + hex.push(' '); + } + hex.push_str(&format!("{byte:02x}")); + } + let text = String::from_utf8_lossy(tail); + format!( + "{} bytes total, showing last {} bytes\nhex: {hex}\nutf8-lossy:\n{text}", + bytes.len(), + tail.len() + ) +} + +fn seed_exported_c_string_value( + store: &mut Store, + instance: &Instance, + env: &WasiFunctionEnv, + name: &str, + value: &str, +) -> Result<()> { + let Ok(global) = instance.exports.get_global(name) else { + return Ok(()); + }; + let wasmer::Value::I32(ptr) = global.get(&mut *store) else { + return Ok(()); + }; + if ptr <= 0 { + return Ok(()); + } + let mut bytes = value.as_bytes().to_vec(); + bytes.push(0); + let view = env + .data(&*store) + .try_memory_view(&*store) + .context("get WASIX memory view")?; + view.write(ptr as u64, &bytes) + .with_context(|| format!("seed {name} at 0x{ptr:x}"))?; + Ok(()) +} + +fn summarize_protocol(bytes: &[u8]) -> String { + if bytes.is_empty() { + return "0 bytes".to_owned(); + } + + let mut cursor = 0usize; + let mut messages = Vec::new(); + while cursor + 5 <= bytes.len() { + let tag = bytes[cursor] as char; + let len = i32::from_be_bytes([ + bytes[cursor + 1], + bytes[cursor + 2], + bytes[cursor + 3], + bytes[cursor + 4], + ]); + if len < 4 { + messages.push(format!("{tag}(bad-len:{len})")); + break; + } + let end = cursor + 1 + len as usize; + if end > bytes.len() { + messages.push(format!("{tag}(truncated:{len})")); + break; + } + messages.push(format!("{tag}({} bytes)", len - 4)); + cursor = end; + } + if cursor < bytes.len() { + messages.push(format!("tail:{} bytes", bytes.len() - cursor)); + } + format!("{} bytes [{}]", bytes.len(), messages.join(", ")) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io; + use std::pin::Pin; + + #[test] + fn protocol_stdio_fails_closed_when_detached() -> Result<()> { + use std::task::{Context, Poll, Waker}; + use wasmer_wasix::VirtualFile; + use wasmer_wasix::virtual_fs::AsyncWrite; + + let mut file = ProtocolStdioFile::new(); + let mut cx = Context::from_waker(Waker::noop()); + + match Pin::new(&mut file).poll_write_ready(&mut cx) { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached write-ready result: {other:?}"), + } + match Pin::new(&mut file).poll_write(&mut cx, b"lost bytes") { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached write result: {other:?}"), + } + match Pin::new(&mut file).poll_flush(&mut cx) { + Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), + other => panic!("unexpected detached flush result: {other:?}"), + } + + Ok(()) + } + + #[test] + fn block_on_tokio_runtime_works_inside_tokio_runtime() -> Result<()> { + let worker = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let host = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + + let value = host.block_on(async { block_on_tokio_runtime(&worker, async { 42 }) }); + + assert_eq!(value, 42); + Ok(()) + } + + #[test] + fn mountfs_pgdata_overlay_exposes_lower_template_files() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(&pgdata_lower)?; + fs::write(pgdata_lower.join("postgresql.conf"), b"from-template\n")?; + + let root = virtual_fs::MountFileSystem::new(); + root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; + root.mount( + Path::new(PGDATA_DIR), + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower.clone(), + )?), + )?; + + virtual_fs::FileSystem::metadata(&root, Path::new("/base/postgresql.conf"))?; + virtual_fs::FileSystem::new_open_options(&root) + .read(true) + .open("/base/postgresql.conf")?; + let mut writable = virtual_fs::FileSystem::new_open_options(&root) + .write(true) + .open("/base/postgresql.conf")?; + runtime.block_on(async { + writable.write_all(b"upper-only\n").await?; + writable.flush().await + })?; + assert!(pgdata_upper.join("postgresql.conf").is_file()); + assert_eq!( + fs::read_to_string(pgdata_lower.join("postgresql.conf"))?, + "from-template\n" + ); + Ok(()) + } + + #[test] + fn mountfs_pgdata_overlay_creates_files_in_lower_only_directories() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(pgdata_lower.join("global"))?; + + let root = virtual_fs::MountFileSystem::new(); + root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; + root.mount( + Path::new(PGDATA_DIR), + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower, + )?), + )?; + + let mut writable = virtual_fs::FileSystem::new_open_options(&root) + .write(true) + .create(true) + .open("/base/global/postmaster.pid")?; + runtime.block_on(async { + writable.write_all(b"lock\n").await?; + writable.flush().await + })?; + + assert_eq!( + fs::read_to_string(pgdata_upper.join("global/postmaster.pid"))?, + "lock\n" + ); + Ok(()) + } + + #[test] + fn mountfs_root_filesystem_routes_pgdata_as_mutable_subtree() -> Result<()> { + use tokio::io::AsyncWriteExt; + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let _guard = runtime.enter(); + let temp = tempfile::TempDir::new()?; + let runtime_root = temp.path().join("runtime"); + let pgdata_upper = runtime_root.join("base"); + let pgdata_lower = temp.path().join("template"); + fs::create_dir_all(&pgdata_upper)?; + fs::create_dir_all(pgdata_lower.join("global"))?; + fs::write(pgdata_lower.join("PG_VERSION"), b"17\n")?; + fs::write(pgdata_lower.join("global/pg_control"), b"control\n")?; + + let root = wasi_root_with_pgdata_mount( + host_filesystem(&runtime_root)?, + Arc::new(EagerCopyOverlayFileSystem::new( + pgdata_upper.clone(), + pgdata_lower, + )?), + )?; + + virtual_fs::FileSystem::metadata(root.as_ref(), Path::new("/base/PG_VERSION"))?; + let mut entries = + virtual_fs::FileSystem::read_dir(root.as_ref(), Path::new("/base/global"))?; + let entry = entries.next().transpose()?.context("expected pg_control")?; + assert_eq!(entry.path, Path::new("/base/global/pg_control")); + + let mut lock_file = virtual_fs::FileSystem::new_open_options(root.as_ref()) + .read(true) + .write(true) + .create_new(true) + .open("/base/postmaster.pid")?; + runtime.block_on(async { + lock_file.write_all(b"lock\n").await?; + lock_file.flush().await + })?; + + assert_eq!( + fs::read_to_string(pgdata_upper.join("postmaster.pid"))?, + "lock\n" + ); + Ok(()) + } +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs new file mode 100644 index 00000000..8e22f9fc --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs @@ -0,0 +1,422 @@ +use std::collections::VecDeque; +use std::fmt; +use std::io::{self, Read, Write}; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context as TaskContext, Poll}; + +use anyhow::{Result, ensure}; +use tokio::io::ReadBuf; +use wasmer_wasix::virtual_fs; + +pub(crate) trait ProtocolStream: Read + Write + Send { + fn read_ready(&mut self) -> io::Result; +} + +#[derive(Debug, Default)] +struct TailCaptureState { + bytes: VecDeque, +} + +#[derive(Debug, Clone)] +pub(super) struct TailCaptureFile { + inner: Arc>, + limit: usize, +} + +#[derive(Debug, Clone)] +pub(super) struct TailCaptureHandle { + inner: Arc>, +} + +impl TailCaptureFile { + pub(super) fn new(limit: usize) -> (Self, TailCaptureHandle) { + let inner = Arc::new(Mutex::new(TailCaptureState::default())); + ( + Self { + inner: inner.clone(), + limit, + }, + TailCaptureHandle { inner }, + ) + } + + fn push_tail(&self, bytes: &[u8]) { + let Ok(mut state) = self.inner.lock() else { + return; + }; + for byte in bytes { + state.bytes.push_back(*byte); + while state.bytes.len() > self.limit { + state.bytes.pop_front(); + } + } + } +} + +impl TailCaptureHandle { + pub(super) fn text(&self) -> String { + let Ok(state) = self.inner.lock() else { + return "".to_owned(); + }; + let bytes = state.bytes.iter().copied().collect::>(); + String::from_utf8_lossy(&bytes).into_owned() + } +} + +impl virtual_fs::AsyncSeek for TailCaptureFile { + fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) + } +} + +impl virtual_fs::AsyncRead for TailCaptureFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl virtual_fs::AsyncWrite for TailCaptureFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + self.push_tail(buf); + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let mut total = 0; + for buf in bufs { + self.push_tail(buf); + total += buf.len(); + } + Poll::Ready(Ok(total)) + } + + fn is_write_vectored(&self) -> bool { + true + } +} + +#[async_trait::async_trait] +impl virtual_fs::VirtualFile for TailCaptureFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + self.inner + .lock() + .map(|state| state.bytes.len() as u64) + .unwrap_or(0) + } + + fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { + Ok(()) + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + Ok(()) + } + + fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(self.limit)) + } +} + +#[derive(Clone)] +pub(super) struct ProtocolStdioFile { + state: Arc, +} + +struct ProtocolStdioState { + inner: Mutex, +} + +#[derive(Default)] +struct ProtocolStdioInner { + stream: Option>, + prefix: Vec, + prefix_offset: usize, +} + +pub(super) struct ProtocolStdioAttachment { + file: ProtocolStdioFile, +} + +impl ProtocolStdioFile { + pub(super) fn new() -> Self { + Self { + state: Arc::new(ProtocolStdioState { + inner: Mutex::new(ProtocolStdioInner::default()), + }), + } + } + + pub(super) fn attach(&self, stream: S) -> Result + where + S: ProtocolStream + 'static, + { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; + ensure!( + guard.stream.is_none(), + "WASIX protocol stdio stream is already attached" + ); + guard.stream = Some(Box::new(stream)); + guard.prefix.clear(); + guard.prefix_offset = 0; + Ok(ProtocolStdioAttachment { file: self.clone() }) + } + + fn detach(&self) { + if let Ok(mut guard) = self.state.inner.lock() { + guard.stream = None; + guard.prefix.clear(); + guard.prefix_offset = 0; + } + } + + pub(super) fn set_prefix(&self, prefix: Vec) -> Result<()> { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; + guard.prefix = prefix; + guard.prefix_offset = 0; + Ok(()) + } + + pub(super) fn clear_prefix(&self) -> Result<()> { + self.set_prefix(Vec::new()) + } + + fn with_inner( + &self, + f: impl FnOnce(&mut ProtocolStdioInner) -> io::Result, + ) -> io::Result { + let mut guard = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned"))?; + f(&mut guard) + } +} + +impl Drop for ProtocolStdioAttachment { + fn drop(&mut self) { + self.file.detach(); + } +} + +impl fmt::Debug for ProtocolStdioFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProtocolStdioFile").finish_non_exhaustive() + } +} + +impl virtual_fs::VirtualFile for ProtocolStdioFile { + fn last_accessed(&self) -> u64 { + 0 + } + + fn last_modified(&self) -> u64 { + 0 + } + + fn created_time(&self) -> u64 { + 0 + } + + fn size(&self) -> u64 { + 0 + } + + fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { + Err(virtual_fs::FsError::PermissionDenied) + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + Ok(()) + } + + fn is_open(&self) -> bool { + self.state + .inner + .lock() + .map(|inner| inner.stream.is_some()) + .unwrap_or(false) + } + + fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + match self.with_inner(|inner| { + if inner.prefix_offset < inner.prefix.len() { + return Ok(true); + } + let stream = inner.stream.as_mut().ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + ) + })?; + stream.read_ready() + }) { + Ok(true) => Poll::Ready(Ok(1)), + Ok(false) => Poll::Pending, + Err(err) => Poll::Ready(Err(err)), + } + } + + fn poll_write_ready( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + match self.with_inner(|inner| { + if inner.stream.is_some() { + Ok(8192) + } else { + Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )) + } + }) { + Ok(ready) => Poll::Ready(Ok(ready)), + Err(err) => Poll::Ready(Err(err)), + } + } +} + +impl virtual_fs::AsyncRead for ProtocolStdioFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + if buf.remaining() == 0 { + return Poll::Ready(Ok(())); + } + let read = self.with_inner(|inner| { + let unfilled = buf.initialize_unfilled(); + if inner.prefix_offset < inner.prefix.len() { + let remaining = &inner.prefix[inner.prefix_offset..]; + let read = remaining.len().min(unfilled.len()); + unfilled[..read].copy_from_slice(&remaining[..read]); + inner.prefix_offset += read; + if inner.prefix_offset == inner.prefix.len() { + inner.prefix.clear(); + inner.prefix_offset = 0; + } + return Ok(read); + } + let stream = inner.stream.as_mut().ok_or_else(|| { + io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + ) + })?; + stream.read(unfilled) + }); + match read { + Ok(read) => { + buf.advance(read); + Poll::Ready(Ok(())) + } + Err(err) => Poll::Ready(Err(err)), + } + } +} + +impl virtual_fs::AsyncWrite for ProtocolStdioFile { + fn poll_write( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + let written = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned")) + .and_then(|mut inner| match inner.stream.as_mut() { + Some(stream) => stream.write(buf), + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )), + }); + Poll::Ready(written) + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + let flushed = self + .state + .inner + .lock() + .map_err(|_| io::Error::other("protocol stdio lock poisoned")) + .and_then(|mut inner| match inner.stream.as_mut() { + Some(stream) => stream.flush(), + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "WASIX protocol stdio stream is not attached", + )), + }); + Poll::Ready(flushed) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl virtual_fs::AsyncSeek for ProtocolStdioFile { + fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { + Ok(()) + } + + fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { + Poll::Ready(Ok(0)) + } +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs new file mode 100644 index 00000000..c0a73326 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs @@ -0,0 +1,664 @@ +use std::fmt; +use std::fs; +use std::future::Future; +use std::io; +use std::path::{Component, Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::task::{Context as TaskContext, Poll}; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use serde::Serialize; +use tokio::io::ReadBuf; +use wasmer_wasix::virtual_fs; + +use super::super::sync_host_fs::SyncHostFileSystem; + +const WASIX_DEVICE_FILES: &[&str] = &[ + "null", "zero", "urandom", "stdin", "stdout", "stderr", "tty", +]; + +static FS_TRACE: FsTraceState = FsTraceState::new(); + +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FsTraceSnapshot { + enabled: bool, + open_count: u64, + read_count: u64, + read_bytes: u64, + write_count: u64, + write_bytes: u64, + seek_count: u64, + metadata_count: u64, + read_dir_count: u64, + create_dir_count: u64, + remove_file_count: u64, + remove_dir_count: u64, + rename_count: u64, + set_len_count: u64, + unlink_count: u64, + total_elapsed_micros: u64, + read_elapsed_micros: u64, + write_elapsed_micros: u64, + seek_elapsed_micros: u64, +} + +struct FsTraceState { + open_count: AtomicU64, + read_count: AtomicU64, + read_bytes: AtomicU64, + write_count: AtomicU64, + write_bytes: AtomicU64, + seek_count: AtomicU64, + metadata_count: AtomicU64, + read_dir_count: AtomicU64, + create_dir_count: AtomicU64, + remove_file_count: AtomicU64, + remove_dir_count: AtomicU64, + rename_count: AtomicU64, + set_len_count: AtomicU64, + unlink_count: AtomicU64, + total_elapsed_micros: AtomicU64, + read_elapsed_micros: AtomicU64, + write_elapsed_micros: AtomicU64, + seek_elapsed_micros: AtomicU64, +} + +impl FsTraceState { + const fn new() -> Self { + Self { + open_count: AtomicU64::new(0), + read_count: AtomicU64::new(0), + read_bytes: AtomicU64::new(0), + write_count: AtomicU64::new(0), + write_bytes: AtomicU64::new(0), + seek_count: AtomicU64::new(0), + metadata_count: AtomicU64::new(0), + read_dir_count: AtomicU64::new(0), + create_dir_count: AtomicU64::new(0), + remove_file_count: AtomicU64::new(0), + remove_dir_count: AtomicU64::new(0), + rename_count: AtomicU64::new(0), + set_len_count: AtomicU64::new(0), + unlink_count: AtomicU64::new(0), + total_elapsed_micros: AtomicU64::new(0), + read_elapsed_micros: AtomicU64::new(0), + write_elapsed_micros: AtomicU64::new(0), + seek_elapsed_micros: AtomicU64::new(0), + } + } + + fn reset(&self) { + for counter in [ + &self.open_count, + &self.read_count, + &self.read_bytes, + &self.write_count, + &self.write_bytes, + &self.seek_count, + &self.metadata_count, + &self.read_dir_count, + &self.create_dir_count, + &self.remove_file_count, + &self.remove_dir_count, + &self.rename_count, + &self.set_len_count, + &self.unlink_count, + &self.total_elapsed_micros, + &self.read_elapsed_micros, + &self.write_elapsed_micros, + &self.seek_elapsed_micros, + ] { + counter.store(0, Ordering::Relaxed); + } + } + + fn record_total(&self, elapsed: Duration) { + self.total_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + + fn snapshot(&self) -> FsTraceSnapshot { + FsTraceSnapshot { + enabled: fs_trace_enabled(), + open_count: self.open_count.load(Ordering::Relaxed), + read_count: self.read_count.load(Ordering::Relaxed), + read_bytes: self.read_bytes.load(Ordering::Relaxed), + write_count: self.write_count.load(Ordering::Relaxed), + write_bytes: self.write_bytes.load(Ordering::Relaxed), + seek_count: self.seek_count.load(Ordering::Relaxed), + metadata_count: self.metadata_count.load(Ordering::Relaxed), + read_dir_count: self.read_dir_count.load(Ordering::Relaxed), + create_dir_count: self.create_dir_count.load(Ordering::Relaxed), + remove_file_count: self.remove_file_count.load(Ordering::Relaxed), + remove_dir_count: self.remove_dir_count.load(Ordering::Relaxed), + rename_count: self.rename_count.load(Ordering::Relaxed), + set_len_count: self.set_len_count.load(Ordering::Relaxed), + unlink_count: self.unlink_count.load(Ordering::Relaxed), + total_elapsed_micros: self.total_elapsed_micros.load(Ordering::Relaxed), + read_elapsed_micros: self.read_elapsed_micros.load(Ordering::Relaxed), + write_elapsed_micros: self.write_elapsed_micros.load(Ordering::Relaxed), + seek_elapsed_micros: self.seek_elapsed_micros.load(Ordering::Relaxed), + } + } +} + +pub fn reset_fs_trace() { + FS_TRACE.reset(); +} + +pub fn fs_trace_snapshot() -> FsTraceSnapshot { + FS_TRACE.snapshot() +} + +pub(super) fn wasi_root_with_devices( + root: Arc, +) -> virtual_fs::Result> { + let devices: Arc = + Arc::new(virtual_fs::RootFileSystemBuilder::default().build_tmp_ext(&[])); + let root_with_default_dirs: Arc = + Arc::new(virtual_fs::OverlayFileSystem::new( + virtual_fs::ArcFileSystem::new(root), + [virtual_fs::ArcFileSystem::new(devices.clone())], + )); + let mount = virtual_fs::MountFileSystem::new(); + mount.mount(Path::new("/"), root_with_default_dirs)?; + for name in WASIX_DEVICE_FILES { + let path = Path::new("/dev").join(name); + mount.mount_with_source(&path, &path, devices.clone())?; + } + Ok(Arc::new(mount)) +} + +pub(super) struct EagerCopyOverlayFileSystem { + upper_root: PathBuf, + lower_root: PathBuf, + overlay: + virtual_fs::OverlayFileSystem, +} + +impl fmt::Debug for EagerCopyOverlayFileSystem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EagerCopyOverlayFileSystem") + .field("upper_root", &self.upper_root) + .field("lower_root", &self.lower_root) + .finish_non_exhaustive() + } +} + +impl EagerCopyOverlayFileSystem { + pub(super) fn new(upper_root: PathBuf, lower_root: PathBuf) -> Result { + fs::create_dir_all(&upper_root) + .with_context(|| format!("create PGDATA overlay upper {}", upper_root.display()))?; + let upper_root = upper_root.canonicalize().with_context(|| { + format!("canonicalize PGDATA overlay upper {}", upper_root.display()) + })?; + let lower_root = lower_root.canonicalize().with_context(|| { + format!("canonicalize PGDATA overlay lower {}", lower_root.display()) + })?; + let upper = virtual_fs::ArcFileSystem::new(host_filesystem(&upper_root)?); + let lower = virtual_fs::ArcFileSystem::new(host_filesystem(&lower_root)?); + Ok(Self { + upper_root, + lower_root, + overlay: virtual_fs::OverlayFileSystem::new(upper, [lower]), + }) + } + + fn ensure_upper_copy( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result<()> { + let Some(relative) = normalize_overlay_path(path)? else { + return Ok(()); + }; + + let upper = self.upper_root.join(&relative); + if upper.exists() { + return Ok(()); + } + + let lower = self.lower_root.join(&relative); + let metadata = match fs::symlink_metadata(&lower) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + if conf.create || conf.create_new { + self.ensure_upper_parent(&relative)?; + } + return Ok(()); + } + Err(err) => return Err(err.into()), + }; + + if conf.create_new { + return Err(virtual_fs::FsError::AlreadyExists); + } + if metadata.is_dir() { + return Ok(()); + } + if !metadata.is_file() { + return Err(virtual_fs::FsError::Unsupported); + } + + if let Some(parent) = upper.parent() { + fs::create_dir_all(parent).map_err(virtual_fs::FsError::from)?; + } + if conf.truncate && !conf.read && !conf.append { + fs::File::create(&upper).map_err(virtual_fs::FsError::from)?; + } else { + fs::copy(&lower, &upper).map_err(virtual_fs::FsError::from)?; + } + Ok(()) + } + + fn ensure_upper_parent(&self, relative: &Path) -> virtual_fs::Result<()> { + let Some(parent) = relative.parent() else { + return Ok(()); + }; + if parent.as_os_str().is_empty() { + return Ok(()); + } + + let upper_parent = self.upper_root.join(parent); + if upper_parent.is_dir() { + return Ok(()); + } + + let lower_parent = self.lower_root.join(parent); + let metadata = match fs::symlink_metadata(&lower_parent) { + Ok(metadata) => metadata, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(virtual_fs::FsError::EntryNotFound); + } + Err(err) => return Err(err.into()), + }; + if !metadata.is_dir() { + return Err(virtual_fs::FsError::BaseNotDirectory); + } + + fs::create_dir_all(upper_parent).map_err(virtual_fs::FsError::from) + } +} + +impl virtual_fs::FileSystem for EagerCopyOverlayFileSystem { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + self.overlay.readlink(path) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.overlay.read_dir(path) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + if let Some(relative) = normalize_overlay_path(path)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.create_dir(path) + } + + fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { + if let Some(relative) = normalize_overlay_path(target)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.create_symlink(source, target) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.overlay.remove_dir(path) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + self.ensure_upper_copy(from, &mutating_open_config())?; + if let Some(relative) = normalize_overlay_path(to)? { + self.ensure_upper_parent(&relative)?; + } + self.overlay.rename(from, to).await + }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.overlay.metadata(path) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + self.overlay.symlink_metadata(path) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.overlay.remove_file(path) + } + + fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { + virtual_fs::OpenOptions::new(self) + } +} + +impl virtual_fs::FileOpener for EagerCopyOverlayFileSystem { + fn open( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result> { + if conf.would_mutate() { + self.ensure_upper_copy(path, conf)?; + } + virtual_fs::FileSystem::new_open_options(&self.overlay) + .options(conf.clone()) + .open(path) + } +} + +fn normalize_overlay_path(path: &Path) -> virtual_fs::Result> { + let mut relative = PathBuf::new(); + for component in path.components() { + match component { + Component::RootDir | Component::CurDir => {} + Component::Normal(part) => relative.push(part), + Component::ParentDir | Component::Prefix(_) => { + return Err(virtual_fs::FsError::PermissionDenied); + } + } + } + if relative.as_os_str().is_empty() { + Ok(None) + } else { + Ok(Some(relative)) + } +} + +fn mutating_open_config() -> virtual_fs::OpenOptionsConfig { + virtual_fs::OpenOptionsConfig { + read: true, + write: true, + create_new: false, + create: false, + append: false, + truncate: false, + } +} + +pub(super) fn host_filesystem( + host_path: &Path, +) -> Result> { + let host_fs = SyncHostFileSystem::new(host_path) + .with_context(|| format!("create host fs rooted at {}", host_path.display()))?; + Ok(Arc::new(host_fs) as Arc) +} + +fn fs_trace_enabled() -> bool { + env_flag_enabled("OLIPHAUNT_WASM_WASIX_FS_TRACE") +} + +fn env_flag_enabled(name: &str) -> bool { + let Some(value) = std::env::var_os(name) else { + return false; + }; + !matches!( + value.to_string_lossy().to_ascii_lowercase().as_str(), + "" | "0" | "false" | "off" | "no" + ) +} + +pub(super) fn maybe_trace_filesystem( + inner: Arc, +) -> Arc { + if fs_trace_enabled() { + Arc::new(TracedFileSystem { inner }) as Arc + } else { + inner + } +} + +#[derive(Debug)] +struct TracedFileSystem { + inner: Arc, +} + +impl TracedFileSystem { + fn record(&self, counter: &AtomicU64, operation: impl FnOnce() -> T) -> T { + counter.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = operation(); + FS_TRACE.record_total(started.elapsed()); + result + } +} + +impl virtual_fs::FileSystem for TracedFileSystem { + fn readlink(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || self.inner.readlink(path)) + } + + fn read_dir(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.read_dir_count, || self.inner.read_dir(path)) + } + + fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.create_dir_count, || self.inner.create_dir(path)) + } + + fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.create_dir_count, || { + self.inner.create_symlink(source, target) + }) + } + + fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.remove_dir_count, || self.inner.remove_dir(path)) + } + + fn rename<'a>( + &'a self, + from: &'a Path, + to: &'a Path, + ) -> Pin> + Send + 'a>> { + FS_TRACE.rename_count.fetch_add(1, Ordering::Relaxed); + Box::pin(async move { + let started = Instant::now(); + let result = self.inner.rename(from, to).await; + FS_TRACE.record_total(started.elapsed()); + result + }) + } + + fn metadata(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || self.inner.metadata(path)) + } + + fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { + self.record(&FS_TRACE.metadata_count, || { + self.inner.symlink_metadata(path) + }) + } + + fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { + self.record(&FS_TRACE.remove_file_count, || self.inner.remove_file(path)) + } + + fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { + virtual_fs::OpenOptions::new(self) + } +} + +impl virtual_fs::FileOpener for TracedFileSystem { + fn open( + &self, + path: &Path, + conf: &virtual_fs::OpenOptionsConfig, + ) -> virtual_fs::Result> { + FS_TRACE.open_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let file = virtual_fs::FileSystem::new_open_options(&self.inner) + .options(conf.clone()) + .open(path); + FS_TRACE.record_total(started.elapsed()); + file.map(|inner| Box::new(TracedVirtualFile { inner }) as _) + } +} + +#[derive(Debug)] +struct TracedVirtualFile { + inner: Box, +} + +impl virtual_fs::VirtualFile for TracedVirtualFile { + fn last_accessed(&self) -> u64 { + self.inner.last_accessed() + } + + fn last_modified(&self) -> u64 { + self.inner.last_modified() + } + + fn created_time(&self) -> u64 { + self.inner.created_time() + } + + fn set_times(&mut self, atime: Option, mtime: Option) -> virtual_fs::Result<()> { + self.inner.set_times(atime, mtime) + } + + fn size(&self) -> u64 { + self.inner.size() + } + + fn set_len(&mut self, new_size: u64) -> virtual_fs::Result<()> { + FS_TRACE.set_len_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = self.inner.set_len(new_size); + FS_TRACE.record_total(started.elapsed()); + result + } + + fn unlink(&mut self) -> virtual_fs::Result<()> { + FS_TRACE.unlink_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = self.inner.unlink(); + FS_TRACE.record_total(started.elapsed()); + result + } + + fn is_open(&self) -> bool { + self.inner.is_open() + } + + fn get_special_fd(&self) -> Option { + self.inner.get_special_fd() + } + + fn write_from_mmap(&mut self, offset: u64, len: u64) -> io::Result<()> { + self.inner.write_from_mmap(offset, len) + } + + fn poll_read_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_read_ready(cx) + } + + fn poll_write_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_write_ready(cx) + } +} + +impl virtual_fs::AsyncRead for TracedVirtualFile { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut TaskContext<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let before = buf.filled().len(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_read(cx, buf); + if let Poll::Ready(Ok(())) = &result { + let bytes = buf.filled().len().saturating_sub(before) as u64; + FS_TRACE.read_count.fetch_add(1, Ordering::Relaxed); + FS_TRACE.read_bytes.fetch_add(bytes, Ordering::Relaxed); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.read_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } +} + +impl virtual_fs::AsyncWrite for TracedVirtualFile { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut TaskContext<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_write(cx, buf); + if let Poll::Ready(Ok(bytes)) = &result { + FS_TRACE.write_count.fetch_add(1, Ordering::Relaxed); + FS_TRACE + .write_bytes + .fetch_add(*bytes as u64, Ordering::Relaxed); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.write_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + Pin::new(&mut *this.inner).poll_shutdown(cx) + } +} + +impl virtual_fs::AsyncSeek for TracedVirtualFile { + fn start_seek(self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { + let this = self.get_mut(); + FS_TRACE.seek_count.fetch_add(1, Ordering::Relaxed); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).start_seek(position); + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.seek_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + result + } + + fn poll_complete(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + let started = Instant::now(); + let result = Pin::new(&mut *this.inner).poll_complete(cx); + if let Poll::Ready(Ok(_)) = &result { + let elapsed = started.elapsed(); + FS_TRACE.record_total(elapsed); + FS_TRACE.seek_elapsed_micros.fetch_add( + elapsed.as_micros().min(u64::MAX as u128) as u64, + Ordering::Relaxed, + ); + } + result + } +} diff --git a/src/pglite/proxy.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/proxy.rs similarity index 98% rename from src/pglite/proxy.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/proxy.rs index a4cae7b6..de8b5b4d 100644 --- a/src/pglite/proxy.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/proxy.rs @@ -11,18 +11,18 @@ use std::sync::{ mpsc::SyncSender, }; -use crate::pglite::backend::{BackendOpenKind, BackendSession}; +use crate::oliphaunt::backend::{BackendOpenKind, BackendSession}; #[cfg(feature = "extensions")] -use crate::pglite::base::install_missing_extension_archives; -use crate::pglite::base::{InstallOutcome, install_into}; -use crate::pglite::config::{PostgresConfig, StartupConfig}; +use crate::oliphaunt::base::install_missing_extension_archives; +use crate::oliphaunt::base::{InstallOutcome, install_into}; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; #[cfg(feature = "extensions")] -use crate::pglite::extensions::Extension; -use crate::pglite::postgres_mod::{ +use crate::oliphaunt::extensions::Extension; +use crate::oliphaunt::postgres_mod::{ ProtocolPumpOutcome, ProtocolStream, StartupProtocolResponse, startup_error_response_output, }; -use crate::pglite::timing; -use crate::pglite::wire::{ +use crate::oliphaunt::timing; +use crate::oliphaunt::wire::{ FrontendFrameKind, FrontendFrameReader, classify_frontend_message, error_response, response_contains_error, simple_query_message, startup_config_for_message, startup_parameter, }; @@ -182,13 +182,13 @@ pub fn protocol_stats_snapshot() -> ProtocolStatsSnapshot { PROTOCOL_STATS.snapshot() } -/// Blocking PostgreSQL socket proxy for the embedded PGlite runtime. +/// Blocking PostgreSQL socket proxy for the embedded Oliphaunt runtime. /// /// The proxy intentionally runs each accepted connection on one blocking thread /// and does not call into the WASIX backend from an async runtime. That avoids /// nested runtime panics when an async wrapper blocks inside the embedded engine. #[derive(Debug, Clone)] -pub struct PgliteProxy { +pub struct OliphauntProxy { root: Arc, prepared_root: Option>, postgres_config: Arc, @@ -197,8 +197,8 @@ pub struct PgliteProxy { extensions: Arc>, } -impl PgliteProxy { - /// Create a proxy that stores the PGlite runtime and cluster under `root`. +impl OliphauntProxy { + /// Create a proxy that stores the Oliphaunt runtime and cluster under `root`. pub fn new(root: impl Into) -> Self { Self { root: Arc::new(root.into()), @@ -851,7 +851,7 @@ impl WireBackend { fn set_role(&mut self, user: &str) -> Result> { let sql = format!( "SET ROLE {}", - crate::pglite::templating::quote_identifier(user) + crate::oliphaunt::templating::quote_identifier(user) ); self.send(&simple_query_message(&sql)) } 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 new file mode 100644 index 00000000..30ff0aa2 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs @@ -0,0 +1,577 @@ +use std::net::{SocketAddr, TcpListener, TcpStream}; +#[cfg(unix)] +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::{Path, PathBuf}; +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc::{Receiver, sync_channel}, +}; +use std::thread::{self, JoinHandle}; + +use anyhow::{Context, Result, anyhow}; +use tempfile::TempDir; + +use crate::oliphaunt::base::{ + PreparedRoot, RootLock, RootPlan, RootSource, RootTarget, prepare_root, +}; +use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; +#[cfg(feature = "extensions")] +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, preflight_wasix_tools, run_server_psql, +}; +use crate::oliphaunt::proxy::OliphauntProxy; +use crate::oliphaunt::timing; + +/// A supervised local PostgreSQL socket backed by one embedded Oliphaunt runtime. +/// +/// This is the compatibility entry point for code that expects a PostgreSQL URL, +/// such as `tokio-postgres`, SQLx, or tools that speak the wire protocol. The +/// server owns one embedded backend, so downstream pools should use a single +/// connection. +#[derive(Debug)] +pub struct OliphauntServer { + root: PathBuf, + _temp_dir: Option, + _root_lock: Option, + endpoint: ServerEndpoint, + startup_config: StartupConfig, + shutdown: Arc, + handle: Option>>, +} + +#[derive(Debug, Clone)] +enum ServerEndpoint { + Tcp(SocketAddr), + #[cfg(unix)] + Unix(PathBuf), +} + +impl OliphauntServer { + /// Build a local Oliphaunt server. The default is a cached temporary database + /// served on `127.0.0.1:0`. + pub fn builder() -> OliphauntServerBuilder { + OliphauntServerBuilder::new() + } + + /// Start a cached temporary database on a random local TCP port. + pub fn temporary_tcp() -> Result { + Self::builder().temporary().start() + } + + /// Return the root directory used for runtime files and cluster data. + pub fn root(&self) -> &Path { + &self.root + } + + /// Return the bound TCP address, if this server is using TCP. + pub fn tcp_addr(&self) -> Option { + match self.endpoint { + ServerEndpoint::Tcp(addr) => Some(addr), + #[cfg(unix)] + ServerEndpoint::Unix(_) => None, + } + } + + /// Return the Unix-domain socket path, if this server is using UDS. + #[cfg(unix)] + pub fn socket_path(&self) -> Option<&Path> { + match &self.endpoint { + ServerEndpoint::Tcp(_) => None, + ServerEndpoint::Unix(path) => Some(path), + } + } + + /// Return a PostgreSQL connection URI for the local server. + pub fn connection_uri(&self) -> String { + match &self.endpoint { + ServerEndpoint::Tcp(addr) => tcp_connection_uri(*addr, &self.startup_config), + #[cfg(unix)] + ServerEndpoint::Unix(path) => { + let host = path.parent().unwrap_or_else(|| Path::new("/tmp")); + let port = parse_unix_socket_port(path).unwrap_or(5432); + format!( + "postgresql://{}@/{}?host={}&port={}&sslmode=disable", + self.startup_config.username, + self.startup_config.database, + percent_encode_query_value(&host.display().to_string()), + port + ) + } + } + } + + /// Alias for [`connection_uri`](Self::connection_uri). + pub fn database_url(&self) -> String { + self.connection_uri() + } + + /// Run the bundled WASIX `pg_dump` against this server and return SQL text. + #[cfg(feature = "tools")] + pub fn dump_sql(&self, options: PgDumpOptions) -> Result { + let addr = self + .tcp_addr() + .context("pg_dump currently requires a TCP OliphauntServer endpoint")?; + 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> { + 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 + /// one blocking backend connection at a time, so an open client can keep the + /// worker thread busy until it disconnects. + pub fn shutdown(mut self) -> Result<()> { + self.stop() + } + + fn stop(&mut self) -> Result<()> { + self.shutdown.store(true, Ordering::SeqCst); + { + let _phase = timing::phase("server.shutdown_wake"); + wake_listener(&self.endpoint); + } + if let Some(handle) = self.handle.take() { + let _phase = timing::phase("server.thread_join"); + handle + .join() + .map_err(|_| anyhow!("oliphaunt server thread panicked"))??; + } + Ok(()) + } +} + +impl Drop for OliphauntServer { + fn drop(&mut self) { + if let Err(err) = self.stop() { + tracing::warn!("oliphaunt server shutdown during drop failed: {err:#}"); + } + } +} + +/// Builder for [`OliphauntServer`]. +#[derive(Debug, Clone)] +pub struct OliphauntServerBuilder { + root: ServerRoot, + endpoint: ServerEndpointConfig, + postgres_config: PostgresConfig, + startup_config: StartupConfig, + #[cfg(feature = "extensions")] + extensions: Vec, +} + +#[derive(Debug, Clone)] +enum ServerRoot { + Temporary { template_cache: bool }, + Path(PathBuf), +} + +#[derive(Debug, Clone)] +enum ServerEndpointConfig { + Tcp(SocketAddr), + #[cfg(unix)] + Unix(PathBuf), +} + +impl Default for OliphauntServerBuilder { + fn default() -> Self { + Self { + root: ServerRoot::Temporary { + template_cache: true, + }, + endpoint: ServerEndpointConfig::Tcp(SocketAddr::from(([127, 0, 0, 1], 0))), + postgres_config: PostgresConfig::default(), + startup_config: StartupConfig::default(), + #[cfg(feature = "extensions")] + extensions: Vec::new(), + } + } +} + +impl OliphauntServerBuilder { + /// Create a builder. Defaults to a cached temporary database on + /// `127.0.0.1:0`. + pub fn new() -> Self { + Self::default() + } + + /// Serve a persistent database rooted at `root`. + pub fn path(mut self, root: impl Into) -> Self { + self.root = ServerRoot::Path(root.into()); + self + } + + /// Serve a temporary database cloned from the process-local template cache. + pub fn temporary(mut self) -> Self { + self.root = ServerRoot::Temporary { + template_cache: true, + }; + self + } + + /// Serve a temporary database initialized without the template cache. + /// + /// This is a compatibility alias for the pre-template-cache public API. + /// Fresh initdb uses the bundled split WASIX `initdb` module; cached + /// temporary databases remain the production fast path. + pub fn fresh_temporary(mut self) -> Self { + self.root = ServerRoot::Temporary { + template_cache: false, + }; + self + } + + /// Bind the server to a TCP address. + pub fn tcp(mut self, addr: SocketAddr) -> Self { + self.endpoint = ServerEndpointConfig::Tcp(addr); + self + } + + /// Bind the server to a Unix-domain socket path. + #[cfg(unix)] + pub fn unix(mut self, path: impl Into) -> Self { + self.endpoint = ServerEndpointConfig::Unix(path.into()); + self + } + + /// Set a PostgreSQL startup GUC for the embedded backend used by this + /// server. + pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { + self.postgres_config.insert(name, value); + self + } + + /// Set multiple PostgreSQL startup GUCs for the embedded backend used by + /// this server. + pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + for (name, value) in settings { + self.postgres_config.insert(name, value); + } + self + } + + /// Default user encoded in [`OliphauntServer::database_url`]. + pub fn username(mut self, username: impl Into) -> Self { + self.startup_config.username = username.into(); + self + } + + /// Default database encoded in [`OliphauntServer::database_url`]. + pub fn database(mut self, database: impl Into) -> Self { + self.startup_config.database = database.into(); + self + } + + /// Enable PostgreSQL debug logging level `0..=5` for server backends. + pub fn debug_level(mut self, level: DebugLevel) -> Self { + self.startup_config.debug_level = Some(level); + self + } + + /// Use lower durability settings for ephemeral or cacheable local + /// workloads. + pub fn relaxed_durability(mut self, enabled: bool) -> Self { + self.startup_config.relaxed_durability = enabled; + self + } + + /// Append an advanced PostgreSQL startup argument for server backends. + pub fn startup_arg(mut self, arg: impl Into) -> Self { + self.startup_config.extra_args.push(arg.into()); + self + } + + /// Append advanced PostgreSQL startup arguments for server backends. + pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { + self.startup_config + .extra_args + .extend(args.into_iter().map(Into::into)); + self + } + + /// Enable a bundled Postgres extension before serving connections. + #[cfg(feature = "extensions")] + pub fn extension(mut self, extension: Extension) -> Self { + self.extensions.push(extension); + self + } + + /// Enable bundled Postgres extensions before serving connections. + #[cfg(feature = "extensions")] + pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { + self.extensions.extend(extensions); + self + } + + /// Install the runtime if needed, initialize the cluster, and start serving. + pub fn start(self) -> Result { + self.postgres_config.validate()?; + self.startup_config.validate()?; + #[cfg(feature = "extensions")] + let extensions = resolve_extension_set(&self.extensions)?; + let postgres_config = self.postgres_config.clone(); + let startup_config = self.startup_config.clone(); + + let prepared_root = { + let _phase = timing::phase("server.root_prepare"); + match self.root { + ServerRoot::Path(root) => { + let _phase = timing::phase("server.root_prepare.path"); + let plan = RootPlan::new(RootTarget::Path(root), RootSource::Template); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); + prepare_root(plan)? + } + ServerRoot::Temporary { template_cache } => { + let source = if template_cache { + RootSource::Template + } else { + RootSource::FreshInitdb + }; + let phase = if template_cache { + "server.root_prepare.temporary_cached" + } else { + "server.root_prepare.temporary_fresh" + }; + let _phase = timing::phase(phase); + let plan = RootPlan::new(RootTarget::Temporary, source); + #[cfg(feature = "extensions")] + let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); + run_blocking("oliphaunt-template-cache", move || prepare_root(plan))? + } + } + }; + let PreparedRoot { + root, + temp_dir, + root_lock, + outcome, + } = prepared_root; + + let shutdown = Arc::new(AtomicBool::new(false)); + let proxy = { + let _phase = timing::phase("server.proxy_create"); + OliphauntProxy::new(root.clone()).with_prepared_root(outcome) + }; + let proxy = proxy + .with_postgres_config(postgres_config) + .with_startup_config(startup_config.clone()); + #[cfg(feature = "extensions")] + let proxy = proxy.with_extensions(extensions); + + let (endpoint, handle) = match self.endpoint { + ServerEndpointConfig::Tcp(addr) => start_tcp(proxy, addr, shutdown.clone())?, + #[cfg(unix)] + ServerEndpointConfig::Unix(path) => start_unix(proxy, path, shutdown.clone())?, + }; + + Ok(OliphauntServer { + root, + _temp_dir: temp_dir, + _root_lock: root_lock, + endpoint, + startup_config, + shutdown, + handle: Some(handle), + }) + } +} + +fn start_tcp( + proxy: OliphauntProxy, + addr: SocketAddr, + shutdown: Arc, +) -> Result<(ServerEndpoint, JoinHandle>)> { + let listener = { + let _phase = timing::phase("server.tcp_bind"); + TcpListener::bind(addr).context("bind Oliphaunt TCP server")? + }; + let addr = { + let _phase = timing::phase("server.tcp_local_addr"); + listener + .local_addr() + .context("read Oliphaunt TCP address")? + }; + let (ready_tx, ready_rx) = sync_channel(1); + let recorder = timing::current_recorder(); + let handle = { + let _phase = timing::phase("server.thread_spawn"); + thread::spawn(move || { + timing::with_recorder(recorder, || { + proxy.serve_tcp_listener_until_ready(listener, shutdown, Some(ready_tx)) + }) + }) + }; + { + let _phase = timing::phase("server.wait_ready"); + wait_until_ready(&ready_rx)?; + } + Ok((ServerEndpoint::Tcp(addr), handle)) +} + +fn tcp_connection_uri(addr: SocketAddr, startup: &StartupConfig) -> String { + match addr { + SocketAddr::V4(addr) => { + format!( + "postgresql://{}@{}:{}/{}?sslmode=disable", + startup.username, + addr.ip(), + addr.port(), + startup.database + ) + } + SocketAddr::V6(addr) => { + format!( + "postgresql://{}@[{}]:{}/{}?sslmode=disable", + startup.username, + addr.ip(), + addr.port(), + startup.database + ) + } + } +} + +fn run_blocking(name: &'static str, f: F) -> Result +where + T: Send + 'static, + F: FnOnce() -> Result + Send + 'static, +{ + let recorder = timing::current_recorder(); + thread::Builder::new() + .name(name.to_string()) + .spawn(move || timing::with_recorder(recorder, f)) + .with_context(|| format!("spawn {name} worker"))? + .join() + .map_err(|_| anyhow!("{name} worker panicked"))? +} + +#[cfg(unix)] +fn start_unix( + proxy: OliphauntProxy, + path: PathBuf, + shutdown: Arc, +) -> Result<(ServerEndpoint, JoinHandle>)> { + { + let _phase = timing::phase("server.unix_prepare_path"); + if path.exists() { + std::fs::remove_file(&path) + .with_context(|| format!("remove stale socket {}", path.display()))?; + } + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("create socket directory {}", parent.display()))?; + } + } + + let listener = { + let _phase = timing::phase("server.unix_bind"); + UnixListener::bind(&path) + .with_context(|| format!("bind Oliphaunt Unix socket {}", path.display()))? + }; + let endpoint = ServerEndpoint::Unix(path); + let (ready_tx, ready_rx) = sync_channel(1); + let recorder = timing::current_recorder(); + let handle = { + let _phase = timing::phase("server.thread_spawn"); + thread::spawn(move || { + timing::with_recorder(recorder, || { + proxy.serve_unix_listener_until_ready(listener, shutdown, Some(ready_tx)) + }) + }) + }; + { + let _phase = timing::phase("server.wait_ready"); + wait_until_ready(&ready_rx)?; + } + Ok((endpoint, handle)) +} + +fn wait_until_ready(ready_rx: &Receiver>) -> Result<()> { + ready_rx + .recv() + .context("Oliphaunt server thread exited before reporting readiness")? +} + +fn wake_listener(endpoint: &ServerEndpoint) { + match endpoint { + ServerEndpoint::Tcp(addr) => { + let _ = TcpStream::connect(addr); + } + #[cfg(unix)] + ServerEndpoint::Unix(path) => { + let _ = UnixStream::connect(path); + } + } +} + +#[cfg(unix)] +fn parse_unix_socket_port(path: &Path) -> Option { + let name = path.file_name()?.to_str()?; + name.strip_prefix(".s.PGSQL.")?.parse().ok() +} + +#[cfg(unix)] +fn percent_encode_query_value(value: &str) -> String { + let mut encoded = String::with_capacity(value.len()); + for byte in value.bytes() { + if matches!( + byte, + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' + ) { + encoded.push(byte as char); + } else { + encoded.push_str(&format!("%{byte:02X}")); + } + } + encoded +} + +#[cfg(all(test, unix))] +mod tests { + use super::percent_encode_query_value; + + #[test] + fn unix_socket_uri_host_is_query_encoded() { + assert_eq!( + percent_encode_query_value("/tmp/Application Support/oliphaunt"), + "/tmp/Application%20Support/oliphaunt" + ); + } +} diff --git a/src/pglite/sync_host_fs.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/sync_host_fs.rs similarity index 100% rename from src/pglite/sync_host_fs.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/sync_host_fs.rs diff --git a/src/pglite/templating.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/templating.rs similarity index 94% rename from src/pglite/templating.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/templating.rs index a00d27ce..13188cd4 100644 --- a/src/pglite/templating.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/templating.rs @@ -3,9 +3,9 @@ use regex::Regex; use serde_json::Value; use std::sync::LazyLock; -use crate::pglite::client::Pglite; -use crate::pglite::interface::QueryOptions; -use crate::pglite::types::TEXT; +use crate::oliphaunt::client::Oliphaunt; +use crate::oliphaunt::interface::QueryOptions; +use crate::oliphaunt::types::TEXT; #[derive(Debug, Clone)] pub struct TemplatedQuery { @@ -58,7 +58,7 @@ pub fn quote_identifier(ident: &str) -> String { format!("\"{}\"", escaped) } -pub fn format_query(pg: &mut Pglite, query: &str, params: &[Value]) -> Result { +pub fn format_query(pg: &mut Oliphaunt, query: &str, params: &[Value]) -> Result { if params.is_empty() { return Ok(query.to_string()); } diff --git a/src/pglite/timing.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/timing.rs similarity index 100% rename from src/pglite/timing.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/timing.rs diff --git a/src/pglite/transport.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/transport.rs similarity index 83% rename from src/pglite/transport.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/transport.rs index 8e6332ce..202f7007 100644 --- a/src/pglite/transport.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/transport.rs @@ -1,9 +1,9 @@ use anyhow::{Result, bail}; use super::postgres_mod::PostgresMod; -use crate::pglite::interface::DataTransferContainer; +use crate::oliphaunt::interface::DataTransferContainer; -/// Protocol transport for the WASIX PGlite backend. +/// Protocol transport for the WASIX Oliphaunt backend. pub enum Transport { Wasix, } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/types.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/types.rs new file mode 100644 index 00000000..6888358d --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/types.rs @@ -0,0 +1,578 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; +use std::sync::LazyLock; + +use anyhow::{Result, anyhow}; +use serde_json::{Value, json}; + +use super::interface::{ParserMap, Serializer, SerializerMap, TypeParser}; + +macro_rules! const_oid { + ($name:ident = $value:expr) => { + pub const $name: i32 = $value; + }; +} + +const_oid!(BOOL = 16); +const_oid!(BYTEA = 17); +const_oid!(CHAR = 18); +const_oid!(INT8 = 20); +const_oid!(INT2 = 21); +const_oid!(INT4 = 23); +const_oid!(TEXT = 25); +const_oid!(OID = 26); +const_oid!(JSON = 114); +const_oid!(FLOAT4 = 700); +const_oid!(FLOAT8 = 701); +const_oid!(DATE = 1082); +const_oid!(TIMESTAMP = 1114); +const_oid!(TIMESTAMPTZ = 1184); +const_oid!(NUMERIC = 1700); +const_oid!(UUID = 2950); +const_oid!(JSONB = 3802); + +pub static DEFAULT_PARSERS: LazyLock = LazyLock::new(build_default_parsers); +pub static DEFAULT_SERIALIZERS: LazyLock = LazyLock::new(build_default_serializers); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ArrayTypeInfo { + pub element_oid: i32, + pub array_oid: i32, + pub delimiter: char, +} + +impl ArrayTypeInfo { + pub const fn new(element_oid: i32, array_oid: i32, delimiter: char) -> Self { + Self { + element_oid, + array_oid, + delimiter, + } + } +} + +pub struct ParserLookup<'a> { + defaults: &'a ParserMap, + overrides: &'a ParserMap, +} + +impl<'a> ParserLookup<'a> { + pub fn new(defaults: &'a ParserMap, overrides: &'a ParserMap) -> Self { + Self { + defaults, + overrides, + } + } + + pub fn apply(&self, text: &str, type_id: i32) -> Value { + let parser = self + .overrides + .get(&type_id) + .or_else(|| self.defaults.get(&type_id)); + if let Some(parser) = parser { + parser(text, type_id) + } else { + json!(text) + } + } +} + +pub fn serialize_array_value( + value: &Value, + element_serializer: Option, + delimiter: char, +) -> Result { + match value { + Value::Array(items) => { + if items.is_empty() { + return Ok("{}".to_string()); + } + + let mut parts = Vec::with_capacity(items.len()); + for item in items { + match item { + Value::Null => parts.push("null".to_string()), + Value::Array(_) => { + parts.push(serialize_array_value( + item, + element_serializer.clone(), + delimiter, + )?); + } + _ => { + let raw = if let Some(serializer) = element_serializer.as_ref() { + serializer(item)? + } else { + value_to_string(item) + }; + let escaped = raw.replace('\\', "\\\\").replace('"', "\\\""); + parts.push(format!("\"{}\"", escaped)); + } + } + } + let joined = parts.join(&delimiter.to_string()); + Ok(format!("{{{}}}", joined)) + } + Value::Null => Ok("null".to_string()), + _ => Ok(value_to_string(value)), + } +} + +fn value_to_string(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => { + if *b { + "t".to_string() + } else { + "f".to_string() + } + } + Value::Null => "null".to_string(), + Value::Array(_) => value.to_string(), + _ => value.to_string(), + } +} + +pub fn parse_array_text( + text: &str, + element_parser: Option, + element_type_id: i32, + delimiter: char, +) -> Value { + let Some(start) = text.find('{') else { + return Value::Array(Vec::new()); + }; + let mut parser = ArrayTextParser { + text, + index: start, + element_parser: element_parser.as_ref(), + element_type_id, + delimiter, + }; + parser.parse_array() +} + +struct ArrayTextParser<'a> { + text: &'a str, + index: usize, + element_parser: Option<&'a TypeParser>, + element_type_id: i32, + delimiter: char, +} + +impl ArrayTextParser<'_> { + fn parse_array(&mut self) -> Value { + if self.peek() != Some('{') { + return Value::Array(Vec::new()); + } + self.advance(); + + let mut values = Vec::new(); + loop { + match self.peek() { + Some('}') => { + self.advance(); + return Value::Array(values); + } + Some('{') => values.push(self.parse_array()), + Some('"') => values.push(self.parse_quoted()), + Some(_) => values.push(self.parse_unquoted()), + None => return Value::Array(values), + } + + match self.peek() { + Some(ch) if ch == self.delimiter => { + self.advance(); + } + Some('}') => {} + Some(_) | None => {} + } + } + } + + fn parse_quoted(&mut self) -> Value { + self.advance(); + let mut value = String::new(); + + while let Some(ch) = self.peek() { + self.advance(); + match ch { + '\\' => { + if let Some(escaped) = self.peek() { + self.advance(); + value.push(escaped); + } + } + '"' => { + return apply_element_parser( + &value, + self.element_parser, + self.element_type_id, + true, + ); + } + other => value.push(other), + } + } + + apply_element_parser(&value, self.element_parser, self.element_type_id, true) + } + + fn parse_unquoted(&mut self) -> Value { + let start = self.index; + while let Some(ch) = self.peek() { + if ch == self.delimiter || ch == '}' { + break; + } + self.advance(); + } + + let slice = self.text[start..self.index].trim(); + apply_element_parser(slice, self.element_parser, self.element_type_id, false) + } + + fn peek(&self) -> Option { + self.text[self.index..].chars().next() + } + + fn advance(&mut self) { + if let Some(ch) = self.peek() { + self.index += ch.len_utf8(); + } + } +} + +fn apply_element_parser( + slice: &str, + parser: Option<&TypeParser>, + element_type_id: i32, + quoted: bool, +) -> Value { + if let Some(p) = parser { + p(slice, element_type_id) + } else if !quoted && slice.eq_ignore_ascii_case("NULL") { + Value::Null + } else { + Value::String(slice.to_string()) + } +} + +fn build_default_parsers() -> ParserMap { + let mut map: ParserMap = HashMap::new(); + + map.insert( + TEXT, + Arc::new(|value: &str, _| json!(value.to_string())) as TypeParser, + ); + map.insert(CHAR, Arc::new(|value: &str, _| json!(value.to_string()))); + + map.insert(INT2, Arc::new(|value: &str, _| parse_int(value))); + map.insert(INT4, Arc::new(|value: &str, _| parse_int(value))); + map.insert(INT8, Arc::new(|value: &str, _| parse_bigint(value))); + map.insert(OID, Arc::new(|value: &str, _| parse_int(value))); + map.insert(NUMERIC, Arc::new(|value: &str, _| parse_numeric(value))); + + map.insert(FLOAT4, Arc::new(|value: &str, _| parse_float(value))); + map.insert(FLOAT8, Arc::new(|value: &str, _| parse_float(value))); + + map.insert(BOOL, Arc::new(|value: &str, _| json!(value == "t"))); + + map.insert(JSON, Arc::new(|value: &str, _| parse_json(value))); + map.insert(JSONB, Arc::new(|value: &str, _| parse_json(value))); + + map.insert(BYTEA, Arc::new(|value: &str, _| parse_bytea(value))); + + map.insert(UUID, Arc::new(|value: &str, _| json!(value.to_string()))); + + map.insert( + TIMESTAMP, + Arc::new(|value: &str, _| json!(value.to_string())), + ); + map.insert( + TIMESTAMPTZ, + Arc::new(|value: &str, _| json!(value.to_string())), + ); + map.insert(DATE, Arc::new(|value: &str, _| json!(value.to_string()))); + + register_builtin_array_parsers(&mut map); + map +} + +fn build_default_serializers() -> SerializerMap { + let mut map: SerializerMap = HashMap::new(); + + map.insert( + TEXT, + Arc::new(|value: &Value| serialize_string(value)) as Serializer, + ); + map.insert(CHAR, Arc::new(|value: &Value| serialize_string(value))); + + map.insert(INT2, Arc::new(|value: &Value| serialize_number(value))); + map.insert(INT4, Arc::new(|value: &Value| serialize_number(value))); + map.insert(INT8, Arc::new(|value: &Value| serialize_number(value))); + map.insert(OID, Arc::new(|value: &Value| serialize_number(value))); + map.insert(NUMERIC, Arc::new(|value: &Value| serialize_number(value))); + map.insert(FLOAT4, Arc::new(|value: &Value| serialize_number(value))); + map.insert(FLOAT8, Arc::new(|value: &Value| serialize_number(value))); + + map.insert(BOOL, Arc::new(|value: &Value| serialize_bool(value))); + map.insert(JSON, Arc::new(|value: &Value| serialize_json(value))); + map.insert(JSONB, Arc::new(|value: &Value| serialize_json(value))); + map.insert(BYTEA, Arc::new(|value: &Value| serialize_bytea(value))); + map.insert(UUID, Arc::new(|value: &Value| serialize_string(value))); + map.insert(TIMESTAMP, Arc::new(|value: &Value| serialize_string(value))); + map.insert( + TIMESTAMPTZ, + Arc::new(|value: &Value| serialize_string(value)), + ); + map.insert(DATE, Arc::new(|value: &Value| serialize_string(value))); + + register_builtin_array_serializers(&mut map); + map +} + +pub fn register_array_type( + parsers: &mut ParserMap, + serializers: &mut SerializerMap, + info: ArrayTypeInfo, +) { + register_array_parser(parsers, info); + register_array_serializer(serializers, info); +} + +fn register_array_parser(parsers: &mut ParserMap, info: ArrayTypeInfo) { + let element_parser = parsers.get(&info.element_oid).cloned(); + let element_oid = info.element_oid; + let delimiter = info.delimiter; + let array_parser: TypeParser = Arc::new(move |text: &str, _| { + parse_array_text(text, element_parser.clone(), element_oid, delimiter) + }); + parsers.insert(info.array_oid, array_parser); +} + +fn register_array_serializer(serializers: &mut SerializerMap, info: ArrayTypeInfo) { + let element_serializer = serializers.get(&info.element_oid).cloned(); + let delimiter = info.delimiter; + let array_serializer: Serializer = Arc::new(move |value: &Value| { + serialize_array_value(value, element_serializer.clone(), delimiter) + }); + serializers.insert(info.array_oid, array_serializer); +} + +fn register_builtin_array_parsers(parsers: &mut ParserMap) { + for info in BUILTIN_ARRAY_TYPES { + register_array_parser(parsers, *info); + } +} + +fn register_builtin_array_serializers(serializers: &mut SerializerMap) { + for info in BUILTIN_ARRAY_TYPES { + register_array_serializer(serializers, *info); + } +} + +// Generated from PostgreSQL's built-in pg_type.dat OID assignments for the +// default Oliphaunt/Postgres 17 catalog. Keep this list to built-in types only: +// extension and runtime-created custom arrays are discovered through the direct +// client type cache when they are actually used. +const BUILTIN_ARRAY_TYPES: &[ArrayTypeInfo] = &[ + ArrayTypeInfo::new(16, 1000, ','), + ArrayTypeInfo::new(17, 1001, ','), + ArrayTypeInfo::new(18, 1002, ','), + ArrayTypeInfo::new(19, 1003, ','), + ArrayTypeInfo::new(20, 1016, ','), + ArrayTypeInfo::new(21, 1005, ','), + ArrayTypeInfo::new(22, 1006, ','), + ArrayTypeInfo::new(23, 1007, ','), + ArrayTypeInfo::new(24, 1008, ','), + ArrayTypeInfo::new(25, 1009, ','), + ArrayTypeInfo::new(26, 1028, ','), + ArrayTypeInfo::new(27, 1010, ','), + ArrayTypeInfo::new(28, 1011, ','), + ArrayTypeInfo::new(29, 1012, ','), + ArrayTypeInfo::new(30, 1013, ','), + ArrayTypeInfo::new(114, 199, ','), + ArrayTypeInfo::new(142, 143, ','), + ArrayTypeInfo::new(600, 1017, ','), + ArrayTypeInfo::new(601, 1018, ','), + ArrayTypeInfo::new(602, 1019, ','), + ArrayTypeInfo::new(603, 1020, ';'), + ArrayTypeInfo::new(604, 1027, ','), + ArrayTypeInfo::new(628, 629, ','), + ArrayTypeInfo::new(700, 1021, ','), + ArrayTypeInfo::new(701, 1022, ','), + ArrayTypeInfo::new(718, 719, ','), + ArrayTypeInfo::new(790, 791, ','), + ArrayTypeInfo::new(829, 1040, ','), + ArrayTypeInfo::new(869, 1041, ','), + ArrayTypeInfo::new(650, 651, ','), + ArrayTypeInfo::new(774, 775, ','), + ArrayTypeInfo::new(1033, 1034, ','), + ArrayTypeInfo::new(1042, 1014, ','), + ArrayTypeInfo::new(1043, 1015, ','), + ArrayTypeInfo::new(1082, 1182, ','), + ArrayTypeInfo::new(1083, 1183, ','), + ArrayTypeInfo::new(1114, 1115, ','), + ArrayTypeInfo::new(1184, 1185, ','), + ArrayTypeInfo::new(1186, 1187, ','), + ArrayTypeInfo::new(1266, 1270, ','), + ArrayTypeInfo::new(1560, 1561, ','), + ArrayTypeInfo::new(1562, 1563, ','), + ArrayTypeInfo::new(1700, 1231, ','), + ArrayTypeInfo::new(1790, 2201, ','), + ArrayTypeInfo::new(2202, 2207, ','), + ArrayTypeInfo::new(2203, 2208, ','), + ArrayTypeInfo::new(2204, 2209, ','), + ArrayTypeInfo::new(2205, 2210, ','), + ArrayTypeInfo::new(4191, 4192, ','), + ArrayTypeInfo::new(2206, 2211, ','), + ArrayTypeInfo::new(4096, 4097, ','), + ArrayTypeInfo::new(4089, 4090, ','), + ArrayTypeInfo::new(2950, 2951, ','), + ArrayTypeInfo::new(3220, 3221, ','), + ArrayTypeInfo::new(3614, 3643, ','), + ArrayTypeInfo::new(3642, 3644, ','), + ArrayTypeInfo::new(3615, 3645, ','), + ArrayTypeInfo::new(3734, 3735, ','), + ArrayTypeInfo::new(3769, 3770, ','), + ArrayTypeInfo::new(3802, 3807, ','), + ArrayTypeInfo::new(4072, 4073, ','), + ArrayTypeInfo::new(2970, 2949, ','), + ArrayTypeInfo::new(5038, 5039, ','), + ArrayTypeInfo::new(3904, 3905, ','), + ArrayTypeInfo::new(3906, 3907, ','), + ArrayTypeInfo::new(3908, 3909, ','), + ArrayTypeInfo::new(3910, 3911, ','), + ArrayTypeInfo::new(3912, 3913, ','), + ArrayTypeInfo::new(3926, 3927, ','), + ArrayTypeInfo::new(4451, 6150, ','), + ArrayTypeInfo::new(4532, 6151, ','), + ArrayTypeInfo::new(4533, 6152, ','), + ArrayTypeInfo::new(4534, 6153, ','), + ArrayTypeInfo::new(4535, 6155, ','), + ArrayTypeInfo::new(4536, 6157, ','), + ArrayTypeInfo::new(2275, 1263, ','), +]; + +fn parse_int(value: &str) -> Value { + match value.parse::() { + Ok(int) => json!(int), + Err(_) => json!(value.to_string()), + } +} + +fn parse_bigint(value: &str) -> Value { + match value.parse::() { + Ok(int) => json!(int), + Err(_) => json!(value.to_string()), + } +} + +fn parse_numeric(value: &str) -> Value { + serde_json::Number::from_str(value) + .map(Value::Number) + .unwrap_or_else(|_| json!(value.to_string())) +} + +fn parse_float(value: &str) -> Value { + match value.parse::() { + Ok(float) => json!(float), + Err(_) => json!(value.to_string()), + } +} + +fn parse_json(value: &str) -> Value { + serde_json::from_str(value).unwrap_or_else(|_| json!(value.to_string())) +} + +fn parse_bytea(value: &str) -> Value { + value + .strip_prefix("\\x") + .and_then(|hex| hex::decode(hex).ok()) + .map(Value::from) + .unwrap_or_else(|| json!(value.to_string())) +} + +fn serialize_string(value: &Value) -> Result { + match value { + Value::String(s) => Ok(s.clone()), + other => Ok(other.to_string()), + } +} + +fn serialize_number(value: &Value) -> Result { + match value { + Value::Number(num) => Ok(num.to_string()), + Value::String(s) => Ok(s.clone()), + other => Err(anyhow!("cannot serialize value {other} as number")), + } +} + +fn serialize_bool(value: &Value) -> Result { + match value { + Value::Bool(b) => Ok(if *b { "t" } else { "f" }.to_string()), + Value::Number(num) => Ok(if num.as_i64().unwrap_or(0) != 0 { + "t" + } else { + "f" + } + .to_string()), + Value::String(s) => Ok(match s.as_ref() { + "true" | "t" | "1" => "t".to_string(), + _ => "f".to_string(), + }), + other => Err(anyhow!("cannot serialize value {other} as boolean")), + } +} + +fn serialize_json(value: &Value) -> Result { + if let Some(value) = value.as_str() { + Ok(value.to_string()) + } else { + serde_json::to_string(value).map_err(|err| anyhow!(err)) + } +} + +fn serialize_bytea(value: &Value) -> Result { + match value { + Value::String(s) => Ok(s.clone()), + Value::Array(arr) => { + let bytes: Vec = arr + .iter() + .filter_map(|v| v.as_u64().map(|n| n as u8)) + .collect(); + Ok(format!("\\x{}", hex::encode(bytes))) + } + Value::Null => Ok("\\x".to_string()), + _ => Err(anyhow!("unsupported value for bytea serialization")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_multidimensional_arrays_without_separator_artifacts() { + let parser: TypeParser = Arc::new(|value, _| parse_float(value)); + let parsed = parse_array_text("{{1.5,2.5},{3.5,4.5}}", Some(parser), FLOAT8, ','); + assert_eq!(parsed, json!([[1.5, 2.5], [3.5, 4.5]])); + } + + #[test] + fn parses_quoted_array_values_and_unquoted_nulls() { + let parsed = parse_array_text( + r#"{"comma,value","quote \" value",NULL,"NULL",""}"#, + None, + TEXT, + ',', + ); + assert_eq!( + parsed, + json!(["comma,value", "quote \" value", null, "NULL", ""]) + ); + } +} diff --git a/src/pglite/wire.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/wire.rs similarity index 99% rename from src/pglite/wire.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/wire.rs index db45b7c5..401ccf9f 100644 --- a/src/pglite/wire.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/wire.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, anyhow, bail}; -use crate::pglite::config::StartupConfig; +use crate::oliphaunt::config::StartupConfig; pub(crate) const SSL_REQUEST_CODE: i32 = 80_877_103; pub(crate) const GSSENC_REQUEST_CODE: i32 = 80_877_104; diff --git a/src/protocol/buffer_reader.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/buffer_reader.rs similarity index 100% rename from src/protocol/buffer_reader.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/buffer_reader.rs diff --git a/src/protocol/buffer_writer.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/buffer_writer.rs similarity index 100% rename from src/protocol/buffer_writer.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/buffer_writer.rs diff --git a/src/protocol/messages.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/messages.rs similarity index 100% rename from src/protocol/messages.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/messages.rs diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/mod.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/mod.rs new file mode 100644 index 00000000..ef8aaf56 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/mod.rs @@ -0,0 +1,27 @@ +#![allow(dead_code)] +// The internal protocol layer keeps full PostgreSQL message shapes even when +// the high-level API only consumes a subset of each message today. + +pub(crate) mod buffer_reader; +pub(crate) mod buffer_writer; +pub(crate) mod messages; +pub(crate) mod parser; +pub(crate) mod serializer; +pub(crate) mod string_utils; +pub(crate) mod types; + +#[cfg(test)] +mod shared_fixture_tests; +#[cfg(test)] +mod tests; + +#[cfg(test)] +use messages::{AuthenticationMessage, BackendMessage, Field, MessageName}; +#[cfg(test)] +use parser::Parser; +#[cfg(test)] +use serializer::{BindConfig, ExecConfig, PortalTarget, Serialize}; +#[cfg(test)] +use string_utils::byte_length_utf8; +#[cfg(test)] +use types::Mode; diff --git a/src/protocol/parser.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/parser.rs similarity index 100% rename from src/protocol/parser.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/parser.rs diff --git a/src/protocol/serializer.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/serializer.rs similarity index 100% rename from src/protocol/serializer.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/serializer.rs diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/shared_fixture_tests.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/shared_fixture_tests.rs new file mode 100644 index 00000000..226cb8a9 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/shared_fixture_tests.rs @@ -0,0 +1,107 @@ +use std::{ + collections::HashSet, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use serde::Deserialize; + +use super::{messages::BackendMessage, parser::Parser}; + +#[test] +fn parser_matches_shared_protocol_wire_fixtures() -> Result<()> { + let Some(path) = shared_fixture_path() else { + eprintln!("skipping shared protocol fixtures outside the monorepo package"); + return Ok(()); + }; + let corpus: ProtocolFixtureCorpus = serde_json::from_str(&fs::read_to_string(path)?)?; + assert_eq!(corpus.schema_version, 1); + assert_eq!(corpus.kind, "postgres-backend-query-response"); + + let mut names = HashSet::new(); + let mut matched = 0usize; + for fixture in corpus.cases { + assert!( + names.insert(fixture.name.clone()), + "duplicate shared protocol fixture {}", + fixture.name + ); + let Some(wire) = fixture.wire_expectation else { + continue; + }; + matched += 1; + let messages = parse_vec_chunks(vec![decode_hex(&fixture.response_hex)])?; + let actual = messages + .iter() + .map(|message| message.name().to_string()) + .collect::>(); + assert_eq!(actual, wire.message_names, "fixture {}", fixture.name); + } + assert!(matched > 0, "shared protocol corpus had no wire fixtures"); + Ok(()) +} + +fn parse_vec_chunks(chunks: Vec>) -> Result> { + let mut parser = Parser::new(); + let mut messages = Vec::new(); + for chunk in chunks { + parser.parse(chunk.as_slice(), |message| { + messages.push(message); + Ok(()) + })?; + } + Ok(messages) +} + +fn shared_fixture_path() -> Option { + if let Some(root) = std::env::var_os("OLIPHAUNT_SHARED_FIXTURES") { + let path = PathBuf::from(root).join("protocol/query-response-cases.json"); + if path.is_file() { + return Some(path); + } + } + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../../src/shared/fixtures/protocol/query-response-cases.json"); + path.is_file().then_some(path) +} + +fn decode_hex(hex: &str) -> Vec { + let compact = hex + .chars() + .filter(|ch| !ch.is_whitespace()) + .collect::(); + assert!( + compact.len() % 2 == 0, + "hex fixture must have an even digit count" + ); + (0..compact.len()) + .step_by(2) + .map(|index| { + u8::from_str_radix(&compact[index..index + 2], 16) + .expect("hex fixture contains invalid byte") + }) + .collect() +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ProtocolFixtureCorpus { + schema_version: u32, + kind: String, + cases: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ProtocolFixtureCase { + name: String, + response_hex: String, + wire_expectation: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct WireExpectation { + message_names: Vec, +} diff --git a/src/protocol/string_utils.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/string_utils.rs similarity index 100% rename from src/protocol/string_utils.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/string_utils.rs diff --git a/src/protocol/tests.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/tests.rs similarity index 100% rename from src/protocol/tests.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/tests.rs diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/types.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/types.rs new file mode 100644 index 00000000..4292a42e --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/protocol/types.rs @@ -0,0 +1,49 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Mode { + Text = 0, + Binary = 1, +} + +impl Mode { + pub fn as_i16(self) -> i16 { + match self { + Mode::Text => 0, + Mode::Binary => 1, + } + } +} + +impl TryFrom for Mode { + type Error = &'static str; + + fn try_from(value: i16) -> Result { + match value { + 0 => Ok(Mode::Text), + 1 => Ok(Mode::Binary), + _ => Err("invalid mode"), + } + } +} + +pub struct Modes; + +impl Modes { + pub const TEXT: Mode = Mode::Text; + pub const BINARY: Mode = Mode::Binary; +} + +pub type BufferParameter<'a> = &'a [u8]; + +#[cfg(test)] +mod tests { + use super::{Mode, Modes}; + + #[test] + fn mode_round_trips_wire_format_codes() { + assert_eq!(Modes::TEXT.as_i16(), 0); + assert_eq!(Modes::BINARY.as_i16(), 1); + assert_eq!(Mode::try_from(0), Ok(Mode::Text)); + assert_eq!(Mode::try_from(1), Ok(Mode::Binary)); + assert_eq!(Mode::try_from(2), Err("invalid mode")); + } +} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/cli_smoke.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/cli_smoke.rs new file mode 100644 index 00000000..72a00fd8 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/cli_smoke.rs @@ -0,0 +1,96 @@ +#![cfg(feature = "extensions")] + +use anyhow::{Context, Result}; +use oliphaunt_wasix::{Oliphaunt, capture_phase_timings}; +use sqlx::{Connection, Row}; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; +use tokio::time::{Duration, timeout}; + +mod support; +use support::{ChildGuard, TestTrace, trace_step}; + +fn direct_open_diagnostic() -> String { + let (result, phases) = capture_phase_timings(|| Oliphaunt::builder().temporary().open()); + let outcome = match result { + Ok(mut pg) => match pg.close() { + Ok(()) => "direct temporary Oliphaunt open succeeded".to_owned(), + Err(err) => format!("direct temporary Oliphaunt open succeeded, close failed: {err:#}"), + }, + Err(err) => format!("direct temporary Oliphaunt open failed: {err:#}"), + }; + format!("{outcome}\nphases:\n{phases:#?}") +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn oliphaunt_proxy_print_uri_accepts_sqlx_connection() -> Result<()> { + let _trace = TestTrace::new("oliphaunt_proxy_print_uri_accepts_sqlx_connection"); + let process = Command::new(env!("CARGO_BIN_EXE_oliphaunt-wasix-proxy")) + .args(["--temporary", "--tcp", "127.0.0.1:0", "--print-uri"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("spawn oliphaunt-wasix-proxy")?; + let mut child = ChildGuard::new(process, "oliphaunt-wasix-proxy")?; + + let stdout = child + .child_mut() + .stdout + .take() + .context("oliphaunt-wasix-proxy stdout pipe")?; + let read_uri = tokio::task::spawn_blocking(move || { + let mut reader = BufReader::new(stdout); + let mut uri = String::new(); + let bytes = reader + .read_line(&mut uri) + .context("read oliphaunt-wasix-proxy printed URI")?; + Ok::<_, anyhow::Error>((bytes, uri)) + }); + let (bytes, uri) = match timeout(Duration::from_secs(30), read_uri).await { + Ok(Ok(Ok(result))) => result, + Ok(Ok(Err(err))) => return Err(err), + Ok(Err(err)) => return Err(err).context("join URI reader task"), + Err(err) => { + let stderr = child.collect_stderr(); + anyhow::bail!( + "timed out waiting for oliphaunt-wasix-proxy URI: {err}\n\nstderr:\n{stderr}" + ); + } + }; + if bytes == 0 { + let stderr = child.collect_stderr(); + anyhow::bail!("oliphaunt-wasix-proxy exited before printing URI\n\nstderr:\n{stderr}"); + } + let uri = uri.trim(); + assert!( + uri.starts_with("postgresql://") || uri.starts_with("postgres://"), + "unexpected URI: {uri}" + ); + trace_step("oliphaunt_proxy printed URI"); + + let mut conn = match timeout(Duration::from_secs(30), sqlx::PgConnection::connect(uri)).await { + Ok(Ok(conn)) => conn, + Ok(Err(err)) => { + let stderr = child.collect_stderr(); + let direct = direct_open_diagnostic(); + anyhow::bail!( + "connect to oliphaunt-wasix-proxy failed: {err:#}\n\nstderr:\n{stderr}\n\ndirect backend diagnostic:\n{direct}" + ); + } + Err(err) => { + let stderr = child.collect_stderr(); + let direct = direct_open_diagnostic(); + anyhow::bail!( + "timed out connecting to oliphaunt-wasix-proxy: {err}\n\nstderr:\n{stderr}\n\ndirect backend diagnostic:\n{direct}" + ); + } + }; + let row = sqlx::query("SELECT $1::int4 + 1 AS answer") + .bind(41_i32) + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("answer")?, 42); + + conn.close().await?; + Ok(()) +} diff --git a/tests/client_compat.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/client_compat.rs similarity index 96% rename from tests/client_compat.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/client_compat.rs index 96587bff..66f49f46 100644 --- a/tests/client_compat.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/client_compat.rs @@ -1,7 +1,7 @@ #![cfg(feature = "extensions")] use anyhow::{Context, Result}; -use pglite_oxide::{Pglite, PgliteServer}; +use oliphaunt_wasix::{Oliphaunt, OliphauntServer}; use sqlx::{Connection, Executor, Row}; use std::io::{Read, Write}; use std::net::TcpStream; @@ -15,7 +15,7 @@ const CANCEL_REQUEST_CODE: i32 = 80_877_102; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_extended_query_works() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -47,7 +47,7 @@ async fn tokio_postgres_extended_query_works() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_extended_query_errors_recover_after_sync() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -91,7 +91,7 @@ async fn tokio_postgres_extended_query_errors_recover_after_sync() -> Result<()> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_bind_errors_recover_after_sync() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -127,7 +127,7 @@ async fn sqlx_bind_errors_recover_after_sync() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_pipelined_extended_queries_keep_ready_state() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -148,7 +148,7 @@ async fn tokio_postgres_pipelined_extended_queries_keep_ready_state() -> Result< #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_mixed_pipelined_success_error_success_recovers() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -177,7 +177,7 @@ async fn tokio_postgres_mixed_pipelined_success_error_success_recovers() -> Resu #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_protocol_bind_errors_are_synchronized() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; tokio::task::spawn_blocking(move || -> Result<()> { @@ -259,7 +259,7 @@ async fn raw_wire_protocol_bind_errors_are_synchronized() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_protocol_handles_partial_reads_and_pipelined_simple_queries() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; tokio::task::spawn_blocking(move || -> Result<()> { @@ -315,7 +315,7 @@ async fn raw_wire_protocol_handles_partial_reads_and_pipelined_simple_queries() #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_copy_from_stdin_streams_through_backend_copy_state() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; tokio::task::spawn_blocking(move || -> Result<()> { @@ -367,7 +367,7 @@ async fn raw_wire_copy_from_stdin_streams_through_backend_copy_state() -> Result #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_extended_copy_from_stdin_uses_backend_protocol_pump() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server .tcp_addr() .context("temporary TCP server should expose addr")?; @@ -424,7 +424,7 @@ async fn raw_wire_extended_copy_from_stdin_uses_backend_protocol_pump() -> Resul #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_copy_variants_and_copyfail_are_backend_owned() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; tokio::task::spawn_blocking(move || -> Result<()> { @@ -493,8 +493,8 @@ async fn raw_wire_copy_variants_and_copyfail_are_backend_owned() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_unix_socket_copy_uses_same_protocol_path() -> Result<()> { let dir = tempfile::TempDir::new().context("create Unix socket tempdir")?; - let socket_path = dir.path().join("pglite.sock"); - let server = PgliteServer::builder() + let socket_path = dir.path().join("oliphaunt.sock"); + let server = OliphauntServer::builder() .temporary() .unix(&socket_path) .start()?; @@ -530,7 +530,7 @@ async fn raw_wire_unix_socket_copy_uses_same_protocol_path() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn raw_wire_disconnect_during_extended_query_does_not_poison_backend() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; tokio::task::spawn_blocking(move || -> Result<()> { @@ -559,7 +559,7 @@ async fn raw_wire_disconnect_during_extended_query_does_not_poison_backend() -> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_query_works() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -591,7 +591,7 @@ async fn sqlx_query_works() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_prepared_statement_reuse_works() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -611,7 +611,7 @@ async fn tokio_postgres_prepared_statement_reuse_works() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_transaction_error_recovers_after_rollback() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -643,7 +643,7 @@ async fn sqlx_transaction_error_recovers_after_rollback() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_transaction_commit_and_rollback_preserve_state() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -687,7 +687,7 @@ async fn sqlx_transaction_commit_and_rollback_preserve_state() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_transaction_commit_rollback_and_error_recovery() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let (mut client, connection) = tokio_postgres::connect(&server.connection_uri(), NoTls) .await .context("connect with tokio-postgres")?; @@ -760,7 +760,7 @@ async fn tokio_postgres_transaction_commit_rollback_and_error_recovery() -> Resu #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_extended_query_errors_recover_after_sync() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -796,7 +796,7 @@ async fn sqlx_extended_query_errors_recover_after_sync() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_simple_query_timezone_errors_recover() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let mut conn = sqlx::PgConnection::connect(&server.connection_uri()) .await .context("connect with SQLx")?; @@ -850,7 +850,7 @@ async fn sqlx_simple_query_timezone_errors_recover() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_server_startup_postgres_config_uses_real_guc_handling() -> Result<()> { - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .postgres_config("synchronous_commit", "off") .postgres_config("work_mem", "8MB") @@ -890,7 +890,7 @@ async fn sqlx_server_startup_postgres_config_uses_real_guc_handling() -> Result< #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sqlx_server_relaxed_durability_is_idempotent_and_user_config_wins() -> Result<()> { - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .relaxed_durability(true) .relaxed_durability(false) @@ -905,7 +905,7 @@ async fn sqlx_server_relaxed_durability_is_idempotent_and_user_config_wins() -> conn.close().await?; server.shutdown()?; - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .relaxed_durability(true) .postgres_config("synchronous_commit", "on") @@ -928,7 +928,7 @@ async fn sqlx_server_startup_identity_can_select_existing_user_and_database() -> let seed_root = root.path().to_path_buf(); let seed_task_root = seed_root.clone(); tokio::task::spawn_blocking(move || -> Result<()> { - let mut admin = Pglite::builder().path(seed_task_root).open()?; + let mut admin = Oliphaunt::builder().path(seed_task_root).open()?; admin.exec("CREATE ROLE server_user LOGIN", None)?; admin.exec("CREATE DATABASE server_db OWNER server_user", None)?; admin.close()?; @@ -937,7 +937,7 @@ async fn sqlx_server_startup_identity_can_select_existing_user_and_database() -> .await .context("join startup identity seed task")??; - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .path(seed_root) .username("server_user") .database("server_db") @@ -958,7 +958,7 @@ async fn sqlx_server_startup_identity_can_select_existing_user_and_database() -> #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn tokio_postgres_startup_options_are_forwarded_to_postgres() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; let mut config = tokio_postgres::Config::new(); config @@ -991,7 +991,7 @@ async fn tokio_postgres_startup_options_are_forwarded_to_postgres() -> Result<() #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn postgres_control_packets_are_handled_safely() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; let ssl_response = tokio::task::spawn_blocking(move || -> Result { @@ -1034,7 +1034,7 @@ async fn postgres_control_packets_are_handled_safely() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn postgres_startup_identity_is_delegated_to_postgres() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; + let server = OliphauntServer::temporary_tcp()?; let addr = server.tcp_addr().context("server should use TCP")?; let bad_user = tokio::task::spawn_blocking(move || -> Result> { diff --git a/tests/extensions_smoke.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/extensions_smoke.rs similarity index 76% rename from tests/extensions_smoke.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/extensions_smoke.rs index b90012ca..9a937514 100644 --- a/tests/extensions_smoke.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/extensions_smoke.rs @@ -1,7 +1,7 @@ #![cfg(feature = "extensions")] use anyhow::Result; -use pglite_oxide::{Pglite, PgliteError, PgliteServer, extensions}; +use oliphaunt_wasix::{Oliphaunt, OliphauntError, OliphauntServer, extensions}; use serde_json::json; use sqlx::{Connection, Row}; use std::path::{Path, PathBuf}; @@ -27,13 +27,13 @@ fn trace_expected(label: &str) { eprintln!("extensions_smoke::expected_sql_error exercising {label}"); } -fn first_f64(result: &pglite_oxide::Results, column: &str) -> f64 { +fn first_f64(result: &oliphaunt_wasix::Results, column: &str) -> f64 { result.rows[0][column].as_f64().expect("floating result") } -fn assert_pglite_code(err: &anyhow::Error, expected_code: &str, message_contains: &str) { +fn assert_oliphaunt_code(err: &anyhow::Error, expected_code: &str, message_contains: &str) { let pg_err = err - .downcast_ref::() + .downcast_ref::() .expect("error should preserve Postgres fields"); assert_eq!(pg_err.database_error().code.as_deref(), Some(expected_code)); assert!( @@ -43,6 +43,57 @@ fn assert_pglite_code(err: &anyhow::Error, expected_code: &str, message_contains ); } +const PGCRYPTO_FUNCTIONAL_SMOKE_SQL: &str = "\ +DO $$ \ +DECLARE \ + hashed text; \ + encrypted bytea; \ + armored text; \ + header_count int; \ + crypto_key bytea := decode('000102030405060708090a0b0c0d0e0f', 'hex'); \ + crypto_iv bytea := decode('101112131415161718191a1b1c1d1e1f', 'hex'); \ +BEGIN \ + IF encode(digest('abc', 'sha256'), 'hex') <> 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' THEN \ + RAISE EXCEPTION 'sha256 digest failed'; \ + END IF; \ + IF encode(hmac('test', 'key', 'sha1'), 'hex') <> '671f54ce0c540f78ffe1e26dcf9c2a047aea4fda' THEN \ + RAISE EXCEPTION 'hmac failed'; \ + END IF; \ + IF length(gen_random_bytes(16)) <> 16 THEN \ + RAISE EXCEPTION 'random bytes length failed'; \ + END IF; \ + IF gen_random_uuid()::text !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' THEN \ + RAISE EXCEPTION 'random uuid format failed'; \ + END IF; \ + SELECT crypt('secret', gen_salt('bf', 4)) INTO hashed; \ + IF crypt('secret', hashed) <> hashed THEN \ + RAISE EXCEPTION 'password hash verify failed'; \ + END IF; \ + SELECT armor(digest('test', 'sha1'), ARRAY['Version'], ARRAY['oliphaunt']) INTO armored; \ + IF position('Version: oliphaunt' in armored) = 0 THEN \ + RAISE EXCEPTION 'armor header failed'; \ + END IF; \ + SELECT count(*) INTO header_count FROM pgp_armor_headers(armored); \ + IF header_count <> 1 THEN \ + RAISE EXCEPTION 'armor header count failed: %', header_count; \ + END IF; \ + SELECT pgp_sym_encrypt('oliphaunt secret', 'passphrase') INTO encrypted; \ + IF pgp_sym_decrypt(encrypted, 'passphrase') <> 'oliphaunt secret' THEN \ + RAISE EXCEPTION 'PGP symmetric decrypt failed'; \ + END IF; \ + IF pgp_key_id(encrypted) <> 'SYMKEY' THEN \ + RAISE EXCEPTION 'PGP symmetric key id failed'; \ + END IF; \ + SELECT encrypt(convert_to('oliphaunt raw cipher', 'UTF8'), crypto_key, 'aes') INTO encrypted; \ + IF convert_from(decrypt(encrypted, crypto_key, 'aes'), 'UTF8') <> 'oliphaunt raw cipher' THEN \ + RAISE EXCEPTION 'raw decrypt failed'; \ + END IF; \ + SELECT encrypt_iv(convert_to('oliphaunt iv cipher', 'UTF8'), crypto_key, crypto_iv, 'aes-cbc') INTO encrypted; \ + IF convert_from(decrypt_iv(encrypted, crypto_key, crypto_iv, 'aes-cbc'), 'UTF8') <> 'oliphaunt iv cipher' THEN \ + RAISE EXCEPTION 'raw iv decrypt failed'; \ + END IF; \ +END $$"; + fn assert_sqlx_code(err: &sqlx::Error, expected_code: &str) { assert_eq!( err.as_database_error().and_then(|db| db.code()).as_deref(), @@ -55,7 +106,7 @@ fn assert_only_requested_extension_assets_are_materialized( requested: &str, unrequested: &str, ) { - let runtime = root.join("tmp/pglite"); + let runtime = root.join("tmp/oliphaunt"); assert!( runtime .join(format!("lib/postgresql/{requested}.so")) @@ -138,7 +189,7 @@ fn relative_files(root: &Path) -> Vec { #[test] fn vector_extension_direct_smoke() -> Result<()> { let _trace = TestTrace::new("vector_extension_direct_smoke"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extension(extensions::VECTOR) .open()?; @@ -174,7 +225,7 @@ fn vector_extension_direct_smoke() -> Result<()> { None, ) .expect_err("division by zero after vector load should fail"); - assert_pglite_code(&err, "22012", "division by zero"); + assert_oliphaunt_code(&err, "22012", "division by zero"); let recovered = db.query("SELECT 13::int AS recovered_after_vector_error", &[], None)?; assert_eq!(recovered.rows[0]["recovered_after_vector_error"], json!(13)); @@ -186,7 +237,7 @@ fn vector_extension_direct_smoke() -> Result<()> { None, ) .expect_err("invalid vector literal should fail inside the vector extension"); - assert_pglite_code( + assert_oliphaunt_code( &invalid_vector, "22P02", "invalid input syntax for type vector", @@ -209,7 +260,7 @@ fn vector_extension_direct_smoke() -> Result<()> { None, ) .expect_err("vector distance should reject mismatched dimensions"); - assert_pglite_code(&dimension_mismatch, "22000", "different vector dimensions"); + assert_oliphaunt_code(&dimension_mismatch, "22000", "different vector dimensions"); let recovered = db.query( "SELECT 16::int AS recovered_after_dimension_mismatch", &[], @@ -229,7 +280,7 @@ fn pure_mountfs_materializes_only_requested_extension_assets() -> Result<()> { let _trace = TestTrace::new("pure_mountfs_materializes_only_requested_extension_assets"); let root = tempfile::TempDir::new()?; { - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .path(root.path()) .extension(extensions::VECTOR) .open()?; @@ -249,7 +300,7 @@ fn pure_mountfs_materializes_only_requested_extension_assets() -> Result<()> { #[test] fn vector_extension_ports_pgvector_core_type_cases() -> Result<()> { let _trace = TestTrace::new("vector_extension_ports_pgvector_core_type_cases"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extension(extensions::VECTOR) .open()?; @@ -289,7 +340,7 @@ fn vector_extension_ports_pgvector_core_type_cases() -> Result<()> { Ok(_) => panic!("{sql} should fail"), Err(err) => err, }; - assert_pglite_code(&err, code, message); + assert_oliphaunt_code(&err, code, message); let recovered = db.query("SELECT 17::int AS recovered", &[], None)?; assert_eq!(recovered.rows[0]["recovered"], json!(17)); } @@ -302,7 +353,7 @@ fn vector_extension_ports_pgvector_core_type_cases() -> Result<()> { fn vector_extension_direct_transaction_commit_rollback_and_error_recovery() -> Result<()> { let _trace = TestTrace::new("vector_extension_direct_transaction_commit_rollback_and_error_recovery"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extension(extensions::VECTOR) .open()?; @@ -346,7 +397,7 @@ fn vector_extension_direct_transaction_commit_rollback_and_error_recovery() -> R Ok(()) }); let failed = failed.expect_err("invalid vector should fail inside transaction"); - assert_pglite_code(&failed, "22P02", "invalid input syntax for type vector"); + assert_oliphaunt_code(&failed, "22P02", "invalid input syntax for type vector"); let result = db.query( "SELECT count(*)::int AS count, \ @@ -371,11 +422,11 @@ fn vector_extension_install_is_demand_driven_idempotent_and_persistent() -> Resu TestTrace::new("vector_extension_install_is_demand_driven_idempotent_and_persistent"); let root = tempfile::TempDir::new()?; { - let mut db = Pglite::builder().path(root.path()).open()?; + let mut db = Oliphaunt::builder().path(root.path()).open()?; assert!( !db.paths() .pgroot - .join("pglite") + .join("oliphaunt") .join("lib/postgresql/vector.so") .exists(), "vector side module should not be installed before it is requested" @@ -386,7 +437,7 @@ fn vector_extension_install_is_demand_driven_idempotent_and_persistent() -> Resu assert!( db.paths() .pgroot - .join("pglite") + .join("oliphaunt") .join("lib/postgresql/vector.so") .exists(), "vector side module should be installed after enable_extension" @@ -402,7 +453,7 @@ fn vector_extension_install_is_demand_driven_idempotent_and_persistent() -> Resu } { - let mut reopened = Pglite::builder().path(root.path()).open()?; + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; let result = reopened.query("SELECT '[1,2,3]'::vector::text AS value", &[], None)?; assert_eq!(result.rows[0]["value"], json!("[1,2,3]")); reopened.close()?; @@ -414,7 +465,7 @@ fn vector_extension_install_is_demand_driven_idempotent_and_persistent() -> Resu #[test] fn pg_trgm_extension_direct_smoke() -> Result<()> { let _trace = TestTrace::new("pg_trgm_extension_direct_smoke"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extension(extensions::PG_TRGM) .open()?; @@ -444,7 +495,7 @@ fn pg_trgm_extension_direct_smoke() -> Result<()> { #[test] fn hstore_extension_direct_smoke() -> Result<()> { let _trace = TestTrace::new("hstore_extension_direct_smoke"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extension(extensions::HSTORE) .open()?; @@ -484,7 +535,7 @@ fn hstore_extension_reopens_cleanly() -> Result<()> { let _trace = TestTrace::new("hstore_extension_reopens_cleanly"); let root = tempfile::TempDir::new()?; { - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .path(root.path()) .extension(extensions::HSTORE) .open()?; @@ -497,7 +548,7 @@ fn hstore_extension_reopens_cleanly() -> Result<()> { } { - let mut reopened = Pglite::builder().path(root.path()).open()?; + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; let result = reopened.query( "SELECT data -> 'name' AS name FROM oxide_hstore_restart", &[], @@ -510,10 +561,104 @@ fn hstore_extension_reopens_cleanly() -> Result<()> { Ok(()) } +#[test] +fn pgcrypto_extension_direct_smoke() -> Result<()> { + let _trace = TestTrace::new("pgcrypto_extension_direct_smoke"); + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::PGCRYPTO) + .open()?; + + db.exec(PGCRYPTO_FUNCTIONAL_SMOKE_SQL, None)?; + + let installed = db.query( + "SELECT count(*)::int AS count, max(n.nspname) AS schema_name \ + FROM pg_extension e \ + JOIN pg_namespace n ON n.oid = e.extnamespace \ + WHERE e.extname = 'pgcrypto'", + &[], + None, + )?; + assert_eq!(installed.rows[0]["count"], json!(1)); + assert_eq!(installed.rows[0]["schema_name"], json!("public")); + + trace_expected("pgcrypto_direct invalid digest algorithm"); + let invalid_algorithm = db + .query("SELECT digest('abc', 'not-a-digest')", &[], None) + .expect_err("invalid pgcrypto digest algorithm should fail"); + assert_oliphaunt_code( + &invalid_algorithm, + "22023", + "Cannot use \"not-a-digest\": No such hash algorithm", + ); + let recovered = db.query( + "SELECT 23::int AS recovered_after_pgcrypto_error", + &[], + None, + )?; + assert_eq!( + recovered.rows[0]["recovered_after_pgcrypto_error"], + json!(23) + ); + + db.close()?; + Ok(()) +} + +#[test] +fn pgcrypto_extension_reopens_cleanly() -> Result<()> { + let _trace = TestTrace::new("pgcrypto_extension_reopens_cleanly"); + let root = tempfile::TempDir::new()?; + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extensions::PGCRYPTO) + .open()?; + db.exec(PGCRYPTO_FUNCTIONAL_SMOKE_SQL, None)?; + db.exec( + "CREATE TABLE oxide_pgcrypto_restart AS \ + SELECT encode(digest('persisted', 'sha256'), 'hex') AS digest_hex", + None, + )?; + db.close()?; + } + + { + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; + let result = reopened.query( + "SELECT digest_hex = encode(digest('persisted', 'sha256'), 'hex') AS ok \ + FROM oxide_pgcrypto_restart", + &[], + None, + )?; + assert_eq!(result.rows[0]["ok"], json!(true)); + reopened.close()?; + } + + Ok(()) +} + +#[test] +fn pgcrypto_extension_materializes_only_requested_assets() -> Result<()> { + let _trace = TestTrace::new("pgcrypto_extension_materializes_only_requested_assets"); + let root = tempfile::TempDir::new()?; + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extensions::PGCRYPTO) + .open()?; + db.exec(PGCRYPTO_FUNCTIONAL_SMOKE_SQL, None)?; + db.close()?; + } + + assert_only_requested_extension_assets_are_materialized(root.path(), "pgcrypto", "vector"); + Ok(()) +} + #[test] fn multiple_extension_set_direct_smoke() -> Result<()> { let _trace = TestTrace::new("multiple_extension_set_direct_smoke"); - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .extensions([extensions::VECTOR, extensions::PG_TRGM]) .open()?; @@ -544,7 +689,7 @@ fn multiple_extension_set_direct_smoke() -> Result<()> { #[tokio::test(flavor = "multi_thread")] async fn pg_trgm_extension_server_sqlx_smoke() -> Result<()> { let _trace = TestTrace::new("pg_trgm_extension_server_sqlx_smoke"); - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .extension(extensions::PG_TRGM) .start()?; @@ -569,7 +714,7 @@ async fn pg_trgm_extension_server_sqlx_smoke() -> Result<()> { #[tokio::test(flavor = "multi_thread")] async fn hstore_extension_server_sqlx_smoke() -> Result<()> { let _trace = TestTrace::new("hstore_extension_server_sqlx_smoke"); - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .extension(extensions::HSTORE) .start()?; @@ -596,10 +741,33 @@ async fn hstore_extension_server_sqlx_smoke() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn pgcrypto_extension_server_sqlx_smoke() -> Result<()> { + let _trace = TestTrace::new("pgcrypto_extension_server_sqlx_smoke"); + let server = OliphauntServer::builder() + .temporary() + .extension(extensions::PGCRYPTO) + .start()?; + let mut conn = sqlx::PgConnection::connect(&server.connection_uri()).await?; + + sqlx::query(PGCRYPTO_FUNCTIONAL_SMOKE_SQL) + .execute(&mut conn) + .await?; + let row = + sqlx::query("SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = 'pgcrypto'") + .fetch_one(&mut conn) + .await?; + assert_eq!(row.try_get::("count")?, 1); + + conn.close().await?; + server.shutdown()?; + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn vector_extension_server_sqlx_smoke() -> Result<()> { let _trace = TestTrace::new("vector_extension_server_sqlx_smoke"); - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .extension(extensions::VECTOR) .start()?; @@ -669,7 +837,7 @@ async fn vector_extension_server_sqlx_transaction_commit_rollback_and_error_reco let _trace = TestTrace::new( "vector_extension_server_sqlx_transaction_commit_rollback_and_error_recovery", ); - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .extension(extensions::VECTOR) .start()?; @@ -758,7 +926,7 @@ async fn vector_extension_server_sqlx_transaction_commit_rollback_and_error_reco #[tokio::test(flavor = "multi_thread")] async fn multiple_extension_set_server_sqlx_smoke() -> Result<()> { let _trace = TestTrace::new("multiple_extension_set_server_sqlx_smoke"); - let server = PgliteServer::builder() + let server = OliphauntServer::builder() .temporary() .extensions([extensions::VECTOR, extensions::PG_TRGM]) .start()?; diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/performance_smoke.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/performance_smoke.rs new file mode 100644 index 00000000..06b53632 --- /dev/null +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/performance_smoke.rs @@ -0,0 +1,306 @@ +#![cfg(feature = "extensions")] + +use anyhow::Result; +use oliphaunt_wasix::OliphauntServer; +use oliphaunt_wasix::extensions; +use oliphaunt_wasix::{Oliphaunt, capture_phase_timings}; +use serde_json::json; +use std::time::Instant; + +fn first_int(result: &oliphaunt_wasix::Results, column: &str) -> i64 { + result.rows[0][column].as_i64().expect("integer result") +} + +fn phase_elapsed_micros(phases: &[oliphaunt_wasix::PhaseTiming], name: &str) -> Option { + phases + .iter() + .find(|phase| phase.name == name) + .map(|phase| phase.elapsed_micros) +} + +fn assert_startup_xlog_fast_if_instrumented( + phases: &[oliphaunt_wasix::PhaseTiming], + context: &str, +) { + let Some(startup_xlog) = phase_elapsed_micros(phases, "postgres.backend.c.startup_xlog") else { + eprintln!( + "{context}: C backend timing is not present; rebuild assets with \ + OLIPHAUNT_WASM_WASIX_BACKEND_TIMING=1 to assert StartupXLOG directly" + ); + return; + }; + assert!( + startup_xlog < 200_000, + "{context} should not require slow StartupXLOG recovery; \ + saw {startup_xlog}us in phases: {phases:#?}" + ); +} + +#[cfg(feature = "extensions")] +fn preload_vector_or_skip(context: &str) -> Result { + match Oliphaunt::preload_extensions([extensions::VECTOR]) { + Ok(()) => Ok(true), + Err(err) + if err + .to_string() + .contains("is not bundled in this oliphaunt-wasix build") => + { + eprintln!("skipping {context}; vector extension assets are not bundled"); + Ok(false) + } + Err(err) => Err(err), + } +} + +#[test] +fn preload_runtime_then_open_smoke() -> Result<()> { + let preload_started = Instant::now(); + Oliphaunt::preload()?; + let preload_elapsed = preload_started.elapsed(); + + let open_started = Instant::now(); + let mut db = Oliphaunt::builder().temporary().open()?; + let open_elapsed = open_started.elapsed(); + + let result = db.query("SELECT $1::int + 1 AS answer", &[json!(41)], None)?; + assert_eq!(first_int(&result, "answer"), 42); + db.close()?; + + eprintln!( + "preload_runtime_then_open_smoke preload_ms={} open_ms={}", + preload_elapsed.as_millis(), + open_elapsed.as_millis() + ); + Ok(()) +} + +#[test] +fn scalar_open_does_not_scan_array_catalog() -> Result<()> { + let (result, phases) = capture_phase_timings(|| { + let mut db = Oliphaunt::builder().temporary().open()?; + let result = db.query("SELECT $1::int + 1 AS answer", &[json!(41)], None)?; + assert_eq!(first_int(&result, "answer"), 42); + db.close() + }); + result?; + + assert!( + !phases + .iter() + .any(|phase| phase.name == "oliphaunt.array_type_catalog_query"), + "scalar open/query should not scan pg_type for array mappings: {phases:#?}" + ); + Ok(()) +} + +#[test] +fn preload_reuses_process_aot_module_cache() -> Result<()> { + let (first, first_phases) = capture_phase_timings(Oliphaunt::preload); + first?; + let (second, second_phases) = capture_phase_timings(Oliphaunt::preload); + second?; + + let first_deserialized = first_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + let second_deserialized = second_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + + if first_deserialized { + assert!( + !second_deserialized, + "second preload should reuse the process module cache instead of deserializing again" + ); + } + Ok(()) +} + +#[test] +fn shared_runtime_does_not_share_database_state_between_instances() -> Result<()> { + Oliphaunt::preload()?; + + let mut first = Oliphaunt::builder().temporary().open()?; + first.exec( + "CREATE TABLE process_cache_isolation(value int); \ + INSERT INTO process_cache_isolation VALUES (42);", + None, + )?; + + let mut second = Oliphaunt::builder().temporary().open()?; + let missing = second + .query("SELECT value FROM process_cache_isolation", &[], None) + .expect_err("temporary database state must not leak across instances"); + assert!( + missing.to_string().contains("process_cache_isolation") + || missing.to_string().contains("does not exist"), + "unexpected isolation error: {missing:#}" + ); + + first.close()?; + second.close()?; + Ok(()) +} + +#[test] +fn persistent_direct_close_avoids_startup_xlog_recovery() -> Result<()> { + let root = tempfile::TempDir::new()?; + { + let mut db = Oliphaunt::builder().path(root.path()).open()?; + db.exec( + "CREATE TABLE clean_shutdown(value int); \ + INSERT INTO clean_shutdown VALUES (42);", + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let mut db = Oliphaunt::open(root.path())?; + let row = db.query("SELECT value FROM clean_shutdown", &[], None)?; + assert_eq!(first_int(&row, "value"), 42); + db.close() + }); + result?; + + assert_startup_xlog_fast_if_instrumented(&phases, "persistent direct close"); + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn preload_extensions_reuses_extension_side_module_cache() -> Result<()> { + let (first, first_phases) = + capture_phase_timings(|| preload_vector_or_skip("extension preload cache smoke")); + if !first? { + return Ok(()); + } + let (second, second_phases) = + capture_phase_timings(|| Oliphaunt::preload_extensions([extensions::VECTOR])); + second?; + + let first_deserialized = first_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + let second_deserialized = second_phases + .iter() + .any(|phase| phase.name == "aot.deserialize"); + + if first_deserialized { + assert!( + !second_deserialized, + "second extension preload should reuse the process side-module cache" + ); + } + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn persistent_extension_server_reopen_uses_single_clean_backend() -> Result<()> { + if !preload_vector_or_skip("persistent extension server reopen smoke")? { + return Ok(()); + } + let root = tempfile::TempDir::new()?; + + { + let mut db = Oliphaunt::builder() + .path(root.path()) + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let server = OliphauntServer::builder() + .path(root.path()) + .extension(extensions::VECTOR) + .start()?; + let url = server.database_url(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + runtime.block_on(async { + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(1) + .connect(&url) + .await?; + let (distance,): (f64,) = + sqlx::query_as("SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector") + .fetch_one(&pool) + .await?; + assert_eq!(distance, 1.0); + pool.close().await; + Ok::<_, anyhow::Error>(()) + })?; + server.shutdown() + }); + result?; + + let backend_starts = phases + .iter() + .filter(|phase| phase.name == "postgres.backend_start") + .count(); + assert_eq!( + backend_starts, 1, + "extension server startup should not use a second setup backend: {phases:#?}" + ); + assert_startup_xlog_fast_if_instrumented(&phases, "extension server reopen"); + Ok(()) +} + +#[cfg(feature = "extensions")] +#[test] +fn cached_extension_template_opens_without_startup_xlog_recovery() -> Result<()> { + if std::env::var_os("OLIPHAUNT_WASM_EXTENSION_TEMPLATE_CACHE").is_none() { + eprintln!( + "skipping cached extension template smoke; set OLIPHAUNT_WASM_EXTENSION_TEMPLATE_CACHE=1 to exercise the opt-in cache" + ); + return Ok(()); + } + + if !preload_vector_or_skip("cached extension template smoke")? { + return Ok(()); + } + + { + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close()?; + } + + let (result, phases) = capture_phase_timings(|| -> Result<()> { + let mut db = Oliphaunt::builder() + .temporary() + .extension(extensions::VECTOR) + .open()?; + db.query( + "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector AS distance", + &[], + None, + )?; + db.close() + }); + result?; + + assert!( + !phases + .iter() + .any(|phase| phase.name == "pgdata.extension_template_build"), + "second extension open should reuse the cached extension template: {phases:#?}" + ); + assert_startup_xlog_fast_if_instrumented(&phases, "cached extension template"); + Ok(()) +} diff --git a/tests/postgres_regression.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/postgres_regression.rs similarity index 91% rename from tests/postgres_regression.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/postgres_regression.rs index c743308e..21e03fa7 100644 --- a/tests/postgres_regression.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/postgres_regression.rs @@ -1,7 +1,7 @@ #![cfg(feature = "extensions")] use anyhow::{Context, Result, anyhow}; -use pglite_oxide::{Pglite, QueryOptions}; +use oliphaunt_wasix::{Oliphaunt, QueryOptions}; use serde_json::{Map, Value, json}; struct TestTrace { @@ -21,7 +21,7 @@ impl Drop for TestTrace { } } -fn first_row(result: &pglite_oxide::Results) -> Result<&Map> { +fn first_row(result: &oliphaunt_wasix::Results) -> Result<&Map> { result .rows .first() @@ -29,7 +29,7 @@ fn first_row(result: &pglite_oxide::Results) -> Result<&Map> { .ok_or_else(|| anyhow!("expected at least one object row")) } -fn single_column_strings(result: &pglite_oxide::Results, column: &str) -> Result> { +fn single_column_strings(result: &oliphaunt_wasix::Results, column: &str) -> Result> { result .rows .iter() @@ -42,10 +42,14 @@ fn single_column_strings(result: &pglite_oxide::Results, column: &str) -> Result .collect() } +fn open_regression_db() -> Result { + Oliphaunt::builder().temporary().open() +} + #[test] -fn datatypes_cover_pglite_basic_surface() -> Result<()> { - let _trace = TestTrace::new("datatypes_cover_pglite_basic_surface"); - let mut db = Pglite::builder().temporary().open()?; +fn datatypes_cover_oliphaunt_basic_surface() -> Result<()> { + let _trace = TestTrace::new("datatypes_cover_oliphaunt_basic_surface"); + let mut db = open_regression_db()?; db.exec( "CREATE TABLE regression_types ( @@ -227,7 +231,7 @@ fn datatypes_cover_pglite_basic_surface() -> Result<()> { #[test] fn ddl_schema_view_trigger_and_rollback_behave_like_postgres() -> Result<()> { let _trace = TestTrace::new("ddl_schema_view_trigger_and_rollback_behave_like_postgres"); - let mut db = Pglite::builder().temporary().open()?; + let mut db = open_regression_db()?; db.exec( "CREATE SCHEMA reg; @@ -293,7 +297,7 @@ fn ddl_schema_view_trigger_and_rollback_behave_like_postgres() -> Result<()> { .expect_err("check constraint should reject negative balance"); eprintln!("postgres_regression::ddl_schema expected check-constraint error returned"); let pg_error = constraint_error - .downcast_ref::() + .downcast_ref::() .context("constraint error should preserve PostgreSQL fields")?; assert_eq!(pg_error.database_error().code.as_deref(), Some("23514")); @@ -332,7 +336,7 @@ fn ddl_schema_view_trigger_and_rollback_behave_like_postgres() -> Result<()> { #[test] fn transactions_savepoints_and_error_recovery_match_postgres() -> Result<()> { let _trace = TestTrace::new("transactions_savepoints_and_error_recovery_match_postgres"); - let mut db = Pglite::builder().temporary().open()?; + let mut db = open_regression_db()?; db.exec( "CREATE TABLE tx_items ( id integer PRIMARY KEY, @@ -372,7 +376,7 @@ fn transactions_savepoints_and_error_recovery_match_postgres() -> Result<()> { .expect_err("duplicate primary key should fail inside savepoint"); eprintln!("postgres_regression::transactions expected duplicate-key error returned"); let pg_error = duplicate - .downcast_ref::() + .downcast_ref::() .context("duplicate error should preserve PostgreSQL fields")?; assert_eq!(pg_error.database_error().code.as_deref(), Some("23505")); db.exec("ROLLBACK TO SAVEPOINT duplicate_guard", None)?; @@ -401,7 +405,7 @@ fn transactions_savepoints_and_error_recovery_match_postgres() -> Result<()> { #[test] fn expected_sql_error_recovery_stays_inside_protocol_loop() -> Result<()> { let _trace = TestTrace::new("expected_sql_error_recovery_stays_inside_protocol_loop"); - let mut db = Pglite::builder().temporary().open()?; + let mut db = open_regression_db()?; db.exec( "CREATE TABLE error_recovery ( id integer PRIMARY KEY, @@ -426,7 +430,7 @@ fn expected_sql_error_recovery_stays_inside_protocol_loop() -> Result<()> { eprintln!("postgres_regression::expected_sql_error exercising {label}"); let err = db.exec(sql, None).expect_err(label); let pg_error = err - .downcast_ref::() + .downcast_ref::() .with_context(|| format!("{label} should preserve PostgreSQL fields"))?; assert_eq!(pg_error.database_error().code.as_deref(), Some(code)); let recovered = db.query( @@ -441,9 +445,9 @@ fn expected_sql_error_recovery_stays_inside_protocol_loop() -> Result<()> { } #[test] -fn pg17_uuidv4_alias_error_is_recoverable() -> Result<()> { - let _trace = TestTrace::new("pg17_uuidv4_alias_error_is_recoverable"); - let mut db = Pglite::builder().temporary().open()?; +fn pg18_uuidv4_alias_is_available_and_session_recovers() -> Result<()> { + let _trace = TestTrace::new("pg18_uuidv4_alias_is_available_and_session_recovers"); + let mut db = open_regression_db()?; let built_in = db.query( "SELECT uuid_extract_version(gen_random_uuid())::int AS version", @@ -452,21 +456,20 @@ fn pg17_uuidv4_alias_error_is_recoverable() -> Result<()> { )?; assert_eq!(first_row(&built_in)?.get("version"), Some(&json!(4))); + let alias = db.query( + "SELECT uuid_extract_version(uuidv4())::int AS version", + &[], + None, + )?; + assert_eq!(first_row(&alias)?.get("version"), Some(&json!(4))); + let err = db - .query("SELECT uuidv4() AS id", &[], None) - .expect_err("PostgreSQL 17 should not expose the PostgreSQL 18 uuidv4 alias"); + .query("SELECT uuidv4(1) AS id", &[], None) + .expect_err("invalid uuidv4 arity should still exercise PostgreSQL error recovery"); let pg_error = err - .downcast_ref::() - .context("uuidv4 error should preserve PostgreSQL fields")?; + .downcast_ref::() + .context("uuidv4 arity error should preserve PostgreSQL fields")?; assert_eq!(pg_error.database_error().code.as_deref(), Some("42883")); - assert!( - pg_error - .database_error() - .message - .contains("function uuidv4() does not exist"), - "unexpected uuidv4 error: {}", - pg_error.database_error().message - ); let recovered = db.query("SELECT 7::int AS recovered", &[], None)?; assert_eq!(first_row(&recovered)?.get("recovered"), Some(&json!(7))); @@ -477,7 +480,7 @@ fn pg17_uuidv4_alias_error_is_recoverable() -> Result<()> { #[test] fn planner_uses_indexes_for_selective_queries_and_updates() -> Result<()> { let _trace = TestTrace::new("planner_uses_indexes_for_selective_queries_and_updates"); - let mut db = Pglite::builder().temporary().open()?; + let mut db = open_regression_db()?; db.exec( "CREATE TABLE plan_items ( id integer PRIMARY KEY, @@ -565,9 +568,9 @@ fn planner_uses_indexes_for_selective_queries_and_updates() -> Result<()> { } #[test] -fn direct_blob_copy_round_trips_csv_with_pglite_dev_blob_surface() -> Result<()> { - let _trace = TestTrace::new("direct_blob_copy_round_trips_csv_with_pglite_dev_blob_surface"); - let mut db = Pglite::builder().temporary().open()?; +fn direct_blob_copy_round_trips_csv_with_oliphaunt_dev_blob_surface() -> Result<()> { + let _trace = TestTrace::new("direct_blob_copy_round_trips_csv_with_oliphaunt_dev_blob_surface"); + let mut db = open_regression_db()?; db.exec( "CREATE TABLE blob_items ( id integer PRIMARY KEY, @@ -642,7 +645,7 @@ fn direct_blob_copy_round_trips_csv_with_pglite_dev_blob_surface() -> Result<()> Ok(()) } -fn explain_text(db: &mut Pglite, sql: &str) -> Result { +fn explain_text(db: &mut Oliphaunt, sql: &str) -> Result { let result = db.query(sql, &[], None)?; let lines = single_column_strings(&result, "QUERY PLAN")?; Ok(lines.join("\n")) diff --git a/tests/proxy_smoke.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/proxy_smoke.rs similarity index 96% rename from tests/proxy_smoke.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/proxy_smoke.rs index 368ecc8f..0600e6fc 100644 --- a/tests/proxy_smoke.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/proxy_smoke.rs @@ -1,7 +1,7 @@ #![cfg(feature = "extensions")] use anyhow::{Result, anyhow, bail, ensure}; -use pglite_oxide::PgliteProxy; +use oliphaunt_wasix::OliphauntProxy; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::thread; @@ -17,7 +17,8 @@ fn tcp_proxy_handles_psql_style_connections() -> Result<()> { let addr = listener.local_addr()?; let root = temp_dir.path().to_path_buf(); - let handle = thread::spawn(move || PgliteProxy::new(root).accept_tcp_connections(&listener, 2)); + let handle = + thread::spawn(move || OliphauntProxy::new(root).accept_tcp_connections(&listener, 2)); let first = query_proxy(addr, false, "SELECT 1 AS one")?; assert_eq!(first, vec!["1"]); @@ -151,7 +152,7 @@ fn startup_message() -> Vec { for (key, value) in [ ("user", "postgres"), ("database", "template1"), - ("application_name", "pglite-oxide-test"), + ("application_name", "oliphaunt-wasix-test"), ] { message.extend_from_slice(key.as_bytes()); message.push(0); diff --git a/tests/runtime_smoke.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/runtime_smoke.rs similarity index 83% rename from tests/runtime_smoke.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/runtime_smoke.rs index cd817fe4..b8673f7f 100644 --- a/tests/runtime_smoke.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/runtime_smoke.rs @@ -1,8 +1,8 @@ #![cfg(feature = "extensions")] -use pglite_oxide::{ - DataDirArchiveFormat, ExecProtocolOptions, Pglite, PgliteError, PgliteServer, QueryOptions, - QueryTemplate, RowMode, format_query, quote_identifier, +use oliphaunt_wasix::{ + DataDirArchiveFormat, ExecProtocolOptions, Oliphaunt, OliphauntError, OliphauntServer, + QueryOptions, QueryTemplate, RowMode, format_query, quote_identifier, }; use serde_json::{Value, json}; use std::io::{BufRead, BufReader}; @@ -12,7 +12,7 @@ use std::sync::{Arc, Mutex}; mod support; use support::{ChildGuard, TestTrace, trace_step}; -fn first_row(result: &pglite_oxide::Results) -> anyhow::Result<&serde_json::Map> { +fn first_row(result: &oliphaunt_wasix::Results) -> anyhow::Result<&serde_json::Map> { result .rows .first() @@ -75,23 +75,23 @@ fn raw_message_tags_ignoring_parameter_status(bytes: &[u8]) -> Vec { .collect() } -fn raw_backend_message_name(message: &pglite_oxide::BackendMessage) -> &'static str { +fn raw_backend_message_name(message: &oliphaunt_wasix::BackendMessage) -> &'static str { match message { - pglite_oxide::BackendMessage::RowDescription(_) => "rowDescription", - pglite_oxide::BackendMessage::DataRow(_) => "dataRow", - pglite_oxide::BackendMessage::CommandComplete(_) => "commandComplete", - pglite_oxide::BackendMessage::ReadyForQuery(_) => "readyForQuery", - pglite_oxide::BackendMessage::Error(_) => "error", - pglite_oxide::BackendMessage::ParseComplete { .. } => "parseComplete", - pglite_oxide::BackendMessage::BindComplete { .. } => "bindComplete", + oliphaunt_wasix::BackendMessage::RowDescription(_) => "rowDescription", + oliphaunt_wasix::BackendMessage::DataRow(_) => "dataRow", + oliphaunt_wasix::BackendMessage::CommandComplete(_) => "commandComplete", + oliphaunt_wasix::BackendMessage::ReadyForQuery(_) => "readyForQuery", + oliphaunt_wasix::BackendMessage::Error(_) => "error", + oliphaunt_wasix::BackendMessage::ParseComplete { .. } => "parseComplete", + oliphaunt_wasix::BackendMessage::BindComplete { .. } => "bindComplete", _ => "other", } } fn assert_core_runtime_assets_stay_in_lower_mount(root: &std::path::Path) { - let runtime = root.join("tmp/pglite"); + let runtime = root.join("tmp/oliphaunt"); assert!( - runtime.join(".pglite-oxide-mountfs-runtime").is_file(), + runtime.join(".oliphaunt-wasix-mountfs-runtime").is_file(), "expected shared runtime overlay marker under {}", runtime.display() ); @@ -112,7 +112,10 @@ fn assert_core_runtime_assets_stay_in_lower_mount(root: &std::path::Path) { #[test] fn template_cache_false_runs_split_initdb() -> anyhow::Result<()> { - let mut db = Pglite::builder().temporary().template_cache(false).open()?; + let mut db = Oliphaunt::builder() + .temporary() + .template_cache(false) + .open()?; let result = db.query("SELECT 1 AS value", &[], None)?; assert_eq!(first_row(&result)?["value"], json!(1)); Ok(()) @@ -120,7 +123,7 @@ fn template_cache_false_runs_split_initdb() -> anyhow::Result<()> { #[test] fn gen_random_uuid_returns_fresh_values_across_queries() -> anyhow::Result<()> { - let mut db = Pglite::builder().temporary().open()?; + let mut db = Oliphaunt::builder().temporary().open()?; let mut ids = Vec::new(); for _ in 0..4 { @@ -144,7 +147,8 @@ fn gen_random_uuid_returns_fresh_values_across_queries() -> anyhow::Result<()> { #[test] fn direct_transaction_commit_rollback_and_error_recovery() -> anyhow::Result<()> { - let mut pg = Pglite::builder().temporary().open()?; + // OLIPHAUNT_DOCS_SNIPPET wasm-quickstart + let mut pg = Oliphaunt::builder().temporary().open()?; pg.exec( "CREATE TABLE direct_tx_items(id int PRIMARY KEY, value text)", None, @@ -185,7 +189,7 @@ fn direct_transaction_commit_rollback_and_error_recovery() -> anyhow::Result<()> }); let failed = failed.expect_err("transaction should return the SQL failure"); let pg_err = failed - .downcast_ref::() + .downcast_ref::() .expect("transaction SQL error should preserve Postgres fields"); assert_eq!(pg_err.database_error().code.as_deref(), Some("22012")); @@ -210,7 +214,7 @@ fn direct_transaction_commit_rollback_and_error_recovery() -> anyhow::Result<()> #[test] fn direct_startup_postgres_config_uses_real_guc_handling() -> anyhow::Result<()> { - let mut pg = Pglite::builder() + let mut pg = Oliphaunt::builder() .temporary() .postgres_config("synchronous_commit", "off") .postgres_config("work_mem", "8MB") @@ -256,7 +260,7 @@ fn direct_startup_postgres_config_uses_real_guc_handling() -> anyhow::Result<()> #[test] fn invalid_postgres_config_is_rejected_before_backend_startup() -> anyhow::Result<()> { - let err = match Pglite::builder() + let err = match Oliphaunt::builder() .temporary() .postgres_config("bad=name", "off") .open() @@ -275,13 +279,13 @@ fn invalid_postgres_config_is_rejected_before_backend_startup() -> anyhow::Resul fn direct_startup_identity_can_select_existing_user_and_database() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; { - let mut db = Pglite::builder().path(root.path()).open()?; + let mut db = Oliphaunt::builder().path(root.path()).open()?; db.exec("CREATE ROLE test_user LOGIN", None)?; db.exec("CREATE DATABASE test_db OWNER test_user", None)?; db.close()?; } - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .path(root.path()) .username("test_user") .database("test_db") @@ -300,7 +304,7 @@ fn direct_startup_identity_can_select_existing_user_and_database() -> anyhow::Re #[test] fn relaxed_durability_uses_postgres_guc() -> anyhow::Result<()> { - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() .relaxed_durability(true) .open()?; @@ -316,7 +320,7 @@ fn relaxed_durability_uses_postgres_guc() -> anyhow::Result<()> { #[test] fn relaxed_durability_is_idempotent_and_user_config_wins() -> anyhow::Result<()> { - let mut disabled = Pglite::builder() + let mut disabled = Oliphaunt::builder() .temporary() .relaxed_durability(true) .relaxed_durability(false) @@ -329,7 +333,7 @@ fn relaxed_durability_is_idempotent_and_user_config_wins() -> anyhow::Result<()> assert_eq!(first_row(&result)?.get("sync_commit"), Some(&json!("on"))); disabled.close()?; - let mut overridden = Pglite::builder() + let mut overridden = Oliphaunt::builder() .temporary() .relaxed_durability(true) .postgres_config("synchronous_commit", "on") @@ -346,9 +350,9 @@ fn relaxed_durability_is_idempotent_and_user_config_wins() -> anyhow::Result<()> #[test] fn startup_args_are_passed_to_postgres() -> anyhow::Result<()> { - let mut db = Pglite::builder() + let mut db = Oliphaunt::builder() .temporary() - .startup_args(["-c", "application_name=pglite-oxide-test"]) + .startup_args(["-c", "application_name=oliphaunt-wasix-test"]) .open()?; let result = db.query( "SELECT current_setting('application_name') AS app", @@ -357,7 +361,7 @@ fn startup_args_are_passed_to_postgres() -> anyhow::Result<()> { )?; assert_eq!( first_row(&result)?.get("app"), - Some(&json!("pglite-oxide-test")) + Some(&json!("oliphaunt-wasix-test")) ); db.close()?; Ok(()) @@ -365,7 +369,7 @@ fn startup_args_are_passed_to_postgres() -> anyhow::Result<()> { #[test] fn data_dir_dump_load_and_clone_round_trip() -> anyhow::Result<()> { - let mut source = Pglite::builder().temporary().open()?; + let mut source = Oliphaunt::builder().temporary().open()?; source.exec( "CREATE TABLE data_dir_items(id serial PRIMARY KEY, value text); INSERT INTO data_dir_items(value) VALUES ('alpha'), ('beta');", @@ -379,7 +383,7 @@ fn data_dir_dump_load_and_clone_round_trip() -> anyhow::Result<()> { )?; let archive = source.dump_data_dir_with_format(DataDirArchiveFormat::Tar)?; - let mut loaded = Pglite::builder() + let mut loaded = Oliphaunt::builder() .temporary() .load_data_dir_archive(archive) .open()?; @@ -415,8 +419,8 @@ fn data_dir_dump_load_and_clone_round_trip() -> anyhow::Result<()> { } #[test] -fn direct_raw_protocol_api_matches_pglite_exec_protocol_cases() -> anyhow::Result<()> { - let mut db = Pglite::builder().temporary().open()?; +fn direct_raw_protocol_api_matches_oliphaunt_exec_protocol_cases() -> anyhow::Result<()> { + let mut db = Oliphaunt::builder().temporary().open()?; let simple = db.exec_protocol( &raw_query_message("SELECT 1"), @@ -431,7 +435,7 @@ fn direct_raw_protocol_api_matches_pglite_exec_protocol_cases() -> anyhow::Resul .messages .iter() .filter(|message| { - !matches!(message, pglite_oxide::BackendMessage::ParameterStatus(_)) + !matches!(message, oliphaunt_wasix::BackendMessage::ParameterStatus(_)) }) .map(raw_backend_message_name) .collect::>(), @@ -462,7 +466,8 @@ fn direct_raw_protocol_api_matches_pglite_exec_protocol_cases() -> anyhow::Resul ) .expect_err("throw_on_error should return the Postgres error"); assert!( - err.downcast_ref::().is_some(), + err.downcast_ref::() + .is_some(), "unexpected raw protocol error: {err:#}" ); @@ -499,7 +504,7 @@ fn direct_raw_protocol_api_matches_pglite_exec_protocol_cases() -> anyhow::Resul #[cfg(debug_assertions)] #[test] fn direct_protocol_bridge_guest_allocations_are_freed() -> anyhow::Result<()> { - let mut db = Pglite::builder().temporary().open()?; + let mut db = Oliphaunt::builder().temporary().open()?; let (allocations_before, frees_before) = db.guest_bridge_allocation_counts(); assert_eq!( allocations_before, frees_before, @@ -540,7 +545,7 @@ fn direct_protocol_bridge_guest_allocations_are_freed() -> anyhow::Result<()> { fn pure_mountfs_serves_core_runtime_assets_from_lower_cache() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; { - let mut pg = Pglite::builder().path(root.path()).open()?; + let mut pg = Oliphaunt::builder().path(root.path()).open()?; let result = pg.query( "SELECT count(*)::int AS utc_zones \ FROM pg_timezone_names \ @@ -560,22 +565,35 @@ fn pure_mountfs_serves_core_runtime_assets_from_lower_cache() -> anyhow::Result< fn server_drop_without_explicit_shutdown_releases_root() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; { - let server = PgliteServer::builder().path(root.path()).start()?; + let server = OliphauntServer::builder().path(root.path()).start()?; assert!(server.tcp_addr().is_some()); } - let mut db = Pglite::builder().path(root.path()).open()?; + let mut db = Oliphaunt::builder().path(root.path()).open()?; let result = db.query("SELECT 1 AS value", &[], None)?; assert_eq!(first_row(&result)?.get("value"), Some(&json!(1))); db.close()?; Ok(()) } +#[test] +fn pg18_embedded_backend_uses_sync_io_method() -> anyhow::Result<()> { + let mut pg = Oliphaunt::builder().temporary().open()?; + let result = pg.query("SHOW io_method", &[], None)?; + assert_eq!( + first_row(&result)?.get("io_method"), + Some(&json!("sync")), + "PostgreSQL 18 worker AIO requires postmaster-managed IO workers; embedded WASIX uses sync AIO" + ); + pg.close()?; + Ok(()) +} + #[test] fn persistent_template_survives_restart_and_stale_state_files() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; { - let mut pg = Pglite::builder().path(root.path()).open()?; + let mut pg = Oliphaunt::builder().path(root.path()).open()?; pg.exec("CREATE TABLE template_restart(value TEXT)", None)?; pg.query( "INSERT INTO template_restart(value) VALUES ($1)", @@ -585,7 +603,7 @@ fn persistent_template_survives_restart_and_stale_state_files() -> anyhow::Resul pg.close()?; } - let pgdata = root.path().join("tmp/pglite/base"); + let pgdata = root.path().join("tmp/oliphaunt/base"); std::fs::write( pgdata.join("postmaster.pid"), b"stale pid from interrupted run", @@ -595,7 +613,7 @@ fn persistent_template_survives_restart_and_stale_state_files() -> anyhow::Resul b"stale opts from interrupted run", )?; - let mut reopened = Pglite::builder().path(root.path()).open()?; + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; let result = reopened.query("SELECT value FROM template_restart", &[], None)?; assert_eq!( first_row(&result)?.get("value"), @@ -610,12 +628,12 @@ fn persistent_template_survives_restart_and_stale_state_files() -> anyhow::Resul #[test] fn persistent_template_recovers_interrupted_pgdata_without_marker() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let pgdata = root.path().join("tmp/pglite/base"); + let pgdata = root.path().join("tmp/oliphaunt/base"); std::fs::create_dir_all(&pgdata)?; std::fs::write(pgdata.join("postmaster.pid"), b"interrupted pid")?; std::fs::write(pgdata.join("partial-bootstrap.sql"), b"interrupted initdb")?; - let mut pg = Pglite::builder().path(root.path()).open()?; + let mut pg = Oliphaunt::builder().path(root.path()).open()?; let result = pg.query("SELECT 1::int AS one", &[], None)?; assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); assert!(pgdata.join("PG_VERSION").exists()); @@ -628,12 +646,12 @@ fn persistent_template_recovers_interrupted_pgdata_without_marker() -> anyhow::R #[test] fn persistent_template_recovers_interrupted_pgdata_with_incomplete_markers() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let pgdata = root.path().join("tmp/pglite/base"); + let pgdata = root.path().join("tmp/oliphaunt/base"); std::fs::create_dir_all(&pgdata)?; std::fs::write(pgdata.join("PG_VERSION"), b"17\n")?; std::fs::write(pgdata.join("partial-bootstrap.sql"), b"interrupted initdb")?; - let mut pg = Pglite::builder().path(root.path()).open()?; + let mut pg = Oliphaunt::builder().path(root.path()).open()?; let result = pg.query("SELECT 2::int AS two", &[], None)?; assert_eq!(first_row(&result)?.get("two"), Some(&json!(2))); assert!(pgdata.join("PG_VERSION").exists()); @@ -646,16 +664,16 @@ fn persistent_template_recovers_interrupted_pgdata_with_incomplete_markers() -> #[test] fn persistent_root_lock_rejects_second_direct_open() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let mut first = Pglite::builder().path(root.path()).open()?; - let err = match Pglite::builder().path(root.path()).open() { + let mut first = Oliphaunt::builder().path(root.path()).open()?; + let err = match Oliphaunt::builder().path(root.path()).open() { Ok(_) => anyhow::bail!("second open must fail while the root lock is held"), Err(err) => err, }; - assert!(format!("{err:#}").contains("PGlite root is already in use")); + assert!(format!("{err:#}").contains("Oliphaunt root is already in use")); first.close()?; - let mut reopened = Pglite::builder().path(root.path()).open()?; + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; let result = reopened.query("SELECT 1::int AS one", &[], None)?; assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); reopened.close()?; @@ -665,12 +683,12 @@ fn persistent_root_lock_rejects_second_direct_open() -> anyhow::Result<()> { #[test] fn persistent_root_lock_rejects_second_server_open() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let server = PgliteServer::builder().path(root.path()).start()?; - let err = match PgliteServer::builder().path(root.path()).start() { + let server = OliphauntServer::builder().path(root.path()).start()?; + let err = match OliphauntServer::builder().path(root.path()).start() { Ok(_) => anyhow::bail!("second server must fail while the root lock is held"), Err(err) => err, }; - assert!(format!("{err:#}").contains("PGlite root is already in use")); + assert!(format!("{err:#}").contains("Oliphaunt root is already in use")); server.shutdown()?; Ok(()) } @@ -678,15 +696,15 @@ fn persistent_root_lock_rejects_second_server_open() -> anyhow::Result<()> { #[test] fn persistent_root_lock_rejects_direct_open_while_server_runs() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let server = PgliteServer::builder().path(root.path()).start()?; - let err = match Pglite::builder().path(root.path()).open() { + let server = OliphauntServer::builder().path(root.path()).start()?; + let err = match Oliphaunt::builder().path(root.path()).open() { Ok(_) => anyhow::bail!("direct open must fail while the server owns the root lock"), Err(err) => err, }; - assert!(format!("{err:#}").contains("PGlite root is already in use")); + assert!(format!("{err:#}").contains("Oliphaunt root is already in use")); server.shutdown()?; - let mut reopened = Pglite::builder().path(root.path()).open()?; + let mut reopened = Oliphaunt::builder().path(root.path()).open()?; let result = reopened.query("SELECT 1::int AS one", &[], None)?; assert_eq!(first_row(&result)?.get("one"), Some(&json!(1))); reopened.close()?; @@ -696,29 +714,29 @@ fn persistent_root_lock_rejects_direct_open_while_server_runs() -> anyhow::Resul #[test] fn persistent_root_lock_rejects_cross_process_open() -> anyhow::Result<()> { let root = tempfile::TempDir::new()?; - let child = Command::new(env!("CARGO_BIN_EXE_pglite-proxy")) + let child = Command::new(env!("CARGO_BIN_EXE_oliphaunt-wasix-proxy")) .arg("--root") .arg(root.path()) .args(["--tcp", "127.0.0.1:0", "--print-uri"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; - let mut child = ChildGuard::new(child, "pglite-proxy")?; + let mut child = ChildGuard::new(child, "oliphaunt-wasix-proxy")?; let stdout = child .child_mut() .stdout .take() - .ok_or_else(|| anyhow::anyhow!("missing pglite-proxy stdout"))?; + .ok_or_else(|| anyhow::anyhow!("missing oliphaunt-wasix-proxy stdout"))?; let mut line = String::new(); let read = BufReader::new(stdout).read_line(&mut line)?; if read == 0 { let stderr = child.collect_stderr(); - anyhow::bail!("pglite-proxy exited before printing URI\n\nstderr:\n{stderr}"); + anyhow::bail!("oliphaunt-wasix-proxy exited before printing URI\n\nstderr:\n{stderr}"); } assert!(line.starts_with("postgresql://"), "{line:?}"); - let err = match Pglite::builder().path(root.path()).open() { + let err = match Oliphaunt::builder().path(root.path()).open() { Ok(mut db) => { let close = db.close(); let stderr = child.collect_stderr(); @@ -729,7 +747,7 @@ fn persistent_root_lock_rejects_cross_process_open() -> anyhow::Result<()> { Err(err) => err, }; let message = format!("{err:#}"); - if !message.contains("PGlite root is already in use") { + if !message.contains("Oliphaunt root is already in use") { let stderr = child.collect_stderr(); anyhow::bail!("unexpected cross-process root-lock error: {message}\n\nstderr:\n{stderr}"); } @@ -739,7 +757,7 @@ fn persistent_root_lock_rejects_cross_process_open() -> anyhow::Result<()> { #[test] fn runtime_smoke() -> anyhow::Result<()> { let _trace = TestTrace::new("runtime_smoke"); - let mut pg = Pglite::builder().temporary().open()?; + let mut pg = Oliphaunt::builder().temporary().open()?; assert!(pg.paths().pgdata.join("PG_VERSION").exists()); let version = pg.query( @@ -789,6 +807,34 @@ fn runtime_smoke() -> anyhow::Result<()> { assert_eq!(timezone_row.get("ny_summer_hour"), Some(&json!(8))); assert_eq!(timezone_row.get("ny_winter_hour"), Some(&json!(7))); + #[cfg(feature = "icu")] + { + let icu_available = pg.query( + "SELECT (count(*) > 0) AS available FROM pg_collation WHERE collprovider = 'i'", + &[], + None, + )?; + assert_eq!( + first_row(&icu_available)?.get("available"), + Some(&json!(true)) + ); + pg.exec( + "CREATE COLLATION und_numeric \ + (provider = icu, locale = 'und-u-kn-true', deterministic = false)", + None, + )?; + let icu_ordered = pg.query( + "SELECT string_agg(value, ',' ORDER BY value COLLATE und_numeric) AS values \ + FROM (VALUES ('10'), ('2'), ('1')) AS input(value)", + &[], + None, + )?; + assert_eq!( + first_row(&icu_ordered)?.get("values"), + Some(&json!("1,2,10")) + ); + } + trace_step("runtime_smoke expected-error invalid-timezone"); pg.exec("SET TIME ZONE 'Missing/Zone'", None) .expect_err("invalid timezone should fail"); @@ -878,7 +924,7 @@ fn runtime_smoke() -> anyhow::Result<()> { &[ json!(41), json!(true), - json!({"name": "pglite", "ok": true}), + json!({"name": "oliphaunt", "ok": true}), json!(["alpha", "beta,gamma"]), json!([0, 1, 2, 255]), ], @@ -889,7 +935,7 @@ fn runtime_smoke() -> anyhow::Result<()> { assert_eq!(typed_row.get("flag"), Some(&json!(true))); assert_eq!( typed_row.get("doc").and_then(|value| value.get("name")), - Some(&json!("pglite")) + Some(&json!("oliphaunt")) ); assert_eq!( typed_row.get("labels"), @@ -961,7 +1007,7 @@ fn runtime_smoke() -> anyhow::Result<()> { .exec("SELECT +", None) .expect_err("syntax error should fail"); let syntax_pg_err = syntax_err - .downcast_ref::() + .downcast_ref::() .expect("syntax error should preserve Postgres error fields"); assert_eq!(syntax_pg_err.query(), "SELECT +"); assert_eq!( @@ -978,7 +1024,7 @@ fn runtime_smoke() -> anyhow::Result<()> { ) .expect_err("missing table should fail"); let missing_pg_err = missing_err - .downcast_ref::() + .downcast_ref::() .expect("extended query error should preserve Postgres error fields"); assert_eq!( missing_pg_err.query(), @@ -995,7 +1041,7 @@ fn runtime_smoke() -> anyhow::Result<()> { .query("SELECT $1::int4 AS value", &[json!("not_an_int")], None) .expect_err("invalid typed parameter should fail during extended-query bind"); let invalid_bind_pg_err = invalid_bind - .downcast_ref::() + .downcast_ref::() .expect("bind error should preserve Postgres error fields"); assert_eq!(invalid_bind_pg_err.query(), "SELECT $1::int4 AS value"); assert_eq!(invalid_bind_pg_err.params(), &[json!("not_an_int")]); @@ -1009,7 +1055,7 @@ fn runtime_smoke() -> anyhow::Result<()> { .query("SELECT $1::int4 + $2::int4 AS value", &[json!(1)], None) .expect_err("missing parameter should fail during extended-query bind"); let wrong_param_count_pg_err = wrong_param_count - .downcast_ref::() + .downcast_ref::() .expect("parameter count error should preserve Postgres error fields"); assert_eq!( wrong_param_count_pg_err.database_error().code.as_deref(), @@ -1022,7 +1068,7 @@ fn runtime_smoke() -> anyhow::Result<()> { pg.close()?; assert!(pg.is_closed()); - let mut restarted = Pglite::temporary()?; + let mut restarted = Oliphaunt::temporary()?; let restarted_result = restarted.query("SELECT 42::int AS answer", &[], None)?; assert_eq!( first_row(&restarted_result)?.get("answer"), @@ -1032,7 +1078,7 @@ fn runtime_smoke() -> anyhow::Result<()> { let persistent_dir = tempfile::TempDir::new()?; { - let mut persisted = Pglite::builder().path(persistent_dir.path()).open()?; + let mut persisted = Oliphaunt::builder().path(persistent_dir.path()).open()?; persisted.exec("CREATE TABLE persisted(value TEXT)", None)?; persisted.query( "INSERT INTO persisted(value) VALUES ($1)", @@ -1042,7 +1088,7 @@ fn runtime_smoke() -> anyhow::Result<()> { persisted.close()?; } { - let mut reopened = Pglite::open(persistent_dir.path())?; + let mut reopened = Oliphaunt::open(persistent_dir.path())?; let persisted_result = reopened.query("SELECT value FROM persisted", &[], None)?; assert_eq!( first_row(&persisted_result)?.get("value"), diff --git a/tests/support/mod.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/support/mod.rs similarity index 100% rename from tests/support/mod.rs rename to src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/support/mod.rs diff --git a/src/bindings/wasix-rust/examples/build_pgdata_template.rs b/src/bindings/wasix-rust/examples/build_pgdata_template.rs new file mode 100644 index 00000000..e9d03b0b --- /dev/null +++ b/src/bindings/wasix-rust/examples/build_pgdata_template.rs @@ -0,0 +1,16 @@ +use std::env; +use std::path::PathBuf; + +use anyhow::Result; +use oliphaunt_wasix::build_pgdata_template; + +fn main() -> Result<()> { + let output_dir = env::args_os().nth(1).map(PathBuf::from).unwrap_or_else(|| { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/oliphaunt-wasix/assets/prepopulated") + }); + + let template = build_pgdata_template(&output_dir)?; + println!("archive: {}", template.archive_path.display()); + println!("manifest: {}", template.manifest_path.display()); + Ok(()) +} diff --git a/examples/tauri-sqlx-vanilla/.gitignore b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/.gitignore similarity index 100% rename from examples/tauri-sqlx-vanilla/.gitignore rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/.gitignore diff --git a/examples/tauri-sqlx-vanilla/.vscode/extensions.json b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/.vscode/extensions.json similarity index 100% rename from examples/tauri-sqlx-vanilla/.vscode/extensions.json rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/.vscode/extensions.json diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md new file mode 100644 index 00000000..b8ab92b5 --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md @@ -0,0 +1,38 @@ +# oliphaunt-wasix Tauri SQLx example + +This is a Tauri v2 example that keeps `oliphaunt-wasix` in Rust state and talks to +it through a real one-connection `sqlx::PgPool`. + +## Run the desktop app + +```sh +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 +is invoked from the UI. + +## Run the headless profiler + +```sh +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 +measure a warm start with an existing cluster. + +## What it demonstrates + +- 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; +- 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/index.html b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/index.html new file mode 100644 index 00000000..089ea325 --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/index.html @@ -0,0 +1,96 @@ + + + + + + + oliphaunt SQLx profile + + + + +
+
+
+

oliphaunt-wasix / Tauri / SQLx

+

Embedded Postgres profile

+
+
Idle
+
+ +
+ + + +
+ +
+
+ Startup + - +
+
+ Workload + - +
+
+ Rows + - +
+
+ Proxy + - +
+
+ +
+
+
+

Startup phases

+ - +
+
+
+ +
+
+

Workload phases

+ - +
+
+
+
+ +
+
+

SQLx query timings

+ P50 / P95 / max +
+
+ + + + + + + + + + + + + +
QueryRunsRowsMeanP50P95Max
+
+
+ +
+
+ + diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json new file mode 100644 index 00000000..fe2fcf3b --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json @@ -0,0 +1,21 @@ +{ + "name": "tauri-sqlx-vanilla", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "vite": "^6.0.3", + "typescript": "~5.6.2" + } +} diff --git a/examples/tauri-sqlx-vanilla/src-tauri/.gitignore b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/.gitignore similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/.gitignore rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/.gitignore 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 new file mode 100644 index 00000000..6ac10672 --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "tauri-sqlx-vanilla" +version = "0.0.0" +description = "Vanilla Tauri v2 SQLx profiler for oliphaunt-wasix" +authors = ["oliphaunt-wasix contributors"] +edition = "2021" +publish = false + +[workspace] + +[lib] +name = "tauri_sqlx_vanilla_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +anyhow = "1" +oliphaunt-wasix = { version = "=0.1.0", features = [ + "extensions", + "tools", +] } +oliphaunt-wasix-tools = { version = "=0.1.0" } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +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" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0" } diff --git a/examples/tauri-sqlx-vanilla/src-tauri/build.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/build.rs similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/build.rs rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/build.rs diff --git a/examples/tauri-sqlx-vanilla/src-tauri/capabilities/default.json b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/capabilities/default.json similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/capabilities/default.json rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/capabilities/default.json diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/128x128.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128@2x.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128@2x.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/128x128@2x.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/128x128@2x.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/32x32.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/32x32.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/32x32.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/32x32.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square107x107Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square107x107Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square107x107Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square107x107Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square142x142Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square142x142Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square142x142Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square142x142Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square150x150Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square150x150Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square150x150Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square150x150Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square284x284Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square284x284Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square284x284Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square284x284Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square30x30Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square30x30Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square30x30Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square30x30Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square310x310Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square310x310Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square310x310Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square310x310Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square44x44Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square44x44Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square44x44Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square44x44Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square71x71Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square71x71Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square71x71Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square71x71Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/Square89x89Logo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square89x89Logo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/Square89x89Logo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/Square89x89Logo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/StoreLogo.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/StoreLogo.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/StoreLogo.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/StoreLogo.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.icns b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.icns similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/icon.icns rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.icns diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.ico b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.ico similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/icon.ico rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.ico diff --git a/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png diff --git a/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs similarity index 89% rename from examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index 8c58ab99..20678a3a 100644 --- a/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -4,7 +4,10 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; use anyhow::{anyhow, bail, Context, Result}; -use pglite_oxide::{install_into, preload_runtime_module, PglitePaths, PgliteServer}; +use oliphaunt_wasix::{ + install_into, preload_runtime_module, OliphauntPaths, OliphauntServer, PgDumpOptions, + PsqlOptions, +}; use serde::Serialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode}; use sqlx::{PgPool, Row}; @@ -64,7 +67,7 @@ impl BenchState { let mut guard = self.inner.lock().await; if fresh && guard.is_some() { bail!( - "fresh profile requires restarting the app so the existing pglite proxy can exit" + "fresh profile requires restarting the app so the existing oliphaunt proxy can exit" ); } @@ -84,7 +87,7 @@ pub struct DatabaseHarness { root: PathBuf, database_url: String, pool: PgPool, - _server: PgliteServer, + _server: OliphauntServer, cold_start: bool, startup: Vec, } @@ -97,7 +100,7 @@ impl DatabaseHarness { } fs::create_dir_all(&root).with_context(|| format!("create {}", root.display()))?; - let paths = PglitePaths::with_root(&root); + let paths = OliphauntPaths::with_root(&root); let cold_start = !paths.pgdata.join("PG_VERSION").exists(); let mut startup = Vec::new(); @@ -116,22 +119,27 @@ impl DatabaseHarness { .await?; let server_root = root.clone(); - let server = time_blocking(&mut startup, "start pglite server", move || { + let server = time_blocking(&mut startup, "start oliphaunt server", move || { preferred_server(server_root) }) .await?; + 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 { let options = - pg_connect_options(&server)?.application_name("pglite-oxide-tauri-sqlx-profile"); + pg_connect_options(&server)?.application_name("oliphaunt-wasix-tauri-sqlx-profile"); PgPoolOptions::new() .max_connections(1) .acquire_timeout(Duration::from_secs(30)) .connect_with(options) .await - .context("connect SQLx pool to pglite proxy") + .context("connect SQLx pool to oliphaunt proxy") }) .await?; @@ -306,7 +314,7 @@ impl DatabaseHarness { "The Tauri window is allowed to paint before this command initializes the database." .to_string(), "Fresh starts use the bundled prepopulated PGDATA template before the backend session starts.".to_string(), - "SQLx is configured with one PostgreSQL connection because the embedded pglite runtime is single-process." + "SQLx is configured with one PostgreSQL connection because the embedded oliphaunt runtime is single-process." .to_string(), "The SQLx pool connection phase includes the first backend wire-protocol handshake." .to_string(), @@ -329,19 +337,28 @@ impl DatabaseHarness { } } -fn preferred_server(root: PathBuf) -> Result { - let builder = PgliteServer::builder().path(&root); - #[cfg(unix)] - { - builder.unix(root.join(".s.PGSQL.5432")).start() - } - #[cfg(not(unix))] - { - builder.start() - } +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"), + "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(()) +} + +fn preferred_server(root: PathBuf) -> Result { + OliphauntServer::builder().path(&root).start() } -fn pg_connect_options(server: &PgliteServer) -> Result { +fn pg_connect_options(server: &OliphauntServer) -> Result { let options = PgConnectOptions::new() .username("postgres") .database("template1") @@ -357,7 +374,7 @@ fn pg_connect_options(server: &PgliteServer) -> Result { let addr = server .tcp_addr() - .ok_or_else(|| anyhow!("PGlite server did not expose a TCP address"))?; + .ok_or_else(|| anyhow!("Oliphaunt server did not expose a TCP address"))?; Ok(options.host(&addr.ip().to_string()).port(addr.port())) } diff --git a/examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs similarity index 86% rename from examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs index 9b84fb33..a358bd85 100644 --- a/examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bin/profile_queries.rs @@ -20,9 +20,9 @@ async fn main() -> anyhow::Result<()> { .and_then(|index| args.get(index + 1)) .map(PathBuf::from); - let root = env::var_os("PGLITE_OXIDE_TAURI_PROFILE_DIR") + let root = env::var_os("OLIPHAUNT_WASM_TAURI_PROFILE_DIR") .map(PathBuf::from) - .unwrap_or_else(|| env::temp_dir().join("pglite-oxide-tauri-sqlx-profile")); + .unwrap_or_else(|| env::temp_dir().join("oliphaunt-wasix-tauri-sqlx-profile")); let state = BenchState::new(root); let report = state.profile_queries(fresh, row_count).await?; diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs new file mode 100644 index 00000000..e7d52d97 --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/lib.rs @@ -0,0 +1,56 @@ +pub mod bench; + +use serde::ser::Serializer; +use tauri::Manager; + +use bench::{BenchReport, BenchState}; + +#[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:#}")) + } +} + +#[tauri::command] +async fn profile_queries( + state: tauri::State<'_, BenchState>, + fresh: bool, + row_count: u32, +) -> Result { + state + .profile_queries(fresh, row_count) + .await + .map_err(CommandError::from) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + let root = app + .path() + .app_data_dir() + .map(|dir| dir.join("oliphaunt-sqlx-profile"))?; + app.manage(BenchState::new(root)); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![profile_queries]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/tauri-sqlx-vanilla/src-tauri/src/main.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/main.rs similarity index 100% rename from examples/tauri-sqlx-vanilla/src-tauri/src/main.rs rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/main.rs diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json new file mode 100644 index 00000000..3cff8824 --- /dev/null +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/tauri.conf.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "oliphaunt SQLx Profile", + "version": "0.1.0", + "identifier": "com.oliphauntoxide.sqlxbench", + "build": { + "beforeDevCommand": "pnpm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "pnpm run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "oliphaunt SQLx Profile", + "width": 1080, + "height": 760 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/examples/tauri-sqlx-vanilla/src/assets/tauri.svg b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/tauri.svg similarity index 100% rename from examples/tauri-sqlx-vanilla/src/assets/tauri.svg rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/tauri.svg diff --git a/examples/tauri-sqlx-vanilla/src/assets/typescript.svg b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/typescript.svg similarity index 100% rename from examples/tauri-sqlx-vanilla/src/assets/typescript.svg rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/typescript.svg diff --git a/examples/tauri-sqlx-vanilla/src/assets/vite.svg b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/vite.svg similarity index 100% rename from examples/tauri-sqlx-vanilla/src/assets/vite.svg rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/assets/vite.svg diff --git a/examples/tauri-sqlx-vanilla/src/main.ts b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/main.ts similarity index 100% rename from examples/tauri-sqlx-vanilla/src/main.ts rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/main.ts diff --git a/examples/tauri-sqlx-vanilla/src/styles.css b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/styles.css similarity index 100% rename from examples/tauri-sqlx-vanilla/src/styles.css rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src/styles.css diff --git a/examples/tauri-sqlx-vanilla/tsconfig.json b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/tsconfig.json similarity index 100% rename from examples/tauri-sqlx-vanilla/tsconfig.json rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/tsconfig.json diff --git a/examples/tauri-sqlx-vanilla/vite.config.ts b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/vite.config.ts similarity index 100% rename from examples/tauri-sqlx-vanilla/vite.config.ts rename to src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/vite.config.ts diff --git a/src/bindings/wasix-rust/moon.yml b/src/bindings/wasix-rust/moon.yml new file mode 100644 index 00000000..494f5974 --- /dev/null +++ b/src/bindings/wasix-rust/moon.yml @@ -0,0 +1,219 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-wasix-rust" +language: "rust" +layer: "library" +stack: "systems" +tags: ["binding", "wasix", "rust", "postgres", "release-product"] +dependsOn: + - "liboliphaunt-wasix" + - id: "shared-fixtures" + scope: "build" + +project: + title: "Oliphaunt WASIX Rust" + description: "Rust binding over the liboliphaunt WASIX runtime." + owner: "oliphaunt" + release: + component: "oliphaunt-wasix-rust" + packagePath: "src/bindings/wasix-rust/crates/oliphaunt-wasix" + +owners: + defaultOwner: "@oliphaunt/wasix-rust" + paths: + "**/*.rs": ["@oliphaunt/wasix-rust"] + "crates/oliphaunt-wasix/**": ["@oliphaunt/wasix-rust"] + "examples/**": ["@oliphaunt/wasix-rust"] + "tools/**": ["@oliphaunt/wasix-rust"] + +tasks: + check: + tags: ["quality", "static", "ci-rust"] + command: "cargo check -p oliphaunt-wasix --locked" + deps: + - "liboliphaunt-wasix:check" + - "shared-fixtures:check" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/check" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + test: + tags: ["quality", "unit", "ci-rust"] + command: "bash src/bindings/wasix-rust/tools/check-unit.sh" + deps: + - "liboliphaunt-wasix:check" + - "shared-fixtures:check" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/test" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/shared/fixtures/**/*" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + package: + tags: ["package"] + command: "bash src/bindings/wasix-rust/tools/check-package.sh" + deps: + - "oliphaunt-wasix-rust:check" + - "oliphaunt-wasix-rust:test" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/package" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + - "/src/bindings/wasix-rust/tools/check-package.sh" + outputs: + - "/target/oliphaunt-wasix-rust/package/oliphaunt-wasix.package-files.txt" + options: + cache: true + runFromWorkspaceRoot: true + + package-artifacts: + tags: ["release", "artifact-package", "ci-wasix-rust-package"] + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-wasix-rust" + deps: + - "oliphaunt-wasix-rust:package" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/package-artifacts" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + - "/src/bindings/wasix-rust/tools/check-package.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/package_oliphaunt_wasix_sdk_crate.mjs" + outputs: + - "/target/sdk-artifacts/oliphaunt-wasix-rust/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + release-check: + tags: ["release", "package"] + 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: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/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 + + example-check: + tags: ["examples", "quality", "ci-wasm-regression"] + command: "bash src/bindings/wasix-rust/tools/check-examples.sh" + deps: + - "liboliphaunt-wasix:check" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + - "/tools/dev/write-scoped-pnpm-workspace.mjs" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/crates/**/*" + - "/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: + cache: local + runFromWorkspaceRoot: true + + bench: + tags: ["bench", "plan"] + command: "cargo test -p oliphaunt-wasix --locked --test performance_smoke --no-run" + deps: + - "liboliphaunt-wasix:check" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/bench" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + - "/tools/perf/**/*" + - "/benchmarks/**/*" + options: + cache: true + runFromWorkspaceRoot: true + runInCI: false + + coverage: + tags: ["coverage", "quality"] + command: "tools/coverage/run-product oliphaunt-wasix-rust" + deps: + - "liboliphaunt-wasix:check" + - "shared-fixtures:check" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/coverage" + inputs: + - "/.config/nextest.toml" + - "/Cargo.lock" + - "/Cargo.toml" + - "/coverage/baseline.toml" + - "/src/shared/fixtures/**/*" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + - "/tools/coverage/**/*" + outputs: + - "/target/coverage/oliphaunt-wasix-rust/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + bench-run: + tags: ["bench", "measured"] + command: "cargo test -p oliphaunt-wasix --locked --test performance_smoke -- --nocapture" + deps: + - "liboliphaunt-wasix:check" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/bench-run" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/bindings/wasix-rust/**/*" + - "/src/runtimes/liboliphaunt/wasix/crates/**/*" + - "/tools/perf/**/*" + - "/benchmarks/**/*" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: false diff --git a/src/bindings/wasix-rust/tools/check-examples.sh b/src/bindings/wasix-rust/tools/check-examples.sh new file mode 100755 index 00000000..6ba9cd80 --- /dev/null +++ b/src/bindings/wasix-rust/tools/check-examples.sh @@ -0,0 +1,61 @@ +#!/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" + +if [[ -z "${CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX:-}" ]]; then + exec examples/tools/with-local-registries.sh bash "$0" +fi + +run() { + printf '\n==> %s\n' "$*" + "$@" +} + +source_dir="src/bindings/wasix-rust/examples/tauri-sqlx-vanilla" +workspace="target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/workspaces/$$" +work="$workspace/$source_dir" +trap 'rm -rf "$workspace"' EXIT +rm -rf "$workspace" +mkdir -p "$work" "$workspace/src/bindings/wasix-rust" "$workspace/src/runtimes/liboliphaunt" + +rsync -a --delete \ + --exclude node_modules \ + --exclude dist \ + --exclude src-tauri/gen \ + --exclude src-tauri/target \ + "$source_dir/" "$work/" + +ln -s "$root/src/bindings/wasix-rust/crates" "$workspace/src/bindings/wasix-rust/crates" +ln -s "$root/src/runtimes/liboliphaunt/wasix" "$workspace/src/runtimes/liboliphaunt/wasix" + +run cargo generate-lockfile \ + --manifest-path "$work/src-tauri/Cargo.toml" +run cargo check \ + --manifest-path "$work/src-tauri/Cargo.toml" \ + --target-dir target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/src-tauri \ + --locked + +cat >"$workspace/package.json" <<'JSON' +{ + "name": "oliphaunt-tauri-example-check-workspace", + "private": true, + "packageManager": "pnpm@11.5.0" +} +JSON +run node "$root/tools/dev/write-scoped-pnpm-workspace.mjs" \ + --source "$root/pnpm-workspace.yaml" \ + --output "$workspace/pnpm-workspace.yaml" \ + --package "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla" +cp pnpm-lock.yaml "$workspace/pnpm-lock.yaml" + +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 diff --git a/src/bindings/wasix-rust/tools/check-package.sh b/src/bindings/wasix-rust/tools/check-package.sh new file mode 100755 index 00000000..9c4b5357 --- /dev/null +++ b/src/bindings/wasix-rust/tools/check-package.sh @@ -0,0 +1,127 @@ +#!/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" + +out_dir="target/oliphaunt-wasix-rust/package" +listing="$out_dir/oliphaunt-wasix.package-files.txt" +mkdir -p "$out_dir" + +cargo package --list -p oliphaunt-wasix --locked --allow-dirty >"$listing" + +require_entry() { + local entry="$1" + if ! grep -Fxq "$entry" "$listing"; then + echo "oliphaunt-wasix package is missing required entry: $entry" >&2 + exit 1 + fi +} + +reject_pattern() { + local pattern="$1" + if grep -Eq "$pattern" "$listing"; then + echo "oliphaunt-wasix package contains forbidden runtime/build entry matching: $pattern" >&2 + grep -E "$pattern" "$listing" >&2 + exit 1 + 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" +require_entry "src/bin/oliphaunt_wasix_dump.rs" +require_entry "src/bin/oliphaunt_wasix_proxy.rs" +require_entry "src/oliphaunt/aot.rs" +require_entry "src/oliphaunt/assets.rs" +require_entry "src/protocol/parser.rs" + +reject_pattern '(^|/)(payload|artifacts|target)(/|$)' +reject_pattern '(^|/)assets/generated(/|$)' +reject_pattern '^src/runtimes/' +reject_pattern '^src/extensions/generated/' +reject_pattern '^release.toml$' + +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 + +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/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/src/bindings/wasix-rust/tools/check-unit.sh b/src/bindings/wasix-rust/tools/check-unit.sh new file mode 100755 index 00000000..90f5dd8f --- /dev/null +++ b/src/bindings/wasix-rust/tools/check-unit.sh @@ -0,0 +1,22 @@ +#!/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" + +if ! cargo nextest --version >/dev/null 2>&1; then + echo "missing cargo-nextest; run tools/dev/bootstrap-tools.sh" >&2 + exit 1 +fi + +printf '\n==> cargo test -p oliphaunt-wasix --doc --locked\n' +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/src/docs/.gitignore b/src/docs/.gitignore new file mode 100644 index 00000000..8a11fd9b --- /dev/null +++ b/src/docs/.gitignore @@ -0,0 +1,26 @@ +# deps +/node_modules + +# generated content +.source + +# test & build +/coverage +/.next/ +/out/ +/build +*.tsbuildinfo + +# misc +.DS_Store +*.pem +/.pnp +.pnp.js +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# others +.env*.local +.vercel +next-env.d.ts diff --git a/src/docs/DESIGN_GROUNDING.md b/src/docs/DESIGN_GROUNDING.md new file mode 100644 index 00000000..b86bd43d --- /dev/null +++ b/src/docs/DESIGN_GROUNDING.md @@ -0,0 +1,108 @@ +# Oliphaunt Docs Design Grounding + +This file keeps the docs-site work scoped to the visual and UX foundation for +`src/docs`. + +## Goal + +Build a striking, mobile-first docs foundation for Oliphaunt. The site should +feel like a polished product surface for a polyglot embedded PostgreSQL library: +SDK packages, runtime modes, tooling, maintainer paths, and equivalent examples +across languages. + +Documentation completeness is secondary. Presentation, wayfinding, interaction, +light/dark quality, and reusable docs affordances are the work. + +The active quality bar is Motion-level craft across the whole docs app, not just +the first viewport. Every pass should re-check Motion, inspect rendered +Oliphaunt pages, remove redundant or over-boxed UI, and keep code-looking text +semantic: inline code for identifiers, real code blocks for commands/examples. + +## Motion Docs Takeaways + +Observed from `https://motion.dev/docs`, `/docs/react`, and deeper docs pages: + +- Oversized, confident page titles with tight copy and generous vertical rhythm. +- Small monospace section labels, breadcrumbs, and version-like pills create a + product/manual feel. +- The best visual texture is functional: line grids, diagonal hatching, compact + charts, code, and live-demo surfaces. +- Cards are crisp and low-radius, often row-based rather than decorative. +- Motion's docs home uses a dark product/manual surface, a compact technical + chart, a small set of primary route cards, and row-based secondary links; it + does not repeat full tutorial content on the landing page. +- Mobile strips the experience down to strong title, intro, actions, and content; + side navigation should not dominate the first screen. +- Animations should be restrained: subtle entrances, hover shifts, focus states, + and ambient technical motion that respects reduced-motion. +- Code examples need to feel central and copyable, with clear language switching + for the same app flow. +- Docs pages rely mostly on prose, rows, dividers, tables, and occasional code; + custom panels should be rare and earn their space. + +## Oliphaunt Foundation Principles + +- Keep the first screen product-like: Oliphaunt, embedded PostgreSQL, SDKs, + runtime modes, and a visible code/system artifact. +- Use a balanced palette: ink/ivory neutrals with green as primary, amber/cyan + accents for state and language surfaces. Avoid a one-note green or slate UI. +- Keep page sections unframed; cards are only for repeated items and tools. +- Use 8px or smaller radii unless Fumadocs requires otherwise. +- Prefer icons for recognisable tools/actions, with text for clear commands. +- Make polyglot examples a first-class pattern, not an afterthought. +- Maintain light and dark mode parity. +- Preserve generated-content boundaries: edit presentation components, app + routes, theme CSS, and docs-app metadata; avoid changing generated targets. +- Prefer divider-based row lists over nested cards when the user is choosing + among pages, SDKs, modes, or reference lookups. + +## Review Protocol + +- Revisit the docs app on mobile and desktop after substantial layout edits. +- Run `pnpm --dir src/docs check` before handing off docs changes. +- Use `pnpm --dir src/docs build` when changes touch route composition, + metadata, generated content, or Next.js boundaries. + +## Implementation Checklist + +- [x] Scope remains inside `src/docs`. +- [ ] Landing page and every docs route reach Motion-level cleanliness on mobile + and desktop. +- [x] Light and dark mode both have intentional contrast and texture. +- [ ] Navigation and doc reading surfaces feel compact, clean, and polished on + every route. +- [x] Polyglot code examples show the same flow across languages. +- [ ] Reusable MDX components share a restrained row/table/prose visual language. +- [x] Browser screenshots reviewed full-page on mobile and desktop after each + major slice. +- [x] Motion reference pages reviewed during each active implementation turn. +- [x] `pnpm --dir src/docs run check` or best available equivalent is + run before final handoff. + +## Current Slice Notes + +- Landing was reduced to hero, SDK choices, and reference paths; standalone + landing code comparisons and repeated runtime/docs/CTA sections were removed. +- `/docs/start` was reduced to quickstart, first-query comparison, and next + steps; redundant outcome and verify panels were removed. +- `/docs/learn` was converted from card-heavy maps/tabs to divider rows and + prose bullets. +- `/docs/sdk` moved from card-heavy SDK chooser and runtime matrix to divider + rows. Focused audit improved `borderedPanels` from 35 to 13 and code blocks + from 7 to 0 on the SDK index. +- Reference lookup/capability/extension/performance/release components moved + from boxed grids to divider rows. The audit metric now separates icon tiles + from real bordered panels. +- This pass refreshed Motion `/docs`, `/docs/react`, `/docs/react-animation`, + `/docs/react-transitions`, and `/docs/react-layout-animations` screenshots. +- Home now uses a dark Motion-like technical hero, a visible mobile product map, + and SDK rows instead of seven uneven SDK cards. +- `/docs/start` now uses unboxed quickstart rows, flatter code blocks, and + row-based next steps. Focused audit improved `borderedPanels` from 8 to 2 on + desktop and mobile with no horizontal overflow. +- Install prose no longer renders as terminal code in shared SDK summary + components; real install commands remain code blocks. +- Next likely targets from full audit: React Native/native runtime panels, + embedded/mobile/SQLite/Tauri/WASM `gap-px bg-fd-border` grids, SDK index + content duplication, API reference identifier semantics, and tabbed polyglot + code affordances. diff --git a/src/docs/README.md b/src/docs/README.md new file mode 100644 index 00000000..f344aece --- /dev/null +++ b/src/docs/README.md @@ -0,0 +1,41 @@ +# docs + +This is a Next.js application generated with +[Create Fumadocs](https://github.com/fuma-nama/fumadocs). + +Run the development server from the repository root: + +```bash +pnpm docs:dev +``` + +Open http://localhost:3000 with your browser to see the result. + +## Explore + +In the project, you can see: + +- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content. +- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep. + +| Route | Description | +| ------------------------- | ------------------------------------------------------ | +| `app/(home)` | The route group for your landing page and other pages. | +| `app/docs` | The documentation layout and pages. | +| `app/api/search/route.ts` | The Route Handler for search. | + +### Fumadocs MDX + +A `source.config.ts` config file has been included, you can customise different options like frontmatter schema. + +Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details. + +## Learn More + +To learn more about Next.js and Fumadocs, take a look at the following +resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js + features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs diff --git a/src/docs/content/learn/embedded-postgres.mdx b/src/docs/content/learn/embedded-postgres.mdx new file mode 100644 index 00000000..eab4f4c8 --- /dev/null +++ b/src/docs/content/learn/embedded-postgres.mdx @@ -0,0 +1,74 @@ +--- +sidebar_position: 1 +title: Embedded PostgreSQL +description: Learn how PostgreSQL roots, WAL, lifecycle, extensions, backup, and restore fit inside an app. +--- + +# Embedded PostgreSQL + +Oliphaunt embeds PostgreSQL behind SDK-native APIs while keeping PostgreSQL's +storage, WAL, SQL, protocol, and extension model recognizable. The SDK boundary +owns lifecycle, packaged runtime assets, exact extension selection, and app-safe +defaults. + + + +The product line has two runtime families. + +| Runtime family | Use it for | +| --- | --- | +| Native `liboliphaunt` | Rust, Swift, Kotlin, React Native, TypeScript native paths, and C ABI consumers | +| `oliphaunt-wasix` | The first-class WASM/WASIX runtime family | + +Both families share public concepts such as roots, capabilities, exact +extensions, backup, and restore. Runtime details differ by family, so native and +WASM packages expose their own capabilities for app decisions. + +## Root Storage + +An Oliphaunt database root is a PostgreSQL directory. A persistent root contains +PostgreSQL data, WAL, Oliphaunt metadata, root locks, selected extension +metadata, and recovery state. Temporary roots are owned by the SDK and can be +removed after close. + +That root model is deliberate: app developers get PostgreSQL's recovery and SQL +behavior, while the SDK owns the app-facing safety rails around paths, locking, +backup, restore, and selected runtime assets. + +## Lifecycle Contract + +Every SDK exposes the same lifecycle phases with ecosystem-native names: + +| Phase | What the SDK owns | +| --- | --- | +| Open | Create or attach to the root, validate locks, and materialize selected runtime resources | +| Query | Route work through the selected engine and preserve mode-specific concurrency rules | +| Background | Checkpoint, cancel, or defer bounded work according to platform lifecycle hooks | +| Close | Reject new work, wait for active work, and detach cleanly from the selected runtime | +| Backup/restore | Validate archives and roots before moving PostgreSQL data | + +Mobile SDKs connect these phases to app background and foreground transitions. +Desktop SDKs add broker and server modes where a helper process or local server +is the better runtime shape. + +## Extension Selection + +Extensions are selected by exact SQL extension name before packaging or opening +the database. App artifacts include the selected extension and its declared +dependencies. An app selecting `vector` ships `vector` and only the dependency +files declared for `vector`. + +`CREATE EXTENSION` succeeds only when the selected runtime resources include +that extension for the target platform. See the +[extension reference](/docs/reference/extensions) for the distribution contract. + +## What is different from SQLite? + +Oliphaunt stores live data as a PostgreSQL root directory with PostgreSQL +recovery behavior. That is a larger runtime model than a single-file database, +but it enables PostgreSQL SQL, types, wire-protocol behavior, and extensions +inside apps that need those features. + +Use SQLite when a small single-file database is the better product fit. Use +Oliphaunt when PostgreSQL compatibility, extensions, or server-compatible +workflows are worth the extra runtime footprint. diff --git a/src/docs/content/learn/index.mdx b/src/docs/content/learn/index.mdx new file mode 100644 index 00000000..b24fb418 --- /dev/null +++ b/src/docs/content/learn/index.mdx @@ -0,0 +1,32 @@ +--- +title: Learn +description: Understand Oliphaunt's runtime model before choosing production settings. +--- + +# Learn + +Use these pages after the first query works and you need to make production +choices: where data lives, which runtime boundary fits, how mobile lifecycle +behaves, and how to package the app without extra extension files. + + + +## Suggested Paths + +- **Mobile apps:** Read [Embedded PostgreSQL](/docs/learn/embedded-postgres), + then [Mobile Stability](/docs/learn/mobile-stability). React Native developers + then read the [React Native architecture page](/docs/sdk/react-native/architecture) + before wiring background hooks or streaming large protocol responses. +- **Desktop apps:** Read [Native Runtime](/docs/learn/native-runtime) first. + Tauri developers then read [Tauri Usage](/docs/learn/tauri) and keep database + ownership in Rust state. +- **SQLite migration:** Read [Moving From SQLite](/docs/learn/sqlite-upgrade), + then use [Extensions](/docs/reference/extensions) and + [Performance](/docs/reference/performance) while sizing the app artifact and + benchmark plan. + +## What Learn Covers + +Learn pages explain product behavior in app terms. Use the SDK page when you +need install steps and code. Use Reference when you need a matrix, catalog, or +version lookup. diff --git a/src/docs/content/learn/mobile-stability.mdx b/src/docs/content/learn/mobile-stability.mdx new file mode 100644 index 00000000..a36b524c --- /dev/null +++ b/src/docs/content/learn/mobile-stability.mdx @@ -0,0 +1,72 @@ +--- +title: Mobile Stability +description: Understand mobile direct mode, app backgrounding, relaunch, and crash consistency on iOS, Android, and React Native. +--- + +# Mobile Stability + +Mobile apps use a different runtime model from desktop apps. On iOS and +Android, Oliphaunt mobile SDKs start with native direct mode: one embedded +PostgreSQL backend lives inside the app process, and SDK calls are serialized +through the platform SDK. + + + +## What developers can rely on + +Direct mode is built for crash consistency: + +- database changes go through PostgreSQL storage and WAL; +- app relaunch reopens the same root and lets PostgreSQL recover; +- SDK calls are serialized so concurrent app tasks share one physical database + session safely; +- React Native delegates to Swift on Apple platforms and Kotlin on Android. + +Direct mode shares the app process. If the process exits, the next launch +reopens the same root and PostgreSQL recovery brings storage back to a +consistent state. Broker and server runtimes add a process boundary on targets +that advertise those modes. + +The direct-mode contract is: + +- one resident backend per app process; +- one physical session; +- serialized requests; +- same-root logical reopen only; +- app-process ownership; +- lifecycle hooks such as `prepareForBackground` and `resumeFromBackground` for + app foreground/background transitions. + +## Close and reopen + +On mobile direct mode, `close()` is a logical detach from the SDK handle. It +cleans up session state and allows the same database root to be reopened in the +same app process. A process owns one resident direct-mode root at a time. + +Use one app-owned persistent root for user data. Temporary roots are useful for +tests and short-lived work. Production app data lives in an app-controlled +directory with normal platform backup and data-protection choices. + +## Background and foreground + +Mobile operating systems can suspend apps while work is still queued. SDK +lifecycle hooks let apps prepare for backgrounding, cancel or checkpoint bounded +work, and resume cleanly when foregrounded. + +React Native apps get the same behavior through the platform SDKs. Bulk protocol +bytes use the New Architecture JSI path; lifecycle and configuration stay on the +typed native module surface. + +## Choosing the mode + +| Need | Mobile direct mode | Broker/server mode | +| --- | --- | --- | +| Lowest call latency | Best fit | Adds IPC or server overhead | +| One app-owned database root | Best fit | Works where supported | +| Independent PostgreSQL clients | Use server-capable runtime | Server mode | +| App survives database-process crash | App-process ownership | Broker/server where supported | +| React Native integration | Delegates to Swift/Kotlin | Delegates to platform support | + +Broker and server modes are platform-advertised capabilities. Android can use a +separate service process when that runtime is available. Apple platforms expose +only runtime modes that fit the app and extension model for that target. diff --git a/src/docs/content/learn/native-runtime.mdx b/src/docs/content/learn/native-runtime.mdx new file mode 100644 index 00000000..c15f8615 --- /dev/null +++ b/src/docs/content/learn/native-runtime.mdx @@ -0,0 +1,210 @@ +--- +title: Native Runtime +description: Direct, broker, and server runtime behavior for native Oliphaunt SDKs. +--- + +# Native Runtime + +This guide describes the native runtime family used by `liboliphaunt` and the +native SDKs. WASM runtime behavior is documented separately in +[`WASM runtime`](/docs/sdk/wasm/runtime). + +## Choose a mode + +Use the mode names as product boundaries: + +- choose `NativeDirect` for the lowest-latency embedded call path when one + application-owned session is the right model; +- choose `NativeBroker` when a desktop app wants the database runtime outside + the UI process or needs to manage several roots deliberately; +- choose `NativeServer` when existing PostgreSQL clients need independent + sessions, connection strings, pools, `psql`, `pg_dump`, or ORM compatibility. + +These are different runtime contracts. Direct mode is the embedded session, +broker mode adds a helper-process boundary, and server mode is the path for +independent PostgreSQL client concurrency. + + + +`NativeDirect` is the lowest-latency embedded mode. It loads `liboliphaunt` in +the host process and owns one resident PostgreSQL backend for the process +lifetime. + +Use it when the Rust SDK owns the database calls and the application wants one +fast embedded PostgreSQL session: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_direct() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .open() + .await?; + +let rows = db.query("SELECT 1::text AS value").await?; +assert_eq!(rows.get_text(0, "value")?, Some("1")); + +db.close().await?; +# Ok(()) +# } +``` + +`NativeBroker` runs the same direct engine in a helper process. It is the robust +desktop/app mode for process isolation and multiple roots managed by one Rust +SDK runtime. Each broker-owned root still has one serialized physical +PostgreSQL backend session. + +Use it when process isolation and multi-root ownership matter more than absolute +minimum call overhead: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_broker() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_broker() + .broker_max_roots(4) + .open() + .await?; + +db.execute("CREATE TABLE IF NOT EXISTS events(id bigint PRIMARY KEY)").await?; +db.close().await?; +# Ok(()) +# } +``` + +`NativeServer` starts a real local PostgreSQL-compatible server process. It is +the only SDK mode for independent client sessions, connection pools, `psql`, +`pg_dump`, ORMs, and libraries that expect a PostgreSQL connection string: + +```rust,no_run +use oliphaunt::Oliphaunt; + +# async fn open_server() -> oliphaunt::Result { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_server() + .max_client_sessions(8) + .open() + .await?; + +Ok(db.connection_string().expect("server mode exposes a URL").to_owned()) +# } +``` + +## Runtime Semantics + +The three modes are intentionally different. Direct and broker mode expose one +serialized SDK-owned database session; server mode is the runtime for +independent PostgreSQL clients. + +| Mode | Process model | Session model | Root model | Reopen/crash behavior | +| --- | --- | --- | --- | --- | +| `NativeDirect` | in-process | one serialized physical session | one resident root per process | same-root logical reopen; WAL recovery after process relaunch | +| `NativeBroker` | helper process per active root | one serialized physical session per root | multiple roots bounded by `broker_max_roots` | helper crash can be restarted; app process remains alive | +| `NativeServer` | PostgreSQL server process | independent PostgreSQL client sessions | one server root per opened handle | use normal server restart/recovery flows | + +`Oliphaunt` is cloneable as an SDK handle. Clones share the same owner executor, +FIFO queue, session pin, cancellation handle, and close state. Use server mode +when the application needs a connection pool or independent client sessions. +Direct and broker mode reject `max_client_sessions` values other than `1`. + +Transactions and explicit session pins reserve the single SDK-owned physical +session. While a pin is active, the owner executor keeps unrelated work outside +that transaction-sensitive session and returns a session-busy error for calls +that require a different database state. + +## Direct Lifecycle + +Direct mode is process-resident: + +- one resident backend per process; +- one physical session; +- serialized requests through the SDK owner executor; +- one root per process after the resident backend exists; +- `close()` detaches the SDK handle from the resident backend; +- reopening is same-root only inside the same process; +- native PostgreSQL crashes terminate the host process. + +Direct mode's reliability contract is crash-consistent storage. If the host +process exits, the next launch reopens the same root and PostgreSQL performs WAL +recovery. Applications that need the app process to survive database-process +death use broker or server mode where the target platform supports them. + +## Storage + +Native live storage is a PostgreSQL root directory. A root contains PGDATA, +Oliphaunt metadata, lock metadata, extension metadata, and recovery state. + +Persistent roots use exclusive locking in direct mode. Broker and server modes +own their roots through the helper/server process. A second unsafe owner fails +instead of sharing a data directory. + +Use SDK backup/restore APIs for ergonomic export/import: + +- direct and broker support same-version physical archives; +- server supports same-version physical archives and SQL dumps through packaged + PostgreSQL tooling; +- logical dumps are the portable cross-version upgrade format. + +## Startup Configuration + +`OliphauntBuilder::runtime_footprint(...)` selects the startup footprint before +PostgreSQL starts: + +- `RuntimeFootprintProfile::Throughput`: throughput defaults; +- `RuntimeFootprintProfile::BalancedMobile`: lower slot counts, smaller shared + buffers/WAL footprint, and PG18 sync I/O for resident mobile apps; +- `RuntimeFootprintProfile::SmallMobile`: the smallest supported resident + profile for memory-pressure experiments. + +`OliphauntBuilder::startup_guc(name, value)` and `startup_gucs(...)` append +validated PostgreSQL `-c name=value` overrides after durability and footprint +profiles. Later overrides win, matching PostgreSQL startup behavior. Server mode +then appends its configured `max_connections` from `max_client_sessions(...)` +because independent session count is the server-mode contract. + +## Extensions + +Extensions are opt-in. Select exact PostgreSQL extension names before opening: + +```rust,no_run +use oliphaunt::{Extension, Oliphaunt}; + +# async fn open_with_vector() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + +db.execute("CREATE EXTENSION IF NOT EXISTS vector").await?; +db.close().await?; +# Ok(()) +# } +``` + +`CREATE EXTENSION` succeeds only when the selected runtime resources contain the +extension assets and, on mobile, when the required static registry entries are +present. Desktop runtimes advertise dynamic loading separately through +capabilities; the portable path is selected runtime resources first. + +## Capabilities + +Read capabilities before enabling mode-specific features: + +- `session_concurrency` distinguishes serialized SDK sessions from independent + server sessions; +- `multi_root` is broker-only today; +- `same_root_logical_reopen`, `root_switchable`, and `crash_restartable` + describe lifecycle semantics explicitly; +- `backup_formats` and `restore_formats` gate backup/restore UI before work is + queued. + +Swift, Kotlin, and React Native expose the same product concepts with +platform-native naming. Platform modes outside advertised capabilities return +explicit errors so app code can choose another mode or hide the option in UI. diff --git a/src/docs/content/learn/sqlite-upgrade.mdx b/src/docs/content/learn/sqlite-upgrade.mdx new file mode 100644 index 00000000..98b76f53 --- /dev/null +++ b/src/docs/content/learn/sqlite-upgrade.mdx @@ -0,0 +1,69 @@ +--- +sidebar_position: 1 +title: Moving From SQLite +description: Map SQLite storage, SQL, backup, and extension assumptions to embedded PostgreSQL. +--- + +# Moving From SQLite + +Oliphaunt is embedded PostgreSQL, so migration from SQLite starts by mapping a +single-file database model to PostgreSQL roots, WAL, extensions, and PostgreSQL +SQL semantics. + +Use this guide when an app already uses SQLite and you are evaluating whether a +PostgreSQL-compatible embedded runtime is worth the extra footprint. + + + +## Concept Map + +| SQLite concept | Oliphaunt concept | +| --- | --- | +| One database file | One PostgreSQL root directory | +| Pragmas | PostgreSQL settings and SDK durability profiles | +| SQLite transaction | PostgreSQL transaction | +| SQLite extension loading | Exact PostgreSQL extension selection before open | +| File copy backup | SDK backup/export API | +| Multiple library handles | Mode-specific Oliphaunt handles and session semantics | + +## Schema And SQL Differences + +PostgreSQL is stricter and richer than SQLite: + +- column types and casts matter more; +- `SERIAL`, `IDENTITY`, sequences, arrays, JSONB, and enums replace many + SQLite-specific conventions; +- PostgreSQL query parameters are `$1`, `$2`, and so on; +- constraints, indexes, and generated columns follow PostgreSQL syntax; +- extension-backed types and operators require exact extension selection. + +Start with a small schema slice. Port table definitions, then migrate one query +path at a time so type and constraint differences are visible early. + +## Storage And Backup + +SQLite apps often back up by copying one file. Oliphaunt live storage is a +PostgreSQL root directory, so data movement goes through SDK backup and restore +APIs. That keeps locks, WAL, selected extensions, archive format, and restore +target checks together. + +For mobile apps, keep the root in app-private storage and use the platform's +normal user-data protection choices. For desktop apps, choose direct, broker, or +server mode based on the concurrency and process-isolation model your app needs. + +## Migration Path + +1. Choose the SDK for the app target. +2. Open a temporary Oliphaunt root and port the schema. +3. Port read paths before write-heavy sync/import paths. +4. Add selected extensions explicitly. +5. Add backup/restore and package-size checks before shipping. +6. Compare app-start, first-query, memory, and package-size numbers against the + SQLite baseline. + +## When SQLite Is Still The Better Fit + +Use Oliphaunt when PostgreSQL compatibility, richer SQL, extensions, and +server-compatible workflows are worth the larger runtime and directory storage +model. Use SQLite when a tiny single-file dependency and SQLite-specific +semantics are the better fit. diff --git a/src/docs/content/learn/tauri.mdx b/src/docs/content/learn/tauri.mdx new file mode 100644 index 00000000..76893960 --- /dev/null +++ b/src/docs/content/learn/tauri.mdx @@ -0,0 +1,111 @@ +--- +title: Tauri Usage +description: Use the Rust SDK from Tauri state and expose app-specific database commands to the webview. +--- + +# Tauri Usage + +Use the Rust SDK from Tauri state. `oliphaunt` is the native SDK for Tauri and +Rust desktop apps; it owns direct embedded mode, broker mode, and server mode +over native PostgreSQL. + +WASM is a separate runtime family. Native Tauri apps start with the Rust SDK. + + + +## App Shape + +Keep the database handle in Rust state and expose narrow Tauri commands to the +webview. The webview calls app-specific commands such as `add_item` or +`search_items`; Rust owns the root directory, lock, runtime handle, lifecycle, +and backup APIs. + +| Need | Recommended mode | +| --- | --- | +| One embedded app database with lowest overhead | `NativeDirect` | +| Multiple roots or helper-process ownership | `NativeBroker` | +| SQLx pools, ORMs, `psql`, or `pg_dump` | `NativeServer` | + +## Direct Rust State + +Use `Oliphaunt` with `NativeDirect` when your Tauri commands own the database +calls and you want the lowest-latency embedded path: + +```rust,no_run +use oliphaunt::Oliphaunt; +use tauri::State; + +struct Db(Oliphaunt); + +#[tauri::command] +async fn add_item(db: State<'_, Db>, value: String) -> Result<(), String> { + db.0 + .query_params("INSERT INTO items(value) VALUES ($1)", [value]) + .await + .map_err(|err| err.to_string())?; + Ok(()) +} +``` + +Open the database under your app data directory during setup: + +```rust,no_run +use oliphaunt::Oliphaunt; + +async fn open_app_database(app_data_dir: std::path::PathBuf) -> oliphaunt::Result { + Oliphaunt::builder() + .path(app_data_dir.join("postgres")) + .native_direct() + .open() + .await +} +``` + +Store the resulting `Oliphaunt` handle with `tauri::State`. Cloned SDK handles +share the same executor and session semantics; server mode is the path for +independent PostgreSQL client sessions. + +## Existing Postgres Clients + +Use `NativeServer` when another Rust library expects a PostgreSQL URL, real +independent sessions, SQLx pools, `psql`, or `pg_dump`: + +```rust,no_run +use oliphaunt::Oliphaunt; + +async fn open_server_mode() -> oliphaunt::Result { + let db = Oliphaunt::builder() + .path("./.liboliphaunt") + .native_server() + .max_client_sessions(8) + .open() + .await?; + + Ok(db.connection_string().expect("server mode has a URL").to_owned()) +} +``` + +Use `NativeBroker` for desktop apps that want helper-process ownership or +multiple database roots managed by the Rust SDK. + +## Extensions And Assets + +Select exact SQL extension names in Rust configuration before opening the root. +Package only the extension artifacts your Tauri app uses, then verify the app +artifact before release. An app that selects `vector` ships `vector` and its +declared dependencies. + +## Backup And Restore + +Use the Rust SDK backup and restore APIs for app export/import flows. Keep live +PostgreSQL roots behind the Rust SDK. The SDK validates archive format, selected +extensions, locks, and restore targets before moving data. + +## Operational Guidance + +- Use `NativeDirect` for one embedded PostgreSQL session with minimal overhead. +- Use `NativeBroker` when helper-process ownership matters more than direct + call overhead. +- Use `NativeServer` for real concurrent PostgreSQL client sessions and pools. +- React Native apps use the React Native SDK, which delegates to Swift on + iOS/macOS and Kotlin on Android. diff --git a/src/docs/content/reference/capabilities.mdx b/src/docs/content/reference/capabilities.mdx new file mode 100644 index 00000000..136366b6 --- /dev/null +++ b/src/docs/content/reference/capabilities.mdx @@ -0,0 +1,74 @@ +--- +title: Capability Matrix +description: Compare SDK mode support, runtime boundaries, protocol features, extensions, backup, restore, and client compatibility. +--- + +# Capability Matrix + +Use this page to choose the right SDK and runtime mode. Oliphaunt SDKs are +peers: Rust is for Tauri and native Rust apps, Swift is for iOS and macOS, +Kotlin is for Android, React Native uses the Swift and Kotlin SDKs underneath, +TypeScript is for desktop JavaScript runtimes, and WASM is the WebAssembly +runtime family. + +Capability checks return explicit errors when a feature is absent. Use them to +show only the modes, controls, and workflows the installed SDK advertises for +the target. + + + +## SDKs + +| SDK | Primary use | Runtime owner | Notes | +| --- | --- | --- | --- | +| C ABI | Binding authors | `liboliphaunt` | Low-level raw protocol and lifecycle boundary | +| Rust | Tauri and native Rust apps | Rust SDK | Full direct, broker, and server model where supported | +| Swift | iOS and macOS apps | Swift SDK | Async Apple API over the native runtime | +| Kotlin | Android apps | Kotlin SDK | Coroutine Android API over the native runtime | +| React Native | Expo and New Architecture apps | Swift/Kotlin | TypeScript API, config plugin, TurboModule/JSI transport | +| TypeScript | Node.js, Bun, Deno | TypeScript SDK | Desktop JavaScript API over native helpers; direct Tauri JavaScript integration is planned | +| WASM | WebAssembly/WASIX apps | WASM SDK | Separate runtime family with WASM-owned assets | + +## Runtime Modes + +| Mode | Best for | Session model | SDK availability | +| --- | --- | --- | --- | +| Native direct | Lowest-latency embedded database | One serialized physical session | Rust, Swift, Kotlin, React Native through Swift/Kotlin, C ABI | +| Native broker | Desktop apps that need process isolation or multiple roots | Helper process owns roots and sessions | Rust first; TypeScript via helper integration where supported | +| Native server | `psql`, `pg_dump`, ORMs, pools, independent clients | PostgreSQL-compatible server process | Rust first; other SDKs only when they advertise support | +| WASM | WebAssembly/WASIX deployments | WASM runtime session model | WASM SDK | + +Mobile direct mode has one resident backend per app process and one physical +session. It is same-root logically reopenable inside that process. Use server +mode only where the SDK reports true server support. Mobile direct mode is not +a broker/server alias; it is not a crash-isolated server and does not provide +independent concurrent PostgreSQL client sessions. + +## Feature Support + +| Feature | Rust | Swift | Kotlin | React Native | TypeScript | WASM | +| --- | --- | --- | --- | --- | --- | --- | +| Open persistent root | Yes | Yes | Yes | Via Swift/Kotlin | Yes | Yes | +| Temporary root | Yes | Yes | Yes | Via Swift/Kotlin | Yes | Yes | +| Simple SQL helpers | Yes | Yes | Yes | Yes | Yes | Yes | +| Raw protocol bytes | Yes | Yes | Yes | Yes | Yes | Yes | +| Chunked protocol streaming | Yes | Yes where reported | Yes where reported | JSI ArrayBuffer stream where reported | Yes where reported | Yes where reported | +| Transactions | Yes | Yes | Yes | Yes | Yes | Yes | +| Exact extension selection | Yes | Yes | Yes | Via Swift/Kotlin | Yes | Yes | +| Physical backup/restore | Yes | Yes | Yes | Via Swift/Kotlin | Yes | Yes | +| PostgreSQL-compatible server | Yes | Where advertised | Where advertised | Where advertised through Swift/Kotlin | Via supported helper path | WASM-specific | +| Independent concurrent clients | Server mode only | Server mode only where available | Server mode only where available | Server mode only where available | Server mode only where available | Runtime-specific | + +## Choosing A Mode + +Choose native direct when app code owns one embedded database and latency is the +priority. + +Choose native broker when a desktop app needs stronger isolation, multiple +database roots, crash recovery, or a long-lived helper process. + +Choose native server when compatibility with existing PostgreSQL clients matters +more than embedding latency. + +Choose WASM when the deployment target is WASIX/WebAssembly. Choose a +native SDK when the app ships a `liboliphaunt` runtime. diff --git a/src/docs/content/reference/extensions.mdx b/src/docs/content/reference/extensions.mdx new file mode 100644 index 00000000..24d72a70 --- /dev/null +++ b/src/docs/content/reference/extensions.mdx @@ -0,0 +1,87 @@ +--- +title: Extensions +description: Select exact PostgreSQL extension names and verify the artifacts that enter each app package. +--- + +# Extensions + +Oliphaunt uses exact, opt-in PostgreSQL extension selection. App developers +choose the SQL extension names their app needs, and release artifacts contain +only those selected extensions plus mandatory dependencies declared by the +extension metadata. + +There are no extension packs, aliases, grouped selectors, or implicit expansion. +Selecting `vector` means selecting the PostgreSQL extension named `vector`. + + + + + +## How Selection Works + +Select extensions before opening the database: + +```rust +use oliphaunt::{Extension, Oliphaunt}; + +# async fn demo() -> oliphaunt::Result<()> { +let db = Oliphaunt::builder() + .temporary() + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + +db.execute("CREATE EXTENSION vector").await?; +# Ok(()) +# } +``` + +`CREATE EXTENSION` succeeds when the selected runtime resources contain that +extension for the target platform. The SDK loads only the selected extension +artifacts and their declared dependencies. + +## Platform Behavior + +| Platform | Expected behavior | +| --- | --- | +| Rust/Tauri desktop | SDK resolves selected runtime extension artifacts for the target | +| iOS/macOS Swift | App bundle includes selected extension artifacts and dependencies only | +| Android Kotlin | Android package includes selected extension artifacts and dependencies only | +| React Native | Config plugin delegates selection to Swift/Kotlin packaging | +| TypeScript desktop | SDK resolves selected runtime artifacts or helper-process resources | +| WASM | WASM assets include selected extension artifacts built for the WASIX runtime | + +## Dependencies + +Some PostgreSQL extensions depend on other extensions or runtime files. Those +dependencies are explicit metadata. If `earthdistance` declares `cube` as a +dependency, selecting `earthdistance` may include `cube`; selecting `vector` +includes `vector` and its declared dependencies only. + +## External Extensions + +External extensions are distributed as exact extension artifacts or indexes. +Consumer apps select them by SQL extension name. If a developer provides a +verified custom artifact for `acme_ext`, the app selects `acme_ext`. + +Each promoted exact extension has its own product tag, changelog, and package +version. PostgreSQL contrib extension packages are runtime-bound and move with +the compatible Oliphaunt runtime group. External extension packages own +independent packaging SemVer; their immutable upstream version/commit and +compatible Oliphaunt runtime versions are separate metadata. Do not assume an +external package version matches either its upstream project version or the +runtime version. + +## Verifying App Artifacts + +Before release, app tooling reports: + +- selected SQL extension names; +- included extension files; +- mandatory dependencies; +- package-size contribution per extension; +- target platform and architecture. + +That report lets developers confirm that an app using only `vector` ships +`vector` and its declared dependencies, without unrelated extension artifacts. diff --git a/src/docs/content/reference/index.mdx b/src/docs/content/reference/index.mdx new file mode 100644 index 00000000..b228c9bf --- /dev/null +++ b/src/docs/content/reference/index.mdx @@ -0,0 +1,51 @@ +--- +title: Reference +description: Look up SDK support, runtime capabilities, extensions, releases, performance, and API surfaces. +--- + +# Reference + +Reference pages answer product lookup questions. Use them when you already know +the app target and need exact support, extension, package, release, or API +details. + + + +## How To Use Reference + + + + + ### Start with the app target + + Use [SDKs And Platforms](/docs/reference/sdk-products) to choose the package + for the app users install: Rust, Swift, Kotlin, React Native, TypeScript, + WASM, or the C ABI. + + + + + ### Check runtime capability + + Use [Capability Matrix](/docs/reference/capabilities) before enabling UI for + broker, server, raw protocol streaming, backup, restore, or independent client + sessions. + + + + + ### Select artifacts deliberately + + Use [Extensions](/docs/reference/extensions) and the generated catalog to + package only the SQL extensions your app selects. + + + + + ### Verify release fit + + Use [Performance](/docs/reference/performance), the generated version matrix, + and SDK API maps when preparing a release candidate. + + + diff --git a/src/docs/content/reference/performance.mdx b/src/docs/content/reference/performance.mdx new file mode 100644 index 00000000..58bd9716 --- /dev/null +++ b/src/docs/content/reference/performance.mdx @@ -0,0 +1,93 @@ +--- +title: Performance +description: Understand the latency, throughput, memory, package-size, and SQLite comparison measurements Oliphaunt publishes. +--- + +# Performance + +Oliphaunt is designed for app-embedded PostgreSQL. Performance work focuses on +the operations developers feel in production apps: open time, simple-query +latency, transaction throughput, bulk load speed, large result streaming, +backup/restore time, memory footprint, and packaged app size. + + + +## What to measure + +Use performance numbers in context: + +| Area | Why it matters | +| --- | --- | +| Cold open | App startup and first database access | +| Warm open | Reopening a database during normal app use | +| Query latency | UI responsiveness for small reads and writes | +| Transaction throughput | Sync, import, and local-first write workloads | +| Bulk load | Initial dataset import and cache hydration | +| Large result streaming | Reports, sync scans, and export flows | +| Backup and restore | User data migration and support workflows | +| RSS and package size | Mobile distribution and desktop app footprint | + +Native direct mode is the lowest-latency embedded path. Broker mode adds +an IPC boundary in exchange for process isolation and multi-root management. +Server mode is the right choice when an app needs real PostgreSQL client +connections, tools, pools, or ORMs. + +## Compare modes honestly + +Compare each mode against the problem it solves: + +- Use direct mode when one embedded database session is enough and latency is + the primary concern. +- Use broker mode when crash isolation, recovery, upgrades, or multiple roots + are more important than the last microseconds of latency. +- Use server mode when independent PostgreSQL clients are part of the product. + +For mobile apps, include startup time, memory footprint, selected extensions, +and app artifact size in the same report. The useful result is the one that +keeps latency, throughput, memory, and selected-extension packaging visible +together. + +## SQLite comparison + +SQLite is the baseline developers already trust for embedded storage. Oliphaunt +is measured against SQLite for: + +- first query after app launch; +- single-row lookup; +- batched insert/update; +- aggregate queries over realistic local datasets; +- transaction cost; +- package size and memory footprint. + +The comparison explains the workload and schema. PostgreSQL features such +as extensions, SQL compatibility, data types, and server-mode interoperability +are part of the value proposition alongside low latency, high throughput, and a +bounded footprint in common app workloads. + +## Release Measurements + +Published performance results include: + +- hardware and operating system; +- SDK and runtime mode; +- PostgreSQL and Oliphaunt versions; +- selected extensions; +- repeat count and percentile method; +- memory/RSS collection method; +- package-size method; +- links to reproducible benchmark workloads. + +Reports must show p50/p90/p95/p99 latency, suite totals, throughput, RSS, +CPU time, package size, and benchmark provenance. + +Native Direct Regression Diagnostics are included when direct mode misses a +gate, so the report links the failing suite back to repeatable diagnostic +commands rather than only showing a red/green result. + +PostgreSQL configuration sweeps must stay inside valid server settings. For +example, `min_wal_size=8MB` is the practical lower bound because values below a +WAL segment are invalid PostgreSQL experiments, not useful mobile footprint +tuning data. + +Public docs present stable methodology and release results. Raw run logs and +benchmark debugging notes stay out of app-developer setup guides. diff --git a/src/docs/content/reference/releases.mdx b/src/docs/content/reference/releases.mdx new file mode 100644 index 00000000..cff4c0e7 --- /dev/null +++ b/src/docs/content/reference/releases.mdx @@ -0,0 +1,104 @@ +--- +sidebar_position: 1 +title: Releases +description: Match SDK versions, runtime artifacts, selected extensions, release notes, and docs versions. +--- + +# Releases + +Oliphaunt products are released independently. Start from the package your app +installs, then check the runtime artifacts and selected extensions that package +expects. + + + +## First Release Boundary + +Before the first public release, tracked product versions remain `0.0.0`. That +is an unreleased source sentinel, not a registry version. The generated release +PR advances new products to `0.1.0`, except Swift, which starts at `0.6.0` +because legacy unscoped SwiftPM tags already occupy `0.1.0` through `0.5.1`. + +An install command is a package contract, not proof that a registry identity +exists. Check the product's promoted tag/release and registry page before using +it in an application. + +## Version Relationships + +| Relationship | Products | Rule | +| --- | --- | --- | +| Independently versioned | SDKs and helper runtimes | Each product owns its SemVer and compatibility fields; a runtime change can select dependent product releases without giving them one shared version | +| Runtime-bound linked group | Native/WASIX runtimes and the PostgreSQL 18 contrib bundle product | The configured linked group moves together because the bundle's exact member bytes are built for the runtime contract | +| Upstream-bound | External exact-extension products | Packaging SemVer is independent; immutable upstream version/commit and compatible runtime versions are recorded separately | +| Documentation | Public documentation site | Guides and references can change without a package release | + +React Native spans two native platform SDKs. A JavaScript package release may +need matching Swift and Kotlin artifacts even when the TypeScript API shape is +unchanged. + +## Target Availability + +The first native desktop carriers cover Linux x64/arm64 GNU, macOS arm64, and +Windows x64 MSVC. Android carriers cover `arm64-v8a` and `x86_64`. Apple uses +the declared iOS XCFramework plus the macOS arm64 runtime carrier. WASIX ships a +portable carrier and AOT carriers for the supported desktop hosts. + +macOS x64, Windows ARM64, Linux musl, Android 32-bit, and undeclared Apple +architectures are not first-release targets. An extension can support a smaller +set: its own explicit target manifest and published carrier metadata are +authoritative. + +### Consumer Compatibility Floors + +The release gate inspects staged binaries against the following contract. These +are consumer compatibility floors, not merely the operating systems used to +build the artifacts. + + +| Published carrier | Enforced consumer compatibility contract | +| --- | --- | +| Linux x64/arm64 GNU | Required symbol versions do not exceed `GLIBC_2.38` or `GLIBCXX_3.4.30`. | +| Direct macOS arm64 runtime | Minimum deployment target is macOS 11.0. | +| Android `arm64-v8a` and `x86_64` | Minimum Android API level is 24; Android binaries must not require GLIBC/GLIBCXX symbol families. | +| Apple XCFramework | Contains macOS arm64, iOS device arm64, and iOS Simulator arm64 slices; minimum targets are macOS 14.0, iOS 17.0, and iOS Simulator 17.0. | +| Windows x64 MSVC | Requires the x64 PE/COFF contract and the declared app-local Visual C++ runtime profile; Windows ARM64 is not published. | + + +The table is synchronized with the authoritative binary compatibility policy +used by release staging. A carrier fails qualification when its inspected ELF, +Mach-O, Android, or PE metadata exceeds this contract. + +### Mobile Package And Runtime Coverage + +Package availability and installed-app execution are separate support claims: + +| Surface | Built and binary-inspected release candidates | Required installed-app execution | +| --- | --- | --- | +| Android | Both `arm64-v8a` and `x86_64` runtime and exact-extension carriers, plus release APKs for both ABIs | The `x86_64` APK runs the full installed-app workload on the API 35 emulator. Android arm64 is not executed on a physical device in the required first-release gate. | +| Apple | The XCFramework's macOS arm64, iOS device arm64, and iOS Simulator arm64 runtime and exact-extension slices | The iOS Simulator arm64 app runs the full installed-app workload. The iOS device arm64 slice is built and inspected, but is not installed or executed on a physical iOS device in the required first-release gate. | + +This boundary does not mean that Android arm64 or iOS device packages are +absent. It distinguishes artifact construction and binary compatibility proof +from hardware-specific execution coverage, so release notes state exactly which +installed-app workloads are required for each platform. + +## What A Release Tells You + +Release notes answer these questions: + +- PostgreSQL baseline used by the runtime; +- SDK packages published in the release; +- platforms and architectures with artifacts; +- exact SQL extensions available for each target; +- direct, broker, server, raw protocol, streaming, backup, and restore support; +- migration or rebuild steps for app developers. + +## Docs Versioning + +The docs site has a `latest` channel for the current product shape. Package +versions, compatibility notes, and release notes tell developers which docs +match the SDK installed in an app. Versioned docs remain available by released +product version when a package line needs stable historical documentation. + +Documentation changes can update the docs site without changing Rust, Swift, +Kotlin, React Native, TypeScript, C ABI, or WASM package versions. diff --git a/src/docs/content/reference/sdk-products.mdx b/src/docs/content/reference/sdk-products.mdx new file mode 100644 index 00000000..100a7b59 --- /dev/null +++ b/src/docs/content/reference/sdk-products.mdx @@ -0,0 +1,58 @@ +--- +title: SDKs And Platforms +description: Compare Oliphaunt SDK package names, target apps, runtime ownership, and platform responsibilities. +--- + +Oliphaunt ships as peer SDKs for the platforms where developers build apps. +Each SDK uses the conventions of its ecosystem while keeping the same database +concepts: open a root, run SQL, manage lifecycle, select exact extensions, and +back up or restore data. + +Rust is the SDK for Tauri and Rust desktop apps. +React Native is the TypeScript/TurboModule SDK over the Swift and Kotlin SDKs. +TypeScript is the SDK for Node.js, Bun, and Deno. Tauri apps currently keep +Oliphaunt in Rust state behind narrow app-owned commands; a direct JavaScript adapter is planned. + +## Choose an SDK + + + +React Native delegates database execution to the platform SDKs: Apple calls flow +through the Swift SDK, and Android calls flow through the Kotlin SDK. That keeps +resource packaging, lifecycle, and native crash behavior predictable for mobile +apps. + +TypeScript broker mode uses a published broker helper, so JavaScript desktop apps +resolve the helper through package configuration. Native Node.js, Bun, and Deno +installs use the npm package; the JSR distribution is deliberately limited to +portable protocol/query helpers. + +## Shared Concepts + +The SDKs intentionally use matching names for the concepts developers need to +move between platforms: + +- `nativeDirect` for the lowest-latency embedded session. +- `nativeBroker` for helper-process isolation and multi-root desktop apps. +- `nativeServer` when real PostgreSQL clients, pools, or tools need independent + sessions. +- `capabilities()` to check what a selected runtime can do. +- `backup` and `restore` for app-friendly data movement. +- structured PostgreSQL errors with SQLSTATE and backend fields where available. + +SDK capability reports describe which modes are available on the selected +platform. Mobile SDKs expose direct mode first; broker and server support appear +when the platform SDK advertises those runtimes. + + + +## Extensions + + + +Extension selection is exact-name only: app configuration names PostgreSQL +extensions such as `vector` directly. + +That model matters for mobile and desktop distribution: app developers decide +which PostgreSQL extensions their app uses, and Oliphaunt packaging makes that +choice auditable before release. diff --git a/src/docs/content/sdk/c-abi/api-reference.md b/src/docs/content/sdk/c-abi/api-reference.md new file mode 100644 index 00000000..ffc3a09b --- /dev/null +++ b/src/docs/content/sdk/c-abi/api-reference.md @@ -0,0 +1,35 @@ +--- +title: API Reference +description: C ABI API map for native runtime initialization, protocol execution, response ownership, and lifecycle. +--- + +# API Reference + +Use the Doxygen reference for exact declarations. This page maps the C ABI by +task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Initialization | `oliphaunt_init`, `oliphaunt_init_ex`, `OliphauntConfig`, `OliphauntInitOptions` | Open a native direct backend with explicit root, durability, runtime resource, and extension settings | +| Versioning | `oliphaunt_version` | Report the runtime and PostgreSQL build identity | +| Capabilities | `oliphaunt_capabilities`, `OliphauntCapabilities` | Discover protocol, streaming, extension, backup, restore, lifecycle, and mode support | +| Raw protocol | `oliphaunt_exec_protocol` | Send PostgreSQL frontend protocol bytes and receive backend messages | +| Streaming | `oliphaunt_exec_protocol_stream`, response sink callbacks | Handle large protocol responses without forcing one contiguous response buffer | +| Simple SQL | SQL helper entry points where exposed | Run smoke and embedding checks without requiring a higher-level SDK parser | +| Response ownership | `OliphauntResponse`, `oliphaunt_free_response` | Free ABI-owned buffers exactly once | +| Errors | `oliphaunt_last_error`, structured error fields where available | Read the last SDK or PostgreSQL error for a handle | +| Data movement | backup and restore entry points where exposed | Move PostgreSQL roots through validated archives | +| Lifecycle | `oliphaunt_checkpoint`, `oliphaunt_close` | Flush, detach, and release the resident backend handle | + +Most app developers use a language SDK instead of calling the C ABI directly. +The C ABI is primarily for binding authors and applications that need the native +runtime boundary itself. + +`oliphaunt_init` retains the ABI v6 behavior used by existing bindings. Its +embedded module directory comes from a valid `OLIPHAUNT_EMBEDDED_MODULE_DIR` +host override, then the packaged release-layout fallbacks. `oliphaunt_init_ex` +accepts a separately versioned `OliphauntInitOptions`. A non-empty +`options.module_dir` is copied into the handle and is authoritative over the +process environment and release-layout discovery. A non-null options record +requires ABI version 1, an existing non-empty `module_dir`, and zero reserved +flags. Pass `NULL` options to preserve the legacy resolution contract. diff --git a/src/docs/content/sdk/c-abi/guide.mdx b/src/docs/content/sdk/c-abi/guide.mdx new file mode 100644 index 00000000..e902de06 --- /dev/null +++ b/src/docs/content/sdk/c-abi/guide.mdx @@ -0,0 +1,116 @@ +--- +title: Build A Binding +description: Build a language binding over opaque C handles, raw protocol bytes, explicit response ownership, lifecycle, extensions, and backup APIs. +--- + +# Build A Binding + +Use the C ABI when building language bindings or platform SDKs. App developers +usually choose Rust, Swift, Kotlin, React Native, TypeScript, or WASM instead. + + +Use this surface when you need opaque handles, explicit response ownership, and +the native runtime boundary. App-facing SDKs own typed queries and platform +lifecycle integration. + + + + + + + +### Install + +Consume the released headers, libraries, and runtime assets for your target. +Language bindings package those artifacts through the target ecosystem so app +developers install one SDK surface. + + + + +### Open and query + +Open a root, send raw protocol bytes, read backend messages, and close the +handle. + + + +```c +#include +#include + +OliphauntConfig config = { + .abi_version = OLIPHAUNT_ABI_VERSION, + .pgdata = "/app/data/main.oliphaunt/pgdata", + .username = "app", + .database = "app", +}; + +OliphauntHandle *db = NULL; +int rc = oliphaunt_init(&config, &db); +if (rc != 0) { + return rc; +} + +OliphauntResponse response = {0}; +const char *sql = "SELECT 1::text AS value"; +rc = oliphaunt_exec_simple_query(db, sql, strlen(sql), &response); +oliphaunt_free_response(&response); +oliphaunt_close(db); +``` + +Higher-level SDKs own SQL builders, typed parsing, async scheduling, resource +selection, and lifecycle integration. + + + + +### Configure + +Configure root, mode, selected extensions, runtime resource paths, startup +identity, durability, and owner identity through the ABI config surface. + + + + +### Choose a mode + +The ABI exposes capabilities for the selected target. Direct mode owns one +serialized embedded session. Broker and server support appear only when the +target runtime advertises those modes. + + + + +### Handle lifecycle + +Each binding runs calls through a single owner queue or equivalent serial +executor. Close rejects queued work and waits for active work according to the +SDK's platform contract. + + + + +### Select extensions + +Pass exact SQL extension names and resource paths. The ABI loads selected +extension artifacts and their declared dependencies. + + + + +### Back up and restore + +Use ABI backup and restore calls for supported physical archive formats. +Bindings validate user input before crossing the ABI boundary. + + + + + + +## Troubleshooting + +Inspect ABI error codes, `oliphaunt_last_error`/Oliphaunt error fields, missing +runtime resources, root locks, mode capability errors, and PostgreSQL SQLSTATE +data. diff --git a/src/docs/content/sdk/c-abi/index.mdx b/src/docs/content/sdk/c-abi/index.mdx new file mode 100644 index 00000000..f054eb23 --- /dev/null +++ b/src/docs/content/sdk/c-abi/index.mdx @@ -0,0 +1,88 @@ +--- +title: C ABI +description: Native runtime boundary for language bindings and direct C consumers. +--- + + + +`liboliphaunt` is the native runtime boundary for SDKs and direct C consumers. +Most app developers use a platform SDK, but binding authors use the C ABI as the +stable layer under Swift, Kotlin, React Native, TypeScript native adapters, and +other language bindings. + +Use this surface when you are writing a new SDK, integrating from a C or C++ +application, or validating the native runtime independently from a language +wrapper. + +## Install + +Consume the released headers, libraries, and runtime assets for the target you +are binding. Language SDKs package those artifacts through their own ecosystems, +so app developers usually install the Rust, Swift, Kotlin, React Native, +TypeScript, or WASM SDK. + +The public boundary is intentionally small: + +- initialize a native direct database session; +- execute raw PostgreSQL protocol bytes or simple SQL; +- stream large protocol responses; +- back up or restore supported archive formats; +- read capabilities and structured errors; +- close or detach according to the process lifecycle. + +The ABI owns handles and response buffers. Language bindings own serialization, +typed query helpers, task scheduling, and platform packaging. + +## Open And Query + +The C ABI uses opaque handles and explicit response ownership: + +```c +#include +#include + +OliphauntHandle *handle = NULL; +OliphauntConfig config = { + .abi_version = OLIPHAUNT_ABI_VERSION, + .pgdata = "./app-data/main.oliphaunt/pgdata", + .username = "app", + .database = "app", +}; + +int32_t status = oliphaunt_init(&config, &handle); +if (status == 0) { + OliphauntResponse response = {0}; + const char *sql = "SELECT 1::text AS value"; + status = oliphaunt_exec_simple_query(handle, sql, strlen(sql), &response); + oliphaunt_free_response(&response); +} +oliphaunt_close(handle); +``` + +## Runtime Shape + +The C ABI is the native direct runtime boundary. It exposes capabilities for the +target artifact so bindings can report which runtime modes, backup formats, +streaming behavior, and extensions are available. + +Direct mode owns one serialized embedded PostgreSQL session. Broker and server +support appear through higher-level SDKs or helper processes only when the +target runtime advertises those capabilities. + +## App Responsibilities + +Bindings and direct C consumers own the app-facing contract above the ABI: + +- Keep all cross-language query transport on raw PostgreSQL protocol bytes or + simple SQL helpers provided by the ABI. +- Serialize work according to the selected runtime mode. +- Select exact SQL extension names before opening the database. +- Surface `capabilities()` and structured errors in ecosystem-native types. +- Use ABI backup and restore calls instead of copying live PostgreSQL roots. + +## First Query + +Use [Build a Binding](/docs/sdk/c-abi/guide) for open, query, close, +lifecycle, extension, and backup behavior. Use the +[API reference](/docs/sdk/c-abi/api-reference) for the public handle and +function map. diff --git a/src/docs/content/sdk/index.mdx b/src/docs/content/sdk/index.mdx new file mode 100644 index 00000000..892a8dc0 --- /dev/null +++ b/src/docs/content/sdk/index.mdx @@ -0,0 +1,82 @@ +--- +title: SDKs +description: Choose the Oliphaunt SDK for Rust, Apple, Android, React Native, TypeScript, WASM, or C ABI binding. +--- + +# SDKs + +Choose the SDK by the app target and delivery path. Oliphaunt is a family of +peer SDKs over the same embedded PostgreSQL runtime model: open an app-owned +root, select the runtime mode, choose exact SQL extensions, run SQL, read +capabilities, and use the SDK's lifecycle and backup APIs. + +Each SDK page starts with the package to install, the platform build step that +matters, a first query, and the runtime behavior that affects real apps. Use +the reference pages when you need exact capability or extension tables. + + + +## How To Choose + +Use the SDK that owns the app binary or helper process that carries the +database runtime: + +- choose Swift for iOS and macOS apps; +- choose Kotlin for Android apps; +- choose React Native when JavaScript owns the app surface and Swift/Kotlin + carry the native runtime underneath; +- choose Rust for Tauri and native Rust desktop apps, and TypeScript for + Node.js, Bun, Deno, and local JavaScript helper processes; +- choose WASM when the runtime host is WASIX; +- choose C ABI only when building another language binding or integrating the + native runtime directly. + +React Native crosses two native platforms. Its public API is TypeScript, while +packaging, lifecycle, and binary transport are handled through Swift on Apple +platforms and Kotlin on Android. + +## Shared Contract + +All SDKs expose the same durable concepts where the platform supports them: + +- explicit runtime mode selection; +- app-owned persistent or temporary roots; +- exact SQL extension selection; +- `capabilities()` before relying on advanced features; +- SQL helpers plus raw protocol access for adapters; +- lifecycle, backup, restore, and structured PostgreSQL errors. + +Each SDK reports platform-specific mode support through capabilities. Mobile +SDKs expose direct mode first; broker and server support appear when a platform +SDK advertises those runtimes. Direct mode uses one physical session with +serialized work. + + + +## What Each SDK Page Answers + +Use the landing page for the package you are adding to an app. It answers the +questions that block the first useful integration: + +| Question | Why it matters | +| --- | --- | +| Which package do I install? | App users install your app; the SDK carries released runtime artifacts. | +| What build step changes my app binary? | Native runtime assets and selected extensions are packaged at build time. | +| Which runtime mode can I use? | Direct, broker, server, and WASM have different concurrency and process boundaries. | +| How do I ship only selected extensions? | App artifacts include exact SQL extension names and declared dependencies only. | +| What happens on close, backgrounding, relaunch, and backup? | Embedded database behavior has to match platform lifecycle expectations. | + +React Native covers the JavaScript boundary, config plugin, TurboModule, JSI +transport, and installed-app wiring. Swift and Kotlin cover platform lifecycle +and runtime resources. Rust and TypeScript cover their own desktop runtime +ownership. WASM remains a separate runtime family. + +## Where To Go Next + +- Use [Start](/docs/start) for the shortest first-query path. +- Use [Capability Matrix](/docs/reference/capabilities) before relying on a + mode or feature in a packaged app. +- Use [Extensions](/docs/reference/extensions) to understand exact extension + selection and app artifact behavior. +- Use [Native Runtime](/docs/learn/native-runtime) for direct, broker, and + server semantics. diff --git a/src/docs/content/sdk/kotlin/api-reference.md b/src/docs/content/sdk/kotlin/api-reference.md new file mode 100644 index 00000000..bdb7a71d --- /dev/null +++ b/src/docs/content/sdk/kotlin/api-reference.md @@ -0,0 +1,26 @@ +--- +title: API Reference +description: Kotlin and Android SDK API map for configuration, coroutine execution, lifecycle, and resources. +--- + +# API Reference + +Use the Dokka reference for exact declarations. This page maps the Kotlin SDK +surface by task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Opening | `OliphauntDatabase.open`, `OliphauntConfig` | Open a persistent or temporary root from Kotlin code | +| Android facade | `OliphauntAndroid` | Resolve Android resources, ABI assets, and app-context defaults | +| Runtime mode | `EngineMode`, `supportedModes()` | Discover modes advertised by the selected Android target | +| Capabilities | `EngineCapabilities` | Check protocol, streaming, backup, restore, lifecycle, and extension support | +| SQL | `query`, `execute`, `QueryResult` | Run SQL and read typed values from coroutine code | +| Raw protocol | `execProtocolRaw`, `execProtocolStream` | Send PostgreSQL protocol bytes through the serialized session | +| Transactions | `transaction`, `OliphauntTransaction` | Keep transaction work inside the pinned session boundary | +| Lifecycle | `prepareForBackground`, `resumeFromBackground`, `cancel`, `close` | Coordinate database work with Android app lifecycle transitions | +| Data movement | `backup`, `restore`, `BackupRequest` | Move app data through validated archives and Android file APIs | +| Errors | `OliphauntException`, `PostgresException` | Handle SDK errors and PostgreSQL SQLSTATE data | + +Android apps use the Android facade for packaged runtime resources. It keeps +native library loading, selected extension assets, and app-private storage in +the platform layer. diff --git a/src/docs/content/sdk/kotlin/guide.mdx b/src/docs/content/sdk/kotlin/guide.mdx new file mode 100644 index 00000000..e177e41a --- /dev/null +++ b/src/docs/content/sdk/kotlin/guide.mdx @@ -0,0 +1,161 @@ +--- +title: Build With Kotlin +description: Add Oliphaunt to an Android app with Gradle, coroutines, app-private storage, lifecycle hooks, selected extensions, and backup APIs. +--- + +# Build With Kotlin + +Use the Kotlin SDK in Android apps. It provides coroutine-friendly APIs over the +native runtime and owns Android resource hydration, root validation, and +extension materialization. + + +React Native on Android delegates runtime behavior through this SDK. Native +Android apps use the Kotlin facade directly for resource hydration and ABI +selection. + + + + + + + +### Install + +Add the Android SDK and app-applied Gradle plugin. The plugin resolves and +packages native runtime artifacts, Android ABIs, and exact extension files. + +```kotlin +plugins { + id("com.android.application") + id("dev.oliphaunt.android") version "0.1.0" +} + +dependencies { + implementation("dev.oliphaunt:oliphaunt-android:0.1.0") +} + +oliphaunt { + extensions.add("vector") +} +``` + +Use app-private storage for persistent roots and temporary roots for tests. + + + + +### Open and query + +Create an `OliphauntConfig`, open a database, run SQL, and close it from +coroutines. + + + +```kotlin +val database = + OliphauntDatabase.open( + OliphauntConfig( + root = context.filesDir.resolve("main.oliphaunt").absolutePath, + mode = EngineMode.NativeDirect, + extensions = listOf("vector"), + ), + ) + +val rows = database.query("SELECT 1::text AS value") +val value = rows.getText(row = 0, column = "value") + +database.close() +``` + +Share the opened database object through your app's dependency graph. It +represents one serialized native session. + + + + +### Create app data + +Use coroutine-friendly SQL helpers from repositories or use cases. Keep +database ownership in an Android service object rather than inside a composable: + +```kotlin +database.execute( + """ + CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() + ) + """.trimIndent(), +) + +database.query( + "INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id::text AS id", + listOf( + QueryParam.text("First note"), + QueryParam.text("Stored in an embedded PostgreSQL root"), + ), +) + +val notes = + database.query("SELECT id, title FROM notes ORDER BY id DESC LIMIT 20") +``` + +Expose app-specific suspend functions to UI code. The SDK owns the serialized +native session underneath those calls. + + + + +### Configure + +Configure root, mode, selected exact extensions, startup identity, Android asset +root, and durability through `OliphauntConfig` and the Android facade. Keep SQL +root selection separate from extension artifact selection. + + + + +### Choose a mode + +Android direct mode owns one resident backend per app process and one physical +session. Broker and server modes appear when the Android SDK reports platform +support for them. + + + + +### Handle lifecycle + +Database calls run through coroutine serialization. Close rejects queued work, +waits for active work, then detaches from the runtime. Use Android lifecycle +hooks to prepare for backgrounding and resume foreground work deliberately. + + + + +### Select extensions + +Select exact SQL extension names before opening the database. Android artifacts +contain only those selected extensions and mandatory dependencies. + + + + +### Back up and restore + +Use SDK backup and restore APIs with Android file/document APIs. The SDK +validates roots, formats, and archive metadata before materialization. + + + + + + +## Troubleshooting + +Check app storage permissions, root locks, missing native libraries, missing +runtime resources, mode capability errors, selected-extension artifacts, and +SQLSTATE-bearing PostgreSQL errors. diff --git a/src/docs/content/sdk/kotlin/index.mdx b/src/docs/content/sdk/kotlin/index.mdx new file mode 100644 index 00000000..cf49b3b9 --- /dev/null +++ b/src/docs/content/sdk/kotlin/index.mdx @@ -0,0 +1,88 @@ +--- +title: Kotlin SDK +description: Android SDK with coroutine-first database APIs and native resource handling. +--- + + + +The Kotlin SDK is the Android SDK for Oliphaunt. It provides coroutine-first +database APIs plus an Android facade for native library loading, runtime asset +materialization, ABI selection, and app-private storage. + +Use this SDK directly from Android apps written with Kotlin, Java interop, or +Jetpack Compose. React Native on Android delegates runtime work through this SDK, +so Android packaging, lifecycle, and resource selection are defined here. + +## Install + +Add the Android package to your app: + +```kotlin +plugins { + id("com.android.application") + id("dev.oliphaunt.android") version "0.1.0" +} + +dependencies { + implementation("dev.oliphaunt:oliphaunt-android:0.1.0") +} + +oliphaunt { + extensions.add("vector") +} +``` + +The Gradle plugin verifies and packages selected native runtime artifacts, +Android ABIs, and exact extension files. The app ships only the selected +extensions plus declared dependencies. + +## Open And Query + +Open a database from a coroutine: + +```kotlin +import dev.oliphaunt.EngineMode +import dev.oliphaunt.OliphauntAndroid +import dev.oliphaunt.OliphauntConfig + +val database = OliphauntAndroid.open( + context = applicationContext, + config = OliphauntConfig( + root = applicationContext.filesDir.resolve("main.oliphaunt").absolutePath, + mode = EngineMode.NativeDirect, + extensions = listOf("vector"), + ), +) + +val rows = database.query("SELECT 1::text AS value") +database.close() +``` + +## Runtime Shape + +Android direct mode owns one resident backend per app process and one serialized +physical session. The SDK reports capabilities for any additional runtime modes +available on the selected Android target. + +Coroutine callers may share a database handle. Direct mode queues work against +the resident backend and preserves transaction ordering. Android packaging owns +ABI selection, native library loading, and runtime resource hydration before the +first open. + +## App Responsibilities + +- Store persistent data in app-private storage unless the app deliberately + exports a backup. +- Select exact SQL extension names at build/configuration time so the APK or AAB + contains only selected extension artifacts and declared dependencies. +- Use lifecycle hooks around background and foreground transitions. +- Use SDK backup and restore APIs for archive validation and root + materialization. +- Check `capabilities()` before showing optional UI for streaming, backup + formats, or additional runtime modes. + +## First Query + +Use [Build With Kotlin](/docs/sdk/kotlin/guide) for open/query, +configuration, lifecycle, exact extensions, backup, restore, and troubleshooting. +Use the [API reference](/docs/sdk/kotlin/api-reference) for the public API map. diff --git a/src/docs/content/sdk/react-native/api-reference.md b/src/docs/content/sdk/react-native/api-reference.md new file mode 100644 index 00000000..718447e8 --- /dev/null +++ b/src/docs/content/sdk/react-native/api-reference.md @@ -0,0 +1,27 @@ +--- +title: API Reference +description: React Native SDK API map for TypeScript, config plugin, TurboModule, JSI binary transport, and mobile lifecycle. +--- + +# API Reference + +Use the TypeDoc reference for exact declarations. This page maps the React Native +SDK by task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| 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 | +| SQL | `query`, `execute`, `QueryResult` | Run SQL and read typed values from JavaScript | +| Raw protocol | `execProtocolRaw` | Send PostgreSQL protocol bytes through JSI `ArrayBuffer` transport | +| Streaming | `execProtocolStream` | Receive large protocol responses as native-backed chunks | +| Lifecycle | `prepareForBackground`, `resumeFromBackground`, `close` | Coordinate database work with app background and foreground transitions | +| Data movement | `backup`, `restore` | Delegate archive validation and root materialization to Swift or Kotlin | +| Package report | package-size and extension artifact reports | Verify that the app ships only selected extensions and target ABIs | +| Errors | `OliphauntError`, `PostgresError` | Handle SDK errors and PostgreSQL SQLSTATE data in TypeScript | + +The React Native SDK owns the JavaScript boundary. Runtime behavior remains +platform-native: Apple calls flow through Swift, Android calls flow through +Kotlin. diff --git a/src/docs/content/sdk/react-native/architecture.mdx b/src/docs/content/sdk/react-native/architecture.mdx new file mode 100644 index 00000000..d84c12b8 --- /dev/null +++ b/src/docs/content/sdk/react-native/architecture.mdx @@ -0,0 +1,138 @@ +--- +title: Architecture +description: Understand how the React Native SDK delegates runtime behavior to Swift and Kotlin while owning TypeScript, config plugin, TurboModule, and JSI transport. +--- + +# Architecture + +`@oliphaunt/react-native` is the React Native New Architecture SDK for +Oliphaunt. It gives JavaScript and TypeScript apps a native embedded PostgreSQL +database through the Swift and Kotlin SDKs. + +The package has three jobs: + +- provide the TypeScript API developers call from React Native; +- configure app builds so the selected native runtime and exact extensions are + packaged; +- move protocol bytes between JavaScript and the platform SDK. + + + +## Runtime Ownership + +Database runtime behavior belongs to the platform SDKs. Apple apps use the +Swift SDK. Android apps use the Kotlin SDK. + +| Platform | Runtime owner | React Native role | +| --- | --- | --- | +| iOS | Swift SDK | TypeScript API, config plugin, TurboModule, JSI byte transport | +| macOS (planned) | Swift SDK | No first-release React Native adapter; a future target must reuse the Swift boundary | +| Android | Kotlin SDK | TypeScript API, config plugin, TurboModule, JSI byte transport | + +That keeps platform behavior consistent. Root validation, extension selection, +backup and restore, lifecycle, cancellation, and capability reporting live in +the same SDKs used by native Swift and Kotlin app developers. + +Android calls go through the Android `dev.oliphaunt.OliphauntAndroid` facade, +returning the Kotlin SDK `OliphauntDatabase` handle behind the React Native +handle. + +## JavaScript Shape + +The JavaScript API is handle-oriented: + +```ts +import { Oliphaunt } from '@oliphaunt/react-native'; + +const db = await Oliphaunt.open({ + engine: 'nativeDirect', + temporary: true, + runtimeFootprint: 'balancedMobile', +}); + +const result = await db.query('SELECT 1::text AS value'); +const value = result.getText(0, 'value'); + +await db.close(); +``` + +The query helpers are layered over PostgreSQL protocol bytes. Use +`query(sql, params)` for normal app work. Use raw protocol APIs when you need +custom frontend protocol behavior, COPY, or multi-result-set handling. + +## Binary Transport + +Oliphaunt uses React Native's New Architecture for module lifecycle and typed +native bindings. Bulk protocol bytes use the binary JSI path. + +The fast path is a versioned JSI ArrayBuffer transport: + +- JS accepts `Uint8Array`, `ArrayBuffer`, and typed-array views; +- native code returns binary responses as owned buffers; +- streaming APIs deliver chunks for large responses when the platform reports + `protocolStream=true`; +- the SDK verifies the JSI installer before opening a native database session. + +This is the performance boundary for large result sets, protocol round trips, +backup and restore bytes, and mobile latency. + +## Config Plugin And Packaging + +The React Native package makes native packaging predictable: + +- select exact SQL extension names; +- include only selected extension artifacts and declared dependencies; +- include the native library and runtime resources for the app target; +- expose package-size reporting so developers can verify what ships. + +An app that selects only `vector` ships `vector` and its declared dependencies. + +## Lifecycle + +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 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 +rules stay native. + +## Capabilities + +Read capabilities from the opened database or SDK support API. Capabilities are +the contract; platform names only identify the target. + +`supportedModes()` is delegated too: iOS reports Swift support, and Android +reports Kotlin support through the Android facade. + +Capabilities report: + +- raw protocol support; +- streaming support; +- backup and restore formats; +- selected extension support; +- process and root behavior; +- whether broker or server mode is available. + +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 +runtime. + +## What The React Native SDK Owns + +React Native owns the React Native boundary: + +- TypeScript API and types; +- TurboModule schema; +- JSI ArrayBuffer transport; +- config plugin resource selection; +- installed iOS and Android app wiring for open, query, stream, backup, restore, + and close operations. + +Database runtime semantics follow the Swift and Kotlin SDKs. React Native docs +focus on packaging, transport, delegation, and installed-app behavior. diff --git a/src/docs/content/sdk/react-native/guide.mdx b/src/docs/content/sdk/react-native/guide.mdx new file mode 100644 index 00000000..a333adef --- /dev/null +++ b/src/docs/content/sdk/react-native/guide.mdx @@ -0,0 +1,201 @@ +--- +title: Build With React Native +description: Install the React Native package, build a native app binary, configure exact extensions, use JSI transport, and wire lifecycle APIs. +--- + +# Build With React Native + +Use the React Native SDK in Expo and New Architecture React Native apps. The JS +package owns TypeScript DX, config plugin behavior, TurboModule/JSI transport, +and installed-app integration. Runtime behavior is delegated to Swift on Apple +platforms and Kotlin on Android. + + +Oliphaunt includes Swift and Kotlin runtime code. Use an Expo development build +or a React Native app binary so the native runtime is present when JavaScript +loads. + + + + + + + +### Install + +Install the package for the app path you own, then build the native app binary. +The runtime uses Swift and Kotlin code, so package installation alone is not the +last step. The installed app must contain the native module and selected runtime +resources before JavaScript calls `Oliphaunt.open()`. + +Expo apps: + +```sh +npx expo install @oliphaunt/react-native +npx expo prebuild +npx expo run:ios +npx expo run:android +``` + +Expo apps use the config plugin so selected native runtime and extension +artifacts are included in the app build. + +```json +{ + "expo": { + "plugins": [ + [ + "@oliphaunt/react-native", + { + "extensions": ["vector"] + } + ] + ] + } +} +``` + +React Native apps without Expo still use the same package, but native projects +own the equivalent Pod, Gradle, and resource configuration. Rebuild after +changing native runtime or extension selection because those choices affect the +installed app binary. + + + + +### Open and query + +Open a database from TypeScript, run SQL, and close it when the app no longer +needs the handle. + + + +```ts +import { Oliphaunt } from '@oliphaunt/react-native'; + +const db = await Oliphaunt.open({ + root: 'main.oliphaunt', + engine: 'nativeDirect', + extensions: ['vector'], +}); + +const rows = await db.query('SELECT 1::text AS value'); +const value = rows.getText(0, 'value'); + +await db.close(); +``` + +Keep the database handle in app state or a service object. Multiple JavaScript +objects share the same mobile direct session through the platform SDK. + + + + +### Create app data + +Use the SQL helpers for application queries. This keeps most React Native code +away from PostgreSQL protocol details: + +```ts +await db.execute(` + CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() + ) +`); + +await db.query( + 'INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id::text AS id', + ['First note', 'Stored in an embedded PostgreSQL root'], +); + +const notes = await db.query( + 'SELECT id, title FROM notes ORDER BY id DESC LIMIT 20', +); +const firstTitle = notes.getText(0, 'title'); +``` + +Reach for raw protocol APIs only when building adapters, COPY flows, or +streaming paths that need PostgreSQL wire messages directly. + + + + +### Configure + +Configure root, mode, selected exact extensions, durability, startup identity, +and native runtime overrides through the JS API and config plugin. Build-time +extension selection controls what ships in the app bundle; runtime root +configuration controls where app data lives. + +Keep build-time and runtime settings separate. The config plugin controls native +artifacts in the installed app. `Oliphaunt.open()` controls the database root, +mode, durability, and extension activation for that app run. + + + + +### Choose a mode + +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 mode +reports. `OpenConfig.engine` currently accepts `nativeDirect` only. + + + + +### Use binary transport + +New Architecture builds use binary ArrayBuffer/JSI transport for raw protocol +bytes and chunked streaming. Bulk payloads stay in binary buffers. + +Treat JSI transport as the bulk-byte boundary. Small configuration and lifecycle +calls use the TurboModule API; protocol payloads and streamed chunks stay in +binary buffers. + + + + +### Handle lifecycle + +Use the SDK lifecycle hooks around background/foreground transitions. Direct +mobile mode is same-root logically reopenable inside one resident app process. +Broker and server runtimes add a process boundary on targets that advertise +those modes. + +Call the JavaScript lifecycle API from app-level lifecycle handlers. The Swift +and Kotlin SDKs own the platform-specific details underneath that call. + + + + +### Select extensions + +Select exact SQL extension names in configuration. The native app packages +include only selected extensions plus declared dependencies. `CREATE EXTENSION` +succeeds when the selected runtime resources contain that extension for the +target platform. + + + + +### Back up and restore + +Use the React Native backup and restore APIs instead of copying platform storage +directories from JavaScript. The SDK delegates archive validation and root +materialization to Swift or Kotlin, so platform storage rules stay native. + + + + + + +## Troubleshooting + +Check the development build, Expo config plugin output, autolinking, +TurboModule codegen, native module availability, selected extension artifacts, +and platform SDK errors. For database runtime behavior, follow the Swift or +Kotlin SDK page for the target platform. diff --git a/src/docs/content/sdk/react-native/index.mdx b/src/docs/content/sdk/react-native/index.mdx new file mode 100644 index 00000000..3539d801 --- /dev/null +++ b/src/docs/content/sdk/react-native/index.mdx @@ -0,0 +1,99 @@ +--- +title: React Native SDK +description: New Architecture package with Expo config plugin, TurboModule, and JSI transport. +--- + + + +The React Native SDK is a New Architecture package over the Swift and Kotlin +SDKs. It provides TypeScript APIs, an Expo config plugin, TurboModule codegen, +and JSI ArrayBuffer transport while native execution stays in the platform SDKs. + +Use this package for Expo development builds and React Native New Architecture +apps on iOS and Android. Apple runtime behavior flows through the Swift SDK; +Android runtime behavior flows through the Kotlin SDK. The JavaScript package +owns TypeScript ergonomics, config plugin output, binary transport, and +installed-app integration. + +## Install + +Install the package and build a development client or native app binary: + +```sh +npx expo install @oliphaunt/react-native +``` + +Oliphaunt includes native Swift and Kotlin code, so React Native apps run it +from an Expo development build or a native app binary. The config plugin +selects the native runtime and exact extension artifacts that ship in the app. + +Configure selected extensions in app config: + +```json +{ + "expo": { + "plugins": [ + [ + "@oliphaunt/react-native", + { + "extensions": ["vector"] + } + ] + ] + } +} +``` + +Rebuild the development client or native app binary after changing native +runtime or extension selections. + +## Open And Query + +Open from TypeScript and keep the handle in app state, a data service, or a +provider that matches your navigation lifetime: + +```ts +import { Oliphaunt } from '@oliphaunt/react-native'; + +const db = await Oliphaunt.open({ + root: 'main.oliphaunt', + engine: 'nativeDirect', + extensions: ['vector'], +}); + +const rows = await db.query('SELECT 1::text AS value'); +await db.close(); +``` + +Use high-level SQL helpers for app code. Use raw protocol and streaming APIs +when building adapters that need PostgreSQL wire messages directly. + +## Runtime Shape + +React Native owns the JavaScript boundary: typed configuration, SQL helpers, +raw protocol bytes, chunked streaming, lifecycle hooks, and package reporting. +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. `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 + +- Build with a native app binary or development client. +- Select exact SQL extension names so only selected extension artifacts and + declared dependencies enter the iOS or Android app artifact. +- Call lifecycle hooks around background and foreground transitions. +- Check `capabilities()` before showing UI that depends on streaming, + extensions, backup formats, or additional runtime modes. +- Use SDK backup and restore APIs for user-visible export/import flows instead + of copying platform storage directories from JavaScript. + +## First Query + +Use [Build With React Native](/docs/sdk/react-native/guide) for Expo setup, first +query, config plugin options, JSI transport, lifecycle, extensions, and backup. +Use the [architecture](/docs/sdk/react-native/architecture) page for the native +boundary model. diff --git a/src/docs/content/sdk/rust/api-reference.md b/src/docs/content/sdk/rust/api-reference.md new file mode 100644 index 00000000..dc1ede0e --- /dev/null +++ b/src/docs/content/sdk/rust/api-reference.md @@ -0,0 +1,25 @@ +--- +title: API Reference +description: Rust SDK API map for builders, runtime modes, query results, lifecycle, and data movement. +--- + +# API Reference + +Use the Rust API reference for exact signatures. This page maps the public +surface so you can jump from a product concept to the item you need. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Opening | `Oliphaunt::builder()`, `OliphauntBuilder` | Choose root, mode, durability, runtime assets, startup identity, and extensions | +| Runtime mode | `EngineMode`, `native_direct()`, `native_broker()`, `native_server()` | Select direct, broker, or server behavior explicitly | +| Capabilities | `EngineCapabilities`, `supported_modes()` | Check protocol, streaming, backup, restore, extension, and session support | +| SQL | `query`, `execute`, `query_params` | Run simple and parameterized SQL through the selected runtime | +| Raw protocol | `exec_protocol_raw`, `exec_protocol_stream` | Send PostgreSQL protocol bytes or stream large responses | +| Transactions | `transaction`, `with_transaction`, `SessionPin` | Pin the physical session while a transaction is active | +| Lifecycle | `checkpoint`, `cancel`, `close` | Control active work and detach from the runtime cleanly | +| Data movement | `backup`, `restore`, `BackupRequest`, `RestoreRequest` | Export, import, and validate physical archives | +| Errors | `Error`, `PostgresError`, `RuntimeUnavailable` | Handle SDK errors and PostgreSQL SQLSTATE data | + +The Rust SDK is the full native mode surface for Tauri and Rust desktop apps. +Use server mode when you need independent PostgreSQL clients; cloned direct-mode +handles still share one serialized session. diff --git a/src/docs/content/sdk/rust/guide.mdx b/src/docs/content/sdk/rust/guide.mdx new file mode 100644 index 00000000..e6e62068 --- /dev/null +++ b/src/docs/content/sdk/rust/guide.mdx @@ -0,0 +1,163 @@ +--- +title: Build With Rust +description: Use the Rust SDK in Tauri or native Rust apps with direct, broker, server, app-owned roots, exact extensions, lifecycle, and backup APIs. +--- + +# Build With Rust + +Use the Rust SDK in Tauri backends and native Rust desktop apps. It owns the +complete native mode model: direct for lowest latency, broker for helper-process +isolation, and server when independent PostgreSQL clients are required. + + +Use this guide when the app runtime is Rust. Tauri webviews and desktop +JavaScript apps use their SDKs and call into Rust through app commands or helper +processes. + + + + + + + +### Install + +Add the crate and let the SDK resolve released runtime assets and helpers +through configuration: + +```toml +[dependencies] +oliphaunt = "0.1" +``` + + + + +### Open and query + +Create a builder, choose storage, select exact extensions, open, query, and +close. + + + +```rust +use oliphaunt::{Extension, Oliphaunt}; + +async fn open_database() -> oliphaunt::Result<()> { + let db = Oliphaunt::builder() + .path(".oliphaunt") + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + + let rows = db.query("SELECT 1::text AS value").await?; + assert_eq!(rows.get_text(0, "value")?, Some("1")); + + db.close().await?; + Ok(()) +} +``` + +Keep the opened `Oliphaunt` handle in application state. Cloned handles point to +the same executor. Use server mode when the app needs a connection pool or +independent PostgreSQL client sessions. + + + + +### Create app data + +Use typed helpers for application queries and keep the database handle in Tauri +state, an async service, or another app-owned dependency container: + +```rust +db.execute( + r#" + CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() + ) + "#, +) +.await?; + +db.query_params( + "INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id::text AS id", + ["First note", "Stored in an embedded PostgreSQL root"], +) +.await?; + +let notes = db + .query("SELECT id, title FROM notes ORDER BY id DESC LIMIT 20") + .await?; +``` + +Expose app-specific commands to a Tauri webview instead of exposing raw database +handles directly to frontend code. + + + + +### Configure + +Configure engine mode, root, durability, startup identity, selected extensions, +runtime assets, broker executable, and server executable through the builder. +Use a persistent app-owned directory for user data. Temporary roots are useful +for tests and short-lived tools. + + + + +### Choose a mode + +`NativeDirect` runs one serialized embedded session in the process. It gives the +lowest round-trip latency and rejects pool sizes above one. + +`NativeBroker` talks to a local helper process. Use it for desktop apps that need +process isolation, crash recovery, or several roots managed by one application. + +`NativeServer` starts a PostgreSQL-compatible server process. Use it when `psql`, +`pg_dump`, ORMs, or true independent client sessions matter. + + + + +### Handle lifecycle + +Direct-mode work queues fairly on one owner executor. Transactions pin the +physical session until commit or rollback. `close()` rejects queued work, waits +for active work, then closes or detaches from the selected runtime. Use explicit +cancellation for long-running SQL. + + + + +### Select extensions + +Select exact SQL extension names before open. There are no packs, aliases, or +implicit selectors. If you select `vector`, the generated artifacts include +`vector` and only its declared dependencies. + + + + +### Back up and restore + +Use SDK backup and restore APIs instead of copying PostgreSQL directories from +application code. The SDK validates formats, target roots, locks, and restore +options before materializing data. + + + + + + +## Troubleshooting + +Check root locks, missing runtime assets, mode capability errors, extension +selection errors, and SQLSTATE-bearing PostgreSQL errors. If concurrency looks +surprising, confirm the selected mode first: direct mode serializes work by +design, while independent sessions require server mode. diff --git a/src/docs/content/sdk/rust/index.mdx b/src/docs/content/sdk/rust/index.mdx new file mode 100644 index 00000000..7b13342e --- /dev/null +++ b/src/docs/content/sdk/rust/index.mdx @@ -0,0 +1,76 @@ +--- +title: Rust SDK +description: Tauri and native Rust SDK for direct, broker, and server runtime modes. +--- + + + +The Rust SDK targets Tauri apps, native Rust desktop apps, and Rust services +that want an embedded PostgreSQL runtime with explicit mode selection. + +Use this SDK when your application is written in Rust or when a Tauri app keeps +database ownership in the Rust sidecar. It owns the full native runtime model: +direct calls for embedded latency, broker helpers for desktop robustness, and +server mode for normal PostgreSQL clients. + +## Install + +Add the crate to your Rust app: + +```toml +[dependencies] +oliphaunt = "0.1" +``` + +## Open And Query + +Then open a root in app-owned storage: + +```rust +use oliphaunt::{Extension, Oliphaunt}; + +async fn open_database() -> oliphaunt::Result<()> { + let db = Oliphaunt::builder() + .path("./app-data/main.oliphaunt") + .native_direct() + .extension(Extension::Vector) + .open() + .await?; + + let rows = db.query("SELECT 1::text AS value").await?; + db.close().await?; + Ok(()) +} +``` + +## Runtime Shape + +Rust exposes the complete native mode model: + +| Mode | Use it for | +| --- | --- | +| Native direct | Lowest-latency embedded PostgreSQL session | +| Native broker | Helper-process isolation and multi-root desktop apps | +| Native server | `psql`, `pg_dump`, ORMs, pools, and independent clients | + +Direct and broker mode serialize work through one physical PostgreSQL session. +Server mode is the mode for independent concurrent PostgreSQL clients. + +Read `capabilities()` after opening when an app enables optional features such +as streaming, backup formats, server compatibility, or exact extensions. + +## App Responsibilities + +- Store persistent roots in an app-owned directory and use temporary roots for + tests. +- Select exact SQL extension names before opening the database. +- Use SDK backup and restore APIs for data movement instead of copying a live + PostgreSQL directory. +- Use broker or server mode when crash isolation or independent clients matter. + +## First Query + +Use [Build With Rust](/docs/sdk/rust/guide) for install, open, query, +configuration, lifecycle, exact extensions, backup, restore, and +troubleshooting. Use the [API reference](/docs/sdk/rust/api-reference) when you +need the public type map. diff --git a/src/docs/content/sdk/swift/api-reference.md b/src/docs/content/sdk/swift/api-reference.md new file mode 100644 index 00000000..583b79f2 --- /dev/null +++ b/src/docs/content/sdk/swift/api-reference.md @@ -0,0 +1,24 @@ +--- +title: API Reference +description: Swift SDK API map for Apple app storage, async database calls, lifecycle, and native resources. +--- + +# API Reference + +Use the Swift DocC reference for exact declarations. This page maps the Apple +SDK surface by task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Opening | `OliphauntDatabase.open`, `OliphauntConfiguration` | Open a persistent or temporary root with Apple-friendly defaults | +| Runtime mode | `OliphauntEngineMode`, `supportedModes()` | Discover modes advertised by the selected Apple target | +| Capabilities | `OliphauntCapabilities` | Check protocol, streaming, backup, restore, lifecycle, and extension support | +| SQL | `query`, `execute`, `OliphauntQueryResult` | Run SQL and read typed values by row and column | +| Raw protocol | `execProtocolRaw`, `execProtocolStream` | Send PostgreSQL protocol bytes without blocking the main actor | +| Transactions | `transaction`, `OliphauntTransaction` | Keep transaction work on the actor-owned session | +| Lifecycle | `prepareForBackground`, `resumeFromBackground`, `cancel`, `close` | Coordinate database work with app lifecycle transitions | +| Data movement | `backup`, `restore`, `OliphauntBackupRequest` | Move user data through validated archives and app-owned file URLs | +| Errors | `OliphauntError`, `OliphauntPostgresError` | Handle Swift errors and PostgreSQL SQLSTATE data | + +iOS and macOS apps start with `OliphauntDatabase`. The C ABI remains the +lower-level boundary used by the Swift package. diff --git a/src/docs/content/sdk/swift/guide.mdx b/src/docs/content/sdk/swift/guide.mdx new file mode 100644 index 00000000..49d84379 --- /dev/null +++ b/src/docs/content/sdk/swift/guide.mdx @@ -0,0 +1,157 @@ +--- +title: Build With Swift +description: Add Oliphaunt to iOS or macOS with Swift concurrency, app-container storage, lifecycle hooks, exact extensions, and backup APIs. +--- + +# Build With Swift + +Use the Swift SDK in iOS and macOS apps. It wraps the native runtime behind +Swift async APIs and keeps database work off the main actor. + + +React Native on Apple platforms delegates runtime behavior through this SDK, but +Swift and SwiftUI apps use `OliphauntDatabase` directly. + + + + + + + +### Install + +Add the Swift package in Xcode or `Package.swift`. The package includes the +Swift API plus the platform runtime artifacts required for the selected target. + +```swift +dependencies: [ + .package(url: "https://github.com/f0rr0/oliphaunt.git", from: "0.6.0") +] +``` + +Persistent roots live under your app container. App users install your app; the +SDK package carries the runtime files it needs. + + + + +### Open and query + +Open an `OliphauntDatabase` with a file URL root, run SQL with async calls, and +close when the app no longer needs the handle. + + + +```swift +let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask +)[0] + +let database = try await OliphauntDatabase.open( + configuration: OliphauntConfiguration( + root: appSupport.appending(path: "main.oliphaunt"), + mode: .nativeDirect, + extensions: ["vector"] + ) +) + +let rows = try await database.query("SELECT 1::text AS value") +let value = try rows.getText(row: 0, column: "value") + +try await database.close() +``` + +Keep one database handle in app state and share it through your app's dependency +model. The handle owns a serialized session boundary. + + + + +### Create app data + +Use async SQL helpers for application data. A SwiftUI model or app service can +own the database handle and expose app-specific methods: + +```swift +try await database.execute(""" +CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +) +""") + +try await database.query( + "INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id::text AS id", + parameters: [ + .text("First note"), + .text("Stored in an embedded PostgreSQL root") + ] +) + +let notes = try await database.query( + "SELECT id, title FROM notes ORDER BY id DESC LIMIT 20" +) +``` + +Keep UI updates on the main actor, but keep database work behind the SDK's async +database actor. + + + + +### Configure + +Configure root URL, runtime mode, selected exact extensions, resource bundle, +startup identity, and durability through the Swift configuration API. Resource +locations are explicit for tests and advanced app layouts; normal apps use the +packaged defaults. + + + + +### Choose a mode + +Mobile direct mode is the primary Apple runtime. It uses one resident backend per +app process and one physical session. Desktop/server boundaries are available on +Apple targets only when `capabilities()` advertises them. + + + + +### Handle lifecycle + +Swift exposes lifecycle as async API over a serial execution model. Use the +provided background/foreground hooks around app lifecycle transitions so the SDK +can checkpoint, close transient work, and resume cleanly where supported. + + + + +### Select extensions + +Select exact SQL extension names in app configuration. The app bundle contains +selected extension artifacts plus required dependencies. `CREATE EXTENSION` +succeeds when the selected runtime resources contain that extension for the +Apple target. + + + + +### Back up and restore + +Use SDK backup and restore APIs with app-owned file URLs. The SDK validates +formats and target roots before calling into the native runtime. + + + + + + +## Troubleshooting + +Most Apple failures come from invalid file URLs, missing runtime resources, root +locks, mode capability errors, or extension selection mismatches. PostgreSQL +errors preserve SQLSTATE where the backend returns it. diff --git a/src/docs/content/sdk/swift/index.mdx b/src/docs/content/sdk/swift/index.mdx new file mode 100644 index 00000000..38716e66 --- /dev/null +++ b/src/docs/content/sdk/swift/index.mdx @@ -0,0 +1,76 @@ +--- +title: Swift SDK +description: Apple SDK for iOS and macOS apps using Swift concurrency. +--- + + + +The Swift SDK is the Apple SDK for iOS and macOS apps. It wraps the native +runtime with Swift concurrency, app-container storage defaults, resource bundle +handling, and platform lifecycle APIs. + +Use this SDK directly in SwiftUI, UIKit, AppKit, and app extension-safe code +where supported. React Native on Apple platforms delegates runtime work through +this SDK, so the Swift lifecycle model is the Apple behavior source. + +## Install + +Add the Swift package from Xcode or `Package.swift`: + +```swift +.package(url: "https://github.com/f0rr0/oliphaunt.git", from: "0.6.0") +``` + +The package carries the Swift API and the native runtime artifacts for supported +Apple targets. + +## Open And Query + +Open a database from an async context: + +```swift +import Oliphaunt + +let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask +)[0] + +let database = try await OliphauntDatabase.open( + configuration: OliphauntConfiguration( + root: appSupport.appending(path: "main.oliphaunt"), + mode: .nativeDirect, + extensions: ["vector"] + ) +) + +let rows = try await database.query("SELECT 1::text AS value") +try await database.close() +``` + +## Runtime Shape + +Swift is async-first and actor-owned. Direct mode owns one resident backend per +app process and one serialized physical session. The SDK reports capabilities +for any additional modes available on the selected Apple target. + +The actor boundary keeps database calls off the main actor. App code can issue +concurrent Swift tasks against the same database handle; direct mode queues them +against the resident backend and preserves transaction ordering. + +## App Responsibilities + +- Keep user data in an app-owned persistent directory. +- Call lifecycle hooks around background and foreground transitions. +- Select exact SQL extension names so the app bundle contains only selected + extension artifacts and declared dependencies. +- Use backup and restore APIs for export, import, and user-visible data + movement. +- Check `capabilities()` before enabling optional UI for streaming, backup + formats, or additional runtime modes. + +## First Query + +Use [Build With Swift](/docs/sdk/swift/guide) for open/query, app lifecycle, +extension selection, and backup/restore. Use the +[API reference](/docs/sdk/swift/api-reference) for the public API map. diff --git a/src/docs/content/sdk/typescript/api-reference.md b/src/docs/content/sdk/typescript/api-reference.md new file mode 100644 index 00000000..86d75389 --- /dev/null +++ b/src/docs/content/sdk/typescript/api-reference.md @@ -0,0 +1,25 @@ +--- +title: API Reference +description: TypeScript SDK API map for desktop JavaScript, native assets, broker helpers, SQL, lifecycle, and data movement. +--- + +# API Reference + +Use the TypeDoc reference for exact declarations. This page maps the TypeScript +SDK by task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Opening | `Oliphaunt.open`, `OpenConfig` | Open a native direct, broker, or server-backed database from Node.js, Bun, or Deno | +| Native assets | asset resolver exports | Locate released runtime artifacts from the package | +| Runtime mode | `engine`, `supportedModes()` | Choose direct, broker, or server where the desktop runtime supports it | +| Capabilities | `capabilities()` | Check protocol, streaming, backup, restore, extension, and lifecycle support | +| SQL | `query`, `execute`, typed result helpers | Run SQL and read typed values from JavaScript | +| Raw protocol | `execProtocolRaw`, protocol utilities | Send PostgreSQL protocol bytes through the selected native path | +| Streaming | `execProtocolStream` | Consume large result sets without materializing one huge JS buffer | +| Broker/server helpers | helper process APIs | Start or connect to a local helper when isolation or PostgreSQL-compatible clients are needed | +| Data movement | `backup`, `restore`, archive helpers | Move roots through validated physical archives | +| Errors | `OliphauntError`, `PostgresError` | Handle SDK errors and SQLSTATE data | + +React Native apps use `@oliphaunt/react-native`. This package is for desktop +JavaScript runtimes. diff --git a/src/docs/content/sdk/typescript/guide.mdx b/src/docs/content/sdk/typescript/guide.mdx new file mode 100644 index 00000000..1ff523db --- /dev/null +++ b/src/docs/content/sdk/typescript/guide.mdx @@ -0,0 +1,144 @@ +--- +title: Build With TypeScript +description: Use the desktop JavaScript SDK in Node.js, Bun, or Deno with helper-backed runtimes and selected extensions. +--- + +# Build With TypeScript + +Use the TypeScript SDK in Node.js, Bun, and Deno. It +provides a JavaScript API over Oliphaunt runtime assets and broker/server +helpers where supported. + + +Use this package for Node.js, Bun, and Deno. Tauri apps currently keep the +database in Rust state behind narrow app-owned commands; direct webview +integration is planned. React Native apps use the React Native SDK because +mobile runtime work flows through Swift and Kotlin. + + + + + + + +### Install + +Install the npm package. Runtime assets and helper executables resolve through +package configuration. + +```sh +npm install @oliphaunt/ts +``` + + + + +### Open and query + +Create a client, open a root, run SQL, and close the handle. + + + +```ts +import { Oliphaunt } from '@oliphaunt/ts'; + +const db = await Oliphaunt.open({ + engine: 'nativeBroker', + root: './app-data/main.oliphaunt', + extensions: ['vector'], +}); + +const rows = await db.query('SELECT 1::text AS value'); +const value = rows.getText(0, 'value'); + +await db.close(); +``` + +Keep one client per app database root unless you intentionally use a mode that +supports independent sessions. + + + + +### Create app data + +Use the TypeScript query helpers for app code and keep the client in a service +or framework-owned dependency container: + +```ts +await db.execute(` + CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() + ) +`); + +await db.query( + 'INSERT INTO notes (title, body) VALUES ($1, $2) RETURNING id::text AS id', + ['First note', 'Stored in an embedded PostgreSQL root'], +); + +const notes = await db.query( + 'SELECT id, title FROM notes ORDER BY id DESC LIMIT 20', +); +const firstTitle = notes.getText(0, 'title'); +``` + +Tauri apps keep the database in Rust state and expose narrow app-owned commands +to the webview. A direct TypeScript/webview adapter is planned, not supported in +the first release. + + + + +### Configure + +Configure mode, root, selected exact extensions, runtime asset locations, +durability, startup identity, broker helper path, and server helper path through +the JS configuration object. + + + + +### Choose a mode + +Direct mode is lowest-latency where a native binding is available. Broker mode +uses a helper process and is the preferred robust desktop JavaScript path. +Server mode is for PostgreSQL-compatible tools and independent clients. + + + + +### Handle lifecycle + +The client queues work through the selected runtime boundary. Close rejects +queued work and waits for active work. Use explicit cancellation for long SQL. + + + + +### Select extensions + +Select exact SQL extension names before open. Generated resources include only +selected extensions and mandatory dependencies. + + + + +### Back up and restore + +Use SDK backup and restore helpers for physical archives and supported server +flows. Keep live PostgreSQL directories behind the SDK data-movement APIs. + + + + + + +## Troubleshooting + +Check runtime asset resolution, helper executable availability, root locks, +mode capability errors, extension selection, and SQLSTATE-bearing PostgreSQL +errors. diff --git a/src/docs/content/sdk/typescript/index.mdx b/src/docs/content/sdk/typescript/index.mdx new file mode 100644 index 00000000..432585d8 --- /dev/null +++ b/src/docs/content/sdk/typescript/index.mdx @@ -0,0 +1,78 @@ +--- +title: TypeScript SDK +description: Desktop JavaScript SDK for Node.js, Bun, and Deno. +--- + + + +The TypeScript SDK targets Node.js, Bun, and Deno. It is +the desktop JavaScript SDK for local native helpers, broker/server flows, and +runtime asset resolution. + +Use this SDK for supported desktop JavaScript runtimes. Tauri apps currently +keep the database in Rust state and expose narrow app-owned commands to the +webview; a direct TypeScript adapter is planned. React Native apps use the React +Native SDK because mobile execution delegates through Swift and Kotlin. + +## Install + +Install the package from npm: + +```sh +npm install @oliphaunt/ts +``` + +npm is the native-runtime distribution for Node.js, Bun, and Deno (Deno can +import it through `npm:`). The JSR package intentionally contains only the +portable protocol and query helpers; it does not claim or resolve a native +runtime. + +Use this SDK for Node.js, Bun, and Deno. Tauri frontends currently call narrow +app-owned Rust commands; the direct JavaScript adapter remains planned. +React Native apps use `@oliphaunt/react-native` because mobile execution +delegates through Swift and Kotlin. + +## Open And Query + +Open a database from TypeScript: + +```ts +import { Oliphaunt } from '@oliphaunt/ts'; + +const db = await Oliphaunt.open({ + root: './app-data/main.oliphaunt', + engine: 'nativeBroker', + extensions: ['vector'], +}); + +const rows = await db.query('SELECT 1::text AS value'); +await db.close(); +``` + +## Runtime Shape + +Broker mode is the robust desktop JavaScript path. Direct mode is available only +where a native adapter is present. Server mode is for PostgreSQL-compatible +tools and independent clients. + +The SDK resolves local helper binaries and runtime assets for the selected +platform. App code keeps using TypeScript promises and typed result helpers while +the runtime mode determines whether calls use direct, broker, or server +semantics. + +## App Responsibilities + +- Ship the runtime helper and selected exact extension artifacts with the app. +- Pick broker mode when a helper process owns the database runtime. +- Use server mode when existing PostgreSQL clients, pools, or ORMs need + independent sessions. +- Use SDK backup and restore APIs for export/import flows. +- Check `capabilities()` before relying on direct adapters, streaming, backup + formats, or server compatibility. + +## First Query + +Use [Build With TypeScript](/docs/sdk/typescript/guide) for open/query, +configuration, helper resolution, lifecycle, exact extensions, backup, restore, +and troubleshooting. Use the [API reference](/docs/sdk/typescript/api-reference) +for the public API map. diff --git a/src/docs/content/sdk/wasm/api-reference.md b/src/docs/content/sdk/wasm/api-reference.md new file mode 100644 index 00000000..27751fc6 --- /dev/null +++ b/src/docs/content/sdk/wasm/api-reference.md @@ -0,0 +1,25 @@ +--- +title: API Reference +description: WASM SDK API map for the WASIX runtime family, protocol types, storage, extensions, and dump/restore. +--- + +# API Reference + +Use the WASM rustdoc reference for exact declarations. This page maps the WASM +SDK by task. + +| Area | Public surface | Use it for | +| --- | --- | --- | +| Opening | runtime builders and root options | Open persistent or temporary WASM roots | +| Runtime assets | asset loader and catalog APIs | Select the released WASIX PostgreSQL runtime artifacts | +| Capabilities | capability reporting | Check protocol, extension, storage, dump, restore, and server support | +| SQL | query and execute helpers | Run SQL through the WASM runtime | +| Raw protocol | protocol request and response types | Send PostgreSQL protocol bytes to the WASM backend | +| Server/proxy | WASM server helper APIs | Expose PostgreSQL-compatible access where the WASM runtime supports it | +| Extensions | exact extension selectors | Include only selected WASM-built extension artifacts | +| Dump/restore | dump and restore APIs | Move data between compatible roots or export logical dumps | +| Errors | WASM SDK and PostgreSQL error types | Handle runtime errors and SQLSTATE data | + +The WASM SDK is a first-class runtime family with its own packaged PostgreSQL +runtime assets. Native direct, broker, and server mode behavior is documented in +the native SDK sections. diff --git a/src/docs/content/sdk/wasm/dump-restore.mdx b/src/docs/content/sdk/wasm/dump-restore.mdx new file mode 100644 index 00000000..699fea45 --- /dev/null +++ b/src/docs/content/sdk/wasm/dump-restore.mdx @@ -0,0 +1,163 @@ +--- +title: Dump, Restore, And Upgrade +description: Use logical dumps, physical archives, CLI exports, and restore flows for the WASM runtime. +--- + +# Dump, Restore, And Upgrade + +`oliphaunt-wasix` uses the WASIX `pg_dump` binary from the shipped portable +runtime asset for portable SQL exports, restores, and version-to-version +upgrades. + + + +## Choose The Right Export Format + +Use logical dumps when you need: + +- a portable SQL export; +- an upgrade path between `oliphaunt-wasix` releases; +- a way to move data between different roots safely. + +Use physical data-dir archives when you need: + +- a same-version clone; +- a same-runtime restore into another `oliphaunt-wasix` root; +- a fast local snapshot of the current cluster state. + +Use logical dumps for cross-version upgrades. Keep physical archives for +same-version clones and restores. + +## Direct API + +Dump an already-open `Oliphaunt` database to SQL: + +```rust,no_run +use oliphaunt_wasix::{PgDumpOptions, Oliphaunt}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + db.exec("CREATE TABLE items(value TEXT)", None)?; + db.exec("INSERT INTO items VALUES ('alpha')", None)?; + + let sql = db.dump_sql(PgDumpOptions::new())?; + assert!(sql.contains("INSERT INTO")); + + db.close()?; + Ok(()) +} +``` + +Get UTF-8 bytes instead: + +```rust,no_run +use oliphaunt_wasix::{PgDumpOptions, Oliphaunt}; + +fn main() -> Result<(), Box> { + let mut db = Oliphaunt::temporary()?; + let bytes = db.dump_bytes(PgDumpOptions::new())?; + assert!(!bytes.is_empty()); + db.close()?; + Ok(()) +} +``` + +Direct dumps run against the already-open embedded backend. If you need to dump +as a different user or from a different database, start a `OliphauntServer` and +use the server dump path instead. + +## Server API + +Dump through a local Postgres endpoint when another part of your workflow +already uses `OliphauntServer`: + +```rust,no_run +use oliphaunt_wasix::{PgDumpOptions, OliphauntServer}; + +fn main() -> Result<(), Box> { + let server = OliphauntServer::temporary_tcp()?; + let sql = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + assert!(!sql.is_empty()); + server.shutdown()?; + Ok(()) +} +``` + +Use a TCP endpoint for `OliphauntServer::dump_sql(...)`. + +## `PgDumpOptions` + +`PgDumpOptions` controls the managed parts of the dump command: + +```rust,no_run +use oliphaunt_wasix::PgDumpOptions; + +let options = PgDumpOptions::new() + .username("postgres") + .database("template1") + .args(["--schema-only", "--quote-all-identifiers"]); +``` + +Useful passthrough flags include dump-shaping options such as: + +- `--schema-only` +- `--quote-all-identifiers` +- `-n ` +- `-t ` + +Managed connection and output flags are reserved by the API: +`--file`, `--format`, `--host`, `--port`, `--username`, `--dbname`, and +`--jobs` are configured by Oliphaunt instead of `arg(...)` or `args(...)`. + +## CLI + +Dump a persistent root: + +```sh +oliphaunt-wasix-dump --root ./.oliphaunt +``` + +Pass through normal `pg_dump` shaping flags after `--`: + +```sh +oliphaunt-wasix-dump --root ./.oliphaunt -- --schema-only +oliphaunt-wasix-dump --root ./.oliphaunt -- --quote-all-identifiers +``` + +## Restore + +Restore a logical dump by executing the SQL against a new database: + +```rust,no_run +use oliphaunt_wasix::{PgDumpOptions, Oliphaunt}; + +fn main() -> Result<(), Box> { + let mut source = Oliphaunt::temporary()?; + source.exec("CREATE TABLE items(value TEXT)", None)?; + source.exec("INSERT INTO items VALUES ('alpha')", None)?; + let dump_sql = source.dump_sql(PgDumpOptions::new())?; + + let mut restored = Oliphaunt::temporary()?; + restored.exec(&dump_sql, None)?; + + source.close()?; + restored.close()?; + Ok(()) +} +``` + +For same-version root copies, prefer `dump_data_dir()` / +`load_data_dir_archive(...)` or `try_clone()`. + +## Upgrade Guidance + +Use logical dump and restore when upgrading between `oliphaunt-wasix` versions or +changing shipped runtime assets: + +1. Open the old database with the old crate/runtime. +2. Create a logical dump with `dump_sql(...)` or `oliphaunt-wasix-dump`. +3. Open a fresh database with the new crate/runtime. +4. Execute the dump SQL into the new database. + +Use logical dumps for general upgrades. Physical data-dir archives are for the +same runtime family and database format. diff --git a/src/docs/content/sdk/wasm/guide.mdx b/src/docs/content/sdk/wasm/guide.mdx new file mode 100644 index 00000000..c0b520dc --- /dev/null +++ b/src/docs/content/sdk/wasm/guide.mdx @@ -0,0 +1,138 @@ +--- +title: Build With WASM +description: Use the WASM/WASIX runtime family with shipped runtime assets, app data, lifecycle, and dump/restore flows. +--- + +# Build With WASM + +Use the WASM SDK when an application needs the WebAssembly/WASIX runtime family. +It is first-class and independent from native `liboliphaunt` SDKs. + + +Use this guide for the WASM/WASIX runtime. Native desktop and mobile apps use the +native SDK pages when they embed `liboliphaunt`. + + + + + + + +### Install + +Install the WASM package when your application targets a WASIX host. The Rust +crate is the API surface; package-manager-resolved WASIX runtime artifacts are +the release boundary for consumer applications. + +```toml +[dependencies] +oliphaunt-wasix = "0.1" +``` + + + + +### Open and query + +Create the WASM database handle, choose storage, run SQL, and close it when the +app no longer needs the runtime. + + + +```rust +use oliphaunt_wasix::Oliphaunt; + +fn main() -> anyhow::Result<()> { + let mut db = Oliphaunt::builder().temporary().open()?; + + let rows = db.query("SELECT 1::text AS value", &[], None)?; + let value = rows.rows[0]["value"].as_str(); + + db.close()?; + Ok(()) +} +``` + + + + +### Create app data + +Use the WASM API for local SQL work in supported WASIX hosts: + +```rust +db.query( + r#" + CREATE TABLE IF NOT EXISTS notes ( + id bigserial PRIMARY KEY, + title text NOT NULL, + body text NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() + ) + "#, + &[], + None, +)?; + +db.query( + "INSERT INTO notes (title, body) VALUES ('First note', 'Stored in a WASM PostgreSQL root')", + &[], + None, +)?; + +let notes = db.query("SELECT id, title FROM notes ORDER BY id DESC LIMIT 20", &[], None)?; +``` + +Use dump/restore when moving data between WASM hosts or across runtime +families. + + + + +### Configure + +Configure persistent or temporary storage, runtime mode, durability, and +import/export paths through the WASM API. + + + + +### Choose a mode + +WASM runtime behavior differs from native runtime behavior. Use the runtime page +for storage, proxy/server compatibility, and host persistence details. + + + + +### Handle lifecycle + +Persistent roots are locked while open. A second open against the same root +returns a lock error so one runtime owns the directory at a time. + + + + +### Select extensions + +Install exact WASIX extension packages for the SQL extensions your app uses. The +base runtime package does not include optional extension payloads. + + + + +### Back up, dump, and restore + +Use SDK APIs for backup/restore and dump/restore flows. Prefer dump/restore when +moving data across runtimes or versions. + + + + + + +## Troubleshooting + +Check storage backend behavior, runtime asset availability, target AOT asset +selection, extension artifacts, host capability errors, and SQLSTATE-bearing +PostgreSQL errors. diff --git a/src/docs/content/sdk/wasm/index.mdx b/src/docs/content/sdk/wasm/index.mdx new file mode 100644 index 00000000..c891823d --- /dev/null +++ b/src/docs/content/sdk/wasm/index.mdx @@ -0,0 +1,74 @@ +--- +title: WASM SDK +description: First-class WASM/WASIX runtime family for WebAssembly hosts. +--- + + + +`oliphaunt-wasix` is the first-class WASM/WASIX runtime family. It shares public +concepts with native Oliphaunt packages while keeping its own runtime assets, +WASM packaging story, and WebAssembly-specific behavior. + +Use this SDK when the deployment target is a WASM/WASIX host or when a Rust app +wants to ship the WASM runtime assets. Native desktop and mobile apps use the +native SDKs when the native runtime is available. + +## Install + +Use the WASM package when your deployment target is WebAssembly/WASIX. The +package includes the SDK surface. Consumer applications receive the matching +portable runtime and target AOT artifacts through package-manager dependencies. + +For Rust hosts: + +```sh +cargo add oliphaunt-wasix +``` + +## Open And Query + +Open a local server-compatible runtime when you want to use existing PostgreSQL +clients: + +```rust +use oliphaunt_wasix::OliphauntServer; + +fn open_server() -> Result<(), Box> { + let server = OliphauntServer::builder() + .path("./app-data/main.oliphaunt") + .start()?; + + let url = server.database_url(); + server.shutdown()?; + Ok(()) +} +``` + +## Runtime Shape + +WASM mode is separate from native direct, native broker, and native server. It +has its own filesystem, startup, server/proxy, dump/restore, and extension +behavior. Use capabilities from the selected WASM runtime before enabling +advanced features in app UI. + +The WASM product can expose direct Rust APIs or a local PostgreSQL-compatible +URL depending on the host and configuration. It keeps runtime assets separate +from native `liboliphaunt` while sharing exact extension names and public +database concepts. + +## App Responsibilities + +- Depend on the portable runtime artifact and target-specific AOT artifact for + each supported host. +- Install exact WASIX extension packages for the SQL extensions your app uses. +- Use the WASM runtime guide for filesystem, server/proxy, and startup behavior. +- Use dump and restore flows for portable data movement and upgrades. +- Check capabilities before enabling extension, streaming, server/proxy, or + backup features. + +## First Query + +Use [Build With WASM](/docs/sdk/wasm/guide) for the first query and runtime +configuration. Use [WASM runtime](/docs/sdk/wasm/runtime) for server/proxy and +asset behavior, and [dump and restore](/docs/sdk/wasm/dump-restore) for data +movement. diff --git a/src/docs/content/sdk/wasm/runtime.mdx b/src/docs/content/sdk/wasm/runtime.mdx new file mode 100644 index 00000000..f2672de7 --- /dev/null +++ b/src/docs/content/sdk/wasm/runtime.mdx @@ -0,0 +1,122 @@ +--- +title: WASIX Runtime Guide +description: Understand WASM runtime modes, persistence choices, root locking, startup, supported targets, and server compatibility. +--- + +# WASIX Runtime Guide + +`oliphaunt-wasix` is the WASM/WASIX runtime family. It shares Oliphaunt's public +concepts with native SDKs while using WASIX runtime assets, WASM host targets, +and WebAssembly-specific persistence behavior. + + + +## Choose A Mode + +Use `Oliphaunt` when your Rust code owns the database calls: + +- direct function and method calls; +- no socket listener; +- best fit for tests, commands, jobs, and app-owned Rust state. + +Use `OliphauntServer` when a library expects a PostgreSQL URI: + +- SQLx, Diesel, SeaORM, `tokio-postgres`, or cross-language clients; +- local TCP or Unix socket listener; +- compatibility with existing Postgres clients inside the selected WASM host. + +Both modes share the same embedded backend for a root. + +## Persistence Modes + +Direct and server builders expose the same root choices: + +- `path(...)` for a persistent database under an explicit directory; +- `app(...)` or `app_id(...)` for a persistent database under app data; +- `temporary()` for a cached temporary database; +- `fresh_temporary()` for a brand-new cluster path. + +Choose `temporary()` for most tests. Choose `fresh_temporary()` when the test +needs a brand-new cluster and can pay the slower startup path. + +## Operational Limits + +The WASM runtime model owns one embedded backend per open root: + +- one `Oliphaunt` instance owns one embedded backend; +- one `OliphauntServer` exposes one embedded backend; +- downstream client pools use one connection; +- server mode gives local PostgreSQL client compatibility inside the selected + WASM host. + +Generated server URLs include `sslmode=disable`. `CancelRequest` and normal +startup packets are supported. The server-compatible API still runs against the +embedded WASM backend for that root. + +## Root Locking And Lifecycle + +Persistent roots are locked while open. A second direct or server open against +the same root returns a lock error so one runtime owns the directory at a time. + +Close database clients before calling `OliphauntServer::shutdown()`. The server +thread waits for active client work to finish before exiting. + +Use `dump_data_dir()`, `load_data_dir_archive(...)`, or `try_clone()` for a +same-version physical clone. Use logical dumps through `pg_dump` for portable +exports and upgrades. + +## Startup And Preload + +The WASIX runtime opens only when the application package contains the matching +portable runtime artifact and the target-specific AOT artifact. Public consumer +builds receive those artifacts through package-manager dependencies, not through +runtime downloads or app-owned archive environment variables. + +The crate exposes two preload hooks: + +```rust,no_run +use oliphaunt_wasix::Oliphaunt; + +fn main() -> Result<(), Box> { + Oliphaunt::preload()?; + Ok(()) +} +``` + +Call `preload` before a visible startup path when you want to warm the shipped +runtime and target AOT artifact. + +Startup configuration belongs on the builders: + +- `postgres_config(...)` for PostgreSQL GUCs; +- `username(...)` and `database(...)` for the session target; +- `relaxed_durability(true)` for cacheable local workloads; +- `startup_arg(...)` for advanced startup arguments. + +## Supported Targets + +Release assets are built for: + +- macOS arm64; +- Linux x64; +- Linux arm64; +- Windows x64. + +Read capabilities and package reports before enabling target-specific UI. A host +target needs matching shipped runtime assets for the WASM runtime to open. + +Browser, worker, and mobile app topics belong to their platform SDK pages. +`oliphaunt-wasix` is the Rust package for local embedded and desktop/server WASM +hosts. + +## Server-Compatible Access + +Reach for `OliphauntServer` when you need client-library compatibility: + +- SQLx migrations and query APIs; +- ORMs that expect a PostgreSQL URI; +- Python, Go, or Node clients in local app tests; +- local tools that already speak the Postgres wire protocol. + +Reach for `Oliphaunt` when you control the Rust call site. It avoids the socket +layer and keeps the API surface smaller. diff --git a/src/docs/content/start/index.mdx b/src/docs/content/start/index.mdx new file mode 100644 index 00000000..b9337b45 --- /dev/null +++ b/src/docs/content/start/index.mdx @@ -0,0 +1,36 @@ +--- +sidebar_position: 1 +title: Start With Oliphaunt +description: Choose an Oliphaunt SDK, open an embedded PostgreSQL root, and run the first query. +--- + +Oliphaunt is embedded PostgreSQL for apps. Install the SDK for your app target, +open a PostgreSQL root inside app-owned storage, run SQL, and select only the +extensions your app ships. + +This page is the shortest tutorial path. Pick one app target, prove the runtime +with one query, then move into the SDK, runtime, extension, or storage page that +matches the next decision. + +## Start In One App Target + + + +## First Query Shape + +Every SDK uses ecosystem-native syntax. The application flow stays the same: +choose storage, select a runtime mode, select exact extensions, open, query, +and close or detach through the SDK lifecycle. + + + +Use the SDK page for your app target when you need platform setup, native build +consequences, lifecycle hooks, extension artifacts, backup, restore, and +troubleshooting. + +## After The First Query + +Use these pages when the first runtime path works and the app needs the next +production decision. + + diff --git a/src/docs/docs-manifest.toml b/src/docs/docs-manifest.toml new file mode 100644 index 00000000..003a012f --- /dev/null +++ b/src/docs/docs-manifest.toml @@ -0,0 +1,323 @@ +schema_version = 1 +generated_root = "target/docs" +site_docs_root = "target/docs/site-docs" +static_root = "target/docs/static" + +[api_reference] +generated_root = "target/docs/generated/api" +summary = "target/docs/generated/api/summary.json" +strict_env = "OLIPHAUNT_DOCS_REQUIRE_NATIVE_API" + +[api_reference.c] +header = "src/runtimes/liboliphaunt/native/include/oliphaunt.h" +doxygen_config = "src/docs/reference/doxygen/Doxyfile" + +[api_reference.rust] +package = "oliphaunt" +docs_entry = "target/docs/generated/api/rust/doc/oliphaunt/index.html" + +[api_reference.swift] +package_path = "src/sdks/swift" +target = "Oliphaunt" +docs_entry = "target/docs/generated/api/swift/Oliphaunt.doccarchive" + +[api_reference.kotlin] +project_path = "src/sdks/kotlin" +task = ":oliphaunt:dokkaGeneratePublicationHtml" +docs_entry = "target/docs/generated/api/kotlin/html/index.html" + +[api_reference.typescript] +package_path = "src/sdks/js" +docs_entry = "target/docs/generated/api/typescript/html/index.html" + +[api_reference.react_native] +package_path = "src/sdks/react-native" +docs_entry = "target/docs/generated/api/react-native/html/index.html" + +[api_reference.wasm] +package = "oliphaunt-wasix" +docs_entry = "target/docs/generated/api/wasm/doc/oliphaunt_wasix/index.html" + +[[routes]] +id = "start" +title = "Start" +kind = "public" +route = "start" +source = "src/docs/content/start" +public = true +version_source = "current" +reference_kind = "none" +tested_snippet_path = "" +tested_snippet_marker = "" +page_order = ["index"] + +[[routes]] +id = "sdk" +title = "SDKs" +kind = "public" +route = "sdk" +source = "src/docs/content/sdk" +public = true +version_source = "current" +reference_kind = "none" +tested_snippet_path = "" +tested_snippet_marker = "" +page_order = ["index"] + +[[routes]] +id = "learn" +title = "Learn" +kind = "public" +route = "learn" +source = "src/docs/content/learn" +public = true +version_source = "current" +reference_kind = "none" +tested_snippet_path = "" +tested_snippet_marker = "" +page_order = [ + "index", + "embedded-postgres", + "native-runtime", + "mobile-stability", + "sqlite-upgrade", + "tauri", +] + +[[routes]] +id = "reference" +title = "Reference" +kind = "public" +route = "reference" +source = "src/docs/content/reference" +public = true +version_source = "current" +reference_kind = "none" +tested_snippet_path = "" +tested_snippet_marker = "" +page_order = [ + "index", + "sdk-products", + "capabilities", + "extensions", + "performance", + "releases", + "version-matrix", + "extension-catalog", + "api-reference", +] +sidebar_pages = [ + "index", + "capabilities", + "extensions", + "performance", +] + +[[routes]] +id = "oliphaunt-rust" +product_id = "oliphaunt-rust" +title = "Rust SDK" +kind = "sdk" +route = "sdk/rust" +source = "src/docs/content/sdk/rust" +public = true +version_source = "src/sdks/rust/release.toml:id" +reference_kind = "rustdoc" +tested_snippet_path = "src/sdks/rust/tests/sdk_shape.rs" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET rust-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", +] +reference_artifact = "target/docs/generated/api/rust/doc/oliphaunt/index.html" +snippet_language = "rust" + +[[routes]] +id = "oliphaunt-swift" +product_id = "oliphaunt-swift" +title = "Swift SDK" +kind = "sdk" +route = "sdk/swift" +source = "src/docs/content/sdk/swift" +public = true +version_source = "src/sdks/swift/release.toml:id" +reference_kind = "swift-docc" +tested_snippet_path = "src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET swift-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", +] +reference_artifact = "target/docs/generated/api/swift/Oliphaunt.doccarchive" +snippet_language = "swift" + +[[routes]] +id = "oliphaunt-kotlin" +product_id = "oliphaunt-kotlin" +title = "Kotlin SDK" +kind = "sdk" +route = "sdk/kotlin" +source = "src/docs/content/sdk/kotlin" +public = true +version_source = "src/sdks/kotlin/release.toml:id" +reference_kind = "dokka" +tested_snippet_path = "src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt" +tested_snippet_marker = "liboliphaunt-doc-example:kotlin-typed-query" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", +] +reference_artifact = "target/docs/generated/api/kotlin/html/index.html" +snippet_language = "kotlin" + +[[routes]] +id = "oliphaunt-react-native" +product_id = "oliphaunt-react-native" +title = "React Native SDK" +kind = "sdk" +route = "sdk/react-native" +source = "src/docs/content/sdk/react-native" +public = true +version_source = "src/sdks/react-native/release.toml:id" +reference_kind = "typedoc" +tested_snippet_path = "src/sdks/react-native/src/__tests__/client.test.ts" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET react-native-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "architecture", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", + "architecture", +] +reference_artifact = "target/docs/generated/api/react-native/html/index.html" +snippet_language = "typescript" + +[[routes]] +id = "oliphaunt-js" +product_id = "oliphaunt-js" +title = "TypeScript SDK" +kind = "sdk" +route = "sdk/typescript" +source = "src/docs/content/sdk/typescript" +public = true +version_source = "src/sdks/js/release.toml:id" +reference_kind = "typedoc" +tested_snippet_path = "src/sdks/js/src/__tests__/client.test.ts" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET typescript-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", +] +reference_artifact = "target/docs/generated/api/typescript/html/index.html" +snippet_language = "typescript" + +[[routes]] +id = "oliphaunt-wasix" +product_id = "oliphaunt-wasix-rust" +title = "WASM SDK" +kind = "sdk" +route = "sdk/wasm" +source = "src/docs/content/sdk/wasm" +public = true +version_source = "src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml:id" +reference_kind = "rustdoc" +tested_snippet_path = "src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/runtime_smoke.rs" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET wasm-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "runtime", + "dump-restore", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", + "runtime", + "dump-restore", +] +reference_artifact = "target/docs/generated/api/wasm/doc/oliphaunt_wasix/index.html" +snippet_language = "rust" + +[[routes]] +id = "liboliphaunt-native" +product_id = "liboliphaunt-native" +title = "C ABI" +kind = "sdk" +route = "sdk/c-abi" +source = "src/docs/content/sdk/c-abi" +public = true +version_source = "src/runtimes/liboliphaunt/native/release.toml:id" +reference_kind = "doxygen" +tested_snippet_path = "src/runtimes/liboliphaunt/native/smoke/liboliphaunt_smoke.c" +tested_snippet_marker = "OLIPHAUNT_DOCS_SNIPPET liboliphaunt-quickstart" +required_pages = [ + "index", + "guide", + "api-reference", +] +page_order = [ + "index", + "guide", + "api-reference", +] +sidebar_pages = [ + "index", + "guide", +] +reference_artifact = "target/docs/generated/api/c/xml" +snippet_language = "c" diff --git a/src/docs/moon.yml b/src/docs/moon.yml new file mode 100644 index 00000000..c47230db --- /dev/null +++ b/src/docs/moon.yml @@ -0,0 +1,143 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "docs" +language: "typescript" +layer: "application" +stack: "frontend" +tags: ["docs", "fumadocs", "next", "public"] +dependsOn: + - id: "postgres18" + scope: "build" + - id: "source-toolchains" + scope: "build" + - id: "third-party-shared" + scope: "build" + - id: "third-party-native" + scope: "build" + - id: "third-party-wasix" + scope: "build" + - id: "extensions" + scope: "build" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + - id: "oliphaunt-rust" + scope: "build" + - id: "oliphaunt-swift" + scope: "build" + - id: "oliphaunt-kotlin" + scope: "build" + - id: "oliphaunt-react-native" + scope: "build" + - id: "oliphaunt-js" + scope: "build" + - id: "oliphaunt-wasix-rust" + scope: "build" + +project: + title: "Oliphaunt Docs" + description: "Public documentation site, generated SDK matrices, tested snippets, and release-readiness docs gates." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/docs" + paths: + "**/*.md": ["@oliphaunt/docs"] + "**/*.ts": ["@oliphaunt/docs"] + "**/*.tsx": ["@oliphaunt/docs"] + "docs-manifest.toml": ["@oliphaunt/docs"] + +tasks: + dev: + tags: ["dev"] + command: "pnpm --dir src/docs run dev" + inputs: + - "/docs/**/*" + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/**/release.toml" + - "/src/docs/**/*" + - "/src/extensions/**/*" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: false + check: + tags: ["quality", "static"] + command: "pnpm --dir src/docs run check" + inputs: + - "/README.md" + - "/.release-please-manifest.json" + - "/docs/**/*" + - "/release-please-config.json" + - "/src/*/README.md" + - "/src/*/typedoc.json" + - "/src/**/release.toml" + - "/src/docs/**/*" + - "/src/extensions/**/*" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + options: + cache: true + runFromWorkspaceRoot: true + build: + tags: ["build", "package"] + command: "pnpm --dir src/docs run build" + deps: + - "docs:check" + inputs: + - "/docs/**/*" + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/**/release.toml" + - "/src/docs/**/*" + - "/src/extensions/**/*" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + outputs: + - "/target/docs/build/**/*" + options: + cache: local + runFromWorkspaceRoot: true + smoke: + tags: ["runtime", "smoke"] + command: "pnpm --dir src/docs run smoke" + deps: + - target: "docs:build" + cacheStrategy: "outputs" + inputs: + - "/target/docs/build/**/*" + - "/target/docs/static/**/*" + - "/src/docs/tools/smoke-built-site.mjs" + outputs: + - "/target/docs/build/**/*" + options: + cache: local + runFromWorkspaceRoot: true + release-check: + tags: ["release", "package"] + command: "pnpm --dir src/docs run release-check" + deps: + - target: "docs:build" + cacheStrategy: "outputs" + inputs: + - "/README.md" + - "/.release-please-manifest.json" + - "/docs/**/*" + - "/release-please-config.json" + - "/src/*/CHANGELOG.md" + - "/src/*/typedoc.json" + - "/src/**/release.toml" + - "/src/docs/**/*" + - "/src/extensions/**/*" + - "/package.json" + - "/pnpm-lock.yaml" + - "/pnpm-workspace.yaml" + options: + cache: local + runFromWorkspaceRoot: true diff --git a/src/docs/next.config.mjs b/src/docs/next.config.mjs new file mode 100644 index 00000000..c70e9bb5 --- /dev/null +++ b/src/docs/next.config.mjs @@ -0,0 +1,16 @@ +import { createMDX } from 'fumadocs-mdx/next'; + +const withMDX = createMDX(); + +/** @type {import('next').NextConfig} */ +const config = { + output: 'export', + reactStrictMode: true, + trailingSlash: true, + images: { + unoptimized: true, + }, + basePath: process.env.OLIPHAUNT_DOCS_BASE_PATH || undefined, +}; + +export default withMDX(config); diff --git a/src/docs/package.json b/src/docs/package.json new file mode 100644 index 00000000..397db38d --- /dev/null +++ b/src/docs/package.json @@ -0,0 +1,46 @@ +{ + "name": "@oliphaunt/docs", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "node tools/run-docs-task.mjs generate && next dev --hostname 127.0.0.1", + "generate": "node tools/run-docs-task.mjs generate", + "api-reference": "node tools/generate-api-reference.mjs --mode=release", + "api-reference:check": "pnpm run api-reference && node tools/check-docs-product.mjs --api-reference", + "check": "node tools/run-docs-task.mjs check", + "build": "node tools/run-docs-task.mjs build", + "smoke": "node tools/smoke-built-site.mjs", + "release-check": "node tools/run-docs-task.mjs release-check", + "start": "next start", + "types:check": "node tools/run-docs-task.mjs check", + "postinstall": "fumadocs-mdx", + "lint": "biome check", + "format": "biome format --write" + }, + "dependencies": { + "@mdx-js/react": "^3.1.0", + "@oliphaunt/react-native": "workspace:*", + "@oliphaunt/ts": "workspace:*", + "clsx": "^2.1.1", + "fumadocs-core": "16.9.3", + "fumadocs-mdx": "15.0.10", + "fumadocs-ui": "16.9.3", + "lucide-react": "^1.17.0", + "next": "16.2.7", + "react": "19.2.7", + "react-dom": "19.2.7", + "smol-toml": "^1.4.2", + "tailwind-merge": "^3.6.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.3.0", + "@types/mdx": "^2.0.13", + "@types/node": "22.19.19", + "@types/react": "19.2.16", + "@types/react-dom": "^19.2.3", + "postcss": "^8.5.15", + "tailwindcss": "4.3.0", + "typescript": "^5.9.3", + "@biomejs/biome": "^2.4.16" + } +} diff --git a/src/docs/postcss.config.mjs b/src/docs/postcss.config.mjs new file mode 100644 index 00000000..297374d8 --- /dev/null +++ b/src/docs/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; + +export default config; diff --git a/src/docs/proxy.ts b/src/docs/proxy.ts new file mode 100644 index 00000000..22426816 --- /dev/null +++ b/src/docs/proxy.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation'; +import { docsContentRoute, docsRoute } from '@/lib/shared'; + +const { rewrite: rewriteDocs } = rewritePath( + `${docsRoute}{/*path}`, + `${docsContentRoute}{/*path}/content.md`, +); +const { rewrite: rewriteSuffix } = rewritePath( + `${docsRoute}{/*path}.md`, + `${docsContentRoute}{/*path}/content.md`, +); + +export default function proxy(request: NextRequest) { + const result = rewriteSuffix(request.nextUrl.pathname); + if (result) { + return NextResponse.rewrite(new URL(result, request.nextUrl.href)); + } + + if (isMarkdownPreferred(request)) { + const result = rewriteDocs(request.nextUrl.pathname); + + if (result) { + return NextResponse.rewrite(new URL(result, request.nextUrl.href)); + } + } + + return NextResponse.next(); +} diff --git a/src/docs/public/img/favicon.svg b/src/docs/public/img/favicon.svg new file mode 100644 index 00000000..659180b0 --- /dev/null +++ b/src/docs/public/img/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/docs/reference/doxygen/Doxyfile b/src/docs/reference/doxygen/Doxyfile new file mode 100644 index 00000000..54ba382a --- /dev/null +++ b/src/docs/reference/doxygen/Doxyfile @@ -0,0 +1,15 @@ +PROJECT_NAME = "Oliphaunt C ABI" +OUTPUT_DIRECTORY = target/docs/generated/api/c/doxygen +INPUT = src/runtimes/liboliphaunt/native/include/oliphaunt.h +FILE_PATTERNS = *.h +RECURSIVE = NO +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml +QUIET = YES +WARN_IF_UNDOCUMENTED = NO +EXTRACT_ALL = YES +EXTRACT_STATIC = NO +MACRO_EXPANSION = YES +PREDEFINED = __cplusplus= diff --git a/src/docs/source.config.ts b/src/docs/source.config.ts new file mode 100644 index 00000000..d5990627 --- /dev/null +++ b/src/docs/source.config.ts @@ -0,0 +1,23 @@ +import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; +import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; +import indexFile from 'fumadocs-mdx/plugins/index-file'; + +export const docs = defineDocs({ + dir: '../../target/docs/site-docs', + docs: { + schema: pageSchema, + postprocess: { + includeProcessedMarkdown: true, + }, + }, + meta: { + schema: metaSchema, + }, +}); + +export default defineConfig({ + plugins: [indexFile()], + mdxOptions: { + // Keep MDX options centralized here; generated docs stay in target/. + }, +}); diff --git a/src/docs/src/app/(home)/layout.tsx b/src/docs/src/app/(home)/layout.tsx new file mode 100644 index 00000000..77379fac --- /dev/null +++ b/src/docs/src/app/(home)/layout.tsx @@ -0,0 +1,6 @@ +import { HomeLayout } from 'fumadocs-ui/layouts/home'; +import { baseOptions } from '@/lib/layout.shared'; + +export default function Layout({ children }: LayoutProps<'/'>) { + return {children}; +} diff --git a/src/docs/src/app/(home)/page.tsx b/src/docs/src/app/(home)/page.tsx new file mode 100644 index 00000000..99f51345 --- /dev/null +++ b/src/docs/src/app/(home)/page.tsx @@ -0,0 +1,231 @@ +import { + ArrowRight, + Database, + ListChecks, + PackageCheck, + PlayCircle, + Route, + SearchCheck, +} from 'lucide-react'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { runtimeModes, sdkSurfaces } from '@/lib/docs-data'; + +const maintainerPaths = [ + { + title: 'Releases', + description: 'Version matrix, release notes, and artifact compatibility.', + href: '/docs/reference/releases', + icon: ListChecks, + }, + { + title: 'Extensions', + description: 'SQL extension names, dependencies, targets, and packaging policy.', + href: '/docs/reference/extensions', + icon: SearchCheck, + }, + { + title: 'API surfaces', + description: 'Language API maps plus the C ABI route for SDK bindings.', + href: '/docs/reference/api-reference', + icon: Route, + }, + { + title: 'Performance', + description: 'Workload results, footprint notes, and claim evidence.', + href: '/docs/reference/performance', + icon: Database, + }, +]; + +const productSignals = [ + { label: 'SDK surfaces', value: '7' }, + { label: 'Runtime families', value: '4' }, + { label: 'First query', value: '1' }, +]; + +function ArrowLink({ href, children }: { href: string; children: ReactNode }) { + return ( + + {children} + + + ); +} + +export default function HomePage() { + return ( +
+
+
+
+
+ Oliphaunt + Docs + PostgreSQL in app storage +
+

+ Oliphaunt +

+

+ Polyglot docs for embedded PostgreSQL SDKs, runtime modes, exact + extension packaging, and app-owned data movement. +

+
+ + + Start with the docs + + + + Choose an SDK + +
+
+ {productSignals.map((signal) => ( +
+

+ {signal.value} +

+

+ {signal.label} +

+
+ ))} +
+
+ +
+
+ Start path + SDK to runtime to verify +
+
+
+ {runtimeModes.map((mode, index) => { + const Icon = mode.icon; + + return ( + + + {String(index + 1).padStart(2, '0')} + + + {mode.name} + + ); + })} +
+ +
+
+

Choose

+ SDK +
+
+

Configure

+ Runtime +
+
+

Verify

+ Query +
+
+
+
+
+
+ +
+
+
+

SDK surfaces

+

+ Choose by package and target. +

+

+ Every SDK keeps the same database intent while matching its + platform package manager, lifecycle, concurrency model, and build + artifacts. +

+
+
+ {sdkSurfaces.map((sdk) => { + const Icon = sdk.icon; + + return ( + + + + +
+

{sdk.title}

+
+ + {sdk.packageName} + +
+
+
+

+ {sdk.target} +

+
+
+ {sdk.modes.map((mode) => ( + + {mode} + + ))} +
+ + + ); + })} +
+
+
+ +
+
+
+
+

Reference paths

+

+ Keep release and tooling details close. +

+

+ Maintainers and SDK authors can jump straight to compatibility, + extension packaging, API surfaces, and performance evidence. +

+
+
+ {maintainerPaths.map((path) => { + const Icon = path.icon; + + return ( + + + + +
+

{path.title}

+

+ {path.description} +

+
+ + + ); + })} +
+
+
+
+
+ ); +} diff --git a/src/docs/src/app/api/search/route.ts b/src/docs/src/app/api/search/route.ts new file mode 100644 index 00000000..92631c74 --- /dev/null +++ b/src/docs/src/app/api/search/route.ts @@ -0,0 +1,9 @@ +import { source } from '@/lib/source'; +import { createFromSource } from 'fumadocs-core/search/server'; + +export const dynamic = 'force-static'; + +export const { GET } = createFromSource(source, { + // https://docs.orama.com/docs/orama-js/supported-languages + language: 'english', +}); diff --git a/src/docs/src/app/docs/[[...slug]]/page.tsx b/src/docs/src/app/docs/[[...slug]]/page.tsx new file mode 100644 index 00000000..e5078568 --- /dev/null +++ b/src/docs/src/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,106 @@ +import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source'; +import { + DocsBody, + DocsDescription, + DocsPage, + DocsTitle, + MarkdownCopyButton, + ViewOptionsPopover, +} from 'fumadocs-ui/layouts/docs/page'; +import { notFound } from 'next/navigation'; +import { getMDXComponents } from '@/components/mdx'; +import type { Metadata } from 'next'; +import { createRelativeLink } from 'fumadocs-ui/mdx'; + +function pageSlug(slug?: string[]) { + return slug && slug.length > 0 ? slug : ['start']; +} + +export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { + const params = await props.params; + const page = source.getPage(pageSlug(params.slug)); + if (!page) notFound(); + + const MDX = page.data.body; + const markdownUrl = getPageMarkdownUrl(page).url; + const compactHeader = page.slugs.length > 1; + const breadcrumbItems = [ + { label: 'Docs', href: '/docs' }, + ...page.slugs.map((slug, index) => ({ + label: slug + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '), + href: `/docs/${page.slugs.slice(0, index + 1).join('/')}`, + })), + ]; + + return ( + +
+ + {page.data.title} + + {page.data.description} + +
+ + +
+
+ + + +
+ ); +} + +export async function generateStaticParams() { + return [{ slug: [] }, ...source.generateParams()]; +} + +export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise { + const params = await props.params; + const page = source.getPage(pageSlug(params.slug)); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + openGraph: { + images: getPageImage(page).url, + }, + }; +} diff --git a/src/docs/src/app/docs/layout.tsx b/src/docs/src/app/docs/layout.tsx new file mode 100644 index 00000000..ea89ac8a --- /dev/null +++ b/src/docs/src/app/docs/layout.tsx @@ -0,0 +1,7 @@ +import { source } from '@/lib/source'; +import { DocsLayout } from 'fumadocs-ui/layouts/docs'; +import { baseOptions } from '@/lib/layout.shared'; + +export default function Layout({ children }: LayoutProps<'/docs'>) { + return {children}; +} diff --git a/src/docs/src/app/global.css b/src/docs/src/app/global.css new file mode 100644 index 00000000..69688304 --- /dev/null +++ b/src/docs/src/app/global.css @@ -0,0 +1,769 @@ +@import 'tailwindcss'; +@import 'fumadocs-ui/css/neutral.css'; +@import 'fumadocs-ui/css/preset.css'; + +@theme { + --font-sans: var(--font-oliphaunt-sans); + --font-mono: var(--font-oliphaunt-mono); + --color-fd-background: hsl(200, 20%, 98%); + --color-fd-foreground: hsl(176, 26%, 10%); + --color-fd-card: hsl(0, 0%, 100%); + --color-fd-card-foreground: hsl(176, 26%, 10%); + --color-fd-popover: hsl(0, 0%, 100%); + --color-fd-popover-foreground: hsl(176, 26%, 10%); + --color-fd-primary: hsl(154, 76%, 27%); + --color-fd-primary-foreground: hsl(0, 0%, 100%); + --color-fd-secondary: hsl(184, 21%, 92%); + --color-fd-secondary-foreground: hsl(176, 26%, 12%); + --color-fd-muted: hsl(190, 10%, 94%); + --color-fd-muted-foreground: hsl(178, 12%, 34%); + --color-fd-accent: hsl(39, 88%, 87%); + --color-fd-accent-foreground: hsl(176, 26%, 10%); + --color-fd-border: hsla(184, 13%, 42%, 0.26); + --color-fd-ring: hsl(154, 76%, 27%); + --color-oliphaunt-tech: hsl(189, 78%, 38%); +} + +.dark { + --color-fd-background: hsl(168, 22%, 6%); + --color-fd-foreground: hsl(47, 24%, 92%); + --color-fd-card: hsl(168, 18%, 9%); + --color-fd-card-foreground: hsl(47, 24%, 92%); + --color-fd-popover: hsl(168, 20%, 7%); + --color-fd-popover-foreground: hsl(47, 24%, 92%); + --color-fd-primary: hsl(157, 74%, 56%); + --color-fd-primary-foreground: hsl(168, 26%, 7%); + --color-fd-secondary: hsl(176, 14%, 15%); + --color-fd-secondary-foreground: hsl(47, 24%, 92%); + --color-fd-muted: hsl(171, 13%, 13%); + --color-fd-muted-foreground: hsl(47, 10%, 66%); + --color-fd-accent: hsl(38, 86%, 57%); + --color-fd-accent-foreground: hsl(168, 26%, 7%); + --color-fd-border: hsla(166, 14%, 54%, 0.28); + --color-fd-ring: hsl(157, 74%, 56%); + --color-oliphaunt-tech: hsl(184, 82%, 58%); +} + +html { + scrollbar-gutter: stable; +} + +body { + font-family: var(--font-oliphaunt-sans), ui-sans-serif, system-ui, sans-serif; + background: + linear-gradient( + 180deg, + color-mix(in oklab, var(--color-fd-background), var(--color-fd-primary) 5%) 0, + var(--color-fd-background) 420px + ), + var(--color-fd-background); + color: var(--color-fd-foreground); + font-feature-settings: + 'ss01' 1, + 'cv10' 1; + letter-spacing: 0; +} + +* { + box-sizing: border-box; +} + +*::before, +*::after { + box-sizing: border-box; +} + +code, +kbd, +pre, +samp { + font-family: var(--font-oliphaunt-mono), ui-monospace, SFMono-Regular, Menlo, monospace; +} + +html > body[data-scroll-locked] { + margin-right: 0px !important; + --removed-body-scroll-bar-size: 0px !important; +} + +#nd-sidebar { + background: + linear-gradient( + 180deg, + color-mix(in oklab, var(--color-fd-card), transparent 0%), + var(--color-fd-background) + ), + var(--color-fd-card); +} + +#nd-sidebar a, +#nd-sidebar button { + border-radius: 8px; +} + +[data-card] { + border-radius: 8px; +} + +.prose table { + font-size: 0.925rem; +} + +.prose .not-prose { + max-width: 100%; +} + +.prose .not-prose :where(a, div, pre, code) { + min-width: 0; +} + +.prose h2 { + margin-top: 2.2rem; + scroll-margin-top: 6rem; +} + +.prose h3 { + scroll-margin-top: 6rem; +} + +.prose a { + text-decoration-thickness: 0.08em; + text-underline-offset: 0.18em; +} + +.prose th { + background: var(--color-fd-muted); + color: var(--color-fd-foreground); + font-weight: 600; +} + +.prose td, +.prose th { + vertical-align: top; +} + +.prose :where(table) { + overflow-wrap: normal; +} + +@media (max-width: 640px) { + .prose { + overflow-wrap: anywhere; + } + + .prose table { + display: block; + max-width: 100%; + overflow-x: auto; + white-space: nowrap; + } +} + +.prose pre { + border: 0; + border-left: 2px solid color-mix(in oklab, var(--color-fd-primary), transparent 34%); + border-radius: 8px; + background: color-mix(in oklab, var(--color-fd-card), var(--color-fd-background) 18%); + box-shadow: none; +} + +.prose code:not(pre code) { + border-radius: 4px; + background: color-mix(in oklab, var(--color-fd-muted), transparent 20%); + padding: 0.08rem 0.26rem; + font-size: 0.88em; +} + +.oliphaunt-home-hero { + --color-fd-foreground: hsl(48, 24%, 94%); + --color-fd-muted-foreground: hsl(47, 8%, 66%); + --color-fd-card: hsl(166, 18%, 10%); + --color-fd-background: hsl(168, 22%, 7%); + --color-fd-border: hsla(160, 12%, 76%, 0.18); + + position: relative; + isolation: isolate; + color: var(--color-fd-foreground); + background: + linear-gradient( + 90deg, + color-mix(in oklab, var(--color-fd-border), transparent 58%) 1px, + transparent 1px + ), + linear-gradient( + 180deg, + color-mix(in oklab, var(--color-fd-border), transparent 68%) 1px, + transparent 1px + ), + linear-gradient(180deg, hsl(168, 22%, 7%), hsl(166, 20%, 8%)); + background-size: + 56px 56px, + 56px 56px, + auto; +} + +.oliphaunt-home-hero::before { + position: absolute; + inset: 0; + z-index: -1; + background: + repeating-linear-gradient( + 115deg, + transparent 0 10px, + color-mix(in oklab, var(--color-fd-primary), transparent 94%) 10px 11px + ), + linear-gradient(180deg, transparent, var(--color-fd-background)); + content: ''; + mask-image: linear-gradient(180deg, black, transparent 76%); +} + +.oliphaunt-section-kicker { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--color-fd-muted-foreground); + font-size: 0.8125rem; + font-weight: 500; +} + +.oliphaunt-section-kicker::before { + width: 0.55rem; + height: 0.55rem; + border: 1px solid var(--color-fd-primary); + background: color-mix(in oklab, var(--color-fd-accent), transparent 35%); + content: ''; +} + +.oliphaunt-rail-label { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.55rem; + color: var(--color-fd-muted-foreground); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; +} + +.oliphaunt-rail-label span { + display: inline-flex; + align-items: center; + gap: 0.55rem; +} + +.oliphaunt-rail-label span:not(:last-child)::after { + color: color-mix(in oklab, var(--color-fd-primary), var(--color-fd-muted-foreground) 50%); + content: '/'; +} + +.oliphaunt-primary-action, +.oliphaunt-secondary-action, +.oliphaunt-arrow-link { + display: inline-flex; + min-height: 2.6rem; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + transition: + transform 160ms ease, + border-color 160ms ease, + background-color 160ms ease, + color 160ms ease; +} + +.oliphaunt-primary-action { + border: 1px solid color-mix(in oklab, var(--color-fd-primary), black 8%); + background: var(--color-fd-primary); + color: var(--color-fd-primary-foreground); + padding: 0.55rem 1rem; +} + +.oliphaunt-secondary-action { + border: 1px solid var(--color-fd-border); + background: color-mix(in oklab, var(--color-fd-card), transparent 8%); + color: var(--color-fd-foreground); + padding: 0.55rem 1rem; +} + +.oliphaunt-arrow-link { + width: fit-content; + margin-top: 1.5rem; + color: var(--color-fd-foreground); +} + +.oliphaunt-primary-action:hover, +.oliphaunt-secondary-action:hover, +.oliphaunt-arrow-link:hover { + transform: translateY(-1px); +} + +.oliphaunt-primary-action:focus-visible, +.oliphaunt-secondary-action:focus-visible, +.oliphaunt-arrow-link:focus-visible, +.oliphaunt-runtime-chip:focus-visible, +.oliphaunt-maintainer-row:focus-visible { + outline: 2px solid var(--color-fd-ring); + outline-offset: 3px; +} + +.oliphaunt-product-visual { + overflow: hidden; + border: 1px solid var(--color-fd-border); + border-radius: 8px; + background: color-mix(in oklab, var(--color-fd-card), transparent 3%); + box-shadow: + inset 0 1px 0 color-mix(in oklab, white, transparent 82%), + 0 24px 70px color-mix(in oklab, var(--color-fd-foreground), transparent 92%); +} + +.oliphaunt-product-visual__bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid var(--color-fd-border); + background: color-mix(in oklab, var(--color-fd-muted), transparent 34%); + padding: 0.75rem 1rem; + color: var(--color-fd-muted-foreground); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.oliphaunt-product-visual__bar--desktop { + display: none; +} + +.oliphaunt-product-visual__body { + display: grid; + gap: 0; + padding: 1rem; +} + +.oliphaunt-runtime-stack { + display: grid; + gap: 1px; + overflow: hidden; + border: 1px solid var(--color-fd-border); + border-radius: 8px; + background: var(--color-fd-border); +} + +.oliphaunt-runtime-chip, +.oliphaunt-maintainer-row { + display: grid; + min-width: 0; + align-items: center; + gap: 0.85rem; + border-radius: 8px; + transition: + background-color 160ms ease, + border-color 160ms ease, + transform 160ms ease; +} + +.oliphaunt-runtime-chip { + grid-template-columns: auto auto minmax(0, 1fr); + border: 0; + border-radius: 0; + background: color-mix(in oklab, var(--color-fd-background), transparent 12%); + padding: 0.85rem 0.9rem; + color: var(--color-fd-muted-foreground); + font-size: 0.78rem; + font-weight: 600; +} + +.oliphaunt-runtime-chip__index { + color: color-mix(in oklab, var(--color-fd-primary), var(--color-fd-foreground) 18%); + font-size: 0.7rem; +} + +.oliphaunt-runtime-chip code { + overflow: hidden; + text-overflow: ellipsis; + color: inherit; + font-size: inherit; +} + +.oliphaunt-runtime-chip:hover, +.oliphaunt-maintainer-row:hover { + border-color: color-mix(in oklab, var(--color-fd-primary), var(--color-fd-border) 45%); + background: color-mix(in oklab, var(--color-fd-accent), var(--color-fd-card) 72%); +} + +.oliphaunt-mode-pill { + display: inline-flex; + align-items: center; + border: 1px solid var(--color-fd-border); + border-radius: 999px; + background: color-mix(in oklab, var(--color-fd-card), transparent 12%); + color: var(--color-fd-muted-foreground); + font-size: 0.68rem; + font-weight: 600; + line-height: 1; +} + +.oliphaunt-mode-pill { + padding: 0.32rem 0.48rem; +} + +.oliphaunt-visual-footer { + display: grid; + gap: 1px; + overflow: hidden; + border: 1px solid var(--color-fd-border); + border-radius: 8px; + background: var(--color-fd-border); + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.oliphaunt-visual-footer > div { + min-width: 0; + background: color-mix(in oklab, var(--color-fd-background), transparent 15%); + padding: 0.8rem; +} + +.oliphaunt-visual-footer p { + color: var(--color-fd-muted-foreground); + font-size: 0.68rem; + font-weight: 600; + text-transform: uppercase; +} + +.oliphaunt-visual-footer span { + display: block; + margin-top: 0.35rem; + overflow-wrap: anywhere; + color: var(--color-fd-foreground); + font-size: 0.82rem; + font-weight: 600; +} + +.oliphaunt-icon-tile { + display: inline-flex; + width: 2.25rem; + height: 2.25rem; + flex: 0 0 auto; + align-items: center; + justify-content: center; + border: 1px solid var(--color-fd-border); + border-radius: 8px; + background: + linear-gradient( + 135deg, + color-mix(in oklab, var(--color-fd-primary), transparent 84%), + color-mix(in oklab, var(--color-oliphaunt-tech), var(--color-fd-accent) 38%) + ), + var(--color-fd-background); + color: var(--color-fd-foreground); +} + +.oliphaunt-maintainer-row { + grid-template-columns: auto minmax(0, 1fr) auto; + border-radius: 0; + padding: 1rem; +} + +.oliphaunt-doc-page { + color: var(--color-fd-foreground); +} + +.oliphaunt-doc-header { + position: relative; + isolation: isolate; + margin-bottom: 2.25rem; + border-bottom: 1px solid color-mix(in oklab, var(--color-fd-border), transparent 18%); + padding: 0 0 1.5rem; +} + +.oliphaunt-doc-header::after { + content: none; +} + +.oliphaunt-doc-header__path { + margin: 0; + color: var(--color-fd-muted-foreground); + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; +} + +.oliphaunt-doc-header__path ol { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.5rem; + margin: 0; + padding: 0; + list-style: none; +} + +.oliphaunt-doc-header__path li { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.oliphaunt-doc-header__path li:not(:last-child)::after { + color: color-mix(in oklab, var(--color-fd-primary), var(--color-fd-muted-foreground) 50%); + content: '/'; +} + +.oliphaunt-doc-header__path a { + text-decoration: none; +} + +.oliphaunt-doc-header__path a:hover { + color: var(--color-fd-foreground); +} + +.oliphaunt-doc-title { + max-width: 50rem; + margin-top: 1rem !important; + margin-bottom: 0 !important; + font-size: 3rem !important; + line-height: 0.95 !important; +} + +.oliphaunt-doc-header--compact { + padding-bottom: 1.35rem; +} + +.oliphaunt-doc-header--compact .oliphaunt-doc-title { + font-size: 2.625rem !important; +} + +.oliphaunt-doc-header--compact .oliphaunt-doc-description { + max-width: 50rem; +} + +.oliphaunt-doc-description { + max-width: 42rem; + margin-top: 1.25rem !important; + margin-bottom: 0 !important; + color: var(--color-fd-muted-foreground); + font-size: 1.05rem !important; + line-height: 1.7 !important; +} + +.oliphaunt-doc-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1.25rem; +} + +.oliphaunt-doc-body { + --tw-prose-body: var(--color-fd-foreground); + --tw-prose-headings: var(--color-fd-foreground); + --tw-prose-links: var(--color-fd-foreground); + --tw-prose-bold: var(--color-fd-foreground); + font-size: 0.98rem; + line-height: 1.75; +} + +.oliphaunt-doc-body > :where(p, ul, ol) { + max-width: 42rem; +} + +.oliphaunt-doc-body > :where(h2) { + margin-top: 2.75rem; + font-size: 1.75rem; + line-height: 1.15; +} + +.oliphaunt-doc-body > :where(h3) { + font-size: 1.25rem; +} + +.oliphaunt-mdx-panel { + border-color: color-mix(in oklab, var(--color-fd-border), transparent 10%); + background: var(--color-fd-card); + box-shadow: none; +} + +.oliphaunt-mdx-panel__header { + background: color-mix(in oklab, var(--color-fd-muted), transparent 34%); +} + +.oliphaunt-mdx-icon { + background: + linear-gradient( + 135deg, + color-mix(in oklab, var(--color-fd-primary), transparent 86%), + color-mix(in oklab, var(--color-oliphaunt-tech), transparent 85%) + ), + var(--color-fd-background); +} + +.oliphaunt-inline-code { + display: inline-block; + max-width: 100%; + overflow-wrap: anywhere; + border-radius: 4px; + background: color-mix(in oklab, var(--color-fd-muted), transparent 18%); + padding: 0.08rem 0.3rem; + color: var(--color-fd-muted-foreground); + font-size: 0.78rem; +} + +.oliphaunt-code-block { + overflow-x: auto; + margin: 0.5rem 0 0; + border: 0; + border-left: 2px solid color-mix(in oklab, var(--color-fd-primary), transparent 34%); + border-radius: 8px; + background: color-mix(in oklab, var(--color-fd-card), var(--color-fd-background) 18%); + padding: 0.85rem 1rem; + color: var(--color-fd-foreground); + font-size: 0.78rem; + line-height: 1.7; +} + +.oliphaunt-code-block code { + display: block; + min-width: max-content; + background: transparent; + padding: 0; +} + +.oliphaunt-mdx-step-grid > div, +.oliphaunt-mdx-step-grid > li, +.oliphaunt-first-query .bg-fd-background { + min-width: 0; +} + +.oliphaunt-first-query pre { + min-height: 9.5rem; + margin: 0; +} + +.oliphaunt-query-example + .oliphaunt-query-example { + border-top: 1px solid var(--color-fd-border); +} + +@media (max-width: 639px) { + .oliphaunt-home-hero > div { + gap: 1.75rem; + padding-top: 2rem; + padding-bottom: 2rem; + } + + .oliphaunt-product-visual__body { + padding: 0.75rem; + } + + .oliphaunt-visual-footer { + margin-top: 1px; + } + + .oliphaunt-runtime-stack { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .oliphaunt-runtime-chip { + grid-template-columns: auto minmax(0, 1fr); + gap: 0.45rem; + padding: 0.55rem; + font-size: 0.68rem; + } + + .oliphaunt-runtime-chip svg { + display: none; + } + + .oliphaunt-product-visual { + margin-top: 0.5rem; + } + + .oliphaunt-doc-header { + padding: 0 0 1.35rem; + } + + .oliphaunt-doc-actions > * { + flex: 1 1 auto; + } + + .oliphaunt-first-query pre { + min-height: 0; + max-height: 18rem; + } +} + +@media (min-width: 640px) { + .oliphaunt-product-visual__bar--desktop { + display: flex; + } + + .oliphaunt-doc-header { + padding-bottom: 2rem; + } + + .oliphaunt-doc-header--compact { + padding-bottom: 1.7rem; + } + + .oliphaunt-doc-title { + font-size: 4.25rem !important; + } + + .oliphaunt-doc-header--compact .oliphaunt-doc-title { + font-size: 3.35rem !important; + } + + .oliphaunt-doc-description { + font-size: 1.125rem !important; + } +} + +@media (min-width: 1024px) { + .oliphaunt-doc-header { + padding-bottom: 2.4rem; + } + + .oliphaunt-doc-header--compact { + padding-bottom: 2rem; + } + + .oliphaunt-doc-title { + font-size: 5.35rem !important; + } + + .oliphaunt-doc-header--compact .oliphaunt-doc-title { + font-size: 4rem !important; + } + + .oliphaunt-product-visual__body { + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + } + + .oliphaunt-visual-footer { + border-left: 0; + border-radius: 0 8px 8px 0; + } + + .oliphaunt-runtime-stack { + border-radius: 8px 0 0 8px; + } + +} + +@media (prefers-reduced-motion: no-preference) { + .oliphaunt-product-visual { + animation: oliphaunt-panel-rise 520ms ease-out both; + } + +} + +@keyframes oliphaunt-panel-rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/docs/src/app/layout.tsx b/src/docs/src/app/layout.tsx new file mode 100644 index 00000000..8d224154 --- /dev/null +++ b/src/docs/src/app/layout.tsx @@ -0,0 +1,44 @@ +import { RootProvider } from 'fumadocs-ui/provider/next'; +import './global.css'; +import { IBM_Plex_Mono, IBM_Plex_Sans } from 'next/font/google'; +import type { Metadata } from 'next'; + +const plexSans = IBM_Plex_Sans({ + subsets: ['latin'], + weight: ['400', '500', '600', '700'], + variable: '--font-oliphaunt-sans', +}); + +const plexMono = IBM_Plex_Mono({ + subsets: ['latin'], + weight: ['400', '500', '600'], + variable: '--font-oliphaunt-mono', +}); + +export const metadata: Metadata = { + metadataBase: new URL(process.env.NEXT_PUBLIC_OLIPHAUNT_DOCS_URL ?? 'https://oliphaunt.dev'), + title: { + default: 'Oliphaunt Docs', + template: '%s | Oliphaunt', + }, + description: + 'Embedded PostgreSQL SDKs for Rust, Swift, Kotlin, React Native, TypeScript, and WASM apps.', + icons: { + icon: [{ url: '/img/favicon.svg', type: 'image/svg+xml' }], + shortcut: '/img/favicon.svg', + }, +}; + +export default function Layout({ children }: LayoutProps<'/'>) { + return ( + + + {children} + + + ); +} diff --git a/src/docs/src/app/llms-full.txt/route.ts b/src/docs/src/app/llms-full.txt/route.ts new file mode 100644 index 00000000..d494d2cb --- /dev/null +++ b/src/docs/src/app/llms-full.txt/route.ts @@ -0,0 +1,10 @@ +import { getLLMText, source } from '@/lib/source'; + +export const revalidate = false; + +export async function GET() { + const scan = source.getPages().map(getLLMText); + const scanned = await Promise.all(scan); + + return new Response(scanned.join('\n\n')); +} diff --git a/src/docs/src/app/llms.mdx/docs/[[...slug]]/route.ts b/src/docs/src/app/llms.mdx/docs/[[...slug]]/route.ts new file mode 100644 index 00000000..250181a7 --- /dev/null +++ b/src/docs/src/app/llms.mdx/docs/[[...slug]]/route.ts @@ -0,0 +1,23 @@ +import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source'; +import { notFound } from 'next/navigation'; + +export const revalidate = false; + +export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) { + const { slug } = await params; + const page = source.getPage(slug?.slice(0, -1)); + if (!page) notFound(); + + return new Response(await getLLMText(page), { + headers: { + 'Content-Type': 'text/markdown', + }, + }); +} + +export function generateStaticParams() { + return source.getPages().map((page) => ({ + lang: page.locale, + slug: getPageMarkdownUrl(page).segments, + })); +} diff --git a/src/docs/src/app/llms.txt/route.ts b/src/docs/src/app/llms.txt/route.ts new file mode 100644 index 00000000..fc80cb65 --- /dev/null +++ b/src/docs/src/app/llms.txt/route.ts @@ -0,0 +1,8 @@ +import { source } from '@/lib/source'; +import { llms } from 'fumadocs-core/source'; + +export const revalidate = false; + +export function GET() { + return new Response(llms(source).index()); +} diff --git a/src/docs/src/app/og/docs/[...slug]/route.tsx b/src/docs/src/app/og/docs/[...slug]/route.tsx new file mode 100644 index 00000000..877166d3 --- /dev/null +++ b/src/docs/src/app/og/docs/[...slug]/route.tsx @@ -0,0 +1,28 @@ +import { getPageImage, source } from '@/lib/source'; +import { notFound } from 'next/navigation'; +import { ImageResponse } from 'next/og'; +import { generate as DefaultImage } from 'fumadocs-ui/og'; +import { appName } from '@/lib/shared'; + +export const revalidate = false; + +export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) { + const { slug } = await params; + const page = source.getPage(slug.slice(0, -1)); + if (!page) notFound(); + + return new ImageResponse( + , + { + width: 1200, + height: 630, + }, + ); +} + +export function generateStaticParams() { + return source.getPages().map((page) => ({ + lang: page.locale, + slug: getPageImage(page).segments, + })); +} diff --git a/src/docs/src/components/mdx.tsx b/src/docs/src/components/mdx.tsx new file mode 100644 index 00000000..0a11907e --- /dev/null +++ b/src/docs/src/components/mdx.tsx @@ -0,0 +1,84 @@ +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Card, Cards } from 'fumadocs-ui/components/card'; +import { File, Files, Folder } from 'fumadocs-ui/components/files'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import type { MDXComponents } from 'mdx/types'; +import { + CapabilitySnapshot, + EmbeddedPostgresModel, + ExactExtensionRule, + ExtensionArtifactFlow, + FirstQueryFlow, + LearnRouteMap, + MobileStabilityContract, + ModeMatrix, + PerformanceResultsGrid, + QuickstartPath, + ReactNativeApproachTable, + ReactNativeBoundaryMap, + ReferenceLookup, + ReleaseLookup, + SdkGuideSummary, + SdkGuideProof, + SdkLanding, + SdkChooser, + ShipChecklist, + SqliteMigrationMap, + StartNextSteps, + StartOutcome, + TauriAppPattern, + VerifyChecklist, + WasmDataMovement, + WasmRuntimeMap, +} from './oliphaunt'; + +export function getMDXComponents(components?: MDXComponents) { + return { + ...defaultMdxComponents, + Callout, + Card, + Cards, + CapabilitySnapshot, + EmbeddedPostgresModel, + File, + Files, + Folder, + Step, + Steps, + Tab, + Tabs, + ExactExtensionRule, + ExtensionArtifactFlow, + FirstQueryFlow, + LearnRouteMap, + MobileStabilityContract, + ModeMatrix, + PerformanceResultsGrid, + QuickstartPath, + ReactNativeApproachTable, + ReactNativeBoundaryMap, + ReferenceLookup, + ReleaseLookup, + SdkGuideSummary, + SdkGuideProof, + SdkLanding, + SdkChooser, + ShipChecklist, + SqliteMigrationMap, + StartNextSteps, + StartOutcome, + TauriAppPattern, + VerifyChecklist, + WasmDataMovement, + WasmRuntimeMap, + ...components, + } satisfies MDXComponents; +} + +export const useMDXComponents = getMDXComponents; + +declare global { + type MDXProvidedComponents = ReturnType; +} diff --git a/src/docs/src/components/oliphaunt.tsx b/src/docs/src/components/oliphaunt.tsx new file mode 100644 index 00000000..a6be3e09 --- /dev/null +++ b/src/docs/src/components/oliphaunt.tsx @@ -0,0 +1,1822 @@ +import { + ArrowRight, + BookOpen, + CheckCircle2, + ClipboardCheck, + Database, + FileSearch, + Gauge, + GitBranch, + HardDriveDownload, + Layers, + ListChecks, + PackageCheck, + PlayCircle, + Route, + Settings2, + ShieldCheck, +} from 'lucide-react'; +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { runtimeModes, sdkSurfaces } from '@/lib/docs-data'; + +function SurfaceIcon({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function InlineCode({ children }: { children: ReactNode }) { + return {children}; +} + +function CodeBlock({ children }: { children: ReactNode }) { + return ( +
+      {children}
+    
+ ); +} + +function InstallSnippet({ children }: { children: string }) { + if (/^(add|use)\b/iu.test(children)) { + return

{children}

; + } + + return {children}; +} + +export function SdkChooser() { + return ( +
+ {sdkSurfaces.map((sdk) => { + const Icon = sdk.icon; + + return ( + + + + +
+

{sdk.title}

+
+ {sdk.packageName} +
+
+

{sdk.target}

+
+ {sdk.modes.map((mode) => ( + + {mode} + + ))} +
+ + + ); + })} +
+ ); +} + +const reactNativeApproaches = [ + { + approach: 'Expo development build', + install: 'npx expo install @oliphaunt/react-native', + nativeBuild: 'Run prebuild, then build iOS or Android.', + bestFor: 'Expo apps that include native modules, selected extensions, and development tooling.', + verify: 'Config plugin output, native module loading, JSI ArrayBuffer roundtrip.', + }, + { + approach: 'React Native New Architecture app', + install: 'npm install @oliphaunt/react-native', + nativeBuild: 'Run CocoaPods and Gradle after package or extension changes.', + bestFor: 'Existing RN apps that already own native projects and New Architecture builds.', + verify: 'Autolinking, Codegen, TurboModule availability, platform resource packaging.', + }, + { + approach: 'Platform-native app', + install: 'Use the Swift or Kotlin SDK directly.', + nativeBuild: 'Build through Xcode or Gradle without the RN package.', + bestFor: 'iOS, macOS, or Android apps without a React Native JavaScript surface.', + verify: 'Swift actor or Kotlin coroutine lifecycle, app storage, selected extensions.', + }, +]; + +export function ReactNativeApproachTable() { + return ( +
+
+
+ + + +
+

Choose the native app path first

+

+ Oliphaunt ships native runtime code. The JavaScript bundle can call it only after the + installed app binary includes the Swift and Kotlin pieces. +

+
+
+
+
+ {reactNativeApproaches.map((row) => ( +
+

{row.approach}

+
+

+ Install +

+ {row.install} +
+
+

+ Native build +

+

{row.nativeBuild}

+
+
+

+ Best for +

+

{row.bestFor}

+
+
+

+ Verify first +

+

{row.verify}

+
+
+ ))} +
+
+ ); +} + +const learnRoutes = [ + { + title: 'Embedded PostgreSQL', + href: '/docs/learn/embedded-postgres', + question: 'What does my app own on disk?', + answer: 'Roots, WAL, extensions, lifecycle, backup, and restore live behind SDK APIs.', + icon: HardDriveDownload, + }, + { + title: 'Native Runtime', + href: '/docs/learn/native-runtime', + question: 'Which runtime boundary fits my app?', + answer: 'Direct gives one embedded session, broker adds a helper process, and server gives PostgreSQL client sessions.', + icon: GitBranch, + }, + { + title: 'Mobile Stability', + href: '/docs/learn/mobile-stability', + question: 'How does this behave on iOS and Android?', + answer: 'Mobile direct mode covers app storage, foreground/background transitions, relaunch, and WAL recovery.', + icon: ShieldCheck, + }, + { + title: 'Moving From SQLite', + href: '/docs/learn/sqlite-upgrade', + question: 'What changes when I move from one file to PostgreSQL?', + answer: 'Storage, schema features, extension selection, export/import, and package-size checks change first.', + icon: Route, + }, + { + title: 'Tauri Usage', + href: '/docs/learn/tauri', + question: 'Where does the database handle live in a Tauri app?', + answer: 'Rust state owns Oliphaunt. The webview calls narrow app commands instead of raw runtime handles.', + icon: BookOpen, + }, +]; + +export function LearnRouteMap() { + return ( +
+
+
+

Read by decision

+

+ Each page answers one production question after the first query works. +

+
+
+
+

Storage

+

Root directory, WAL, backup.

+
+
+

Runtime

+

Direct, broker, server, WASM.

+
+
+

App fit

+

Mobile, Tauri, SQLite migration.

+
+
+
+
+ {learnRoutes.map((route) => { + const Icon = route.icon; + + return ( + +
+ + + +

{route.title}

+
+

{route.question}

+

{route.answer}

+ + + ); + })} +
+
+ ); +} + +const embeddedModelRows = [ + { + title: 'Root directory', + description: 'Live data is PostgreSQL storage and WAL inside an app-owned root directory.', + icon: HardDriveDownload, + }, + { + title: 'SDK lifecycle', + description: 'Open, query, background, close, backup, and restore stay behind platform APIs.', + icon: Route, + }, + { + title: 'Exact extensions', + description: 'Apps select SQL extension names before packaging or opening the database.', + icon: ShieldCheck, + }, + { + title: 'Runtime family', + description: 'Native SDKs and WASM share concepts while advertising their own capabilities.', + icon: Database, + }, +]; + +export function EmbeddedPostgresModel() { + return ( +
+
+
+

Embedded PostgreSQL model

+

+ Oliphaunt keeps PostgreSQL behavior and puts app-facing ownership in SDKs. +

+
+
+
+

Native family

+

+ Rust, Swift, Kotlin, React Native, TypeScript, and C ABI over native runtime assets. +

+
+
+

WASM family

+

+ First-class WASM/WASIX runtime family with separate assets and capabilities. +

+
+
+
+
+ {embeddedModelRows.map((row) => { + const Icon = row.icon; + + return ( +
+ + + +

{row.title}

+

{row.description}

+
+ ); + })} +
+
+ ); +} + +const mobileContractRows = [ + { + title: 'One resident backend', + description: 'Mobile direct mode owns one embedded PostgreSQL backend in the app process.', + }, + { + title: 'Serialized work', + description: 'Concurrent app tasks share one physical session through the platform SDK.', + }, + { + title: 'WAL recovery', + description: 'After process exit, the next launch reopens the root and PostgreSQL recovers storage.', + }, + { + title: 'Platform lifecycle', + description: 'SDK hooks prepare backgrounding, resume foreground work, and close handles cleanly.', + }, +]; + +export function MobileStabilityContract() { + return ( +
+
+

Mobile direct-mode contract

+

+ Use this model for iOS, Android, and React Native until the target advertises another + runtime mode. +

+
+
+ {mobileContractRows.map((row) => ( +
+ +

{row.title}

+

{row.description}

+
+ ))} +
+
+ ); +} + +const sqliteMigrationRows = [ + { + sqlite: 'One database file', + oliphaunt: 'PostgreSQL root directory', + action: 'Move data movement to SDK backup and restore APIs.', + }, + { + sqlite: 'Pragmas', + oliphaunt: 'PostgreSQL settings and durability profiles', + action: 'Choose startup and durability configuration through the SDK.', + }, + { + sqlite: 'SQLite extensions', + oliphaunt: 'Exact PostgreSQL extension names', + action: 'Select extensions before opening and verify package contents.', + }, + { + sqlite: 'Multiple library handles', + oliphaunt: 'Mode-specific sessions', + action: 'Use server mode when independent PostgreSQL clients are required.', + }, +]; + +export function SqliteMigrationMap() { + return ( +
+
+

Migration map

+

+ Start by replacing SQLite assumptions with PostgreSQL and SDK-owned app boundaries. +

+
+
+ {sqliteMigrationRows.map((row) => ( +
+

+ SQLite assumption +

+

{row.sqlite}

+

+ Oliphaunt model +

+

{row.oliphaunt}

+

+ Migration action +

+

{row.action}

+
+ ))} +
+
+
+ + + + + + + + + {sqliteMigrationRows.map((row) => ( + + + + + + ))} + +
SQLite assumptionOliphaunt modelMigration action
{row.sqlite}{row.oliphaunt}{row.action}
+ + + ); +} + +const referenceRows = [ + { + need: 'Choose an SDK package', + answer: 'Compare package names, app targets, runtime owners, and first verification steps.', + href: '/docs/reference/sdk-products', + label: 'SDKs And Platforms', + icon: ListChecks, + }, + { + need: 'Gate a feature in UI', + answer: 'Check direct, broker, server, streaming, backup, restore, and client-session capability.', + href: '/docs/reference/capabilities', + label: 'Capability Matrix', + icon: FileSearch, + }, + { + need: 'Ship one extension', + answer: 'Select exact SQL extension names and verify the app artifact contains only selected files.', + href: '/docs/reference/extensions', + label: 'Extensions', + icon: ShieldCheck, + }, + { + need: 'Look up exact extension support', + answer: 'Use the generated catalog for extension status, dependencies, and runtime availability.', + href: '/docs/reference/extension-catalog', + label: 'Extension Catalog', + icon: FileSearch, + }, + { + need: 'Read performance claims', + answer: 'Use the measurement guide for latency, throughput, package size, memory, and comparison scope.', + href: '/docs/reference/performance', + label: 'Performance', + icon: Gauge, + }, + { + need: 'Update an installed app', + answer: 'Match SDK versions, runtime artifacts, selected extensions, docs versions, and release notes.', + href: '/docs/reference/releases', + label: 'Releases', + icon: PackageCheck, + }, + { + need: 'Match versions', + answer: 'Use the generated version matrix for product compatibility and release contents.', + href: '/docs/reference/version-matrix', + label: 'Version Matrix', + icon: GitBranch, + }, + { + need: 'Find language API details', + answer: 'Use each SDK API map for open, query, lifecycle, capabilities, extensions, and backup calls.', + href: '/docs/reference/api-reference', + label: 'API Reference', + icon: BookOpen, + }, +]; + +export function ReferenceLookup() { + return ( +
+
+
+ + + +
+

Use Reference as a lookup surface

+

+ These pages answer specific product questions. Start with the question, then open the + smallest page that gives the exact answer. +

+
+
+
+
+ {referenceRows.map((row) => { + const Icon = row.icon; + + return ( + +
+ + + +

{row.need}

+
+

{row.answer}

+

{row.label}

+ + + ); + })} +
+
+ ); +} + +const releaseLookupRows = [ + { + question: 'Which package version fits my app?', + answer: 'Start with the SDK package, then check the runtime dependency it carries.', + href: '/docs/reference/version-matrix', + label: 'Version Matrix', + icon: PackageCheck, + }, + { + question: 'Which compatibility updates move together?', + answer: + 'Runtime changes can select dependent SDK releases, but every SDK keeps its own SemVer.', + href: '/docs/reference/sdk-products', + label: 'SDKs And Platforms', + icon: GitBranch, + }, + { + question: 'Which extensions can this release ship?', + answer: 'Check extension availability by SQL extension name and target runtime.', + href: '/docs/reference/extension-catalog', + label: 'Extension Catalog', + icon: FileSearch, + }, + { + question: 'Did performance or package size change?', + answer: 'Read release measurements by workload, target hardware, and selected extensions.', + href: '/docs/reference/performance', + label: 'Performance', + icon: Gauge, + }, +]; + +export function ReleaseLookup() { + return ( +
+
+
+ + + +
+

Read releases by the app artifact you ship

+

+ Match the SDK package, runtime artifacts, selected extensions, and performance notes + before updating an installed app. +

+
+
+
+
+ {releaseLookupRows.map((row) => { + const Icon = row.icon; + + return ( + +
+ + + +

{row.question}

+
+

{row.answer}

+

{row.label}

+ + + ); + })} +
+
+ ); +} + +const capabilityCards = [ + { + title: 'Direct mode', + value: 'one serialized session', + description: 'Use it when app code owns one embedded PostgreSQL root and latency matters.', + icon: Database, + }, + { + title: 'Broker mode', + value: 'helper-owned roots', + description: 'Use it when a desktop app wants process ownership, recovery, or multiple roots.', + icon: GitBranch, + }, + { + title: 'Server mode', + value: 'independent clients', + description: ( + <> + Use it for pools, ORMs, psql, pg_dump, and PostgreSQL + connection strings. + + ), + icon: ListChecks, + }, + { + title: 'Extension artifacts', + value: 'exact selection', + description: 'Use selected SQL extension names to decide what enters the app artifact.', + icon: ShieldCheck, + }, +]; + +export function CapabilitySnapshot() { + return ( +
+
+
+ + + +
+

Read capabilities before enabling a workflow

+

+ Capabilities describe what the installed SDK and runtime advertise on this target. +

+
+
+
+
+ {capabilityCards.map((card) => { + const Icon = card.icon; + + return ( +
+ + + +
+

{card.title}

+
+ {card.value} +
+

+ {card.description} +

+
+
+ ); + })} +
+
+ ); +} + +const extensionFlow = [ + { + title: 'Select SQL names', + description: ( + <> + Choose extension names such as vector in SDK configuration before opening. + + ), + }, + { + title: 'Resolve dependencies', + description: 'Include only dependencies declared by the selected extension metadata.', + }, + { + title: 'Package artifacts', + description: 'Swift, Kotlin, React Native, desktop, and WASM tooling package target artifacts.', + }, + { + title: 'Verify the app', + description: 'Report selected names, included files, dependency files, target, and package-size cost.', + }, +]; + +export function ExtensionArtifactFlow() { + return ( +
+
+

Extension packaging flow

+

+ The selector is the SQL extension name. Build tooling handles target artifacts and + dependency metadata. +

+
+
    + {extensionFlow.map((step, index) => ( +
  1. + + {index + 1} + +
    +

    {step.title}

    +

    + {step.description} +

    +
    +
  2. + ))} +
+
+ ); +} + +const performanceResults = [ + { + title: 'Open path', + metrics: 'cold open, warm open, first query', + description: 'Use these numbers for startup and resume behavior.', + }, + { + title: 'Interactive work', + metrics: 'simple query p50, p90, p99', + description: 'Use these numbers for UI reads, writes, and short transactions.', + }, + { + title: 'Bulk work', + metrics: 'batched insert, update, import', + description: 'Use these numbers for sync, preload, and local cache hydration.', + }, + { + title: 'Large reads', + metrics: 'stream setup, chunks, total time', + description: 'Use these numbers for reports, exports, and sync scans.', + }, + { + title: 'Footprint', + metrics: 'RSS, CPU, artifact size', + description: 'Use these numbers when mobile package size or desktop memory matters.', + }, + { + title: 'Data movement', + metrics: 'backup, restore, dump', + description: 'Use these numbers for user-visible export, import, and support flows.', + }, +]; + +export function PerformanceResultsGrid() { + return ( +
+
+
+ + + +
+

Use performance results by workload

+

+ A useful report names the app workload, runtime mode, selected extensions, target + hardware, and collection method. +

+
+
+
+
+ {performanceResults.map((item) => ( +
+

{item.title}

+
+ {item.metrics} +
+

{item.description}

+
+ ))} +
+
+ ); +} + +const tauriModeCards = [ + { + title: 'App commands own calls', + value: 'NativeDirect', + description: 'Use Rust state when Tauri commands own one app database and latency matters.', + icon: Database, + }, + { + title: 'Helper owns roots', + value: 'NativeBroker', + description: 'Use a broker when a desktop app wants process ownership or multiple roots.', + icon: GitBranch, + }, + { + title: 'Clients need a URL', + value: 'NativeServer', + description: ( + <> + Use server mode for pools, ORMs, psql, pg_dump, and + independent sessions. + + ), + icon: Route, + }, +]; + +export function TauriAppPattern() { + return ( +
+
+
+ + + +
+

Keep PostgreSQL ownership in Rust state

+

+ The webview calls app commands. Rust owns the database handle, root directory, + lifecycle, extension selection, and backup APIs. +

+
+
+
+
+ {tauriModeCards.map((card) => { + const Icon = card.icon; + + return ( +
+ + + +

{card.title}

+
+ {card.value} +
+

+ {card.description} +

+
+ ); + })} +
+
+ {[ + { + key: 'commands', + label: ( + <> + Expose narrow commands such as add_item or search_items. + + ), + }, + { key: 'roots', label: 'Keep roots, locks, and handles out of the webview.' }, + { key: 'backup', label: 'Use SDK backup and restore APIs for app import/export.' }, + ].map((item) => ( +
+ + {item.label} +
+ ))} +
+
+ ); +} + +const reactNativeBoundaryRows = [ + { + layer: 'TypeScript', + owns: 'API shape, handles, typed results, config plugin options, and lifecycle calls.', + transport: 'TurboModule for small calls; JSI ArrayBuffer for protocol bytes and chunks.', + }, + { + layer: 'Swift', + owns: 'Apple runtime resources, app storage, lifecycle, capabilities, backup, and restore.', + transport: 'Actor-owned native direct database handle on iOS and macOS targets.', + }, + { + layer: 'Kotlin', + owns: 'Android resources, ABI artifact selection, coroutine lifecycle, capabilities, backup, and restore.', + transport: 'Android facade over the Kotlin SDK database handle.', + }, +]; + +export function ReactNativeBoundaryMap() { + return ( +
+
+
+ + + +
+

React Native owns the JS boundary

+

+ Platform runtime behavior flows through Swift on Apple targets and Kotlin on + Android. JavaScript gets one consistent SDK surface over those native handles. +

+
+
+
+
+ + + + + + + + + + {reactNativeBoundaryRows.map((row) => ( + + + + + + ))} + +
LayerOwnsBoundary
{row.layer}{row.owns}{row.transport}
+
+
+ {[ + 'Use high-level query helpers for app code.', + 'Use raw protocol and streaming APIs for adapters and COPY-style flows.', + 'Read capabilities before enabling platform-specific UI.', + ].map((item) => ( +
+ + {item} +
+ ))} +
+
+ ); +} + +const wasmRuntimeCards = [ + { + title: 'Direct Rust API', + value: 'Oliphaunt', + description: 'Use direct calls when Rust code owns SQL work inside the WASM host.', + icon: Database, + }, + { + title: 'PostgreSQL URL', + value: 'OliphauntServer', + description: 'Use server-compatible mode when a library expects a local PostgreSQL endpoint.', + icon: Route, + }, + { + title: 'Runtime assets', + value: 'WASIX', + description: 'Package the WASIX runtime assets and exact extension files selected by the app.', + icon: PackageCheck, + }, + { + title: 'Data movement', + value: 'dump / restore', + description: 'Use logical dumps for portable exports and upgrades between runtime versions.', + icon: HardDriveDownload, + }, +]; + +export function WasmRuntimeMap() { + return ( +
+
+
+ + + +
+

Use WASM as its own runtime family

+

+ WASM shares Oliphaunt concepts with native SDKs, while packaging its own WASIX + runtime assets, host targets, persistence behavior, and extension artifacts. +

+
+
+
+
+ {wasmRuntimeCards.map((card) => { + const Icon = card.icon; + + return ( +
+ + + +

{card.title}

+
+ {card.value} +
+

+ {card.description} +

+
+ ); + })} +
+
+ ); +} + +const wasmDataMovementRows = [ + { + format: 'Logical dump', + use: 'Portable SQL export, version upgrade, runtime-to-runtime movement.', + api: 'dump_sql, dump_bytes, oliphaunt-wasix-dump', + }, + { + format: 'Physical archive', + use: 'Same-version clone or restore into another WASM root.', + api: 'dump_data_dir, load_data_dir_archive, try_clone', + }, + { + format: 'Server dump', + use: 'Workflows already using a local PostgreSQL endpoint.', + api: 'OliphauntServer::dump_sql', + }, +]; + +export function WasmDataMovement() { + return ( +
+
+
+ + + +
+

Choose the export format by destination

+

+ Logical dumps move across runtime versions. Physical archives are fast snapshots for + the same runtime family and database format. +

+
+
+
+
+ + + + + + + + + + {wasmDataMovementRows.map((row) => ( + + + + + + ))} + +
FormatUse it forAPI
{row.format}{row.use} + {row.api} +
+
+
+ ); +} + +export function SdkGuideSummary({ id }: { id: string }) { + const sdk = sdkSurfaces.find((surface) => surface.id === id); + + if (!sdk) { + return null; + } + + const Icon = sdk.icon; + + return ( +
+
+
+ + + +
+

{sdk.title} setup path

+

{sdk.startWith}

+
+
+
+ {sdk.packageName} +
+ {sdk.modes.map((mode) => ( + + {mode} + + ))} +
+
+
+
+
+

Install

+ {sdk.install} +
+
+

Target

+

{sdk.target}

+
+
+

SDK owns

+

{sdk.owns}

+
+
+

Verify first

+

{sdk.verifyFirst}

+
+
+
+ {sdk.guideOutcomes.map((outcome) => ( +
+ + {outcome} +
+ ))} +
+
+ ); +} + +const guideProofs: Record> = { + 'c-abi': [ + { + title: 'Handle lifecycle', + description: 'A binding can open an opaque handle, send protocol bytes, free responses, and close cleanly.', + }, + { + title: 'Ownership', + description: 'The binding exposes response ownership, last-error reads, capabilities, and close state directly.', + }, + { + title: 'Runtime assets', + description: 'The app carries only the native runtime and exact extension artifacts selected by the binding.', + }, + { + title: 'Language surface', + description: 'The public wrapper uses platform-native async, errors, and buffers over the C ABI.', + }, + ], + rust: [ + { + title: 'First query', + description: 'A Rust or Tauri app opens an app-owned root and runs a query through the chosen mode.', + }, + { + title: 'Mode choice', + description: 'Direct, broker, and server paths are chosen through builder configuration and capabilities.', + }, + { + title: 'Data movement', + description: 'Backup, restore, dump, or server tools stay behind Rust SDK APIs.', + }, + { + title: 'App boundary', + description: 'Tauri webviews call narrow Rust commands instead of owning database roots or raw handles.', + }, + ], + swift: [ + { + title: 'First query', + description: 'An iOS or macOS target opens from app storage and runs a query off the main actor.', + }, + { + title: 'Lifecycle', + description: 'The app calls lifecycle hooks around foreground, background, cancellation, and close.', + }, + { + title: 'Resources', + description: 'The Apple package carries the native runtime and only selected extension artifacts.', + }, + { + title: 'Concurrency', + description: 'Swift tasks share the actor-owned database handle and preserve transaction ordering.', + }, + ], + kotlin: [ + { + title: 'First query', + description: 'An Android app opens from app-private storage and runs a query from coroutine code.', + }, + { + title: 'Packaging', + description: 'The Gradle plugin resolves ABI assets, native libraries, and selected extension resources.', + }, + { + title: 'Lifecycle', + description: 'Android lifecycle calls prepare backgrounding, resume foreground work, and close handles.', + }, + { + title: 'App artifact', + description: 'The APK or AAB contains selected extension files and their declared dependencies only.', + }, + ], + 'react-native': [ + { + title: 'Native app binary', + description: 'The app runs in an Expo development build or React Native New Architecture binary.', + }, + { + title: 'Binary transport', + description: 'Raw protocol bytes and streamed chunks move through JSI ArrayBuffer paths.', + }, + { + title: 'Platform delegation', + description: 'Apple behavior flows through Swift, Android behavior flows through Kotlin, and JS owns DX.', + }, + { + title: 'Config output', + description: 'The config plugin selects exact extensions and native runtime assets for the app artifact.', + }, + ], + typescript: [ + { + title: 'Runtime resolver', + description: 'Node, Bun, or Deno resolves helper assets from the installed package.', + }, + { + title: 'Mode connection', + description: 'The app connects to broker or server mode where the selected runtime advertises it.', + }, + { + title: 'Query shape', + description: 'High-level query helpers and raw protocol APIs share one error and capability model.', + }, + { + title: 'Desktop packaging', + description: 'The app packages helper executables, selected extensions, and backup/restore flows together.', + }, + ], + wasm: [ + { + title: 'Runtime assets', + description: 'A WASM/WASIX host loads the WASM runtime assets before opening a root.', + }, + { + title: 'First query', + description: 'The app opens a WASM root and runs SQL through the WASM runtime.', + }, + { + title: 'Data movement', + description: 'Dump, restore, and upgrade flows use the WASM runtime tooling documented for that runtime.', + }, + { + title: 'Runtime family', + description: 'The app treats WASM as its own runtime family with separate assets and build rules.', + }, + ], +}; + +export function SdkGuideProof({ id }: { id: string }) { + const sdk = sdkSurfaces.find((surface) => surface.id === id); + const checks = guideProofs[id]; + + if (!sdk || !checks) { + return null; + } + + return ( +
+
+

This guide is complete when

+

+ Use these checks before moving from a first query to application code. +

+
+
+ {checks.map((check) => ( +
+ +
+

{check.title}

+

+ {check.description} +

+
+
+ ))} +
+ + Open the {sdk.title} API map + + +
+ ); +} + +export function SdkLanding({ id }: { id: string }) { + const sdk = sdkSurfaces.find((surface) => surface.id === id); + + if (!sdk) { + return null; + } + + const Icon = sdk.icon; + const guideHref = `${sdk.href}/guide`; + const apiHref = `${sdk.href}/api-reference`; + const extraHref = id === 'react-native' ? `${sdk.href}/architecture` : undefined; + + return ( +
+
+
+
+ + + +
+

{sdk.title} at a glance

+

{sdk.target}

+
+
+

{sdk.startWith}

+
+
+

Install

+ {sdk.install} +
+ {sdk.packageName} +
+
+
+
+
+

SDK owns

+

{sdk.owns}

+
+
+

Modes

+
+ {sdk.modes.map((mode) => ( + + {mode} + + ))} +
+
+
+

Verify first

+

{sdk.verifyFirst}

+
+
+
+ {[ + { + href: guideHref, + title: 'Build guide', + description: 'Install, open, configure, select extensions, and verify lifecycle.', + }, + { + href: apiHref, + title: 'API map', + description: 'Find the public surface for open, query, lifecycle, capabilities, and backup.', + }, + { + href: extraHref ?? '/docs/reference/capabilities', + title: extraHref ? 'Architecture' : 'Capabilities', + description: extraHref + ? 'Understand the React Native, Swift, Kotlin, TurboModule, and JSI boundary.' + : 'Check mode, streaming, extension, backup, restore, and client-session support.', + }, + ].map((item) => ( + +

{item.title}

+

{item.description}

+ + + ))} +
+
+ ); +} + +export function QuickstartPath() { + const steps = [ + { + title: 'Pick the SDK', + description: 'Choose the package for the app users install: Rust, Swift, Kotlin, React Native, TypeScript, WASM, or C ABI.', + }, + { + title: 'Install through the platform tool', + description: 'Use Cargo, SwiftPM/Xcode, Gradle, npm, Expo, or the released C artifacts. Native apps rebuild when runtime assets or selected extensions change.', + }, + { + title: 'Open an app-owned root', + description: 'Use persistent app storage for user data and a temporary root for tests. A root is a PostgreSQL directory managed through SDK APIs.', + }, + { + title: 'Run SQL and verify capabilities', + description: ( + <> + Run SELECT 1, read capabilities(), and create only the + extensions selected for the app artifact. + + ), + }, + ]; + + return ( +
+
+
+ +
+

Start in one app target

+

+ The first path is short: install, open, query, verify, then use the platform page for + lifecycle, packaging, and data movement. +

+
+
+
+
    + {steps.map((step, index) => ( +
  1. + + {String(index + 1).padStart(2, '0')} + +

    {step.title}

    +

    {step.description}

    +
  2. + ))} +
+
+ ); +} + +export function StartOutcome() { + const outcomes = [ + { + title: 'One SDK selected', + description: 'You have the package, runtime artifacts, and build path for the app users install.', + icon: PackageCheck, + }, + { + title: 'One root opened', + description: 'The database lives in app-owned storage and uses SDK lifecycle APIs.', + icon: HardDriveDownload, + }, + { + title: 'One query verified', + description: ( + <> + SELECT 1, capabilities(), and selected extensions prove the + runtime path. + + ), + icon: ClipboardCheck, + }, + { + title: 'One next page', + description: 'You move to the platform guide, runtime model, extensions, or performance lookup.', + icon: Route, + }, + ]; + + return ( +
+
+
+

Finish this page with a working app path

+

+ Start proves one target. Platform guides handle deeper app wiring after that. +

+
+ + Tutorial + +
+
    + {outcomes.map((outcome) => { + const Icon = outcome.icon; + + return ( +
  • + + + +
    +

    {outcome.title}

    +

    + {outcome.description} +

    +
    +
  • + ); + })} +
+
+ ); +} + +const firstQueryExamples = [ + { + language: 'TypeScript', + packageName: '@oliphaunt/ts', + code: `const db = await Oliphaunt.open({ + root: 'main.oliphaunt', + engine: 'nativeDirect', + extensions: ['vector'], +}); + +const rows = await db.query('select 1 as ready'); +await db.close();`, + }, + { + language: 'Rust', + packageName: 'oliphaunt', + code: `let db = Oliphaunt::builder() + .root("main.oliphaunt") + .mode(RuntimeMode::NativeDirect) + .extension("vector") + .open() + .await?; + +db.query("select 1 as ready").await?; +db.close().await?;`, + }, + { + language: 'Swift', + packageName: 'Oliphaunt', + code: `let db = try await Oliphaunt.open( + root: .appStorage("main.oliphaunt"), + mode: .nativeDirect, + extensions: ["vector"] +) + +try await db.query("select 1 as ready") +try await db.close()`, + }, +]; + +export function FirstQueryFlow() { + return ( +
+
+
+

First query shape

+

+ Same storage, mode, extension, query, and lifecycle path across SDK syntax. +

+
+
    + {['Storage', 'Mode', 'Extensions', 'Query', 'Lifecycle'].map((item, index) => ( +
  1. + + {String(index + 1).padStart(2, '0')} + +

    {item}

    +
  2. + ))} +
+
+
+ {firstQueryExamples.map((example) => ( +
+
+
+

{example.language}

+
+ {example.packageName} +
+
+
+
+              {example.code}
+            
+
+ ))} +
+
+ ); +} + +const startNextSteps = [ + { + title: 'Choose the platform guide', + description: 'Install, build, query, lifecycle, extensions, backup, and troubleshooting for one SDK.', + href: '/docs/sdk', + label: 'SDKs', + icon: PackageCheck, + }, + { + title: 'Understand runtime modes', + description: 'Direct, broker, server, WASM, root ownership, sessions, and process boundaries.', + href: '/docs/learn/native-runtime', + label: 'Native Runtime', + icon: GitBranch, + }, + { + title: 'Select extensions exactly', + description: 'Choose SQL extension names and verify only selected files enter the app artifact.', + href: '/docs/reference/extensions', + label: 'Extensions', + icon: ShieldCheck, + }, + { + title: 'Plan storage and backup', + description: 'Use app-owned PostgreSQL roots, lifecycle APIs, backup, restore, and recovery behavior.', + href: '/docs/learn/embedded-postgres', + label: 'Embedded PostgreSQL', + icon: HardDriveDownload, + }, +]; + +export function StartNextSteps() { + return ( +
+ {startNextSteps.map((step) => { + const Icon = step.icon; + + return ( + + + + +
+

+ {step.label} +

+

{step.title}

+
+

{step.description}

+ + + ); + })} +
+ ); +} + +export function VerifyChecklist() { + const checks = [ + { + title: 'Install', + description: 'The package resolves through the normal package manager and platform build tool.', + icon: PackageCheck, + }, + { + title: 'Configure', + description: 'Runtime mode, root, selected extensions, and platform assets are explicit.', + icon: Settings2, + }, + { + title: 'Verify', + description: ( + <> + SELECT 1, capabilities(), and selected extensions behave on + the target. + + ), + icon: ClipboardCheck, + }, + ]; + + return ( +
+ {checks.map((check) => { + const Icon = check.icon; + + return ( +
+ + + +

{check.title}

+

{check.description}

+
+ ); + })} +
+ ); +} + +export function ShipChecklist() { + const items = [ + { + title: 'Package', + description: 'Build the app binary or helper package that carries the selected runtime artifacts.', + }, + { + title: 'Lifecycle', + description: 'Wire close, foreground, background, cancellation, and restart behavior through the SDK.', + }, + { + title: 'Extensions', + description: 'Select SQL extension names explicitly and verify selected files in the app artifact.', + }, + { + title: 'Data movement', + description: 'Use SDK backup, restore, dump, or archive APIs for user-visible export and import.', + }, + { + title: 'Capabilities', + description: 'Read capability flags before enabling streaming, broker, server, or platform-specific UI.', + }, + ]; + + return ( +
+
+

Before shipping

+

+ The first query proves the runtime is present. These checks prove the app integration is + ready for users. +

+
+
+ {items.map((item) => ( +
+ +

{item.title}

+

{item.description}

+
+ ))} +
+
+ ); +} + +export function ModeMatrix() { + return ( +
+ {runtimeModes.map((mode) => { + const Icon = mode.icon; + + return ( +
+
+ + + +
+ {mode.name} +

{mode.label}

+
+
+
+

Use it when

+

{mode.useWhen}

+
+
+

Boundary

+

{mode.boundary}

+
+
+ ); + })} +
+ ); +} + +export function ExactExtensionRule() { + return ( +
+
+ +
+

Extension selection is exact SQL extension name only.

+

+ If an app selects vector, the app artifact contains vector{' '} + and its declared dependencies. Unrelated search, geo, graph, or development-only + extension files stay out of that app artifact. +

+
+
+
+ ); +} diff --git a/src/docs/src/lib/cn.ts b/src/docs/src/lib/cn.ts new file mode 100644 index 00000000..ba66fd25 --- /dev/null +++ b/src/docs/src/lib/cn.ts @@ -0,0 +1 @@ +export { twMerge as cn } from 'tailwind-merge'; diff --git a/src/docs/src/lib/docs-data.ts b/src/docs/src/lib/docs-data.ts new file mode 100644 index 00000000..23a419ed --- /dev/null +++ b/src/docs/src/lib/docs-data.ts @@ -0,0 +1,225 @@ +import { + Boxes, + Braces, + CodeXml, + Database, + HardDrive, + Laptop, + Layers, + Network, + Server, + ShieldCheck, + Smartphone, + type LucideIcon, +} from 'lucide-react'; + +export type SdkSurface = { + id: string; + title: string; + href: string; + packageName: string; + install: string; + target: string; + startWith: string; + owns: string; + modes: string[]; + verifyFirst: string; + guideOutcomes: string[]; + icon: LucideIcon; +}; + +export const sdkSurfaces: SdkSurface[] = [ + { + id: 'rust', + title: 'Rust', + href: '/docs/sdk/rust', + packageName: 'oliphaunt', + install: 'cargo add oliphaunt', + target: 'Tauri and native Rust desktop apps', + startWith: 'Direct, broker, and server modes', + owns: 'Rust-native async APIs, helper processes, and desktop runtime selection.', + modes: ['direct', 'broker', 'server'], + verifyFirst: 'Run a direct query, then verify broker or server capability before using pools.', + guideOutcomes: [ + 'Open a persistent or temporary root from async Rust code.', + 'Choose direct, broker, or server mode deliberately.', + 'Select exact extensions and keep backup/restore behind SDK APIs.', + ], + icon: Laptop, + }, + { + id: 'swift', + title: 'Swift', + href: '/docs/sdk/swift', + packageName: 'Oliphaunt', + install: 'Add package in Xcode or Package.swift', + target: 'iOS and macOS apps', + startWith: 'Swift concurrency, app storage, and lifecycle', + owns: 'Apple app storage, actors, lifecycle hooks, and native runtime resources.', + modes: ['direct'], + verifyFirst: 'Open from app storage, run a query off the main actor, and exercise app lifecycle hooks.', + guideOutcomes: [ + 'Add the Swift package to an iOS or macOS app target.', + 'Open from Swift concurrency without blocking the main actor.', + 'Coordinate app lifecycle, exact extensions, and backup/restore.', + ], + icon: Smartphone, + }, + { + id: 'kotlin', + title: 'Kotlin', + href: '/docs/sdk/kotlin', + packageName: 'dev.oliphaunt:oliphaunt-android', + install: 'id("dev.oliphaunt.android") + implementation("dev.oliphaunt:oliphaunt-android:0.1.0")', + target: 'Android apps', + startWith: 'Coroutines, Android resources, and ABI artifacts', + owns: 'Android resource hydration, ABI selection, coroutines, and lifecycle.', + modes: ['direct'], + verifyFirst: 'Build the Android app, open from app-private storage, and confirm selected ABI assets.', + guideOutcomes: [ + 'Add the Android package through Gradle.', + 'Open from coroutine code using app-private storage.', + 'Package only selected extensions and use Android lifecycle hooks.', + ], + icon: Smartphone, + }, + { + id: 'react-native', + title: 'React Native', + href: '/docs/sdk/react-native', + packageName: '@oliphaunt/react-native', + install: 'npx expo install @oliphaunt/react-native', + target: 'Expo and React Native New Architecture apps', + startWith: 'Config plugin, TurboModule, and JSI transport', + owns: 'TypeScript DX, config plugin behavior, JSI bytes, and platform delegation.', + modes: ['direct'], + verifyFirst: 'Build a development client, confirm native module loading, and move bytes through JSI.', + guideOutcomes: [ + 'Install the package and build a native app binary or development client.', + 'Use the config plugin for exact extension artifacts.', + 'Move SQL, raw protocol bytes, streaming, and lifecycle through JSI/TurboModule APIs.', + ], + icon: Layers, + }, + { + id: 'typescript', + title: 'TypeScript', + href: '/docs/sdk/typescript', + packageName: '@oliphaunt/ts', + install: 'npm install @oliphaunt/ts', + target: 'Node.js, Bun, and Deno', + startWith: 'Desktop JavaScript over native helpers', + owns: 'JavaScript API shape, runtime asset resolution, and helper-backed modes.', + modes: ['broker', 'server', 'direct adapter'], + verifyFirst: 'Resolve helper assets, connect to broker or server mode, and run the same query path.', + guideOutcomes: [ + 'Install the desktop JavaScript package from npm.', + 'Resolve helper-backed runtime assets from the package.', + 'Choose broker or server mode for robust desktop JavaScript apps.', + ], + icon: Braces, + }, + { + id: 'wasm', + title: 'WASM', + href: '/docs/sdk/wasm', + packageName: 'oliphaunt-wasix', + install: 'cargo add oliphaunt-wasix', + target: 'WASM/WASIX hosts', + startWith: 'WASM/WASIX runtime family', + owns: 'WASM runtime behavior, WASIX assets, dump and restore flows.', + modes: ['WASIX'], + verifyFirst: 'Load WASM runtime assets, open a root, and prove dump or restore for data movement.', + guideOutcomes: [ + 'Install the WASM package for WASIX hosts.', + 'Open a WASM runtime root and run SQL through WebAssembly.', + 'Use dump/restore when moving data across runtimes or versions.', + ], + icon: Boxes, + }, + { + id: 'c-abi', + title: 'C ABI', + href: '/docs/sdk/c-abi', + packageName: 'liboliphaunt', + install: 'Use released headers, libraries, and runtime assets', + target: 'New language bindings', + startWith: 'Native runtime ownership and ABI rules', + owns: 'Opaque handles, raw protocol bytes, response ownership, and lifecycle.', + modes: ['direct ABI'], + verifyFirst: 'Open an opaque handle, send protocol bytes, free responses, and close cleanly.', + guideOutcomes: [ + 'Consume released headers, libraries, and native runtime assets.', + 'Open an opaque handle and manage response ownership explicitly.', + 'Build language bindings that expose capabilities, errors, lifecycle, and backup APIs.', + ], + icon: CodeXml, + }, +]; + +export type RuntimeMode = { + name: string; + label: string; + href: string; + useWhen: string; + boundary: string; + icon: LucideIcon; +}; + +export const runtimeModes: RuntimeMode[] = [ + { + name: 'nativeDirect', + label: 'Embedded latency', + href: '/docs/learn/native-runtime', + useWhen: 'One app database needs the lowest overhead path.', + boundary: 'One physical PostgreSQL session with serialized work.', + icon: Database, + }, + { + name: 'nativeBroker', + label: 'Desktop isolation', + href: '/docs/learn/native-runtime', + useWhen: 'A desktop app needs helper-process ownership, multiple roots, or recovery.', + boundary: 'Helper process boundary for desktop SDKs.', + icon: Network, + }, + { + name: 'nativeServer', + label: 'Client compatibility', + href: '/docs/learn/native-runtime', + useWhen: 'Existing PostgreSQL clients, ORMs, psql, or pg_dump need real sessions.', + boundary: 'PostgreSQL-compatible process boundary with independent client sessions.', + icon: Server, + }, + { + name: 'WASM', + label: 'WASIX runtime', + href: '/docs/sdk/wasm/runtime', + useWhen: 'The app targets a WASM/WASIX host.', + boundary: 'Separate build and packaging rules from native SDKs.', + icon: Boxes, + }, +]; + +export const productPillars = [ + { + title: 'PostgreSQL semantics', + description: 'Use PostgreSQL storage, WAL, SQL, protocol behavior, and selected extensions inside app-owned storage.', + icon: Database, + }, + { + title: 'Runtime modes with clear boundaries', + description: 'Direct optimizes embedded latency, broker optimizes desktop isolation, and server optimizes independent client sessions.', + icon: Server, + }, + { + title: 'Exact extension packaging', + description: 'Apps select SQL extension names explicitly so release artifacts include only what the app uses.', + icon: ShieldCheck, + }, + { + title: 'App-grade data movement', + description: 'SDK backup and restore APIs keep PostgreSQL directory mechanics out of application code.', + icon: HardDrive, + }, +]; diff --git a/src/docs/src/lib/layout.shared.tsx b/src/docs/src/lib/layout.shared.tsx new file mode 100644 index 00000000..ddc6eabb --- /dev/null +++ b/src/docs/src/lib/layout.shared.tsx @@ -0,0 +1,35 @@ +import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import { appName, gitConfig } from './shared'; + +export function baseOptions(): BaseLayoutProps { + return { + nav: { + title: appName, + url: '/', + }, + links: [ + { + text: 'Start', + url: '/docs/start', + }, + { + text: 'SDKs', + url: '/docs/sdk', + }, + { + text: 'Learn', + url: '/docs/learn', + }, + { + text: 'Reference', + url: '/docs/reference', + }, + { + text: 'GitHub', + url: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, + external: true, + }, + ], + githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, + }; +} diff --git a/src/docs/src/lib/shared.ts b/src/docs/src/lib/shared.ts new file mode 100644 index 00000000..94e6de5b --- /dev/null +++ b/src/docs/src/lib/shared.ts @@ -0,0 +1,10 @@ +export const appName = 'Oliphaunt'; +export const docsRoute = '/docs'; +export const docsImageRoute = '/og/docs'; +export const docsContentRoute = '/llms.mdx/docs'; + +export const gitConfig = { + user: 'f0rr0', + repo: 'oliphaunt', + branch: 'main', +}; diff --git a/src/docs/src/lib/source.ts b/src/docs/src/lib/source.ts new file mode 100644 index 00000000..a00a3fcf --- /dev/null +++ b/src/docs/src/lib/source.ts @@ -0,0 +1,37 @@ +import { docs } from 'collections/server'; +import { loader } from 'fumadocs-core/source'; +import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons'; +import { docsContentRoute, docsImageRoute, docsRoute } from './shared'; + +// See https://fumadocs.dev/docs/headless/source-api for more info +export const source = loader({ + baseUrl: docsRoute, + source: docs.toFumadocsSource(), + plugins: [lucideIconsPlugin()], +}); + +export function getPageImage(page: (typeof source)['$inferPage']) { + const segments = [...page.slugs, 'image.png']; + + return { + segments, + url: `${docsImageRoute}/${segments.join('/')}`, + }; +} + +export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) { + const segments = [...page.slugs, 'content.md']; + + return { + segments, + url: `${docsContentRoute}/${segments.join('/')}`, + }; +} + +export async function getLLMText(page: (typeof source)['$inferPage']) { + const processed = await page.data.getText('processed'); + + return `# ${page.data.title} (${page.url}) + +${processed}`; +} diff --git a/src/docs/static/img/favicon.svg b/src/docs/static/img/favicon.svg new file mode 100644 index 00000000..659180b0 --- /dev/null +++ b/src/docs/static/img/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/docs/tools/check-docs-product.mjs b/src/docs/tools/check-docs-product.mjs new file mode 100644 index 00000000..923c160f --- /dev/null +++ b/src/docs/tools/check-docs-product.mjs @@ -0,0 +1,1430 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { generateDocs } from './generate-content.mjs'; + +const args = new Set(process.argv.slice(2)); +const apiReferenceRequested = args.has('--api-reference'); +const result = generateDocs({ + apiMode: apiReferenceRequested ? 'release' : 'fast', + publishApiArtifacts: apiReferenceRequested, +}); +const { manifest, sdkManifest, releaseGraph, routeRecords, paths } = result; +const { repoRoot, siteDocsRoot, staticRoot, generatedMetaRoot } = paths; +const { apiSummary } = result; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function requireFile(relativePath) { + const fullPath = path.join(repoRoot, relativePath); + if (!fs.existsSync(fullPath)) { + fail(`required docs file missing: ${relativePath}`); + } + return fullPath; +} + +function readText(relativePath) { + return fs.readFileSync(requireFile(relativePath), 'utf8'); +} + +function readJsonFile(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +function routeSourcePagePath(route, page) { + const matches = ['.md', '.mdx'] + .map((extension) => path.join(route.source, `${page}${extension}`)) + .filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath))); + if (matches.length > 1) { + fail(`${route.id} docs contain duplicate source pages for ${page}: ${matches.join(', ')}`); + } + return matches[0] ?? null; +} + +function routePageSet(routeId) { + const route = manifest.routes.find((entry) => entry.id === routeId); + return new Set(route?.page_order ?? []); +} + +function sidebarPagesForRoute(route) { + return route.sidebar_pages ?? route.page_order ?? []; +} + +function gitTrackedFiles(pathspec) { + try { + return execFileSync('git', ['ls-files', pathspec], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean); + } catch { + return []; + } +} + +function assertNoTrackedRootProductsDocs() { + const tracked = gitTrackedFiles('docs/products'); + if (tracked.length > 0) { + fail( + `public product docs must live under src/docs/content, found tracked docs/products files:\n${tracked.join('\n')}`, + ); + } +} + +function assertNoProductLocalPublicDocs() { + const tracked = gitTrackedFiles('src/*/docs/**').filter( + (file) => !file.startsWith('src/docs/') && /\.(md|mdx)$/u.test(file), + ); + if (tracked.length > 0) { + fail( + `public SDK docs must be centralized under src/docs/content; product-local docs require an explicit package-shipped exception:\n${tracked.join('\n')}`, + ); + } +} + +function assertNoTrackedRootPublicDocs() { + const tracked = gitTrackedFiles('docs').filter((file) => /^docs\/[^/]+\.md$/u.test(file)); + const unexpected = tracked.filter((file) => file !== 'docs/README.md'); + if (unexpected.length > 0) { + fail( + `top-level root docs are maintainer-only; move public docs into src/docs or docs subdirectories:\n${unexpected.join('\n')}`, + ); + } +} + +function assertRootDocsBuckets() { + for (const dir of ['docs/architecture', 'docs/maintainers', 'docs/internal']) { + if (!fs.existsSync(path.join(repoRoot, dir))) { + fail(`required root docs bucket missing: ${dir}`); + } + } +} + +function assertNoDocsMoonProject() { + if (fs.existsSync(path.join(repoRoot, 'docs/moon.yml'))) { + fail('docs/moon.yml must not exist; docs is the only docs project'); + } +} + +function assertDocsChromeDoesNotExposeSourcePaths() { + const pageShell = readText('src/docs/src/app/docs/[[...slug]]/page.tsx'); + if (/ViewOptionsPopover[\s\S]{0,240}\bgithubUrl\s*=/u.test(pageShell)) { + fail( + 'public docs page actions must not expose monorepo source-file links through ViewOptionsPopover', + ); + } + if (pageShell.includes('src/docs/content')) { + fail('public docs page actions must not construct GitHub links to source content paths'); + } +} + +function assertUniqueRoutes() { + const seen = new Set(); + for (const route of manifest.routes ?? []) { + if (!route.id || !route.route || !route.source) { + fail(`docs-manifest route is missing id, route, or source: ${JSON.stringify(route)}`); + } + if (route.route.startsWith('/') || route.route.includes('\\')) { + fail(`docs route must be relative and URL-safe: ${route.id}`); + } + if (seen.has(route.route)) { + fail(`duplicate docs route: ${route.route}`); + } + seen.add(route.route); + } +} + +function assertGeneratedFiles() { + const referencePages = routePageSet('reference'); + const generatedReferencePages = [ + 'sdk-matrix', + 'platforms', + 'extension-catalog', + 'api-reference', + 'tested-snippets', + 'artifact-provenance', + 'version-matrix', + ] + .filter((page) => referencePages.has(page)) + .map((page) => path.join(siteDocsRoot, 'reference', `${page}.md`)); + const required = [ + ...generatedReferencePages, + path.join(staticRoot, 'llms.txt'), + path.join(staticRoot, 'llms-full.txt'), + path.join(generatedMetaRoot, 'routes.json'), + path.join(generatedMetaRoot, 'navigation.json'), + path.join(repoRoot, 'target', 'docs', 'generated', 'api', 'summary.json'), + path.join(siteDocsRoot, 'meta.json'), + path.join(siteDocsRoot, 'sdk', 'meta.json'), + ]; + for (const file of required) { + if (!fs.existsSync(file)) { + fail(`generated docs artifact missing: ${path.relative(repoRoot, file)}`); + } + } +} + +function assertGeneratedFumadocsMetadata() { + const rootMeta = readJsonFile(path.join(siteDocsRoot, 'meta.json')); + const expectedRootPages = ['start', 'sdk', 'learn', 'reference']; + if (JSON.stringify(rootMeta.pages) !== JSON.stringify(expectedRootPages)) { + fail(`root Fumadocs metadata must keep compact public nav: ${expectedRootPages.join(', ')}`); + } + if (!rootMeta.description || rootMeta.description.length < 48) { + fail('root Fumadocs metadata must include a useful reader-facing description'); + } + + for (const route of manifest.routes ?? []) { + const metaPath = path.join(siteDocsRoot, route.route, 'meta.json'); + if (!fs.existsSync(metaPath)) { + fail(`generated Fumadocs metadata missing for route ${route.id}`); + } + const metadata = readJsonFile(metaPath); + if (!metadata.title) { + fail(`generated Fumadocs metadata missing title for route ${route.id}`); + } + if (!metadata.description || metadata.description === `${route.title} documentation`) { + fail(`generated Fumadocs metadata needs a real description for route ${route.id}`); + } + if (!metadata.icon) { + fail(`generated Fumadocs metadata needs an icon for route ${route.id}`); + } + if ((route.page_order ?? []).includes('index')) { + if (metadata.pagesIndex !== 'index') { + fail(`${route.id} metadata must expose index as the folder pagesIndex`); + } + if (metadata.pages?.includes('index')) { + fail(`${route.id} metadata must not duplicate index as a sidebar child page`); + } + } + if (route.kind === 'sdk') { + if (metadata.pagesIndex !== 'index') { + fail(`${route.id} SDK metadata must use the overview page as pagesIndex`); + } + if (!metadata.pages?.includes('guide')) { + fail(`${route.id} SDK metadata must expose guide in the SDK folder`); + } + if (metadata.pages?.includes('api-reference')) { + fail( + `${route.id} SDK metadata must keep API Reference out of the primary sidebar; link it from Reference and SDK page bodies`, + ); + } + for (const page of ['api-reference']) { + const routePath = page === 'index' ? `/${route.route}` : `/${route.route}/${page}`; + if (!routeRecords.some((record) => record.route === routePath)) { + fail(`${route.id} SDK metadata requires reachable ${page} route`); + } + } + } + } +} + +function assertSdkSidebarPages() { + const expectedOrder = [ + 'oliphaunt-rust', + 'oliphaunt-swift', + 'oliphaunt-kotlin', + 'oliphaunt-react-native', + 'oliphaunt-js', + 'oliphaunt-wasix', + 'liboliphaunt-native', + ]; + const actualOrder = manifest.routes + .filter((entry) => entry.kind === 'sdk') + .map((entry) => entry.id); + if (JSON.stringify(actualOrder) !== JSON.stringify(expectedOrder)) { + fail(`SDK route order must stay app-developer first: ${expectedOrder.join(' -> ')}`); + } + + for (const route of manifest.routes.filter((entry) => entry.kind === 'sdk')) { + const expected = + route.id === 'oliphaunt-react-native' + ? ['index', 'guide', 'architecture'] + : route.id === 'oliphaunt-wasix' + ? ['index', 'guide', 'runtime', 'dump-restore'] + : ['index', 'guide']; + const actual = route.sidebar_pages ?? []; + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + fail(`${route.id} sidebar_pages must be ${expected.join(', ')}`); + } + if (actual.includes('api-reference')) { + fail(`${route.id} sidebar_pages must not expose API Reference as a primary SDK page`); + } + } +} + +function assertReferenceSidebarPages() { + const route = manifest.routes.find((entry) => entry.id === 'reference'); + if (!route) { + fail('docs manifest is missing reference route'); + } + const expected = ['index', 'capabilities', 'extensions', 'performance']; + const actual = route.sidebar_pages ?? []; + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + fail(`reference sidebar_pages must stay focused: ${expected.join(', ')}`); + } + for (const reachable of [ + 'sdk-products', + 'releases', + 'version-matrix', + 'extension-catalog', + 'api-reference', + ]) { + if (!(route.page_order ?? []).includes(reachable)) { + fail(`reference page_order must keep ${reachable} reachable from lookup pages`); + } + if (actual.includes(reachable)) { + fail(`reference sidebar_pages must keep ${reachable} as a lookup page, not primary nav`); + } + } +} + +function assertNoStaleGeneratedNavigation() { + const stale = path.join(generatedMetaRoot, 'sidebars.json'); + if (fs.existsSync(stale)) { + fail( + 'stale generated sidebars.json must not exist; Fumadocs metadata is generated from meta.json and navigation.json', + ); + } +} + +function assertPublicContentIsMarkdownOnly() { + const contentRoot = path.join(repoRoot, 'src/docs/content'); + const unexpected = []; + function visit(dirPath) { + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + visit(fullPath); + continue; + } + if (entry.isFile() && !/\.mdx?$/u.test(entry.name)) { + unexpected.push(path.relative(repoRoot, fullPath)); + } + } + } + visit(contentRoot); + if (unexpected.length > 0) { + fail( + `public docs content may only contain Markdown/MDX pages; move data or policy files out of src/docs/content:\n${unexpected.join('\n')}`, + ); + } +} + +function collectPublicContentPages() { + const contentRoot = path.join(repoRoot, 'src/docs/content'); + const pages = []; + function visit(dirPath) { + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + visit(fullPath); + continue; + } + if (entry.isFile() && /\.mdx?$/u.test(entry.name)) { + pages.push(fullPath); + } + } + } + visit(contentRoot); + return pages.sort(); +} + +function frontmatterValue(markdown, key) { + const frontmatter = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u); + if (!frontmatter) { + return ''; + } + const match = frontmatter[1].match(new RegExp(`^${key}\\s*:\\s*(.+)$`, 'mu')); + return match?.[1]?.trim().replace(/^["']|["']$/gu, '') ?? ''; +} + +function assertPublicContentMetadata() { + const missing = []; + for (const file of collectPublicContentPages()) { + const relative = path.relative(repoRoot, file); + const markdown = fs.readFileSync(file, 'utf8'); + const title = frontmatterValue(markdown, 'title'); + const description = frontmatterValue(markdown, 'description'); + if (!title) { + missing.push(`${relative}: missing title frontmatter`); + } + if (!description) { + missing.push(`${relative}: missing description frontmatter`); + } else if (description.length < 24) { + missing.push(`${relative}: description is too terse for a docs page`); + } + } + if (missing.length > 0) { + fail(`public docs pages must have explicit reader-facing metadata:\n${missing.join('\n')}`); + } +} + +function assertApplicabilityMetadata() { + const missing = []; + for (const record of routeRecords) { + const markdown = fs.readFileSync(record.file, 'utf8'); + if (!record.appliesTo || !/^applies_to\s*:/mu.test(markdown)) { + missing.push(record.source); + } + } + if (missing.length > 0) { + fail(`public docs pages must declare generated applies_to metadata:\n${missing.join('\n')}`); + } +} + +function assertLightweightVersioning() { + const releaseIndex = readText('src/docs/content/reference/releases.mdx'); + for (const required of [ + '`latest` channel', + 'package versions', + 'compatibility notes', + 'release notes', + 'Versioned docs remain available', + 'Documentation changes can update the docs site', + ]) { + if (!releaseIndex.includes(required)) { + fail(`release docs must describe lightweight docs versioning policy: missing ${required}`); + } + } + const versionMatrix = readText( + path.relative(repoRoot, path.join(siteDocsRoot, 'reference', 'version-matrix.md')), + ); + for (const required of [ + '| Product | Current source version | First public version | Version relationship | Publish targets | Tag prefix |', + '0.0.0 (unreleased)', + 'oliphaunt-swift | 0.0.0 (unreleased) | 0.6.0', + 'runtime-bound', + 'upstream-bound', + 'Release coupling is derived from Moon production and peer dependency scopes', + 'liboliphaunt-native', + 'oliphaunt-react-native', + 'oliphaunt-wasix-rust', + ]) { + if (!versionMatrix.includes(required)) { + fail(`generated version matrix is missing compatibility/release data: ${required}`); + } + } +} + +function assertRouteCoverage() { + const routes = new Set(routeRecords.map((record) => record.route)); + const requiredRoutes = []; + for (const route of manifest.routes ?? []) { + for (const page of route.page_order ?? ['index']) { + requiredRoutes.push(page === 'index' ? `/${route.route}` : `/${route.route}/${page}`); + } + for (const page of route.required_pages ?? []) { + requiredRoutes.push(page === 'index' ? `/${route.route}` : `/${route.route}/${page}`); + } + } + for (const route of requiredRoutes) { + if (!routes.has(route)) { + fail(`generated docs route missing: ${route}`); + } + } +} + +function assertPublicRootLandingPages() { + for (const route of manifest.routes.filter((entry) => entry.kind === 'public')) { + if (!(route.page_order ?? []).includes('index')) { + fail(`${route.id} public docs section must include an index landing page`); + } + const pagePath = routeSourcePagePath(route, 'index'); + if (!pagePath) { + fail(`${route.id} public docs section is missing index.md or index.mdx`); + } + } +} + +function assertLlmRouteCoverage() { + const llms = readText(path.relative(repoRoot, path.join(staticRoot, 'llms.txt'))); + const full = readText(path.relative(repoRoot, path.join(staticRoot, 'llms-full.txt'))); + for (const record of routeRecords) { + if (!llms.includes(record.route)) { + fail(`llms.txt is missing route ${record.route}`); + } + if (!full.includes(`Route: ${record.route}`)) { + fail(`llms-full.txt is missing route ${record.route}`); + } + } +} + +function stripMarkdownCodeBlocks(markdown) { + return markdown.replace(/```[\s\S]*?```/gu, ''); +} + +function extractHrefTargets(text) { + const hrefs = []; + const stripped = stripMarkdownCodeBlocks(text); + const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/gu; + const mdxHrefPattern = /\bhref=(?:"([^"]+)"|'([^']+)')/gu; + for (const match of stripped.matchAll(markdownLinkPattern)) { + hrefs.push(match[1]); + } + for (const match of stripped.matchAll(mdxHrefPattern)) { + hrefs.push(match[1] ?? match[2]); + } + return hrefs; +} + +function normalizedDocsPath(href) { + if (!href || href.startsWith('#')) { + return null; + } + if (/^(?:[a-z][a-z0-9+.-]*:)?\/\//iu.test(href) || /^[a-z][a-z0-9+.-]*:/iu.test(href)) { + return null; + } + if (!href.startsWith('/docs')) { + return null; + } + const [withoutHash] = href.split('#'); + const [withoutQuery] = withoutHash.split('?'); + return withoutQuery.replace(/\/+$/u, '') || '/docs'; +} + +function collectSourceTextFiles(dirPath, output = []) { + if (!fs.existsSync(dirPath)) { + return output; + } + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + if (!['node_modules', '.next', 'out'].includes(entry.name)) { + collectSourceTextFiles(fullPath, output); + } + continue; + } + if (entry.isFile() && /\.(?:md|mdx|ts|tsx|js|jsx)$/iu.test(entry.name)) { + output.push(fullPath); + } + } + return output; +} + +function assertDocsInternalLinksResolve() { + const validDocsPaths = new Set(['/docs']); + for (const record of routeRecords) { + validDocsPaths.add(`/docs${record.route}`); + } + + const failures = []; + const files = [ + ...routeRecords.map((record) => record.file), + ...collectSourceTextFiles(path.join(repoRoot, 'src/docs/src')), + ]; + for (const file of files) { + const relative = path.relative(repoRoot, file); + const text = fs.readFileSync(file, 'utf8'); + for (const href of extractHrefTargets(text)) { + const docsPath = normalizedDocsPath(href); + if (docsPath && !validDocsPaths.has(docsPath)) { + failures.push(`${relative}: unresolved docs link ${href}`); + } + } + } + if (failures.length > 0) { + fail(`public docs contain unresolved internal links:\n${failures.join('\n')}`); + } +} + +function assertSdkSectionCoverage() { + const guideSummaryIds = { + 'liboliphaunt-native': 'c-abi', + 'oliphaunt-rust': 'rust', + 'oliphaunt-swift': 'swift', + 'oliphaunt-kotlin': 'kotlin', + 'oliphaunt-react-native': 'react-native', + 'oliphaunt-js': 'typescript', + 'oliphaunt-wasix': 'wasm', + }; + const guideHeadingOrder = { + 'liboliphaunt-native': [ + 'Install', + 'Open and query', + 'Configure', + 'Choose a mode', + 'Handle lifecycle', + 'Select extensions', + 'Back up and restore', + ], + default: [ + 'Install', + 'Open and query', + 'Create app data', + 'Configure', + 'Choose a mode', + 'Handle lifecycle', + 'Select extensions', + 'Back up and restore', + ], + 'oliphaunt-wasix': [ + 'Install', + 'Open and query', + 'Create app data', + 'Configure', + 'Choose a mode', + 'Handle lifecycle', + 'Select extensions', + 'Back up, dump, and restore', + ], + }; + for (const route of manifest.routes.filter((entry) => entry.kind === 'sdk')) { + const requiredPages = route.required_pages ?? []; + for (const required of ['index', 'guide', 'api-reference']) { + if (!requiredPages.includes(required)) { + fail(`${route.id} docs must declare ${required} in docs-manifest.toml`); + } + } + if ((route.page_order ?? []).length > 6) { + fail( + `${route.id} docs sidebar is too granular; keep Overview, Guide, API Reference, and only justified deep pages`, + ); + } + for (const page of requiredPages) { + const pagePath = routeSourcePagePath(route, page); + if (!pagePath) { + fail(`${route.id} docs are missing required page ${page}.md or ${page}.mdx`); + } + } + const indexPath = routeSourcePagePath(route, 'index'); + const indexMarkdown = readText(indexPath); + const landingId = guideSummaryIds[route.id]; + if (!landingId || !indexMarkdown.includes(``)) { + fail(`${route.id} SDK overview is missing the SDK landing component`); + } + const requiredOverviewHeadings = [ + 'Install', + 'Open And Query', + 'Runtime Shape', + 'App Responsibilities', + 'First Query', + ]; + let previousHeadingIndex = -1; + for (const heading of requiredOverviewHeadings) { + const headingPattern = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu'); + const headingIndex = indexMarkdown.search(headingPattern); + if (!headingPattern.test(indexMarkdown)) { + fail(`${route.id} SDK overview is missing required section: ${heading}`); + } + if (headingIndex < previousHeadingIndex) { + fail( + `${route.id} SDK overview sections must use this order: ${requiredOverviewHeadings.join(' -> ')}`, + ); + } + previousHeadingIndex = headingIndex; + } + const guidePath = routeSourcePagePath(route, 'guide'); + const guideMarkdown = readText(guidePath); + const guideSummaryId = guideSummaryIds[route.id]; + const hasGuideSummary = + guideSummaryId && guideMarkdown.includes(``); + const hasEquivalentGuideSummary = + route.id === 'oliphaunt-react-native' && + guideMarkdown.includes(''); + if (!hasGuideSummary && !hasEquivalentGuideSummary) { + fail(`${route.id} developer guide is missing the SDK guide summary component`); + } + if (!guideMarkdown.includes(``)) { + fail(`${route.id} developer guide is missing the SDK guide proof component`); + } + const expectedGuideHeadings = guideHeadingOrder[route.id] ?? guideHeadingOrder.default; + let previousGuideHeadingIndex = -1; + for (const heading of expectedGuideHeadings) { + const headingIndex = guideMarkdown.search( + new RegExp(`^###\\s+${escapeRegExp(heading)}\\s*$`, 'mu'), + ); + if (headingIndex < 0) { + fail(`${route.id} developer guide is missing required step heading: ${heading}`); + } + if (headingIndex < previousGuideHeadingIndex) { + fail( + `${route.id} developer guide steps must use this order: ${expectedGuideHeadings.join(' -> ')}`, + ); + } + previousGuideHeadingIndex = headingIndex; + } + if (!/^##\s+Troubleshooting\s*$/mu.test(guideMarkdown)) { + fail(`${route.id} developer guide is missing Troubleshooting`); + } + const sourceFiles = requiredPages.map((page) => + readText(routeSourcePagePath(route, page)).toLowerCase(), + ); + const combined = sourceFiles.join('\n'); + if (!combined.includes('exact') || !combined.includes('extension')) { + fail(`${route.id} docs must explain exact extension selection across its SDK section`); + } + if (!combined.includes('backup') || !combined.includes('restore')) { + fail(`${route.id} docs must include backup and restore guidance`); + } + if (route.id === 'oliphaunt-react-native') { + const architecturePath = routeSourcePagePath(route, 'architecture'); + if (!architecturePath) { + fail('React Native SDK docs are missing architecture.md or architecture.mdx'); + } + const architectureMarkdown = readText(architecturePath); + if (!architectureMarkdown.includes('')) { + fail('React Native architecture docs are missing ReactNativeBoundaryMap'); + } + for (const heading of [ + 'Runtime Ownership', + 'JavaScript Shape', + 'Binary Transport', + 'Config Plugin And Packaging', + 'Lifecycle', + 'Capabilities', + 'What The React Native SDK Owns', + ]) { + if (!new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu').test(architectureMarkdown)) { + fail(`React Native architecture docs are missing required section: ${heading}`); + } + } + } + if (route.id === 'oliphaunt-wasix') { + const runtimePath = routeSourcePagePath(route, 'runtime'); + if (!runtimePath) { + fail('WASM SDK docs are missing runtime.md or runtime.mdx'); + } + const runtimeMarkdown = readText(runtimePath); + if (!runtimeMarkdown.includes('')) { + fail('WASM runtime docs are missing WasmRuntimeMap'); + } + for (const heading of [ + 'Choose A Mode', + 'Persistence Modes', + 'Operational Limits', + 'Root Locking And Lifecycle', + 'Startup And Preload', + 'Supported Targets', + 'Server-Compatible Access', + ]) { + if (!new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu').test(runtimeMarkdown)) { + fail(`WASM runtime docs are missing required section: ${heading}`); + } + } + + const dumpRestorePath = routeSourcePagePath(route, 'dump-restore'); + if (!dumpRestorePath) { + fail('WASM SDK docs are missing dump-restore.md or dump-restore.mdx'); + } + const dumpRestoreMarkdown = readText(dumpRestorePath); + if (!dumpRestoreMarkdown.includes('')) { + fail('WASM dump/restore docs are missing WasmDataMovement'); + } + for (const heading of [ + 'Choose The Right Export Format', + 'Direct API', + 'Server API', + '`PgDumpOptions`', + 'CLI', + 'Restore', + 'Upgrade Guidance', + ]) { + if (!new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu').test(dumpRestoreMarkdown)) { + fail(`WASM dump/restore docs are missing required section: ${heading}`); + } + } + } + } +} + +function assertStartPageCoverage() { + const startRoute = manifest.routes.find((entry) => entry.id === 'start'); + if (!startRoute) { + fail('docs manifest is missing the Start route'); + } + const startPath = routeSourcePagePath(startRoute, 'index'); + if (!startPath) { + fail('Start docs are missing index.md or index.mdx'); + } + const markdown = readText(startPath); + const requiredComponents = ['QuickstartPath', 'FirstQueryFlow', 'StartNextSteps']; + for (const component of requiredComponents) { + if (!markdown.includes(`<${component}`)) { + fail(`Start docs are missing ${component}`); + } + } + const requiredHeadings = [ + 'Start In One App Target', + 'First Query Shape', + 'After The First Query', + ]; + let previousHeadingIndex = -1; + for (const heading of requiredHeadings) { + const headingIndex = markdown.search(new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu')); + if (headingIndex < 0) { + fail(`Start docs are missing required section: ${heading}`); + } + if (headingIndex < previousHeadingIndex) { + fail(`Start docs sections must use this order: ${requiredHeadings.join(' -> ')}`); + } + previousHeadingIndex = headingIndex; + } +} + +function assertReferencePageCoverage() { + const referenceRoute = manifest.routes.find((entry) => entry.id === 'reference'); + if (!referenceRoute) { + fail('docs manifest is missing the Reference route'); + } + const requirements = [ + { + page: 'capabilities', + title: 'Capability Matrix', + components: ['CapabilitySnapshot'], + headings: ['SDKs', 'Runtime Modes', 'Feature Support', 'Choosing A Mode'], + }, + { + page: 'extensions', + title: 'Extensions', + components: ['ExactExtensionRule', 'ExtensionArtifactFlow'], + headings: [ + 'How Selection Works', + 'Platform Behavior', + 'Dependencies', + 'External Extensions', + 'Verifying App Artifacts', + ], + }, + { + page: 'performance', + title: 'Performance', + components: ['PerformanceResultsGrid'], + headings: [ + 'What to measure', + 'Compare modes honestly', + 'SQLite comparison', + 'Release Measurements', + ], + }, + { + page: 'releases', + title: 'Releases', + components: ['ReleaseLookup'], + headings: [ + 'First Release Boundary', + 'Version Relationships', + 'Target Availability', + 'What A Release Tells You', + 'Docs Versioning', + ], + }, + ]; + for (const requirement of requirements) { + const pagePath = routeSourcePagePath(referenceRoute, requirement.page); + if (!pagePath) { + fail(`Reference docs are missing ${requirement.page}.md or ${requirement.page}.mdx`); + } + const markdown = readText(pagePath); + if (!new RegExp(`^#\\s+${escapeRegExp(requirement.title)}\\s*$`, 'mu').test(markdown)) { + fail(`Reference page ${requirement.page} is missing title heading: ${requirement.title}`); + } + for (const component of requirement.components) { + if (!markdown.includes(`<${component}`)) { + fail(`Reference page ${requirement.page} is missing ${component}`); + } + } + let previousHeadingIndex = -1; + for (const heading of requirement.headings) { + const headingIndex = markdown.search( + new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu'), + ); + if (headingIndex < 0) { + fail(`Reference page ${requirement.page} is missing required section: ${heading}`); + } + if (headingIndex < previousHeadingIndex) { + fail( + `Reference page ${requirement.page} sections must use this order: ${requirement.headings.join(' -> ')}`, + ); + } + previousHeadingIndex = headingIndex; + } + } +} + +function assertLearnPageCoverage() { + const learnRoute = manifest.routes.find((entry) => entry.id === 'learn'); + if (!learnRoute) { + fail('docs manifest is missing the Learn route'); + } + const requirements = [ + { + page: 'embedded-postgres', + title: 'Embedded PostgreSQL', + components: ['EmbeddedPostgresModel'], + headings: [ + 'Root Storage', + 'Lifecycle Contract', + 'Extension Selection', + 'What is different from SQLite?', + ], + }, + { + page: 'native-runtime', + title: 'Native Runtime', + components: ['ModeMatrix'], + headings: [ + 'Choose a mode', + 'Runtime Semantics', + 'Direct Lifecycle', + 'Storage', + 'Startup Configuration', + 'Extensions', + 'Capabilities', + ], + }, + { + page: 'mobile-stability', + title: 'Mobile Stability', + components: ['MobileStabilityContract'], + headings: [ + 'What developers can rely on', + 'Close and reopen', + 'Background and foreground', + 'Choosing the mode', + ], + }, + { + page: 'sqlite-upgrade', + title: 'Moving From SQLite', + components: ['SqliteMigrationMap'], + headings: [ + 'Concept Map', + 'Schema And SQL Differences', + 'Storage And Backup', + 'Migration Path', + 'When SQLite Is Still The Better Fit', + ], + }, + { + page: 'tauri', + title: 'Tauri Usage', + components: ['TauriAppPattern'], + headings: [ + 'App Shape', + 'Direct Rust State', + 'Existing Postgres Clients', + 'Extensions And Assets', + 'Backup And Restore', + 'Operational Guidance', + ], + }, + ]; + for (const requirement of requirements) { + const pagePath = routeSourcePagePath(learnRoute, requirement.page); + if (!pagePath) { + fail(`Learn docs are missing ${requirement.page}.md or ${requirement.page}.mdx`); + } + const markdown = readText(pagePath); + if (!new RegExp(`^#\\s+${escapeRegExp(requirement.title)}\\s*$`, 'mu').test(markdown)) { + fail(`Learn page ${requirement.page} is missing title heading: ${requirement.title}`); + } + for (const component of requirement.components) { + if (!markdown.includes(`<${component}`)) { + fail(`Learn page ${requirement.page} is missing ${component}`); + } + } + let previousHeadingIndex = -1; + for (const heading of requirement.headings) { + const headingIndex = markdown.search( + new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, 'mu'), + ); + if (headingIndex < 0) { + fail(`Learn page ${requirement.page} is missing required section: ${heading}`); + } + if (headingIndex < previousHeadingIndex) { + fail( + `Learn page ${requirement.page} sections must use this order: ${requirement.headings.join(' -> ')}`, + ); + } + previousHeadingIndex = headingIndex; + } + } +} + +function markerDisplayId(marker) { + if (!marker) { + return ''; + } + const colon = marker.lastIndexOf(':'); + if (colon >= 0) { + return marker.slice(colon + 1); + } + const parts = marker.trim().split(/\s+/); + return parts[parts.length - 1] ?? marker; +} + +function assertSnippetMarkers() { + for (const route of manifest.routes.filter((entry) => entry.kind === 'sdk')) { + const snippetPath = route.tested_snippet_path; + const marker = route.tested_snippet_marker; + if (!snippetPath || !marker) { + fail(`SDK route ${route.id} must declare tested snippet path and marker`); + } + const source = readText(snippetPath); + if (!source.includes(marker)) { + fail(`${route.id} snippet source is missing marker "${marker}" in ${snippetPath}`); + } + const guidePath = routeSourcePagePath(route, 'guide'); + if (!guidePath) { + fail(`${route.id} guide source is missing`); + } + const guide = readText(guidePath); + if (!guide.includes(`oliphaunt-snippet: ${route.id}`)) { + fail(`${route.id} guide must include the manifest-owned snippet directive`); + } + } +} + +function flattenNavigationItems(items, output = []) { + for (const item of items ?? []) { + if (typeof item === 'string') { + output.push(item); + } else if (item?.type === 'category') { + flattenNavigationItems(item.items, output); + } + } + return output; +} + +function assertFumadocsMetaCoverage() { + const rootMeta = JSON.parse(fs.readFileSync(path.join(siteDocsRoot, 'meta.json'), 'utf8')); + for (const section of ['start', 'sdk', 'learn', 'reference']) { + if (!(rootMeta.pages ?? []).includes(section)) { + fail(`Fumadocs root meta is missing section ${section}`); + } + } + for (const section of ['concepts', 'guides', 'releases']) { + if ((rootMeta.pages ?? []).includes(section)) { + fail(`Fumadocs root meta still exposes stale shallow section ${section}`); + } + } + + for (const route of manifest.routes ?? []) { + const metaPath = path.join(siteDocsRoot, route.route, 'meta.json'); + if (!fs.existsSync(metaPath)) { + fail(`Fumadocs meta missing for route ${route.route}`); + } + const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); + const firstSegments = new Set(sidebarPagesForRoute(route).map((page) => page.split('/')[0])); + for (const segment of firstSegments) { + const present = (meta.pages ?? []).includes(segment) || meta.pagesIndex === segment; + if (!present) { + fail(`Fumadocs meta for ${route.route} is missing page or folder ${segment}`); + } + } + } +} + +function assertNavigationCoverage() { + const navigationPath = path.join(generatedMetaRoot, 'navigation.json'); + const navigation = JSON.parse(fs.readFileSync(navigationPath, 'utf8')); + const navigationItems = new Set(flattenNavigationItems(navigation.docs)); + const docIds = new Set(routeRecords.map((record) => record.docId)); + for (const item of navigationItems) { + if (!docIds.has(item)) { + fail(`generated navigation references missing doc id: ${item}`); + } + } + for (const route of manifest.routes ?? []) { + for (const page of sidebarPagesForRoute(route)) { + const item = `${route.route}/${page}`; + if (!navigationItems.has(item)) { + fail(`generated navigation missing sidebar page ${item}`); + } + } + } +} + +function assertApiReferenceSummary({ requireGenerated = false } = {}) { + const apiFileNames = { + 'liboliphaunt-native': 'c-abi', + 'oliphaunt-rust': 'rust', + 'oliphaunt-swift': 'swift', + 'oliphaunt-kotlin': 'kotlin', + 'oliphaunt-react-native': 'react-native', + 'oliphaunt-js': 'typescript', + 'oliphaunt-wasix': 'wasm', + }; + const expected = new Set( + manifest.routes.filter((entry) => entry.kind === 'sdk').map((entry) => entry.id), + ); + const records = new Map((apiSummary.records ?? []).map((record) => [record.id, record])); + for (const id of expected) { + const record = records.get(id); + if (!record) { + fail(`API reference summary missing ${id}`); + } + if (!record.status || record.status === 'stub' || record.status === 'failed') { + fail(`API reference status for ${id} is not truthful`); + } + if (!record.artifact) { + fail(`API reference summary for ${id} is missing an artifact path`); + } + if (requireGenerated && record.status !== 'generated') { + fail( + `API reference generation did not complete for ${id}: ${record.reason ?? record.status}`, + ); + } + } + for (const record of records.values()) { + const apiPage = apiFileNames[record.id] ?? record.id; + if (!routePageSet('reference').has(`api/${apiPage}`)) { + continue; + } + const siteApiPage = path.join(siteDocsRoot, 'reference', 'api', `${apiPage}.md`); + if (!fs.existsSync(siteApiPage)) { + fail(`generated API reference site page missing for ${record.id}`); + } + } +} + +function assertSdkManifestCoverage() { + const manifestSdkIds = new Set(Object.keys(sdkManifest.sdks ?? {})); + const required = ['rust', 'swift', 'kotlin', 'react-native', 'typescript']; + for (const sdk of required) { + if (!manifestSdkIds.has(sdk)) { + fail(`SDK manifest missing ${sdk}`); + } + } +} + +function assertReleaseGraphPolicy() { + if (releaseGraph.products?.['docs']) { + fail('docs must not be a release product'); + } +} + +function assertNoNodeModulesGenerated() { + const bad = routeRecords.filter((record) => + record.file.includes(`${path.sep}node_modules${path.sep}`), + ); + if (bad.length > 0) { + fail(`docs generator traversed node_modules:\n${bad.map((record) => record.file).join('\n')}`); + } +} + +function assertMdxComponentPagesStayMdx() { + const componentPattern = + /<(SdkChooser|SdkLanding|SdkGuideProof|StartOutcome|StartNextSteps|EmbeddedPostgresModel|MobileStabilityContract|SqliteMigrationMap|TauriAppPattern|ReactNativeBoundaryMap|WasmRuntimeMap|WasmDataMovement|CapabilitySnapshot|ExtensionArtifactFlow|PerformanceResultsGrid|ReleaseLookup|QuickstartPath|FirstQueryFlow|VerifyChecklist|ModeMatrix|ExactExtensionRule|Steps|Step|Callout|Tabs|Tab|Cards|Card|Files|File|Folder)\b/u; + const bad = routeRecords.filter( + (record) => record.file.endsWith('.md') && componentPattern.test(readText(record.source)), + ); + if (bad.length > 0) { + fail( + `docs pages with React components must be emitted as .mdx, not .md:\n${bad.map((record) => record.source).join('\n')}`, + ); + } +} + +function assertPublicDocsLanguageHygiene() { + const disallowed = [ + { label: 'stale sdk-parity route', pattern: /sdk-parity/u }, + { label: 'source checkout', pattern: /\bsource checkout\b/iu }, + { label: 'stale Expo Go wording', pattern: /\bExpo Go\b/u }, + { label: 'stale base64 transport wording', pattern: /\bbase64\b/iu }, + { label: 'advisory should wording', pattern: /\bshould\b/iu }, + { label: 'defensive should-not wording', pattern: /\bshould not\b/iu }, + { label: 'planning phrase "not pretend"', pattern: /\bnot pretend\b/iu }, + { label: 'runtime smoke evidence', pattern: /\bruntime smoke evidence\b/iu }, + { label: 'package evidence', pattern: /\bpackage evidence\b/iu }, + { label: 'real device evidence', pattern: /\breal device evidence\b/iu }, + { label: 'internal evidence wording', pattern: /\bevidence\b/iu }, + { + label: 'future or placeholder language', + pattern: /\b(?:TODO|placeholder|not yet|coming soon|eventually|can be added later)\b/iu, + }, + { label: 'release metadata internals', pattern: /\brelease metadata\b/iu }, + { label: 'maintainer-facing language', pattern: /\bmaintainer\b/iu }, + { label: 'internal-facing language', pattern: /\binternal\b/iu }, + { label: 'CI internals', pattern: /\bCI\b/u }, + { label: 'tooling path', pattern: /tools\//u }, + { label: 'source path', pattern: /src\//u }, + { label: 'target path', pattern: /target\//u }, + { label: 'fixture path', pattern: /fixtures\//u }, + { label: 'repo-structure language', pattern: /\bmonorepo\b/iu }, + { label: 'pre-release status language', pattern: /\bbefore the first stable\b/iu }, + { label: 'publication timing language', pattern: /\bonce release artifacts are published\b/iu }, + { label: 'defensive fallback wording', pattern: /\bfallback paths\b/iu }, + { + label: 'stale unavailable extension wording', + pattern: /\b(?:not available|not selected|not a pack)\b/iu, + }, + { label: 'stale WASM comparison wording', pattern: /\bOlder WASM examples\b/u }, + { label: 'defensive crash isolation wording', pattern: /\bCrash isolation belongs\b/u }, + { + label: 'defensive unsupported wording', + pattern: /\bunsupported (?:operation|extension|extensions)\b/iu, + }, + { label: 'internal lane wording', pattern: /\blane\b/iu }, + ]; + const failures = []; + for (const record of routeRecords) { + const markdown = readText(record.source); + const lines = markdown.split('\n'); + lines.forEach((line, index) => { + for (const rule of disallowed) { + if (rule.pattern.test(line)) { + failures.push(`${record.route}:${index + 1}: ${rule.label}: ${line.trim()}`); + } + } + }); + } + if (failures.length > 0) { + fail(`public generated docs include maintainer or planning language:\n${failures.join('\n')}`); + } +} + +function walkPublicTextFiles(dirPath, output = []) { + if (!fs.existsSync(dirPath)) { + return output; + } + for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) { + const fullPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + walkPublicTextFiles(fullPath, output); + continue; + } + if (entry.isFile() && /\.(?:html|json|md|mdx|txt|xml)$/iu.test(entry.name)) { + output.push(fullPath); + } + } + return output; +} + +function assertPublicGeneratedOutputHygiene() { + const publicApiArtifacts = path.join(staticRoot, 'api-artifacts'); + if (!apiReferenceRequested && fs.existsSync(publicApiArtifacts)) { + fail( + 'default docs builds must not publish API reference artifacts; run the explicit api-reference task when those artifacts are needed', + ); + } + + const disallowed = [ + { label: 'stale sdk-parity route', pattern: /sdk-parity/iu }, + { label: 'source checkout', pattern: /\bsource checkout\b/iu }, + { label: 'stale Expo Go wording', pattern: /\bExpo Go\b/u }, + { label: 'stale base64 transport wording', pattern: /\bbase64\b/iu }, + { label: 'advisory should wording', pattern: /\bshould\b/iu }, + { label: 'defensive should-not wording', pattern: /\bshould not\b/iu }, + { label: 'planning phrase "not pretend"', pattern: /\bnot pretend\b/iu }, + { label: 'runtime smoke evidence', pattern: /\bruntime smoke evidence\b/iu }, + { label: 'package evidence', pattern: /\bpackage evidence\b/iu }, + { label: 'real device evidence', pattern: /\breal device evidence\b/iu }, + { label: 'internal evidence wording', pattern: /\bevidence\b/iu }, + { + label: 'future or placeholder language', + pattern: /\b(?:TODO|placeholder|not yet|coming soon|eventually|can be added later)\b/iu, + }, + { label: 'release metadata internals', pattern: /\brelease metadata\b/iu }, + { label: 'maintainer-facing language', pattern: /\bmaintainer\b/iu }, + { label: 'internal-facing language', pattern: /\binternal\b/iu }, + { label: 'CI internals', pattern: /\bCI\b/u }, + { label: 'tooling path', pattern: /tools\//u }, + { label: 'source path', pattern: /src\//u }, + { label: 'target path', pattern: /target\//u }, + { label: 'fixture path', pattern: /fixtures\//u }, + { label: 'repo-structure language', pattern: /\bmonorepo\b/iu }, + { label: 'pre-release status language', pattern: /\bbefore the first stable\b/iu }, + { label: 'publication timing language', pattern: /\bonce release artifacts are published\b/iu }, + { label: 'defensive fallback wording', pattern: /\bfallback paths\b/iu }, + { + label: 'stale unavailable extension wording', + pattern: /\b(?:not available|not selected|not a pack)\b/iu, + }, + { label: 'stale WASM comparison wording', pattern: /\bOlder WASM examples\b/u }, + { label: 'defensive crash isolation wording', pattern: /\bCrash isolation belongs\b/u }, + { + label: 'defensive unsupported wording', + pattern: /\bunsupported (?:operation|extension|extensions)\b/iu, + }, + { label: 'internal lane wording', pattern: /\blane\b/iu }, + { + label: 'generated API field', + pattern: /\b(?:implementation_path|documentation_path|tested_snippet|reference_artifact)\b/iu, + }, + { label: 'raw extension source kind', pattern: /\boliphaunt-other-extension\b/iu }, + { label: 'unrendered extension placeholder', pattern: /@EXTVERSION@|@MODULEPATH@/u }, + { label: 'generated reference wording', pattern: /\bgenerated language reference/iu }, + { + label: 'removed upstream reference', + pattern: new RegExp(`\\b${'pg'}${'lite'}\\b`, 'iu'), + }, + ]; + const failures = []; + for (const file of [...walkPublicTextFiles(siteDocsRoot), ...walkPublicTextFiles(staticRoot)]) { + const relative = path.relative(repoRoot, file); + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, index) => { + for (const rule of disallowed) { + if (rule.pattern.test(line)) { + failures.push(`${relative}:${index + 1}: ${rule.label}: ${line.trim()}`); + } + } + }); + } + if (failures.length > 0) { + fail( + `public generated docs output includes maintainer or planning language:\n${failures.join('\n')}`, + ); + } +} + +function assertReleaseReadinessDocs() { + for (const route of manifest.routes.filter((entry) => entry.kind === 'sdk')) { + const productId = route.product_id; + const product = releaseGraph.products?.[productId]; + if (!product) { + fail(`release metadata missing docs product ${productId}`); + } + if (product.changelog_path) { + requireFile(product.changelog_path); + } + for (const page of ['index', 'guide', 'api-reference']) { + const pagePath = routeSourcePagePath(route, page); + if (!pagePath) { + fail(`${productId} release docs are missing ${page}.md or ${page}.mdx`); + } + const markdown = readText(pagePath); + if (!markdown.includes('# ')) { + fail(`${productId} release docs page ${page}.md is missing a title heading`); + } + } + } +} + +function assertSdkInstallReleaseContracts() { + const releasePlease = JSON.parse(readText('release-please-config.json')); + const packages = Object.values(releasePlease.packages ?? {}); + const packageConfig = (component) => { + const matches = packages.filter((entry) => entry?.component === component); + if (matches.length !== 1) { + fail(`release-please must define exactly one ${component} package`); + } + return matches[0]; + }; + const initialVersion = (component) => + packageConfig(component)['initial-version'] ?? releasePlease['initial-version']; + const swiftVersion = initialVersion('oliphaunt-swift'); + const kotlinVersion = initialVersion('oliphaunt-kotlin'); + if (swiftVersion !== '0.6.0') { + fail(`the first SwiftPM-compatible Oliphaunt version must remain 0.6.0; got ${swiftVersion}`); + } + + const required = new Map([ + [ + 'src/docs/content/sdk/swift/index.mdx', + `.package(url: "https://github.com/f0rr0/oliphaunt.git", from: "${swiftVersion}")`, + ], + [ + 'src/docs/content/sdk/swift/guide.mdx', + `.package(url: "https://github.com/f0rr0/oliphaunt.git", from: "${swiftVersion}")`, + ], + [ + 'src/sdks/swift/README.md', + `.package(url: "https://github.com/f0rr0/oliphaunt.git", exact: "${swiftVersion}")`, + ], + [ + 'src/docs/content/sdk/kotlin/index.mdx', + `implementation("dev.oliphaunt:oliphaunt-android:${kotlinVersion}")`, + ], + [ + 'src/docs/content/sdk/kotlin/guide.mdx', + `implementation("dev.oliphaunt:oliphaunt-android:${kotlinVersion}")`, + ], + [ + 'src/sdks/kotlin/README.md', + `implementation("dev.oliphaunt:oliphaunt-android:${kotlinVersion}")`, + ], + ['src/docs/src/lib/docs-data.ts', `packageName: 'dev.oliphaunt:oliphaunt-android'`], + ]); + for (const [file, text] of required) { + if (!readText(file).includes(text)) { + fail(`${file} must use the release-owned SDK install contract ${JSON.stringify(text)}`); + } + } + + const publicKotlin = [ + 'src/docs/content/sdk/kotlin/index.mdx', + 'src/docs/content/sdk/kotlin/guide.mdx', + 'src/docs/src/lib/docs-data.ts', + ] + .map(readText) + .join('\n'); + if (publicKotlin.includes('dev.oliphaunt:oliphaunt:')) { + fail( + 'public Kotlin install docs must not advertise the unpublished dev.oliphaunt:oliphaunt coordinate', + ); + } + const typescriptDocs = [ + 'src/docs/content/sdk/typescript/index.mdx', + 'src/docs/content/reference/sdk-products.mdx', + ] + .map(readText) + .join('\n'); + if ( + !typescriptDocs.includes('JSR package intentionally contains only') || + !typescriptDocs.includes('JSR distribution is deliberately limited') + ) { + fail( + 'TypeScript public docs must distinguish the native npm distribution from protocol/query-only JSR', + ); + } +} + +assertNoTrackedRootProductsDocs(); +assertNoProductLocalPublicDocs(); +assertNoTrackedRootPublicDocs(); +assertRootDocsBuckets(); +assertNoDocsMoonProject(); +assertDocsChromeDoesNotExposeSourcePaths(); +assertUniqueRoutes(); +assertGeneratedFiles(); +assertGeneratedFumadocsMetadata(); +assertSdkSidebarPages(); +assertReferenceSidebarPages(); +assertNoStaleGeneratedNavigation(); +assertPublicContentIsMarkdownOnly(); +assertPublicContentMetadata(); +assertApplicabilityMetadata(); +assertLightweightVersioning(); +assertRouteCoverage(); +assertPublicRootLandingPages(); +assertLlmRouteCoverage(); +assertDocsInternalLinksResolve(); +assertStartPageCoverage(); +assertLearnPageCoverage(); +assertReferencePageCoverage(); +assertSdkSectionCoverage(); +assertSnippetMarkers(); +assertSdkManifestCoverage(); +assertReleaseGraphPolicy(); +assertNoNodeModulesGenerated(); +assertMdxComponentPagesStayMdx(); +assertPublicDocsLanguageHygiene(); +assertPublicGeneratedOutputHygiene(); +assertFumadocsMetaCoverage(); +assertNavigationCoverage(); +assertApiReferenceSummary({ requireGenerated: apiReferenceRequested }); +assertSdkInstallReleaseContracts(); + +if (args.has('--release')) { + assertReleaseReadinessDocs(); +} + +if (args.has('--snippets')) { + assertSnippetMarkers(); +} + +console.log(`docs product checks passed (${routeRecords.length} routes)`); diff --git a/src/docs/tools/check-fumadocs-source.mjs b/src/docs/tools/check-fumadocs-source.mjs new file mode 100644 index 00000000..a6f19fa1 --- /dev/null +++ b/src/docs/tools/check-fumadocs-source.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(scriptDir, '..'); +const sourceRoot = path.join(docsRoot, '.source'); + +const requiredFiles = [ + { + file: 'server.ts', + pattern: /\bexport\s+const\s+docs\b/u, + }, + { + file: 'browser.ts', + pattern: /\bexport\s+default\s+browserCollections\b/u, + }, + { + file: 'dynamic.ts', + pattern: /\bdynamic<.*\bConfig\b/su, + }, +]; + +for (const required of requiredFiles) { + const filePath = path.join(sourceRoot, required.file); + if (!fs.existsSync(filePath)) { + console.error(`Fumadocs generated source is missing: .source/${required.file}`); + process.exit(1); + } + const text = fs.readFileSync(filePath, 'utf8'); + if (text.trim().length === 0) { + console.error(`Fumadocs generated source is empty: .source/${required.file}`); + process.exit(1); + } + if (!required.pattern.test(text)) { + console.error(`Fumadocs generated source is malformed: .source/${required.file}`); + process.exit(1); + } +} diff --git a/src/docs/tools/generate-api-reference.mjs b/src/docs/tools/generate-api-reference.mjs new file mode 100644 index 00000000..f817ef35 --- /dev/null +++ b/src/docs/tools/generate-api-reference.mjs @@ -0,0 +1,462 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { parse as parseToml } from 'smol-toml'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(scriptDir, '..'); +const repoRoot = path.resolve(scriptDir, '../../..'); +const manifestPath = path.join(docsRoot, 'docs-manifest.toml'); +const apiRoot = path.join(repoRoot, 'target', 'docs', 'generated', 'api'); +const summaryPath = path.join(apiRoot, 'summary.json'); +const defaultCommandTimeoutMs = Number.parseInt( + process.env.OLIPHAUNT_DOCS_API_TIMEOUT_MS ?? '600000', + 10, +); + +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function parseTomlFile(filePath) { + return parseToml(readText(filePath)); +} + +function ensureDir(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function relative(filePath) { + return path.relative(repoRoot, filePath).replaceAll(path.sep, '/'); +} + +function commandExists(command) { + try { + execFileSync('sh', ['-c', `command -v ${command}`], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + return true; + } catch { + return false; + } +} + +function run(command, args, options = {}) { + execFileSync(command, args, { + cwd: repoRoot, + encoding: 'utf8', + stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit', + timeout: options.timeout ?? defaultCommandTimeoutMs, + env: { + ...process.env, + ...options.env, + }, + }); +} + +function commandFailureStatus(error) { + if (error?.signal === 'SIGTERM' || error?.killed || error?.code === 'ETIMEDOUT') { + return 'skipped'; + } + return 'failed'; +} + +function commandFailureReason(error) { + if (error?.signal === 'SIGTERM' || error?.killed || error?.code === 'ETIMEDOUT') { + return `timed out after ${defaultCommandTimeoutMs}ms`; + } + return error?.message ?? 'command failed'; +} + +function statusRecord(route, status, details) { + return { + id: route.id, + productId: route.product_id, + title: route.title, + referenceKind: route.reference_kind, + status, + ...details, + }; +} + +function parseCHeader(headerPath) { + const header = readText(headerPath); + const withoutComments = header.replace(/\/\*[\s\S]*?\*\//g, ''); + const functions = [ + ...withoutComments.matchAll( + /\b(?:int32_t|uint64_t|void|const\s+char\s+\*)\s+(oliphaunt_[a-z0-9_]+)\s*\(([\s\S]*?)\);/g, + ), + ].map((match) => ({ + name: match[1], + args: match[2].replace(/\s+/g, ' ').trim(), + })); + const constants = [ + ...header.matchAll(/^#define[ \t]+(OLIPHAUNT_[A-Z0-9_]+)(?:[ \t]+([^\r\n]+))?$/gm), + ] + .map((match) => ({ + name: match[1], + value: (match[2] ?? '').trim(), + })) + .filter((constant) => constant.value.length > 0); + return { functions, constants }; +} + +function escapeXml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function writeCReference(manifest, route, fullMode) { + const config = manifest.api_reference?.c ?? {}; + const headerPath = path.join( + repoRoot, + config.header ?? 'src/runtimes/liboliphaunt/native/include/oliphaunt.h', + ); + const outputRoot = path.join(apiRoot, 'c'); + const xmlRoot = path.join(outputRoot, 'xml'); + ensureDir(xmlRoot); + + const parsed = parseCHeader(headerPath); + const xml = ` + + + ${escapeXml(relative(headerPath))} + +${parsed.constants + .map( + (constant) => + ` ${escapeXml(constant.name)}${escapeXml(constant.value)}`, + ) + .join('\n')} + + +${parsed.functions + .map( + (fn) => + ` ${escapeXml(fn.name)}(${escapeXml(fn.args)})`, + ) + .join('\n')} + + + +`; + const fallbackXmlPath = path.join(xmlRoot, 'oliphaunt-header.xml'); + fs.writeFileSync(fallbackXmlPath, xml); + + const markdownPath = path.join(outputRoot, 'reference.md'); + fs.writeFileSync( + markdownPath, + `# C ABI Reference + +Generated from \`${relative(headerPath)}\`. + +## Functions + +${parsed.functions.map((fn) => `- \`${fn.name}(${fn.args})\``).join('\n')} + +## Constants + +${parsed.constants.map((constant) => `- \`${constant.name}\` = \`${constant.value}\``).join('\n')} +`, + ); + + let doxygenStatus = 'not-run'; + let doxygenXmlPath = ''; + let doxygenFailureReason = ''; + const doxygenConfig = config.doxygen_config; + const expectedDoxygenXml = path.join(apiRoot, 'c', 'doxygen', 'xml', 'index.xml'); + if (fullMode && doxygenConfig) { + if (commandExists('doxygen')) { + try { + run('doxygen', [doxygenConfig]); + if (fs.existsSync(expectedDoxygenXml)) { + doxygenStatus = 'generated'; + doxygenXmlPath = relative(expectedDoxygenXml); + } else { + doxygenStatus = 'failed: expected Doxygen XML index missing'; + doxygenFailureReason = 'Doxygen completed but expected XML index is missing'; + } + } catch (error) { + doxygenStatus = `failed: ${error.message}`; + doxygenFailureReason = commandFailureReason(error); + } + } else { + doxygenStatus = 'failed: doxygen not installed'; + doxygenFailureReason = 'doxygen not installed'; + } + } else if (doxygenConfig && fs.existsSync(expectedDoxygenXml)) { + doxygenStatus = 'generated'; + doxygenXmlPath = relative(expectedDoxygenXml); + } + const fullModeRequiresDoxygen = fullMode && Boolean(doxygenConfig); + const generatedByDoxygen = doxygenStatus === 'generated'; + + return statusRecord( + route, + fullModeRequiresDoxygen && !generatedByDoxygen ? 'failed' : 'generated', + { + artifact: relative(markdownPath), + machineReadableArtifact: relative(fallbackXmlPath), + docsEntry: relative(markdownPath), + symbolCount: parsed.functions.length, + constantCount: parsed.constants.length, + generator: generatedByDoxygen ? 'doxygen+xml' : 'header-parser+xml', + doxygenStatus, + doxygenXmlPath, + reason: doxygenFailureReason, + }, + ); +} + +function runCargoDoc(route, packageName, outputKey, fullMode) { + const outputRoot = path.join(apiRoot, outputKey); + ensureDir(outputRoot); + const docsEntry = path.join(outputRoot, 'doc', packageName.replaceAll('-', '_'), 'index.html'); + if (!fullMode) { + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'configured', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'cargo doc', + reason: fs.existsSync(docsEntry) + ? 'using existing generated rustdoc artifact' + : 'full rustdoc generation runs in release documentation checks', + }); + } + if (!commandExists('cargo')) { + return statusRecord(route, 'skipped', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'cargo doc', + reason: 'cargo not installed', + }); + } + try { + run('cargo', [ + 'doc', + '--no-deps', + '--package', + packageName, + '--target-dir', + relative(outputRoot), + ]); + run('cargo', ['test', '--doc', '--package', packageName], { env: { RUSTDOCFLAGS: '' } }); + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'failed', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'cargo doc', + reason: fs.existsSync(docsEntry) + ? '' + : 'cargo doc completed but expected index.html is missing', + }); + } catch (error) { + return statusRecord(route, commandFailureStatus(error), { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'cargo doc', + reason: commandFailureReason(error), + }); + } +} + +function runSwiftDocC(manifest, route, fullMode) { + const config = manifest.api_reference?.swift ?? {}; + const outputRoot = path.join(apiRoot, 'swift'); + ensureDir(outputRoot); + const docsEntry = path.join(outputRoot, 'Oliphaunt.doccarchive'); + if (!fullMode) { + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'configured', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Swift-DocC', + reason: fs.existsSync(docsEntry) + ? 'using existing generated DocC archive' + : 'full DocC generation runs in release documentation checks', + }); + } + if (!commandExists('swift')) { + return statusRecord(route, 'skipped', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Swift-DocC', + reason: 'swift not installed', + }); + } + try { + fs.rmSync(docsEntry, { force: true, recursive: true }); + run('swift', [ + 'package', + '--package-path', + config.package_path ?? 'src/sdks/swift', + '--allow-writing-to-directory', + relative(outputRoot), + 'generate-documentation', + '--target', + config.target ?? 'Oliphaunt', + '--output-path', + relative(docsEntry), + '--disable-indexing', + ]); + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'failed', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Swift-DocC', + reason: fs.existsSync(docsEntry) + ? '' + : 'Swift-DocC completed but expected archive is missing', + }); + } catch (error) { + return statusRecord(route, commandFailureStatus(error), { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Swift-DocC', + reason: commandFailureReason(error), + }); + } +} + +function runKotlinDokka(manifest, route, fullMode) { + const config = manifest.api_reference?.kotlin ?? {}; + const projectPath = path.join(repoRoot, config.project_path ?? 'src/sdks/kotlin'); + const gradlew = path.join(projectPath, 'gradlew'); + const docsEntry = path.join(apiRoot, 'kotlin', 'html', 'index.html'); + if (!fullMode) { + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'configured', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Dokka v2', + reason: fs.existsSync(docsEntry) + ? 'using existing generated Dokka artifact' + : 'full Dokka generation runs in release documentation checks', + }); + } + if (!fs.existsSync(gradlew)) { + return statusRecord(route, 'skipped', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Dokka v2', + reason: 'Gradle wrapper missing', + }); + } + try { + execFileSync( + gradlew, + ['--no-daemon', config.task ?? ':oliphaunt:dokkaGeneratePublicationHtml'], + { + cwd: projectPath, + stdio: 'inherit', + timeout: defaultCommandTimeoutMs, + env: { + ...process.env, + OLIPHAUNT_GRADLE_BUILD_ROOT: path.join(repoRoot, 'target', 'oliphaunt-gradle-build'), + }, + }, + ); + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'failed', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Dokka v2', + reason: fs.existsSync(docsEntry) ? '' : 'Dokka completed but expected index.html is missing', + }); + } catch (error) { + return statusRecord(route, commandFailureStatus(error), { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'Dokka v2', + reason: commandFailureReason(error), + }); + } +} + +function runTypeDoc(route, packagePath, outputKey, fullMode) { + const docsEntry = path.join(apiRoot, outputKey, 'html', 'index.html'); + if (!fullMode) { + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'configured', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'TypeDoc', + reason: fs.existsSync(docsEntry) + ? 'using existing generated TypeDoc artifact' + : 'full TypeDoc generation runs in release documentation checks', + }); + } + try { + run('pnpm', ['--dir', packagePath, 'run', 'docs:api']); + return statusRecord(route, fs.existsSync(docsEntry) ? 'generated' : 'failed', { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'TypeDoc', + reason: fs.existsSync(docsEntry) + ? '' + : 'TypeDoc completed but expected index.html is missing', + }); + } catch (error) { + return statusRecord(route, commandFailureStatus(error), { + artifact: relative(docsEntry), + docsEntry: relative(docsEntry), + generator: 'TypeDoc', + reason: commandFailureReason(error), + }); + } +} + +function routeById(manifest, id) { + return manifest.routes.find((route) => route.id === id); +} + +export function generateApiReferenceArtifacts(options = {}) { + const manifest = options.manifest ?? parseTomlFile(manifestPath); + const fullMode = options.mode === 'release' || options.mode === 'full'; + ensureDir(apiRoot); + fs.rmSync(summaryPath, { force: true }); + + const records = [ + writeCReference(manifest, routeById(manifest, 'liboliphaunt-native'), fullMode), + runCargoDoc(routeById(manifest, 'oliphaunt-rust'), 'oliphaunt', 'rust', fullMode), + runSwiftDocC(manifest, routeById(manifest, 'oliphaunt-swift'), fullMode), + runKotlinDokka(manifest, routeById(manifest, 'oliphaunt-kotlin'), fullMode), + runTypeDoc( + routeById(manifest, 'oliphaunt-react-native'), + 'src/sdks/react-native', + 'react-native', + fullMode, + ), + runTypeDoc(routeById(manifest, 'oliphaunt-js'), 'src/sdks/js', 'typescript', fullMode), + runCargoDoc(routeById(manifest, 'oliphaunt-wasix'), 'oliphaunt-wasix', 'wasm', fullMode), + ]; + + const summary = { + mode: fullMode ? 'release' : 'fast', + generatedAt: new Date().toISOString(), + records, + }; + fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`); + return summary; +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + const modeArg = process.argv.find((arg) => arg.startsWith('--mode=')); + const mode = modeArg ? modeArg.split('=')[1] : 'fast'; + const summary = generateApiReferenceArtifacts({ mode }); + console.log( + `generated API reference status for ${summary.records.length} surfaces (${summary.mode})`, + ); + const requireGenerated = + mode === 'release' || process.env.OLIPHAUNT_DOCS_REQUIRE_NATIVE_API === '1'; + const failed = summary.records.filter((record) => + requireGenerated ? record.status !== 'generated' : record.status === 'failed', + ); + if (failed.length > 0) { + for (const record of failed) { + console.error(`${record.id}: ${record.status}: ${record.reason || record.doxygenStatus}`); + } + process.exit(1); + } +} diff --git a/src/docs/tools/generate-content.mjs b/src/docs/tools/generate-content.mjs new file mode 100644 index 00000000..e3ca1d7e --- /dev/null +++ b/src/docs/tools/generate-content.mjs @@ -0,0 +1,1247 @@ +#!/usr/bin/env node +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { parse as parseToml } from 'smol-toml'; + +import { generateApiReferenceArtifacts } from './generate-api-reference.mjs'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(scriptDir, '..'); +const repoRoot = path.resolve(scriptDir, '../../..'); +const manifestPath = path.join(docsRoot, 'docs-manifest.toml'); +const generatedRoot = path.join(repoRoot, 'target', 'docs'); +const siteDocsRoot = path.join(generatedRoot, 'site-docs'); +const staticRoot = path.join(generatedRoot, 'static'); +const staticApiArtifactsRoot = path.join(staticRoot, 'api-artifacts'); +const generatedMetaRoot = path.join(generatedRoot, 'generated'); +const generationLockDir = path.join(generatedRoot, '.generate.lock'); +const generationLockMetadata = path.join(generationLockDir, 'owner.json'); + +const SKIP_DIRS = new Set(['node_modules', '.git', '.moon', '.docusaurus', 'build', 'target']); + +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function parseTomlFile(filePath) { + return parseToml(readText(filePath)); +} + +function parseJsonFile(filePath) { + return JSON.parse(readText(filePath)); +} + +function releaseProductMetadata() { + const releasePlease = parseJsonFile(path.join(repoRoot, 'release-please-config.json')); + const releaseManifest = parseJsonFile(path.join(repoRoot, '.release-please-manifest.json')); + const packages = releasePlease.packages ?? {}; + const tagSeparator = releasePlease['tag-separator'] ?? '-'; + const tagVersionPrefix = releasePlease['include-v-in-tag'] === false ? '' : 'v'; + const defaultInitialVersion = releasePlease['initial-version']; + const linkedVersionGroups = new Map(); + for (const plugin of releasePlease.plugins ?? []) { + if (plugin?.type !== 'linked-versions' || typeof plugin.groupName !== 'string') { + continue; + } + for (const component of plugin.components ?? []) { + linkedVersionGroups.set(component, plugin.groupName); + } + } + const products = {}; + for (const [packagePath, packageConfig] of Object.entries(packages)) { + const productId = packageConfig.component; + if (!productId) { + throw new Error(`release-please package ${packagePath} is missing component`); + } + const metadata = parseTomlFile(path.join(repoRoot, packagePath, 'release.toml')); + const currentVersion = releaseManifest[packagePath]; + const initialVersion = packageConfig['initial-version'] ?? defaultInitialVersion; + if (typeof currentVersion !== 'string' || typeof initialVersion !== 'string') { + throw new Error(`release version metadata is incomplete for ${productId}`); + } + const extensionVersioning = metadata.extension?.versioning; + const linkedVersionGroup = linkedVersionGroups.get(productId); + products[productId] = { + ...metadata, + current_version: currentVersion, + initial_version: initialVersion, + version_relationship: + extensionVersioning ?? + (linkedVersionGroup ? `linked:${linkedVersionGroup}` : 'independent'), + tag_prefix: `${productId}${tagSeparator}${tagVersionPrefix}`, + }; + } + return { + policy: { + repository: 'f0rr0/oliphaunt', + default_branch: 'main', + versioning: 'independent', + extension_selection: 'exact-sql-extension', + }, + input_groups: {}, + products, + }; +} + +function ensureDir(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +function resetDir(dirPath) { + fs.rmSync(dirPath, { force: true, recursive: true }); + ensureDir(dirPath); +} + +function resetGeneratedMetadata() { + ensureDir(generatedMetaRoot); + for (const entry of fs.readdirSync(generatedMetaRoot, { withFileTypes: true })) { + if (entry.name === 'api') { + continue; + } + fs.rmSync(path.join(generatedMetaRoot, entry.name), { force: true, recursive: true }); + } +} + +function sleep(milliseconds) { + const buffer = new SharedArrayBuffer(4); + const view = new Int32Array(buffer); + Atomics.wait(view, 0, 0, milliseconds); +} + +function processIsAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) { + return false; + } + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function removeStaleGenerationLock() { + if (!fs.existsSync(generationLockDir)) { + return false; + } + try { + if (fs.existsSync(generationLockMetadata)) { + const metadata = JSON.parse(readText(generationLockMetadata)); + if (!processIsAlive(metadata.pid)) { + fs.rmSync(generationLockDir, { force: true, recursive: true }); + return true; + } + return false; + } + const stat = fs.statSync(generationLockDir); + if (Date.now() - stat.mtimeMs > 120_000) { + fs.rmSync(generationLockDir, { force: true, recursive: true }); + return true; + } + } catch { + fs.rmSync(generationLockDir, { force: true, recursive: true }); + return true; + } + return false; +} + +function withGenerationLock(callback) { + ensureDir(generatedRoot); + const started = Date.now(); + while (true) { + try { + fs.mkdirSync(generationLockDir); + fs.writeFileSync( + generationLockMetadata, + `${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2)}\n`, + ); + break; + } catch (error) { + if (error?.code !== 'EEXIST') { + throw error; + } + if (removeStaleGenerationLock()) { + continue; + } + if (Date.now() - started > 120_000) { + throw new Error('timed out waiting for docs generation lock'); + } + sleep(100); + } + } + try { + return callback(); + } finally { + fs.rmSync(generationLockDir, { force: true, recursive: true }); + } +} + +function assertInsideRepo(relativePath, label) { + if (!relativePath || path.isAbsolute(relativePath) || relativePath.includes('\0')) { + throw new Error(`${label} must be a repository-relative path`); + } + const resolved = path.resolve(repoRoot, relativePath); + if (!resolved.startsWith(repoRoot + path.sep)) { + throw new Error(`${label} escapes the repository: ${relativePath}`); + } + return resolved; +} + +function replaceSnippetDirectives(markdown, context) { + return markdown.replace( + //giu, + (match, routeId) => { + if (!context.sdkRoutesById.has(routeId)) { + throw new Error(`unknown docs snippet route id: ${routeId}`); + } + return ''; + }, + ); +} + +function yamlString(value) { + return JSON.stringify(String(value ?? '')); +} + +function ensureTitleFrontmatter(markdown, fallbackTitle) { + const title = firstHeading(markdown, fallbackTitle); + const frontmatterMatch = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u); + if (!frontmatterMatch) { + return `---\ntitle: ${yamlString(title)}\n---\n\n${markdown}`; + } + if (/^title\s*:/mu.test(frontmatterMatch[1])) { + return markdown; + } + return markdown.replace(/^---\r?\n/u, `---\ntitle: ${yamlString(title)}\n`); +} + +function stripMatchingLeadingTitleHeading(markdown) { + const title = frontmatterValue(markdown, 'title'); + if (!title) { + return markdown; + } + const frontmatterMatch = markdown.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/u); + const prefix = frontmatterMatch ? frontmatterMatch[1] : ''; + const body = frontmatterMatch ? frontmatterMatch[2] : markdown; + const headingPattern = /^(\s*)#\s+(.+?)\s*#?\s*(?:\r?\n|$)/u; + const headingMatch = body.match(headingPattern); + if (!headingMatch || headingMatch[1].trim().length > 0) { + return markdown; + } + if (headingMatch[2].trim() !== title) { + return markdown; + } + const strippedBody = body.slice(headingMatch[0].length).replace(/^\r?\n/u, ''); + return `${prefix}${strippedBody}`; +} + +function normalizePageMarkdown(markdown, fallbackTitle) { + return stripMatchingLeadingTitleHeading(ensureTitleFrontmatter(markdown, fallbackTitle)); +} + +function normalizeCodeFenceInfoStrings(markdown) { + return markdown.replace( + /^(`{3,})([A-Za-z0-9_+-]+),([^\r\n]*)$/gmu, + (_match, fence, lang, meta) => { + return `${fence}${lang} ${meta.trim()}`; + }, + ); +} + +function copyDir(source, destination, context) { + if (!fs.existsSync(source)) { + throw new Error(`docs source does not exist: ${path.relative(repoRoot, source)}`); + } + ensureDir(destination); + for (const entry of fs.readdirSync(source, { withFileTypes: true })) { + if (SKIP_DIRS.has(entry.name)) { + continue; + } + const from = path.join(source, entry.name); + const to = path.join(destination, entry.name); + if (entry.isDirectory()) { + copyDir(from, to, context); + } else if (entry.isFile()) { + if (/\.mdx?$/u.test(entry.name)) { + const markdown = normalizeCodeFenceInfoStrings( + replaceSnippetDirectives(readText(from), context), + ); + const fallbackTitle = path.basename(entry.name, path.extname(entry.name)); + fs.writeFileSync(to, normalizePageMarkdown(markdown, fallbackTitle)); + } else { + fs.copyFileSync(from, to); + } + } + } +} + +function routeSourcePagePath(source, page) { + for (const extension of ['.md', '.mdx']) { + const candidate = path.join(source, `${page}${extension}`); + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +function copyMarkdownPage(from, to, context) { + const markdown = normalizeCodeFenceInfoStrings(replaceSnippetDirectives(readText(from), context)); + const fallbackTitle = path.basename(from, path.extname(from)); + ensureDir(path.dirname(to)); + fs.writeFileSync(to, normalizePageMarkdown(markdown, fallbackTitle)); +} + +function copyRoutePages(route, context) { + const source = assertInsideRepo(route.source, `source for ${route.id}`); + const destination = path.join(siteDocsRoot, route.route); + ensureDir(destination); + for (const page of uniqueInOrder([ + ...(route.page_order ?? []), + ...(route.required_pages ?? []), + ])) { + const from = routeSourcePagePath(source, page); + if (!from) { + continue; + } + const to = path.join(destination, `${page}${path.extname(from)}`); + copyMarkdownPage(from, to, context); + } +} + +function copyStaticPath(source, destination) { + if (!fs.existsSync(source)) { + return false; + } + ensureDir(path.dirname(destination)); + const stat = fs.statSync(source); + if (stat.isDirectory()) { + fs.cpSync(source, destination, { force: true, recursive: true }); + } else if (stat.isFile()) { + fs.copyFileSync(source, destination); + } + return true; +} + +function escapeMarkdown(value) { + return String(value ?? '') + .replaceAll('\\', '\\\\') + .replaceAll('|', '\\|') + .replaceAll('\n', ' '); +} + +function firstHeading(markdown, fallback) { + const match = markdown.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : fallback; +} + +function frontmatterValue(markdown, key) { + const frontmatterMatch = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u); + if (!frontmatterMatch) { + return ''; + } + const match = frontmatterMatch[1].match(new RegExp(`^${key}\\s*:\\s*(.+)$`, 'mu')); + if (!match) { + return ''; + } + return match[1].trim().replace(/^["']|["']$/gu, ''); +} + +function collectMarkdownFiles(root) { + const files = []; + function visit(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (SKIP_DIRS.has(entry.name)) { + continue; + } + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + visit(fullPath); + } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) { + files.push(fullPath); + } + } + } + if (fs.existsSync(root)) { + visit(root); + } + return files.sort(); +} + +function markdownRouteFor(filePath) { + const relative = path.relative(siteDocsRoot, filePath).replaceAll(path.sep, '/'); + const withoutExtension = relative.replace(/\.mdx?$/, ''); + const route = withoutExtension.replace(/\/index$/, ''); + return `/${route}`; +} + +function markdownDocIdFor(filePath) { + return path + .relative(siteDocsRoot, filePath) + .replaceAll(path.sep, '/') + .replace(/\.mdx?$/, ''); +} + +function releaseProducts(releaseGraph) { + return Object.entries(releaseGraph.products ?? {}).sort(([left], [right]) => + left.localeCompare(right), + ); +} + +function sdkRows(sdkManifest) { + return Object.entries(sdkManifest.sdks ?? {}).sort(([left], [right]) => + left.localeCompare(right), + ); +} + +function generateSdkMatrix(sdkManifest) { + const rows = sdkRows(sdkManifest).map(([id, sdk]) => { + const modes = (sdk.available_modes ?? []).join(', '); + return `| ${escapeMarkdown(id)} | ${escapeMarkdown(sdk.package_identity)} | ${escapeMarkdown((sdk.supported_consumer_targets ?? []).join(', '))} | ${escapeMarkdown((sdk.planned_consumer_targets ?? []).join(', ') || 'none')} | ${escapeMarkdown(sdk.runtime_boundary)} | ${escapeMarkdown(modes)} |`; + }); + return `--- +title: SDK Matrix +--- + +# SDK Matrix + +Use this matrix to compare registry-qualified package identities, supported and +planned consumer targets, runtime boundaries, and advertised modes. + +| SDK | Package identity | Supported targets | Planned targets | Runtime boundary | Advertised modes | +| --- | --- | --- | --- | --- | --- | +${rows.join('\n')} +`; +} + +function generatePlatformMatrix(sdkManifest) { + const rows = sdkRows(sdkManifest).flatMap(([id, sdk]) => + (sdk.supported_consumer_targets ?? []).map( + (target) => + `| ${escapeMarkdown(target)} | ${escapeMarkdown(id)} | ${escapeMarkdown(sdk.package_identity)} |`, + ), + ); + return `--- +title: Platform And Package Matrix +--- + +# Platform And Package Matrix + +Use this matrix to pick the package for each app target. + +| Platform target | SDK | Package | +| --- | --- | --- | +${rows.join('\n')} +`; +} + +function generateExtensionCatalog() { + const catalogPath = path.join(repoRoot, 'src/extensions/generated/extensions.catalog.json'); + if (!fs.existsSync(catalogPath)) { + throw new Error('extension catalog source is required for public docs generation'); + } + const catalog = JSON.parse(readText(catalogPath)); + const rows = (catalog.extensions ?? []) + .filter((extension) => { + const promotion = extension.promotion ?? {}; + return promotion.stable && promotion.packaged && promotion.promoted; + }) + .sort((left, right) => + String(left['sql-name'] ?? left.id).localeCompare(String(right['sql-name'] ?? right.id)), + ) + .map((extension) => { + const control = extension.control ?? {}; + return `| ${escapeMarkdown(extension['sql-name'] ?? extension.id)} | ${escapeMarkdown(extension['display-name'] ?? extension.id)} | ${escapeMarkdown(extensionVersion(control['default-version']))} | ${escapeMarkdown(extensionFamily(extension['source-kind']))} | ${escapeMarkdown(extensionActivation(extension))} |`; + }); + return `--- +title: Extension Catalog +--- + +# Extension Catalog + +Use this table to find exact SQL extension names. SDK and app packaging +selection uses the SQL extension name. The table shows stable packaged +extensions only. + +| SQL extension | Display name | Version | Family | Activation | +| --- | --- | --- | --- | --- | +${rows.join('\n')} +`; +} + +function extensionVersion(version) { + if (!version || String(version).includes('@')) { + return 'Packaged with runtime'; + } + return version; +} + +function extensionFamily(sourceKind) { + const labels = { + 'postgres-contrib': 'PostgreSQL contrib', + 'oliphaunt-other-extension': 'External extension', + postgis: 'PostGIS', + }; + return labels[sourceKind] ?? 'Extension artifact'; +} + +function extensionActivation(extension) { + if (extension.lifecycle?.['create-extension'] === false) { + return 'Runtime module'; + } + return 'CREATE EXTENSION'; +} + +function statusLabel(status) { + if (status === 'generated') { + return 'generated'; + } + if (status === 'configured') { + return 'configured'; + } + if (status === 'skipped') { + return 'skipped'; + } + return status || 'unknown'; +} + +function referenceLabel(referenceKind) { + const labels = { + doxygen: 'Doxygen header reference', + rustdoc: 'Rustdoc', + 'swift-docc': 'Swift DocC', + dokka: 'Dokka', + typedoc: 'TypeDoc', + }; + return labels[referenceKind] ?? referenceKind ?? 'API reference'; +} + +function link(label, href) { + return href ? `[${label}](${href})` : ''; +} + +function apiArtifactHref(relativePath) { + return `/api-artifacts/${relativePath}`; +} + +function staticArtifactPlan(record) { + const plans = { + 'liboliphaunt-native': [ + { + source: record.artifact, + destination: 'c/reference.md', + href: apiArtifactHref('c/reference.md'), + label: 'Open C ABI Markdown', + }, + { + source: record.machineReadableArtifact, + destination: 'c/xml/oliphaunt-header.xml', + href: apiArtifactHref('c/xml/oliphaunt-header.xml'), + label: 'Open C ABI XML', + }, + { + source: record.doxygenXmlPath, + destination: 'c/doxygen/xml/index.xml', + href: apiArtifactHref('c/doxygen/xml/index.xml'), + label: 'Open Doxygen XML index', + }, + ], + 'oliphaunt-rust': [ + { + source: 'target/docs/generated/api/rust/doc', + destination: 'rust/doc', + href: apiArtifactHref('rust/doc/oliphaunt/index.html'), + label: 'Open rustdoc', + }, + ], + 'oliphaunt-swift': [ + { + source: record.artifact, + destination: 'swift/Oliphaunt.doccarchive', + href: apiArtifactHref('swift/Oliphaunt.doccarchive/index.html'), + label: 'Open Swift DocC archive', + }, + ], + 'oliphaunt-kotlin': [ + { + source: 'target/docs/generated/api/kotlin/html', + destination: 'kotlin/html', + href: apiArtifactHref('kotlin/html/index.html'), + label: 'Open Dokka reference', + }, + ], + 'oliphaunt-react-native': [ + { + source: 'target/docs/generated/api/react-native/html', + destination: 'react-native/html', + href: apiArtifactHref('react-native/html/index.html'), + label: 'Open TypeDoc reference', + }, + ], + 'oliphaunt-js': [ + { + source: 'target/docs/generated/api/typescript/html', + destination: 'typescript/html', + href: apiArtifactHref('typescript/html/index.html'), + label: 'Open TypeDoc reference', + }, + ], + 'oliphaunt-wasix': [ + { + source: 'target/docs/generated/api/wasm/doc', + destination: 'wasm/doc', + href: apiArtifactHref('wasm/doc/oliphaunt_wasix/index.html'), + label: 'Open WASM rustdoc', + }, + ], + }; + return plans[record.id] ?? []; +} + +function copyApiArtifactsToStatic(apiSummary) { + const linksByRecordId = new Map(); + for (const record of apiSummary.records ?? []) { + const links = []; + for (const plan of staticArtifactPlan(record)) { + if (!plan.source) { + continue; + } + const source = assertInsideRepo(plan.source, `API artifact for ${record.id}`); + const destination = path.join(staticApiArtifactsRoot, plan.destination); + if (copyStaticPath(source, destination)) { + links.push({ + label: plan.label, + href: plan.href, + }); + } + } + linksByRecordId.set(record.id, links); + } + return linksByRecordId; +} + +function cReferenceBody(record) { + if (record.id !== 'liboliphaunt-native' || !record.artifact) { + return ''; + } + const artifactPath = path.join(repoRoot, record.artifact); + if (!fs.existsSync(artifactPath)) { + return ''; + } + return readText(artifactPath) + .replace(/^# C ABI Reference\s*/u, '') + .trim(); +} + +function generateApiReference(manifest) { + const rows = manifest.routes + .filter((route) => route.kind === 'sdk') + .map((route) => { + const reference = referenceLabel(route.reference_kind); + return `| ${escapeMarkdown(route.title)} | [Open](/docs/${route.route}/api-reference) | ${escapeMarkdown(reference)} |`; + }); + return `--- +title: API Reference +--- + +# API Reference + +Use this page when you know the SDK and need the API surface by task. SDK guides +show the first integration path. These maps point to the language reference for +configuration, query results, lifecycle, extension selection, backup and +restore, and error handling. + +## Choose By Task + +| Task | Look for | +| --- | --- | +| Open a database | builder or open configuration, root storage, runtime mode, durability | +| Run SQL | query, execute, parameters, row access, result typing | +| Use raw protocol | raw bytes, streaming, response ownership, cancellation | +| Manage lifecycle | close, background, foreground, cancellation, capability checks | +| Move data | backup, restore, dump, archive validation | +| Ship extensions | exact SQL extension names, dependency files, artifact reports | +| Handle errors | SDK errors, PostgreSQL SQLSTATE data, capability errors | + +## Language References + +| Surface | Reference page | Native reference format | +| --- | --- | --- | +${rows.join('\n')} +`; +} + +function generateSdkApiReferencePage(record, artifactLinks = []) { + const cBody = cReferenceBody(record); + const links = artifactLinks + .map((artifactLink) => `- ${link(artifactLink.label, artifactLink.href)}`) + .join('\n'); + const reference = referenceLabel(record.referenceKind); + return `--- +title: ${record.title} +--- + +# ${record.title} + +Use this page with the ${reference}. Product guides explain runtime behavior; +the API reference gives exact declarations for the released SDK. + +${statusLabel(record.status) === 'generated' && links ? `## Reference\n\n${links}\n` : ''} +${cBody ? `## Symbols\n\n${cBody}\n` : ''} +`; +} + +function apiReferenceFileName(record) { + const names = { + 'liboliphaunt-native': 'c-abi', + 'oliphaunt-rust': 'rust', + 'oliphaunt-swift': 'swift', + 'oliphaunt-kotlin': 'kotlin', + 'oliphaunt-react-native': 'react-native', + 'oliphaunt-js': 'typescript', + 'oliphaunt-wasix': 'wasm', + }; + return names[record.id] ?? record.id; +} + +function generateTestedSnippets(manifest) { + const rows = manifest.routes + .filter((route) => route.kind === 'sdk') + .map( + (route) => + `| ${escapeMarkdown(route.title)} | ${escapeMarkdown(route.tested_snippet_marker)} | ${escapeMarkdown(route.tested_snippet_path)} |`, + ); + return `--- +title: Tested Snippets +--- + +# Tested Snippets + +Public SDK snippets are tied to executable product tests or smoke files by +marker. The docs checker fails when a marker disappears. + +| Surface | Marker | Executable source | +| --- | --- | --- | +${rows.join('\n')} +`; +} + +function generateArtifactProvenance(releaseGraph) { + const rows = releaseProducts(releaseGraph).map(([id, product]) => { + return `| ${escapeMarkdown(id)} | ${escapeMarkdown((product.publish_targets ?? []).join(', '))} | ${escapeMarkdown((product.release_artifacts ?? []).join(', '))} | ${escapeMarkdown(product.tag_prefix)} |`; + }); + return `--- +title: Artifact And Provenance Matrix +--- + +# Artifact And Provenance Matrix + +Release verification checks asset checksums, attestations, and registry +publication for these surfaces. + +| Product | Publish targets | Release artifacts | Tag prefix | +| --- | --- | --- | --- | +${rows.join('\n')} +`; +} + +function generateVersionMatrix(releaseGraph) { + const rows = releaseProducts(releaseGraph).map(([id, product]) => { + const currentVersion = + product.current_version === '0.0.0' + ? `${product.current_version} (unreleased)` + : product.current_version; + return `| ${escapeMarkdown(id)} | ${escapeMarkdown(currentVersion)} | ${escapeMarkdown(product.initial_version)} | ${escapeMarkdown(product.version_relationship)} | ${escapeMarkdown((product.publish_targets ?? []).join(', ') || 'none')} | ${escapeMarkdown(product.tag_prefix)} |`; + }); + return `--- +title: Version Matrix +--- + +# Version Matrix + +Products are versioned independently. + +The source version \`0.0.0\` is the unreleased sentinel, not a public registry +version. The first-public-version column is derived from Release Please's +global or per-product initial version. + +Use this matrix before upgrading an app dependency. Start with the package your +app installs, then read the products it depends on for runtime artifact, +extension, and compatibility notes. A linked or runtime-bound release +relationship does not turn the repository into one version. + +Release coupling is derived from Moon production and peer dependency scopes. + +| Product | Current source version | First public version | Version relationship | Publish targets | Tag prefix | +| --- | --- | --- | --- | --- | --- | +${rows.join('\n')} +`; +} + +function routePageSet(manifest, routeId) { + const route = (manifest.routes ?? []).find((entry) => entry.id === routeId); + return new Set(route?.page_order ?? []); +} + +function writeGeneratedReferencePages( + manifest, + sdkManifest, + releaseGraph, + apiSummary, + artifactLinksByRecordId, +) { + const referenceRoot = path.join(siteDocsRoot, 'reference'); + const apiRootForSite = path.join(referenceRoot, 'api'); + const referencePages = routePageSet(manifest, 'reference'); + ensureDir(referenceRoot); + if (referencePages.has('sdk-matrix')) { + fs.writeFileSync(path.join(referenceRoot, 'sdk-matrix.md'), generateSdkMatrix(sdkManifest)); + } + if (referencePages.has('platforms')) { + fs.writeFileSync(path.join(referenceRoot, 'platforms.md'), generatePlatformMatrix(sdkManifest)); + } + if (referencePages.has('extension-catalog')) { + fs.writeFileSync(path.join(referenceRoot, 'extension-catalog.md'), generateExtensionCatalog()); + } + if (referencePages.has('api-reference')) { + fs.writeFileSync(path.join(referenceRoot, 'api-reference.md'), generateApiReference(manifest)); + } + const apiPages = [...referencePages] + .filter((page) => page.startsWith('api/')) + .map((page) => page.slice('api/'.length)); + if (apiPages.length > 0) { + ensureDir(apiRootForSite); + for (const record of apiSummary.records ?? []) { + const fileName = apiReferenceFileName(record); + if (!apiPages.includes(fileName)) { + continue; + } + fs.writeFileSync( + path.join(apiRootForSite, `${fileName}.md`), + generateSdkApiReferencePage(record, artifactLinksByRecordId.get(record.id) ?? []), + ); + } + } + if (referencePages.has('tested-snippets')) { + fs.writeFileSync( + path.join(referenceRoot, 'tested-snippets.md'), + generateTestedSnippets(manifest), + ); + } + if (referencePages.has('artifact-provenance')) { + fs.writeFileSync( + path.join(referenceRoot, 'artifact-provenance.md'), + generateArtifactProvenance(releaseGraph), + ); + } + if (referencePages.has('version-matrix')) { + fs.writeFileSync( + path.join(referenceRoot, 'version-matrix.md'), + generateVersionMatrix(releaseGraph), + ); + } +} + +function writeMetadata(routeRecords) { + ensureDir(generatedMetaRoot); + fs.writeFileSync( + path.join(generatedMetaRoot, 'routes.json'), + `${JSON.stringify({ routes: routeRecords }, null, 2)}\n`, + ); +} + +function itemForPage(route, page) { + return `${route.route}/${page}`; +} + +const routePresentation = { + start: { + description: 'Install an SDK, open app-owned storage, and run the first PostgreSQL query.', + icon: 'Route', + defaultOpen: true, + collapsible: false, + }, + sdk: { + description: 'Choose Rust, Swift, Kotlin, React Native, TypeScript, WASM, or the C ABI.', + icon: 'PackageCheck', + defaultOpen: false, + }, + learn: { + description: + 'Understand embedded PostgreSQL storage, lifecycle, runtime modes, and migrations.', + icon: 'BookOpen', + defaultOpen: false, + }, + reference: { + description: 'Look up capabilities, extensions, releases, performance results, and API links.', + icon: 'SearchCheck', + defaultOpen: false, + }, + 'liboliphaunt-native': { + description: 'Stable C ABI, opaque handles, raw protocol bytes, and binding rules.', + icon: 'CodeXml', + }, + 'oliphaunt-rust': { + description: 'Rust and Tauri SDK with direct, broker, and server runtime modes.', + icon: 'Laptop', + }, + 'oliphaunt-swift': { + description: 'Apple SDK for iOS and macOS apps using Swift concurrency.', + icon: 'Smartphone', + }, + 'oliphaunt-kotlin': { + description: 'Android SDK with coroutine-first APIs and exact native resource packaging.', + icon: 'Smartphone', + }, + 'oliphaunt-react-native': { + description: 'New Architecture package with Expo config plugin, TurboModule, and JSI bytes.', + icon: 'Layers', + }, + 'oliphaunt-js': { + description: 'TypeScript SDK for Node.js, Bun, and Deno.', + icon: 'Braces', + }, + 'oliphaunt-wasix': { + description: 'First-class WASM/WASIX runtime family and dump/restore flows.', + icon: 'Boxes', + }, +}; + +function metadataForRoute(route) { + const presentation = routePresentation[route.id] ?? {}; + return Object.fromEntries( + Object.entries(presentation).filter(([, value]) => value !== undefined), + ); +} + +function category(label, items) { + return { + type: 'category', + label, + items, + }; +} + +function sidebarPagesForRoute(route) { + return route.sidebar_pages ?? route.page_order ?? []; +} + +function orderedItemsForRoute(route, routeRecords) { + const available = new Set( + routeRecords + .filter( + (record) => + record.route === `/${route.route}` || record.route.startsWith(`/${route.route}/`), + ) + .map((record) => record.docId), + ); + const declared = sidebarPagesForRoute(route); + const declaredItems = declared.map((page) => itemForPage(route, page)); + if (declared.length > 0) { + return declaredItems.filter((item) => available.has(item)); + } + return [...available].sort((left, right) => left.localeCompare(right)); +} + +function writeJson(filePath, value) { + ensureDir(path.dirname(filePath)); + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); +} + +function uniqueInOrder(values) { + const seen = new Set(); + const output = []; + for (const value of values) { + if (!seen.has(value)) { + seen.add(value); + output.push(value); + } + } + return output; +} + +function pageOrderForFumadocs(route) { + return uniqueInOrder( + sidebarPagesForRoute(route).map((page) => { + const [first] = page.split('/'); + return first; + }), + ); +} + +function titleForPathSegment(segment) { + return segment + .split('-') + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} + +function writeRouteMeta(route) { + const routeRoot = path.join(siteDocsRoot, route.route); + const metadata = { + title: route.title, + ...metadataForRoute(route), + pages: pageOrderForFumadocs(route), + }; + if (metadata.pages.includes('index')) { + metadata.pagesIndex = 'index'; + metadata.pages = metadata.pages.filter((page) => page !== 'index'); + } + if (route.kind === 'public') { + metadata.root = true; + metadata.description ??= `${route.title} documentation`; + } + writeJson(path.join(routeRoot, 'meta.json'), metadata); + + const nested = new Map(); + for (const page of sidebarPagesForRoute(route)) { + const parts = page.split('/'); + if (parts.length < 2) { + continue; + } + const [folder, child] = parts; + const children = nested.get(folder) ?? []; + children.push(child); + nested.set(folder, children); + } + + for (const [folder, pages] of nested) { + writeJson(path.join(routeRoot, folder, 'meta.json'), { + title: titleForPathSegment(folder), + pages: uniqueInOrder(pages), + }); + } +} + +function writeFumadocsMeta(manifest) { + const sdkRoutes = (manifest.routes ?? []).filter((route) => route.kind === 'sdk'); + writeJson(path.join(siteDocsRoot, 'meta.json'), { + title: 'Oliphaunt', + description: + 'Embedded PostgreSQL SDK documentation for native, mobile, desktop, React Native, TypeScript, and WASM apps.', + pages: ['start', 'sdk', 'learn', 'reference'], + }); + for (const route of manifest.routes ?? []) { + if (route.id !== 'sdk') { + writeRouteMeta(route); + } + } + writeJson(path.join(siteDocsRoot, 'sdk', 'meta.json'), { + title: 'SDKs', + description: routePresentation.sdk.description, + icon: routePresentation.sdk.icon, + root: true, + defaultOpen: routePresentation.sdk.defaultOpen, + pagesIndex: 'index', + pages: sdkRoutes.map((route) => route.route.replace(/^sdk\//u, '')), + }); +} + +function writeNavigationMetadata(manifest, routeRecords) { + const byId = new Map((manifest.routes ?? []).map((route) => [route.id, route])); + const sdkRoutes = (manifest.routes ?? []).filter((route) => route.kind === 'sdk'); + const navigation = { + docs: [ + 'start/index', + category('SDKs', [ + 'sdk/index', + ...sdkRoutes.map((route) => + category(route.title, orderedItemsForRoute(route, routeRecords)), + ), + ]), + category('Learn', orderedItemsForRoute(byId.get('learn'), routeRecords)), + category('Reference', orderedItemsForRoute(byId.get('reference'), routeRecords)), + ], + }; + writeJson(path.join(generatedMetaRoot, 'navigation.json'), navigation); + writeFumadocsMeta(manifest); +} + +function stripFrontmatter(markdown) { + return markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/u, ''); +} + +function writeLlmFiles(routeRecords) { + ensureDir(staticRoot); + const summary = [ + '# Oliphaunt Docs', + '', + 'Oliphaunt is embedded PostgreSQL for native, mobile, desktop, React Native, TypeScript, and WASM apps.', + '', + '## Public routes', + ...routeRecords.map((record) => `- ${record.title}: ${record.route}`), + '', + ].join('\n'); + fs.writeFileSync(path.join(staticRoot, 'llms.txt'), summary); + + const full = routeRecords + .map((record) => { + const markdown = stripFrontmatter(readText(record.file)); + return `# ${record.title}\n\nRoute: ${record.route}\n\n${markdown}`; + }) + .join('\n\n---\n\n'); + fs.writeFileSync(path.join(staticRoot, 'llms-full.txt'), full); +} + +function appliesToForRoute(route) { + if (route.applies_to) { + return String(route.applies_to); + } + if (route.kind === 'sdk' && route.product_id) { + return `current ${route.product_id}`; + } + return 'current'; +} + +function routeForGeneratedPage(manifest, pageRoute) { + const candidates = (manifest.routes ?? []) + .filter((route) => { + const root = `/${route.route}`; + return pageRoute === root || pageRoute.startsWith(`${root}/`); + }) + .sort((left, right) => right.route.length - left.route.length); + return candidates[0]; +} + +function ensureApplicabilityFrontmatter(markdown, appliesTo) { + const escaped = yamlString(appliesTo); + const frontmatterMatch = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u); + if (!frontmatterMatch) { + return `---\napplies_to: ${escaped}\n---\n\n${markdown}`; + } + if (/^applies_to\s*:/mu.test(frontmatterMatch[1])) { + return markdown; + } + return markdown.replace(/^---\r?\n/u, `---\napplies_to: ${escaped}\n`); +} + +function stampApplicabilityMetadata(manifest) { + for (const file of collectMarkdownFiles(siteDocsRoot)) { + const pageRoute = markdownRouteFor(file); + const route = routeForGeneratedPage(manifest, pageRoute); + if (!route) { + throw new Error(`no docs manifest route owns generated page ${pageRoute}`); + } + const markdown = readText(file); + fs.writeFileSync( + file, + stripMatchingLeadingTitleHeading( + ensureApplicabilityFrontmatter(markdown, appliesToForRoute(route)), + ), + ); + } +} + +function currentGitSha() { + try { + return execFileSync('git', ['rev-parse', '--short', 'HEAD'], { + cwd: repoRoot, + encoding: 'utf8', + }).trim(); + } catch { + return 'unknown'; + } +} + +function generateDocsUnlocked(options = {}) { + const manifest = parseTomlFile(manifestPath); + const sdkManifest = parseTomlFile(path.join(repoRoot, 'tools/policy/sdk-manifest.toml')); + const releaseGraph = releaseProductMetadata(); + const apiSummary = generateApiReferenceArtifacts({ + manifest, + mode: options.apiMode ?? 'fast', + }); + + resetDir(siteDocsRoot); + resetDir(staticRoot); + resetGeneratedMetadata(); + copyDir(path.join(docsRoot, 'static'), staticRoot); + const artifactLinksByRecordId = options.publishApiArtifacts + ? copyApiArtifactsToStatic(apiSummary) + : new Map(); + + const context = { + sdkRoutesById: new Map( + (manifest.routes ?? []) + .filter((route) => route.kind === 'sdk') + .map((route) => [route.id, route]), + ), + }; + + for (const route of manifest.routes ?? []) { + copyRoutePages(route, context); + } + + writeGeneratedReferencePages( + manifest, + sdkManifest, + releaseGraph, + apiSummary, + artifactLinksByRecordId, + ); + stampApplicabilityMetadata(manifest); + + const routeRecords = collectMarkdownFiles(siteDocsRoot).map((file) => { + const markdown = readText(file); + return { + route: markdownRouteFor(file), + docId: markdownDocIdFor(file), + title: + frontmatterValue(markdown, 'title') || + firstHeading(markdown, path.basename(file, path.extname(file))), + appliesTo: frontmatterValue(markdown, 'applies_to'), + file, + source: path.relative(repoRoot, file), + }; + }); + + writeMetadata(routeRecords); + writeNavigationMetadata(manifest, routeRecords); + writeLlmFiles(routeRecords); + fs.writeFileSync( + path.join(generatedMetaRoot, 'build-metadata.json'), + `${JSON.stringify( + { + generatedAt: new Date().toISOString(), + gitSha: currentGitSha(), + routeCount: routeRecords.length, + apiReferenceMode: apiSummary.mode, + apiArtifactsPublished: Boolean(options.publishApiArtifacts), + }, + null, + 2, + )}\n`, + ); + + return { + manifest, + sdkManifest, + releaseGraph, + apiSummary, + routeRecords, + paths: { + repoRoot, + docsRoot, + siteDocsRoot, + staticRoot, + generatedMetaRoot, + }, + }; +} + +export function generateDocs(options = {}) { + return withGenerationLock(() => generateDocsUnlocked(options)); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + const modeArg = process.argv.find((arg) => arg.startsWith('--api-mode=')); + const apiMode = modeArg ? modeArg.split('=')[1] : 'fast'; + const publishApiArtifacts = process.argv.includes('--publish-api-artifacts'); + const result = generateDocs({ apiMode, publishApiArtifacts }); + console.log( + `generated ${result.routeRecords.length} docs routes in target/docs (api artifacts published: ${publishApiArtifacts})`, + ); +} diff --git a/src/docs/tools/publish-next-export.mjs b/src/docs/tools/publish-next-export.mjs new file mode 100644 index 00000000..5f4f7589 --- /dev/null +++ b/src/docs/tools/publish-next-export.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(scriptDir, '..'); +const repoRoot = path.resolve(scriptDir, '../../..'); +const nextExportRoot = path.join(docsRoot, 'out'); +const generatedStaticRoot = path.join(repoRoot, 'target', 'docs', 'static'); +const buildRoot = path.join(repoRoot, 'target', 'docs', 'build'); + +function fail(message) { + console.error(message); + process.exit(1); +} + +if (!fs.existsSync(nextExportRoot)) { + fail('Next static export is missing; run next build before publishing docs output'); +} + +fs.rmSync(buildRoot, { force: true, recursive: true }); +fs.mkdirSync(path.dirname(buildRoot), { recursive: true }); +fs.cpSync(nextExportRoot, buildRoot, { force: true, recursive: true }); + +if (fs.existsSync(generatedStaticRoot)) { + fs.cpSync(generatedStaticRoot, buildRoot, { force: true, recursive: true }); +} + +console.log(`published docs static export to ${path.relative(repoRoot, buildRoot)}`); diff --git a/src/docs/tools/run-docs-task.mjs b/src/docs/tools/run-docs-task.mjs new file mode 100644 index 00000000..8604a63c --- /dev/null +++ b/src/docs/tools/run-docs-task.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const docsRoot = path.resolve(scriptDir, '..'); +const repoRoot = path.resolve(scriptDir, '../../..'); +const generatedRoot = path.join(repoRoot, 'target', 'docs'); +const lockDir = path.join(generatedRoot, '.docs-task.lock'); +const lockMetadata = path.join(lockDir, 'owner.json'); +const lockTimeoutMs = Number.parseInt(process.env.OLIPHAUNT_DOCS_LOCK_TIMEOUT_MS ?? '120000', 10); + +function fail(message) { + console.error(message); + process.exit(1); +} + +function sleep(milliseconds) { + const buffer = new SharedArrayBuffer(4); + const view = new Int32Array(buffer); + Atomics.wait(view, 0, 0, milliseconds); +} + +function processIsAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) { + return false; + } + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function removeStaleLock() { + if (!fs.existsSync(lockDir)) { + return false; + } + try { + if (fs.existsSync(lockMetadata)) { + const metadata = JSON.parse(fs.readFileSync(lockMetadata, 'utf8')); + if (!processIsAlive(metadata.pid)) { + fs.rmSync(lockDir, { force: true, recursive: true }); + return true; + } + return false; + } + const stat = fs.statSync(lockDir); + if (Date.now() - stat.mtimeMs > lockTimeoutMs) { + fs.rmSync(lockDir, { force: true, recursive: true }); + return true; + } + } catch { + fs.rmSync(lockDir, { force: true, recursive: true }); + return true; + } + return false; +} + +function acquireLock() { + fs.mkdirSync(generatedRoot, { recursive: true }); + const started = Date.now(); + while (Date.now() - started <= lockTimeoutMs) { + try { + fs.mkdirSync(lockDir); + fs.writeFileSync( + lockMetadata, + `${JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }, null, 2)}\n`, + ); + return () => fs.rmSync(lockDir, { force: true, recursive: true }); + } catch (error) { + if (error?.code !== 'EEXIST') { + throw error; + } + removeStaleLock(); + sleep(100); + } + } + fail(`timed out waiting for docs generation lock: ${path.relative(repoRoot, lockDir)}`); +} + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: docsRoot, + env: process.env, + stdio: 'inherit', + }); + if (result.error) { + fail(`could not run ${command}: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +const task = process.argv[2]; +const fumadocsMdx = ['pnpm', ['exec', 'fumadocs-mdx']]; +const checkFumadocsSource = ['node', ['tools/check-fumadocs-source.mjs']]; +const tasks = { + generate: [['node', ['tools/generate-content.mjs']], fumadocsMdx, checkFumadocsSource], + check: [ + ['node', ['tools/check-docs-product.mjs']], + fumadocsMdx, + checkFumadocsSource, + ['pnpm', ['exec', 'next', 'typegen']], + fumadocsMdx, + checkFumadocsSource, + ['pnpm', ['exec', 'tsc', '--noEmit']], + ], + build: [ + ['node', ['tools/check-docs-product.mjs']], + fumadocsMdx, + checkFumadocsSource, + ['pnpm', ['exec', 'next', 'build']], + fumadocsMdx, + checkFumadocsSource, + ['node', ['tools/publish-next-export.mjs']], + ], + 'release-check': [['node', ['tools/check-docs-product.mjs', '--release']]], +}; + +if (!Object.hasOwn(tasks, task)) { + fail(`usage: node tools/run-docs-task.mjs ${Object.keys(tasks).join('|')}`); +} + +const releaseLock = acquireLock(); +try { + for (const [command, args] of tasks[task]) { + run(command, args); + } +} finally { + releaseLock(); +} diff --git a/src/docs/tools/smoke-built-site.mjs b/src/docs/tools/smoke-built-site.mjs new file mode 100644 index 00000000..862951d3 --- /dev/null +++ b/src/docs/tools/smoke-built-site.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '../../..'); +const buildRoot = path.join(repoRoot, 'target', 'docs', 'build'); +const routesPath = path.join(repoRoot, 'target', 'docs', 'generated', 'routes.json'); + +function fail(message) { + console.error(message); + process.exit(1); +} + +function findFiles(root, predicate) { + const files = []; + function visit(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + visit(fullPath); + } else if (entry.isFile() && predicate(fullPath)) { + files.push(fullPath); + } + } + } + if (fs.existsSync(root)) { + visit(root); + } + return files; +} + +if (!fs.existsSync(buildRoot)) { + fail('docs build output is missing; run pnpm docs:build first'); +} + +if (!fs.existsSync(routesPath)) { + fail('generated docs route metadata is missing; run docs generation before smoke'); +} + +const htmlFiles = findFiles(buildRoot, (file) => file.endsWith('.html')); +if (htmlFiles.length === 0) { + fail('docs build produced no HTML files'); +} + +function routeHtmlPath(route) { + const normalized = route === '/' ? 'index' : route.replace(/^\/+/u, '').replace(/\/$/u, ''); + return path.join(buildRoot, normalized, 'index.html'); +} + +const routeMetadata = JSON.parse(fs.readFileSync(routesPath, 'utf8')); +for (const record of routeMetadata.routes ?? []) { + const route = record.route === '/' ? '/' : `/docs${record.route}`; + const htmlPath = routeHtmlPath(route); + if (!fs.existsSync(htmlPath)) { + fail( + `docs build did not export generated route ${route}: ${path.relative(repoRoot, htmlPath)}`, + ); + } +} + +const combined = htmlFiles.map((file) => fs.readFileSync(file, 'utf8')).join('\n'); +for (const phrase of ['Oliphaunt', 'Rust SDK', 'Extension Catalog', 'SQLite']) { + if (!combined.includes(phrase)) { + fail(`docs build output missing phrase: ${phrase}`); + } +} + +const disallowedHtml = [ + { label: 'directory listing', pattern: /Directory listing for/iu }, + { + label: 'removed upstream reference', + pattern: new RegExp(`\\b${'pg'}${'lite'}\\b`, 'iu'), + }, + { label: 'internal lane wording', pattern: /\blane\b/iu }, + { label: 'internal evidence wording', pattern: /\bevidence\b/iu }, + { label: 'future planning language', pattern: /\b(?:TODO|coming soon)\b/iu }, +]; +for (const file of htmlFiles) { + const html = fs.readFileSync(file, 'utf8'); + for (const rule of disallowedHtml) { + if (rule.pattern.test(html)) { + fail(`${path.relative(repoRoot, file)} contains ${rule.label}`); + } + } +} + +for (const staticFile of ['llms.txt', 'llms-full.txt']) { + const fullPath = path.join(buildRoot, staticFile); + if (!fs.existsSync(fullPath)) { + fail(`docs build did not publish ${staticFile}`); + } +} + +const faviconPath = path.join(buildRoot, 'img', 'favicon.svg'); +if (!fs.existsSync(faviconPath)) { + fail('docs build did not publish img/favicon.svg'); +} + +const hasFaviconLink = htmlFiles.some((file) => { + const html = fs.readFileSync(file, 'utf8'); + return /]+rel="(?:shortcut icon|icon)"[^>]+href="\/img\/favicon\.svg"/u.test(html); +}); +if (!hasFaviconLink) { + fail('docs build output is missing a favicon link'); +} + +console.log(`docs smoke passed (${htmlFiles.length} HTML files)`); diff --git a/src/docs/tsconfig.json b/src/docs/tsconfig.json new file mode 100644 index 00000000..bac1d839 --- /dev/null +++ b/src/docs/tsconfig.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "tsBuildInfoFile": "../../target/docs/tsconfig.tsbuildinfo", + "paths": { + "@/*": [ + "./src/*" + ], + "collections/*": [ + "./.source/*" + ] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/src/extensions/artifacts/native/moon.yml b/src/extensions/artifacts/native/moon.yml new file mode 100644 index 00000000..fc213293 --- /dev/null +++ b/src/extensions/artifacts/native/moon.yml @@ -0,0 +1,128 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-artifacts-native" +language: "unknown" +layer: "tool" +stack: "systems" +tags: ["extensions", "artifacts", "native"] +dependsOn: + - id: "extension-model" + scope: "build" + - id: "extensions" + scope: "build" + - id: "liboliphaunt-native" + scope: "build" + - id: "source-inputs" + scope: "build" + +project: + title: "Native Extension Artifacts" + description: "Publishable exact-extension artifact checks for native liboliphaunt targets." + owner: "oliphaunt" + +tasks: + check: + tags: ["quality", "static"] + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + deps: + - "extension-model:check" + inputs: + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/shared/extension-runtime-contract/**/*" + - "/tools/release/release_graph_query.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/tools/xtask/**/*" + - "/tools/release/check-linux-consumer-baseline.sh" + - "/tools/release/linux-abi-baseline.test.mjs" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/platform-compatibility-policy.test.mjs" + - "/tools/release/platform-binary-contract.mjs" + - "/tools/release/platform-binary-contract.test.mjs" + - "/Cargo.lock" + - "/Cargo.toml" + options: + cache: true + runFromWorkspaceRoot: true + release-check: + tags: ["release", "package"] + command: "bash src/extensions/artifacts/native/tools/check-release-artifacts.sh" + env: + OLIPHAUNT_TRACK_BUILD: "missing" + deps: + - "extension-artifacts-native:check" + - "source-inputs:source-fetch-native-runtime" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/postgres/versions/18/**/*" + - "/src/runtimes/liboliphaunt/native/**/*" + - "/src/shared/extension-runtime-contract/**/*" + - "/src/sdks/rust/extension-artifact-archive-policy.properties" + - "/src/sources/third-party/shared/**/*" + - "/src/sources/third-party/native/**/*" + - "/tools/xtask/**/*" + - "/tools/release/check-linux-consumer-baseline.sh" + - "/tools/release/linux-abi-baseline.test.mjs" + - "/tools/release/materialize-release-symlinks.mjs" + - "/tools/release/extension-artifact-archive-policy.mjs" + - "/tools/release/native-extension-asset-index-contract.mjs" + - "/tools/release/native-runtime-payload-policy.json" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/platform-compatibility-policy.test.mjs" + - "/tools/release/platform-binary-contract.mjs" + - "/tools/release/platform-binary-contract.test.mjs" + - "/tools/release/windows-vc-runtime-closure.mjs" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: true + build-target: + tags: ["release", "artifact-builder", "ci-extension-artifacts-native"] + command: "bash src/extensions/artifacts/native/tools/package-release-assets.sh" + deps: + - "extension-artifacts-native:check" + - "source-inputs:source-fetch-native-runtime" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/postgres/versions/18/**/*" + - "/src/runtimes/liboliphaunt/native/**/*" + - "/src/shared/extension-runtime-contract/**/*" + - "/src/sdks/rust/extension-artifact-archive-policy.properties" + - "/src/sources/third-party/shared/**/*" + - "/src/sources/third-party/native/**/*" + - "/tools/xtask/**/*" + - "/tools/release/check-linux-consumer-baseline.sh" + - "/tools/release/linux-abi-baseline.test.mjs" + - "/tools/release/materialize-release-symlinks.mjs" + - "/tools/release/extension-artifact-archive-policy.mjs" + - "/tools/release/native-extension-asset-index-contract.mjs" + - "/tools/release/native-runtime-payload-policy.json" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/platform-compatibility-policy.test.mjs" + - "/tools/release/platform-binary-contract.mjs" + - "/tools/release/platform-binary-contract.test.mjs" + - "/tools/release/windows-vc-runtime-closure.mjs" + outputs: + - "/target/extensions/native/release-assets/**/*" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: true diff --git a/src/extensions/artifacts/native/tools/check-release-artifacts.sh b/src/extensions/artifacts/native/tools/check-release-artifacts.sh new file mode 100755 index 00000000..089be488 --- /dev/null +++ b/src/extensions/artifacts/native/tools/check-release-artifacts.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)" || { + echo "check-native-extension-artifacts.sh: unable to determine repository root from $script_dir; run this script from a Git checkout" >&2 + exit 1 +} +cd "$root" + +target="${OLIPHAUNT_EXTENSION_TARGET:-macos-arm64}" +case "$target" in + macos-arm64|linux-x64-gnu|linux-arm64-gnu|windows-x64-msvc|ios-xcframework|android-arm64-v8a|android-x86_64) + exec src/extensions/artifacts/native/tools/package-release-assets.sh + ;; + *) + cat >&2 < right ? 1 : 0; +} + +async function readText(relativeOrAbsolute) { + return fs.readFile(path.isAbsolute(relativeOrAbsolute) ? relativeOrAbsolute : path.join(root, relativeOrAbsolute), 'utf8'); +} + +async function readJson(relativeOrAbsolute) { + return JSON.parse(await readText(relativeOrAbsolute)); +} + +async function readToml(relativeOrAbsolute) { + return Bun.TOML.parse(await readText(relativeOrAbsolute)); +} + +async function exists(file) { + try { + await fs.access(file); + return true; + } catch { + return false; + } +} + +async function isFile(file) { + try { + return (await fs.stat(file)).isFile(); + } catch { + return false; + } +} + +async function isDirectory(file) { + try { + return (await fs.stat(file)).isDirectory(); + } catch { + return false; + } +} + +function stringList(value) { + if (!Array.isArray(value)) { + return []; + } + return value.filter((item) => typeof item === 'string' && item.length > 0).sort(); +} + +function splitCsv(value) { + if (value === undefined || value === null || value === '' || value === '-') { + return []; + } + return String(value) + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +function sortedDeduped(values) { + return [...new Set(values.filter((item) => item !== undefined && item !== null && String(item).length > 0).map(String))].sort(); +} + +function dashIfEmpty(value) { + if (Array.isArray(value)) { + return value.length === 0 ? '-' : value.join(','); + } + return value === undefined || value === null || value === '' ? '-' : String(value); +} + +function yesNo(value) { + return value ? 'yes' : 'no'; +} + +function validatePortableId(value, label) { + if (!/^[A-Za-z0-9._-]{1,128}$/.test(value)) { + fail(`${label} '${value}' must contain 1 to 128 ASCII letters, digits, '.', '_' or '-'`); + } +} + +function validateCIdentifier(value, label) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) { + fail(`${label} '${value}' must be a portable C identifier`); + } +} + +function validateRelativeArtifactPath(value, label) { + if (!value || path.isAbsolute(value)) { + fail(`${label} '${value}' must be a relative path`); + } + const parts = value.split(/[\\/]+/); + if (parts.some((part) => part === '' || part === '.' || part === '..')) { + fail(`${label} '${value}' must not contain '.', '..', or empty path components`); + } + return parts.join('/'); +} + +function nativeModuleStem(extension) { + const moduleFile = extension['native-module-file'] ?? extension['module-file']; + if (typeof moduleFile !== 'string' || moduleFile.length === 0) { + return ''; + } + for (const suffix of ['.so', '.dylib', '.dll']) { + if (moduleFile.endsWith(suffix)) { + return moduleFile.slice(0, -suffix.length); + } + } + return moduleFile; +} + +function sharedPreloadLibraries(extension) { + const startupConfig = extension.lifecycle?.['startup-config'] ?? []; + const libraries = []; + for (const assignment of startupConfig) { + if (typeof assignment !== 'string') { + continue; + } + const separator = assignment.indexOf('='); + if (separator <= 0) { + continue; + } + if (assignment.slice(0, separator) === 'shared_preload_libraries') { + libraries.push(...splitCsv(assignment.slice(separator + 1))); + } + } + return sortedDeduped(libraries); +} + +async function externalRecipe(sqlName) { + const recipePath = path.join(root, 'src/extensions/external', sqlName, 'recipe.toml'); + if (!(await isFile(recipePath))) { + return null; + } + return readToml(recipePath); +} + +async function extensionDataFiles(extension, contribRows) { + const sqlName = extension['sql-name'] ?? extension.id; + const recipe = await externalRecipe(sqlName); + if (recipe !== null) { + return stringList(recipe.artifacts?.data_files); + } + const row = contribRows.find((item) => item?.['sql-name'] === sqlName); + return stringList(row?.['data-files']); +} + +function runtimeShareDataFiles(dataFiles) { + const prefix = 'share/postgresql/'; + return dataFiles.map((item) => (item.startsWith(prefix) ? item.slice(prefix.length) : item)).sort(); +} + +async function extensionArtifactList(sqlName, field) { + const recipe = await externalRecipe(sqlName); + if (recipe === null) { + return []; + } + return stringList(recipe.artifacts?.[field]); +} + +async function externalTargetStatus(sqlName, target) { + const targetPath = path.join(root, 'src/extensions/external', sqlName, 'targets', `${target}.toml`); + if (!(await isFile(targetPath))) { + return null; + } + const data = await readToml(targetPath); + return typeof data.status === 'string' ? data.status : null; +} + +async function extensionSupportStatuses(sqlName, family) { + const recipe = await externalRecipe(sqlName); + const support = recipe?.support?.[family]; + if (support === undefined || support === null || typeof support !== 'object') { + return []; + } + return Object.values(support).filter((value) => typeof value === 'string'); +} + +async function mobileReleaseReady(sqlName) { + const targetStatus = await externalTargetStatus(sqlName, 'mobile'); + if (targetStatus !== null) { + return targetStatus === 'supported'; + } + const statuses = await extensionSupportStatuses(sqlName, 'mobile'); + return statuses.length === 0 || statuses.every((status) => status === 'supported'); +} + +async function desktopReleaseReady(sqlName, promotion) { + if (!(promotion?.promoted === true && promotion?.stable === true)) { + return false; + } + const targetStatus = await externalTargetStatus(sqlName, 'native'); + if (targetStatus !== null) { + return targetStatus === 'supported'; + } + const statuses = await extensionSupportStatuses(sqlName, 'native'); + return statuses.length === 0 || statuses.every((status) => status === 'supported'); +} + +async function catalogRows() { + const catalog = await readJson(CATALOG_PATH); + const contrib = await readToml(CONTRIB_RECIPE_PATH); + const contribRows = Array.isArray(contrib.extensions) ? contrib.extensions : []; + const extensions = Array.isArray(catalog.extensions) ? catalog.extensions : []; + const publicSqlNames = new Set( + extensions + .filter((extension) => extension.promotion?.promoted === true) + .map((extension) => extension['sql-name'] ?? extension.id), + ); + const rows = []; + for (const extension of extensions) { + const promotion = extension.promotion ?? {}; + if (!(promotion.promoted === true && promotion.stable === true)) { + continue; + } + const sqlName = extension['sql-name'] ?? extension.id; + const dependencies = stringList(extension.dependencies).filter((dependency) => publicSqlNames.has(dependency)); + const dataFiles = runtimeShareDataFiles(await extensionDataFiles(extension, contribRows)); + const stem = nativeModuleStem(extension); + rows.push({ + sqlName, + pgMajor: '18', + createsExtension: Boolean(extension.lifecycle?.['create-extension']), + stem, + dependencies, + sharedPreload: sharedPreloadLibraries(extension), + desktopPrebuilt: await desktopReleaseReady(sqlName, promotion), + mobilePrebuilt: await mobileReleaseReady(sqlName), + mobileStaticRequired: stem.length > 0, + mobileStaticTargets: [], + dataFiles, + artifact: 'first-party', + }); + } + rows.sort((left, right) => compareText(left.sqlName, right.sqlName)); + return rows; +} + +async function catalogDefaultVersion(sqlName) { + const catalog = await readJson(CATALOG_PATH); + const matches = (Array.isArray(catalog.extensions) ? catalog.extensions : []) + .filter((extension) => (extension['sql-name'] ?? extension.id) === sqlName); + if (matches.length !== 1) { + fail(`generated extension catalog must contain exactly one row for '${sqlName}'`); + } + const version = matches[0].control?.['default-version']; + if (typeof version !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(version) || version.includes('--')) { + fail(`generated extension catalog has no literal control.default-version for '${sqlName}'`); + } + return version; +} + +async function listCatalog() { + const header = [ + 'sql_name', + 'pg_major', + 'creates_extension', + 'native_module_stem', + 'dependencies', + 'shared_preload', + 'desktop_prebuilt', + 'mobile_prebuilt', + 'mobile_static_registry_required', + 'mobile_static_archive_targets', + 'data_files', + 'artifact', + ]; + console.log(header.join('\t')); + for (const row of await catalogRows()) { + console.log( + [ + row.sqlName, + row.pgMajor, + yesNo(row.createsExtension), + dashIfEmpty(row.stem), + dashIfEmpty(row.dependencies), + dashIfEmpty(row.sharedPreload), + yesNo(row.desktopPrebuilt), + yesNo(row.mobilePrebuilt), + yesNo(row.mobileStaticRequired), + dashIfEmpty(row.mobileStaticTargets), + dashIfEmpty(row.dataFiles), + row.artifact, + ].join('\t'), + ); + } +} + +async function releasePackageByProduct(product) { + const releaseConfig = await readJson(RELEASE_CONFIG_PATH); + const packages = releaseConfig.packages ?? {}; + for (const [packagePath, config] of Object.entries(packages)) { + if (config?.component === product) { + return { packagePath, config }; + } + } + fail(`unknown release product '${product}'`); +} + +async function productReleaseMetadata(product) { + const { packagePath } = await releasePackageByProduct(product); + const releaseTomlPath = path.join(root, packagePath, 'release.toml'); + const metadata = await readToml(releaseTomlPath); + if (metadata.id !== product) { + fail(`${path.relative(root, releaseTomlPath)} must declare id = '${product}'`); + } + return metadata; +} + +async function selectedSqlNames(productsCsv) { + const products = sortedDeduped(splitCsv(productsCsv)); + if (products.length === 0) { + fail('no exact-extension products were selected'); + } + const sqlNames = []; + for (const product of products) { + const metadata = await productReleaseMetadata(product); + const members = metadata.kind === 'exact-extension-artifact' + ? [metadata.extension_sql_name] + : metadata.kind === 'exact-extension-bundle' + ? metadata.extension_sql_names + : undefined; + if (!Array.isArray(members) || members.length === 0 || members.some((sqlName) => typeof sqlName !== 'string' || sqlName.length === 0)) { + fail(`${product} must declare exact extension_sql_name or extension_sql_names members`); + } + sqlNames.push(...members); + } + console.log(sortedDeduped(sqlNames).join(',')); +} + +function parseCargoVersion(text) { + let inPackage = false; + for (const rawLine of text.split('\n')) { + const line = rawLine.trim(); + if (line === '[package]') { + inPackage = true; + continue; + } + if (inPackage && line.startsWith('[')) { + break; + } + if (inPackage) { + const match = /^version\s*=\s*"([^"]+)"/.exec(line); + if (match) { + return match[1]; + } + } + } + return ''; +} + +function parseJsonPath(text, dotted) { + let value = JSON.parse(text); + for (const key of dotted.split('.')) { + if (value === null || typeof value !== 'object' || !(key in value)) { + return ''; + } + value = value[key]; + } + return String(value); +} + +async function productVersion(product) { + const { packagePath, config } = await releasePackageByProduct(product); + const releaseType = config['release-type']; + const relativeVersionFile = + typeof config['version-file'] === 'string' && config['version-file'].length > 0 + ? config['version-file'] + : releaseType === 'rust' + ? 'Cargo.toml' + : releaseType === 'node' || releaseType === 'expo' + ? 'package.json' + : null; + if (relativeVersionFile === null) { + fail(`${product} release-please config must declare version-file for release type '${releaseType}'`); + } + const versionFile = path.join(root, packagePath, relativeVersionFile); + const text = await readText(versionFile); + const parser = + path.basename(versionFile) === 'Cargo.toml' + ? 'cargo' + : path.basename(versionFile) === 'package.json' + ? 'json:version' + : 'raw'; + const version = parser === 'cargo' ? parseCargoVersion(text) : parser.startsWith('json:') ? parseJsonPath(text, parser.slice(5)) : text.trim(); + if (!/^[0-9]+[.][0-9]+[.][0-9]+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?$/.test(version)) { + fail(`${product} version is not semver-like: '${version}'`); + } + console.log(version); +} + +function parseArgs(argv) { + const args = { + dependencies: [], + dataFiles: [], + sharedPreloadLibraries: [], + mobileStaticArchives: [], + mobileStaticDependencyArchives: [], + staticSymbolAliases: [], + createsExtension: true, + mobilePrebuilt: false, + format: 'directory', + force: false, + }; + for (let index = 0; index < argv.length; index += 1) { + let arg = argv[index]; + let value = null; + const equals = arg.indexOf('='); + if (arg.startsWith('--') && equals > 0) { + value = arg.slice(equals + 1); + arg = arg.slice(0, equals); + } + const nextValue = () => { + if (value !== null) { + return value; + } + index += 1; + if (index >= argv.length) { + fail(`${arg} requires a value`); + } + return argv[index]; + }; + switch (arg) { + case '--force': + args.force = true; + break; + case '--no-create-extension': + args.createsExtension = false; + break; + case '--mobile-prebuilt': + args.mobilePrebuilt = value === null ? true : parseBoolean(nextValue(), arg); + break; + case '--no-mobile-prebuilt': + args.mobilePrebuilt = false; + break; + case '--runtime': + args.runtime = nextValue(); + break; + case '--embedded-module-root': + args.embeddedModuleRoot = nextValue(); + break; + case '--sql-name': + args.sqlName = nextValue(); + break; + case '--creates-extension': + args.createsExtension = parseBoolean(nextValue(), arg); + break; + case '--target': + case '--native-target': + args.nativeTarget = nextValue(); + break; + case '--output': + case '-o': + args.output = nextValue(); + break; + case '--stage-root': + args.stageRoot = nextValue(); + break; + case '--format': + args.format = nextValue(); + break; + case '--native-module-stem': + args.nativeModuleStem = nextValue(); + break; + case '--native-module-file': + args.nativeModuleFile = nextValue(); + break; + case '--native-runtime-product': + args.nativeRuntimeProduct = nextValue(); + break; + case '--native-runtime-version': + args.nativeRuntimeVersion = nextValue(); + break; + case '--dependency': + case '--dependencies': + args.dependencies.push(...splitCsv(nextValue())); + break; + case '--data-file': + case '--data-files': + args.dataFiles.push(...splitCsv(nextValue()).map((item) => validateRelativeArtifactPath(item, 'data file'))); + break; + case '--shared-preload-library': + case '--shared-preload-libraries': + args.sharedPreloadLibraries.push(...splitCsv(nextValue())); + break; + case '--mobile-static-archive': + case '--mobile-static-archives': + args.mobileStaticArchives.push(...splitCsv(nextValue()).map(parseMobileStaticArchive)); + break; + case '--mobile-static-dependency-archive': + case '--mobile-static-dependency-archives': + args.mobileStaticDependencyArchives.push(...splitCsv(nextValue()).map(parseMobileStaticDependencyArchive)); + break; + case '--static-symbol-prefix': + args.staticSymbolPrefix = nextValue(); + break; + case '--static-symbol-alias': + case '--static-symbol-aliases': + args.staticSymbolAliases.push(...splitCsv(nextValue()).map(parseStaticSymbolAlias)); + break; + default: + fail(`unknown argument '${arg}'`); + } + } + return args; +} + +function parseBoolean(value, label) { + if (['true', 'yes', '1'].includes(value)) { + return true; + } + if (['false', 'no', '0'].includes(value)) { + return false; + } + fail(`${label} expected true/false, got '${value}'`); +} + +function parseMobileStaticArchive(value) { + const separator = value.includes('=') ? value.indexOf('=') : value.indexOf(':'); + if (separator <= 0) { + fail('--mobile-static-archive values must use : or ='); + } + const target = value.slice(0, separator).trim(); + const archive = value.slice(separator + 1).trim(); + if (target.length === 0 || archive.length === 0) { + fail('--mobile-static-archive values must include both target and archive path'); + } + return { target, archive }; +} + +function parseMobileStaticDependencyArchive(value) { + if (value.includes('=')) { + const [left, archive] = value.split(/=(.*)/s); + const [target, name] = left.split(':'); + if (!target || !name || !archive) { + fail('--mobile-static-dependency-archive values must use :: or :='); + } + return { target: target.trim(), name: name.trim(), archive: archive.trim() }; + } + const parts = value.split(':'); + if (parts.length < 3) { + fail('--mobile-static-dependency-archive values must use :: or :='); + } + const target = parts.shift().trim(); + const name = parts.shift().trim(); + const archive = parts.join(':').trim(); + if (!target || !name || !archive) { + fail('--mobile-static-dependency-archive values must include target, name, and archive path'); + } + return { target, name, archive }; +} + +function parseStaticSymbolAlias(value) { + const separator = value.includes('=') ? value.indexOf('=') : value.indexOf(':'); + if (separator <= 0) { + fail('--static-symbol-alias values must use : or ='); + } + const sqlSymbol = value.slice(0, separator).trim(); + const linkedSymbol = value.slice(separator + 1).trim(); + if (sqlSymbol.length === 0 || linkedSymbol.length === 0) { + fail('--static-symbol-alias values must include both SQL and linked C symbols'); + } + return { sqlSymbol, linkedSymbol }; +} + +async function validateArtifactArgs(args) { + for (const [value, label] of [ + [args.sqlName, 'prebuilt extension sqlName'], + [args.nativeModuleStem, 'prebuilt extension native module stem'], + [args.nativeModuleFile, 'prebuilt extension native module file'], + [args.nativeTarget, 'prebuilt extension native target'], + ]) { + if (value !== undefined) { + validatePortableId(value, label); + } + } + if (args.output === undefined) { + fail('missing required --output '); + } + if (args.runtime === undefined) { + fail('missing required --runtime '); + } + if (args.sqlName === undefined) { + fail('missing required --sql-name '); + } + if (args.nativeRuntimeProduct !== 'liboliphaunt-native') { + fail('prebuilt extension artifact --native-runtime-product must be liboliphaunt-native'); + } + if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(args.nativeRuntimeVersion ?? '')) { + fail('prebuilt extension artifact --native-runtime-version must be stable SemVer X.Y.Z'); + } + if (!(await isDirectory(args.runtime))) { + fail(`prebuilt extension artifact runtime root ${args.runtime} must be an existing directory`); + } + if (args.embeddedModuleRoot !== undefined && !(await isDirectory(args.embeddedModuleRoot))) { + fail( + `prebuilt extension artifact embedded module root ${args.embeddedModuleRoot} must be an existing directory`, + ); + } + if (args.nativeModuleFile !== undefined && args.nativeModuleStem === undefined) { + fail('prebuilt extension nativeModuleFile requires nativeModuleStem'); + } + if (args.nativeModuleStem !== undefined && args.nativeTarget === undefined) { + fail('prebuilt extension artifacts with nativeModuleStem must declare nativeTarget'); + } + if ( + args.nativeModuleStem !== undefined + && DESKTOP_NATIVE_TARGETS.has(args.nativeTarget) + && args.embeddedModuleRoot === undefined + ) { + fail('desktop prebuilt extension artifacts with nativeModuleStem require --embedded-module-root'); + } + if ( + args.embeddedModuleRoot !== undefined + && (args.nativeModuleStem === undefined || !DESKTOP_NATIVE_TARGETS.has(args.nativeTarget)) + ) { + fail('--embedded-module-root is only valid for desktop native-module extension artifacts'); + } + if (args.staticSymbolPrefix !== undefined) { + validateCIdentifier(args.staticSymbolPrefix, 'prebuilt extension static symbol prefix'); + } + const aliasSqlSymbols = new Set(); + for (const alias of args.staticSymbolAliases) { + validateCIdentifier(alias.sqlSymbol, 'prebuilt extension static symbol alias'); + validateCIdentifier(alias.linkedSymbol, 'prebuilt extension static symbol alias target'); + if (aliasSqlSymbols.has(alias.sqlSymbol)) { + fail(`prebuilt extension repeats static symbol alias for '${alias.sqlSymbol}'`); + } + aliasSqlSymbols.add(alias.sqlSymbol); + } + if (args.mobileStaticArchives.length > 0 && args.nativeModuleStem === undefined) { + fail('prebuilt extension mobile static archives require nativeModuleStem'); + } + const mobilePrebuilt = artifactMobilePrebuilt(args); + if (mobilePrebuilt && args.nativeModuleStem !== undefined && args.mobileStaticArchives.length === 0) { + fail('mobilePrebuilt native-module artifacts must carry at least one mobile static archive'); + } + const mobileTargets = new Set(); + for (const archive of args.mobileStaticArchives) { + validatePortableId(archive.target, 'prebuilt extension mobile static archive target'); + if (mobileTargets.has(archive.target)) { + fail(`prebuilt extension mobile static archives repeat target '${archive.target}'`); + } + mobileTargets.add(archive.target); + if (!(await isFile(archive.archive))) { + fail(`prebuilt extension mobile static archive for target '${archive.target}' must be a file: ${archive.archive}`); + } + } + const mobileDependencyKeys = new Set(); + for (const archive of args.mobileStaticDependencyArchives) { + validatePortableId(archive.target, 'prebuilt extension mobile static dependency archive target'); + validatePortableId(archive.name, 'prebuilt extension mobile static dependency archive name'); + if (!mobileTargets.has(archive.target)) { + fail(`prebuilt extension mobile static dependency archive '${archive.name}' for target '${archive.target}' requires a matching mobile static archive target`); + } + const key = `${archive.target}:${archive.name}`; + if (mobileDependencyKeys.has(key)) { + fail(`prebuilt extension mobile static dependency archives repeat '${archive.name}' for target '${archive.target}'`); + } + mobileDependencyKeys.add(key); + validatePortableId(path.basename(archive.archive), 'prebuilt extension mobile static dependency archive file'); + if (!(await isFile(archive.archive))) { + fail(`prebuilt extension mobile static dependency archive '${archive.name}' for target '${archive.target}' must be a file: ${archive.archive}`); + } + } + for (const dataFile of args.dataFiles) { + validateRelativeArtifactPath(dataFile, 'data file'); + if (dataFile.split('/')[0] === 'extension') { + fail(`prebuilt extension data file '${dataFile}' must not be under share/postgresql/extension; control and SQL files are selected from sqlName`); + } + } +} + +function artifactMobilePrebuilt(args) { + return args.mobilePrebuilt || args.mobileStaticArchives.length > 0; +} + +function extensionSqlFileBelongs(sqlName, fileName, extraSql, createsExtension) { + return ( + (createsExtension && fileName === `${sqlName}.control`) || + (createsExtension && fileName === `${sqlName}.sql`) || + (createsExtension && fileName.startsWith(`${sqlName}--`) && fileName.endsWith('.sql')) || + extraSql.names.includes(fileName) || + (fileName.endsWith('.sql') && extraSql.prefixes.some((prefix) => fileName.startsWith(prefix))) + ); +} + +async function extensionSqlMetadata(sqlName) { + const prefixes = await extensionArtifactList(sqlName, 'extension_sql_file_prefixes'); + const names = await extensionArtifactList(sqlName, 'extension_sql_file_names'); + if (new Set(prefixes).size !== prefixes.length || new Set(names).size !== names.length) { + fail(`prebuilt extension '${sqlName}' SQL file names and prefixes must be sorted and unique`); + } + for (const name of names) { + if (path.basename(name) !== name || !name.endsWith('.sql')) { + fail(`prebuilt extension '${sqlName}' SQL file name '${name}' must be a .sql basename`); + } + } + for (const prefix of prefixes) { + validatePortableId(prefix, `prebuilt extension '${sqlName}' SQL file prefix`); + if (prefix.includes('.')) { + fail(`prebuilt extension '${sqlName}' SQL file prefix '${prefix}' must not contain '.'`); + } + } + return { names, prefixes }; +} + +async function ensureParent(file) { + await fs.mkdir(path.dirname(file), { recursive: true }); +} + +async function copyFileChecked(sourceRoot, source, destination) { + const sourceReal = await fs.realpath(source); + const rootReal = await fs.realpath(sourceRoot); + if (!sourceReal.startsWith(`${rootReal}${path.sep}`) && sourceReal !== rootReal) { + fail(`selected extension runtime symlink ${source} resolves outside runtime root ${sourceRoot}`); + } + const stat = await fs.stat(source); + if (!stat.isFile()) { + fail(`prebuilt extension artifact source runtime file ${source} must be a regular file`); + } + await ensureParent(destination); + await fs.copyFile(source, destination); + await fs.chmod(destination, stat.mode & 0o111 ? 0o755 : 0o644); +} + +async function copyRuntimeRelativeFile(runtime, artifactFiles, relative) { + const normalized = validateRelativeArtifactPath(relative, 'runtime file'); + const source = path.join(runtime, normalized); + if (!(await isFile(source))) { + fail(`prebuilt extension artifact source runtime is missing declared file ${source}`); + } + await copyFileChecked(runtime, source, path.join(artifactFiles, normalized)); +} + +async function copySqlFiles(args, artifactRoot, artifactFiles, extraSql) { + const sourceDir = path.join(args.runtime, 'share/postgresql/extension'); + const targetDir = path.join(artifactFiles, 'share/postgresql/extension'); + if (!(await isDirectory(sourceDir))) { + if (args.createsExtension) { + fail(`prebuilt extension artifact source runtime ${args.runtime} is missing share/postgresql/extension for '${args.sqlName}'`); + } + return; + } + let copied = 0; + let copiedControl = false; + let copiedSql = false; + const copiedFileNames = []; + const entries = (await fs.readdir(sourceDir)).sort(); + for (const entry of entries) { + if (!extensionSqlFileBelongs(args.sqlName, entry, extraSql, args.createsExtension)) { + continue; + } + copied += 1; + copiedFileNames.push(entry); + if (entry === `${args.sqlName}.control`) { + copiedControl = true; + } else if (isCanonicalExtensionInstallSql(entry, args.sqlName)) { + copiedSql = true; + } + await copyFileChecked(args.runtime, path.join(sourceDir, entry), path.join(targetDir, entry)); + } + if (args.createsExtension && (!copiedControl || !copiedSql)) { + fail(`prebuilt extension artifact ${artifactRoot} for '${args.sqlName}' must include a control file and canonical base install SQL`); + } + if (args.createsExtension) { + const control = await fs.readFile(path.join(targetDir, `${args.sqlName}.control`), 'utf8'); + const actualDefaultVersion = extensionControlDefaultVersion( + control, + args.sqlName, + `prebuilt extension artifact ${artifactRoot}`, + ); + const expectedDefaultVersion = await catalogDefaultVersion(args.sqlName); + if (actualDefaultVersion !== expectedDefaultVersion) { + fail( + `prebuilt extension artifact ${artifactRoot} ${args.sqlName}.control default_version ` + + `'${actualDefaultVersion}' does not match source-owned catalog version '${expectedDefaultVersion}'`, + ); + } + validateExtensionInstallSqlReachability({ + sqlName: args.sqlName, + control, + fileNames: copiedFileNames, + label: `prebuilt extension artifact ${artifactRoot}`, + }); + } + if (!args.createsExtension && copied === 0) { + return; + } +} + +function mobileStaticArchiveRelativePath(target, stem) { + return `mobile-static/${target}/extensions/${stem}/liboliphaunt_extension_${stem}.a`; +} + +function mobileStaticDependencyArchiveRelativePath(target, name, archivePath) { + return `mobile-static/${target}/dependencies/${name}/${path.basename(archivePath)}`; +} + +async function copyStandaloneFile(source, destination) { + const stat = await fs.stat(source); + if (!stat.isFile()) { + fail(`prebuilt extension artifact source file ${source} must be a regular file`); + } + await ensureParent(destination); + await fs.copyFile(source, destination); + await fs.chmod(destination, stat.mode & 0o111 ? 0o755 : 0o644); +} + +function extensionMetadata(args, extraSql) { + const dependencies = sortedDeduped(args.dependencies); + const dataFiles = sortedDeduped(args.dataFiles); + const sharedPreloadLibraries = sortedDeduped(args.sharedPreloadLibraries); + const mobileStaticArchives = args.nativeModuleStem === undefined + ? [] + : args.mobileStaticArchives + .map((archive) => ({ + target: archive.target, + source: archive.archive, + relativePath: mobileStaticArchiveRelativePath(archive.target, args.nativeModuleStem), + })) + .sort((left, right) => compareText(left.target, right.target)); + const mobileStaticDependencyArchives = args.mobileStaticDependencyArchives + .map((archive) => ({ + target: archive.target, + name: archive.name, + source: archive.archive, + relativePath: mobileStaticDependencyArchiveRelativePath(archive.target, archive.name, archive.archive), + })) + .sort((left, right) => compareText(left.target, right.target) || compareText(left.name, right.name)); + const staticSymbolAliases = [...args.staticSymbolAliases].sort( + (left, right) => compareText(left.sqlSymbol, right.sqlSymbol) || compareText(left.linkedSymbol, right.linkedSymbol), + ); + return { + dependencies, + dataFiles, + extensionSqlFileNames: extraSql.names, + extensionSqlFilePrefixes: extraSql.prefixes, + sharedPreloadLibraries, + mobileStaticArchives, + mobileStaticDependencyArchives, + staticSymbolAliases, + mobilePrebuilt: artifactMobilePrebuilt(args), + nativeModuleFile: args.nativeModuleStem === undefined ? '' : args.nativeModuleFile ?? args.nativeModuleStem, + }; +} + +async function writeArtifactDirectory(artifactRoot, args) { + const filesRoot = path.join(artifactRoot, 'files'); + const extraSql = await extensionSqlMetadata(args.sqlName); + const metadata = extensionMetadata(args, extraSql); + await copySqlFiles(args, artifactRoot, filesRoot, extraSql); + for (const dataFile of metadata.dataFiles) { + await copyRuntimeRelativeFile(args.runtime, filesRoot, `share/postgresql/${dataFile}`); + } + if (metadata.nativeModuleFile.length > 0) { + await copyRuntimeRelativeFile(args.runtime, filesRoot, `lib/postgresql/${metadata.nativeModuleFile}`); + if (DESKTOP_NATIVE_TARGETS.has(args.nativeTarget)) { + await copyFileChecked( + args.embeddedModuleRoot, + path.join(args.embeddedModuleRoot, metadata.nativeModuleFile), + path.join(filesRoot, 'lib/modules', metadata.nativeModuleFile), + ); + } + } + for (const archive of metadata.mobileStaticArchives) { + await copyStandaloneFile(archive.source, path.join(artifactRoot, archive.relativePath)); + } + for (const archive of metadata.mobileStaticDependencyArchives) { + await copyStandaloneFile(archive.source, path.join(artifactRoot, archive.relativePath)); + } + const manifest = [ + 'packageLayout=oliphaunt-extension-artifact-v1', + 'pgMajor=18', + `sqlName=${args.sqlName}`, + `createsExtension=${yesNo(args.createsExtension)}`, + `nativeModuleStem=${args.nativeModuleStem ?? ''}`, + `nativeModuleFile=${metadata.nativeModuleFile}`, + `nativeTarget=${args.nativeTarget ?? ''}`, + `nativeRuntimeProduct=${args.nativeRuntimeProduct}`, + `nativeRuntimeVersion=${args.nativeRuntimeVersion}`, + `dependencies=${metadata.dependencies.join(',')}`, + `dataFiles=${metadata.dataFiles.join(',')}`, + `extensionSqlFileNames=${metadata.extensionSqlFileNames.join(',')}`, + `extensionSqlFilePrefixes=${metadata.extensionSqlFilePrefixes.join(',')}`, + `sharedPreloadLibraries=${metadata.sharedPreloadLibraries.join(',')}`, + `mobilePrebuilt=${yesNo(metadata.mobilePrebuilt)}`, + `mobileStaticArchives=${metadata.mobileStaticArchives.map((archive) => `${archive.target}:${archive.relativePath}`).join(',')}`, + `mobileStaticDependencyArchives=${metadata.mobileStaticDependencyArchives.map((archive) => `${archive.target}:${archive.name}:${archive.relativePath}`).join(',')}`, + `staticSymbolPrefix=${args.staticSymbolPrefix ?? ''}`, + `staticSymbolAliases=${metadata.staticSymbolAliases.map((alias) => `${alias.sqlSymbol}:${alias.linkedSymbol}`).join(',')}`, + 'files=files', + '', + ].join('\n'); + await fs.mkdir(artifactRoot, { recursive: true }); + await fs.writeFile(path.join(artifactRoot, 'manifest.properties'), manifest); +} + +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( + process.execPath, + stripArgs, + { 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}`); + } +} + +export async function validateExactArtifactBinaryContract(artifactRoot, args) { + if ( + args.nativeModuleStem === undefined + || !DESKTOP_NATIVE_TARGETS.has(args.nativeTarget) + ) { + return; + } + if (args.nativeTarget === 'windows-x64-msvc') { + return validateWindowsExtensionArtifactBinaryContract({ + artifactRoot, + providerRuntimeRoot: args.runtime, + }); + } + return inspectPlatformBinaryTree(artifactRoot, { + target: args.nativeTarget, + }); +} + +async function prepareOutputFile(output, force) { + if (await exists(output)) { + if (!force) { + fail(`prebuilt extension artifact output ${output} already exists; pass --force`); + } + const stat = await fs.lstat(output); + if (stat.isDirectory()) { + await fs.rm(output, { recursive: true, force: true }); + } else { + await fs.unlink(output); + } + } + await ensureParent(output); +} + +async function createArtifact(argv) { + const args = parseArgs(argv); + args.nativeRuntimeProduct ??= 'liboliphaunt-native'; + args.nativeRuntimeVersion ??= (await readText('src/runtimes/liboliphaunt/native/VERSION')).trim(); + await validateArtifactArgs(args); + if (!['directory', 'dir', 'tar', 'tar-gz', 'tar.gz', 'tgz', 'gz'].includes(args.format)) { + fail(`unknown extension artifact format '${args.format}'`); + } + const output = path.resolve(args.output); + if (args.format === 'directory' || args.format === 'dir') { + if (await exists(output)) { + if (!args.force) { + fail(`prebuilt extension artifact output ${output} already exists; pass --force`); + } + await fs.rm(output, { recursive: true, force: true }); + } + await writeArtifactDirectory(output, args); + stripNativeReleaseBinaries(output, args.nativeTarget); + await validateExactArtifactBinaryContract(output, args); + console.log(`path=${output}`); + console.log(`sqlName=${args.sqlName}`); + console.log('format=directory'); + console.log(`manifest=${path.join(output, 'manifest.properties')}`); + return; + } + await prepareOutputFile(output, args.force); + const stageRoot = path.resolve(args.stageRoot ?? path.join(root, 'target/extensions/native/release-stage/local')); + const artifactRoot = path.join(stageRoot, `.artifact-${args.sqlName}-${process.pid}-${Date.now()}`); + const formatLabel = args.format === 'tar' ? 'tar' : 'tar-gz'; + await fs.rm(artifactRoot, { recursive: true, force: true }); + await fs.mkdir(artifactRoot, { recursive: true }); + try { + await writeArtifactDirectory(artifactRoot, args); + stripNativeReleaseBinaries(artifactRoot, args.nativeTarget); + await validateExactArtifactBinaryContract(artifactRoot, args); + if (args.format === 'tar') { + await fs.writeFile(output, await createTar(artifactRoot)); + } else { + await fs.writeFile(output, canonicalGzip(await createTar(artifactRoot))); + } + } finally { + await fs.rm(artifactRoot, { recursive: true, force: true }); + } + console.log(`path=${output}`); + console.log(`sqlName=${args.sqlName}`); + console.log(`format=${formatLabel}`); + console.log('manifest='); +} + +async function listFilesRecursive(base, current = base) { + const entries = (await fs.readdir(current, { withFileTypes: true })).sort((left, right) => compareText(left.name, right.name)); + const files = []; + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isSymbolicLink()) { + fail(`prebuilt extension artifact archives do not support symlinks: ${fullPath}`); + } + if (entry.isDirectory()) { + files.push(...(await listFilesRecursive(base, fullPath))); + continue; + } + if (!entry.isFile()) { + fail(`prebuilt extension artifact archives only support files and directories: ${fullPath}`); + } + files.push(fullPath); + } + return files; +} + +function tarPathParts(relativePath) { + const normalized = relativePath.split(path.sep).join('/'); + const bytes = Buffer.byteLength(normalized); + if (bytes <= 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(`prebuilt extension artifact 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).padStart(length - 1, '0').slice(-(length - 1)); + writeString(buffer, offset, length, `${text}\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, 265, 32, 'root'); + writeString(header, 297, 32, 'root'); + writeString(header, 345, 155, prefix); + let checksum = 0; + for (const byte of header) { + checksum += byte; + } + const checksumText = checksum.toString(8).padStart(6, '0'); + writeString(header, 148, 8, `${checksumText}\0 `); + return header; +} + +async function createTar(base) { + const chunks = []; + const files = await listFilesRecursive(base); + const members = []; + for (const file of files) { + const relative = validateRelativeArtifactPath(path.relative(base, file).split(path.sep).join('/'), 'archive file'); + const stat = await fs.stat(file); + const mode = stat.mode & 0o111 ? 0o755 : 0o644; + const data = await fs.readFile(file); + members.push({ name: relative, bytes: data.length }); + chunks.push(tarHeader(relative, data.length, mode)); + chunks.push(data); + const remainder = data.length % 512; + if (remainder !== 0) { + chunks.push(Buffer.alloc(512 - remainder, 0)); + } + } + const expectedBytes = validateExtensionArtifactArchivePlan( + members, + `prebuilt extension artifact ${base}`, + ); + chunks.push(Buffer.alloc(1024, 0)); + const archive = Buffer.concat(chunks); + if (archive.length !== expectedBytes) { + fail(`prebuilt extension artifact ${base} tar bytes ${archive.length} do not match bounded plan ${expectedBytes}`); + } + return archive; +} + +function canonicalGzip(bytes) { + const compressed = Buffer.from(Bun.gzipSync(bytes)); + if ( + compressed.length < 18 + || compressed[0] !== 0x1f + || compressed[1] !== 0x8b + || compressed[2] !== 8 + || compressed[3] !== 0 + ) { + fail('Bun.gzipSync did not produce the expected flag-free gzip stream'); + } + compressed.fill(0, 4, 9); + compressed[9] = 0x03; + validateExtensionArtifactCompressedBytes(compressed.length, 'prebuilt extension artifact'); + return compressed; +} + +async function main() { + const [command, ...args] = process.argv.slice(2); + switch (command) { + case 'list-catalog': + await listCatalog(); + break; + case 'selected-sql-names': + await selectedSqlNames(args[0] ?? ''); + break; + case 'product-version': + await productVersion(args[0] ?? ''); + break; + case 'create-artifact': + await createArtifact(args); + break; + default: + fail('usage: extension-artifact-packager.mjs [options]'); + } +} + +if (import.meta.main) { + main().catch((error) => { + console.error(`extension-artifact-packager.mjs: ${error.message}`); + process.exit(2); + }); +} diff --git a/src/extensions/artifacts/native/tools/package-release-assets.sh b/src/extensions/artifacts/native/tools/package-release-assets.sh new file mode 100755 index 00000000..bb40f1c7 --- /dev/null +++ b/src/extensions/artifacts/native/tools/package-release-assets.sh @@ -0,0 +1,720 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)" || { + echo "package-native-extension-assets.sh: unable to determine repository root from $script_dir; run this script from a Git checkout" >&2 + exit 1 +} +cd "$root" + +fail() { + echo "package-native-extension-assets.sh: $*" >&2 + exit 1 +} + +require() { + command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" +} + +source "$root/src/runtimes/liboliphaunt/native/bin/mobile-static-extensions.sh" +packager="src/extensions/artifacts/native/tools/extension-artifact-packager.mjs" +native_asset_index_contract="tools/release/native-extension-asset-index-contract.mjs" + +target_id="${OLIPHAUNT_EXTENSION_TARGET:-${1:-}}" +case "$target_id" in + macos-arm64|linux-x64-gnu|linux-arm64-gnu|windows-x64-msvc|ios-xcframework|android-arm64-v8a|android-x86_64) + ;; + "") + fail "usage: OLIPHAUNT_EXTENSION_TARGET= $0, where target is macos-arm64, linux-x64-gnu, linux-arm64-gnu, windows-x64-msvc, ios-xcframework, android-arm64-v8a, or android-x86_64" + ;; + *) + fail "unsupported native extension artifact target: $target_id" + ;; +esac + +require awk +require bun +native_extension_runtime_kind="$(bun "$native_asset_index_contract" runtime-kind)" + +case "$target_id" in + windows-x64-msvc) ;; + ios-xcframework) + require ditto + require rsync + ;; + *) require rsync ;; +esac + +extension_product="${OLIPHAUNT_EXTENSION_PRODUCT:-${2:-}}" +extension_products="${OLIPHAUNT_EXTENSION_PRODUCTS:-}" +if [ -n "$extension_product" ]; then + if [ -n "$extension_products" ]; then + extension_products="$extension_products,$extension_product" + else + extension_products="$extension_product" + fi +fi +selected_sql_names="" +if [ -n "$extension_products" ]; then + selected_sql_names="$(bun "$packager" selected-sql-names "$extension_products")" +fi + +version="${OLIPHAUNT_EXTENSION_RELEASE_VERSION:-$(bun "$packager" product-version liboliphaunt-native)}" +native_runtime_version="$(tr -d '[:space:]' < "$root/src/runtimes/liboliphaunt/native/VERSION")" +default_out_dir="$root/target/extensions/native/release-assets/$target_id" +default_stage_root="$root/target/extensions/native/release-stage/$target_id" +if [ -n "$extension_product" ] && [ -z "${OLIPHAUNT_EXTENSION_PRODUCTS:-}" ]; then + default_out_dir="$default_out_dir/$extension_product" + default_stage_root="$default_stage_root/$extension_product" +fi +out_dir="${OLIPHAUNT_EXTENSION_RELEASE_ASSET_DIR:-$default_out_dir}" +stage_root="${OLIPHAUNT_EXTENSION_RELEASE_STAGE_ROOT:-$default_stage_root}" +catalog_file="$stage_root/extension-catalog.tsv" +legacy_extension_index="$out_dir/liboliphaunt-${version}-extension-assets.tsv" +native_asset_index="$out_dir/liboliphaunt-${version}-native-extension-assets.tsv" + +rm -rf "$stage_root" +mkdir -p "$out_dir" "$stage_root" + +csv_join() { + paste -sd ',' - +} + +catalog_rows() { + awk -F '\t' 'NR > 1 { print }' "$catalog_file" +} + +mobile_module_extensions_csv() { + catalog_rows | awk -F '\t' -v selected="$selected_sql_names" ' + function selected_match(sql_name, selected, parts, count, i) { + if (selected == "") { + return 1 + } + count = split(selected, parts, ",") + for (i = 1; i <= count; i++) { + if (parts[i] == sql_name) { + return 1 + } + } + return 0 + } + $8 == "yes" && $9 == "yes" && $4 != "-" && selected_match($1, selected) { print $1 } + ' | csv_join +} + +selected_sql_name_matches() { + local sql_name="$1" + local selected + [ -n "$selected_sql_names" ] || return 0 + IFS=',' read -r -a selected <<<"$selected_sql_names" + local item + for item in "${selected[@]}"; do + [ "$item" = "$sql_name" ] && return 0 + done + return 1 +} + +require_file() { + local path="$1" + local description="$2" + [ -f "$path" ] || fail "missing $description at $path" +} + +require_dir() { + local path="$1" + local description="$2" + [ -d "$path" ] || fail "missing $description at $path" +} + +artifact_bytes() { + local artifact="$1" + require_file "$out_dir/$artifact" "release artifact $artifact" + wc -c <"$out_dir/$artifact" | awk '{ print $1 }' +} + +artifact_bytes_or_dash() { + local artifact="${1:-}" + if [ -z "$artifact" ] || [ "$artifact" = "-" ]; then + printf '%s\n' '-' + return 0 + fi + artifact_bytes "$artifact" +} + +write_indexes() { + printf 'sql_name\tcreates_extension\tnative_module_stem\tdependencies\tshared_preload\tmobile_prebuilt\tmobile_static_archive_targets\truntime_artifact\tios_xcframework_artifact\tandroid_arm64_artifact\tandroid_x86_64_artifact\truntime_artifact_bytes\tios_xcframework_artifact_bytes\tandroid_arm64_artifact_bytes\tandroid_x86_64_artifact_bytes\tdata_files\n' >"$legacy_extension_index" + bun "$native_asset_index_contract" header >"$native_asset_index" +} + +append_legacy_index_row() { + local sql_name="$1" + local creates_extension="$2" + local stem="$3" + local dependencies="$4" + local shared_preload="$5" + local mobile_prebuilt="$6" + local mobile_targets="$7" + local runtime_artifact="$8" + local ios_artifact="${9:-}" + local android_arm64_artifact="${10:-}" + local android_x86_64_artifact="${11:-}" + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$sql_name" \ + "$creates_extension" \ + "$stem" \ + "$dependencies" \ + "$shared_preload" \ + "$mobile_prebuilt" \ + "$mobile_targets" \ + "${runtime_artifact:--}" \ + "${ios_artifact:--}" \ + "${android_arm64_artifact:--}" \ + "${android_x86_64_artifact:--}" \ + "$(artifact_bytes_or_dash "${runtime_artifact:-}")" \ + "$(artifact_bytes_or_dash "${ios_artifact:-}")" \ + "$(artifact_bytes_or_dash "${android_arm64_artifact:-}")" \ + "$(artifact_bytes_or_dash "${android_x86_64_artifact:-}")" \ + "${12:-}" >>"$legacy_extension_index" +} + +append_native_asset_index_row() { + local sql_name="$1" + local kind="$2" + local artifact="$3" + local identity="${4:--}" + local registration_artifact="${5:--}" + [ -n "$artifact" ] && [ "$artifact" != "-" ] || return 0 + if [ "$registration_artifact" != "-" ]; then + require_file "$out_dir/$registration_artifact" "iOS extension registration metadata for $sql_name" + fi + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "$sql_name" \ + "$target_id" \ + "$kind" \ + "$identity" \ + "$artifact" \ + "$(artifact_bytes "$artifact")" \ + "$registration_artifact" >>"$native_asset_index" +} + +fetch_extension_source_assets() { + if [ "${OLIPHAUNT_RELEASE_FETCH_ASSETS:-0}" != "1" ]; then + echo "==> Source asset fetch handled by the source-inputs Moon dependency; set OLIPHAUNT_RELEASE_FETCH_ASSETS=1 for standalone refresh" + return 0 + fi + echo "==> Fetching pinned native runtime and extension source assets" + bun tools/policy/fetch-sources.mjs native-runtime >/tmp/liboliphaunt-release-extension-assets-fetch.log +} + +archive_swiftpm_xcframework() { + local xcframework="$1" + local output="$2" + [ -d "$xcframework" ] || fail "missing SwiftPM XCFramework input at $xcframework" + rm -f "$output" + ( + cd "$(dirname "$xcframework")" + ditto -c -k --keepParent "$(basename "$xcframework")" "$output" + ) +} + +mobile_static_dependency_archive() { + local work_root="$1" + local dependency="$2" + local dependency_root="$work_root/out/dependencies" + local archive + if archive="$(oliphaunt_mobile_static_dependency_archive_for_root "$dependency_root" "$dependency")"; then + printf '%s\n' "$archive" + return 0 + fi + return 1 +} + +mobile_dependency_args=() + +collect_mobile_static_dependency_archive_args() { + local target="$1" + local work_root="$2" + local sql_name="$3" + local dependency archive + mobile_dependency_args=() + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + if ! archive="$(mobile_static_dependency_archive "$work_root" "$dependency")"; then + fail "missing $target static dependency archive for $sql_name dependency $dependency under $work_root/out/dependencies" + fi + mobile_dependency_args+=(--mobile-static-dependency-archive "$target:$dependency:$archive") + done < <(oliphaunt_mobile_static_extension_dependencies_for_target "$sql_name" "$target" || true) +} + +module_suffix_for_target() { + case "$target_id" in + macos-*|ios-xcframework) printf 'dylib\n' ;; + android-*) printf 'so\n' ;; + linux-*) printf 'so\n' ;; + windows-*) printf 'dll\n' ;; + *) fail "no module suffix for target $target_id" ;; + esac +} + +host_extension_runtime_root() { + case "$target_id" in + macos-arm64) + printf '%s\n' "${OLIPHAUNT_WORK_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id}/install" + ;; + linux-x64-gnu|linux-arm64-gnu) + printf '%s\n' "${OLIPHAUNT_LINUX_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}/install" + ;; + windows-x64-msvc) + printf '%s\n' "${OLIPHAUNT_WINDOWS_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}/install" + ;; + ios-xcframework|android-arm64-v8a|android-x86_64) + case "$target_id" in + ios-xcframework) + printf '%s\n' "${OLIPHAUNT_EXTENSION_HOST_RUNTIME_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id/install}" + ;; + android-*) + printf '%s\n' "${OLIPHAUNT_EXTENSION_HOST_RUNTIME_ROOT:-$root/target/liboliphaunt-pg18-linux-x64-gnu-extension-release/install}" + ;; + esac + ;; + esac +} + +host_extension_embedded_modules_root() { + case "$target_id" in + macos-arm64) + printf '%s\n' "${OLIPHAUNT_WORK_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id}/out/modules" + ;; + linux-x64-gnu|linux-arm64-gnu) + printf '%s\n' "${OLIPHAUNT_LINUX_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}/out/modules" + ;; + windows-x64-msvc) + printf '%s\n' "${OLIPHAUNT_WINDOWS_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}/out/modules" + ;; + *) + fail "embedded desktop extension modules are not defined for $target_id" + ;; + esac +} + +prepare_extension_release_runtime() { + local source_runtime="$1" + if [ "$target_id" = "windows-x64-msvc" ]; then + printf '%s\n' "$source_runtime" + return 0 + fi + + # Build/install trees legitimately contain PostgreSQL shared-library aliases. + # They are not release carriers. Validate and materialize those aliases in a + # disposable stage so both the binary contract and the artifact packager see + # the exact link-free bytes that can be published. + local staged_runtime="$stage_root/prepared-runtime" + rm -rf "$staged_runtime" + mkdir -p "$staged_runtime" + rsync -a --delete "$source_runtime/" "$staged_runtime/" + tools/dev/bun.sh tools/release/materialize-release-symlinks.mjs "$staged_runtime" >&2 + printf '%s\n' "$staged_runtime" +} + +prepare_windows_binary_contract_runtime() { + local source_runtime="$1" + local staged_runtime="$stage_root/windows-binary-contract-runtime" + tools/dev/bun.sh \ + src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs \ + --runtime "$source_runtime" \ + --catalog "$catalog_file" \ + --selected-sql-names "$selected_sql_names" \ + --output "$staged_runtime" >&2 + printf '%s\n' "$staged_runtime" +} + +build_desktop_extension_runtime() { + case "$target_id" in + macos-arm64) + [ "$(uname -s)" = "Darwin" ] || fail "$target_id extension artifacts must be built on macOS" + env \ + OLIPHAUNT_WORK_ROOT="${OLIPHAUNT_WORK_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id}" \ + OLIPHAUNT_BUILD_EXTENSIONS=1 \ + OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES="$selected_sql_names" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh >/tmp/liboliphaunt-release-"$target_id"-extensions.log + ;; + linux-x64-gnu|linux-arm64-gnu) + [ "$(uname -s)" = "Linux" ] || fail "$target_id extension artifacts must be built on Linux" + env \ + OLIPHAUNT_LINUX_WORK_ROOT="${OLIPHAUNT_LINUX_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}" \ + OLIPHAUNT_BUILD_EXTENSIONS=1 \ + OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES="$selected_sql_names" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh >/tmp/liboliphaunt-release-"$target_id"-extensions.log + ;; + windows-x64-msvc) + env \ + OLIPHAUNT_WINDOWS_WORK_ROOT="${OLIPHAUNT_WINDOWS_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id-extension-release}" \ + OLIPHAUNT_BUILD_EXTENSIONS=1 \ + OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES="$selected_sql_names" \ + pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass \ + -File src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 >/tmp/liboliphaunt-release-"$target_id"-extensions.log + ;; + *) + fail "desktop extension runtime builder called for non-desktop target $target_id" + ;; + esac +} + +build_mobile_host_extension_runtime() { + case "$target_id" in + ios-xcframework) + [ "$(uname -s)" = "Darwin" ] || fail "$target_id host extension runtime must be built on macOS" + env \ + OLIPHAUNT_WORK_ROOT="${OLIPHAUNT_EXTENSION_MACOS_RUNTIME_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id}" \ + OLIPHAUNT_BUILD_EXTENSIONS=1 \ + OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES="$selected_sql_names" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh >/tmp/liboliphaunt-release-mobile-host-extensions.log + ;; + android-*) + [ "$(uname -s)" = "Linux" ] || fail "$target_id host extension runtime must be built on Linux" + env \ + OLIPHAUNT_LINUX_WORK_ROOT="${OLIPHAUNT_EXTENSION_LINUX_RUNTIME_ROOT:-$root/target/liboliphaunt-pg18-linux-x64-gnu-extension-release}" \ + OLIPHAUNT_BUILD_EXTENSIONS=1 \ + OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES="$selected_sql_names" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh >/tmp/liboliphaunt-release-mobile-host-extensions.log + ;; + *) + fail "mobile host extension runtime requested for non-mobile target $target_id" + ;; + esac +} + +build_mobile_static_artifacts() { + local mobile_extensions="$1" + local macos_runtime_root macos_archive_root + [ -n "$mobile_extensions" ] || return 0 + case "$target_id" in + ios-xcframework) + [ "$(uname -s)" = "Darwin" ] || fail "$target_id extension artifacts must be built on macOS" + env \ + OLIPHAUNT_IOS_SIMULATOR_ROOT="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-simulator" \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh >/tmp/liboliphaunt-release-ios-simulator-extensions.log + env \ + OLIPHAUNT_IOS_DEVICE_ROOT="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-device" \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh >/tmp/liboliphaunt-release-ios-device-extensions.log + macos_runtime_root="${OLIPHAUNT_EXTENSION_MACOS_RUNTIME_ROOT:-$root/target/liboliphaunt-pg18-extension-release-$target_id}" + macos_archive_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/macos-extension-archives" + env \ + OLIPHAUNT_MACOS_RUNTIME_ROOT="$macos_runtime_root" \ + OLIPHAUNT_MACOS_EXTENSION_ARCHIVE_ROOT="$macos_archive_root" \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh >/tmp/liboliphaunt-release-macos-extension-archives.log + env \ + OLIPHAUNT_IOS_SIMULATOR_OUT="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-simulator/out" \ + OLIPHAUNT_IOS_DEVICE_OUT="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-device/out" \ + OLIPHAUNT_MACOS_EXTENSION_OUT="$macos_archive_root/out" \ + OLIPHAUNT_IOS_EXTENSION_XCFRAMEWORK_ROOT="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-extension-xcframeworks" \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh >/tmp/liboliphaunt-release-ios-extension-xcframeworks.log + ;; + android-arm64-v8a) + env \ + OLIPHAUNT_ANDROID_ARM64_ROOT="$root/target/liboliphaunt-mobile-extension-release/$target_id/android-arm64" \ + OLIPHAUNT_ANDROID_ABI=arm64-v8a \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh >/tmp/liboliphaunt-release-android-arm64-extensions.log + ;; + android-x86_64) + env \ + OLIPHAUNT_ANDROID_X86_64_ROOT="$root/target/liboliphaunt-mobile-extension-release/$target_id/android-x86_64" \ + OLIPHAUNT_ANDROID_ABI=x86_64 \ + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="$mobile_extensions" \ + src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh >/tmp/liboliphaunt-release-android-x86_64-extensions.log + ;; + esac +} + +desktop_runtime_artifact_name() { + local sql_name="$1" + printf 'liboliphaunt-%s-extension-%s-%s-runtime.tar.gz\n' "$version" "$sql_name" "$target_id" +} + +mobile_runtime_artifact_name() { + local sql_name="$1" + printf 'liboliphaunt-%s-extension-%s-%s-runtime.tar.gz\n' "$version" "$sql_name" "$target_id" +} + +make_extension_artifact() { + local runtime="$1" + local sql_name="$2" + local creates_extension="$3" + local stem="$4" + local dependencies="$5" + local shared_preload="$6" + local data_files="$7" + local output="$8" + shift 8 + + local -a artifact_args=( + "$packager" create-artifact + --runtime "$runtime" + --sql-name "$sql_name" + --creates-extension "$creates_extension" + --target "$target_id" + --native-runtime-product liboliphaunt-native + --native-runtime-version "$native_runtime_version" + --output "$out_dir/$output" + --stage-root "$stage_root" + --format tar-gz + --force + ) + if [ "$stem" != "-" ]; then + artifact_args+=(--native-module-stem "$stem" --native-module-file "$stem.$(module_suffix_for_target)") + fi + if [ "$dependencies" != "-" ]; then + artifact_args+=(--dependency "$dependencies") + fi + if [ "$shared_preload" != "-" ]; then + artifact_args+=(--shared-preload-library "$shared_preload") + fi + if [ "$data_files" != "-" ]; then + IFS=',' read -r -a data_file_array <<<"$data_files" + for data_file in "${data_file_array[@]}"; do + [ -n "$data_file" ] || continue + artifact_args+=(--data-file "$data_file") + done + fi + if [ "$#" -gt 0 ]; then + artifact_args+=("$@") + fi + bun "${artifact_args[@]}" >/tmp/liboliphaunt-release-extension-artifact-"$target_id"-"$sql_name".log +} + +package_desktop_target() { + local source_runtime embedded_modules runtime binary_contract_runtime + build_desktop_extension_runtime + source_runtime="$(host_extension_runtime_root)" + embedded_modules="$(host_extension_embedded_modules_root)" + require_dir "$source_runtime" "$target_id extension runtime" + require_dir "$embedded_modules" "$target_id embedded extension modules" + runtime="$(prepare_extension_release_runtime "$source_runtime")" + if [ "$target_id" = "windows-x64-msvc" ]; then + tools/dev/bun.sh tools/release/windows-vc-runtime-closure.mjs verify \ + --root "$runtime" \ + --profile provider \ + --search-root "$runtime/bin" + binary_contract_runtime="$(prepare_windows_binary_contract_runtime "$runtime")" + tools/dev/bun.sh tools/release/platform-binary-contract.mjs \ + --target "$target_id" \ + --root "$binary_contract_runtime" \ + --windows-vc-runtime-profile provider + else + tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target "$target_id" --root "$runtime" + fi + if [[ "$target_id" == linux-*-gnu ]]; then + tools/release/check-linux-consumer-baseline.sh --target "$target_id" --root "$runtime" + fi + + local module_suffix + module_suffix="$(module_suffix_for_target)" + local sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy runtime_artifact + local -a profile_args=() + while IFS=$'\t' read -r sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy; do + [ -n "$sql_name" ] || continue + selected_sql_name_matches "$sql_name" || continue + [ "$pg_major" = "18" ] || fail "extension catalog row for $sql_name targets PostgreSQL $pg_major" + [ "$desktop_prebuilt" = "yes" ] || continue + runtime_artifact="$(desktop_runtime_artifact_name "$sql_name")" + profile_args=() + if [ "$stem" != "-" ]; then + profile_args+=(--embedded-module-root "$embedded_modules") + fi + make_extension_artifact \ + "$runtime" \ + "$sql_name" \ + "$creates_extension" \ + "$stem" \ + "$dependencies" \ + "$shared_preload" \ + "$data_files" \ + "$runtime_artifact" \ + ${profile_args[@]+"${profile_args[@]}"} + append_native_asset_index_row "$sql_name" "$native_extension_runtime_kind" "$runtime_artifact" + append_legacy_index_row "$sql_name" "$creates_extension" "$stem" "$dependencies" "$shared_preload" "$mobile_prebuilt" "-" "$runtime_artifact" "-" "-" "-" "$data_files" + done < <(catalog_rows) + printf '%s\n' "$module_suffix" >/dev/null +} + +package_ios_target() { + local source_runtime runtime mobile_extensions ios_sim_root ios_device_root macos_archive_root ios_xcframework_root + build_mobile_host_extension_runtime + mobile_extensions="$(mobile_module_extensions_csv)" + build_mobile_static_artifacts "$mobile_extensions" + source_runtime="$(host_extension_runtime_root)" + require_dir "$source_runtime" "mobile host extension runtime" + runtime="$(prepare_extension_release_runtime "$source_runtime")" + tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target macos-arm64 --root "$runtime" + ios_sim_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-simulator" + ios_device_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-device" + macos_archive_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/macos-extension-archives" + ios_xcframework_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/ios-extension-xcframeworks" + + local sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy runtime_artifact ios_artifact static_prefix registration_artifact dependency dependency_xcframework dependency_artifact + while IFS=$'\t' read -r sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy; do + [ -n "$sql_name" ] || continue + selected_sql_name_matches "$sql_name" || continue + [ "$pg_major" = "18" ] || fail "extension catalog row for $sql_name targets PostgreSQL $pg_major" + [ "$mobile_prebuilt" = "yes" ] || continue + + runtime_artifact="$(mobile_runtime_artifact_name "$sql_name")" + extra_args=() + if [ "$stem" != "-" ]; then + ios_sim_archive="$ios_sim_root/out/extensions/$stem/liboliphaunt_extension_$stem.a" + ios_device_archive="$ios_device_root/out/extensions/$stem/liboliphaunt_extension_$stem.a" + static_prefix="$(oliphaunt_static_symbol_prefix "$stem")" + require_file "$ios_sim_archive" "iOS simulator static archive for $sql_name" + require_file "$ios_device_archive" "iOS device static archive for $sql_name" + extra_args+=( + --mobile-static-archive "ios-simulator:$ios_sim_archive" + --mobile-static-archive "ios-device:$ios_device_archive" + --static-symbol-prefix "$static_prefix" + ) + if [ "$sql_name" = "postgis" ]; then + extra_args+=( + --static-symbol-alias "difference:${static_prefix}_difference" + --static-symbol-alias "pg_finfo_difference:pg_finfo_${static_prefix}_difference" + ) + fi + collect_mobile_static_dependency_archive_args ios-simulator "$ios_sim_root" "$sql_name" + extra_args+=(${mobile_dependency_args[@]+"${mobile_dependency_args[@]}"}) + collect_mobile_static_dependency_archive_args ios-device "$ios_device_root" "$sql_name" + extra_args+=(${mobile_dependency_args[@]+"${mobile_dependency_args[@]}"}) + + stage_ios_extension="$stage_root/liboliphaunt-${version}-ios-extension-$stem" + rm -rf "$stage_ios_extension" + mkdir -p "$stage_ios_extension" + rsync -a --delete \ + "$ios_xcframework_root/out/$stem/liboliphaunt_extension_$stem.xcframework" \ + "$stage_ios_extension/" + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + dependency_xcframework="$ios_xcframework_root/out/dependencies/$dependency/liboliphaunt_dependency_$dependency.xcframework" + require_dir "$dependency_xcframework" "iOS dependency XCFramework for $sql_name dependency $dependency" + mkdir -p "$stage_ios_extension/dependencies/$dependency" + rsync -a --delete "$dependency_xcframework" "$stage_ios_extension/dependencies/$dependency/" + done < <(oliphaunt_mobile_static_extension_dependencies_for_target "$sql_name" ios || true) + tools/dev/bun.sh tools/release/platform-binary-contract.mjs \ + --target "$target_id" \ + --root "$stage_ios_extension" \ + --required-apple-platforms macos,ios,ios-simulator + archive_swiftpm_xcframework \ + "$stage_ios_extension/liboliphaunt_extension_$stem.xcframework" \ + "$out_dir/liboliphaunt-${version}-apple-spm-extension-$stem.zip" + ios_artifact="liboliphaunt-${version}-apple-spm-extension-$stem.zip" + registration_artifact="liboliphaunt-${version}-apple-spm-extension-$stem-registration.json" + bun tools/release/ios-extension-registration.mjs \ + --sql-name "$sql_name" \ + --native-module-stem "$stem" \ + --simulator-out "$ios_sim_root/out" \ + --device-out "$ios_device_root/out" \ + --macos-out "$macos_archive_root/out" \ + --output "$out_dir/$registration_artifact" + + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + dependency_xcframework="$ios_xcframework_root/out/dependencies/$dependency/liboliphaunt_dependency_$dependency.xcframework" + require_dir "$dependency_xcframework" "iOS dependency XCFramework for $sql_name dependency $dependency" + tools/dev/bun.sh tools/release/platform-binary-contract.mjs \ + --target "$target_id" \ + --root "$dependency_xcframework" \ + --required-apple-platforms macos,ios,ios-simulator + dependency_artifact="liboliphaunt-${version}-apple-spm-dependency-$dependency.zip" + archive_swiftpm_xcframework \ + "$dependency_xcframework" \ + "$out_dir/$dependency_artifact" + append_native_asset_index_row "$sql_name" ios-dependency-xcframework "$dependency_artifact" "$dependency" + done < <(oliphaunt_mobile_static_extension_dependencies_for_target "$sql_name" ios || true) + else + ios_artifact="-" + registration_artifact="-" + fi + make_extension_artifact "$runtime" "$sql_name" "$creates_extension" "$stem" "$dependencies" "$shared_preload" "$data_files" "$runtime_artifact" ${extra_args[@]+"${extra_args[@]}"} + append_native_asset_index_row "$sql_name" "$native_extension_runtime_kind" "$runtime_artifact" + append_native_asset_index_row "$sql_name" ios-xcframework "$ios_artifact" "$stem" "$registration_artifact" + append_legacy_index_row "$sql_name" "$creates_extension" "$stem" "$dependencies" "$shared_preload" "$mobile_prebuilt" "ios-simulator,ios-device" "$runtime_artifact" "$ios_artifact" "-" "-" "$data_files" + done < <(catalog_rows) +} + +package_android_target() { + local source_runtime runtime mobile_extensions android_root android_static_target + build_mobile_host_extension_runtime + mobile_extensions="$(mobile_module_extensions_csv)" + build_mobile_static_artifacts "$mobile_extensions" + source_runtime="$(host_extension_runtime_root)" + require_dir "$source_runtime" "mobile host extension runtime" + runtime="$(prepare_extension_release_runtime "$source_runtime")" + tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target linux-x64-gnu --root "$runtime" + case "$target_id" in + android-arm64-v8a) + android_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/android-arm64" + android_static_target="android-arm64-v8a" + ;; + android-x86_64) + android_root="$root/target/liboliphaunt-mobile-extension-release/$target_id/android-x86_64" + android_static_target="android-x86_64" + ;; + *) fail "Android target packager called for $target_id" ;; + esac + tools/dev/bun.sh tools/release/platform-binary-contract.mjs --target "$target_id" --root "$android_root/out" + + local sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy runtime_artifact android_archive static_prefix + while IFS=$'\t' read -r sql_name pg_major creates_extension stem dependencies shared_preload desktop_prebuilt mobile_prebuilt mobile_static_required mobile_static_targets data_files artifact_policy; do + [ -n "$sql_name" ] || continue + selected_sql_name_matches "$sql_name" || continue + [ "$pg_major" = "18" ] || fail "extension catalog row for $sql_name targets PostgreSQL $pg_major" + [ "$mobile_prebuilt" = "yes" ] || continue + + runtime_artifact="$(mobile_runtime_artifact_name "$sql_name")" + extra_args=() + if [ "$stem" != "-" ]; then + android_archive="$android_root/out/extensions/$stem/liboliphaunt_extension_$stem.a" + static_prefix="$(oliphaunt_static_symbol_prefix "$stem")" + require_file "$android_archive" "Android static archive for $sql_name" + extra_args+=( + --mobile-static-archive "$android_static_target:$android_archive" + --static-symbol-prefix "$static_prefix" + ) + if [ "$sql_name" = "postgis" ]; then + extra_args+=( + --static-symbol-alias "difference:${static_prefix}_difference" + --static-symbol-alias "pg_finfo_difference:pg_finfo_${static_prefix}_difference" + ) + fi + collect_mobile_static_dependency_archive_args "$android_static_target" "$android_root" "$sql_name" + extra_args+=(${mobile_dependency_args[@]+"${mobile_dependency_args[@]}"}) + fi + make_extension_artifact "$runtime" "$sql_name" "$creates_extension" "$stem" "$dependencies" "$shared_preload" "$data_files" "$runtime_artifact" ${extra_args[@]+"${extra_args[@]}"} + append_native_asset_index_row "$sql_name" "$native_extension_runtime_kind" "$runtime_artifact" + append_legacy_index_row "$sql_name" "$creates_extension" "$stem" "$dependencies" "$shared_preload" "$mobile_prebuilt" "$android_static_target" "$runtime_artifact" "-" "-" "-" "$data_files" + done < <(catalog_rows) +} + +fetch_extension_source_assets +echo "==> Reading exact extension catalog" +bun "$packager" list-catalog >"$catalog_file" +write_indexes + +case "$target_id" in + macos-arm64|linux-x64-gnu|linux-arm64-gnu|windows-x64-msvc) + package_desktop_target + ;; + ios-xcframework) + package_ios_target + ;; + android-arm64-v8a|android-x86_64) + package_android_target + ;; +esac + +[ "$(wc -l <"$native_asset_index" | awk '{ print $1 }')" -gt 1 ] || + fail "no native exact-extension artifacts were produced for target $target_id${extension_product:+ product $extension_product}" + +echo "extensionReleaseAssetDir=$out_dir" diff --git a/src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs b/src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs new file mode 100644 index 00000000..b49c8474 --- /dev/null +++ b/src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs @@ -0,0 +1,595 @@ +#!/usr/bin/env bun + +import { createHash, randomUUID } from "node:crypto"; +import { + copyFile, + lstat, + mkdir, + readFile, + readdir, + realpath, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import path from "node:path"; + +import { WINDOWS_VC_RUNTIME_DLLS } from "../../../../../tools/release/windows-vc-runtime-closure.mjs"; +import { + inspectPlatformBinaryBuffer, + inspectPlatformBinaryEntries, +} from "../../../../../tools/release/platform-binary-contract.mjs"; + +const TOOL = "stage-windows-binary-contract.mjs"; +const CATALOG_HEADER = Object.freeze([ + "sql_name", + "pg_major", + "creates_extension", + "native_module_stem", + "dependencies", + "shared_preload", + "desktop_prebuilt", + "mobile_prebuilt", + "mobile_static_registry_required", + "mobile_static_archive_targets", + "data_files", + "artifact", +]); + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function failure(message) { + return new Error(`${TOOL}: ${message}`); +} + +export function validateWindowsEmbeddedModuleImports(data, label) { + const inspected = inspectPlatformBinaryBuffer(data, { + target: "windows-x64-msvc", + label, + }); + const imports = new Set( + inspected.slices + .flatMap((slice) => slice.imports ?? []) + .map((name) => name.toLowerCase()), + ); + if (imports.has("postgres.exe")) { + throw failure( + `${label} imports postgres.exe; embedded extension DLLs must not bind to the standalone server provider (they may bind to oliphaunt.dll or be host-neutral)`, + ); + } + const providerBound = imports.has("oliphaunt.dll"); + return { + imports: [...imports].sort(compareText), + backendProvider: providerBound ? "oliphaunt.dll" : "host-neutral", + hostNeutral: !providerBound, + providerBound, + }; +} + +export function validateWindowsServerModuleImports(data, label) { + const inspected = inspectPlatformBinaryBuffer(data, { + target: "windows-x64-msvc", + label, + }); + const imports = new Set( + inspected.slices + .flatMap((slice) => slice.imports ?? []) + .map((name) => name.toLowerCase()), + ); + if (imports.has("oliphaunt.dll")) { + throw failure( + `${label} imports oliphaunt.dll; standalone PostgreSQL extension DLLs must not bind to the embedded provider (they may bind to postgres.exe or be host-neutral)`, + ); + } + const serverBound = imports.has("postgres.exe"); + return { + imports: [...imports].sort(compareText), + backendProvider: serverBound ? "postgres.exe" : "host-neutral", + hostNeutral: !serverBound, + serverBound, + }; +} + +function sha256(data) { + return createHash("sha256").update(data).digest("hex"); +} + +function portableId(value, label) { + if (!/^[A-Za-z0-9._-]{1,128}$/u.test(value)) { + throw failure( + `${label} ${JSON.stringify(value)} is not a portable identifier`, + ); + } + return value; +} + +function parseSelection(value) { + if (value === undefined || value === null || value === "") return []; + const result = String(value) + .split(",") + .map((item) => portableId(item.trim(), "selected SQL name")); + if (result.some((item) => item.length === 0)) { + throw failure( + "selected SQL names must be a comma-separated list without empty entries", + ); + } + if (new Set(result).size !== result.length) { + throw failure("selected SQL names contain duplicates"); + } + return result; +} + +export function parseExtensionCatalog(text, selectedSqlNames = "") { + const lines = String(text).replace(/\r\n/gu, "\n").split("\n"); + if (lines.at(-1) === "") lines.pop(); + if (lines.length === 0 || lines[0] !== CATALOG_HEADER.join("\t")) { + throw failure( + "extension catalog header does not match the exact native artifact schema", + ); + } + + const rows = new Map(); + for (const [offset, line] of lines.slice(1).entries()) { + if (line.length === 0) + throw failure(`extension catalog row ${offset + 2} is empty`); + const columns = line.split("\t"); + if (columns.length !== CATALOG_HEADER.length) { + throw failure( + `extension catalog row ${offset + 2} has ${columns.length} columns; expected ${CATALOG_HEADER.length}`, + ); + } + const sqlName = portableId( + columns[0], + `extension catalog row ${offset + 2} SQL name`, + ); + if (rows.has(sqlName)) + throw failure(`extension catalog repeats SQL name ${sqlName}`); + if (columns[1] !== "18") + throw failure( + `extension catalog ${sqlName} targets PostgreSQL ${columns[1]}, not 18`, + ); + if (!["yes", "no"].includes(columns[6])) { + throw failure( + `extension catalog ${sqlName} has invalid desktop_prebuilt value ${JSON.stringify(columns[6])}`, + ); + } + const stem = + columns[3] === "-" + ? null + : portableId(columns[3], `extension catalog ${sqlName} module stem`); + rows.set(sqlName, { sqlName, stem, desktopPrebuilt: columns[6] === "yes" }); + } + + const requested = parseSelection(selectedSqlNames); + const selected = + requested.length === 0 + ? [...rows.values()].filter(({ desktopPrebuilt }) => desktopPrebuilt) + : requested.map((sqlName) => { + const row = rows.get(sqlName); + if (row === undefined) + throw failure( + `selected SQL name ${sqlName} is absent from the extension catalog`, + ); + if (!row.desktopPrebuilt) + throw failure( + `selected SQL name ${sqlName} is not a desktop prebuilt extension`, + ); + return row; + }); + return selected.sort((left, right) => + compareText(left.sqlName, right.sqlName), + ); +} + +function containsPath(parent, candidate) { + const comparableParent = + process.platform === "win32" ? parent.toLowerCase() : parent; + const comparableCandidate = + process.platform === "win32" ? candidate.toLowerCase() : candidate; + const relative = path.relative(comparableParent, comparableCandidate); + return ( + relative === "" || + (!path.isAbsolute(relative) && + relative !== ".." && + !relative.startsWith(`..${path.sep}`)) + ); +} + +async function canonicalProspectivePath(candidate) { + const suffix = []; + let current = path.resolve(candidate); + while (true) { + const stat = await lstat(current).catch((error) => { + if (error?.code === "ENOENT") return null; + throw failure( + `cannot inspect prospective binary-contract output ancestor ${current}: ${error.message}`, + ); + }); + if (stat !== null) { + const canonical = await realpath(current).catch((error) => { + throw failure( + `cannot resolve prospective binary-contract output ancestor ${current}: ${error.message}`, + ); + }); + if (suffix.length > 0 && !(await lstat(canonical)).isDirectory()) { + throw failure( + `prospective binary-contract output ancestor ${current} is not a directory`, + ); + } + return path.resolve(canonical, ...suffix); + } + const parent = path.dirname(current); + if (parent === current) { + throw failure( + `cannot find an existing ancestor for binary-contract output ${candidate}`, + ); + } + suffix.unshift(path.basename(current)); + current = parent; + } +} + +async function requireRealDirectory(directory, label) { + const stat = await lstat(directory).catch(() => null); + if (stat === null || !stat.isDirectory() || stat.isSymbolicLink()) { + throw failure(`${label} ${directory} must be a real directory`); + } + return realpath(directory); +} + +async function copyContainedFile( + runtimeRoot, + runtimeReal, + relativePath, + stageRoot, +) { + const source = path.join(runtimeRoot, ...relativePath.split("/")); + const stat = await lstat(source).catch(() => null); + if (stat === null || !stat.isFile() || stat.isSymbolicLink()) { + throw failure( + `required Windows carrier file ${relativePath} must be a real regular file under ${runtimeRoot}`, + ); + } + const sourceReal = await realpath(source); + if (!containsPath(runtimeReal, sourceReal)) { + throw failure( + `required Windows carrier file ${relativePath} resolves outside ${runtimeRoot}`, + ); + } + const destination = path.join(stageRoot, ...relativePath.split("/")); + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(source, destination); + return relativePath; +} + +async function collectArtifactEntries( + artifactRoot, + artifactReal, + relative = "", +) { + const entries = []; + const directory = path.join(artifactRoot, relative); + const children = await readdir(directory, { withFileTypes: true }); + children.sort((left, right) => compareText(left.name, right.name)); + for (const child of children) { + const childRelative = relative ? `${relative}/${child.name}` : child.name; + const childPath = path.join(artifactRoot, ...childRelative.split("/")); + const stat = await lstat(childPath); + if (stat.isSymbolicLink()) { + throw failure( + `exact Windows extension artifact contains symbolic link ${childRelative}`, + ); + } + if (stat.isDirectory()) { + entries.push( + ...(await collectArtifactEntries( + artifactRoot, + artifactReal, + childRelative, + )), + ); + continue; + } + if (!stat.isFile()) { + throw failure( + `exact Windows extension artifact contains non-regular entry ${childRelative}`, + ); + } + const childReal = await realpath(childPath); + if (!containsPath(artifactReal, childReal)) { + throw failure( + `exact Windows extension artifact file ${childRelative} resolves outside ${artifactRoot}`, + ); + } + entries.push({ + name: `artifact/${childRelative}`, + data: await readFile(childPath), + isFile: true, + }); + } + return entries; +} + +export async function validateWindowsExtensionArtifactBinaryContract({ + artifactRoot, + providerRuntimeRoot, +}) { + if (!artifactRoot) throw failure("artifactRoot is required"); + if (!providerRuntimeRoot) throw failure("providerRuntimeRoot is required"); + const artifact = path.resolve(artifactRoot); + const provider = path.resolve(providerRuntimeRoot); + const artifactReal = await requireRealDirectory( + artifact, + "exact Windows extension artifact root", + ); + const providerReal = await requireRealDirectory( + provider, + "Windows provider runtime root", + ); + const entries = await collectArtifactEntries(artifact, artifactReal); + const serverModules = new Map(); + const embeddedModules = new Map(); + for (const entry of entries) { + if (/^artifact\/files\/lib\/postgresql\/[^/]+\.dll$/iu.test(entry.name)) { + const moduleName = path.posix.basename(entry.name); + serverModules.set(moduleName, { + data: entry.data, + ...validateWindowsServerModuleImports(entry.data, entry.name), + }); + } else if (/^artifact\/files\/lib\/modules\/[^/]+\.dll$/iu.test(entry.name)) { + const moduleName = path.posix.basename(entry.name); + embeddedModules.set(moduleName, { + data: entry.data, + ...validateWindowsEmbeddedModuleImports(entry.data, entry.name), + }); + } + } + const moduleNames = [ + ...new Set([...serverModules.keys(), ...embeddedModules.keys()]), + ].sort(compareText); + for (const moduleName of moduleNames) { + if (!serverModules.has(moduleName)) { + throw failure( + `exact Windows extension artifact is missing standalone server profile files/lib/postgresql/${moduleName}`, + ); + } + if (!embeddedModules.has(moduleName)) { + throw failure( + `exact Windows extension artifact is missing embedded provider profile files/lib/modules/${moduleName}`, + ); + } + const serverModule = serverModules.get(moduleName); + const embeddedModule = embeddedModules.get(moduleName); + const serverDigest = sha256(serverModule.data); + const embeddedDigest = sha256(embeddedModule.data); + if ( + serverDigest === embeddedDigest && + !(serverModule.hostNeutral && embeddedModule.hostNeutral) + ) { + throw failure( + `exact Windows extension artifact ${moduleName} host-bound server and embedded profiles have identical SHA-256 ${serverDigest}`, + ); + } + } + for (const name of WINDOWS_VC_RUNTIME_DLLS) { + const relativePath = `bin/${name}`; + const source = path.join(provider, ...relativePath.split("/")); + const stat = await lstat(source).catch(() => null); + if (stat === null || !stat.isFile() || stat.isSymbolicLink()) { + throw failure( + `required Windows carrier file ${relativePath} must be a real regular file under ${provider}`, + ); + } + const sourceReal = await realpath(source); + if (!containsPath(providerReal, sourceReal)) { + throw failure( + `required Windows carrier file ${relativePath} resolves outside ${provider}`, + ); + } + entries.push({ + name: `provider/${relativePath}`, + data: await readFile(source), + isFile: true, + }); + } + const inspection = inspectPlatformBinaryEntries(entries, { + target: "windows-x64-msvc", + rootLabel: "exact Windows extension artifact with provider runtime", + windowsVcRuntimeProfile: "provider", + }); + return { + ...inspection, + standaloneBackendProvider: "postgres.exe", + embeddedBackendProvider: "oliphaunt.dll", + serverBoundExtensionModules: moduleNames.filter( + (moduleName) => serverModules.get(moduleName).serverBound, + ), + hostNeutralServerModules: moduleNames.filter( + (moduleName) => serverModules.get(moduleName).hostNeutral, + ), + providerBoundEmbeddedModules: moduleNames.filter( + (moduleName) => embeddedModules.get(moduleName).providerBound, + ), + hostNeutralEmbeddedModules: moduleNames.filter( + (moduleName) => embeddedModules.get(moduleName).hostNeutral, + ), + byteIdenticalHostNeutralModules: moduleNames.filter((moduleName) => { + const serverModule = serverModules.get(moduleName); + const embeddedModule = embeddedModules.get(moduleName); + return ( + serverModule.hostNeutral && + embeddedModule.hostNeutral && + sha256(serverModule.data) === sha256(embeddedModule.data) + ); + }), + profileBindings: Object.fromEntries( + moduleNames.map((moduleName) => [ + moduleName, + { + server: serverModules.get(moduleName).backendProvider, + embedded: embeddedModules.get(moduleName).backendProvider, + }, + ]), + ), + profileSha256: Object.fromEntries( + moduleNames.map((moduleName) => [ + moduleName, + { + server: sha256(serverModules.get(moduleName).data), + embedded: sha256(embeddedModules.get(moduleName).data), + }, + ]), + ), + }; +} + +export async function stageWindowsExtensionBinaryContract({ + runtimeRoot, + catalogText, + selectedSqlNames = "", + outputRoot, +}) { + if (!runtimeRoot) throw failure("runtimeRoot is required"); + if (catalogText === undefined) throw failure("catalogText is required"); + if (!outputRoot) throw failure("outputRoot is required"); + + const runtime = path.resolve(runtimeRoot); + const output = path.resolve(outputRoot); + const runtimeReal = await requireRealDirectory( + runtime, + "Windows extension runtime root", + ); + const outputStat = await lstat(output).catch((error) => { + if (error?.code === "ENOENT") return null; + throw failure( + `cannot inspect binary-contract output ${output}: ${error.message}`, + ); + }); + if ( + outputStat !== null && + (!outputStat.isDirectory() || outputStat.isSymbolicLink()) + ) { + throw failure( + `existing binary-contract output ${output} must be a real directory`, + ); + } + const outputCanonical = await canonicalProspectivePath(output); + if ( + containsPath(runtimeReal, outputCanonical) || + containsPath(outputCanonical, runtimeReal) + ) { + throw failure("binary-contract output must not overlap the source runtime"); + } + + const selected = parseExtensionCatalog(catalogText, selectedSqlNames); + const moduleNames = [ + ...new Set(selected.map(({ stem }) => stem).filter(Boolean)), + ].sort(); + const serverBoundExtensionModules = []; + const hostNeutralServerModules = []; + const partial = `${output}.partial-${process.pid}-${randomUUID()}`; + await rm(partial, { recursive: true, force: true }); + try { + await mkdir(partial, { recursive: true }); + const files = []; + for (const name of WINDOWS_VC_RUNTIME_DLLS) { + files.push( + await copyContainedFile(runtime, runtimeReal, `bin/${name}`, partial), + ); + } + for (const stem of moduleNames) { + const relativePath = await copyContainedFile( + runtime, + runtimeReal, + `lib/postgresql/${stem}.dll`, + partial, + ); + const binding = validateWindowsServerModuleImports( + await readFile(path.join(partial, ...relativePath.split("/"))), + relativePath, + ); + if (binding.serverBound) { + serverBoundExtensionModules.push(`${stem}.dll`); + } else { + hostNeutralServerModules.push(`${stem}.dll`); + } + files.push(relativePath); + } + const manifest = { + schema: "oliphaunt-windows-extension-binary-contract-v4", + selectedSqlNames: selected.map(({ sqlName }) => sqlName), + standaloneBackendProvider: "postgres.exe", + forbiddenEmbeddedBackendProvider: "oliphaunt.dll", + providerRuntimeDlls: [...WINDOWS_VC_RUNTIME_DLLS], + extensionModules: moduleNames.map((stem) => `${stem}.dll`), + serverBoundExtensionModules, + hostNeutralServerModules, + files: [...files].sort(), + }; + await writeFile( + path.join(partial, "binary-contract-manifest.json"), + `${JSON.stringify(manifest, null, 2)}\n`, + "utf8", + ); + await rm(output, { recursive: true, force: true }); + await mkdir(path.dirname(output), { recursive: true }); + await rename(partial, output); + return manifest; + } catch (error) { + await rm(partial, { recursive: true, force: true }); + throw error; + } +} + +function usage() { + return `usage: ${TOOL} --runtime DIR --catalog FILE --output DIR [--selected-sql-names CSV]\n`; +} + +function parseArgs(argv) { + const args = { runtime: "", catalog: "", output: "", selectedSqlNames: "" }; + for (let index = 0; index < argv.length; index += 1) { + const flag = argv[index]; + if (flag === "--help" || flag === "-h") return { help: true }; + const value = argv[++index]; + if (value === undefined) throw failure(`${flag} requires a value`); + if (flag === "--runtime") args.runtime = value; + else if (flag === "--catalog") args.catalog = value; + else if (flag === "--output") args.output = value; + else if (flag === "--selected-sql-names") args.selectedSqlNames = value; + else throw failure(`unknown argument ${flag}`); + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + if (args.help) { + process.stdout.write(usage()); + return; + } + if (!args.runtime || !args.catalog || !args.output) { + process.stderr.write(usage()); + process.exitCode = 2; + return; + } + const manifest = await stageWindowsExtensionBinaryContract({ + runtimeRoot: args.runtime, + catalogText: await readFile(args.catalog, "utf8"), + selectedSqlNames: args.selectedSqlNames, + outputRoot: args.output, + }); + console.log( + `Windows server extension binary-contract view staged: modules=${manifest.extensionModules.length} serverBound=${manifest.serverBoundExtensionModules.length} hostNeutral=${manifest.hostNeutralServerModules.length} providerDlls=${manifest.providerRuntimeDlls.length}`, + ); +} + +if (import.meta.main) { + try { + await main(Bun.argv.slice(2)); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/src/extensions/artifacts/packages/moon.yml b/src/extensions/artifacts/packages/moon.yml new file mode 100644 index 00000000..795d0de8 --- /dev/null +++ b/src/extensions/artifacts/packages/moon.yml @@ -0,0 +1,116 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-packages" +language: "javascript" +layer: "tool" +stack: "systems" +tags: ["extensions", "artifacts", "release"] +dependsOn: + - id: "extension-artifacts-native" + scope: "build" + - id: "extension-artifacts-wasix" + scope: "build" + - id: "extensions" + scope: "build" + +project: + title: "Extension Packages" + description: "Publishable exact SQL extension artifacts staged per release product." + owner: "oliphaunt" + +tasks: + assemble-mobile: + tags: ["release", "artifact-package", "ci-mobile-extension-packages"] + command: "bash src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh" + inputs: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/runtimes/liboliphaunt/native/moon.yml" + - "/src/runtimes/liboliphaunt/native/release.toml" + - "/src/runtimes/liboliphaunt/native/VERSION" + - "/src/runtimes/liboliphaunt/wasix/moon.yml" + - "/src/runtimes/liboliphaunt/wasix/release.toml" + - "/src/runtimes/liboliphaunt/wasix/VERSION" + - "/src/shared/extension-runtime-contract/**/*" + - "/src/sdks/react-native/tools/validate-mobile-runtime-files.mjs" + - "/tools/dev/bun.sh" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/cargo-source-package.mjs" + - "/tools/release/check-staged-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/extension-target-profiles.toml" + - "/tools/release/ios-carrier-manifest.mjs" + - "/tools/release/tar-command.mjs" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/prepare-swift-release-consumer.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/tools/release/release-semantic-inputs.mjs" + - "/tools/release/release-semantic-inputs.toml" + - "/tools/release/rust-native-targets.mjs" + - "/tools/release/swift-source-carrier-contract.mjs" + - "/tools/release/wasix-aot-manifest.mjs" + - "/tools/release/wasix-cargo-artifact-contract.mjs" + - "/tools/dev/moon-command.mjs" + - "/src/postgres/versions/18/source.toml" + - "/target/extensions/native/release-assets/**/*" + outputs: + - "/target/extension-artifacts/**/*" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: true + + assemble-release: + tags: ["release", "artifact-package", "ci-extension-packages"] + command: "bash src/extensions/artifacts/packages/tools/package-release-assets.sh" + inputs: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/runtimes/liboliphaunt/native/moon.yml" + - "/src/runtimes/liboliphaunt/native/release.toml" + - "/src/runtimes/liboliphaunt/native/VERSION" + - "/src/runtimes/liboliphaunt/wasix/moon.yml" + - "/src/runtimes/liboliphaunt/wasix/release.toml" + - "/src/runtimes/liboliphaunt/wasix/VERSION" + - "/src/shared/extension-runtime-contract/**/*" + - "/src/sdks/react-native/tools/validate-mobile-runtime-files.mjs" + - "/tools/dev/bun.sh" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/cargo-source-package.mjs" + - "/tools/release/check-staged-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/extension-target-profiles.toml" + - "/tools/release/ios-carrier-manifest.mjs" + - "/tools/release/tar-command.mjs" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/prepare-swift-release-consumer.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/tools/release/release-semantic-inputs.mjs" + - "/tools/release/release-semantic-inputs.toml" + - "/tools/release/rust-native-targets.mjs" + - "/tools/release/swift-source-carrier-contract.mjs" + - "/tools/release/wasix-aot-manifest.mjs" + - "/tools/release/wasix-cargo-artifact-contract.mjs" + - "/tools/dev/moon-command.mjs" + - "/src/postgres/versions/18/source.toml" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + - "/target/extensions/wasix/aot-artifacts/**/*" + outputs: + - "/target/extension-artifacts/**/*" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: true diff --git a/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh new file mode 100755 index 00000000..f7be6f10 --- /dev/null +++ b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh @@ -0,0 +1,65 @@ +#!/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 "package-mobile-release-assets.sh: $*" >&2 + exit 1 +} + +targets_csv="${OLIPHAUNT_EXTENSION_PACKAGE_NATIVE_TARGETS:-}" +[ -n "$targets_csv" ] || fail "OLIPHAUNT_EXTENSION_PACKAGE_NATIVE_TARGETS must list one or more native targets" + +products_csv="${OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS:-}" +args=() +validation_args=() +if [ -z "$products_csv" ]; then + fail "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS must list selected exact-extension products for mobile packaging" +fi +IFS=',' read -r -a products <<<"$products_csv" +for product in "${products[@]}"; do + product="$(printf '%s' "$product" | xargs)" + [ -n "$product" ] || continue + args+=("$product") + validation_args+=(--require-extension-product "$product") +done + +[ "${#args[@]}" -gt 0 ] || fail "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS did not contain any products" + +IFS=',' read -r -a targets <<<"$targets_csv" +seen="," +for target in "${targets[@]}"; do + target="$(printf '%s' "$target" | xargs)" + [ -n "$target" ] || continue + case "$target" in + android-arm64-v8a|android-x86_64|ios-xcframework) + ;; + *) + fail "mobile extension package target must be android-arm64-v8a, android-x86_64, or ios-xcframework; got $target" + ;; + esac + case "$seen" in + *",$target,"*) + ;; + *) + seen="$seen$target," + args+=(--require-native-target "$target") + ;; + esac +done + +case " ${args[*]} " in + *" --require-native-target "*) + ;; + *) + fail "OLIPHAUNT_EXTENSION_PACKAGE_NATIVE_TARGETS did not contain any targets" + ;; +esac + +tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs "${args[@]}" +tools/dev/bun.sh tools/release/check-staged-artifacts.mjs "${validation_args[@]}" diff --git a/src/extensions/artifacts/packages/tools/package-release-assets.sh b/src/extensions/artifacts/packages/tools/package-release-assets.sh new file mode 100755 index 00000000..62dfbef0 --- /dev/null +++ b/src/extensions/artifacts/packages/tools/package-release-assets.sh @@ -0,0 +1,23 @@ +#!/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" + +validation_args=(--require-full-extension-targets) +products_csv="${OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS:-}" +IFS=',' read -r -a products <<<"$products_csv" +for product in "${products[@]}"; do + product="$(printf '%s' "$product" | xargs)" + [ -n "$product" ] || continue + validation_args+=(--require-extension-product "$product") +done +if [ "${#validation_args[@]}" -eq 1 ]; then + validation_args+=(--require-extension-product all) +fi + +tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs --all --require-native --require-wasix +tools/dev/bun.sh tools/release/check-staged-artifacts.mjs "${validation_args[@]}" diff --git a/src/extensions/artifacts/wasix/moon.yml b/src/extensions/artifacts/wasix/moon.yml new file mode 100644 index 00000000..6f761612 --- /dev/null +++ b/src/extensions/artifacts/wasix/moon.yml @@ -0,0 +1,69 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-artifacts-wasix" +language: "unknown" +layer: "tool" +stack: "systems" +tags: ["extensions", "artifacts", "wasix"] +dependsOn: + - id: "extension-model" + scope: "build" + - id: "extensions" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + - id: "source-inputs" + scope: "build" + +project: + title: "WASIX Extension Artifacts" + description: "Publishable exact-extension artifact checks for the WASIX runtime." + owner: "oliphaunt" + +tasks: + check: + tags: ["quality", "static"] + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + deps: + - "extension-model:check" + inputs: + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/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" + options: + cache: true + runFromWorkspaceRoot: true + build-target: + tags: ["release", "artifact-builder", "ci-extension-artifacts-wasix"] + command: "bash src/extensions/artifacts/wasix/tools/package-release-assets.sh" + deps: + - "extension-artifacts-wasix:check" + - "liboliphaunt-wasix:runtime-portable" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/postgres/versions/18/**/*" + - "/src/runtimes/liboliphaunt/wasix/**/*" + - "/src/shared/extension-runtime-contract/**/*" + - "/tools/xtask/**/*" + outputs: + - "/target/extensions/wasix/release-assets/**/*" + options: + cache: false + runFromWorkspaceRoot: true + runInCI: true 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..a2c2ddc3 --- /dev/null +++ b/src/extensions/artifacts/wasix/tools/package-release-assets.mjs @@ -0,0 +1,237 @@ +#!/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); + const classReleasePath = path.join(classRoot, "release.toml"); + if ((await fileSize(classReleasePath)) !== undefined) { + files.push(classReleasePath); + } + 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; + const members = metadata.kind === "exact-extension-artifact" + ? [metadata.extension_sql_name] + : metadata.kind === "exact-extension-bundle" + ? metadata.extension_sql_names + : undefined; + if (!Array.isArray(members) || members.length === 0 || members.some((sqlName) => typeof sqlName !== "string" || sqlName.length === 0)) { + fail(`${relativeToRoot(root, releasePath)} must declare exact extension_sql_name or extension_sql_names members`); + } + for (const sqlName of members) 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 new file mode 100755 index 00000000..25607e29 --- /dev/null +++ b/src/extensions/artifacts/wasix/tools/package-release-assets.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)" || { + echo "unable to determine repository root from $script_dir; run this script from a Git checkout" >&2 + exit 1 +} +[ -f "$root/package.json" ] && [ -d "$root/src/extensions/artifacts/wasix" ] || { + echo "package-wasix-extension-assets.sh: must run inside the Oliphaunt workspace" >&2 + exit 1 +} +cd "$root" + +fail() { + echo "package-wasix-extension-assets.sh: $*" >&2 + exit 1 +} + +raw_target="${OLIPHAUNT_EXTENSION_TARGET:-portable}" +case "$raw_target" in + portable | wasix-portable) target_id="wasix-portable" ;; + *) fail "WASIX exact-extension artifacts are portable; unsupported target '$raw_target'" ;; +esac + +extension_product="${OLIPHAUNT_EXTENSION_PRODUCT:-${1:-}}" +extension_products="${OLIPHAUNT_EXTENSION_PRODUCTS:-}" +if [ -n "$extension_product" ]; then + if [ -n "$extension_products" ]; then + extension_products="$extension_products,$extension_product" + else + extension_products="$extension_product" + fi +fi +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" +if [ -n "$extension_product" ] && [ -z "${OLIPHAUNT_EXTENSION_PRODUCTS:-}" ]; then + default_out_dir="$default_out_dir/$extension_product" +fi +out_dir="${OLIPHAUNT_WASIX_EXTENSION_RELEASE_ASSET_DIR:-$default_out_dir}" + +[ -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" + +"$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/assets/extensions.promoted.toml b/src/extensions/catalog/extensions.promoted.toml similarity index 81% rename from assets/extensions.promoted.toml rename to src/extensions/catalog/extensions.promoted.toml index 7844e8d7..a4befb01 100644 --- a/assets/extensions.promoted.toml +++ b/src/extensions/catalog/extensions.promoted.toml @@ -2,7 +2,9 @@ format-version = 1 [[extensions]] id = "age" -stable = true +build = false +stable = false +blocker = "Apache AGE does not currently build against PostgreSQL 18.4 in the WASIX lane; ag_label.c still calls ExecInitExtraTupleSlot, which is not available in PG18. Keep graph/pgGraph out of release artifacts until there is an official PG18-compatible upstream." [[extensions]] id = "amcheck" @@ -118,9 +120,7 @@ stable = true [[extensions]] id = "pgcrypto" -build = false -stable = false -blocker = "Requires a pinned WASIX OpenSSL/libcrypto sysroot; current generic contrib build fails on openssl/evp.h." +stable = true [[extensions]] id = "pgtap" @@ -128,9 +128,7 @@ stable = true [[extensions]] id = "postgis" -build = false -stable = false -blocker = "Requires a pinned WASIX geospatial dependency stack and PostGIS configure/install-delta packaging before smoke." +stable = true [[extensions]] id = "seg" @@ -158,9 +156,7 @@ stable = true [[extensions]] id = "uuid_ossp" -build = false -stable = false -blocker = "Requires a pinned WASIX OSSP UUID/libuuid sysroot; upstream Emscripten builder provides this separately." +stable = true [[extensions]] id = "vector" diff --git a/src/extensions/catalog/extensions.smoke.toml b/src/extensions/catalog/extensions.smoke.toml new file mode 100644 index 00000000..0406c8ec --- /dev/null +++ b/src/extensions/catalog/extensions.smoke.toml @@ -0,0 +1,281 @@ +format-version = 1 + +[[extensions]] +id = "age" +direct = "not-run" +server = "not-run" +restart = "not-run" +dump-restore = "not-run" + +[[extensions]] +id = "amcheck" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "auto_explain" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "bloom" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "btree_gin" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "btree_gist" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "citext" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "cube" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "dict_int" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "dict_xsyn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "earthdistance" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "file_fdw" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "fuzzystrmatch" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "hstore" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "intarray" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "isn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "lo" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "ltree" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pageinspect" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_buffercache" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_freespacemap" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_hashids" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_ivm" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_surgery" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_textsearch" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_trgm" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_uuidv7" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_visibility" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pg_walinspect" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pgcrypto" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "pgtap" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "postgis" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "seg" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "tablefunc" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "tcn" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "tsm_system_rows" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "tsm_system_time" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "unaccent" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "uuid_ossp" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" + +[[extensions]] +id = "vector" +direct = "passed" +server = "passed" +restart = "passed" +dump-restore = "passed" diff --git a/src/extensions/catalog/extensions.source.json b/src/extensions/catalog/extensions.source.json new file mode 100644 index 00000000..2d1c336f --- /dev/null +++ b/src/extensions/catalog/extensions.source.json @@ -0,0 +1,1536 @@ +{ + "format-version": 1, + "extensions": [ + { + "id": "age", + "sql-name": "age", + "rust-constant": "AGE", + "display-name": "Apache AGE", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "age", + "package-export": "./age", + "tags": [ + "postgres extension" + ], + "bundle-size": 141551, + "control-file": "target/oliphaunt-sources/checkouts/age/age.control", + "control": { + "module-pathname": "$libdir/age", + "requires": [], + "schema": "ag_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "ag_catalog", + "load-sql": [ + "LOAD 'age';" + ], + "post-create-sql": [ + "SET search_path = ag_catalog, \"$user\", public;" + ], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/age.test.ts" + ], + "native-module-file": "age.so", + "notes": [] + }, + { + "id": "amcheck", + "sql-name": "amcheck", + "rust-constant": "AMCHECK", + "display-name": "amcheck", + "source-kind": "postgres-contrib", + "upstream-import-name": "amcheck", + "package-export": "./contrib/amcheck", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 18815, + "control-file": "src/postgres/versions/18/contrib/amcheck/amcheck.control", + "control": { + "module-pathname": "$libdir/amcheck", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/amcheck.test.js" + ], + "native-module-file": "amcheck.so", + "notes": [] + }, + { + "id": "auto_explain", + "sql-name": "auto_explain", + "rust-constant": "AUTO_EXPLAIN", + "display-name": "auto_explain", + "source-kind": "postgres-contrib", + "upstream-import-name": "auto_explain", + "package-export": "./contrib/auto_explain", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3125, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/auto_explain.test.js" + ], + "native-module-file": "auto_explain.so", + "notes": [] + }, + { + "id": "bloom", + "sql-name": "bloom", + "rust-constant": "BLOOM", + "display-name": "bloom", + "source-kind": "postgres-contrib", + "upstream-import-name": "bloom", + "package-export": "./contrib/bloom", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6197, + "control-file": "src/postgres/versions/18/contrib/bloom/bloom.control", + "control": { + "module-pathname": "$libdir/bloom", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/bloom.test.js" + ], + "native-module-file": "bloom.so", + "notes": [] + }, + { + "id": "btree_gin", + "sql-name": "btree_gin", + "rust-constant": "BTREE_GIN", + "display-name": "btree_gin", + "source-kind": "postgres-contrib", + "upstream-import-name": "btree_gin", + "package-export": "./contrib/btree_gin", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6347, + "control-file": "src/postgres/versions/18/contrib/btree_gin/btree_gin.control", + "control": { + "module-pathname": "$libdir/btree_gin", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gin.test.js" + ], + "native-module-file": "btree_gin.so", + "notes": [] + }, + { + "id": "btree_gist", + "sql-name": "btree_gist", + "rust-constant": "BTREE_GIST", + "display-name": "btree_gist", + "source-kind": "postgres-contrib", + "upstream-import-name": "btree_gist", + "package-export": "./contrib/btree_gist", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 24181, + "control-file": "src/postgres/versions/18/contrib/btree_gist/btree_gist.control", + "control": { + "module-pathname": "$libdir/btree_gist", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gist.test.js" + ], + "native-module-file": "btree_gist.so", + "notes": [] + }, + { + "id": "citext", + "sql-name": "citext", + "rust-constant": "CITEXT", + "display-name": "citext", + "source-kind": "postgres-contrib", + "upstream-import-name": "citext", + "package-export": "./contrib/citext", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4983, + "control-file": "src/postgres/versions/18/contrib/citext/citext.control", + "control": { + "module-pathname": "$libdir/citext", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/citext.test.js" + ], + "native-module-file": "citext.so", + "notes": [] + }, + { + "id": "cube", + "sql-name": "cube", + "rust-constant": "CUBE", + "display-name": "cube", + "source-kind": "postgres-contrib", + "upstream-import-name": "cube", + "package-export": "./contrib/cube", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15104, + "control-file": "src/postgres/versions/18/contrib/cube/cube.control", + "control": { + "module-pathname": "$libdir/cube", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/cube.test.js" + ], + "native-module-file": "cube.so", + "notes": [] + }, + { + "id": "dict_int", + "sql-name": "dict_int", + "rust-constant": "DICT_INT", + "display-name": "dict_int", + "source-kind": "postgres-contrib", + "upstream-import-name": "dict_int", + "package-export": "./contrib/dict_int", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1361, + "control-file": "src/postgres/versions/18/contrib/dict_int/dict_int.control", + "control": { + "module-pathname": "$libdir/dict_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_int.test.js" + ], + "native-module-file": "dict_int.so", + "notes": [] + }, + { + "id": "dict_xsyn", + "sql-name": "dict_xsyn", + "rust-constant": "DICT_XSYN", + "display-name": "dict_xsyn", + "source-kind": "postgres-contrib", + "upstream-import-name": "dict_xsyn", + "package-export": "./contrib/dict_xsyn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1948, + "control-file": "src/postgres/versions/18/contrib/dict_xsyn/dict_xsyn.control", + "control": { + "module-pathname": "$libdir/dict_xsyn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_xsyn.test.ts" + ], + "native-module-file": "dict_xsyn.so", + "notes": [] + }, + { + "id": "earthdistance", + "sql-name": "earthdistance", + "rust-constant": "EARTHDISTANCE", + "display-name": "earthdistance", + "source-kind": "postgres-contrib", + "upstream-import-name": "earthdistance", + "package-export": "./contrib/earthdistance", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2220, + "control-file": "src/postgres/versions/18/contrib/earthdistance/earthdistance.control", + "control": { + "module-pathname": "$libdir/earthdistance", + "requires": [ + "cube" + ], + "relocatable": "true" + }, + "dependencies": [ + "cube" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/earthdistance.test.js" + ], + "native-module-file": "earthdistance.so", + "notes": [] + }, + { + "id": "file_fdw", + "sql-name": "file_fdw", + "rust-constant": "FILE_FDW", + "display-name": "file_fdw", + "source-kind": "postgres-contrib", + "upstream-import-name": "file_fdw", + "package-export": "./contrib/file_fdw", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4467, + "control-file": "src/postgres/versions/18/contrib/file_fdw/file_fdw.control", + "control": { + "module-pathname": "$libdir/file_fdw", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/file_fdw.test.js" + ], + "native-module-file": "file_fdw.so", + "notes": [] + }, + { + "id": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "rust-constant": "FUZZYSTRMATCH", + "display-name": "fuzzystrmatch", + "source-kind": "postgres-contrib", + "upstream-import-name": "fuzzystrmatch", + "package-export": "./contrib/fuzzystrmatch", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 12026, + "control-file": "src/postgres/versions/18/contrib/fuzzystrmatch/fuzzystrmatch.control", + "control": { + "module-pathname": "$libdir/fuzzystrmatch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/fuzzystrmatch.test.js" + ], + "native-module-file": "fuzzystrmatch.so", + "notes": [] + }, + { + "id": "hstore", + "sql-name": "hstore", + "rust-constant": "HSTORE", + "display-name": "hstore", + "source-kind": "postgres-contrib", + "upstream-import-name": "hstore", + "package-export": "./contrib/hstore", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 21380, + "control-file": "src/postgres/versions/18/contrib/hstore/hstore.control", + "control": { + "module-pathname": "$libdir/hstore", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/hstore.test.js" + ], + "native-module-file": "hstore.so", + "notes": [] + }, + { + "id": "intarray", + "sql-name": "intarray", + "rust-constant": "INTARRAY", + "display-name": "intarray", + "source-kind": "postgres-contrib", + "upstream-import-name": "intarray", + "package-export": "./contrib/intarray", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 14712, + "control-file": "src/postgres/versions/18/contrib/intarray/intarray.control", + "control": { + "module-pathname": "$libdir/_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/intarray.test.js" + ], + "native-module-file": "_int.so", + "notes": [] + }, + { + "id": "isn", + "sql-name": "isn", + "rust-constant": "ISN", + "display-name": "isn", + "source-kind": "postgres-contrib", + "upstream-import-name": "isn", + "package-export": "./contrib/isn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 31417, + "control-file": "src/postgres/versions/18/contrib/isn/isn.control", + "control": { + "module-pathname": "$libdir/isn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/isn.test.js" + ], + "native-module-file": "isn.so", + "notes": [] + }, + { + "id": "lo", + "sql-name": "lo", + "rust-constant": "LO", + "display-name": "lo", + "source-kind": "postgres-contrib", + "upstream-import-name": "lo", + "package-export": "./contrib/lo", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1822, + "control-file": "src/postgres/versions/18/contrib/lo/lo.control", + "control": { + "module-pathname": "$libdir/lo", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/lo.test.js" + ], + "native-module-file": "lo.so", + "notes": [] + }, + { + "id": "ltree", + "sql-name": "ltree", + "rust-constant": "LTREE", + "display-name": "ltree", + "source-kind": "postgres-contrib", + "upstream-import-name": "ltree", + "package-export": "./contrib/ltree", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 19553, + "control-file": "src/postgres/versions/18/contrib/ltree/ltree.control", + "control": { + "module-pathname": "$libdir/ltree", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/ltree.test.js" + ], + "native-module-file": "ltree.so", + "notes": [] + }, + { + "id": "pageinspect", + "sql-name": "pageinspect", + "rust-constant": "PAGEINSPECT", + "display-name": "pageinspect", + "source-kind": "postgres-contrib", + "upstream-import-name": "pageinspect", + "package-export": "./contrib/pageinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15923, + "control-file": "src/postgres/versions/18/contrib/pageinspect/pageinspect.control", + "control": { + "module-pathname": "$libdir/pageinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pageinspect.test.js" + ], + "native-module-file": "pageinspect.so", + "notes": [] + }, + { + "id": "pg_buffercache", + "sql-name": "pg_buffercache", + "rust-constant": "PG_BUFFERCACHE", + "display-name": "pg_buffercache", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_buffercache", + "package-export": "./contrib/pg_buffercache", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3133, + "control-file": "src/postgres/versions/18/contrib/pg_buffercache/pg_buffercache.control", + "control": { + "module-pathname": "$libdir/pg_buffercache", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_buffercache.test.js" + ], + "native-module-file": "pg_buffercache.so", + "notes": [] + }, + { + "id": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "rust-constant": "PG_FREESPACEMAP", + "display-name": "pg_freespacemap", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_freespacemap", + "package-export": "./contrib/pg_freespacemap", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1485, + "control-file": "src/postgres/versions/18/contrib/pg_freespacemap/pg_freespacemap.control", + "control": { + "module-pathname": "$libdir/pg_freespacemap", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_freespacemap.test.ts" + ], + "native-module-file": "pg_freespacemap.so", + "notes": [] + }, + { + "id": "pg_hashids", + "sql-name": "pg_hashids", + "rust-constant": "PG_HASHIDS", + "display-name": "pg_hashids", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_hashids", + "package-export": "./pg_hashids", + "tags": [ + "postgres extension" + ], + "bundle-size": 4212, + "control-file": "target/oliphaunt-sources/checkouts/pg_hashids/pg_hashids.control", + "control": { + "module-pathname": "$libdir/pg_hashids", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_hashids.test.ts" + ], + "native-module-file": "pg_hashids.so", + "notes": [] + }, + { + "id": "pg_ivm", + "sql-name": "pg_ivm", + "rust-constant": "PG_IVM", + "display-name": "pg_ivm", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_ivm", + "package-export": "./pg_ivm", + "tags": [ + "postgres extension" + ], + "bundle-size": 24865, + "control-file": "target/oliphaunt-sources/checkouts/pg_ivm/pg_ivm.control", + "control": { + "module-pathname": "$libdir/pg_ivm", + "requires": [], + "relocatable": "false", + "schema": "pg_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_ivm.test.ts" + ], + "native-module-file": "pg_ivm.so", + "notes": [] + }, + { + "id": "pg_surgery", + "sql-name": "pg_surgery", + "rust-constant": "PG_SURGERY", + "display-name": "pg_surgery", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_surgery", + "package-export": "./contrib/pg_surgery", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2635, + "control-file": "src/postgres/versions/18/contrib/pg_surgery/pg_surgery.control", + "control": { + "module-pathname": "$libdir/pg_surgery", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_surgery.test.js" + ], + "native-module-file": "pg_surgery.so", + "notes": [] + }, + { + "id": "pg_textsearch", + "sql-name": "pg_textsearch", + "rust-constant": "PG_TEXTSEARCH", + "display-name": "pg_textsearch", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_textsearch", + "package-export": "./pg_textsearch", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 55062, + "control-file": "target/oliphaunt-sources/checkouts/pg_textsearch/pg_textsearch.control", + "control": { + "module-pathname": "$libdir/pg_textsearch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_textsearch.test.ts" + ], + "native-module-file": "pg_textsearch.so", + "notes": [] + }, + { + "id": "pg_trgm", + "sql-name": "pg_trgm", + "rust-constant": "PG_TRGM", + "display-name": "pg_trgm", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_trgm", + "package-export": "./contrib/pg_trgm", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 16208, + "control-file": "src/postgres/versions/18/contrib/pg_trgm/pg_trgm.control", + "control": { + "module-pathname": "$libdir/pg_trgm", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_trgm.test.js" + ], + "native-module-file": "pg_trgm.so", + "notes": [] + }, + { + "id": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "rust-constant": "PG_UUIDV7", + "display-name": "pg_uuidv7", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_uuidv7", + "package-export": "./pg_uuidv7", + "tags": [ + "postgres extension" + ], + "bundle-size": 1522, + "control-file": "target/oliphaunt-sources/checkouts/pg_uuidv7/pg_uuidv7.control", + "control": { + "module-pathname": "$libdir/pg_uuidv7", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_uuidv7.test.ts" + ], + "native-module-file": "pg_uuidv7.so", + "notes": [] + }, + { + "id": "pg_visibility", + "sql-name": "pg_visibility", + "rust-constant": "PG_VISIBILITY", + "display-name": "pg_visibility", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_visibility", + "package-export": "./contrib/pg_visibility", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4159, + "control-file": "src/postgres/versions/18/contrib/pg_visibility/pg_visibility.control", + "control": { + "module-pathname": "$libdir/pg_visibility", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_visibility.test.js" + ], + "native-module-file": "pg_visibility.so", + "notes": [] + }, + { + "id": "pg_walinspect", + "sql-name": "pg_walinspect", + "rust-constant": "PG_WALINSPECT", + "display-name": "pg_walinspect", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_walinspect", + "package-export": "./contrib/pg_walinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4689, + "control-file": "src/postgres/versions/18/contrib/pg_walinspect/pg_walinspect.control", + "control": { + "module-pathname": "$libdir/pg_walinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_walinspect.test.js" + ], + "native-module-file": "pg_walinspect.so", + "notes": [] + }, + { + "id": "pgcrypto", + "sql-name": "pgcrypto", + "rust-constant": "PGCRYPTO", + "display-name": "pgcrypto", + "source-kind": "postgres-contrib", + "upstream-import-name": "pgcrypto", + "package-export": "./contrib/pgcrypto", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1148162, + "control-file": "src/postgres/versions/18/contrib/pgcrypto/pgcrypto.control", + "control": { + "module-pathname": "$libdir/pgcrypto", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "load-order": [], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pgcrypto.test.js" + ], + "native-module-file": "pgcrypto.so", + "notes": [] + }, + { + "id": "pgtap", + "sql-name": "pgtap", + "rust-constant": "PGTAP", + "display-name": "pgtap", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pgtap", + "package-export": "./pgtap", + "tags": [ + "postgres extension" + ], + "bundle-size": 239428, + "control-file": "target/oliphaunt-sources/checkouts/pgtap/pgtap.control", + "control": { + "requires": [ + "plpgsql" + ], + "relocatable": "true" + }, + "dependencies": [ + "plpgsql" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgtap.test.ts" + ], + "notes": [] + }, + { + "id": "postgis", + "sql-name": "postgis", + "rust-constant": "POSTGIS", + "display-name": "PostGIS", + "source-kind": "postgis", + "upstream-import-name": "postgis", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 8551161, + "control-file": "target/oliphaunt-sources/checkouts/postgis/extensions/postgis/postgis.control.in", + "control": { + "module-pathname": "@MODULEPATH@", + "requires": [], + "relocatable": "false" + }, + "dependencies": [], + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "load-order": [ + "lib/postgresql/postgis-3.so" + ], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/postgis/postgis.test.ts" + ], + "native-module-file": "postgis-3.so", + "notes": [] + }, + { + "id": "seg", + "sql-name": "seg", + "rust-constant": "SEG", + "display-name": "seg", + "source-kind": "postgres-contrib", + "upstream-import-name": "seg", + "package-export": "./contrib/seg", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 10426, + "control-file": "src/postgres/versions/18/contrib/seg/seg.control", + "control": { + "module-pathname": "$libdir/seg", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/seg.test.js" + ], + "native-module-file": "seg.so", + "notes": [] + }, + { + "id": "tablefunc", + "sql-name": "tablefunc", + "rust-constant": "TABLEFUNC", + "display-name": "tablefunc", + "source-kind": "postgres-contrib", + "upstream-import-name": "tablefunc", + "package-export": "./contrib/tablefunc", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 5824, + "control-file": "src/postgres/versions/18/contrib/tablefunc/tablefunc.control", + "control": { + "module-pathname": "$libdir/tablefunc", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tablefunc.test.js" + ], + "native-module-file": "tablefunc.so", + "notes": [] + }, + { + "id": "tcn", + "sql-name": "tcn", + "rust-constant": "TCN", + "display-name": "tcn", + "source-kind": "postgres-contrib", + "upstream-import-name": "tcn", + "package-export": "./contrib/tcn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1914, + "control-file": "src/postgres/versions/18/contrib/tcn/tcn.control", + "control": { + "module-pathname": "$libdir/tcn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tcn.test.js" + ], + "native-module-file": "tcn.so", + "notes": [] + }, + { + "id": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "rust-constant": "TSM_SYSTEM_ROWS", + "display-name": "tsm_system_rows", + "source-kind": "postgres-contrib", + "upstream-import-name": "tsm_system_rows", + "package-export": "./contrib/tsm_system_rows", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2048, + "control-file": "src/postgres/versions/18/contrib/tsm_system_rows/tsm_system_rows.control", + "control": { + "module-pathname": "$libdir/tsm_system_rows", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_rows.test.js" + ], + "native-module-file": "tsm_system_rows.so", + "notes": [] + }, + { + "id": "tsm_system_time", + "sql-name": "tsm_system_time", + "rust-constant": "TSM_SYSTEM_TIME", + "display-name": "tsm_system_time", + "source-kind": "postgres-contrib", + "upstream-import-name": "tsm_system_time", + "package-export": "./contrib/tsm_system_time", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2099, + "control-file": "src/postgres/versions/18/contrib/tsm_system_time/tsm_system_time.control", + "control": { + "module-pathname": "$libdir/tsm_system_time", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_time.test.js" + ], + "native-module-file": "tsm_system_time.so", + "notes": [] + }, + { + "id": "unaccent", + "sql-name": "unaccent", + "rust-constant": "UNACCENT", + "display-name": "unaccent", + "source-kind": "postgres-contrib", + "upstream-import-name": "unaccent", + "package-export": "./contrib/unaccent", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 9323, + "control-file": "src/postgres/versions/18/contrib/unaccent/unaccent.control", + "control": { + "module-pathname": "$libdir/unaccent", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/unaccent.test.js" + ], + "native-module-file": "unaccent.so", + "notes": [] + }, + { + "id": "uuid_ossp", + "sql-name": "uuid-ossp", + "rust-constant": "UUID_OSSP", + "display-name": "uuid-ossp", + "source-kind": "postgres-contrib", + "upstream-import-name": "uuid_ossp", + "package-export": "./contrib/uuid_ossp", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 17936, + "control-file": "src/postgres/versions/18/contrib/uuid-ossp/uuid-ossp.control", + "control": { + "module-pathname": "$libdir/uuid-ossp", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/uuid_ossp.test.js" + ], + "native-module-file": "uuid-ossp.so", + "notes": [] + }, + { + "id": "vector", + "sql-name": "vector", + "rust-constant": "VECTOR", + "display-name": "pgvector", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "vector", + "package-export": "./vector", + "tags": [ + "postgres extension" + ], + "bundle-size": 43953, + "control-file": "target/oliphaunt-sources/checkouts/pgvector/vector.control", + "control": { + "module-pathname": "$libdir/vector", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgvector.test.ts" + ], + "native-module-file": "vector.so", + "notes": [] + } + ] +} diff --git a/src/extensions/contrib/.release-semantic-inputs.json b/src/extensions/contrib/.release-semantic-inputs.json new file mode 100644 index 00000000..78039ac1 --- /dev/null +++ b/src/extensions/contrib/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-contrib-pg18", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "9f2210a59fabc1a84964219c3fc16e3f2986f197257271541cebf93d4cbdec99" +} diff --git a/src/extensions/contrib/CHANGELOG.md b/src/extensions/contrib/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/contrib/VERSION b/src/extensions/contrib/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/contrib/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/contrib/amcheck/moon.yml b/src/extensions/contrib/amcheck/moon.yml new file mode 100644 index 00000000..72646774 --- /dev/null +++ b/src/extensions/contrib/amcheck/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-amcheck" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/amcheck" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/amcheck/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/amcheck/targets/artifacts.toml b/src/extensions/contrib/amcheck/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/amcheck/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/auto_explain/moon.yml b/src/extensions/contrib/auto_explain/moon.yml new file mode 100644 index 00000000..239a5f2d --- /dev/null +++ b/src/extensions/contrib/auto_explain/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-auto-explain" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/auto_explain" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/auto_explain/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/auto_explain/targets/artifacts.toml b/src/extensions/contrib/auto_explain/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/auto_explain/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/bloom/moon.yml b/src/extensions/contrib/bloom/moon.yml new file mode 100644 index 00000000..2632362d --- /dev/null +++ b/src/extensions/contrib/bloom/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-bloom" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/bloom" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/bloom/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/bloom/targets/artifacts.toml b/src/extensions/contrib/bloom/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/bloom/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/btree_gin/moon.yml b/src/extensions/contrib/btree_gin/moon.yml new file mode 100644 index 00000000..4db9a577 --- /dev/null +++ b/src/extensions/contrib/btree_gin/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-btree-gin" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/btree_gin" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/btree_gin/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/btree_gin/targets/artifacts.toml b/src/extensions/contrib/btree_gin/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/btree_gin/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/btree_gist/moon.yml b/src/extensions/contrib/btree_gist/moon.yml new file mode 100644 index 00000000..2c052eb2 --- /dev/null +++ b/src/extensions/contrib/btree_gist/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-btree-gist" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/btree_gist" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/btree_gist/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/btree_gist/targets/artifacts.toml b/src/extensions/contrib/btree_gist/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/btree_gist/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/citext/moon.yml b/src/extensions/contrib/citext/moon.yml new file mode 100644 index 00000000..61454cfb --- /dev/null +++ b/src/extensions/contrib/citext/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-citext" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/citext" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/citext/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/citext/targets/artifacts.toml b/src/extensions/contrib/citext/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/citext/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/cube/moon.yml b/src/extensions/contrib/cube/moon.yml new file mode 100644 index 00000000..48ead474 --- /dev/null +++ b/src/extensions/contrib/cube/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-cube" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/cube" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/cube/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/cube/targets/artifacts.toml b/src/extensions/contrib/cube/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/cube/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/dict_int/moon.yml b/src/extensions/contrib/dict_int/moon.yml new file mode 100644 index 00000000..323949c1 --- /dev/null +++ b/src/extensions/contrib/dict_int/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-dict-int" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/dict_int" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/dict_int/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/dict_int/targets/artifacts.toml b/src/extensions/contrib/dict_int/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/dict_int/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/dict_xsyn/moon.yml b/src/extensions/contrib/dict_xsyn/moon.yml new file mode 100644 index 00000000..bbdd674c --- /dev/null +++ b/src/extensions/contrib/dict_xsyn/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-dict-xsyn" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/dict_xsyn" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/dict_xsyn/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/dict_xsyn/targets/artifacts.toml b/src/extensions/contrib/dict_xsyn/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/dict_xsyn/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/earthdistance/moon.yml b/src/extensions/contrib/earthdistance/moon.yml new file mode 100644 index 00000000..84e127f5 --- /dev/null +++ b/src/extensions/contrib/earthdistance/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-earthdistance" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/earthdistance" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/earthdistance/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/earthdistance/targets/artifacts.toml b/src/extensions/contrib/earthdistance/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/earthdistance/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/file_fdw/moon.yml b/src/extensions/contrib/file_fdw/moon.yml new file mode 100644 index 00000000..8b884eb3 --- /dev/null +++ b/src/extensions/contrib/file_fdw/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-file-fdw" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/file_fdw" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/file_fdw/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/file_fdw/targets/artifacts.toml b/src/extensions/contrib/file_fdw/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/file_fdw/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/fuzzystrmatch/moon.yml b/src/extensions/contrib/fuzzystrmatch/moon.yml new file mode 100644 index 00000000..13e97bda --- /dev/null +++ b/src/extensions/contrib/fuzzystrmatch/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-fuzzystrmatch" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/fuzzystrmatch" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/fuzzystrmatch/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/fuzzystrmatch/targets/artifacts.toml b/src/extensions/contrib/fuzzystrmatch/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/fuzzystrmatch/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/hstore/moon.yml b/src/extensions/contrib/hstore/moon.yml new file mode 100644 index 00000000..cdd5c30a --- /dev/null +++ b/src/extensions/contrib/hstore/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-hstore" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/hstore" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/hstore/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/hstore/targets/artifacts.toml b/src/extensions/contrib/hstore/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/hstore/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/intarray/moon.yml b/src/extensions/contrib/intarray/moon.yml new file mode 100644 index 00000000..330ab650 --- /dev/null +++ b/src/extensions/contrib/intarray/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-intarray" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/intarray" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/intarray/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/intarray/targets/artifacts.toml b/src/extensions/contrib/intarray/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/intarray/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/isn/moon.yml b/src/extensions/contrib/isn/moon.yml new file mode 100644 index 00000000..e9cce8a8 --- /dev/null +++ b/src/extensions/contrib/isn/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-isn" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/isn" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/isn/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/isn/targets/artifacts.toml b/src/extensions/contrib/isn/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/isn/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/lo/moon.yml b/src/extensions/contrib/lo/moon.yml new file mode 100644 index 00000000..40c4c8f2 --- /dev/null +++ b/src/extensions/contrib/lo/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-lo" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/lo" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/lo/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/lo/targets/artifacts.toml b/src/extensions/contrib/lo/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/lo/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/ltree/moon.yml b/src/extensions/contrib/ltree/moon.yml new file mode 100644 index 00000000..5aaf9588 --- /dev/null +++ b/src/extensions/contrib/ltree/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-ltree" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/ltree" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/ltree/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/ltree/targets/artifacts.toml b/src/extensions/contrib/ltree/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/ltree/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/moon.yml b/src/extensions/contrib/moon.yml new file mode 100644 index 00000000..d1013673 --- /dev/null +++ b/src/extensions/contrib/moon.yml @@ -0,0 +1,58 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-contrib-pg18" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "pg18", "release-product"] +dependsOn: + - "postgres18" + - "extension-runtime-contract" + - "liboliphaunt-native" + - "liboliphaunt-wasix" + +project: + title: "PostgreSQL 18 Contrib Extensions" + description: "PostgreSQL contrib extension definitions tied to the active PostgreSQL 18.4 source baseline." + owner: "oliphaunt" + release: + component: "oliphaunt-extension-contrib-pg18" + packagePath: "src/extensions/contrib" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/postgres/versions/18/**/*" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-contrib-pg18 --require-native --require-wasix" + deps: + - "oliphaunt-extension-contrib-pg18:check" + inputs: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/contrib/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-contrib-pg18/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pageinspect/moon.yml b/src/extensions/contrib/pageinspect/moon.yml new file mode 100644 index 00000000..d4184e2d --- /dev/null +++ b/src/extensions/contrib/pageinspect/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pageinspect" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pageinspect" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pageinspect/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pageinspect/targets/artifacts.toml b/src/extensions/contrib/pageinspect/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pageinspect/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_buffercache/moon.yml b/src/extensions/contrib/pg_buffercache/moon.yml new file mode 100644 index 00000000..8ae1d571 --- /dev/null +++ b/src/extensions/contrib/pg_buffercache/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-buffercache" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_buffercache" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_buffercache/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_buffercache/targets/artifacts.toml b/src/extensions/contrib/pg_buffercache/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_buffercache/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_freespacemap/moon.yml b/src/extensions/contrib/pg_freespacemap/moon.yml new file mode 100644 index 00000000..797f3251 --- /dev/null +++ b/src/extensions/contrib/pg_freespacemap/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-freespacemap" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_freespacemap" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_freespacemap/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_freespacemap/targets/artifacts.toml b/src/extensions/contrib/pg_freespacemap/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_freespacemap/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_surgery/moon.yml b/src/extensions/contrib/pg_surgery/moon.yml new file mode 100644 index 00000000..62be90a3 --- /dev/null +++ b/src/extensions/contrib/pg_surgery/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-surgery" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_surgery" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_surgery/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_surgery/targets/artifacts.toml b/src/extensions/contrib/pg_surgery/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_surgery/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_trgm/moon.yml b/src/extensions/contrib/pg_trgm/moon.yml new file mode 100644 index 00000000..8a7a5bba --- /dev/null +++ b/src/extensions/contrib/pg_trgm/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-trgm" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_trgm" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_trgm/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_trgm/targets/artifacts.toml b/src/extensions/contrib/pg_trgm/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_trgm/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_visibility/moon.yml b/src/extensions/contrib/pg_visibility/moon.yml new file mode 100644 index 00000000..07516c9a --- /dev/null +++ b/src/extensions/contrib/pg_visibility/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-visibility" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_visibility" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_visibility/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_visibility/targets/artifacts.toml b/src/extensions/contrib/pg_visibility/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_visibility/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pg_walinspect/moon.yml b/src/extensions/contrib/pg_walinspect/moon.yml new file mode 100644 index 00000000..c1ca11c9 --- /dev/null +++ b/src/extensions/contrib/pg_walinspect/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-walinspect" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_walinspect" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pg_walinspect/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pg_walinspect/targets/artifacts.toml b/src/extensions/contrib/pg_walinspect/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pg_walinspect/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/pgcrypto/moon.yml b/src/extensions/contrib/pgcrypto/moon.yml new file mode 100644 index 00000000..b8e47469 --- /dev/null +++ b/src/extensions/contrib/pgcrypto/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pgcrypto" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pgcrypto" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/pgcrypto/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/pgcrypto/targets/artifacts.toml b/src/extensions/contrib/pgcrypto/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/pgcrypto/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/postgres18.toml b/src/extensions/contrib/postgres18.toml new file mode 100644 index 00000000..40e67436 --- /dev/null +++ b/src/extensions/contrib/postgres18.toml @@ -0,0 +1,236 @@ +format-version = 1 +postgres-version = "18.4" +source-kind = "postgres-contrib" +source-root = "src/postgres/versions/18/contrib" + +[[extensions]] +id = "amcheck" +sql-name = "amcheck" +contrib-dir = "amcheck" +module-file = "amcheck.so" +default-version = "1.5" + +[[extensions]] +id = "auto_explain" +sql-name = "auto_explain" +contrib-dir = "auto_explain" +module-file = "auto_explain.so" + +[[extensions]] +id = "bloom" +sql-name = "bloom" +contrib-dir = "bloom" +module-file = "bloom.so" +default-version = "1.0" + +[[extensions]] +id = "btree_gin" +sql-name = "btree_gin" +contrib-dir = "btree_gin" +module-file = "btree_gin.so" +default-version = "1.3" + +[[extensions]] +id = "btree_gist" +sql-name = "btree_gist" +contrib-dir = "btree_gist" +module-file = "btree_gist.so" +default-version = "1.8" + +[[extensions]] +id = "citext" +sql-name = "citext" +contrib-dir = "citext" +module-file = "citext.so" +default-version = "1.8" + +[[extensions]] +id = "cube" +sql-name = "cube" +contrib-dir = "cube" +module-file = "cube.so" +default-version = "1.5" + +[[extensions]] +id = "dict_int" +sql-name = "dict_int" +contrib-dir = "dict_int" +module-file = "dict_int.so" +default-version = "1.0" + +[[extensions]] +id = "dict_xsyn" +sql-name = "dict_xsyn" +contrib-dir = "dict_xsyn" +module-file = "dict_xsyn.so" +default-version = "1.0" +data-files = ["share/postgresql/tsearch_data/xsyn_sample.rules"] + +[[extensions]] +id = "earthdistance" +sql-name = "earthdistance" +contrib-dir = "earthdistance" +module-file = "earthdistance.so" +default-version = "1.2" + +[[extensions]] +id = "file_fdw" +sql-name = "file_fdw" +contrib-dir = "file_fdw" +module-file = "file_fdw.so" +default-version = "1.0" + +[[extensions]] +id = "fuzzystrmatch" +sql-name = "fuzzystrmatch" +contrib-dir = "fuzzystrmatch" +module-file = "fuzzystrmatch.so" +default-version = "1.2" + +[[extensions]] +id = "hstore" +sql-name = "hstore" +contrib-dir = "hstore" +module-file = "hstore.so" +default-version = "1.8" + +[[extensions]] +id = "intarray" +sql-name = "intarray" +contrib-dir = "intarray" +module-file = "_int.so" +default-version = "1.5" + +[[extensions]] +id = "isn" +sql-name = "isn" +contrib-dir = "isn" +module-file = "isn.so" +default-version = "1.3" + +[[extensions]] +id = "lo" +sql-name = "lo" +contrib-dir = "lo" +module-file = "lo.so" +default-version = "1.2" + +[[extensions]] +id = "ltree" +sql-name = "ltree" +contrib-dir = "ltree" +module-file = "ltree.so" +default-version = "1.3" + +[[extensions]] +id = "pageinspect" +sql-name = "pageinspect" +contrib-dir = "pageinspect" +module-file = "pageinspect.so" +default-version = "1.13" + +[[extensions]] +id = "pg_buffercache" +sql-name = "pg_buffercache" +contrib-dir = "pg_buffercache" +module-file = "pg_buffercache.so" +default-version = "1.6" + +[[extensions]] +id = "pg_freespacemap" +sql-name = "pg_freespacemap" +contrib-dir = "pg_freespacemap" +module-file = "pg_freespacemap.so" +default-version = "1.3" + +[[extensions]] +id = "pg_surgery" +sql-name = "pg_surgery" +contrib-dir = "pg_surgery" +module-file = "pg_surgery.so" +default-version = "1.0" + +[[extensions]] +id = "pg_trgm" +sql-name = "pg_trgm" +contrib-dir = "pg_trgm" +module-file = "pg_trgm.so" +default-version = "1.6" + +[[extensions]] +id = "pg_visibility" +sql-name = "pg_visibility" +contrib-dir = "pg_visibility" +module-file = "pg_visibility.so" +default-version = "1.2" + +[[extensions]] +id = "pg_walinspect" +sql-name = "pg_walinspect" +contrib-dir = "pg_walinspect" +module-file = "pg_walinspect.so" +default-version = "1.1" + +[[extensions]] +id = "pgcrypto" +sql-name = "pgcrypto" +contrib-dir = "pgcrypto" +module-file = "pgcrypto.so" +default-version = "1.4" +mobile-static-dependencies = ["openssl"] +mobile-static-include-dependencies = ["openssl"] +mobile-static-hash-source-dependencies = ["openssl"] + +[[extensions]] +id = "seg" +sql-name = "seg" +contrib-dir = "seg" +module-file = "seg.so" +default-version = "1.4" + +[[extensions]] +id = "tablefunc" +sql-name = "tablefunc" +contrib-dir = "tablefunc" +module-file = "tablefunc.so" +default-version = "1.0" + +[[extensions]] +id = "tcn" +sql-name = "tcn" +contrib-dir = "tcn" +module-file = "tcn.so" +default-version = "1.0" + +[[extensions]] +id = "tsm_system_rows" +sql-name = "tsm_system_rows" +contrib-dir = "tsm_system_rows" +module-file = "tsm_system_rows.so" +default-version = "1.0" + +[[extensions]] +id = "tsm_system_time" +sql-name = "tsm_system_time" +contrib-dir = "tsm_system_time" +module-file = "tsm_system_time.so" +default-version = "1.0" + +[[extensions]] +id = "unaccent" +sql-name = "unaccent" +contrib-dir = "unaccent" +module-file = "unaccent.so" +default-version = "1.1" +data-files = ["share/postgresql/tsearch_data/unaccent.rules"] + +[[extensions]] +id = "uuid_ossp" +sql-name = "uuid-ossp" +contrib-dir = "uuid-ossp" +module-file = "uuid-ossp.so" +default-version = "1.1" +mobile-static-dependencies = ["uuid"] +mobile-static-include-dirs = ["src/runtimes/liboliphaunt/native/portable-uuid/include"] +mobile-static-cflags = ["-DHAVE_UUID_E2FS=1", "-DHAVE_UUID_UUID_H=1"] +mobile-static-hash-dirs = ["src/runtimes/liboliphaunt/native/portable-uuid"] diff --git a/src/extensions/contrib/release.toml b/src/extensions/contrib/release.toml new file mode 100644 index 00000000..17e2b2f4 --- /dev/null +++ b/src/extensions/contrib/release.toml @@ -0,0 +1,89 @@ +id = "oliphaunt-extension-contrib-pg18" +owner = "@oliphaunt/extensions" +kind = "exact-extension-bundle" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-contrib-pg18", + "crates:oliphaunt-extension-contrib-pg18-aot-linux-arm64", + "crates:oliphaunt-extension-contrib-pg18-aot-linux-x64", + "crates:oliphaunt-extension-contrib-pg18-aot-macos-arm64", + "crates:oliphaunt-extension-contrib-pg18-aot-windows-x64", + "crates:oliphaunt-extension-contrib-pg18-linux-arm64-gnu", + "crates:oliphaunt-extension-contrib-pg18-linux-x64-gnu", + "crates:oliphaunt-extension-contrib-pg18-macos-arm64", + "crates:oliphaunt-extension-contrib-pg18-wasix", + "crates:oliphaunt-extension-contrib-pg18-windows-x64-msvc", + "npm:@oliphaunt/extension-contrib-pg18", + "npm:@oliphaunt/extension-contrib-pg18-linux-arm64-gnu", + "npm:@oliphaunt/extension-contrib-pg18-linux-x64-gnu", + "npm:@oliphaunt/extension-contrib-pg18-macos-arm64", + "npm:@oliphaunt/extension-contrib-pg18-windows-x64-msvc", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-contrib-pg18-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-contrib-pg18-android-x86_64", +] +release_artifacts = ["exact-extension-artifacts"] +extension_sql_names = [ + "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_surgery", + "pg_trgm", + "pg_visibility", + "pg_walinspect", + "pgcrypto", + "seg", + "tablefunc", + "tcn", + "tsm_system_rows", + "tsm_system_time", + "unaccent", + "uuid-ossp", +] + +[extension] +class = "contrib" +versioning = "runtime-bound" +member_manifest = "src/extensions/contrib/postgres18.toml" + +[extension.source] +path = "src/postgres/versions/18/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" + +[compatibility_versions.oliphaunt-extension-contrib-pg18-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/contrib/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-contrib-pg18-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/contrib/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/contrib/seg/moon.yml b/src/extensions/contrib/seg/moon.yml new file mode 100644 index 00000000..128f82d6 --- /dev/null +++ b/src/extensions/contrib/seg/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-seg" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/seg" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/seg/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/seg/targets/artifacts.toml b/src/extensions/contrib/seg/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/seg/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/tablefunc/moon.yml b/src/extensions/contrib/tablefunc/moon.yml new file mode 100644 index 00000000..68614b46 --- /dev/null +++ b/src/extensions/contrib/tablefunc/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-tablefunc" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tablefunc" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/tablefunc/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/tablefunc/targets/artifacts.toml b/src/extensions/contrib/tablefunc/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/tablefunc/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/tcn/moon.yml b/src/extensions/contrib/tcn/moon.yml new file mode 100644 index 00000000..98e87f72 --- /dev/null +++ b/src/extensions/contrib/tcn/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-tcn" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tcn" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/tcn/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/tcn/targets/artifacts.toml b/src/extensions/contrib/tcn/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/tcn/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/tsm_system_rows/moon.yml b/src/extensions/contrib/tsm_system_rows/moon.yml new file mode 100644 index 00000000..98a3dc64 --- /dev/null +++ b/src/extensions/contrib/tsm_system_rows/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-tsm-system-rows" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tsm_system_rows" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/tsm_system_rows/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/tsm_system_rows/targets/artifacts.toml b/src/extensions/contrib/tsm_system_rows/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/tsm_system_rows/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/tsm_system_time/moon.yml b/src/extensions/contrib/tsm_system_time/moon.yml new file mode 100644 index 00000000..d4ea54e5 --- /dev/null +++ b/src/extensions/contrib/tsm_system_time/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-tsm-system-time" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tsm_system_time" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/tsm_system_time/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/tsm_system_time/targets/artifacts.toml b/src/extensions/contrib/tsm_system_time/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/tsm_system_time/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/unaccent/moon.yml b/src/extensions/contrib/unaccent/moon.yml new file mode 100644 index 00000000..5eed09c7 --- /dev/null +++ b/src/extensions/contrib/unaccent/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-unaccent" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/unaccent" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/unaccent/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/unaccent/targets/artifacts.toml b/src/extensions/contrib/unaccent/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/unaccent/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/contrib/uuid_ossp/moon.yml b/src/extensions/contrib/uuid_ossp/moon.yml new file mode 100644 index 00000000..f9636371 --- /dev/null +++ b/src/extensions/contrib/uuid_ossp/moon.yml @@ -0,0 +1,29 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-uuid-ossp" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "contrib", "postgres18"] +dependsOn: + - id: "extension-runtime-contract" + scope: "production" + - id: "liboliphaunt-native" + scope: "production" + - id: "liboliphaunt-wasix" + scope: "production" + +tasks: + check: + tags: ["quality", "static"] + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/uuid_ossp" + deps: + - "extension-runtime-contract:check" + inputs: + - "/src/extensions/contrib/uuid_ossp/**/*" + - "/src/extensions/contrib/postgres18.toml" + - "/src/extensions/tools/check-extension-tree.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/contrib/uuid_ossp/targets/artifacts.toml b/src/extensions/contrib/uuid_ossp/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/contrib/uuid_ossp/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/evidence/matrix.toml b/src/extensions/evidence/matrix.toml new file mode 100644 index 00000000..dfdcaf5b --- /dev/null +++ b/src/extensions/evidence/matrix.toml @@ -0,0 +1,411 @@ +format-version = 1 +source-digest-inputs = [ + "src/postgres/versions/18/source.toml", + "src/extensions/catalog/extensions.promoted.toml", + "src/extensions/catalog/extensions.smoke.toml", + "src/extensions/catalog/extensions.source.json", + "src/extensions/contrib/postgres18.toml", + "src/extensions/generated/extensions.catalog.json", + "src/extensions/generated/extensions.build-plan.json", + "src/extensions/generated/contrib-build.tsv", + "src/extensions/generated/pgxs-build.tsv", + "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "src/extensions/external/age/source.toml", + "src/extensions/external/pg_hashids/source.toml", + "src/extensions/external/pg_ivm/source.toml", + "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_uuidv7/source.toml", + "src/extensions/external/pgtap/source.toml", + "src/extensions/external/postgis/dependencies/geos/source.toml", + "src/extensions/external/postgis/dependencies/json-c/source.toml", + "src/extensions/external/postgis/dependencies/libiconv/source.toml", + "src/extensions/external/postgis/dependencies/libxml2/source.toml", + "src/extensions/external/postgis/dependencies/proj/source.toml", + "src/extensions/external/postgis/dependencies/sqlite/source.toml", + "src/extensions/external/postgis/source.toml", + "src/extensions/external/vector/source.toml", + "src/sources/third-party/shared/icu.toml", + "src/sources/third-party/shared/openssl.toml", + "src/extensions/external/README.md", + "src/extensions/external/age/moon.yml", + "src/extensions/external/pg_hashids/moon.yml", + "src/extensions/external/pg_ivm/moon.yml", + "src/extensions/external/pg_ivm/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/moon.yml", + "src/extensions/external/pg_textsearch/recipe.toml", + "src/extensions/external/pg_textsearch/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/tests/smoke.sql", + "src/extensions/external/pg_textsearch/tests/upstream.toml", + "src/extensions/external/pg_uuidv7/moon.yml", + "src/extensions/external/pgtap/moon.yml", + "src/extensions/external/pgtap/recipe.toml", + "src/extensions/external/pgtap/targets/native-static-registry.toml", + "src/extensions/external/pgtap/targets/native.toml", + "src/extensions/external/pgtap/targets/wasix.toml", + "src/extensions/external/pgtap/tests/smoke.sql", + "src/extensions/external/pgtap/tests/upstream.toml", + "src/extensions/external/postgis/blockers.toml", + "src/extensions/external/postgis/deps.toml", + "src/extensions/external/postgis/moon.yml", + "src/extensions/external/postgis/patches/README.md", + "src/extensions/external/postgis/recipe.toml", + "src/extensions/external/postgis/targets/native-static-registry.toml", + "src/extensions/external/postgis/targets/native.toml", + "src/extensions/external/postgis/targets/wasix.toml", + "src/extensions/external/postgis/tests/regression.sql", + "src/extensions/external/postgis/tests/smoke.sql", + "src/extensions/external/postgis/tests/upstream.toml", + "src/extensions/external/postgis/tools/build_wasix.sh", + "src/extensions/external/vector/moon.yml", +] + +[[claims]] +extension = "amcheck" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "auto_explain" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "bloom" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "btree_gin" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "btree_gist" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "citext" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "cube" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "dict_int" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "dict_xsyn" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "earthdistance" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "file_fdw" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "fuzzystrmatch" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "hstore" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "intarray" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "isn" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "lo" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "ltree" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pageinspect" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_buffercache" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_freespacemap" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_hashids" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_ivm" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_surgery" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_textsearch" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_trgm" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_uuidv7" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_visibility" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pg_walinspect" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pgcrypto" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "pgtap" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "postgis" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "seg" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "tablefunc" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "tcn" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "tsm_system_rows" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "tsm_system_time" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "unaccent" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "uuid_ossp" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = true + +[[claims]] +extension = "vector" +postgres-major = 18 +artifact-family = "wasix-runtime" +platform-targets = ["portable"] +runtime-modes = ["direct", "server", "restart", "dump-restore"] +evidence-required = ["wasix-full-lifecycle-v1"] +public = 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 new file mode 100644 index 00000000..ebdf8040 --- /dev/null +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -0,0 +1,577 @@ +{ + "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.", + "observedAt": "2026-06-07T00:00:00Z", + "results": [ + { + "artifactFamily": "wasix-runtime", + "extension": "amcheck", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "amcheck" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "auto_explain", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "auto_explain" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "bloom", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "bloom" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "btree_gin", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "btree_gin" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "btree_gist", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "btree_gist" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "citext", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "citext" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "cube", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "cube" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "dict_int", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "dict_int" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "dict_xsyn", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "dict_xsyn" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "earthdistance", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "earthdistance" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "file_fdw", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "file_fdw" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "fuzzystrmatch", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "fuzzystrmatch" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "hstore", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "hstore" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "intarray", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "intarray" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "isn", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "isn" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "lo", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "lo" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "ltree", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "ltree" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pageinspect", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pageinspect" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_buffercache", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_buffercache" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_freespacemap", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_freespacemap" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_hashids", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_hashids" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_ivm", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_ivm" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_surgery", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_surgery" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_textsearch", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_textsearch" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_trgm", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_trgm" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_uuidv7", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_uuidv7" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_visibility", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_visibility" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pg_walinspect", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pg_walinspect" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pgcrypto", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pgcrypto" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "pgtap", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "pgtap" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "postgis", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "postgis" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "seg", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "seg" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "tablefunc", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "tablefunc" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "tcn", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "tcn" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "tsm_system_rows", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "tsm_system_rows" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "tsm_system_time", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "tsm_system_time" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "unaccent", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "unaccent" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "uuid_ossp", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "uuid-ossp" + }, + { + "artifactFamily": "wasix-runtime", + "extension": "vector", + "platformTarget": "portable", + "postgresMajor": 18, + "runtimeModeStatuses": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sqlName": "vector" + } + ], + "schema": "oliphaunt-extension-evidence-v1", + "sourceDigest": "sha256:de166adedb39eb3a867e42290903ea34c5a08810df9bc2e7e225f8c17cab5dfe", + "sourceDigestInputs": [ + "src/postgres/versions/18/source.toml", + "src/extensions/catalog/extensions.promoted.toml", + "src/extensions/catalog/extensions.smoke.toml", + "src/extensions/contrib/postgres18.toml", + "src/extensions/generated/extensions.catalog.json", + "src/extensions/generated/extensions.build-plan.json", + "src/extensions/generated/contrib-build.tsv", + "src/extensions/generated/pgxs-build.tsv", + "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "src/extensions/external/age/source.toml", + "src/extensions/external/pg_hashids/source.toml", + "src/extensions/external/pg_ivm/source.toml", + "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_uuidv7/source.toml", + "src/extensions/external/pgtap/source.toml", + "src/extensions/external/postgis/dependencies/geos/source.toml", + "src/extensions/external/postgis/dependencies/json-c/source.toml", + "src/extensions/external/postgis/dependencies/libiconv/source.toml", + "src/extensions/external/postgis/dependencies/libxml2/source.toml", + "src/extensions/external/postgis/dependencies/proj/source.toml", + "src/extensions/external/postgis/dependencies/sqlite/source.toml", + "src/extensions/external/postgis/source.toml", + "src/extensions/external/vector/source.toml", + "src/sources/third-party/shared/icu.toml", + "src/sources/third-party/shared/openssl.toml", + "src/extensions/external/README.md", + "src/extensions/external/age/moon.yml", + "src/extensions/external/pg_hashids/moon.yml", + "src/extensions/external/pg_ivm/moon.yml", + "src/extensions/external/pg_ivm/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/moon.yml", + "src/extensions/external/pg_textsearch/recipe.toml", + "src/extensions/external/pg_textsearch/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/tests/smoke.sql", + "src/extensions/external/pg_textsearch/tests/upstream.toml", + "src/extensions/external/pg_uuidv7/moon.yml", + "src/extensions/external/pgtap/moon.yml", + "src/extensions/external/pgtap/recipe.toml", + "src/extensions/external/pgtap/targets/native-static-registry.toml", + "src/extensions/external/pgtap/targets/native.toml", + "src/extensions/external/pgtap/targets/wasix.toml", + "src/extensions/external/pgtap/tests/smoke.sql", + "src/extensions/external/pgtap/tests/upstream.toml", + "src/extensions/external/postgis/blockers.toml", + "src/extensions/external/postgis/deps.toml", + "src/extensions/external/postgis/moon.yml", + "src/extensions/external/postgis/patches/README.md", + "src/extensions/external/postgis/recipe.toml", + "src/extensions/external/postgis/targets/native-static-registry.toml", + "src/extensions/external/postgis/targets/native.toml", + "src/extensions/external/postgis/targets/wasix.toml", + "src/extensions/external/postgis/tests/regression.sql", + "src/extensions/external/postgis/tests/smoke.sql", + "src/extensions/external/postgis/tests/upstream.toml", + "src/extensions/external/postgis/tools/build_wasix.sh", + "src/extensions/external/vector/moon.yml" + ], + "status": "passed" +} diff --git a/src/extensions/evidence/schemas/matrix.schema.json b/src/extensions/evidence/schemas/matrix.schema.json new file mode 100644 index 00000000..c0c2e943 --- /dev/null +++ b/src/extensions/evidence/schemas/matrix.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://oliphaunt.dev/schemas/extensions/evidence-matrix.schema.json", + "title": "Oliphaunt Extension Evidence Matrix", + "type": "object", + "additionalProperties": false, + "required": ["format-version", "source-digest-inputs", "claims"], + "properties": { + "format-version": { "const": 1 }, + "source-digest-inputs": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + }, + "claims": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "extension", + "postgres-major", + "artifact-family", + "platform-targets", + "runtime-modes", + "evidence-required", + "public" + ], + "properties": { + "extension": { "type": "string" }, + "postgres-major": { "type": "integer" }, + "artifact-family": { "type": "string" }, + "platform-targets": { "type": "array", "items": { "type": "string" } }, + "runtime-modes": { "type": "array", "items": { "type": "string" } }, + "evidence-required": { "type": "array", "items": { "type": "string" } }, + "public": { "type": "boolean" } + } + } + } + } +} diff --git a/src/extensions/evidence/schemas/run.schema.json b/src/extensions/evidence/schemas/run.schema.json new file mode 100644 index 00000000..b94aa418 --- /dev/null +++ b/src/extensions/evidence/schemas/run.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://oliphaunt.dev/schemas/extensions/evidence-run.schema.json", + "title": "Oliphaunt Extension Evidence Run", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "id", + "evidenceTier", + "status", + "sourceDigest", + "sourceDigestInputs", + "observedAt", + "collector", + "results" + ], + "properties": { + "schema": { "const": "oliphaunt-extension-evidence-v1" }, + "id": { "type": "string" }, + "evidenceTier": { "type": "string" }, + "status": { "enum": ["passed", "failed", "blocked"] }, + "sourceDigest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" }, + "sourceDigestInputs": { "type": "array", "items": { "type": "string" } }, + "sourceCommit": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "sourceTree": { "type": "string", "pattern": "^[0-9a-f]{40}$" }, + "observedAt": { "type": "string" }, + "collector": { "type": "string" }, + "github": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "workflow", "runId", "runAttempt", "job"], + "properties": { + "repository": { "type": "string", "minLength": 1 }, + "workflow": { "type": "string", "minLength": 1 }, + "runId": { "type": "integer", "minimum": 1 }, + "runAttempt": { "type": "integer", "minimum": 1 }, + "job": { "type": "string", "minLength": 1 } + } + }, + "notes": { "type": "string" }, + "results": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "extension", + "sqlName", + "postgresMajor", + "artifactFamily", + "platformTarget", + "runtimeModeStatuses" + ], + "properties": { + "extension": { "type": "string" }, + "sqlName": { "type": "string" }, + "postgresMajor": { "type": "integer" }, + "artifactFamily": { "type": "string" }, + "platformTarget": { "type": "string" }, + "runtimeModeStatuses": { + "type": "object", + "additionalProperties": { "enum": ["passed", "failed", "blocked", "not-run"] } + } + } + } + } + } +} diff --git a/src/extensions/external/README.md b/src/extensions/external/README.md new file mode 100644 index 00000000..d3f6930b --- /dev/null +++ b/src/extensions/external/README.md @@ -0,0 +1,8 @@ +# Shared External Extension Recipes + +This directory is the tracked source root for shared external extension recipe +metadata. Per-extension recipes move here as they graduate from the transitional +catalog rows to source-owned recipes. + +Do not place generated checkouts here. Source checkouts stay under +`target/oliphaunt-sources/checkouts/`. diff --git a/src/extensions/external/age/moon.yml b/src/extensions/external/age/moon.yml new file mode 100644 index 00000000..55014882 --- /dev/null +++ b/src/extensions/external/age/moon.yml @@ -0,0 +1,23 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-age" +language: "unknown" +layer: "configuration" +stack: "systems" +tags: ["extensions", "external"] +dependsOn: + - "extension-runtime-contract" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/external/age/source.toml b/src/extensions/external/age/source.toml new file mode 100644 index 00000000..cb6c1ec0 --- /dev/null +++ b/src/extensions/external/age/source.toml @@ -0,0 +1,9 @@ +name = "age" +url = "https://github.com/apache/age.git" +branch = "PG17" +commit = "e1467f12e0b1d15dd35d3ab93f057a7112d425b8" + +[extension-control] +sql-name = "age" +source-path = "age.control" +default-version = "1.7.0" diff --git a/src/extensions/external/pg_hashids/.release-semantic-inputs.json b/src/extensions/external/pg_hashids/.release-semantic-inputs.json new file mode 100644 index 00000000..f6ea7127 --- /dev/null +++ b/src/extensions/external/pg_hashids/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-pg-hashids", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "bf96611246fa30463b91cbec80ce72eb476ff914d358dbe7f5da58ba9c526911" +} diff --git a/src/extensions/external/pg_hashids/CHANGELOG.md b/src/extensions/external/pg_hashids/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/pg_hashids/VERSION b/src/extensions/external/pg_hashids/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/pg_hashids/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/pg_hashids/moon.yml b/src/extensions/external/pg_hashids/moon.yml new file mode 100644 index 00000000..ee1c1e0e --- /dev/null +++ b/src/extensions/external/pg_hashids/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-hashids" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-pg-hashids" + packagePath: "src/extensions/external/pg_hashids" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/pg_hashids/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-pg-hashids/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pg_hashids/release.toml b/src/extensions/external/pg_hashids/release.toml new file mode 100644 index 00000000..1d1a1a48 --- /dev/null +++ b/src/extensions/external/pg_hashids/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-pg-hashids" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-pg-hashids", + "crates:oliphaunt-extension-pg-hashids-aot-linux-arm64", + "crates:oliphaunt-extension-pg-hashids-aot-linux-x64", + "crates:oliphaunt-extension-pg-hashids-aot-macos-arm64", + "crates:oliphaunt-extension-pg-hashids-aot-windows-x64", + "crates:oliphaunt-extension-pg-hashids-linux-arm64-gnu", + "crates:oliphaunt-extension-pg-hashids-linux-x64-gnu", + "crates:oliphaunt-extension-pg-hashids-macos-arm64", + "crates:oliphaunt-extension-pg-hashids-wasix", + "crates:oliphaunt-extension-pg-hashids-windows-x64-msvc", + "npm:@oliphaunt/extension-pg-hashids", + "npm:@oliphaunt/extension-pg-hashids-linux-arm64-gnu", + "npm:@oliphaunt/extension-pg-hashids-linux-x64-gnu", + "npm:@oliphaunt/extension-pg-hashids-macos-arm64", + "npm:@oliphaunt/extension-pg-hashids-windows-x64-msvc", + "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" + +[extension] +sql_name = "pg_hashids" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/pg_hashids/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-pg-hashids-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/pg_hashids/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-pg-hashids-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/pg_hashids/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/pg_hashids/source.toml b/src/extensions/external/pg_hashids/source.toml new file mode 100644 index 00000000..81fb473d --- /dev/null +++ b/src/extensions/external/pg_hashids/source.toml @@ -0,0 +1,9 @@ +name = "pg_hashids" +url = "https://github.com/iCyberon/pg_hashids" +branch = "pinned" +commit = "8c404dd86408f3a987a3ff6825ac7e42bd618b98" + +[extension-control] +sql-name = "pg_hashids" +source-path = "pg_hashids.control" +default-version = "1.3" diff --git a/src/extensions/external/pg_hashids/targets/artifacts.toml b/src/extensions/external/pg_hashids/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/pg_hashids/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pg_ivm/.release-semantic-inputs.json b/src/extensions/external/pg_ivm/.release-semantic-inputs.json new file mode 100644 index 00000000..123aaad5 --- /dev/null +++ b/src/extensions/external/pg_ivm/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-pg-ivm", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "e76cddfe11561ae8219b2f3ab7eadcfa50d7f53d25fab865e73448c41fe8eb76" +} diff --git a/src/extensions/external/pg_ivm/CHANGELOG.md b/src/extensions/external/pg_ivm/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/pg_ivm/VERSION b/src/extensions/external/pg_ivm/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/pg_ivm/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/pg_ivm/moon.yml b/src/extensions/external/pg_ivm/moon.yml new file mode 100644 index 00000000..0a423710 --- /dev/null +++ b/src/extensions/external/pg_ivm/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-ivm" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-pg-ivm" + packagePath: "src/extensions/external/pg_ivm" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/pg_ivm/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-pg-ivm/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pg_ivm/release.toml b/src/extensions/external/pg_ivm/release.toml new file mode 100644 index 00000000..bb81b1dc --- /dev/null +++ b/src/extensions/external/pg_ivm/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-pg-ivm" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-pg-ivm", + "crates:oliphaunt-extension-pg-ivm-aot-linux-arm64", + "crates:oliphaunt-extension-pg-ivm-aot-linux-x64", + "crates:oliphaunt-extension-pg-ivm-aot-macos-arm64", + "crates:oliphaunt-extension-pg-ivm-aot-windows-x64", + "crates:oliphaunt-extension-pg-ivm-linux-arm64-gnu", + "crates:oliphaunt-extension-pg-ivm-linux-x64-gnu", + "crates:oliphaunt-extension-pg-ivm-macos-arm64", + "crates:oliphaunt-extension-pg-ivm-wasix", + "crates:oliphaunt-extension-pg-ivm-windows-x64-msvc", + "npm:@oliphaunt/extension-pg-ivm", + "npm:@oliphaunt/extension-pg-ivm-linux-arm64-gnu", + "npm:@oliphaunt/extension-pg-ivm-linux-x64-gnu", + "npm:@oliphaunt/extension-pg-ivm-macos-arm64", + "npm:@oliphaunt/extension-pg-ivm-windows-x64-msvc", + "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" + +[extension] +sql_name = "pg_ivm" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/pg_ivm/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-pg-ivm-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/pg_ivm/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-pg-ivm-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/pg_ivm/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/pg_ivm/source.toml b/src/extensions/external/pg_ivm/source.toml new file mode 100644 index 00000000..30babd02 --- /dev/null +++ b/src/extensions/external/pg_ivm/source.toml @@ -0,0 +1,9 @@ +name = "pg_ivm" +url = "https://github.com/sraoss/pg_ivm.git" +branch = "pinned" +commit = "b66487f7a6f8deee3998e858d773e19923e4bd4b" + +[extension-control] +sql-name = "pg_ivm" +source-path = "pg_ivm.control" +default-version = "1.13" diff --git a/src/extensions/external/pg_ivm/targets/artifacts.toml b/src/extensions/external/pg_ivm/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/pg_ivm/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pg_ivm/targets/native-static-registry.toml b/src/extensions/external/pg_ivm/targets/native-static-registry.toml new file mode 100644 index 00000000..7118eeea --- /dev/null +++ b/src/extensions/external/pg_ivm/targets/native-static-registry.toml @@ -0,0 +1,11 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "native-static-registry" +status = "supported" +build_kind = "pgxs-static-registry" +source_files = [ + "createas.c", + "matview.c", + "pg_ivm.c", + "ruleutils.c", + "subselect.c", +] diff --git a/src/extensions/external/pg_textsearch/.release-semantic-inputs.json b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json new file mode 100644 index 00000000..fd70342e --- /dev/null +++ b/src/extensions/external/pg_textsearch/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-pg-textsearch", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "8f17bdad70ee97db82a17475057762eab053ecc308e4c4188f387563c6391872" +} diff --git a/src/extensions/external/pg_textsearch/CHANGELOG.md b/src/extensions/external/pg_textsearch/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/pg_textsearch/VERSION b/src/extensions/external/pg_textsearch/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/pg_textsearch/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/pg_textsearch/moon.yml b/src/extensions/external/pg_textsearch/moon.yml new file mode 100644 index 00000000..afc39f15 --- /dev/null +++ b/src/extensions/external/pg_textsearch/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-textsearch" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-pg-textsearch" + packagePath: "src/extensions/external/pg_textsearch" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/pg_textsearch/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-pg-textsearch/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pg_textsearch/recipe.toml b/src/extensions/external/pg_textsearch/recipe.toml new file mode 100644 index 00000000..ba9ff8c2 --- /dev/null +++ b/src/extensions/external/pg_textsearch/recipe.toml @@ -0,0 +1,43 @@ +schema = "oliphaunt-extension-recipe-v1" +sql_name = "pg_textsearch" +display_name = "pg_textsearch" +kind = "external-simple-pgxs" +license = "PostgreSQL" +source = "pg_textsearch" +postgres_majors = [18] + +[lifecycle] +creates_extension = true +requires = [] +implicit_sql_dependencies = [] +load_sql = [] +post_create_sql = [] +shared_preload_libraries = [] +restart_required = false +background_workers = false +shared_memory = false +session_load_required = false +needs_superuser = false +trusted = false + +[artifacts] +control_files = ["share/postgresql/extension/pg_textsearch.control"] +sql_globs = ["share/postgresql/extension/pg_textsearch--*.sql"] +native_modules = [] +native_dependency_modules = [] +data_files = [] +headers = [] +licenses = ["share/licenses/pg_textsearch/LICENSE"] + +[support.native] +direct = "supported" +broker = "supported" +server = "supported" + +[support.wasix] +direct = "supported" +server = "supported" + +[support.mobile] +ios = "supported" +android = "supported" diff --git a/src/extensions/external/pg_textsearch/release.toml b/src/extensions/external/pg_textsearch/release.toml new file mode 100644 index 00000000..68135235 --- /dev/null +++ b/src/extensions/external/pg_textsearch/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-pg-textsearch" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-pg-textsearch", + "crates:oliphaunt-extension-pg-textsearch-aot-linux-arm64", + "crates:oliphaunt-extension-pg-textsearch-aot-linux-x64", + "crates:oliphaunt-extension-pg-textsearch-aot-macos-arm64", + "crates:oliphaunt-extension-pg-textsearch-aot-windows-x64", + "crates:oliphaunt-extension-pg-textsearch-linux-arm64-gnu", + "crates:oliphaunt-extension-pg-textsearch-linux-x64-gnu", + "crates:oliphaunt-extension-pg-textsearch-macos-arm64", + "crates:oliphaunt-extension-pg-textsearch-wasix", + "crates:oliphaunt-extension-pg-textsearch-windows-x64-msvc", + "npm:@oliphaunt/extension-pg-textsearch", + "npm:@oliphaunt/extension-pg-textsearch-linux-arm64-gnu", + "npm:@oliphaunt/extension-pg-textsearch-linux-x64-gnu", + "npm:@oliphaunt/extension-pg-textsearch-macos-arm64", + "npm:@oliphaunt/extension-pg-textsearch-windows-x64-msvc", + "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" + +[extension] +sql_name = "pg_textsearch" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/pg_textsearch/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-pg-textsearch-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/pg_textsearch/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-pg-textsearch-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/pg_textsearch/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/pg_textsearch/source.toml b/src/extensions/external/pg_textsearch/source.toml new file mode 100644 index 00000000..fbb9779b --- /dev/null +++ b/src/extensions/external/pg_textsearch/source.toml @@ -0,0 +1,9 @@ +name = "pg_textsearch" +url = "https://github.com/timescale/pg_textsearch.git" +branch = "pinned" +commit = "5c5147bf2610d786f1bd139951b9fb7fe4ac68fb" + +[extension-control] +sql-name = "pg_textsearch" +source-path = "pg_textsearch.control" +default-version = "0.5.1" diff --git a/src/extensions/external/pg_textsearch/targets/artifacts.toml b/src/extensions/external/pg_textsearch/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/pg_textsearch/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pg_textsearch/targets/native-static-registry.toml b/src/extensions/external/pg_textsearch/targets/native-static-registry.toml new file mode 100644 index 00000000..b206cc4f --- /dev/null +++ b/src/extensions/external/pg_textsearch/targets/native-static-registry.toml @@ -0,0 +1,6 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "native-static-registry" +status = "supported" +build_kind = "pgxs-static-registry" +source_recursive_dirs = ["src"] +include_dirs = ["source:src"] diff --git a/src/extensions/external/pg_textsearch/tests/smoke.sql b/src/extensions/external/pg_textsearch/tests/smoke.sql new file mode 100644 index 00000000..a232ab82 --- /dev/null +++ b/src/extensions/external/pg_textsearch/tests/smoke.sql @@ -0,0 +1,5 @@ +-- oliphaunt-statement +CREATE EXTENSION IF NOT EXISTS pg_textsearch; + +-- oliphaunt-statement +SELECT extname FROM pg_extension WHERE extname = 'pg_textsearch'; diff --git a/src/extensions/external/pg_textsearch/tests/upstream.toml b/src/extensions/external/pg_textsearch/tests/upstream.toml new file mode 100644 index 00000000..5fe1e901 --- /dev/null +++ b/src/extensions/external/pg_textsearch/tests/upstream.toml @@ -0,0 +1,6 @@ +schema = "oliphaunt-extension-upstream-tests-v1" +runner = "pgxs-installcheck" +status = "candidate" +reason = "pg_textsearch is currently covered by Oliphaunt direct/server/restart/dump smoke evidence. Upstream PGXS installcheck should be wired before using the full upstream suite as release evidence." +included_suites = ["pg_textsearch"] +excluded_suites = [] diff --git a/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json new file mode 100644 index 00000000..a0cb6385 --- /dev/null +++ b/src/extensions/external/pg_uuidv7/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-pg-uuidv7", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "377790f28d177cac7059d33d7da26128efbb6840670da9a9d1ea5d80d8396150" +} diff --git a/src/extensions/external/pg_uuidv7/CHANGELOG.md b/src/extensions/external/pg_uuidv7/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/pg_uuidv7/VERSION b/src/extensions/external/pg_uuidv7/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/pg_uuidv7/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/pg_uuidv7/moon.yml b/src/extensions/external/pg_uuidv7/moon.yml new file mode 100644 index 00000000..d688418d --- /dev/null +++ b/src/extensions/external/pg_uuidv7/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pg-uuidv7" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-pg-uuidv7" + packagePath: "src/extensions/external/pg_uuidv7" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/pg_uuidv7/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-pg-uuidv7/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pg_uuidv7/release.toml b/src/extensions/external/pg_uuidv7/release.toml new file mode 100644 index 00000000..5782a0c2 --- /dev/null +++ b/src/extensions/external/pg_uuidv7/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-pg-uuidv7" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-pg-uuidv7", + "crates:oliphaunt-extension-pg-uuidv7-aot-linux-arm64", + "crates:oliphaunt-extension-pg-uuidv7-aot-linux-x64", + "crates:oliphaunt-extension-pg-uuidv7-aot-macos-arm64", + "crates:oliphaunt-extension-pg-uuidv7-aot-windows-x64", + "crates:oliphaunt-extension-pg-uuidv7-linux-arm64-gnu", + "crates:oliphaunt-extension-pg-uuidv7-linux-x64-gnu", + "crates:oliphaunt-extension-pg-uuidv7-macos-arm64", + "crates:oliphaunt-extension-pg-uuidv7-wasix", + "crates:oliphaunt-extension-pg-uuidv7-windows-x64-msvc", + "npm:@oliphaunt/extension-pg-uuidv7", + "npm:@oliphaunt/extension-pg-uuidv7-linux-arm64-gnu", + "npm:@oliphaunt/extension-pg-uuidv7-linux-x64-gnu", + "npm:@oliphaunt/extension-pg-uuidv7-macos-arm64", + "npm:@oliphaunt/extension-pg-uuidv7-windows-x64-msvc", + "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" + +[extension] +sql_name = "pg_uuidv7" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/pg_uuidv7/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-pg-uuidv7-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/pg_uuidv7/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-pg-uuidv7-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/pg_uuidv7/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/pg_uuidv7/source.toml b/src/extensions/external/pg_uuidv7/source.toml new file mode 100644 index 00000000..5c31a9e7 --- /dev/null +++ b/src/extensions/external/pg_uuidv7/source.toml @@ -0,0 +1,9 @@ +name = "pg_uuidv7" +url = "https://github.com/fboulnois/pg_uuidv7/" +branch = "pinned" +commit = "c707aae2411181be4802f5fa565b44d9c0bcbc29" + +[extension-control] +sql-name = "pg_uuidv7" +source-path = "pg_uuidv7.control" +default-version = "1.7" diff --git a/src/extensions/external/pg_uuidv7/targets/artifacts.toml b/src/extensions/external/pg_uuidv7/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/pg_uuidv7/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pgtap/.release-semantic-inputs.json b/src/extensions/external/pgtap/.release-semantic-inputs.json new file mode 100644 index 00000000..756d90b0 --- /dev/null +++ b/src/extensions/external/pgtap/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-pgtap", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "c9202c9da84bebdd146bd9439170792fd699427ed47e1ddf17d821b13e6a0e04" +} diff --git a/src/extensions/external/pgtap/CHANGELOG.md b/src/extensions/external/pgtap/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/pgtap/VERSION b/src/extensions/external/pgtap/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/pgtap/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/pgtap/moon.yml b/src/extensions/external/pgtap/moon.yml new file mode 100644 index 00000000..988aebc9 --- /dev/null +++ b/src/extensions/external/pgtap/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-pgtap" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-pgtap" + packagePath: "src/extensions/external/pgtap" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/pgtap/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-pgtap/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/pgtap/recipe.toml b/src/extensions/external/pgtap/recipe.toml new file mode 100644 index 00000000..03810e6d --- /dev/null +++ b/src/extensions/external/pgtap/recipe.toml @@ -0,0 +1,48 @@ +schema = "oliphaunt-extension-recipe-v1" +sql_name = "pgtap" +display_name = "pgTAP" +kind = "external-simple-pgxs" +license = "PostgreSQL" +source = "pgtap" +postgres_majors = [18] + +[lifecycle] +creates_extension = true +requires = ["plpgsql"] +implicit_sql_dependencies = [] +load_sql = [] +post_create_sql = [] +shared_preload_libraries = [] +restart_required = false +background_workers = false +shared_memory = false +session_load_required = false +needs_superuser = false +trusted = false + +[artifacts] +control_files = ["share/postgresql/extension/pgtap.control"] +sql_globs = ["share/postgresql/extension/pgtap--*.sql"] +extension_sql_file_prefixes = [ + "pgtap-core", + "pgtap-schema", +] +extension_sql_file_names = ["uninstall_pgtap.sql"] +native_modules = [] +native_dependency_modules = [] +data_files = [] +headers = [] +licenses = [] + +[support.native] +direct = "supported" +broker = "supported" +server = "supported" + +[support.wasix] +direct = "supported" +server = "supported" + +[support.mobile] +ios = "supported" +android = "supported" diff --git a/src/extensions/external/pgtap/release.toml b/src/extensions/external/pgtap/release.toml new file mode 100644 index 00000000..e91ef966 --- /dev/null +++ b/src/extensions/external/pgtap/release.toml @@ -0,0 +1,51 @@ +id = "oliphaunt-extension-pgtap" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-pgtap", + "crates:oliphaunt-extension-pgtap-linux-arm64-gnu", + "crates:oliphaunt-extension-pgtap-linux-x64-gnu", + "crates:oliphaunt-extension-pgtap-macos-arm64", + "crates:oliphaunt-extension-pgtap-wasix", + "crates:oliphaunt-extension-pgtap-windows-x64-msvc", + "npm:@oliphaunt/extension-pgtap", + "npm:@oliphaunt/extension-pgtap-linux-arm64-gnu", + "npm:@oliphaunt/extension-pgtap-linux-x64-gnu", + "npm:@oliphaunt/extension-pgtap-macos-arm64", + "npm:@oliphaunt/extension-pgtap-windows-x64-msvc", + "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" + +[extension] +sql_name = "pgtap" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/pgtap/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-pgtap-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/pgtap/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-pgtap-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/pgtap/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/pgtap/source.toml b/src/extensions/external/pgtap/source.toml new file mode 100644 index 00000000..98265fca --- /dev/null +++ b/src/extensions/external/pgtap/source.toml @@ -0,0 +1,9 @@ +name = "pgtap" +url = "https://github.com/theory/pgtap.git" +branch = "pinned" +commit = "b89585a64ffef012ff0f219de9197c669aa8485b" + +[extension-control] +sql-name = "pgtap" +source-path = "pgtap.control" +default-version = "1.3.5" diff --git a/src/extensions/external/pgtap/targets/artifacts.toml b/src/extensions/external/pgtap/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/pgtap/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/pgtap/targets/native-static-registry.toml b/src/extensions/external/pgtap/targets/native-static-registry.toml new file mode 100644 index 00000000..5d889b07 --- /dev/null +++ b/src/extensions/external/pgtap/targets/native-static-registry.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "supported" +build_kind = "pgxs-sql-only" +platform_targets = [ + "macos-arm64", + "ios-arm64", + "ios-simulator-arm64", + "android-arm64-v8a", + "android-x86_64", +] +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgtap/targets/native.toml b/src/extensions/external/pgtap/targets/native.toml new file mode 100644 index 00000000..cea227e2 --- /dev/null +++ b/src/extensions/external/pgtap/targets/native.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "supported" +build_kind = "pgxs-sql-only" +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgtap/targets/wasix.toml b/src/extensions/external/pgtap/targets/wasix.toml new file mode 100644 index 00000000..cea227e2 --- /dev/null +++ b/src/extensions/external/pgtap/targets/wasix.toml @@ -0,0 +1,7 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "sql-only" +status = "supported" +build_kind = "pgxs-sql-only" +dependencies = [] +module_files = [] +data_files = [] diff --git a/src/extensions/external/pgtap/tests/smoke.sql b/src/extensions/external/pgtap/tests/smoke.sql new file mode 100644 index 00000000..635a7482 --- /dev/null +++ b/src/extensions/external/pgtap/tests/smoke.sql @@ -0,0 +1,7 @@ +SELECT plan(2); +-- oliphaunt-statement +SELECT ok(true, 'pgTAP ok() executes'); +-- oliphaunt-statement +SELECT is(1, 1, 'pgTAP is() compares values'); +-- oliphaunt-statement +SELECT * FROM finish(); diff --git a/src/extensions/external/pgtap/tests/upstream.toml b/src/extensions/external/pgtap/tests/upstream.toml new file mode 100644 index 00000000..385c41a6 --- /dev/null +++ b/src/extensions/external/pgtap/tests/upstream.toml @@ -0,0 +1,6 @@ +schema = "oliphaunt-extension-upstream-tests-v1" +runner = "pgxs-installcheck" +status = "candidate" +reason = "pgTAP is currently covered by Oliphaunt direct/server/restart/dump smoke evidence. Upstream PGXS installcheck should be wired before using upstream pgTAP's full suite as release evidence." +included_suites = ["pgtap"] +excluded_suites = [] diff --git a/src/extensions/external/postgis/.release-semantic-inputs.json b/src/extensions/external/postgis/.release-semantic-inputs.json new file mode 100644 index 00000000..15b3f5da --- /dev/null +++ b/src/extensions/external/postgis/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-postgis", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "726c44b441fc6c150e465f627081cabb942d0caeff6bd74a8ef9a76fbc298d07" +} diff --git a/src/extensions/external/postgis/CHANGELOG.md b/src/extensions/external/postgis/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/postgis/VERSION b/src/extensions/external/postgis/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/postgis/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/postgis/blockers.toml b/src/extensions/external/postgis/blockers.toml new file mode 100644 index 00000000..d897e16d --- /dev/null +++ b/src/extensions/external/postgis/blockers.toml @@ -0,0 +1,2 @@ +schema = "oliphaunt-extension-blockers-v1" +blockers = [] diff --git a/src/extensions/external/postgis/dependencies/geos/source.toml b/src/extensions/external/postgis/dependencies/geos/source.toml new file mode 100644 index 00000000..a066a551 --- /dev/null +++ b/src/extensions/external/postgis/dependencies/geos/source.toml @@ -0,0 +1,4 @@ +name = "geos" +url = "https://github.com/libgeos/geos.git" +branch = "3.14.1" +commit = "c389f532d25fe6228861d9b19339f9cb57ca4bdb" diff --git a/src/extensions/external/postgis/dependencies/json-c/source.toml b/src/extensions/external/postgis/dependencies/json-c/source.toml new file mode 100644 index 00000000..9e386232 --- /dev/null +++ b/src/extensions/external/postgis/dependencies/json-c/source.toml @@ -0,0 +1,4 @@ +name = "json-c" +url = "https://github.com/json-c/json-c.git" +branch = "json-c-0.18-20240915" +commit = "41a55cfcedb54d9c1874f2f0eb07b504091d7e37" diff --git a/src/extensions/external/postgis/dependencies/libiconv/source.toml b/src/extensions/external/postgis/dependencies/libiconv/source.toml new file mode 100644 index 00000000..6e1a3fb0 --- /dev/null +++ b/src/extensions/external/postgis/dependencies/libiconv/source.toml @@ -0,0 +1,7 @@ +name = "libiconv" +kind = "archive" +url = "https://ftpmirror.gnu.org/libiconv/libiconv-1.19.tar.gz" +branch = "1.19" +commit = "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" +sha256 = "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" +strip_prefix = "libiconv-1.19" diff --git a/src/extensions/external/postgis/dependencies/libxml2/source.toml b/src/extensions/external/postgis/dependencies/libxml2/source.toml new file mode 100644 index 00000000..349df0ec --- /dev/null +++ b/src/extensions/external/postgis/dependencies/libxml2/source.toml @@ -0,0 +1,5 @@ +name = "libxml2" +url = "https://gitlab.gnome.org/GNOME/libxml2.git" +mirror_url = "https://github.com/GNOME/libxml2.git" +branch = "v2.14.6" +commit = "d23960a130c5bb82779c9405fbbf85e65fb3c57c" diff --git a/src/extensions/external/postgis/dependencies/proj/source.toml b/src/extensions/external/postgis/dependencies/proj/source.toml new file mode 100644 index 00000000..4e7134dd --- /dev/null +++ b/src/extensions/external/postgis/dependencies/proj/source.toml @@ -0,0 +1,4 @@ +name = "proj" +url = "https://github.com/OSGeo/PROJ.git" +branch = "9.8.1" +commit = "f08fa86c478c4bbbf003b1ec751dd84aa6eca486" diff --git a/src/extensions/external/postgis/dependencies/sqlite/source.toml b/src/extensions/external/postgis/dependencies/sqlite/source.toml new file mode 100644 index 00000000..61eab363 --- /dev/null +++ b/src/extensions/external/postgis/dependencies/sqlite/source.toml @@ -0,0 +1,4 @@ +name = "sqlite" +url = "https://github.com/sqlite/sqlite.git" +branch = "version-3.53.1" +commit = "ccd445d76a9362c63add000354fac84ba9022176" diff --git a/src/extensions/external/postgis/deps.toml b/src/extensions/external/postgis/deps.toml new file mode 100644 index 00000000..52004b0e --- /dev/null +++ b/src/extensions/external/postgis/deps.toml @@ -0,0 +1,49 @@ +schema = "oliphaunt-extension-deps-v1" + +[[dependencies]] +name = "geos" +source = "geos" +version = "3.14.1" +linkage = "static" +license = "LGPL-2.1-or-later" +mobile-static-dependencies = ["geos-c", "geos"] + +[[dependencies]] +name = "proj" +source = "proj" +version = "9.8.1" +linkage = "static" +license = "MIT" +mobile-static-dependencies = ["proj"] + +[[dependencies]] +name = "sqlite" +source = "sqlite" +version = "3.53.1" +linkage = "static" +license = "blessing" +mobile-static-dependencies = ["sqlite"] + +[[dependencies]] +name = "libxml2" +source = "libxml2" +version = "2.14.6" +linkage = "static" +license = "MIT" +mobile-static-dependencies = ["libxml2"] + +[[dependencies]] +name = "json-c" +source = "json-c" +version = "0.18" +linkage = "static" +license = "MIT" +mobile-static-dependencies = ["json-c"] + +[[dependencies]] +name = "libiconv" +source = "libiconv" +version = "1.19" +linkage = "static" +license = "LGPL-2.1-or-later" +mobile-static-dependencies = ["libiconv", "libcharset"] diff --git a/src/extensions/external/postgis/moon.yml b/src/extensions/external/postgis/moon.yml new file mode 100644 index 00000000..d3ae5d59 --- /dev/null +++ b/src/extensions/external/postgis/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-postgis" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "postgis", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-postgis" + packagePath: "src/extensions/external/postgis" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/postgis/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-postgis/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/postgis/patches/README.md b/src/extensions/external/postgis/patches/README.md new file mode 100644 index 00000000..c797b150 --- /dev/null +++ b/src/extensions/external/postgis/patches/README.md @@ -0,0 +1,3 @@ +PostGIS patches for Oliphaunt live here when they are introduced. + +The current reduced build uses build-system flags instead of source patches. diff --git a/src/extensions/external/postgis/recipe.toml b/src/extensions/external/postgis/recipe.toml new file mode 100644 index 00000000..6517000a --- /dev/null +++ b/src/extensions/external/postgis/recipe.toml @@ -0,0 +1,72 @@ +schema = "oliphaunt-extension-recipe-v1" +sql_name = "postgis" +display_name = "PostGIS" +kind = "external-complex" +license = "GPL-2.0-or-later" +source = "postgis" +postgres_majors = [18] + +[lifecycle] +creates_extension = true +requires = [] +implicit_sql_dependencies = [] +load_sql = [] +post_create_sql = [] +shared_preload_libraries = [] +restart_required = false +background_workers = false +shared_memory = false +session_load_required = false +needs_superuser = true +trusted = false + +[artifacts] +control_files = ["share/postgresql/extension/postgis.control"] +sql_globs = ["share/postgresql/extension/postgis--*.sql"] +extension_sql_file_prefixes = [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis", +] +extension_sql_file_names = ["uninstall_postgis.sql"] +native_modules = ["lib/postgresql/postgis-3.so"] +native_dependency_modules = ["lib/postgresql/liboliphaunt_postgis_deps.so"] +data_files = [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db", +] +headers = [] +licenses = [ + "share/licenses/postgis/COPYING", + "share/licenses/geos/COPYING", + "share/licenses/proj/COPYING", + "share/licenses/sqlite/LICENSE.md", + "share/licenses/libxml2/Copyright", + "share/licenses/json-c/COPYING", + "share/licenses/libiconv/COPYING", +] + +[[runtime_environment]] +name = "PROJ_DATA" +path = "share/postgresql/proj" +required_file = "proj.db" + +[support.native] +direct = "supported" +broker = "supported" +server = "supported" + +[support.wasix] +direct = "supported" +server = "supported" + +[support.mobile] +ios = "supported" +android = "supported" diff --git a/src/extensions/external/postgis/release.toml b/src/extensions/external/postgis/release.toml new file mode 100644 index 00000000..aa8e3988 --- /dev/null +++ b/src/extensions/external/postgis/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-postgis" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-postgis", + "crates:oliphaunt-extension-postgis-aot-linux-arm64", + "crates:oliphaunt-extension-postgis-aot-linux-x64", + "crates:oliphaunt-extension-postgis-aot-macos-arm64", + "crates:oliphaunt-extension-postgis-aot-windows-x64", + "crates:oliphaunt-extension-postgis-linux-arm64-gnu", + "crates:oliphaunt-extension-postgis-linux-x64-gnu", + "crates:oliphaunt-extension-postgis-macos-arm64", + "crates:oliphaunt-extension-postgis-wasix", + "crates:oliphaunt-extension-postgis-windows-x64-msvc", + "npm:@oliphaunt/extension-postgis", + "npm:@oliphaunt/extension-postgis-linux-arm64-gnu", + "npm:@oliphaunt/extension-postgis-linux-x64-gnu", + "npm:@oliphaunt/extension-postgis-macos-arm64", + "npm:@oliphaunt/extension-postgis-windows-x64-msvc", + "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" + +[extension] +sql_name = "postgis" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/postgis/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-postgis-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/postgis/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-postgis-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/postgis/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/postgis/source.toml b/src/extensions/external/postgis/source.toml new file mode 100644 index 00000000..1f57d6de --- /dev/null +++ b/src/extensions/external/postgis/source.toml @@ -0,0 +1,10 @@ +name = "postgis" +url = "https://github.com/postgis/postgis.git" +branch = "3.6.3" +commit = "3d12666588a84b23a3147618eaa9b40b0fe5e796" + +[extension-control] +sql-name = "postgis" +source-path = "extensions/postgis/postgis.control.in" +source-default-version = "@EXTVERSION@" +default-version = "3.6.3" diff --git a/src/extensions/external/postgis/targets/artifacts.toml b/src/extensions/external/postgis/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/postgis/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/src/extensions/external/postgis/targets/native-static-registry.toml b/src/extensions/external/postgis/targets/native-static-registry.toml new file mode 100644 index 00000000..35aefc09 --- /dev/null +++ b/src/extensions/external/postgis/targets/native-static-registry.toml @@ -0,0 +1,41 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "native-static-registry" +status = "supported" +build_kind = "autotools-static-registry" +platform_targets = [ + "macos-arm64", + "ios-arm64", + "ios-simulator-arm64", + "android-arm64-v8a", + "android-x86_64", +] +dependencies = ["geos", "proj", "sqlite", "libxml2", "json-c", "libiconv"] +ios_dependencies = ["geos", "proj", "sqlite", "libxml2", "json-c"] +android_dependencies = ["geos", "proj", "sqlite", "libxml2", "json-c", "libiconv"] +module_files = ["postgis-3"] +data_files = [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db", +] +configure_flags = [ + "--without-raster", + "--without-topology", + "--without-sfcgal", + "--without-address-standardizer", + "--without-protobuf", +] +excluded_sql_extensions = [ + "postgis_raster", + "postgis_topology", + "postgis_sfcgal", + "postgis_tiger_geocoder", + "address_standardizer", + "address_standardizer_data_us", +] diff --git a/src/extensions/external/postgis/targets/native.toml b/src/extensions/external/postgis/targets/native.toml new file mode 100644 index 00000000..7c6cbf89 --- /dev/null +++ b/src/extensions/external/postgis/targets/native.toml @@ -0,0 +1,35 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "native-dynamic" +status = "supported" +build_kind = "autotools" +dependencies = ["geos", "proj", "sqlite", "libxml2", "json-c", "libiconv"] +module_files = [ + "lib/postgresql/postgis-3.so", + "lib/postgresql/liboliphaunt_postgis_deps.so", +] +data_files = [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db", +] +configure_flags = [ + "--without-raster", + "--without-topology", + "--without-sfcgal", + "--without-address-standardizer", + "--without-protobuf", +] +excluded_sql_extensions = [ + "postgis_raster", + "postgis_topology", + "postgis_sfcgal", + "postgis_tiger_geocoder", + "address_standardizer", + "address_standardizer_data_us", +] diff --git a/src/extensions/external/postgis/targets/wasix.toml b/src/extensions/external/postgis/targets/wasix.toml new file mode 100644 index 00000000..818c728b --- /dev/null +++ b/src/extensions/external/postgis/targets/wasix.toml @@ -0,0 +1,62 @@ +schema = "oliphaunt-extension-target-v1" +artifact_family = "wasix-side-module" +status = "supported" +build_kind = "autotools" +build_script = "src/extensions/external/postgis/tools/build_wasix.sh" +dependencies = ["geos", "proj", "sqlite", "libxml2", "json-c", "libiconv"] +module_files = [ + "lib/postgresql/postgis-3.so", + "lib/postgresql/liboliphaunt_postgis_deps.so", +] +data_files = [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db", +] +configure_flags = [ + "--without-raster", + "--without-topology", + "--without-sfcgal", + "--without-address-standardizer", + "--without-tiger", + "--without-protobuf", + "--disable-nls", +] +required_build_files = [ + "postgis/postgis-3.so", + "postgis/liboliphaunt_postgis_deps.so", + "extensions/postgis/postgis.control", + "share/proj/proj.db", +] +required_build_globs = [ + "extensions/postgis/sql/postgis--*.sql", +] +excluded_sql_extensions = [ + "postgis_raster", + "postgis_topology", + "postgis_sfcgal", + "postgis_tiger_geocoder", + "address_standardizer", + "address_standardizer_data_us", +] + +[[native_support_modules]] +name = "postgis_deps" +runtime_path = "lib/postgresql/liboliphaunt_postgis_deps.so" +build_path = "postgis/postgis/liboliphaunt_postgis_deps.so" +aot_file = "postgis_deps-llvm-opta.bin.zst" + +[staging] +module_source_dir = "postgis/postgis" +control_source = "postgis/extensions/postgis/postgis.control" +sql_source_dir = "postgis/extensions/postgis/sql" + +[[staging.data_dirs]] +source = "postgis/share/proj" +destination = "share/proj" diff --git a/src/extensions/external/postgis/tests/regression.sql b/src/extensions/external/postgis/tests/regression.sql new file mode 100644 index 00000000..243a42b5 --- /dev/null +++ b/src/extensions/external/postgis/tests/regression.sql @@ -0,0 +1,3 @@ +-- Full upstream PostGIS regression coverage is declared in upstream.toml. +-- This file reserves the Oliphaunt-owned regression slot for runtime-specific +-- additions that are not covered by the smoke path. diff --git a/src/extensions/external/postgis/tests/smoke.sql b/src/extensions/external/postgis/tests/smoke.sql new file mode 100644 index 00000000..4f05b7be --- /dev/null +++ b/src/extensions/external/postgis/tests/smoke.sql @@ -0,0 +1,61 @@ +DROP TABLE IF EXISTS liboliphaunt_postgis_points; +-- oliphaunt-statement +CREATE TEMP TABLE liboliphaunt_postgis_points(id int PRIMARY KEY, geom geometry(Point, 4326)); +-- oliphaunt-statement +INSERT INTO liboliphaunt_postgis_points VALUES + (1, ST_SetSRID(ST_MakePoint(-71.060316, 48.432044), 4326)), + (2, ST_SetSRID(ST_MakePoint(-71.061, 48.433), 4326)); +-- oliphaunt-statement +CREATE INDEX liboliphaunt_postgis_points_gix ON liboliphaunt_postgis_points USING GIST (geom); +-- oliphaunt-statement +DO $$ +DECLARE + distance float8; + srid int; + area float8; + polygons int; + nearby int; +BEGIN + SELECT ST_Distance(ST_GeomFromText('POINT(0 0)'), ST_GeomFromText('POINT(3 4)')) INTO distance; + IF distance <> 5 THEN + RAISE EXCEPTION 'postgis geometry distance failed: %', distance; + END IF; + IF ST_AsText(ST_Buffer(ST_GeomFromText('POINT(0 0)'), 1, 'quad_segs=1')) IS NULL THEN + RAISE EXCEPTION 'postgis buffer failed'; + END IF; + IF NOT ST_Within( + ST_GeomFromText('POINT(0.5 0.5)'), + ST_GeomFromText('POLYGON((0 0,0 1,1 1,1 0,0 0))') + ) THEN + RAISE EXCEPTION 'postgis within failed'; + END IF; + SELECT ST_SRID(ST_Transform(ST_SetSRID(ST_MakePoint(-71.060316, 48.432044), 4326), 3857)) INTO srid; + IF srid <> 3857 THEN + RAISE EXCEPTION 'postgis transform failed: %', srid; + END IF; + SELECT ST_Area(ST_Transform(ST_SetSRID(ST_MakePolygon(ST_GeomFromText('LINESTRING(-71.1776848522251 42.3902896512902,-71.1776843766797 42.3903701743239,-71.1775844305465 42.3903829478009,-71.1775825927231 42.3902893647987,-71.1776848522251 42.3902896512902)')), 4326), 26986)) INTO area; + IF area <= 0 THEN + RAISE EXCEPTION 'postgis projected area failed: %', area; + END IF; + SELECT ST_NumGeometries(ST_Polygonize(ARRAY[ + ST_GeomFromText('LINESTRING(0 0, 1 0)'), + ST_GeomFromText('LINESTRING(1 0, 1 1)'), + ST_GeomFromText('LINESTRING(1 1, 0 1)'), + ST_GeomFromText('LINESTRING(0 1, 0 0)') + ])) INTO polygons; + IF polygons <> 1 THEN + RAISE EXCEPTION 'postgis polygonize failed: %', polygons; + END IF; + SELECT count(*) INTO nearby + FROM liboliphaunt_postgis_points + WHERE ST_DWithin( + geom::geography, + ST_SetSRID(ST_MakePoint(-71.060316, 48.432044), 4326)::geography, + 200 + ); + IF nearby <> 2 THEN + RAISE EXCEPTION 'postgis dwithin failed: %', nearby; + END IF; +END $$; +-- oliphaunt-statement +DROP TABLE IF EXISTS liboliphaunt_postgis_points; diff --git a/src/extensions/external/postgis/tests/upstream.toml b/src/extensions/external/postgis/tests/upstream.toml new file mode 100644 index 00000000..5fb15934 --- /dev/null +++ b/src/extensions/external/postgis/tests/upstream.toml @@ -0,0 +1,12 @@ +schema = "oliphaunt-extension-upstream-tests-v1" +runner = "postgis-installcheck" +status = "candidate" +reason = "The reduced WASIX/mobile PostGIS surface is smoke-gated today. Upstream PostGIS installcheck is required before broadening the public evidence tier beyond transitional catalog smoke." +included_suites = ["postgis"] +excluded_suites = [ + "raster", + "topology", + "sfcgal", + "tiger_geocoder", + "address_standardizer", +] diff --git a/src/extensions/external/postgis/tools/build_wasix.sh b/src/extensions/external/postgis/tools/build_wasix.sh new file mode 100755 index 00000000..0bd64449 --- /dev/null +++ b/src/extensions/external/postgis/tools/build_wasix.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +oliphaunt_postgis_bootstrap_repo_root() { + local repo_root + if repo_root="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "$repo_root" + return 0 + fi + + repo_root="$SCRIPT_DIR" + while [ "$repo_root" != "/" ]; do + if [ -f "$repo_root/package.json" ] && + [ -d "$repo_root/src/runtimes/liboliphaunt/wasix/assets/build" ]; then + printf '%s\n' "$repo_root" + return 0 + fi + repo_root="$(dirname "$repo_root")" + done + + echo "unable to determine repository root from $SCRIPT_DIR; run this script from a Git checkout" >&2 + return 1 +} + +BOOTSTRAP_REPO_ROOT="$(oliphaunt_postgis_bootstrap_repo_root)" +ROOT="${OLIPHAUNT_WASIX_BUILD_ROOT:-$BOOTSTRAP_REPO_ROOT/src/runtimes/liboliphaunt/wasix/assets/build}" +. "$ROOT/wasix_third_party.sh" +. "$ROOT/source_lane.sh" + +REPO_ROOT="$(oliphaunt_wasix_repo_root "$SCRIPT_DIR")" +SOURCE_LANE="$(oliphaunt_wasix_source_lane)" +JOBS="${JOBS:-4}" +oliphaunt_wasix_run_extension_build_in_docker_if_needed \ + "$ROOT" \ + "$REPO_ROOT" \ + "$SOURCE_LANE" \ + "src/extensions/external/postgis/tools/build_wasix.sh" + +POSTGIS_SOURCE_DIR="${POSTGIS_SOURCE_DIR:-$(oliphaunt_wasix_extension_source_dir "$REPO_ROOT" postgis)}" +GENERATED_ROOT="$(oliphaunt_wasix_generated_root "$REPO_ROOT")" +export CONTAINER_GENERATED_ROOT="${CONTAINER_GENERATED_ROOT:-$GENERATED_ROOT}" +BUILD_DIR="${BUILD_DIR:-$(oliphaunt_wasix_default_build_dir "$SOURCE_LANE")}" +PGSRC="${PGSRC:-$(SOURCE_CACHE="${SOURCE_CACHE:-$REPO_ROOT/target/liboliphaunt-pg18/source}" "$ROOT/prepare_postgres_source.sh")}" +POSTGIS_BUILD_DIR="${POSTGIS_BUILD_DIR:-$(oliphaunt_wasix_extension_build_dir "$BUILD_DIR" postgis)}" +oliphaunt_wasix_export_extension_dependency_prefixes "$ROOT" "$REPO_ROOT" postgis +postgis_configure_flags=() +while IFS= read -r flag; do + [ -n "$flag" ] && postgis_configure_flags+=("$flag") +done < <(oliphaunt_wasix_extension_wasix_configure_flags "$REPO_ROOT" postgis) + +if [ ! -f "$POSTGIS_SOURCE_DIR/configure.ac" ]; then + echo "missing PostGIS source checkout at $POSTGIS_SOURCE_DIR; run assets fetch/source-spine first" >&2 + exit 1 +fi +if [ ! -f "$BUILD_DIR/config.status" ]; then + echo "missing WASIX PostgreSQL build at $BUILD_DIR; run docker_oliphaunt.sh first" >&2 + exit 1 +fi + +. "$ROOT/docker_wasix_env.sh" +. "$ROOT/profile_flags.sh" +oliphaunt_wasix_apply_wasix_profile build + +source_commit="$(oliphaunt_wasix_source_commit "$POSTGIS_SOURCE_DIR")" +script_sha256="$(oliphaunt_wasix_script_sha256 "$0")" +helper_sha256="$(oliphaunt_wasix_script_sha256 "$ROOT/wasix_third_party.sh")" +dependency_stamp_block="$(oliphaunt_wasix_extension_dependency_stamp_block "$REPO_ROOT" postgis)" +wasixcc_version="$(wasixcc --version 2>/dev/null)" +wasixcc_version="${wasixcc_version%%$'\n'*}" +stamp="source=$source_commit +script=$script_sha256 +helper=$helper_sha256 +$dependency_stamp_block +profile=$(oliphaunt_wasix_wasix_profile_signature) +wasixcc=$wasixcc_version +configure=$(oliphaunt_wasix_extension_configure_signature "$REPO_ROOT" postgis) +link=postgis-plus-selected-support-side-module-v2" + +if [ -f "$POSTGIS_BUILD_DIR/.oliphaunt-wasix-postgis-build" ] && + oliphaunt_wasix_extension_build_outputs_exist "$REPO_ROOT" postgis "$POSTGIS_BUILD_DIR" --quiet && + [ "$(cat "$POSTGIS_BUILD_DIR/.oliphaunt-wasix-postgis-build")" = "$stamp" ]; then + echo "$POSTGIS_BUILD_DIR" + exit 0 +fi + +{ + rm -rf "$POSTGIS_BUILD_DIR" + mkdir -p "$(dirname "$POSTGIS_BUILD_DIR")" + oliphaunt_wasix_copy_source_clean "$POSTGIS_SOURCE_DIR" "$POSTGIS_BUILD_DIR" + + geos_config="$POSTGIS_BUILD_DIR/oliphaunt-geos-config" + cat >"$geos_config" <"$pkg_config" <"$pg_config" <postgis/oliphaunt_postgis_deps_stubs.c <<'EOF' +/* + * WASIX C++ dependency objects can emit process/TLS lifecycle hooks that are + * normally supplied by a full C++ runtime. The selected PostGIS dependency side + * module is process-owned and lives for the duration of the embedded runtime, + * so there is no separate thread-local destructor lifecycle to register here. + */ +__attribute__((visibility("hidden"))) void _ZTH5errno(void) {} + +__attribute__((visibility("hidden"))) int __cxa_thread_atexit_impl( + void (*destructor)(void *), + void *object, + void *dso_symbol) +{ + (void)destructor; + (void)object; + (void)dso_symbol; + return 0; +} +EOF + wasixcc $CFLAGS -c \ + postgis/oliphaunt_postgis_deps_stubs.c \ + -o postgis/oliphaunt_postgis_deps_stubs.o + + wasm_ld="$WASIX_HOME/llvm/bin/wasm-ld" + wasix_sysroot_lib="$WASIX_HOME/sysroot/sysroot-exnref-ehpic/lib/wasm32-wasi" + postgis_deps_module="$POSTGIS_BUILD_DIR/postgis/liboliphaunt_postgis_deps.so" + "$wasm_ld" \ + --shared \ + --shared-memory \ + --experimental-pic \ + --unresolved-symbols=import-dynamic \ + --extra-features=atomics,bulk-memory,mutable-globals \ + --export=__wasm_call_ctors \ + --export-if-defined=__wasm_apply_data_relocs \ + --export-all \ + --no-gc-sections \ + "$POSTGIS_BUILD_DIR/postgis/oliphaunt_postgis_deps_stubs.o" \ + --whole-archive \ + "$GEOS_PREFIX/lib/libgeos_c.a" \ + "$GEOS_PREFIX/lib/libgeos.a" \ + "$PROJ_PREFIX/lib/libproj.a" \ + "$SQLITE_PREFIX/lib/libsqlite3.a" \ + "$JSONC_PREFIX/lib/libjson-c.a" \ + "$LIBXML2_PREFIX/lib/libxml2.a" \ + "$LIBICONV_PREFIX/lib/libiconv.a" \ + "$wasix_sysroot_lib/libc++.a" \ + "$wasix_sysroot_lib/libc++abi.a" \ + "$wasix_sysroot_lib/libunwind.a" \ + --no-whole-archive \ + "$wasix_sysroot_lib/libm.a" \ + "$wasix_sysroot_lib/libc.a" \ + "$wasix_sysroot_lib/libclang_rt.builtins-wasm32.a" \ + -o "$postgis_deps_module" + + export OLIPHAUNT_POSTGIS_STATIC_ARCHIVES="$GEOS_PREFIX/lib/libgeos_c.a $GEOS_PREFIX/lib/libgeos.a $PROJ_PREFIX/lib/libproj.a $SQLITE_PREFIX/lib/libsqlite3.a $JSONC_PREFIX/lib/libjson-c.a $LIBXML2_PREFIX/lib/libxml2.a $LIBICONV_PREFIX/lib/libiconv.a $LIBICONV_PREFIX/lib/libcharset.a $wasix_sysroot_lib/libc++.a $wasix_sysroot_lib/libc++abi.a $wasix_sysroot_lib/libunwind.a $wasix_sysroot_lib/libm.a $wasix_sysroot_lib/libc.a $wasix_sysroot_lib/libclang_rt.builtins-wasm32.a" + perl -0pi -e ' + s|^OBJS=\$\(PG_OBJS\)$|OBJS=\$(PG_OBJS) oliphaunt_postgis_deps_stubs.o $ENV{OLIPHAUNT_POSTGIS_STATIC_ARCHIVES}|m; + s|^FLATGEOBUF_LIB = .*$|FLATGEOBUF_LIB = ../deps/flatgeobuf/flatgeobuf_c.o ../deps/flatgeobuf/geometrywriter.o ../deps/flatgeobuf/geometryreader.o ../deps/flatgeobuf/packedrtree.o -lc++|m; + s|^(SHLIB_LINK := .*)$|$1 -lc|m; + s|^(LDFLAGS = .*)$|$1 -lc|m; + ' postgis/Makefile + perl -0pi -e ' + s|^(CXXFLAGS =.*)$|$1 -fvisibility=hidden -fvisibility-inlines-hidden|m; + ' deps/flatgeobuf/Makefile + + oliphaunt_wasix_apply_wasix_profile build + make -s -j"$JOBS" -C liblwgeom liblwgeom.la + make -s -j"$JOBS" -C libpgcommon libpgcommon.a + make -s -j"$JOBS" -C postgis all + postgis_objects=() + while IFS= read -r object; do + [ -n "$object" ] && postgis_objects+=("$object") + done < <(find "$POSTGIS_BUILD_DIR/postgis" -maxdepth 1 -name '*.o' ! -name 'oliphaunt_errno_tls_init_stub.o' | sort) + "$wasm_ld" \ + --shared \ + --shared-memory \ + --experimental-pic \ + --unresolved-symbols=import-dynamic \ + --extra-features=atomics,bulk-memory,mutable-globals \ + --export=__wasm_call_ctors \ + --export-if-defined=__wasm_apply_data_relocs \ + --export-all \ + --no-gc-sections \ + "${postgis_objects[@]}" \ + "$POSTGIS_BUILD_DIR/deps/flatgeobuf/flatgeobuf_c.o" \ + "$POSTGIS_BUILD_DIR/deps/flatgeobuf/geometrywriter.o" \ + "$POSTGIS_BUILD_DIR/deps/flatgeobuf/geometryreader.o" \ + "$POSTGIS_BUILD_DIR/deps/flatgeobuf/packedrtree.o" \ + "$POSTGIS_BUILD_DIR/libpgcommon/libpgcommon.a" \ + "$POSTGIS_BUILD_DIR/liblwgeom/.libs/liblwgeom.a" \ + --Bdynamic \ + -L"$POSTGIS_BUILD_DIR/postgis" \ + -loliphaunt_postgis_deps \ + -rpath '$ORIGIN' \ + -o "$POSTGIS_BUILD_DIR/postgis/postgis-3.so" + # PostGIS core upgrade SQL still includes raster-unpackage stubs even when + # raster support is disabled. Generate those SQL inputs as a best-effort + # prerequisite before packaging PostGIS. Keep this serial: the + # PostGIS SQL Makefiles use generated .tmp files and have incomplete + # parallel dependency edges. + make -s -j1 raster-sql || true + make -s -j1 -C raster/rt_pg sql_objs + make -s -j1 -C extensions postgis_extension_helper.sql + make -s -j1 -C extensions/postgis postgis.control all + mkdir -p "$POSTGIS_BUILD_DIR/share" + rm -rf "$POSTGIS_BUILD_DIR/share/proj" + cp -R "$PROJ_PREFIX/share/proj" "$POSTGIS_BUILD_DIR/share/proj" +} >&2 + +oliphaunt_wasix_extension_build_outputs_exist "$REPO_ROOT" postgis "$POSTGIS_BUILD_DIR" +printf '%s\n' "$stamp" > "$POSTGIS_BUILD_DIR/.oliphaunt-wasix-postgis-build" +echo "$POSTGIS_BUILD_DIR" diff --git a/src/extensions/external/vector/.release-semantic-inputs.json b/src/extensions/external/vector/.release-semantic-inputs.json new file mode 100644 index 00000000..223d6865 --- /dev/null +++ b/src/extensions/external/vector/.release-semantic-inputs.json @@ -0,0 +1,265 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-extension-vector", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "extension-artifact-carrier-producers", + "paths": [ + "src/extensions/artifacts/native/tools/**", + "src/extensions/artifacts/packages/tools/**", + "src/extensions/artifacts/wasix/tools/**", + "src/sdks/rust/extension-artifact-archive-policy.properties", + "tools/release/bounded-gunzip-to-file.mjs", + "tools/release/build-extension-ci-artifacts.mjs", + "tools/release/extension-artifact-archive-policy.mjs", + "tools/release/extension-artifact-inventory.mjs", + "tools/release/extension-registry-carrier-materializer.mjs", + "tools/release/extension-registry-packages.mjs", + "tools/release/extension-runtime-asset-contract.mjs", + "tools/release/npm-trusted-publishing.mjs", + "tools/release/package-extension-cargo-facades.mjs" + ], + "inputs": [ + { + "path": "src/extensions/artifacts/native/tools/check-release-artifacts.sh", + "sha256": "fb373917ccf7646131061c9d7e07c0de003e6bd20f026b4935a86c8bb1840846" + }, + { + "path": "src/extensions/artifacts/native/tools/extension-artifact-packager.mjs", + "sha256": "8324ff71e67fb658709bb54a7df0cac94e03cd9156fbcae0db512272210b7916" + }, + { + "path": "src/extensions/artifacts/native/tools/package-release-assets.sh", + "sha256": "74ead9a4b5854f02b4f85f1c0a65861403d658176736ee7d0f05abc3653bafa2" + }, + { + "path": "src/extensions/artifacts/native/tools/stage-windows-binary-contract.mjs", + "sha256": "d719ea6994193915e1eade038ebe02869826f8b51109d90f7570f3b8c24a3f59" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "sha256": "3ac0465833b5d93554385b4f86c82c836acc728ab2167243d7fb237862952ef9" + }, + { + "path": "src/extensions/artifacts/packages/tools/package-release-assets.sh", + "sha256": "62be302e4325b171b0ad8433eca0ad878e4ffd3f19f91fcc785861b5ff855199" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.mjs", + "sha256": "e9885ed6cc12984b64fabe51e0fa68094277aba54f2b00ea0d83b12867ade7c3" + }, + { + "path": "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "sha256": "9cf6944bd958fcfb624e06e3b8e55135dcf1f872443cb909153c6551c058b0c3" + }, + { + "path": "src/sdks/rust/extension-artifact-archive-policy.properties", + "sha256": "e708d2a5749cdcbca9de2abe44abdf36b5faaa5f9eb010edc412496876d955fa" + }, + { + "path": "tools/release/bounded-gunzip-to-file.mjs", + "sha256": "e7b3683aced134c9b346ef2cfd398ab9af1d6d9a65819ead233ca49dbea72ed7" + }, + { + "path": "tools/release/build-extension-ci-artifacts.mjs", + "sha256": "c79912531e264220d9e7497a10f1cd759a892878bf306d01f388deda61bc55ef" + }, + { + "path": "tools/release/extension-artifact-archive-policy.mjs", + "sha256": "1cbacb741cbab94a561b761598aad3c951236123c0365949d2646c80def568f9" + }, + { + "path": "tools/release/extension-artifact-inventory.mjs", + "sha256": "605bd86ef475b6fd3ac91e7d74ddd61b2024d8e3441b35cf48c2a057aed83ce6" + }, + { + "path": "tools/release/extension-registry-carrier-materializer.mjs", + "sha256": "c48d49df1eb124b9183674d8e7f2a0eebeca6ea975df7cad00609fac48dde0fd" + }, + { + "path": "tools/release/extension-registry-packages.mjs", + "sha256": "304cb2e5610ba25c501896845ddd861093a6635d2e65b1225e2d909259abd341" + }, + { + "path": "tools/release/extension-runtime-asset-contract.mjs", + "sha256": "37658ad5bb46c2e88817cb0504bc7d927e883e3643a91ba9ef5d010408188eef" + }, + { + "path": "tools/release/npm-trusted-publishing.mjs", + "sha256": "07facc639082a935dee5d8d558bdff678446a9dd81a462900e55b3275d654a9e" + }, + { + "path": "tools/release/package-extension-cargo-facades.mjs", + "sha256": "946811a2afbd8524c21aea42698ba24ef8247587357ccc01dfbcd79f5c1cffd3" + } + ] + }, + { + "id": "apple-carrier-manifest-producer", + "paths": [ + "tools/release/ios-carrier-manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/ios-carrier-manifest.mjs", + "sha256": "849cdd05b9410b313dd573ce7c7f61dbbe1d6738bd071a1c7052918a85805d1d" + } + ] + }, + { + "id": "extension-kotlin-runtime-catalog", + "paths": [ + "src/extensions/generated/sdk/kotlin.json" + ], + "inputs": [ + { + "path": "src/extensions/generated/sdk/kotlin.json", + "sha256": "7cc0df19dab8949ebe7215d48a4053aac24c553c6545453d939b5d4831454187" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "wasix-runtime-extension-cargo-carrier-producer", + "paths": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "sha256": "cc6c7d782470cb6f19473f2940cfa3cbbc14711dcd4f8ea9a4011f33440cad75" + } + ] + }, + { + "id": "shared-cargo-source-archive-producer", + "paths": [ + "tools/release/cargo-source-package.mjs" + ], + "inputs": [ + { + "path": "tools/release/cargo-source-package.mjs", + "sha256": "0dbb3331a9ab8a7968626317b2c1249ce05e85265893f43fb6acc0cfd913b3b3" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "52f56f698eb22e65abbb516c75584658eb178818e9ad45c39811ae5441fa22c8" +} diff --git a/src/extensions/external/vector/CHANGELOG.md b/src/extensions/external/vector/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/extensions/external/vector/VERSION b/src/extensions/external/vector/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/extensions/external/vector/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/extensions/external/vector/moon.yml b/src/extensions/external/vector/moon.yml new file mode 100644 index 00000000..d2c361f2 --- /dev/null +++ b/src/extensions/external/vector/moon.yml @@ -0,0 +1,54 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-extension-vector" +language: "unknown" +layer: "library" +stack: "systems" +tags: ["extensions", "external", "vector", "release-product"] +dependsOn: + - "extension-runtime-contract" + - id: "liboliphaunt-native" + scope: "build" + - id: "liboliphaunt-wasix" + scope: "build" + +project: + release: + component: "oliphaunt-extension-vector" + packagePath: "src/extensions/external/vector" + +tasks: + check: + tags: ["quality", "static"] + 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.mjs" + - "/src/shared/extension-runtime-contract/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + assemble-release: + tags: ["release", "artifact-package"] + 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: + - "/.release-please-manifest.json" + - "/release-please-config.json" + - "/src/extensions/external/vector/**/*" + - "/src/extensions/generated/wasix/extensions.json" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/extension-runtime-asset-contract.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/target/extensions/native/release-assets/**/*" + - "/target/extensions/wasix/release-assets/**/*" + outputs: + - "/target/extension-artifacts/oliphaunt-extension-vector/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/extensions/external/vector/release.toml b/src/extensions/external/vector/release.toml new file mode 100644 index 00000000..7e024a73 --- /dev/null +++ b/src/extensions/external/vector/release.toml @@ -0,0 +1,55 @@ +id = "oliphaunt-extension-vector" +owner = "@oliphaunt/extensions" +kind = "exact-extension-artifact" +publish_targets = [ + "github-release-assets", + "npm", + "maven-central", + "crates-io", +] +registry_packages = [ + "crates:oliphaunt-extension-vector", + "crates:oliphaunt-extension-vector-aot-linux-arm64", + "crates:oliphaunt-extension-vector-aot-linux-x64", + "crates:oliphaunt-extension-vector-aot-macos-arm64", + "crates:oliphaunt-extension-vector-aot-windows-x64", + "crates:oliphaunt-extension-vector-linux-arm64-gnu", + "crates:oliphaunt-extension-vector-linux-x64-gnu", + "crates:oliphaunt-extension-vector-macos-arm64", + "crates:oliphaunt-extension-vector-wasix", + "crates:oliphaunt-extension-vector-windows-x64-msvc", + "npm:@oliphaunt/extension-vector", + "npm:@oliphaunt/extension-vector-linux-arm64-gnu", + "npm:@oliphaunt/extension-vector-linux-x64-gnu", + "npm:@oliphaunt/extension-vector-macos-arm64", + "npm:@oliphaunt/extension-vector-windows-x64-msvc", + "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" + +[extension] +sql_name = "vector" +class = "external" +versioning = "upstream-bound" + +[extension.source] +path = "src/extensions/external/vector/source.toml" + +[extension.compatibility] +postgres_major = "18" +extension_runtime_contract = "src/shared/extension-runtime-contract/contract.toml" +native_runtime_product = "liboliphaunt-native" +native_runtime_version = "0.0.0" +wasix_runtime_product = "liboliphaunt-wasix" +wasix_runtime_version = "0.0.0" +[compatibility_versions.oliphaunt-extension-vector-liboliphaunt-native] +source_product = "liboliphaunt-native" +path = "src/extensions/external/vector/release.toml" +parser = "toml:extension.compatibility.native_runtime_version" + +[compatibility_versions.oliphaunt-extension-vector-liboliphaunt-wasix] +source_product = "liboliphaunt-wasix" +path = "src/extensions/external/vector/release.toml" +parser = "toml:extension.compatibility.wasix_runtime_version" diff --git a/src/extensions/external/vector/source.toml b/src/extensions/external/vector/source.toml new file mode 100644 index 00000000..9793cfe0 --- /dev/null +++ b/src/extensions/external/vector/source.toml @@ -0,0 +1,9 @@ +name = "pgvector" +url = "https://github.com/pgvector/pgvector.git" +branch = "master" +commit = "d238409becebb8172fe696ffa776badfad4b631c" + +[extension-control] +sql-name = "vector" +source-path = "vector.control" +default-version = "0.8.2" diff --git a/src/extensions/external/vector/targets/artifacts.toml b/src/extensions/external/vector/targets/artifacts.toml new file mode 100644 index 00000000..9d969294 --- /dev/null +++ b/src/extensions/external/vector/targets/artifacts.toml @@ -0,0 +1,14 @@ +schema = "oliphaunt-extension-artifact-targets-v1" +profiles = ["native-desktop-v1", "native-mobile-v1", "wasix-portable-v1"] + +[evidence.native-desktop-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + tools/release/check-staged-artifacts.mjs" + +[evidence.native-mobile-v1] +kind = "build-package-and-install" +reference = "src/extensions/artifacts/native/tools/package-release-assets.sh + src/sdks/react-native/tools/mobile-e2e.sh" + +[evidence.wasix-portable-v1] +kind = "build-and-lifecycle-smoke" +reference = "src/extensions/evidence/matrix.toml + src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh" diff --git a/assets/generated/contrib-build.tsv b/src/extensions/generated/contrib-build.tsv similarity index 93% rename from assets/generated/contrib-build.tsv rename to src/extensions/generated/contrib-build.tsv index fd3fab14..02fe670c 100644 --- a/assets/generated/contrib-build.tsv +++ b/src/extensions/generated/contrib-build.tsv @@ -23,9 +23,11 @@ pg_surgery pg_surgery pg_surgery pg_surgery.so extensions/pg_surgery.tar.zst tru pg_trgm pg_trgm pg_trgm pg_trgm.so extensions/pg_trgm.tar.zst true pg_visibility pg_visibility pg_visibility pg_visibility.so extensions/pg_visibility.tar.zst true pg_walinspect pg_walinspect pg_walinspect pg_walinspect.so extensions/pg_walinspect.tar.zst true +pgcrypto pgcrypto pgcrypto pgcrypto.so extensions/pgcrypto.tar.zst true seg seg seg seg.so extensions/seg.tar.zst true tablefunc tablefunc tablefunc tablefunc.so extensions/tablefunc.tar.zst true tcn tcn tcn tcn.so extensions/tcn.tar.zst true tsm_system_rows tsm_system_rows tsm_system_rows tsm_system_rows.so extensions/tsm_system_rows.tar.zst true tsm_system_time tsm_system_time tsm_system_time tsm_system_time.so extensions/tsm_system_time.tar.zst true unaccent unaccent unaccent unaccent.so extensions/unaccent.tar.zst true +uuid_ossp uuid-ossp uuid-ossp uuid-ossp.so extensions/uuid-ossp.tar.zst true diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json new file mode 100644 index 00000000..daf3dec9 --- /dev/null +++ b/src/extensions/generated/docs/extension-evidence.json @@ -0,0 +1,1410 @@ +{ + "claims": [ + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "amcheck", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "amcheck" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "auto_explain", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "auto_explain" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "bloom", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "bloom" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "btree_gin", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "btree_gin" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "btree_gist", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "btree_gist" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "citext", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "citext" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "cube", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "cube" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "dict_int", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "dict_int" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "dict_xsyn", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "dict_xsyn" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "earthdistance", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "earthdistance" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "file_fdw", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "file_fdw" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "fuzzystrmatch", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "fuzzystrmatch" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "hstore", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "hstore" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "intarray", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "intarray" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "isn", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "isn" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "lo", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "lo" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "ltree", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "ltree" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pageinspect", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pageinspect" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_buffercache", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_buffercache" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_freespacemap", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_freespacemap" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_hashids", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_hashids" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_ivm", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_ivm" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_surgery", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_surgery" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_textsearch", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_textsearch" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_trgm", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_trgm" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_uuidv7", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_uuidv7" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_visibility", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_visibility" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pg_walinspect", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pg_walinspect" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pgcrypto", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pgcrypto" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "pgtap", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "pgtap" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "postgis", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "postgis" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "seg", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "seg" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "tablefunc", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "tablefunc" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "tcn", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "tcn" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "tsm_system_rows", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "tsm_system_rows" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "tsm_system_time", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "tsm_system_time" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "unaccent", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "unaccent" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "uuid_ossp", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "uuid-ossp" + }, + { + "artifact-family": "wasix-runtime", + "evidence-required": [ + "wasix-full-lifecycle-v1" + ], + "extension": "vector", + "latest-accepted-evidence": [], + "missing-current-evidence": [ + { + "artifact-family": "wasix-runtime", + "evidence-tier": "wasix-full-lifecycle-v1", + "platform-target": "portable", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ] + } + ], + "platform-targets": [ + "portable" + ], + "postgres-major": 18, + "public": true, + "qualification-status": "requires-exact-candidate-ci", + "runtime-modes": [ + "direct", + "server", + "restart", + "dump-restore" + ], + "sql-name": "vector" + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "evidence-matrix", + "path": "src/extensions/evidence/matrix.toml" + }, + { + "name": "evidence-runs", + "path": "src/extensions/evidence/runs" + } + ], + "qualification-authority": { + "collector": "src/extensions/tools/collect-wasix-evidence.sh", + "current-source-status": "requires-exact-candidate-ci", + "kind": "exact-sha-ci" + }, + "source-digest": "sha256:2fea0868333e011c3790e19ced69f995cc3e9fbf58c4b065f213f0a9f03d6920", + "source-digest-inputs": [ + "src/postgres/versions/18/source.toml", + "src/extensions/catalog/extensions.promoted.toml", + "src/extensions/catalog/extensions.smoke.toml", + "src/extensions/catalog/extensions.source.json", + "src/extensions/contrib/postgres18.toml", + "src/extensions/generated/extensions.catalog.json", + "src/extensions/generated/extensions.build-plan.json", + "src/extensions/generated/contrib-build.tsv", + "src/extensions/generated/pgxs-build.tsv", + "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", + "src/extensions/external/age/source.toml", + "src/extensions/external/pg_hashids/source.toml", + "src/extensions/external/pg_ivm/source.toml", + "src/extensions/external/pg_textsearch/source.toml", + "src/extensions/external/pg_uuidv7/source.toml", + "src/extensions/external/pgtap/source.toml", + "src/extensions/external/postgis/dependencies/geos/source.toml", + "src/extensions/external/postgis/dependencies/json-c/source.toml", + "src/extensions/external/postgis/dependencies/libiconv/source.toml", + "src/extensions/external/postgis/dependencies/libxml2/source.toml", + "src/extensions/external/postgis/dependencies/proj/source.toml", + "src/extensions/external/postgis/dependencies/sqlite/source.toml", + "src/extensions/external/postgis/source.toml", + "src/extensions/external/vector/source.toml", + "src/sources/third-party/shared/icu.toml", + "src/sources/third-party/shared/openssl.toml", + "src/extensions/external/README.md", + "src/extensions/external/age/moon.yml", + "src/extensions/external/pg_hashids/moon.yml", + "src/extensions/external/pg_ivm/moon.yml", + "src/extensions/external/pg_ivm/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/moon.yml", + "src/extensions/external/pg_textsearch/recipe.toml", + "src/extensions/external/pg_textsearch/targets/native-static-registry.toml", + "src/extensions/external/pg_textsearch/tests/smoke.sql", + "src/extensions/external/pg_textsearch/tests/upstream.toml", + "src/extensions/external/pg_uuidv7/moon.yml", + "src/extensions/external/pgtap/moon.yml", + "src/extensions/external/pgtap/recipe.toml", + "src/extensions/external/pgtap/targets/native-static-registry.toml", + "src/extensions/external/pgtap/targets/native.toml", + "src/extensions/external/pgtap/targets/wasix.toml", + "src/extensions/external/pgtap/tests/smoke.sql", + "src/extensions/external/pgtap/tests/upstream.toml", + "src/extensions/external/postgis/blockers.toml", + "src/extensions/external/postgis/deps.toml", + "src/extensions/external/postgis/moon.yml", + "src/extensions/external/postgis/patches/README.md", + "src/extensions/external/postgis/recipe.toml", + "src/extensions/external/postgis/targets/native-static-registry.toml", + "src/extensions/external/postgis/targets/native.toml", + "src/extensions/external/postgis/targets/wasix.toml", + "src/extensions/external/postgis/tests/regression.sql", + "src/extensions/external/postgis/tests/smoke.sql", + "src/extensions/external/postgis/tests/upstream.toml", + "src/extensions/external/postgis/tools/build_wasix.sh", + "src/extensions/external/vector/moon.yml" + ] +} diff --git a/src/extensions/generated/docs/extensions.json b/src/extensions/generated/docs/extensions.json new file mode 100644 index 00000000..261d98d8 --- /dev/null +++ b/src/extensions/generated/docs/extensions.json @@ -0,0 +1,1358 @@ +{ + "extensions": [ + { + "activation": "CREATE EXTENSION + LOAD", + "archive": "extensions/age.tar.zst", + "blocker": "Apache AGE does not currently build against PostgreSQL 18.4 in the WASIX lane; ag_label.c still calls ExecInitExtraTupleSlot, which is not available in PG18. Keep graph/pgGraph out of release artifacts until there is an official PG18-compatible upstream.", + "dependencies": [], + "desktop-release-ready": false, + "display-name": "Apache AGE", + "family": "External PGXS", + "id": "age", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": false, + "preload-required": false, + "promoted": false, + "public": false, + "restart-required": false, + "smoke": { + "direct": "not-run", + "dump-restore": "not-run", + "restart": "not-run", + "server": "not-run" + }, + "sql-name": "age", + "stable": false, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.7.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/amcheck.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "family": "PostgreSQL contrib", + "id": "amcheck", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.5" + }, + { + "activation": "LOAD", + "archive": "extensions/auto_explain.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "family": "PostgreSQL contrib", + "id": "auto_explain", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/bloom.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "family": "PostgreSQL contrib", + "id": "bloom", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/btree_gin.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "family": "PostgreSQL contrib", + "id": "btree_gin", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/btree_gist.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "family": "PostgreSQL contrib", + "id": "btree_gist", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.8" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/citext.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "family": "PostgreSQL contrib", + "id": "citext", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.8" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/cube.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "family": "PostgreSQL contrib", + "id": "cube", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.5" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/dict_int.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "family": "PostgreSQL contrib", + "id": "dict_int", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/dict_xsyn.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "family": "PostgreSQL contrib", + "id": "dict_xsyn", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/earthdistance.tar.zst", + "blocker": "", + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "family": "PostgreSQL contrib", + "id": "earthdistance", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.2" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/file_fdw.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "family": "PostgreSQL contrib", + "id": "file_fdw", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/fuzzystrmatch.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "family": "PostgreSQL contrib", + "id": "fuzzystrmatch", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.2" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/hstore.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "family": "PostgreSQL contrib", + "id": "hstore", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.8" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/intarray.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "family": "PostgreSQL contrib", + "id": "intarray", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.5" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/isn.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "family": "PostgreSQL contrib", + "id": "isn", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/lo.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "family": "PostgreSQL contrib", + "id": "lo", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.2" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/ltree.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "family": "PostgreSQL contrib", + "id": "ltree", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pageinspect.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "family": "PostgreSQL contrib", + "id": "pageinspect", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.13" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_buffercache.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "family": "PostgreSQL contrib", + "id": "pg_buffercache", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.6" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_freespacemap.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "family": "PostgreSQL contrib", + "id": "pg_freespacemap", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_hashids.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "family": "External PGXS", + "id": "pg_hashids", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_ivm.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "family": "External PGXS", + "id": "pg_ivm", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.13" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_surgery.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "family": "PostgreSQL contrib", + "id": "pg_surgery", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_textsearch.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "family": "External PGXS", + "id": "pg_textsearch", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "0.5.1" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_trgm.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "family": "PostgreSQL contrib", + "id": "pg_trgm", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.6" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_uuidv7.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "family": "External PGXS", + "id": "pg_uuidv7", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.7" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_visibility.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "family": "PostgreSQL contrib", + "id": "pg_visibility", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.2" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pg_walinspect.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "family": "PostgreSQL contrib", + "id": "pg_walinspect", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.1" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pgcrypto.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "family": "PostgreSQL contrib", + "id": "pgcrypto", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.4" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/pgtap.tar.zst", + "blocker": "", + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "family": "External PGXS", + "id": "pgtap", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + }, + "version": "1.3.5" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/postgis.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "family": "Complex external", + "id": "postgis", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + }, + "version": "3.6.3" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/seg.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "family": "PostgreSQL contrib", + "id": "seg", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.4" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/tablefunc.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "family": "PostgreSQL contrib", + "id": "tablefunc", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/tcn.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "family": "PostgreSQL contrib", + "id": "tcn", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/tsm_system_rows.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "family": "PostgreSQL contrib", + "id": "tsm_system_rows", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/tsm_system_time.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "family": "PostgreSQL contrib", + "id": "tsm_system_time", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.0" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/unaccent.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "family": "PostgreSQL contrib", + "id": "unaccent", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.1" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/uuid-ossp.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "family": "PostgreSQL contrib", + "id": "uuid_ossp", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "1.1" + }, + { + "activation": "CREATE EXTENSION", + "archive": "extensions/vector.tar.zst", + "blocker": "", + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "family": "External PGXS", + "id": "vector", + "mobile-release-ready": true, + "native-dependencies": [], + "packaged": true, + "preload-required": false, + "promoted": true, + "public": true, + "restart-required": false, + "smoke": { + "direct": "passed", + "dump-restore": "passed", + "restart": "passed", + "server": "passed" + }, + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + }, + "version": "0.8.2" + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-build-plan", + "path": "src/extensions/generated/extensions.build-plan.json" + }, + { + "name": "promotion-config", + "path": "src/extensions/catalog/extensions.promoted.toml" + }, + { + "name": "smoke-evidence", + "path": "src/extensions/catalog/extensions.smoke.toml" + } + ] +} diff --git a/src/extensions/generated/extensions.build-plan.json b/src/extensions/generated/extensions.build-plan.json new file mode 100644 index 00000000..0d67dbdc --- /dev/null +++ b/src/extensions/generated/extensions.build-plan.json @@ -0,0 +1,1429 @@ +{ + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "promotion-config", + "path": "src/extensions/catalog/extensions.promoted.toml" + }, + { + "name": "asset-manifest-evidence", + "path": "target/oliphaunt-wasix/assets/manifest.json" + } + ], + "extensions": [ + { + "id": "amcheck", + "sql-name": "amcheck", + "display-name": "amcheck", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/amcheck", + "contrib-dir": "amcheck", + "module-file": "amcheck.so", + "archive": "extensions/amcheck.tar.zst", + "control-file": "src/postgres/versions/18/contrib/amcheck/amcheck.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/amcheck.test.js" + ] + }, + { + "id": "auto_explain", + "sql-name": "auto_explain", + "display-name": "auto_explain", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/auto_explain", + "contrib-dir": "auto_explain", + "module-file": "auto_explain.so", + "archive": "extensions/auto_explain.tar.zst", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/auto_explain.test.js" + ] + }, + { + "id": "bloom", + "sql-name": "bloom", + "display-name": "bloom", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/bloom", + "contrib-dir": "bloom", + "module-file": "bloom.so", + "archive": "extensions/bloom.tar.zst", + "control-file": "src/postgres/versions/18/contrib/bloom/bloom.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/bloom.test.js" + ] + }, + { + "id": "btree_gin", + "sql-name": "btree_gin", + "display-name": "btree_gin", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/btree_gin", + "contrib-dir": "btree_gin", + "module-file": "btree_gin.so", + "archive": "extensions/btree_gin.tar.zst", + "control-file": "src/postgres/versions/18/contrib/btree_gin/btree_gin.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gin.test.js" + ] + }, + { + "id": "btree_gist", + "sql-name": "btree_gist", + "display-name": "btree_gist", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/btree_gist", + "contrib-dir": "btree_gist", + "module-file": "btree_gist.so", + "archive": "extensions/btree_gist.tar.zst", + "control-file": "src/postgres/versions/18/contrib/btree_gist/btree_gist.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gist.test.js" + ] + }, + { + "id": "citext", + "sql-name": "citext", + "display-name": "citext", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/citext", + "contrib-dir": "citext", + "module-file": "citext.so", + "archive": "extensions/citext.tar.zst", + "control-file": "src/postgres/versions/18/contrib/citext/citext.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/citext.test.js" + ] + }, + { + "id": "cube", + "sql-name": "cube", + "display-name": "cube", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/cube", + "contrib-dir": "cube", + "module-file": "cube.so", + "archive": "extensions/cube.tar.zst", + "control-file": "src/postgres/versions/18/contrib/cube/cube.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/cube.test.js" + ] + }, + { + "id": "dict_int", + "sql-name": "dict_int", + "display-name": "dict_int", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/dict_int", + "contrib-dir": "dict_int", + "module-file": "dict_int.so", + "archive": "extensions/dict_int.tar.zst", + "control-file": "src/postgres/versions/18/contrib/dict_int/dict_int.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_int.test.js" + ] + }, + { + "id": "dict_xsyn", + "sql-name": "dict_xsyn", + "display-name": "dict_xsyn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/dict_xsyn", + "contrib-dir": "dict_xsyn", + "module-file": "dict_xsyn.so", + "archive": "extensions/dict_xsyn.tar.zst", + "control-file": "src/postgres/versions/18/contrib/dict_xsyn/dict_xsyn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_xsyn.test.ts" + ] + }, + { + "id": "earthdistance", + "sql-name": "earthdistance", + "display-name": "earthdistance", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/earthdistance", + "contrib-dir": "earthdistance", + "module-file": "earthdistance.so", + "archive": "extensions/earthdistance.tar.zst", + "control-file": "src/postgres/versions/18/contrib/earthdistance/earthdistance.control", + "stable": true, + "dependencies": [ + "cube" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/earthdistance.test.js" + ] + }, + { + "id": "file_fdw", + "sql-name": "file_fdw", + "display-name": "file_fdw", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/file_fdw", + "contrib-dir": "file_fdw", + "module-file": "file_fdw.so", + "archive": "extensions/file_fdw.tar.zst", + "control-file": "src/postgres/versions/18/contrib/file_fdw/file_fdw.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/file_fdw.test.js" + ] + }, + { + "id": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "display-name": "fuzzystrmatch", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/fuzzystrmatch", + "contrib-dir": "fuzzystrmatch", + "module-file": "fuzzystrmatch.so", + "archive": "extensions/fuzzystrmatch.tar.zst", + "control-file": "src/postgres/versions/18/contrib/fuzzystrmatch/fuzzystrmatch.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/fuzzystrmatch.test.js" + ] + }, + { + "id": "hstore", + "sql-name": "hstore", + "display-name": "hstore", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/hstore", + "contrib-dir": "hstore", + "module-file": "hstore.so", + "archive": "extensions/hstore.tar.zst", + "control-file": "src/postgres/versions/18/contrib/hstore/hstore.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/hstore.test.js" + ] + }, + { + "id": "intarray", + "sql-name": "intarray", + "display-name": "intarray", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/intarray", + "contrib-dir": "intarray", + "module-file": "_int.so", + "archive": "extensions/intarray.tar.zst", + "control-file": "src/postgres/versions/18/contrib/intarray/intarray.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/intarray.test.js" + ] + }, + { + "id": "isn", + "sql-name": "isn", + "display-name": "isn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/isn", + "contrib-dir": "isn", + "module-file": "isn.so", + "archive": "extensions/isn.tar.zst", + "control-file": "src/postgres/versions/18/contrib/isn/isn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/isn.test.js" + ] + }, + { + "id": "lo", + "sql-name": "lo", + "display-name": "lo", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/lo", + "contrib-dir": "lo", + "module-file": "lo.so", + "archive": "extensions/lo.tar.zst", + "control-file": "src/postgres/versions/18/contrib/lo/lo.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/lo.test.js" + ] + }, + { + "id": "ltree", + "sql-name": "ltree", + "display-name": "ltree", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/ltree", + "contrib-dir": "ltree", + "module-file": "ltree.so", + "archive": "extensions/ltree.tar.zst", + "control-file": "src/postgres/versions/18/contrib/ltree/ltree.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/ltree.test.js" + ] + }, + { + "id": "pageinspect", + "sql-name": "pageinspect", + "display-name": "pageinspect", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pageinspect", + "contrib-dir": "pageinspect", + "module-file": "pageinspect.so", + "archive": "extensions/pageinspect.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pageinspect/pageinspect.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pageinspect.test.js" + ] + }, + { + "id": "pg_buffercache", + "sql-name": "pg_buffercache", + "display-name": "pg_buffercache", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_buffercache", + "contrib-dir": "pg_buffercache", + "module-file": "pg_buffercache.so", + "archive": "extensions/pg_buffercache.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_buffercache/pg_buffercache.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_buffercache.test.js" + ] + }, + { + "id": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "display-name": "pg_freespacemap", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_freespacemap", + "contrib-dir": "pg_freespacemap", + "module-file": "pg_freespacemap.so", + "archive": "extensions/pg_freespacemap.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_freespacemap/pg_freespacemap.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_freespacemap.test.ts" + ] + }, + { + "id": "pg_hashids", + "sql-name": "pg_hashids", + "display-name": "pg_hashids", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-external", + "source-dir": "target/oliphaunt-sources/checkouts/pg_hashids", + "module-file": "pg_hashids.so", + "archive": "extensions/pg_hashids.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pg_hashids/pg_hashids.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_hashids.test.ts" + ] + }, + { + "id": "pg_ivm", + "sql-name": "pg_ivm", + "display-name": "pg_ivm", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-external", + "source-dir": "target/oliphaunt-sources/checkouts/pg_ivm", + "module-file": "pg_ivm.so", + "archive": "extensions/pg_ivm.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pg_ivm/pg_ivm.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_ivm.test.ts" + ] + }, + { + "id": "pg_surgery", + "sql-name": "pg_surgery", + "display-name": "pg_surgery", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_surgery", + "contrib-dir": "pg_surgery", + "module-file": "pg_surgery.so", + "archive": "extensions/pg_surgery.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_surgery/pg_surgery.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_surgery.test.js" + ] + }, + { + "id": "pg_textsearch", + "sql-name": "pg_textsearch", + "display-name": "pg_textsearch", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-external", + "source-dir": "target/oliphaunt-sources/checkouts/pg_textsearch", + "module-file": "pg_textsearch.so", + "archive": "extensions/pg_textsearch.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pg_textsearch/pg_textsearch.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_textsearch.test.ts" + ] + }, + { + "id": "pg_trgm", + "sql-name": "pg_trgm", + "display-name": "pg_trgm", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_trgm", + "contrib-dir": "pg_trgm", + "module-file": "pg_trgm.so", + "archive": "extensions/pg_trgm.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_trgm/pg_trgm.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_trgm.test.js" + ] + }, + { + "id": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "display-name": "pg_uuidv7", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-external", + "source-dir": "target/oliphaunt-sources/checkouts/pg_uuidv7", + "module-file": "pg_uuidv7.so", + "archive": "extensions/pg_uuidv7.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pg_uuidv7/pg_uuidv7.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_uuidv7.test.ts" + ] + }, + { + "id": "pg_visibility", + "sql-name": "pg_visibility", + "display-name": "pg_visibility", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_visibility", + "contrib-dir": "pg_visibility", + "module-file": "pg_visibility.so", + "archive": "extensions/pg_visibility.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_visibility/pg_visibility.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_visibility.test.js" + ] + }, + { + "id": "pg_walinspect", + "sql-name": "pg_walinspect", + "display-name": "pg_walinspect", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pg_walinspect", + "contrib-dir": "pg_walinspect", + "module-file": "pg_walinspect.so", + "archive": "extensions/pg_walinspect.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pg_walinspect/pg_walinspect.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_walinspect.test.js" + ] + }, + { + "id": "pgcrypto", + "sql-name": "pgcrypto", + "display-name": "pgcrypto", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/pgcrypto", + "contrib-dir": "pgcrypto", + "module-file": "pgcrypto.so", + "archive": "extensions/pgcrypto.tar.zst", + "control-file": "src/postgres/versions/18/contrib/pgcrypto/pgcrypto.control", + "stable": true, + "dependencies": [], + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "load-order": [], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pgcrypto.test.js" + ] + }, + { + "id": "pgtap", + "sql-name": "pgtap", + "display-name": "pgtap", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-sql-only", + "source-dir": "target/oliphaunt-sources/checkouts/pgtap", + "archive": "extensions/pgtap.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pgtap/pgtap.control", + "stable": true, + "dependencies": [ + "plpgsql" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgtap.test.ts" + ] + }, + { + "id": "postgis", + "sql-name": "postgis", + "display-name": "PostGIS", + "source-kind": "postgis", + "build-kind": "autotools", + "build-script": "src/extensions/external/postgis/tools/build_wasix.sh", + "required-build-files": [ + "postgis/postgis-3.so", + "postgis/liboliphaunt_postgis_deps.so", + "extensions/postgis/postgis.control", + "share/proj/proj.db" + ], + "required-build-globs": [ + "extensions/postgis/sql/postgis--*.sql" + ], + "source-dir": "target/oliphaunt-sources/checkouts/postgis", + "module-file": "postgis-3.so", + "archive": "extensions/postgis.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/postgis/extensions/postgis/postgis.control.in", + "stable": true, + "dependencies": [], + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-support-modules": [ + { + "name": "postgis_deps", + "runtime-path": "lib/postgresql/liboliphaunt_postgis_deps.so", + "build-path": "postgis/postgis/liboliphaunt_postgis_deps.so", + "aot-file": "postgis_deps-llvm-opta.bin.zst" + } + ], + "excluded-sql-extensions": [ + "address_standardizer", + "address_standardizer_data_us", + "postgis_raster", + "postgis_sfcgal", + "postgis_tiger_geocoder", + "postgis_topology" + ], + "staging": { + "module-source-dir": "postgis/postgis", + "control-source": "postgis/extensions/postgis/postgis.control", + "sql-source-dir": "postgis/extensions/postgis/sql", + "data-dirs": [ + { + "source": "postgis/share/proj", + "destination": "share/proj" + } + ] + }, + "load-order": [ + "lib/postgresql/postgis-3.so" + ], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/postgis/postgis.test.ts" + ] + }, + { + "id": "seg", + "sql-name": "seg", + "display-name": "seg", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/seg", + "contrib-dir": "seg", + "module-file": "seg.so", + "archive": "extensions/seg.tar.zst", + "control-file": "src/postgres/versions/18/contrib/seg/seg.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/seg.test.js" + ] + }, + { + "id": "tablefunc", + "sql-name": "tablefunc", + "display-name": "tablefunc", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/tablefunc", + "contrib-dir": "tablefunc", + "module-file": "tablefunc.so", + "archive": "extensions/tablefunc.tar.zst", + "control-file": "src/postgres/versions/18/contrib/tablefunc/tablefunc.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tablefunc.test.js" + ] + }, + { + "id": "tcn", + "sql-name": "tcn", + "display-name": "tcn", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/tcn", + "contrib-dir": "tcn", + "module-file": "tcn.so", + "archive": "extensions/tcn.tar.zst", + "control-file": "src/postgres/versions/18/contrib/tcn/tcn.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tcn.test.js" + ] + }, + { + "id": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "display-name": "tsm_system_rows", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/tsm_system_rows", + "contrib-dir": "tsm_system_rows", + "module-file": "tsm_system_rows.so", + "archive": "extensions/tsm_system_rows.tar.zst", + "control-file": "src/postgres/versions/18/contrib/tsm_system_rows/tsm_system_rows.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_rows.test.js" + ] + }, + { + "id": "tsm_system_time", + "sql-name": "tsm_system_time", + "display-name": "tsm_system_time", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/tsm_system_time", + "contrib-dir": "tsm_system_time", + "module-file": "tsm_system_time.so", + "archive": "extensions/tsm_system_time.tar.zst", + "control-file": "src/postgres/versions/18/contrib/tsm_system_time/tsm_system_time.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_time.test.js" + ] + }, + { + "id": "unaccent", + "sql-name": "unaccent", + "display-name": "unaccent", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/unaccent", + "contrib-dir": "unaccent", + "module-file": "unaccent.so", + "archive": "extensions/unaccent.tar.zst", + "control-file": "src/postgres/versions/18/contrib/unaccent/unaccent.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/unaccent.test.js" + ] + }, + { + "id": "uuid_ossp", + "sql-name": "uuid-ossp", + "display-name": "uuid-ossp", + "source-kind": "postgres-contrib", + "build-kind": "postgres-contrib", + "source-dir": "src/postgres/versions/18/contrib/uuid-ossp", + "contrib-dir": "uuid-ossp", + "module-file": "uuid-ossp.so", + "archive": "extensions/uuid-ossp.tar.zst", + "control-file": "src/postgres/versions/18/contrib/uuid-ossp/uuid-ossp.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/uuid_ossp.test.js" + ] + }, + { + "id": "vector", + "sql-name": "vector", + "display-name": "pgvector", + "source-kind": "oliphaunt-other-extension", + "build-kind": "pgxs-external", + "source-dir": "target/oliphaunt-sources/checkouts/pgvector", + "module-file": "vector.so", + "archive": "extensions/vector.tar.zst", + "control-file": "target/oliphaunt-sources/checkouts/pgvector/vector.control", + "stable": true, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgvector.test.ts" + ] + } + ] +} diff --git a/src/extensions/generated/extensions.catalog.json b/src/extensions/generated/extensions.catalog.json new file mode 100644 index 00000000..eaf29cb8 --- /dev/null +++ b/src/extensions/generated/extensions.catalog.json @@ -0,0 +1,2207 @@ +{ + "format-version": 1, + "generated-from": [ + { + "name": "postgres18-source", + "path": "src/postgres/versions/18/source.toml" + }, + { + "name": "extension-catalog", + "path": "src/extensions/catalog/extensions.source.json" + }, + { + "name": "postgres-contrib", + "path": "src/extensions/contrib/postgres18.toml" + }, + { + "name": "external-extension-recipes", + "path": "src/extensions/external" + }, + { + "name": "extension-promotion-config", + "path": "src/extensions/catalog/extensions.promoted.toml" + }, + { + "name": "extension-smoke-evidence", + "path": "src/extensions/catalog/extensions.smoke.toml" + }, + { + "name": "asset-manifest-evidence", + "path": "target/oliphaunt-wasix/assets/manifest.json" + } + ], + "extensions": [ + { + "id": "age", + "sql-name": "age", + "rust-constant": "AGE", + "display-name": "Apache AGE", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "age", + "package-export": "./age", + "tags": [ + "postgres extension" + ], + "bundle-size": 141551, + "control-file": "target/oliphaunt-sources/checkouts/age/age.control", + "control": { + "default-version": "1.7.0", + "module-pathname": "$libdir/age", + "requires": [], + "schema": "ag_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "ag_catalog", + "load-sql": [ + "LOAD 'age';" + ], + "post-create-sql": [ + "SET search_path = ag_catalog, \"$user\", public;" + ], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "not-run", + "server": "not-run", + "restart": "not-run", + "dump-restore": "not-run" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/age.test.ts" + ], + "native-module-file": "age.so", + "promotion": { + "configured": true, + "requested": false, + "packaged": false, + "promoted": false, + "stable": false, + "archive": "extensions/age.tar.zst", + "blocker": "Apache AGE does not currently build against PostgreSQL 18.4 in the WASIX lane; ag_label.c still calls ExecInitExtraTupleSlot, which is not available in PG18. Keep graph/pgGraph out of release artifacts until there is an official PG18-compatible upstream." + }, + "notes": [ + "promotion blocker: Apache AGE does not currently build against PostgreSQL 18.4 in the WASIX lane; ag_label.c still calls ExecInitExtraTupleSlot, which is not available in PG18. Keep graph/pgGraph out of release artifacts until there is an official PG18-compatible upstream." + ] + }, + { + "id": "amcheck", + "sql-name": "amcheck", + "rust-constant": "AMCHECK", + "display-name": "amcheck", + "source-kind": "postgres-contrib", + "upstream-import-name": "amcheck", + "package-export": "./contrib/amcheck", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 18815, + "control-file": "src/postgres/versions/18/contrib/amcheck/amcheck.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/amcheck", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/amcheck.test.js" + ], + "native-module-file": "amcheck.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/amcheck.tar.zst", + "module-sha256": "bf278ec30e1bb1abee705a74bed8d73df00ef0f93f0423dc36631fb5dc3ac283" + }, + "notes": [] + }, + { + "id": "auto_explain", + "sql-name": "auto_explain", + "rust-constant": "AUTO_EXPLAIN", + "display-name": "auto_explain", + "source-kind": "postgres-contrib", + "upstream-import-name": "auto_explain", + "package-export": "./contrib/auto_explain", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3125, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/auto_explain.test.js" + ], + "native-module-file": "auto_explain.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/auto_explain.tar.zst", + "module-sha256": "7753c981916345ff17fe1af012c25f1b669a09e2bef0707fd5b9186ebf1c3c63" + }, + "notes": [] + }, + { + "id": "bloom", + "sql-name": "bloom", + "rust-constant": "BLOOM", + "display-name": "bloom", + "source-kind": "postgres-contrib", + "upstream-import-name": "bloom", + "package-export": "./contrib/bloom", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6197, + "control-file": "src/postgres/versions/18/contrib/bloom/bloom.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/bloom", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/bloom.test.js" + ], + "native-module-file": "bloom.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/bloom.tar.zst", + "module-sha256": "c579d5cf88563ede721ccea6c1fed87f8d6f768406e94527e4f45da9592f75d2" + }, + "notes": [] + }, + { + "id": "btree_gin", + "sql-name": "btree_gin", + "rust-constant": "BTREE_GIN", + "display-name": "btree_gin", + "source-kind": "postgres-contrib", + "upstream-import-name": "btree_gin", + "package-export": "./contrib/btree_gin", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 6347, + "control-file": "src/postgres/versions/18/contrib/btree_gin/btree_gin.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/btree_gin", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gin.test.js" + ], + "native-module-file": "btree_gin.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/btree_gin.tar.zst", + "module-sha256": "5a968545bfaee911c613c942334ea2a3a33d35cff9c4b36b3f7c2f328680c0d1" + }, + "notes": [] + }, + { + "id": "btree_gist", + "sql-name": "btree_gist", + "rust-constant": "BTREE_GIST", + "display-name": "btree_gist", + "source-kind": "postgres-contrib", + "upstream-import-name": "btree_gist", + "package-export": "./contrib/btree_gist", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 24181, + "control-file": "src/postgres/versions/18/contrib/btree_gist/btree_gist.control", + "control": { + "default-version": "1.8", + "module-pathname": "$libdir/btree_gist", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/btree_gist.test.js" + ], + "native-module-file": "btree_gist.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/btree_gist.tar.zst", + "module-sha256": "76680a643f9634d57baa1b653ea31758d9bb20fd689cf2675879f2e4a87d81e5" + }, + "notes": [] + }, + { + "id": "citext", + "sql-name": "citext", + "rust-constant": "CITEXT", + "display-name": "citext", + "source-kind": "postgres-contrib", + "upstream-import-name": "citext", + "package-export": "./contrib/citext", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4983, + "control-file": "src/postgres/versions/18/contrib/citext/citext.control", + "control": { + "default-version": "1.8", + "module-pathname": "$libdir/citext", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/citext.test.js" + ], + "native-module-file": "citext.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/citext.tar.zst", + "module-sha256": "6153d92b5256e998a1fb86b0308e78f720727fc273b678b5886c809e96469a59" + }, + "notes": [] + }, + { + "id": "cube", + "sql-name": "cube", + "rust-constant": "CUBE", + "display-name": "cube", + "source-kind": "postgres-contrib", + "upstream-import-name": "cube", + "package-export": "./contrib/cube", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15104, + "control-file": "src/postgres/versions/18/contrib/cube/cube.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/cube", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/cube.test.js" + ], + "native-module-file": "cube.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/cube.tar.zst", + "module-sha256": "23a003d739659616a7f79a4b119a24cdfd2cb55eca10779ba8e1d2a4619201c7" + }, + "notes": [] + }, + { + "id": "dict_int", + "sql-name": "dict_int", + "rust-constant": "DICT_INT", + "display-name": "dict_int", + "source-kind": "postgres-contrib", + "upstream-import-name": "dict_int", + "package-export": "./contrib/dict_int", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1361, + "control-file": "src/postgres/versions/18/contrib/dict_int/dict_int.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/dict_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_int.test.js" + ], + "native-module-file": "dict_int.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/dict_int.tar.zst", + "module-sha256": "a7c56355a069fe86de74c225d35a080f031cc9dd01a32919431fb90d62a59da1" + }, + "notes": [] + }, + { + "id": "dict_xsyn", + "sql-name": "dict_xsyn", + "rust-constant": "DICT_XSYN", + "display-name": "dict_xsyn", + "source-kind": "postgres-contrib", + "upstream-import-name": "dict_xsyn", + "package-export": "./contrib/dict_xsyn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1948, + "control-file": "src/postgres/versions/18/contrib/dict_xsyn/dict_xsyn.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/dict_xsyn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/dict_xsyn.test.ts" + ], + "native-module-file": "dict_xsyn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/dict_xsyn.tar.zst", + "module-sha256": "c7868984e329691c3135c2d1bc0c538bcdb3a04733a1f2ac08ce9f85d5bbaf5d" + }, + "notes": [] + }, + { + "id": "earthdistance", + "sql-name": "earthdistance", + "rust-constant": "EARTHDISTANCE", + "display-name": "earthdistance", + "source-kind": "postgres-contrib", + "upstream-import-name": "earthdistance", + "package-export": "./contrib/earthdistance", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2220, + "control-file": "src/postgres/versions/18/contrib/earthdistance/earthdistance.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/earthdistance", + "requires": [ + "cube" + ], + "relocatable": "true" + }, + "dependencies": [ + "cube" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/earthdistance.test.js" + ], + "native-module-file": "earthdistance.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/earthdistance.tar.zst", + "module-sha256": "bc3ac31e73fc60f6d651b7b6c11ab6f33ca1f9a28943d889e11c59d3122f39f8" + }, + "notes": [] + }, + { + "id": "file_fdw", + "sql-name": "file_fdw", + "rust-constant": "FILE_FDW", + "display-name": "file_fdw", + "source-kind": "postgres-contrib", + "upstream-import-name": "file_fdw", + "package-export": "./contrib/file_fdw", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4467, + "control-file": "src/postgres/versions/18/contrib/file_fdw/file_fdw.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/file_fdw", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/file_fdw.test.js" + ], + "native-module-file": "file_fdw.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/file_fdw.tar.zst", + "module-sha256": "76408a7f107282951b0068638cd8739f6e065d2b7941becbc02e505e7f37a247" + }, + "notes": [] + }, + { + "id": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "rust-constant": "FUZZYSTRMATCH", + "display-name": "fuzzystrmatch", + "source-kind": "postgres-contrib", + "upstream-import-name": "fuzzystrmatch", + "package-export": "./contrib/fuzzystrmatch", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 12026, + "control-file": "src/postgres/versions/18/contrib/fuzzystrmatch/fuzzystrmatch.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/fuzzystrmatch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/fuzzystrmatch.test.js" + ], + "native-module-file": "fuzzystrmatch.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/fuzzystrmatch.tar.zst", + "module-sha256": "e1efa7136ae9845f34ef6079f124d305959af74233dc51f756ce21ba8f506959" + }, + "notes": [] + }, + { + "id": "hstore", + "sql-name": "hstore", + "rust-constant": "HSTORE", + "display-name": "hstore", + "source-kind": "postgres-contrib", + "upstream-import-name": "hstore", + "package-export": "./contrib/hstore", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 21380, + "control-file": "src/postgres/versions/18/contrib/hstore/hstore.control", + "control": { + "default-version": "1.8", + "module-pathname": "$libdir/hstore", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/hstore.test.js" + ], + "native-module-file": "hstore.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/hstore.tar.zst", + "module-sha256": "a78967a5a7c956720bc5498de4e3208de3c270df0951b6b25ff38eb3ce389fef" + }, + "notes": [] + }, + { + "id": "intarray", + "sql-name": "intarray", + "rust-constant": "INTARRAY", + "display-name": "intarray", + "source-kind": "postgres-contrib", + "upstream-import-name": "intarray", + "package-export": "./contrib/intarray", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 14712, + "control-file": "src/postgres/versions/18/contrib/intarray/intarray.control", + "control": { + "default-version": "1.5", + "module-pathname": "$libdir/_int", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/intarray.test.js" + ], + "native-module-file": "_int.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/intarray.tar.zst", + "module-sha256": "f89d422a0830d63744c505103bcdd0271fd787c3c1c44a3ea1c3960701047db1" + }, + "notes": [] + }, + { + "id": "isn", + "sql-name": "isn", + "rust-constant": "ISN", + "display-name": "isn", + "source-kind": "postgres-contrib", + "upstream-import-name": "isn", + "package-export": "./contrib/isn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 31417, + "control-file": "src/postgres/versions/18/contrib/isn/isn.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/isn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/isn.test.js" + ], + "native-module-file": "isn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/isn.tar.zst", + "module-sha256": "b3533bd943d666fe2464ed7d16b00c0a6d1daa86b9d8621eab033b04e29c7b41" + }, + "notes": [] + }, + { + "id": "lo", + "sql-name": "lo", + "rust-constant": "LO", + "display-name": "lo", + "source-kind": "postgres-contrib", + "upstream-import-name": "lo", + "package-export": "./contrib/lo", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1822, + "control-file": "src/postgres/versions/18/contrib/lo/lo.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/lo", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/lo.test.js" + ], + "native-module-file": "lo.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/lo.tar.zst", + "module-sha256": "b0192b73ef0615f88148ac83ec34be449c942f0d6c7550b9ebfc4bce92bcd1e3" + }, + "notes": [] + }, + { + "id": "ltree", + "sql-name": "ltree", + "rust-constant": "LTREE", + "display-name": "ltree", + "source-kind": "postgres-contrib", + "upstream-import-name": "ltree", + "package-export": "./contrib/ltree", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 19553, + "control-file": "src/postgres/versions/18/contrib/ltree/ltree.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/ltree", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/ltree.test.js" + ], + "native-module-file": "ltree.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/ltree.tar.zst", + "module-sha256": "be272de20224b03b301fc65231b4bb97ebaa1815129387ce446c3aa1df6079ec" + }, + "notes": [] + }, + { + "id": "pageinspect", + "sql-name": "pageinspect", + "rust-constant": "PAGEINSPECT", + "display-name": "pageinspect", + "source-kind": "postgres-contrib", + "upstream-import-name": "pageinspect", + "package-export": "./contrib/pageinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 15923, + "control-file": "src/postgres/versions/18/contrib/pageinspect/pageinspect.control", + "control": { + "default-version": "1.13", + "module-pathname": "$libdir/pageinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pageinspect.test.js" + ], + "native-module-file": "pageinspect.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pageinspect.tar.zst", + "module-sha256": "1fef5827902d161b1d55f87b4a4df390ced16b13150d4be59b45618b29f3bf9a" + }, + "notes": [] + }, + { + "id": "pg_buffercache", + "sql-name": "pg_buffercache", + "rust-constant": "PG_BUFFERCACHE", + "display-name": "pg_buffercache", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_buffercache", + "package-export": "./contrib/pg_buffercache", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 3133, + "control-file": "src/postgres/versions/18/contrib/pg_buffercache/pg_buffercache.control", + "control": { + "default-version": "1.6", + "module-pathname": "$libdir/pg_buffercache", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_buffercache.test.js" + ], + "native-module-file": "pg_buffercache.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_buffercache.tar.zst", + "module-sha256": "cda0de1b917fc04b483a9c67dfce5f7b6e483a7cc306360e4c336b88f93c5e9c" + }, + "notes": [] + }, + { + "id": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "rust-constant": "PG_FREESPACEMAP", + "display-name": "pg_freespacemap", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_freespacemap", + "package-export": "./contrib/pg_freespacemap", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1485, + "control-file": "src/postgres/versions/18/contrib/pg_freespacemap/pg_freespacemap.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/pg_freespacemap", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_freespacemap.test.ts" + ], + "native-module-file": "pg_freespacemap.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_freespacemap.tar.zst", + "module-sha256": "e392cfd2bd06a2520bd09d8ecc7061e81c611dacfb413b590b081049347cbffc" + }, + "notes": [] + }, + { + "id": "pg_hashids", + "sql-name": "pg_hashids", + "rust-constant": "PG_HASHIDS", + "display-name": "pg_hashids", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_hashids", + "package-export": "./pg_hashids", + "tags": [ + "postgres extension" + ], + "bundle-size": 4212, + "control-file": "target/oliphaunt-sources/checkouts/pg_hashids/pg_hashids.control", + "control": { + "default-version": "1.3", + "module-pathname": "$libdir/pg_hashids", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_hashids.test.ts" + ], + "native-module-file": "pg_hashids.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_hashids.tar.zst", + "module-sha256": "b995931f524dc4a5eae7314d2229f47d2f92135d7273f49609f244859adf845d" + }, + "notes": [] + }, + { + "id": "pg_ivm", + "sql-name": "pg_ivm", + "rust-constant": "PG_IVM", + "display-name": "pg_ivm", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_ivm", + "package-export": "./pg_ivm", + "tags": [ + "postgres extension" + ], + "bundle-size": 24865, + "control-file": "target/oliphaunt-sources/checkouts/pg_ivm/pg_ivm.control", + "control": { + "default-version": "1.13", + "module-pathname": "$libdir/pg_ivm", + "requires": [], + "relocatable": "false", + "schema": "pg_catalog" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_ivm.test.ts" + ], + "native-module-file": "pg_ivm.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_ivm.tar.zst", + "module-sha256": "b9180a0a82d92f522a30ef4490286db90bf53b641bdada594f60023f69cea8ba" + }, + "notes": [] + }, + { + "id": "pg_surgery", + "sql-name": "pg_surgery", + "rust-constant": "PG_SURGERY", + "display-name": "pg_surgery", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_surgery", + "package-export": "./contrib/pg_surgery", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2635, + "control-file": "src/postgres/versions/18/contrib/pg_surgery/pg_surgery.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/pg_surgery", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_surgery.test.js" + ], + "native-module-file": "pg_surgery.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_surgery.tar.zst", + "module-sha256": "45c9750f40fece7b5532c953d66253aa35a37d13a52f0acc5e1050ceaf196e5d" + }, + "notes": [] + }, + { + "id": "pg_textsearch", + "sql-name": "pg_textsearch", + "rust-constant": "PG_TEXTSEARCH", + "display-name": "pg_textsearch", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_textsearch", + "package-export": "./pg_textsearch", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 55062, + "control-file": "target/oliphaunt-sources/checkouts/pg_textsearch/pg_textsearch.control", + "control": { + "default-version": "0.5.1", + "module-pathname": "$libdir/pg_textsearch", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_textsearch.test.ts" + ], + "native-module-file": "pg_textsearch.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_textsearch.tar.zst", + "module-sha256": "6c7e6a948fd00c065070b60bc804b417dff384a6af4783589f735a0474cbefb3" + }, + "notes": [] + }, + { + "id": "pg_trgm", + "sql-name": "pg_trgm", + "rust-constant": "PG_TRGM", + "display-name": "pg_trgm", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_trgm", + "package-export": "./contrib/pg_trgm", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 16208, + "control-file": "src/postgres/versions/18/contrib/pg_trgm/pg_trgm.control", + "control": { + "default-version": "1.6", + "module-pathname": "$libdir/pg_trgm", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_trgm.test.js" + ], + "native-module-file": "pg_trgm.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_trgm.tar.zst", + "module-sha256": "d48f1d8a6c338d945c4bd11789751cb5c36035bc90ea7d61d6505bec0ae48972" + }, + "notes": [] + }, + { + "id": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "rust-constant": "PG_UUIDV7", + "display-name": "pg_uuidv7", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pg_uuidv7", + "package-export": "./pg_uuidv7", + "tags": [ + "postgres extension" + ], + "bundle-size": 1522, + "control-file": "target/oliphaunt-sources/checkouts/pg_uuidv7/pg_uuidv7.control", + "control": { + "default-version": "1.7", + "module-pathname": "$libdir/pg_uuidv7", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pg_uuidv7.test.ts" + ], + "native-module-file": "pg_uuidv7.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_uuidv7.tar.zst", + "module-sha256": "8b1f9d686f2feac4dc7b807365e7a00163cd0dfe5f857723bd24e2c6eb65cfa6" + }, + "notes": [] + }, + { + "id": "pg_visibility", + "sql-name": "pg_visibility", + "rust-constant": "PG_VISIBILITY", + "display-name": "pg_visibility", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_visibility", + "package-export": "./contrib/pg_visibility", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4159, + "control-file": "src/postgres/versions/18/contrib/pg_visibility/pg_visibility.control", + "control": { + "default-version": "1.2", + "module-pathname": "$libdir/pg_visibility", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_visibility.test.js" + ], + "native-module-file": "pg_visibility.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_visibility.tar.zst", + "module-sha256": "adbb9fedcfad207ee2f5e9b37c625bfb964e946df4715f360ce6fa52cf26e99d" + }, + "notes": [] + }, + { + "id": "pg_walinspect", + "sql-name": "pg_walinspect", + "rust-constant": "PG_WALINSPECT", + "display-name": "pg_walinspect", + "source-kind": "postgres-contrib", + "upstream-import-name": "pg_walinspect", + "package-export": "./contrib/pg_walinspect", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 4689, + "control-file": "src/postgres/versions/18/contrib/pg_walinspect/pg_walinspect.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/pg_walinspect", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pg_walinspect.test.js" + ], + "native-module-file": "pg_walinspect.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pg_walinspect.tar.zst", + "module-sha256": "916b8a98aab6e1d3b2f36ea4027822186786ed72ad8a2cc306c52db4b1c5f369" + }, + "notes": [] + }, + { + "id": "pgcrypto", + "sql-name": "pgcrypto", + "rust-constant": "PGCRYPTO", + "display-name": "pgcrypto", + "source-kind": "postgres-contrib", + "upstream-import-name": "pgcrypto", + "package-export": "./contrib/pgcrypto", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1148162, + "control-file": "src/postgres/versions/18/contrib/pgcrypto/pgcrypto.control", + "control": { + "default-version": "1.4", + "module-pathname": "$libdir/pgcrypto", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "load-order": [], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/pgcrypto.test.js" + ], + "native-module-file": "pgcrypto.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pgcrypto.tar.zst", + "module-sha256": "426bf253c5f4385eb0cfabe0b6b149d28f87f3417c729a2650381d16261ce93f" + }, + "notes": [] + }, + { + "id": "pgtap", + "sql-name": "pgtap", + "rust-constant": "PGTAP", + "display-name": "pgtap", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "pgtap", + "package-export": "./pgtap", + "tags": [ + "postgres extension" + ], + "bundle-size": 239428, + "control-file": "target/oliphaunt-sources/checkouts/pgtap/pgtap.control", + "control": { + "default-version": "1.3.5", + "requires": [ + "plpgsql" + ], + "relocatable": "true" + }, + "dependencies": [ + "plpgsql" + ], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgtap.test.ts" + ], + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/pgtap.tar.zst", + "module-sha256": "" + }, + "notes": [] + }, + { + "id": "postgis", + "sql-name": "postgis", + "rust-constant": "POSTGIS", + "display-name": "PostGIS", + "source-kind": "postgis", + "upstream-import-name": "postgis", + "tags": [ + "postgres extension", + "experimental" + ], + "bundle-size": 8551161, + "control-file": "target/oliphaunt-sources/checkouts/postgis/extensions/postgis/postgis.control.in", + "control": { + "default-version": "3.6.3", + "module-pathname": "@MODULEPATH@", + "requires": [], + "relocatable": "false" + }, + "dependencies": [], + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "load-order": [ + "lib/postgresql/postgis-3.so" + ], + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/postgis/postgis.test.ts" + ], + "native-module-file": "postgis-3.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/postgis.tar.zst", + "module-sha256": "251f7115009a45f528eacf870a65bb26025714ba374f51e210c5ec1ed8a4f4bf" + }, + "notes": [] + }, + { + "id": "seg", + "sql-name": "seg", + "rust-constant": "SEG", + "display-name": "seg", + "source-kind": "postgres-contrib", + "upstream-import-name": "seg", + "package-export": "./contrib/seg", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 10426, + "control-file": "src/postgres/versions/18/contrib/seg/seg.control", + "control": { + "default-version": "1.4", + "module-pathname": "$libdir/seg", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/seg.test.js" + ], + "native-module-file": "seg.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/seg.tar.zst", + "module-sha256": "8de6090da8f297b49b1183102b8f4200e11b79251fac001ce165c1fdc1f9dfc4" + }, + "notes": [] + }, + { + "id": "tablefunc", + "sql-name": "tablefunc", + "rust-constant": "TABLEFUNC", + "display-name": "tablefunc", + "source-kind": "postgres-contrib", + "upstream-import-name": "tablefunc", + "package-export": "./contrib/tablefunc", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 5824, + "control-file": "src/postgres/versions/18/contrib/tablefunc/tablefunc.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tablefunc", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tablefunc.test.js" + ], + "native-module-file": "tablefunc.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tablefunc.tar.zst", + "module-sha256": "2664083fc238d5dda4204ef3f0bf689feb0406e78caca9aa0195d6610459e4b0" + }, + "notes": [] + }, + { + "id": "tcn", + "sql-name": "tcn", + "rust-constant": "TCN", + "display-name": "tcn", + "source-kind": "postgres-contrib", + "upstream-import-name": "tcn", + "package-export": "./contrib/tcn", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 1914, + "control-file": "src/postgres/versions/18/contrib/tcn/tcn.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tcn", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tcn.test.js" + ], + "native-module-file": "tcn.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tcn.tar.zst", + "module-sha256": "1d6a19a4b684fd1d0443a31220afe2ed7f401a55e13ba5cca3b0015dbfee020e" + }, + "notes": [] + }, + { + "id": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "rust-constant": "TSM_SYSTEM_ROWS", + "display-name": "tsm_system_rows", + "source-kind": "postgres-contrib", + "upstream-import-name": "tsm_system_rows", + "package-export": "./contrib/tsm_system_rows", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2048, + "control-file": "src/postgres/versions/18/contrib/tsm_system_rows/tsm_system_rows.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tsm_system_rows", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_rows.test.js" + ], + "native-module-file": "tsm_system_rows.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tsm_system_rows.tar.zst", + "module-sha256": "7f271afd0574aa6cba5b85b103140c2cbf84143d113ff43f26aab1754e26b645" + }, + "notes": [] + }, + { + "id": "tsm_system_time", + "sql-name": "tsm_system_time", + "rust-constant": "TSM_SYSTEM_TIME", + "display-name": "tsm_system_time", + "source-kind": "postgres-contrib", + "upstream-import-name": "tsm_system_time", + "package-export": "./contrib/tsm_system_time", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 2099, + "control-file": "src/postgres/versions/18/contrib/tsm_system_time/tsm_system_time.control", + "control": { + "default-version": "1.0", + "module-pathname": "$libdir/tsm_system_time", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/tsm_system_time.test.js" + ], + "native-module-file": "tsm_system_time.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/tsm_system_time.tar.zst", + "module-sha256": "b1c1898ab13d905f2b05f4137b74f4747d14a20959bbc20c7ab0150b81355e4e" + }, + "notes": [] + }, + { + "id": "unaccent", + "sql-name": "unaccent", + "rust-constant": "UNACCENT", + "display-name": "unaccent", + "source-kind": "postgres-contrib", + "upstream-import-name": "unaccent", + "package-export": "./contrib/unaccent", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 9323, + "control-file": "src/postgres/versions/18/contrib/unaccent/unaccent.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/unaccent", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/unaccent.test.js" + ], + "native-module-file": "unaccent.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/unaccent.tar.zst", + "module-sha256": "9a873164201fe57e351999559446f788a5206bc870d6375ad989c554631307a1" + }, + "notes": [] + }, + { + "id": "uuid_ossp", + "sql-name": "uuid-ossp", + "rust-constant": "UUID_OSSP", + "display-name": "uuid-ossp", + "source-kind": "postgres-contrib", + "upstream-import-name": "uuid_ossp", + "package-export": "./contrib/uuid_ossp", + "tags": [ + "postgres extension", + "postgres/contrib" + ], + "bundle-size": 17936, + "control-file": "src/postgres/versions/18/contrib/uuid-ossp/uuid-ossp.control", + "control": { + "default-version": "1.1", + "module-pathname": "$libdir/uuid-ossp", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/contrib/uuid_ossp.test.js" + ], + "native-module-file": "uuid-ossp.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/uuid-ossp.tar.zst", + "module-sha256": "2b323b7a442dba7f751744e3fa207bd04dab9be0dbc310b704ed03a2e2d6085e" + }, + "notes": [] + }, + { + "id": "vector", + "sql-name": "vector", + "rust-constant": "VECTOR", + "display-name": "pgvector", + "source-kind": "oliphaunt-other-extension", + "upstream-import-name": "vector", + "package-export": "./vector", + "tags": [ + "postgres extension" + ], + "bundle-size": 43953, + "control-file": "target/oliphaunt-sources/checkouts/pgvector/vector.control", + "control": { + "default-version": "0.8.2", + "module-pathname": "$libdir/vector", + "requires": [], + "relocatable": "true" + }, + "dependencies": [], + "native-dependencies": [], + "load-order": [], + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "startup-config": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false + }, + "smoke": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed" + }, + "tests": [ + "target/oliphaunt-sources/checkouts/src/extensions/tests/pgvector.test.ts" + ], + "native-module-file": "vector.so", + "promotion": { + "configured": true, + "requested": true, + "packaged": true, + "promoted": true, + "stable": true, + "archive": "extensions/vector.tar.zst", + "module-sha256": "f6bb2e87d2c23513ace39cd3b0f77f727031cdb65b7c9a945e7c395d92595937" + }, + "notes": [] + } + ] +} diff --git a/src/extensions/generated/mobile/static-extensions.tsv b/src/extensions/generated/mobile/static-extensions.tsv new file mode 100644 index 00000000..355fff7c --- /dev/null +++ b/src/extensions/generated/mobile/static-extensions.tsv @@ -0,0 +1,40 @@ +# @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 +bloom bloom contrib contrib/bloom +btree_gin btree_gin contrib contrib/btree_gin +btree_gist btree_gist contrib contrib/btree_gist +citext citext contrib contrib/citext +cube cube contrib contrib/cube +dict_int dict_int contrib contrib/dict_int +dict_xsyn dict_xsyn contrib contrib/dict_xsyn +earthdistance earthdistance contrib contrib/earthdistance +file_fdw file_fdw contrib contrib/file_fdw +fuzzystrmatch fuzzystrmatch contrib contrib/fuzzystrmatch +hstore hstore contrib contrib/hstore +intarray _int contrib contrib/intarray +isn isn contrib contrib/isn +lo lo contrib contrib/lo +ltree ltree contrib contrib/ltree +pageinspect pageinspect contrib contrib/pageinspect +pg_buffercache pg_buffercache contrib contrib/pg_buffercache +pg_freespacemap pg_freespacemap contrib contrib/pg_freespacemap +pg_hashids pg_hashids external target/oliphaunt-sources/checkouts/pg_hashids +pg_ivm pg_ivm external target/oliphaunt-sources/checkouts/pg_ivm createas.c,matview.c,pg_ivm.c,ruleutils.c,subselect.c +pg_surgery pg_surgery contrib contrib/pg_surgery +pg_textsearch pg_textsearch external target/oliphaunt-sources/checkouts/pg_textsearch source:src src +pg_trgm pg_trgm contrib contrib/pg_trgm +pg_uuidv7 pg_uuidv7 external target/oliphaunt-sources/checkouts/pg_uuidv7 +pg_visibility pg_visibility contrib contrib/pg_visibility +pg_walinspect pg_walinspect contrib contrib/pg_walinspect +pgcrypto pgcrypto contrib contrib/pgcrypto openssl openssl openssl openssl openssl openssl openssl +postgis postgis-3 external target/oliphaunt-sources/checkouts/postgis geos,geos-c,json-c,libcharset,libiconv,libxml2,proj,sqlite geos,geos-c,json-c,libxml2,proj,sqlite geos,geos-c,json-c,libcharset,libiconv,libxml2,proj,sqlite geos,json-c,libiconv,libxml2,proj,sqlite geos,json-c,libxml2,proj,sqlite geos,json-c,libiconv,libxml2,proj,sqlite +seg seg contrib contrib/seg +tablefunc tablefunc contrib contrib/tablefunc +tcn tcn contrib contrib/tcn +tsm_system_rows tsm_system_rows contrib contrib/tsm_system_rows +tsm_system_time tsm_system_time contrib contrib/tsm_system_time +unaccent unaccent contrib contrib/unaccent +uuid-ossp uuid-ossp contrib contrib/uuid-ossp uuid uuid uuid src/runtimes/liboliphaunt/native/portable-uuid/include -DHAVE_UUID_E2FS=1,-DHAVE_UUID_UUID_H=1 src/runtimes/liboliphaunt/native/portable-uuid +vector vector external target/oliphaunt-sources/checkouts/pgvector diff --git a/src/extensions/generated/mobile/static-registry.json b/src/extensions/generated/mobile/static-registry.json new file mode 100644 index 00000000..405a0943 --- /dev/null +++ b/src/extensions/generated/mobile/static-registry.json @@ -0,0 +1,342 @@ +{ + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-definitions", + "path": "src/extensions/external" + } + ], + "modules": [ + { + "data-files": [], + "id": "amcheck", + "native-dependencies": [], + "native-module-stem": "amcheck", + "sql-name": "amcheck", + "static-registry-required": true + }, + { + "data-files": [], + "id": "auto_explain", + "native-dependencies": [], + "native-module-stem": "auto_explain", + "sql-name": "auto_explain", + "static-registry-required": true + }, + { + "data-files": [], + "id": "bloom", + "native-dependencies": [], + "native-module-stem": "bloom", + "sql-name": "bloom", + "static-registry-required": true + }, + { + "data-files": [], + "id": "btree_gin", + "native-dependencies": [], + "native-module-stem": "btree_gin", + "sql-name": "btree_gin", + "static-registry-required": true + }, + { + "data-files": [], + "id": "btree_gist", + "native-dependencies": [], + "native-module-stem": "btree_gist", + "sql-name": "btree_gist", + "static-registry-required": true + }, + { + "data-files": [], + "id": "citext", + "native-dependencies": [], + "native-module-stem": "citext", + "sql-name": "citext", + "static-registry-required": true + }, + { + "data-files": [], + "id": "cube", + "native-dependencies": [], + "native-module-stem": "cube", + "sql-name": "cube", + "static-registry-required": true + }, + { + "data-files": [], + "id": "dict_int", + "native-dependencies": [], + "native-module-stem": "dict_int", + "sql-name": "dict_int", + "static-registry-required": true + }, + { + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "id": "dict_xsyn", + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "sql-name": "dict_xsyn", + "static-registry-required": true + }, + { + "data-files": [], + "id": "earthdistance", + "native-dependencies": [], + "native-module-stem": "earthdistance", + "sql-name": "earthdistance", + "static-registry-required": true + }, + { + "data-files": [], + "id": "file_fdw", + "native-dependencies": [], + "native-module-stem": "file_fdw", + "sql-name": "file_fdw", + "static-registry-required": true + }, + { + "data-files": [], + "id": "fuzzystrmatch", + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "sql-name": "fuzzystrmatch", + "static-registry-required": true + }, + { + "data-files": [], + "id": "hstore", + "native-dependencies": [], + "native-module-stem": "hstore", + "sql-name": "hstore", + "static-registry-required": true + }, + { + "data-files": [], + "id": "intarray", + "native-dependencies": [], + "native-module-stem": "_int", + "sql-name": "intarray", + "static-registry-required": true + }, + { + "data-files": [], + "id": "isn", + "native-dependencies": [], + "native-module-stem": "isn", + "sql-name": "isn", + "static-registry-required": true + }, + { + "data-files": [], + "id": "lo", + "native-dependencies": [], + "native-module-stem": "lo", + "sql-name": "lo", + "static-registry-required": true + }, + { + "data-files": [], + "id": "ltree", + "native-dependencies": [], + "native-module-stem": "ltree", + "sql-name": "ltree", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pageinspect", + "native-dependencies": [], + "native-module-stem": "pageinspect", + "sql-name": "pageinspect", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_buffercache", + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "sql-name": "pg_buffercache", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_freespacemap", + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "sql-name": "pg_freespacemap", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_hashids", + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "sql-name": "pg_hashids", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_ivm", + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "sql-name": "pg_ivm", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_surgery", + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "sql-name": "pg_surgery", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_textsearch", + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "sql-name": "pg_textsearch", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_trgm", + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "sql-name": "pg_trgm", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_uuidv7", + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "sql-name": "pg_uuidv7", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_visibility", + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "sql-name": "pg_visibility", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pg_walinspect", + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "sql-name": "pg_walinspect", + "static-registry-required": true + }, + { + "data-files": [], + "id": "pgcrypto", + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "sql-name": "pgcrypto", + "static-registry-required": true + }, + { + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "id": "postgis", + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "sql-name": "postgis", + "static-registry-required": true + }, + { + "data-files": [], + "id": "seg", + "native-dependencies": [], + "native-module-stem": "seg", + "sql-name": "seg", + "static-registry-required": true + }, + { + "data-files": [], + "id": "tablefunc", + "native-dependencies": [], + "native-module-stem": "tablefunc", + "sql-name": "tablefunc", + "static-registry-required": true + }, + { + "data-files": [], + "id": "tcn", + "native-dependencies": [], + "native-module-stem": "tcn", + "sql-name": "tcn", + "static-registry-required": true + }, + { + "data-files": [], + "id": "tsm_system_rows", + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "sql-name": "tsm_system_rows", + "static-registry-required": true + }, + { + "data-files": [], + "id": "tsm_system_time", + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "sql-name": "tsm_system_time", + "static-registry-required": true + }, + { + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "id": "unaccent", + "native-dependencies": [], + "native-module-stem": "unaccent", + "sql-name": "unaccent", + "static-registry-required": true + }, + { + "data-files": [], + "id": "uuid_ossp", + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "sql-name": "uuid-ossp", + "static-registry-required": true + }, + { + "data-files": [], + "id": "vector", + "native-dependencies": [], + "native-module-stem": "vector", + "sql-name": "vector", + "static-registry-required": true + } + ] +} diff --git a/src/extensions/generated/pgxs-build.tsv b/src/extensions/generated/pgxs-build.tsv new file mode 100644 index 00000000..b6452c27 --- /dev/null +++ b/src/extensions/generated/pgxs-build.tsv @@ -0,0 +1,7 @@ +# id sql_name source_dir module_file archive stable make_args +pg_hashids pg_hashids target/oliphaunt-sources/checkouts/pg_hashids pg_hashids.so extensions/pg_hashids.tar.zst true - +pg_ivm pg_ivm target/oliphaunt-sources/checkouts/pg_ivm pg_ivm.so extensions/pg_ivm.tar.zst true - +pg_textsearch pg_textsearch target/oliphaunt-sources/checkouts/pg_textsearch pg_textsearch.so extensions/pg_textsearch.tar.zst true - +pg_uuidv7 pg_uuidv7 target/oliphaunt-sources/checkouts/pg_uuidv7 pg_uuidv7.so extensions/pg_uuidv7.tar.zst true - +pgtap pgtap target/oliphaunt-sources/checkouts/pgtap - extensions/pgtap.tar.zst true - +vector vector target/oliphaunt-sources/checkouts/pgvector vector.so extensions/vector.tar.zst true - diff --git a/src/extensions/generated/sdk/js.json b/src/extensions/generated/sdk/js.json new file mode 100644 index 00000000..e1516302 --- /dev/null +++ b/src/extensions/generated/sdk/js.json @@ -0,0 +1,1484 @@ +{ + "consumer": "js", + "extension-catalog-sha256": "c73b96459c6d5b86d34ecd34e3af89d383320de8851f2988368552352bf349bc", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ] +} diff --git a/src/extensions/generated/sdk/kotlin.json b/src/extensions/generated/sdk/kotlin.json new file mode 100644 index 00000000..9d9e269e --- /dev/null +++ b/src/extensions/generated/sdk/kotlin.json @@ -0,0 +1,1484 @@ +{ + "consumer": "kotlin", + "extension-catalog-sha256": "c73b96459c6d5b86d34ecd34e3af89d383320de8851f2988368552352bf349bc", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ] +} diff --git a/src/extensions/generated/sdk/react-native.json b/src/extensions/generated/sdk/react-native.json new file mode 100644 index 00000000..98ec0004 --- /dev/null +++ b/src/extensions/generated/sdk/react-native.json @@ -0,0 +1,1534 @@ +{ + "consumer": "react-native", + "extension-catalog-sha256": "c8b0f5da02b60d68b141b273da7e3c3cad626bfe295bb0725241b483b1eda8b9", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "ios-static-dependencies": [ + "openssl" + ], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "ios-static-dependencies": [ + "geos", + "geos-c", + "json-c", + "libxml2", + "proj", + "sqlite" + ], + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "ios-static-dependencies": [ + "uuid" + ], + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "ios-static-dependencies": [], + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ] +} diff --git a/src/extensions/generated/sdk/rust.json b/src/extensions/generated/sdk/rust.json new file mode 100644 index 00000000..9bb6a421 --- /dev/null +++ b/src/extensions/generated/sdk/rust.json @@ -0,0 +1,1484 @@ +{ + "consumer": "rust", + "extension-catalog-sha256": "c73b96459c6d5b86d34ecd34e3af89d383320de8851f2988368552352bf349bc", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ] +} diff --git a/src/extensions/generated/sdk/swift.json b/src/extensions/generated/sdk/swift.json new file mode 100644 index 00000000..d2956d56 --- /dev/null +++ b/src/extensions/generated/sdk/swift.json @@ -0,0 +1,1484 @@ +{ + "consumer": "swift", + "extension-catalog-sha256": "c73b96459c6d5b86d34ecd34e3af89d383320de8851f2988368552352bf349bc", + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "amcheck", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "amcheck", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "amcheck", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "amcheck", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/auto_explain.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": false, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "auto_explain", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "auto_explain", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "auto_explain", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "auto_explain", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/bloom.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "bloom", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "bloom", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "bloom", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "bloom", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gin.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gin", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gin", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gin", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gin", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/btree_gist.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "btree_gist", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "btree_gist", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "btree_gist", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "btree_gist", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/citext.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "citext", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "citext", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "citext", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "citext", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/cube.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "cube", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "cube", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "cube", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "cube", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_int.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_int", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_int", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_int", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/xsyn_sample.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "dict_xsyn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "dict_xsyn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "dict_xsyn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/xsyn_sample.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "dict_xsyn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/earthdistance.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "cube" + ], + "desktop-release-ready": true, + "display-name": "earthdistance", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "earthdistance", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "earthdistance", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [ + "cube" + ], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "earthdistance", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/file_fdw.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "file_fdw", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "file_fdw", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "file_fdw", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "file_fdw", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "fuzzystrmatch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "fuzzystrmatch", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "fuzzystrmatch", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "fuzzystrmatch", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/hstore.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "hstore", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "hstore", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "hstore", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "hstore", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/intarray.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "intarray", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "intarray", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "_int", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "intarray", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/isn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "isn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "isn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "isn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "isn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/lo.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "lo", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "lo", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "lo", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "lo", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/ltree.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "ltree", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "ltree", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "ltree", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "ltree", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pageinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pageinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pageinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pageinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pageinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_buffercache", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_buffercache", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_buffercache", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_buffercache", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_freespacemap", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_freespacemap", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_freespacemap", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_freespacemap", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "cargo-package": "oliphaunt-extension-pg-hashids", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_hashids", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_hashids", + "maven-artifact": "oliphaunt-extension-pg-hashids", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_hashids", + "npm-package": "@oliphaunt/extension-pg-hashids", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-hashids", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_hashids", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "cargo-package": "oliphaunt-extension-pg-ivm", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_ivm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_ivm", + "maven-artifact": "oliphaunt-extension-pg-ivm", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_ivm", + "npm-package": "@oliphaunt/extension-pg-ivm", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-ivm", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_ivm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_surgery", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_surgery", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_surgery", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_surgery", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "cargo-package": "oliphaunt-extension-pg-textsearch", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_textsearch", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_textsearch", + "maven-artifact": "oliphaunt-extension-pg-textsearch", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_textsearch", + "npm-package": "@oliphaunt/extension-pg-textsearch", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-textsearch", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_textsearch", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_trgm", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_trgm", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_trgm", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_trgm", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "cargo-package": "oliphaunt-extension-pg-uuidv7", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_uuidv7", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_uuidv7", + "maven-artifact": "oliphaunt-extension-pg-uuidv7", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_uuidv7", + "npm-package": "@oliphaunt/extension-pg-uuidv7", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pg-uuidv7", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pg_uuidv7", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_visibility", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_visibility", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_visibility", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_visibility", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pg_walinspect", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pg_walinspect", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "pg_walinspect", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pg_walinspect", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgcrypto", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "pgcrypto", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "openssl:3.5.6-libcrypto-wasix-static" + ], + "native-module-stem": "pgcrypto", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "pgcrypto", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/pgtap.tar.zst", + "cargo-package": "oliphaunt-extension-pgtap", + "creates-extension": true, + "data-files": [], + "dependencies": [ + "plpgsql" + ], + "desktop-release-ready": true, + "display-name": "pgtap", + "extension-sql-file-names": [ + "uninstall_pgtap.sql" + ], + "extension-sql-file-prefixes": [ + "pgtap-core", + "pgtap-schema" + ], + "id": "pgtap", + "maven-artifact": "oliphaunt-extension-pgtap", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": null, + "npm-package": "@oliphaunt/extension-pgtap", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-pgtap", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "pgtap", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/postgis.tar.zst", + "cargo-package": "oliphaunt-extension-postgis", + "creates-extension": true, + "data-files": [ + "share/postgresql/contrib/postgis-3.6/legacy.sql", + "share/postgresql/contrib/postgis-3.6/legacy_gist.sql", + "share/postgresql/contrib/postgis-3.6/legacy_minimal.sql", + "share/postgresql/contrib/postgis-3.6/postgis.sql", + "share/postgresql/contrib/postgis-3.6/postgis_upgrade.sql", + "share/postgresql/contrib/postgis-3.6/spatial_ref_sys.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_legacy.sql", + "share/postgresql/contrib/postgis-3.6/uninstall_postgis.sql", + "share/postgresql/proj/proj.db" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "PostGIS", + "extension-sql-file-names": [ + "uninstall_postgis.sql" + ], + "extension-sql-file-prefixes": [ + "postgis_comments", + "postgis_proc_set_search_path", + "rtpostgis" + ], + "id": "postgis", + "maven-artifact": "oliphaunt-extension-postgis", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [ + "geos:3.14.1-static", + "proj:9.8.1-static", + "sqlite:3.53.1-static", + "libxml2:2.14.6-static", + "json-c:0.18-static", + "libiconv:1.19-static" + ], + "native-module-stem": "postgis-3", + "npm-package": "@oliphaunt/extension-postgis", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-postgis", + "runtime-bound": false, + "runtime-environment": [ + { + "name": "PROJ_DATA", + "path": "share/postgresql/proj", + "required_file": "proj.db" + } + ], + "runtime-share-data-files": [ + "contrib/postgis-3.6/legacy.sql", + "contrib/postgis-3.6/legacy_gist.sql", + "contrib/postgis-3.6/legacy_minimal.sql", + "contrib/postgis-3.6/postgis.sql", + "contrib/postgis-3.6/postgis_upgrade.sql", + "contrib/postgis-3.6/spatial_ref_sys.sql", + "contrib/postgis-3.6/uninstall_legacy.sql", + "contrib/postgis-3.6/uninstall_postgis.sql", + "proj/proj.db" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgis", + "sql-name": "postgis", + "stable": true, + "support": { + "mobile": { + "android": "supported", + "ios": "supported" + }, + "native": { + "broker": "supported", + "direct": "supported", + "server": "supported" + }, + "wasix": { + "direct": "supported", + "server": "supported" + } + }, + "target-status": { + "mobile": null, + "native": "supported", + "wasix": "supported" + } + }, + { + "archive": "extensions/seg.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "seg", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "seg", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "seg", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "seg", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tablefunc.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tablefunc", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tablefunc", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tablefunc", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tablefunc", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tcn.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tcn", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tcn", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tcn", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tcn", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_rows", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_rows", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_rows", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_rows", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "tsm_system_time", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "tsm_system_time", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "tsm_system_time", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "tsm_system_time", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/unaccent.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [ + "share/postgresql/tsearch_data/unaccent.rules" + ], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "unaccent", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "unaccent", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "unaccent", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [ + "tsearch_data/unaccent.rules" + ], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "unaccent", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "cargo-package": "oliphaunt-extension-contrib-pg18", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "uuid-ossp", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "uuid_ossp", + "maven-artifact": "oliphaunt-extension-contrib-pg18", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "uuid-ossp", + "npm-package": "@oliphaunt/extension-contrib-pg18", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-contrib-pg18", + "runtime-bound": true, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "postgres-contrib", + "sql-name": "uuid-ossp", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + }, + { + "archive": "extensions/vector.tar.zst", + "cargo-package": "oliphaunt-extension-vector", + "creates-extension": true, + "data-files": [], + "dependencies": [], + "desktop-release-ready": true, + "display-name": "pgvector", + "extension-sql-file-names": [], + "extension-sql-file-prefixes": [], + "id": "vector", + "maven-artifact": "oliphaunt-extension-vector", + "maven-group": "dev.oliphaunt.extensions", + "mobile-release-ready": true, + "native-dependencies": [], + "native-module-stem": "vector", + "npm-package": "@oliphaunt/extension-vector", + "postgres-major": 18, + "public": true, + "release-product": "oliphaunt-extension-vector", + "runtime-bound": false, + "runtime-environment": [], + "runtime-share-data-files": [], + "selected-extension-dependencies": [], + "shared-preload-libraries": [], + "source-kind": "oliphaunt-other-extension", + "sql-name": "vector", + "stable": true, + "support": {}, + "target-status": { + "mobile": null, + "native": null, + "wasix": null + } + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-evidence", + "path": "src/extensions/generated/docs/extension-evidence.json" + } + ] +} diff --git a/src/extensions/generated/wasix/extensions.json b/src/extensions/generated/wasix/extensions.json new file mode 100644 index 00000000..1f51932e --- /dev/null +++ b/src/extensions/generated/wasix/extensions.json @@ -0,0 +1,771 @@ +{ + "extensions": [ + { + "archive": "extensions/amcheck.tar.zst", + "dependencies": [], + "id": "amcheck", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "amcheck.so", + "native-support-modules": [], + "sql-name": "amcheck" + }, + { + "archive": "extensions/auto_explain.tar.zst", + "dependencies": [], + "id": "auto_explain", + "lifecycle": { + "create-extension": false, + "load-sql": [ + "LOAD 'auto_explain';", + "SET auto_explain.log_min_duration = '0';", + "SET auto_explain.log_analyze = 'true';", + "SET auto_explain.log_level = 'NOTICE';" + ], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "auto_explain.so", + "native-support-modules": [], + "sql-name": "auto_explain" + }, + { + "archive": "extensions/bloom.tar.zst", + "dependencies": [], + "id": "bloom", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "bloom.so", + "native-support-modules": [], + "sql-name": "bloom" + }, + { + "archive": "extensions/btree_gin.tar.zst", + "dependencies": [], + "id": "btree_gin", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "btree_gin.so", + "native-support-modules": [], + "sql-name": "btree_gin" + }, + { + "archive": "extensions/btree_gist.tar.zst", + "dependencies": [], + "id": "btree_gist", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "btree_gist.so", + "native-support-modules": [], + "sql-name": "btree_gist" + }, + { + "archive": "extensions/citext.tar.zst", + "dependencies": [], + "id": "citext", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "citext.so", + "native-support-modules": [], + "sql-name": "citext" + }, + { + "archive": "extensions/cube.tar.zst", + "dependencies": [], + "id": "cube", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "cube.so", + "native-support-modules": [], + "sql-name": "cube" + }, + { + "archive": "extensions/dict_int.tar.zst", + "dependencies": [], + "id": "dict_int", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "dict_int.so", + "native-support-modules": [], + "sql-name": "dict_int" + }, + { + "archive": "extensions/dict_xsyn.tar.zst", + "dependencies": [], + "id": "dict_xsyn", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "dict_xsyn.so", + "native-support-modules": [], + "sql-name": "dict_xsyn" + }, + { + "archive": "extensions/earthdistance.tar.zst", + "dependencies": [ + "cube" + ], + "id": "earthdistance", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "earthdistance.so", + "native-support-modules": [], + "sql-name": "earthdistance" + }, + { + "archive": "extensions/file_fdw.tar.zst", + "dependencies": [], + "id": "file_fdw", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "file_fdw.so", + "native-support-modules": [], + "sql-name": "file_fdw" + }, + { + "archive": "extensions/fuzzystrmatch.tar.zst", + "dependencies": [], + "id": "fuzzystrmatch", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "fuzzystrmatch.so", + "native-support-modules": [], + "sql-name": "fuzzystrmatch" + }, + { + "archive": "extensions/hstore.tar.zst", + "dependencies": [], + "id": "hstore", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "hstore.so", + "native-support-modules": [], + "sql-name": "hstore" + }, + { + "archive": "extensions/intarray.tar.zst", + "dependencies": [], + "id": "intarray", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "_int.so", + "native-support-modules": [], + "sql-name": "intarray" + }, + { + "archive": "extensions/isn.tar.zst", + "dependencies": [], + "id": "isn", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "isn.so", + "native-support-modules": [], + "sql-name": "isn" + }, + { + "archive": "extensions/lo.tar.zst", + "dependencies": [], + "id": "lo", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "lo.so", + "native-support-modules": [], + "sql-name": "lo" + }, + { + "archive": "extensions/ltree.tar.zst", + "dependencies": [], + "id": "ltree", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "ltree.so", + "native-support-modules": [], + "sql-name": "ltree" + }, + { + "archive": "extensions/pageinspect.tar.zst", + "dependencies": [], + "id": "pageinspect", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pageinspect.so", + "native-support-modules": [], + "sql-name": "pageinspect" + }, + { + "archive": "extensions/pg_buffercache.tar.zst", + "dependencies": [], + "id": "pg_buffercache", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_buffercache.so", + "native-support-modules": [], + "sql-name": "pg_buffercache" + }, + { + "archive": "extensions/pg_freespacemap.tar.zst", + "dependencies": [], + "id": "pg_freespacemap", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_freespacemap.so", + "native-support-modules": [], + "sql-name": "pg_freespacemap" + }, + { + "archive": "extensions/pg_hashids.tar.zst", + "dependencies": [], + "id": "pg_hashids", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_hashids.so", + "native-support-modules": [], + "sql-name": "pg_hashids" + }, + { + "archive": "extensions/pg_ivm.tar.zst", + "dependencies": [], + "id": "pg_ivm", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_ivm.so", + "native-support-modules": [], + "sql-name": "pg_ivm" + }, + { + "archive": "extensions/pg_surgery.tar.zst", + "dependencies": [], + "id": "pg_surgery", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_surgery.so", + "native-support-modules": [], + "sql-name": "pg_surgery" + }, + { + "archive": "extensions/pg_textsearch.tar.zst", + "dependencies": [], + "id": "pg_textsearch", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_textsearch.so", + "native-support-modules": [], + "sql-name": "pg_textsearch" + }, + { + "archive": "extensions/pg_trgm.tar.zst", + "dependencies": [], + "id": "pg_trgm", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_trgm.so", + "native-support-modules": [], + "sql-name": "pg_trgm" + }, + { + "archive": "extensions/pg_uuidv7.tar.zst", + "dependencies": [], + "id": "pg_uuidv7", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_uuidv7.so", + "native-support-modules": [], + "sql-name": "pg_uuidv7" + }, + { + "archive": "extensions/pg_visibility.tar.zst", + "dependencies": [], + "id": "pg_visibility", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_visibility.so", + "native-support-modules": [], + "sql-name": "pg_visibility" + }, + { + "archive": "extensions/pg_walinspect.tar.zst", + "dependencies": [], + "id": "pg_walinspect", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pg_walinspect.so", + "native-support-modules": [], + "sql-name": "pg_walinspect" + }, + { + "archive": "extensions/pgcrypto.tar.zst", + "dependencies": [], + "id": "pgcrypto", + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "pgcrypto.so", + "native-support-modules": [], + "sql-name": "pgcrypto" + }, + { + "archive": "extensions/pgtap.tar.zst", + "dependencies": [ + "plpgsql" + ], + "id": "pgtap", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": null, + "native-support-modules": [], + "sql-name": "pgtap" + }, + { + "archive": "extensions/postgis.tar.zst", + "dependencies": [], + "id": "postgis", + "lifecycle": { + "create-extension": true, + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [ + "lib/postgresql/postgis-3.so" + ], + "native-module-file": "postgis-3.so", + "native-support-modules": [ + { + "aot-file": "postgis_deps-llvm-opta.bin.zst", + "build-path": "postgis/postgis/liboliphaunt_postgis_deps.so", + "name": "postgis_deps", + "runtime-path": "lib/postgresql/liboliphaunt_postgis_deps.so" + } + ], + "sql-name": "postgis" + }, + { + "archive": "extensions/seg.tar.zst", + "dependencies": [], + "id": "seg", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "seg.so", + "native-support-modules": [], + "sql-name": "seg" + }, + { + "archive": "extensions/tablefunc.tar.zst", + "dependencies": [], + "id": "tablefunc", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "tablefunc.so", + "native-support-modules": [], + "sql-name": "tablefunc" + }, + { + "archive": "extensions/tcn.tar.zst", + "dependencies": [], + "id": "tcn", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "tcn.so", + "native-support-modules": [], + "sql-name": "tcn" + }, + { + "archive": "extensions/tsm_system_rows.tar.zst", + "dependencies": [], + "id": "tsm_system_rows", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "tsm_system_rows.so", + "native-support-modules": [], + "sql-name": "tsm_system_rows" + }, + { + "archive": "extensions/tsm_system_time.tar.zst", + "dependencies": [], + "id": "tsm_system_time", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "tsm_system_time.so", + "native-support-modules": [], + "sql-name": "tsm_system_time" + }, + { + "archive": "extensions/unaccent.tar.zst", + "dependencies": [], + "id": "unaccent", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "unaccent.so", + "native-support-modules": [], + "sql-name": "unaccent" + }, + { + "archive": "extensions/uuid-ossp.tar.zst", + "dependencies": [], + "id": "uuid_ossp", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "uuid-ossp.so", + "native-support-modules": [], + "sql-name": "uuid-ossp" + }, + { + "archive": "extensions/vector.tar.zst", + "dependencies": [], + "id": "vector", + "lifecycle": { + "create-extension": true, + "create-schema": "pg_catalog", + "load-sql": [], + "post-create-sql": [], + "preload-required": false, + "restart-required": false, + "shared-memory-required": false, + "startup-config": [] + }, + "load-order": [], + "native-module-file": "vector.so", + "native-support-modules": [], + "sql-name": "vector" + } + ], + "format-version": 1, + "generated-from": [ + { + "name": "extension-catalog", + "path": "src/extensions/generated/extensions.catalog.json" + }, + { + "name": "extension-definitions", + "path": "src/extensions/external" + } + ] +} diff --git a/src/extensions/model/moon.yml b/src/extensions/model/moon.yml new file mode 100644 index 00000000..948e6be1 --- /dev/null +++ b/src/extensions/model/moon.yml @@ -0,0 +1,35 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extension-model" +language: "unknown" +layer: "configuration" +stack: "systems" +tags: ["extensions", "model"] + +project: + title: "Extension Model" + description: "Exact SQL extension catalog, recipe, generated metadata, and policy checks." + owner: "oliphaunt" + +tasks: + check: + tags: ["quality", "static"] + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + inputs: + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/src/shared/extension-runtime-contract/**/*" + - "/tools/release/release_graph_query.mjs" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/platform-compatibility-policy.test.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" + - "/tools/xtask/**/*" + - "/Cargo.lock" + - "/Cargo.toml" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/moon.yml b/src/extensions/moon.yml new file mode 100644 index 00000000..adebae1a --- /dev/null +++ b/src/extensions/moon.yml @@ -0,0 +1,173 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "extensions" +language: "unknown" +layer: "tool" +stack: "systems" +tags: ["extensions", "catalog", "assets"] +dependsOn: + - id: "postgres18" + scope: "build" + - id: "extension-runtime-contract" + scope: "build" + - id: "oliphaunt-extension-contrib-pg18" + scope: "build" + - id: "extension-age" + scope: "build" + - id: "oliphaunt-extension-amcheck" + scope: "build" + - id: "oliphaunt-extension-auto-explain" + scope: "build" + - id: "oliphaunt-extension-bloom" + scope: "build" + - id: "oliphaunt-extension-btree-gin" + scope: "build" + - id: "oliphaunt-extension-btree-gist" + scope: "build" + - id: "oliphaunt-extension-citext" + scope: "build" + - id: "oliphaunt-extension-cube" + scope: "build" + - id: "oliphaunt-extension-dict-int" + scope: "build" + - id: "oliphaunt-extension-dict-xsyn" + scope: "build" + - id: "oliphaunt-extension-earthdistance" + scope: "build" + - id: "oliphaunt-extension-file-fdw" + scope: "build" + - id: "oliphaunt-extension-fuzzystrmatch" + scope: "build" + - id: "oliphaunt-extension-hstore" + scope: "build" + - id: "oliphaunt-extension-intarray" + scope: "build" + - id: "oliphaunt-extension-isn" + scope: "build" + - id: "oliphaunt-extension-lo" + scope: "build" + - id: "oliphaunt-extension-ltree" + scope: "build" + - id: "oliphaunt-extension-pageinspect" + scope: "build" + - id: "oliphaunt-extension-pg-buffercache" + scope: "build" + - id: "oliphaunt-extension-pg-freespacemap" + scope: "build" + - id: "oliphaunt-extension-pg-surgery" + scope: "build" + - id: "oliphaunt-extension-pg-trgm" + scope: "build" + - id: "oliphaunt-extension-pg-visibility" + scope: "build" + - id: "oliphaunt-extension-pg-walinspect" + scope: "build" + - id: "oliphaunt-extension-pgcrypto" + scope: "build" + - id: "oliphaunt-extension-seg" + scope: "build" + - id: "oliphaunt-extension-tablefunc" + scope: "build" + - id: "oliphaunt-extension-tcn" + scope: "build" + - id: "oliphaunt-extension-tsm-system-rows" + scope: "build" + - id: "oliphaunt-extension-tsm-system-time" + scope: "build" + - id: "oliphaunt-extension-unaccent" + scope: "build" + - id: "oliphaunt-extension-uuid-ossp" + scope: "build" + - id: "oliphaunt-extension-pg-hashids" + scope: "build" + - id: "oliphaunt-extension-pg-ivm" + scope: "build" + - id: "oliphaunt-extension-pg-textsearch" + scope: "build" + - id: "oliphaunt-extension-pg-uuidv7" + scope: "build" + - id: "oliphaunt-extension-pgtap" + scope: "build" + - id: "oliphaunt-extension-postgis" + scope: "build" + - id: "oliphaunt-extension-vector" + scope: "build" + +project: + title: "Extension Catalog" + description: "Exact SQL extension catalog, build plans, and generated tables." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/core" + paths: + "**/*": ["@oliphaunt/core"] + +tasks: + check: + tags: ["quality", "static"] + command: "bun tools/policy/assertions/assert-source-inputs.mjs extensions" + deps: + - "postgres18:check" + - "extension-runtime-contract:check" + - "oliphaunt-extension-contrib-pg18:check" + - "extension-age:check" + - "oliphaunt-extension-amcheck:check" + - "oliphaunt-extension-auto-explain:check" + - "oliphaunt-extension-bloom:check" + - "oliphaunt-extension-btree-gin:check" + - "oliphaunt-extension-btree-gist:check" + - "oliphaunt-extension-citext:check" + - "oliphaunt-extension-cube:check" + - "oliphaunt-extension-dict-int:check" + - "oliphaunt-extension-dict-xsyn:check" + - "oliphaunt-extension-earthdistance:check" + - "oliphaunt-extension-file-fdw:check" + - "oliphaunt-extension-fuzzystrmatch:check" + - "oliphaunt-extension-hstore:check" + - "oliphaunt-extension-intarray:check" + - "oliphaunt-extension-isn:check" + - "oliphaunt-extension-lo:check" + - "oliphaunt-extension-ltree:check" + - "oliphaunt-extension-pageinspect:check" + - "oliphaunt-extension-pg-buffercache:check" + - "oliphaunt-extension-pg-freespacemap:check" + - "oliphaunt-extension-pg-surgery:check" + - "oliphaunt-extension-pg-trgm:check" + - "oliphaunt-extension-pg-visibility:check" + - "oliphaunt-extension-pg-walinspect:check" + - "oliphaunt-extension-pgcrypto:check" + - "oliphaunt-extension-seg:check" + - "oliphaunt-extension-tablefunc:check" + - "oliphaunt-extension-tcn:check" + - "oliphaunt-extension-tsm-system-rows:check" + - "oliphaunt-extension-tsm-system-time:check" + - "oliphaunt-extension-unaccent:check" + - "oliphaunt-extension-uuid-ossp:check" + - "oliphaunt-extension-pg-hashids:check" + - "oliphaunt-extension-pg-ivm:check" + - "oliphaunt-extension-pg-textsearch:check" + - "oliphaunt-extension-pg-uuidv7:check" + - "oliphaunt-extension-pgtap:check" + - "oliphaunt-extension-postgis:check" + - "oliphaunt-extension-vector:check" + inputs: + - "/src/postgres/versions/18/**/*" + - "/src/shared/extension-runtime-contract/**/*" + - "/src/extensions/**/*" + - "!/src/extensions/generated/docs" + - "!/src/extensions/generated/docs/**" + - "!/src/extensions/evidence" + - "!/src/extensions/evidence/**" + - "/tools/xtask/**/*" + - "/tools/policy/assertions/assert-source-inputs.mjs" + - "/tools/policy/fetch-sources.mjs" + - "/tools/policy/source-fetch-core.mjs" + - "/tools/policy/source-fetch-core.test.mjs" + - "/tools/policy/source-archive.py" + - "/tools/policy/verify-source-tree.py" + - "/Cargo.lock" + - "/Cargo.toml" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/extensions/schemas/recipe.schema.json b/src/extensions/schemas/recipe.schema.json new file mode 100644 index 00000000..ffe970c2 --- /dev/null +++ b/src/extensions/schemas/recipe.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://oliphaunt.dev/schemas/extensions/recipe.schema.json", + "title": "Oliphaunt Extension Recipe", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "sql_name", + "display_name", + "kind", + "license", + "source", + "postgres_majors", + "lifecycle", + "artifacts", + "support" + ], + "properties": { + "schema": { "const": "oliphaunt-extension-recipe-v1" }, + "sql_name": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*$" }, + "display_name": { "type": "string", "minLength": 1 }, + "kind": { + "type": "string", + "enum": ["external-simple-pgxs", "external-complex", "postgres-contrib"] + }, + "license": { "type": "string", "minLength": 1 }, + "source": { "type": "string", "pattern": "^[a-z][a-z0-9_-]*$" }, + "postgres_majors": { + "type": "array", + "minItems": 1, + "items": { "type": "integer", "minimum": 18 } + }, + "lifecycle": { "type": "object" }, + "artifacts": { "type": "object" }, + "support": { + "type": "object", + "additionalProperties": { "type": "object" } + } + } +} diff --git a/src/extensions/schemas/support-table.schema.json b/src/extensions/schemas/support-table.schema.json new file mode 100644 index 00000000..863e67af --- /dev/null +++ b/src/extensions/schemas/support-table.schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://oliphaunt.dev/schemas/extensions/support-table.schema.json", + "title": "Oliphaunt Extension Support Table", + "type": "object", + "additionalProperties": false, + "required": ["format-version", "generated-from", "extensions"], + "properties": { + "format-version": { "const": 1 }, + "generated-from": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "path"], + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" } + } + } + }, + "extensions": { + "type": "array", + "items": { + "type": "object", + "required": ["sql-name", "display-name", "family", "public", "stable", "smoke"], + "properties": { + "id": { "type": "string" }, + "sql-name": { "type": "string" }, + "display-name": { "type": "string" }, + "version": { "type": "string" }, + "family": { "type": "string" }, + "public": { "type": "boolean" }, + "stable": { "type": "boolean" }, + "packaged": { "type": "boolean" }, + "promoted": { "type": "boolean" }, + "desktop-release-ready": { "type": "boolean" }, + "mobile-release-ready": { "type": "boolean" }, + "target-status": { + "type": "object", + "additionalProperties": false, + "properties": { + "native": { "type": ["string", "null"] }, + "wasix": { "type": ["string", "null"] }, + "mobile": { "type": ["string", "null"] } + } + }, + "support": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "archive": { "type": "string" }, + "blocker": { "type": "string" }, + "activation": { "type": "string" }, + "dependencies": { "type": "array", "items": { "type": "string" } }, + "native-dependencies": { "type": "array", "items": { "type": "string" } }, + "smoke": { "type": "object" } + } + } + } + } +} 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/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py new file mode 100755 index 00000000..480810ff --- /dev/null +++ b/src/extensions/tools/check-extension-model.py @@ -0,0 +1,2726 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import shutil +import subprocess +import tomllib +from functools import lru_cache +from pathlib import Path +from tempfile import TemporaryDirectory + +ROOT = Path(__file__).resolve().parents[3] + +PROMOTED = ROOT / "src/extensions/catalog/extensions.promoted.toml" +SMOKE = ROOT / "src/extensions/catalog/extensions.smoke.toml" +SOURCE_CATALOG = ROOT / "src/extensions/catalog/extensions.source.json" +CATALOG = ROOT / "src/extensions/generated/extensions.catalog.json" +BUILD_PLAN = ROOT / "src/extensions/generated/extensions.build-plan.json" +CONTRIB_RECIPE = ROOT / "src/extensions/contrib/postgres18.toml" +RECIPE_SCHEMA = ROOT / "src/extensions/schemas/recipe.schema.json" +SUPPORT_SCHEMA = ROOT / "src/extensions/schemas/support-table.schema.json" +SUPPORT_TABLE = ROOT / "src/extensions/generated/docs/extensions.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" +EVIDENCE_TABLE = ROOT / "src/extensions/generated/docs/extension-evidence.json" +THIRD_PARTY_ROOT = ROOT / "src/sources/third-party" +EXTERNAL_ROOT = ROOT / "src/extensions/external" +EXTENSION_ENVELOPE_FILENAMES = { + ".release-semantic-inputs.json", + "CHANGELOG.md", + "VERSION", + "artifacts.toml", + "release.toml", +} +GENERATED_SDKS = { + "rust": ROOT / "src/extensions/generated/sdk/rust.json", + "swift": ROOT / "src/extensions/generated/sdk/swift.json", + "kotlin": ROOT / "src/extensions/generated/sdk/kotlin.json", + "js": ROOT / "src/extensions/generated/sdk/js.json", + "react-native": ROOT / "src/extensions/generated/sdk/react-native.json", +} +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_KOTLIN_GRADLE_PLUGIN_CATALOG = ( + ROOT + / "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/resources/dev/oliphaunt/android/extensions.properties" +) +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" +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" +CHECK_EXTENSION_MODEL_WRITE_EVIDENCE_SUMMARY_COMMAND = ( + f"{CHECK_EXTENSION_MODEL_COMMAND} --write-evidence-summary" +) +WASIX_EVIDENCE_TIER = "wasix-full-lifecycle-v1" + +RUST_INTERNAL_EXTENSION_CANDIDATES = [ + { + "id": "graph", + "sql-name": "graph", + "rust-constant": "GRAPH", + "creates-extension": True, + "native-module-stem": "graph", + "selected-extension-dependencies": [], + "runtime-share-data-files": [], + "shared-preload-libraries": [], + "first-party": False, + "mobile-release-ready": False, + "external-policy": { + "upstream": "https://github.com/evokoa/pggraph", + "license": "Apache-2.0", + "source-kind": "Pgrx", + "redistribution": "Allowed", + "requires-shared-preload": False, + "notes": "Optional shared_preload_libraries='graph' enables startup _PG_init behavior; background-worker maintenance paths must be tested per engine mode.", + }, + }, + { + "id": "pg_search", + "sql-name": "pg_search", + "rust-constant": "PG_SEARCH", + "creates-extension": True, + "native-module-stem": "pg_search", + "selected-extension-dependencies": [], + "runtime-share-data-files": [], + "shared-preload-libraries": ["pg_search"], + "first-party": False, + "mobile-release-ready": False, + "external-policy": { + "upstream": "https://github.com/paradedb/paradedb", + "license": "AGPL-3.0 community edition", + "source-kind": "Pgrx", + "redistribution": "RequiresCommercialLicense", + "requires-shared-preload": True, + "notes": "ParadeDB pg_search requires shared_preload_libraries='pg_search', registers preload-time WAL machinery, and uses PostgreSQL parallel workers.", + }, + }, +] + +BASE_SOURCE_DIGEST_INPUTS = [ + "src/postgres/versions/18/source.toml", + "src/extensions/catalog/extensions.promoted.toml", + "src/extensions/catalog/extensions.smoke.toml", + "src/extensions/catalog/extensions.source.json", + "src/extensions/contrib/postgres18.toml", + "src/extensions/generated/extensions.catalog.json", + "src/extensions/generated/extensions.build-plan.json", + "src/extensions/generated/contrib-build.tsv", + "src/extensions/generated/pgxs-build.tsv", + "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", +] + +ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") +SQL_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$") +SMOKE_STATUSES = {"passed", "failed", "not-run", "blocked"} +SUPPORT_STATUSES = {"unsupported", "candidate", "experimental", "supported"} + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def ensure_trailing_newline(text: str) -> str: + return text if text.endswith("\n") else f"{text}\n" + + +def format_rust_source(source: str) -> str: + try: + return ensure_trailing_newline( + subprocess.check_output( + ["rustfmt", "--emit", "stdout"], + cwd=ROOT, + input=source, + text=True, + ) + ) + except (FileNotFoundError, subprocess.CalledProcessError) as error: + fail(f"failed to format generated Rust extension metadata with rustfmt: {error}") + + +def format_typescript_source(source: str, path: Path) -> str: + pnpm = shutil.which("pnpm") or shutil.which("pnpm.cmd") + if pnpm is None: + fail(f"failed to format generated TypeScript extension metadata with Biome {BIOME_VERSION}: pnpm was not found") + try: + return ensure_trailing_newline( + subprocess.check_output( + [ + pnpm, + f"--package=@biomejs/biome@{BIOME_VERSION}", + "dlx", + "biome", + "format", + "--stdin-file-path", + rel(path), + ], + cwd=ROOT, + input=source, + text=True, + ) + ) + except (FileNotFoundError, subprocess.CalledProcessError) as error: + 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": + 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] + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str) -> tuple[dict, ...]: + try: + output = subprocess.check_output( + bun_command("tools/release/release_graph_query.mjs", command), + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except (FileNotFoundError, subprocess.CalledProcessError) as 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) + 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", + "memberPath", + "class", + "versioning", + "sourcePath", + "cargoPackage", + "npmPackage", + "mavenGroup", + "mavenArtifact", + ]: + 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) + sql_name = str(row["sqlName"]) + if sql_name in seen: + fail(f"release graph extension-metadata query returned duplicate SQL member {sql_name}") + seen.add(sql_name) + if not rows: + fail("release graph extension-metadata query returned no products") + return rows + + +@lru_cache(maxsize=1) +def extension_metadata_by_sql_name() -> dict[str, dict]: + return {str(row["sqlName"]): row for row in extension_metadata_rows()} + + +def rel(path: Path) -> str: + try: + return path.relative_to(ROOT).as_posix() + except ValueError: + return path.as_posix() + + +def read_toml(path: Path) -> dict: + try: + with path.open("rb") as handle: + return tomllib.load(handle) + except tomllib.TOMLDecodeError as error: + fail(f"{rel(path)} is invalid TOML: {error}") + + +def read_json(path: Path) -> dict: + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as error: + fail(f"{rel(path)} is invalid JSON: {error}") + + +def source_pin_paths() -> list[Path]: + if not THIRD_PARTY_ROOT.is_dir(): + fail(f"{rel(THIRD_PARTY_ROOT)} must exist") + if not EXTERNAL_ROOT.is_dir(): + fail(f"{rel(EXTERNAL_ROOT)} must exist") + paths = [ + path + for path in THIRD_PARTY_ROOT.glob("**/*.toml") + if path.is_file() + ] + paths.extend( + path + for path in EXTERNAL_ROOT.glob("**/source.toml") + if path.is_file() + ) + return sorted(paths, key=rel) + + +def normalized_rel_list(values: object, label: str) -> list[str]: + if not isinstance(values, list) or not all(isinstance(value, str) for value in values): + fail(f"{label} must be a list of repository-relative paths") + return [value.replace("\\", "/") for value in values] + + +def load_source_names() -> set[str]: + source_names: set[str] = set() + for path in source_pin_paths(): + data = read_toml(path) + name = data.get("name") + if not isinstance(name, str) or not name: + fail(f"{rel(path)} must declare a source name") + if name in source_names: + fail(f"duplicate source pin {name} across source metadata") + source_names.add(name) + if not source_names: + fail("source metadata must contain at least one source pin") + return source_names + + +def source_digest_inputs() -> list[str]: + source_files = [rel(path) for path in source_pin_paths()] + recipe_files = sorted( + rel(path) + for path in EXTERNAL_ROOT.glob("**/*") + if path.is_file() + and path.name != "source.toml" + and path.name not in EXTENSION_ENVELOPE_FILENAMES + ) + return [*BASE_SOURCE_DIGEST_INPUTS, *source_files, *recipe_files] + + +def source_digest(paths: list[str] | None = None) -> str: + paths = source_digest_inputs() if paths is None else paths + digest = hashlib.sha256() + for relative in paths: + path = ROOT / relative + if not path.exists(): + fail(f"source digest input is missing: {relative}") + contents = path.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n") + digest.update(relative.encode("utf-8")) + digest.update(b"\0") + digest.update(contents) + digest.update(b"\0") + return f"sha256:{digest.hexdigest()}" + + +@lru_cache(maxsize=1) +def current_git_identity() -> tuple[str, str]: + try: + commit = subprocess.check_output( + ["git", "rev-parse", "HEAD^{commit}"], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ).strip() + tree = subprocess.check_output( + ["git", "rev-parse", f"{commit}^{{tree}}"], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ).strip() + except (FileNotFoundError, subprocess.CalledProcessError) as error: + stderr = getattr(error, "stderr", "") or "" + fail(f"failed to resolve exact evidence source commit/tree: {stderr.strip() or error}") + if re.fullmatch(r"[0-9a-f]{40}", commit) is None or re.fullmatch(r"[0-9a-f]{40}", tree) is None: + fail("git returned an invalid exact evidence source commit/tree") + return commit, tree + + +def require_clean_evidence_inputs() -> None: + try: + status = subprocess.check_output( + ["git", "status", "--porcelain=v1", "--", *source_digest_inputs()], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ).strip() + except (FileNotFoundError, subprocess.CalledProcessError) as error: + stderr = getattr(error, "stderr", "") or "" + fail(f"failed to verify exact evidence source inputs: {stderr.strip() or error}") + if status: + fail( + "full WASIX evidence source inputs differ from the recorded commit; " + f"refusing dirty exact-SHA evidence:\n{status}" + ) + + +def validate_id(value: object, label: str) -> str: + if not isinstance(value, str) or ID_RE.fullmatch(value) is None: + fail(f"{label} must be a lower snake-case extension id, got {value!r}") + return value + + +def validate_sql_name(value: object, label: str) -> str: + if not isinstance(value, str) or SQL_NAME_RE.fullmatch(value) is None: + fail(f"{label} must be an exact SQL extension name, got {value!r}") + return value + + +def extension_rows(path: Path) -> list[dict]: + data = read_toml(path) + if data.get("format-version") != 1: + fail(f"{rel(path)} must use format-version = 1") + rows = data.get("extensions") + if not isinstance(rows, list) or not rows: + fail(f"{rel(path)} must define [[extensions]] rows") + return rows + + +def validate_catalog_rows() -> None: + promoted = extension_rows(PROMOTED) + smoke = extension_rows(SMOKE) + promoted_ids: set[str] = set() + for row in promoted: + extension_id = validate_id(row.get("id"), f"{rel(PROMOTED)} row id") + if extension_id in promoted_ids: + fail(f"{rel(PROMOTED)} has duplicate extension id {extension_id}") + promoted_ids.add(extension_id) + unexpected = sorted(key for key in row if "pack" in key or "bundle" in key or "alias" in key) + if unexpected: + fail(f"{rel(PROMOTED)} row {extension_id} must not use pack/bundle/alias keys: {unexpected}") + build = row.get("build", True) + stable = row.get("stable", False) + blocker = row.get("blocker") + if not isinstance(build, bool): + fail(f"{rel(PROMOTED)} row {extension_id} build must be boolean when present") + if not isinstance(stable, bool): + fail(f"{rel(PROMOTED)} row {extension_id} stable must be boolean when present") + if not build or not stable: + if not isinstance(blocker, str) or not blocker.strip(): + fail(f"{rel(PROMOTED)} row {extension_id} must explain non-release status with blocker") + elif blocker is not None: + fail( + f"{rel(PROMOTED)} row {extension_id} is buildable and stable, " + "so it must not retain a release blocker" + ) + + smoke_ids: set[str] = set() + for row in smoke: + extension_id = validate_id(row.get("id"), f"{rel(SMOKE)} row id") + if extension_id in smoke_ids: + fail(f"{rel(SMOKE)} has duplicate extension id {extension_id}") + smoke_ids.add(extension_id) + for field in ("direct", "server", "restart", "dump-restore"): + status = row.get(field, "not-run") + if status not in SMOKE_STATUSES: + fail(f"{rel(SMOKE)} row {extension_id} has invalid {field} status {status!r}") + + missing_smoke = sorted(promoted_ids - smoke_ids) + extra_smoke = sorted(smoke_ids - promoted_ids) + if missing_smoke: + fail(f"{rel(SMOKE)} is missing rows for promoted catalog ids: {missing_smoke}") + if extra_smoke: + fail(f"{rel(SMOKE)} has rows for unknown promoted catalog ids: {extra_smoke}") + + +def validate_contrib_recipe(build_plan: dict) -> None: + data = read_toml(CONTRIB_RECIPE) + if data.get("format-version") != 1: + fail(f"{rel(CONTRIB_RECIPE)} must use format-version = 1") + if data.get("postgres-version") != "18.4": + fail(f"{rel(CONTRIB_RECIPE)} must target PostgreSQL 18.4") + if data.get("source-kind") != "postgres-contrib": + fail(f"{rel(CONTRIB_RECIPE)} must declare source-kind = postgres-contrib") + if data.get("source-root") != "src/postgres/versions/18/contrib": + fail(f"{rel(CONTRIB_RECIPE)} must point at src/postgres/versions/18/contrib") + rows = data.get("extensions") + if not isinstance(rows, list) or not rows: + fail(f"{rel(CONTRIB_RECIPE)} must declare contrib extension rows") + recipe_by_id: dict[str, dict] = {} + for row in rows: + extension_id = validate_id(row.get("id"), f"{rel(CONTRIB_RECIPE)} row id") + validate_sql_name(row.get("sql-name"), f"{rel(CONTRIB_RECIPE)} row {extension_id} sql-name") + for field in ("contrib-dir", "module-file"): + if not isinstance(row.get(field), str) or not row[field]: + fail(f"{rel(CONTRIB_RECIPE)} row {extension_id} must define {field}") + data_files = row.get("data-files", []) + if not isinstance(data_files, list) or not all(isinstance(value, str) for value in data_files): + fail(f"{rel(CONTRIB_RECIPE)} row {extension_id} data-files must be an array of strings when present") + for recipe_field in ( + "mobile-static-dependencies", + "mobile-static-include-dependencies", + "mobile-static-include-dirs", + "mobile-static-cflags", + "mobile-static-hash-source-dependencies", + "mobile-static-hash-dirs", + ): + values = row.get(recipe_field, []) + if not isinstance(values, list) or not all(isinstance(value, str) and value for value in values): + fail( + f"{rel(CONTRIB_RECIPE)} row {extension_id} {recipe_field} " + "must be an array of strings when present" + ) + if extension_id in recipe_by_id: + fail(f"{rel(CONTRIB_RECIPE)} has duplicate extension id {extension_id}") + recipe_by_id[extension_id] = row + + plan_rows = [ + row for row in build_plan.get("extensions", []) if row.get("build-kind") == "postgres-contrib" + ] + plan_by_id = {validate_id(row.get("id"), f"{rel(BUILD_PLAN)} row id"): row for row in plan_rows} + if sorted(recipe_by_id) != sorted(plan_by_id): + fail( + f"{rel(CONTRIB_RECIPE)} ids must match generated contrib build plan; " + f"recipe-only={sorted(set(recipe_by_id) - set(plan_by_id))}, " + f"plan-only={sorted(set(plan_by_id) - set(recipe_by_id))}" + ) + for extension_id, plan in plan_by_id.items(): + recipe = recipe_by_id[extension_id] + expected = { + "sql-name": plan.get("sql-name"), + "contrib-dir": plan.get("contrib-dir"), + "module-file": plan.get("module-file"), + } + for field, value in expected.items(): + if recipe.get(field) != value: + fail( + f"{rel(CONTRIB_RECIPE)} row {extension_id} {field}={recipe.get(field)!r} " + f"does not match generated build plan {value!r}" + ) + + +def validate_external_recipes() -> None: + source_names = load_source_names() + build_plan = read_json(BUILD_PLAN) + build_by_sql_name = { + row.get("sql-name", row.get("id")): row + for row in build_plan.get("extensions", []) + if isinstance(row, dict) + } + validate_external_source_pins(build_by_sql_name, source_names) + for recipe in sorted(EXTERNAL_ROOT.glob("*/recipe.toml")): + 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_name(data.get("sql_name"), f"{rel(recipe)} sql_name") + if recipe.parent.name != sql_name: + fail(f"{rel(recipe)} directory name must match sql_name {sql_name}") + kind = data.get("kind") + if kind not in {"external-simple-pgxs", "external-complex"}: + fail(f"{rel(recipe)} kind must be external-simple-pgxs or external-complex") + source = data.get("source") + if source not in source_names: + fail(f"{rel(recipe)} source {source!r} must reference source metadata") + majors = data.get("postgres_majors") + if not isinstance(majors, list) or 18 not in majors: + fail(f"{rel(recipe)} must explicitly support postgres_majors including 18") + if not isinstance(data.get("license"), str) or not data["license"]: + fail(f"{rel(recipe)} must declare license metadata") + lifecycle = data.get("lifecycle") + artifacts = data.get("artifacts") + support = data.get("support") + if not isinstance(lifecycle, dict) or not isinstance(artifacts, dict) or not isinstance(support, dict): + fail(f"{rel(recipe)} must declare lifecycle, artifacts, and support tables") + runtime_environment = data.get("runtime_environment") or [] + if not isinstance(runtime_environment, list): + fail(f"{rel(recipe)} runtime_environment must be an array when present") + for index, entry in enumerate(runtime_environment): + if not isinstance(entry, dict): + fail(f"{rel(recipe)} runtime_environment[{index}] must be a table") + for field in ("name", "path", "required_file"): + if not isinstance(entry.get(field), str) or not entry[field]: + fail(f"{rel(recipe)} runtime_environment[{index}].{field} must be a non-empty string") + for field in ( + "requires", + "implicit_sql_dependencies", + "load_sql", + "post_create_sql", + "shared_preload_libraries", + ): + if not isinstance(lifecycle.get(field), list): + fail(f"{rel(recipe)} lifecycle.{field} must be an array") + for field in ( + "creates_extension", + "restart_required", + "background_workers", + "shared_memory", + "session_load_required", + "needs_superuser", + "trusted", + ): + if not isinstance(lifecycle.get(field), bool): + fail(f"{rel(recipe)} lifecycle.{field} must be boolean") + for field in ( + "control_files", + "sql_globs", + "native_modules", + "native_dependency_modules", + "data_files", + "headers", + "licenses", + ): + if not isinstance(artifacts.get(field), list): + fail(f"{rel(recipe)} artifacts.{field} must be an array") + for field in ("extension_sql_file_prefixes", "extension_sql_file_names"): + if field in artifacts and not isinstance(artifacts.get(field), list): + fail(f"{rel(recipe)} artifacts.{field} must be an array when present") + for family, claims in support.items(): + if not isinstance(claims, dict): + fail(f"{rel(recipe)} support.{family} must be a table") + for mode, status in claims.items(): + if status not in SUPPORT_STATUSES: + fail(f"{rel(recipe)} support.{family}.{mode} has invalid status {status!r}") + + blockers_path = recipe.parent / "blockers.toml" + if blockers_path.exists(): + blockers_data = read_toml(blockers_path) + if blockers_data.get("schema") != "oliphaunt-extension-blockers-v1": + fail(f"{rel(blockers_path)} must use schema = oliphaunt-extension-blockers-v1") + blocker_rows = blockers_data.get("blockers", []) + if not isinstance(blocker_rows, list): + fail(f"{rel(blockers_path)} blockers must be an array of tables") + blocker_targets: set[str] = set() + for index, blocker_row in enumerate(blocker_rows): + if not isinstance(blocker_row, dict): + fail(f"{rel(blockers_path)} blockers[{index}] must be a table") + target = blocker_row.get("target") + status = blocker_row.get("status") + reason = blocker_row.get("reason") + if not isinstance(target, str) or target not in support: + fail( + f"{rel(blockers_path)} blockers[{index}].target must name a recipe support family" + ) + if target in blocker_targets: + fail(f"{rel(blockers_path)} has duplicate blocker target {target}") + blocker_targets.add(target) + if status not in SUPPORT_STATUSES - {"supported"}: + fail( + f"{rel(blockers_path)} blockers[{index}].status must be unsupported, " + "candidate, or experimental" + ) + if not isinstance(reason, str) or not reason.strip(): + fail(f"{rel(blockers_path)} blockers[{index}].reason must be a non-empty string") + family_statuses = set(support[target].values()) + if status not in family_statuses: + fail( + f"{rel(blockers_path)} blocker {target} status {status!r} contradicts " + f"recipe support statuses {sorted(family_statuses)}" + ) + + tests = recipe.parent / "tests" + for path in (tests / "smoke.sql", tests / "upstream.toml"): + if not path.exists(): + fail(f"{rel(recipe)} must provide {rel(path)}") + if "-- oliphaunt-statement" not in (tests / "smoke.sql").read_text(encoding="utf-8"): + fail(f"{rel(tests / 'smoke.sql')} must include explicit statement delimiters") + + if kind == "external-complex": + for path in ( + recipe.parent / "deps.toml", + recipe.parent / "targets/native.toml", + recipe.parent / "targets/wasix.toml", + recipe.parent / "targets/native-static-registry.toml", + recipe.parent / "patches/README.md", + blockers_path, + ): + if not path.exists(): + fail(f"{rel(recipe)} complex recipe is missing {rel(path)}") + deps = read_toml(recipe.parent / "deps.toml") + declared_deps = [ + row.get("name") + for row in deps.get("dependencies", []) + if isinstance(row, dict) and isinstance(row.get("name"), str) + ] + if len(declared_deps) != len(set(declared_deps)): + fail(f"{rel(recipe.parent / 'deps.toml')} has duplicate dependency names") + missing_source_pins = sorted(set(declared_deps) - source_names) + if missing_source_pins: + fail( + f"{rel(recipe.parent / 'deps.toml')} references sources missing from source metadata: " + f"{missing_source_pins}" + ) + for dependency in deps.get("dependencies", []): + if not isinstance(dependency, dict) or not dependency.get("license"): + fail(f"{rel(recipe.parent / 'deps.toml')} dependencies must include license metadata") + + generated = build_by_sql_name.get(sql_name) + if generated is None: + fail(f"{rel(recipe)} has no matching generated build-plan row") + if generated.get("source-kind") != "postgis" and kind == "external-complex": + fail(f"{rel(recipe)} complex recipe must match generated source-kind postgis") + generated_modules = set(generated.get("load-order") or []) + for module in artifacts.get("native_modules", []): + if module not in generated_modules: + fail(f"{rel(recipe)} native module {module!r} must match generated load-order") + + +def validate_external_source_pins(build_by_sql_name: dict[str, dict], source_names: set[str]) -> None: + for source_path in sorted(EXTERNAL_ROOT.glob("*/source.toml")): + extension_dir = source_path.parent + sql_name = validate_sql_name(extension_dir.name, f"{rel(source_path)} directory") + source = read_toml(source_path) + name = source.get("name") + if not isinstance(name, str) or name not in source_names: + fail(f"{rel(source_path)} must declare a valid source name") + if sql_name not in build_by_sql_name: + continue + generated = build_by_sql_name[sql_name] + generated_source_dir = generated.get("source-dir") + if isinstance(generated_source_dir, str) and generated_source_dir: + expected_checkout = f"target/oliphaunt-sources/checkouts/{name}" + if generated_source_dir != expected_checkout: + fail( + f"{rel(source_path)} source name {name!r} implies checkout " + f"{expected_checkout}, but generated build plan uses {generated_source_dir}" + ) + + +def validate_extension_release_metadata() -> None: + extension_metadata_rows() + + +def extension_family(source_kind: object) -> str: + return { + "postgres-contrib": "PostgreSQL contrib", + "oliphaunt-other-extension": "External PGXS", + "postgis": "Complex external", + }.get(str(source_kind), "Other") + + +def extension_activation(extension: dict) -> str: + lifecycle = extension.get("lifecycle", {}) + create_extension = bool(lifecycle.get("create-extension")) + load_sql = lifecycle.get("load-sql") or [] + if create_extension and load_sql: + return "CREATE EXTENSION + LOAD" + if create_extension: + return "CREATE EXTENSION" + if load_sql: + return "LOAD" + return "manual" + + +def extension_version(extension: dict) -> str: + control = extension.get("control") + if isinstance(control, dict): + version = control.get("default-version") + if isinstance(version, str) and "@" not in version: + return version + return "" + + +def native_module_stem(extension: dict) -> str | None: + module_file = extension.get("native-module-file") or extension.get("module-file") + if not isinstance(module_file, str) or not module_file: + return None + for suffix in (".so", ".dylib", ".dll"): + if module_file.endswith(suffix): + return module_file[: -len(suffix)] + return module_file + + +def shared_preload_libraries(extension: dict) -> list[str]: + lifecycle = extension.get("lifecycle") or {} + values = [] + for assignment in lifecycle.get("startup-config") or []: + if not isinstance(assignment, str): + continue + key, separator, value = assignment.partition("=") + if separator and key == "shared_preload_libraries": + values.extend(part.strip() for part in value.split(",") if part.strip()) + return sorted(set(values)) + + +def extension_data_files_from_recipe(extension: dict) -> list[str]: + sql_name = extension.get("sql-name", extension.get("id")) + if not isinstance(sql_name, str): + return [] + recipe = ROOT / "src/extensions/external" / sql_name / "recipe.toml" + if not recipe.exists(): + contrib_rows = read_toml(CONTRIB_RECIPE).get("extensions") or [] + for row in contrib_rows: + if isinstance(row, dict) and row.get("sql-name") == sql_name: + data_files = row.get("data-files") or [] + return sorted(value for value in data_files if isinstance(value, str)) + return [] + artifacts = read_toml(recipe).get("artifacts") or {} + data_files = artifacts.get("data_files") or [] + return sorted(value for value in data_files if isinstance(value, str)) + + +def extension_artifact_list_from_recipe(extension: dict, field: str) -> list[str]: + sql_name = extension.get("sql-name", extension.get("id")) + if not isinstance(sql_name, str): + return [] + recipe = ROOT / "src/extensions/external" / sql_name / "recipe.toml" + if not recipe.exists(): + return [] + artifacts = read_toml(recipe).get("artifacts") or {} + values = artifacts.get(field) or [] + return sorted(value for value in values if isinstance(value, str)) + + +def extension_runtime_environment_from_recipe(extension: dict) -> list[dict[str, str]]: + sql_name = extension.get("sql-name", extension.get("id")) + if not isinstance(sql_name, str): + return [] + recipe = ROOT / "src/extensions/external" / sql_name / "recipe.toml" + if not recipe.exists(): + return [] + rows = read_toml(recipe).get("runtime_environment") or [] + env = [] + for row in rows: + if not isinstance(row, dict): + continue + name = row.get("name") + path = row.get("path") + required_file = row.get("required_file") + if all(isinstance(value, str) and value for value in (name, path, required_file)): + env.append({"name": name, "path": path, "required_file": required_file}) + return sorted(env, key=lambda row: (row["name"], row["path"], row["required_file"])) + + +def runtime_share_data_files(data_files: list[str]) -> list[str]: + prefix = "share/postgresql/" + return sorted(value[len(prefix) :] if value.startswith(prefix) else value for value in data_files) + + +def contrib_recipe_row(sql_name: str) -> dict | None: + for row in read_toml(CONTRIB_RECIPE).get("extensions") or []: + if isinstance(row, dict) and row.get("sql-name") == sql_name: + return row + return None + + +def validate_string_list(values: object, label: str) -> list[str]: + if values is None: + return [] + if not isinstance(values, list) or not all(isinstance(value, str) and value for value in values): + fail(f"{label} must be an array of non-empty strings") + return values + + +def external_mobile_dependency_archive_map(sql_name: str) -> dict[str, list[str]]: + deps_path = ROOT / "src/extensions/external" / sql_name / "deps.toml" + if not deps_path.exists(): + return {} + archive_map: dict[str, list[str]] = {} + for row in read_toml(deps_path).get("dependencies") or []: + if not isinstance(row, dict): + fail(f"{rel(deps_path)} dependencies must be tables") + name = row.get("name") + if not isinstance(name, str) or not name: + fail(f"{rel(deps_path)} dependency rows must define a name") + archives = validate_string_list( + row.get("mobile-static-dependencies"), + f"{rel(deps_path)} dependency {name} mobile-static-dependencies", + ) + archive_map[name] = archives or [name] + return archive_map + + +def expand_mobile_static_dependencies( + sql_name: str, + dependency_names: list[str], + archive_map: dict[str, list[str]], +) -> list[str]: + archives: list[str] = [] + for dependency in dependency_names: + if dependency not in archive_map: + fail(f"mobile static dependency {dependency!r} for {sql_name} has no archive mapping") + archives.extend(archive_map[dependency]) + return sorted(dict.fromkeys(archives)) + + +def external_mobile_static_dependencies(sql_name: str, field: str) -> list[str]: + target_path = ROOT / "src/extensions/external" / sql_name / "targets/native-static-registry.toml" + if not target_path.exists(): + return [] + target = read_toml(target_path) + dependencies = validate_string_list(target.get(field), f"{rel(target_path)} {field}") + if not dependencies and field != "dependencies": + dependencies = validate_string_list(target.get("dependencies"), f"{rel(target_path)} dependencies") + return expand_mobile_static_dependencies( + sql_name, + dependencies, + external_mobile_dependency_archive_map(sql_name), + ) + + +def contrib_mobile_static_dependencies(sql_name: str) -> list[str]: + row = contrib_recipe_row(sql_name) + if row is None: + return [] + return sorted( + dict.fromkeys( + validate_string_list( + row.get("mobile-static-dependencies"), + f"{rel(CONTRIB_RECIPE)} row {sql_name} mobile-static-dependencies", + ) + ) + ) + + +def mobile_static_dependencies(sql_name: str, field: str = "dependencies") -> list[str]: + external = external_mobile_static_dependencies(sql_name, field) + if external: + return external + return contrib_mobile_static_dependencies(sql_name) + + +def contrib_mobile_static_list(sql_name: str, recipe_field: str) -> list[str]: + row = contrib_recipe_row(sql_name) + if row is None: + return [] + return sorted( + dict.fromkeys( + validate_string_list( + row.get(recipe_field), + f"{rel(CONTRIB_RECIPE)} row {sql_name} {recipe_field}", + ) + ) + ) + + +def external_mobile_target_list(sql_name: str, field: str) -> list[str]: + target_path = ROOT / "src/extensions/external" / sql_name / "targets/native-static-registry.toml" + if not target_path.exists(): + return [] + target = read_toml(target_path) + return sorted( + dict.fromkeys( + validate_string_list(target.get(field), f"{rel(target_path)} {field}") + ) + ) + + +def mobile_static_include_dependencies(sql_name: str) -> list[str]: + external = external_mobile_target_list(sql_name, "include_dependencies") + if external: + return external + return contrib_mobile_static_list(sql_name, "mobile-static-include-dependencies") + + +def mobile_static_include_dirs(sql_name: str) -> list[str]: + external = external_mobile_target_list(sql_name, "include_dirs") + if external: + return external + return contrib_mobile_static_list(sql_name, "mobile-static-include-dirs") + + +def mobile_static_cflags(sql_name: str) -> list[str]: + external = external_mobile_target_list(sql_name, "cflags") + if external: + return external + return contrib_mobile_static_list(sql_name, "mobile-static-cflags") + + +def mobile_static_hash_source_dependencies(sql_name: str, field: str = "dependencies") -> list[str]: + target_path = ROOT / "src/extensions/external" / sql_name / "targets/native-static-registry.toml" + if target_path.exists(): + target_field = { + "dependencies": "dependencies", + "ios_dependencies": "ios_dependencies", + "android_dependencies": "android_dependencies", + }[field] + target = read_toml(target_path) + dependencies = validate_string_list(target.get(target_field), f"{rel(target_path)} {target_field}") + if not dependencies and target_field != "dependencies": + dependencies = validate_string_list(target.get("dependencies"), f"{rel(target_path)} dependencies") + return sorted( + dict.fromkeys( + dependencies + ) + ) + return contrib_mobile_static_list(sql_name, "mobile-static-hash-source-dependencies") + + +def mobile_static_hash_dirs(sql_name: str) -> list[str]: + external = external_mobile_target_list(sql_name, "hash_dirs") + if external: + return external + return contrib_mobile_static_list(sql_name, "mobile-static-hash-dirs") + + +def mobile_static_source_files(sql_name: str) -> list[str]: + return external_mobile_target_list(sql_name, "source_files") + + +def mobile_static_source_recursive_dirs(sql_name: str) -> list[str]: + return external_mobile_target_list(sql_name, "source_recursive_dirs") + + +def external_target_data(sql_name: str, target: str) -> dict | None: + path = ROOT / "src/extensions/external" / sql_name / "targets" / f"{target}.toml" + if not path.exists(): + return None + data = read_toml(path) + status = data.get("status") + if status not in SUPPORT_STATUSES: + fail(f"{rel(path)} status has invalid value {status!r}") + return data + + +def external_target_status(sql_name: str, target: str) -> str | None: + data = external_target_data(sql_name, target) + if data is None: + return None + return str(data["status"]) + + +def external_recipe_support(sql_name: str) -> dict: + recipe = ROOT / "src/extensions/external" / sql_name / "recipe.toml" + if not recipe.exists(): + return {} + support = read_toml(recipe).get("support") or {} + return support if isinstance(support, dict) else {} + + +def extension_target_statuses(sql_name: str) -> dict[str, str | None]: + return { + "native": external_target_status(sql_name, "native"), + "wasix": external_target_status(sql_name, "wasix"), + "mobile": external_target_status(sql_name, "mobile"), + } + + +def extension_support_statuses(sql_name: str) -> dict: + support = external_recipe_support(sql_name) + return { + family: { + mode: status + for mode, status in claims.items() + if isinstance(mode, str) and status in SUPPORT_STATUSES + } + for family, claims in support.items() + if isinstance(family, str) and isinstance(claims, dict) + } + + +def mobile_release_ready(sql_name: str) -> bool: + target_status = external_target_status(sql_name, "mobile") + if target_status is not None: + return target_status == "supported" + + mobile_support = extension_support_statuses(sql_name).get("mobile") + if mobile_support: + return all(status == "supported" for status in mobile_support.values()) + + return True + + +def desktop_release_ready(sql_name: str, promotion: dict) -> bool: + if not (bool(promotion.get("promoted")) and bool(promotion.get("stable"))): + return False + + target_status = external_target_status(sql_name, "native") + if target_status is not None: + return target_status == "supported" + + native_support = extension_support_statuses(sql_name).get("native") + if native_support: + return all(status == "supported" for status in native_support.values()) + + return True + + +def target_native_support_modules(sql_name: str, target: str) -> list[dict]: + path = ROOT / "src/extensions/external" / sql_name / "targets" / f"{target}.toml" + if not path.exists(): + return [] + rows = read_toml(path).get("native_support_modules") or [] + modules = [] + for index, row in enumerate(rows): + if not isinstance(row, dict): + fail(f"{rel(path)} native_support_modules[{index}] must be a table") + module = {} + for field in ("name", "runtime_path", "build_path", "aot_file"): + value = row.get(field) + if not isinstance(value, str) or not value: + fail(f"{rel(path)} native_support_modules[{index}] must define {field}") + module[field.replace("_", "-")] = value + modules.append(module) + modules.sort(key=lambda module: module["name"]) + return modules + + +def generated_sdk_metadata(catalog: dict, sdk: str) -> dict: + rows = [] + release_metadata = extension_metadata_by_sql_name() + public_sql_names = { + extension.get("sql-name", extension.get("id")) + for extension in catalog.get("extensions", []) + if (extension.get("promotion") or {}).get("promoted") is True + } + for extension in catalog.get("extensions", []): + promotion = extension.get("promotion") or {} + if promotion.get("promoted") is not True: + continue + data_files = extension_data_files_from_recipe(extension) + dependencies = extension.get("dependencies") or [] + sql_name = str(extension.get("sql-name", extension.get("id"))) + release = release_metadata.get(sql_name) + if release is None: + fail(f"release graph has no exact release owner for public extension {sql_name}") + row = { + "id": extension.get("id"), + "sql-name": sql_name, + "display-name": extension.get("display-name", extension.get("id")), + "postgres-major": 18, + "release-product": release["product"], + "cargo-package": release["cargoPackage"], + "npm-package": release["npmPackage"], + "maven-group": release["mavenGroup"], + "maven-artifact": release["mavenArtifact"], + "runtime-bound": release["versioning"] == "runtime-bound", + "creates-extension": bool((extension.get("lifecycle") or {}).get("create-extension")), + "native-module-stem": native_module_stem(extension), + "dependencies": dependencies, + "selected-extension-dependencies": sorted( + dependency for dependency in dependencies if dependency in public_sql_names + ), + "native-dependencies": extension.get("native-dependencies") or [], + "shared-preload-libraries": shared_preload_libraries(extension), + "data-files": data_files, + "runtime-share-data-files": runtime_share_data_files(data_files), + "extension-sql-file-prefixes": extension_artifact_list_from_recipe( + extension, "extension_sql_file_prefixes" + ), + "extension-sql-file-names": extension_artifact_list_from_recipe( + extension, "extension_sql_file_names" + ), + "runtime-environment": extension_runtime_environment_from_recipe(extension), + "public": bool(promotion.get("promoted")), + "stable": bool(promotion.get("stable")), + "desktop-release-ready": desktop_release_ready(sql_name, promotion), + "mobile-release-ready": mobile_release_ready(sql_name), + "target-status": extension_target_statuses(sql_name), + "support": extension_support_statuses(sql_name), + "source-kind": extension.get("source-kind"), + "archive": promotion.get("archive") or "", + } + if sdk == "react-native": + row["ios-static-dependencies"] = mobile_static_dependencies( + sql_name, "ios_dependencies" + ) + rows.append(row) + rows.sort(key=lambda row: (str(row["sql-name"]), str(row["id"]))) + catalog_sha256 = hashlib.sha256( + json.dumps(rows, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + return { + "format-version": 1, + "consumer": sdk, + "extension-catalog-sha256": catalog_sha256, + "generated-from": [ + {"name": "extension-catalog", "path": rel(CATALOG)}, + {"name": "extension-evidence", "path": rel(EVIDENCE_TABLE)}, + ], + "extensions": rows, + } + + +def generated_typescript_extension_module(metadata: dict) -> str: + include_ios_static_dependencies = metadata.get("consumer") == "react-native" + + def camel(row: dict) -> dict: + result = { + "id": row["id"], + "sqlName": row["sql-name"], + "displayName": row["display-name"], + "postgresMajor": row["postgres-major"], + "releaseProduct": row["release-product"], + "cargoPackage": row["cargo-package"], + "npmPackage": row["npm-package"], + "mavenGroup": row["maven-group"], + "mavenArtifact": row["maven-artifact"], + "runtimeBound": row["runtime-bound"], + "createsExtension": row["creates-extension"], + "nativeModuleStem": row["native-module-stem"], + "dependencies": row["dependencies"], + "selectedExtensionDependencies": row["selected-extension-dependencies"], + "nativeDependencies": row["native-dependencies"], + "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"], + "mobileReleaseReady": row["mobile-release-ready"], + "targetStatus": row["target-status"], + "support": row["support"], + "sourceKind": row["source-kind"], + "archive": row["archive"], + } + if include_ios_static_dependencies: + result["iosStaticDependencies"] = row["ios-static-dependencies"] + return result + + rows = [camel(row) for row in metadata.get("extensions", [])] + ios_static_dependency_type = ( + " readonly iosStaticDependencies: readonly string[];\n" + if include_ios_static_dependencies + else "" + ) + source = ( + 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" + " readonly sqlName: string;\n" + " readonly displayName: string;\n" + " readonly postgresMajor: number;\n" + " readonly releaseProduct: string;\n" + " readonly cargoPackage: string;\n" + " readonly npmPackage: string;\n" + " readonly mavenGroup: string;\n" + " readonly mavenArtifact: string;\n" + " readonly runtimeBound: boolean;\n" + " readonly createsExtension: boolean;\n" + " readonly nativeModuleStem: string | null;\n" + " readonly dependencies: readonly string[];\n" + " readonly selectedExtensionDependencies: readonly string[];\n" + " readonly nativeDependencies: readonly string[];\n" + f"{ios_static_dependency_type}" + " 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" + " readonly mobileReleaseReady: boolean;\n" + " readonly targetStatus: { readonly native?: string | null; readonly wasix?: string | null; readonly mobile?: string | null };\n" + " readonly support: Readonly>>>;\n" + " readonly sourceKind: string;\n" + " readonly archive: string;\n" + "};\n\n" + f"export const GENERATED_EXTENSION_METADATA_SHA256 = {json.dumps(metadata['extension-catalog-sha256'])} as const;\n\n" + f"export const GENERATED_EXTENSION_METADATA = {json.dumps(rows, indent=2, sort_keys=True)} as const satisfies readonly GeneratedExtensionMetadata[];\n\n" + "export function generatedExtensionBySqlName(sqlName: string): GeneratedExtensionMetadata | undefined {\n" + " return GENERATED_EXTENSION_METADATA.find((extension) => extension.sqlName === sqlName);\n" + "}\n\n" + "export function generatedSharedPreloadLibraries(extensionSqlNames: readonly string[]): string[] {\n" + " const libraries = new Set();\n" + " for (const sqlName of extensionSqlNames) {\n" + " const extension = generatedExtensionBySqlName(sqlName);\n" + " for (const library of extension?.sharedPreloadLibraries ?? []) {\n" + " libraries.add(library);\n" + " }\n" + " }\n" + " return [...libraries].sort();\n" + "}\n" + ) + return format_typescript_source(source, GENERATED_TS_SDK_MODULE) + + +def generated_kotlin_extension_module(metadata: dict) -> str: + rows = sorted(metadata.get("extensions", []), key=lambda row: str(row["sql-name"])) + body = "\n".join( + " " + + json.dumps(str(row["sql-name"])) + + " to GeneratedExtensionRuntimeContract(" + + f"createsExtension = {'true' if row['creates-extension'] else 'false'}, " + + "nativeModuleStem = " + + (json.dumps(str(row["native-module-stem"])) if row["native-module-stem"] is not None else "null") + + ")," + for row in rows + ) + return ( + f"// This file is generated by {CHECK_EXTENSION_MODEL_PATH}.\n" + "// Do not edit by hand.\n\n" + "package dev.oliphaunt\n\n" + "internal data class GeneratedExtensionRuntimeContract(\n" + " val createsExtension: Boolean,\n" + " val nativeModuleStem: String?,\n" + ")\n\n" + "internal val generatedExtensionRuntimeContracts: Map = mapOf(\n" + f"{body}\n" + ")\n\n" + "internal val generatedExtensionSqlNames: Set = generatedExtensionRuntimeContracts.keys\n\n" + "internal fun generatedExtensionSqlNameExists(sqlName: String): Boolean = generatedExtensionSqlNames.contains(sqlName)\n" + "\n" + "internal fun generatedExtensionRuntimeContract(sqlName: String): GeneratedExtensionRuntimeContract? = generatedExtensionRuntimeContracts[sqlName]\n" + ) + + +def generated_kotlin_gradle_plugin_catalog(metadata: dict) -> str: + lines = [ + f"# This file is generated by {CHECK_EXTENSION_MODEL_PATH}.", + "# Do not edit by hand.", + "schema=oliphaunt-android-extension-catalog-v1", + f"catalogSha256={metadata['extension-catalog-sha256']}", + ] + for row in sorted(metadata.get("extensions", []), key=lambda item: str(item["sql-name"])): + sql_name = str(row["sql-name"]) + prefix = f"extension.{sql_name}" + lines.extend( + [ + f"{prefix}.releaseProduct={row['release-product']}", + f"{prefix}.mavenGroup={row['maven-group']}", + f"{prefix}.mavenArtifact={row['maven-artifact']}", + f"{prefix}.runtimeBound={'true' if row['runtime-bound'] else 'false'}", + f"{prefix}.dependencies={','.join(row['selected-extension-dependencies'])}", + ] + ) + return "\n".join(lines) + "\n" + + +def rust_string_literal(value: str) -> str: + return json.dumps(value) + + +def rust_variant_from_constant(value: str) -> str: + parts = [part for part in value.split("_") if part] + if not parts: + fail(f"invalid rust extension constant {value!r}") + return "".join(part.lower().capitalize() for part in parts) + + +def rust_extension_expr(row: dict) -> str: + return f"Extension::{rust_variant_from_constant(str(row['rust-constant']))}" + + +def rust_doc_comment(text: str, *, indent: str = "") -> str: + escaped = text.replace("*/", "* /") + return "\n".join(f"{indent}/// {line}" if line else f"{indent}///" for line in escaped.splitlines()) + + +def rust_array( + values: list[str], + *, + item_indent: str = " ", + closing_indent: str = "", +) -> str: + if not values: + return "&[]" + if len(values) <= 2 and all(len(value) <= 72 for value in values): + return f"&[{', '.join(values)}]" + rendered = "".join(f"{item_indent}{value},\n" for value in values) + return "&[\n" + rendered + closing_indent + "]" + + +def rust_extension_slice( + rows: list[dict], + *, + item_indent: str = " ", + closing_indent: str = "", +) -> str: + return rust_array( + [rust_extension_expr(row) for row in rows], + item_indent=item_indent, + closing_indent=closing_indent, + ) + + +def rust_option_string(value: object) -> str: + if value is None or value == "": + return "None" + if not isinstance(value, str): + fail(f"Rust string option must be a string or null, got {value!r}") + return f"Some({rust_string_literal(value)})" + + +def rust_string_slice( + values: list[str], + *, + item_indent: str = " ", + closing_indent: str = "", +) -> str: + return rust_array( + [rust_string_literal(value) for value in values], + item_indent=item_indent, + closing_indent=closing_indent, + ) + + +def rust_runtime_environment_slice( + values: list[dict], + *, + item_indent: str = " ", + closing_indent: str = "", +) -> str: + if len(values) == 1: + value = values[0] + field_indent = item_indent + return ( + "&[ExtensionRuntimeEnvironment {\n" + f"{field_indent}name: {rust_string_literal(value['name'])},\n" + f"{field_indent}relative_path: {rust_string_literal(value['path'])},\n" + f"{field_indent}required_file: {rust_string_literal(value['required_file'])},\n" + f"{closing_indent}}}]" + ) + return rust_array( + [ + "ExtensionRuntimeEnvironment { " + f"name: {rust_string_literal(value['name'])}, " + f"relative_path: {rust_string_literal(value['path'])}, " + f"required_file: {rust_string_literal(value['required_file'])} " + "}" + for value in values + ], + item_indent=item_indent, + closing_indent=closing_indent, + ) + + +def rust_extension_dependency_slice( + values: list[str], + rows_by_sql_name: dict[str, dict], + *, + item_indent: str = " ", + closing_indent: str = "", +) -> str: + if not values: + return "&[]" + dependencies = [] + for value in values: + dependency = rows_by_sql_name.get(value) + if dependency is None: + fail(f"generated Rust dependency {value!r} is not a known Rust extension row") + dependencies.append(rust_extension_expr(dependency)) + return rust_array( + dependencies, + item_indent=item_indent, + closing_indent=closing_indent, + ) + + +def generated_rust_extension_rows(catalog: dict) -> list[dict]: + rows = [] + public_sql_names = { + extension.get("sql-name", extension.get("id")) + for extension in catalog.get("extensions", []) + if (extension.get("promotion") or {}).get("promoted") is True + } + for extension in catalog.get("extensions", []): + promotion = extension.get("promotion") or {} + if promotion.get("promoted") is not True: + continue + sql_name = str(extension.get("sql-name", extension.get("id"))) + rows.append( + { + "id": extension.get("id"), + "sql-name": sql_name, + "rust-constant": extension.get("rust-constant"), + "creates-extension": bool((extension.get("lifecycle") or {}).get("create-extension")), + "native-module-stem": native_module_stem(extension), + "selected-extension-dependencies": sorted( + dependency + for dependency in (extension.get("dependencies") or []) + if dependency in public_sql_names + ), + "runtime-share-data-files": runtime_share_data_files( + extension_data_files_from_recipe(extension) + ), + "shared-preload-libraries": shared_preload_libraries(extension), + "first-party": True, + "desktop-release-ready": desktop_release_ready(sql_name, promotion), + "mobile-release-ready": mobile_release_ready(sql_name), + "extension-sql-file-prefixes": extension_artifact_list_from_recipe( + extension, "extension_sql_file_prefixes" + ), + "extension-sql-file-names": extension_artifact_list_from_recipe( + extension, "extension_sql_file_names" + ), + "runtime-environment": extension_runtime_environment_from_recipe(extension), + "external-policy": None, + } + ) + rows.extend(RUST_INTERNAL_EXTENSION_CANDIDATES) + rows.sort(key=lambda row: str(row["sql-name"])) + for row in rows: + if not isinstance(row.get("rust-constant"), str) or not row["rust-constant"]: + fail(f"Rust generated extension row {row.get('id')} must define rust-constant") + return rows + + +def rust_match( + function_name: str, + return_type: str, + rows: list[dict], + value_for_row, +) -> str: + arms = [ + f" {rust_extension_expr(row)} => {value_for_row(row)}," + for row in rows + ] + signature = f"pub(super) const fn {function_name}(extension: Extension) -> {return_type} {{" + if len(signature) > 100: + signature = ( + f"pub(super) const fn {function_name}(\n" + " extension: Extension,\n" + f") -> {return_type} {{" + ) + return ( + "/// Generated extension metadata accessor.\n" + f"{signature}\n" + " match extension {\n" + + "\n".join(arms) + + "\n }\n" + "}\n" + ) + + +def generated_rust_extension_module(catalog: dict) -> str: + rows = generated_rust_extension_rows(catalog) + rows_by_sql_name = {str(row["sql-name"]): row for row in rows} + first_party_rows = [row for row in rows if row["first-party"]] + release_ready_rows = [row for row in rows if row.get("desktop-release-ready")] + external_rows = [row for row in rows if not row["first-party"]] + mobile_ready_rows = [row for row in rows if row["mobile-release-ready"]] + + for row in rows: + if len(row["shared-preload-libraries"]) > 1: + fail( + f"Rust Extension::required_shared_preload_library supports one library; " + f"{row['sql-name']} declared {row['shared-preload-libraries']}" + ) + + text = [ + f"// @generated by {CHECK_EXTENSION_MODEL_PATH} --write", + "// Do not edit by hand.", + "", + "use super::{", + " ExtensionArtifactPolicy, ExtensionCoverage, ExtensionManifestEntry, ExtensionModuleAsset,", + " ExtensionRedistribution, ExtensionRuntimeEnvironment, ExtensionSmokePlan, ExtensionSourceKind,", + " ExtensionSqlAsset, MobileStaticLinkStatus,", + "};", + "", + "/// Native PostgreSQL 18 extension that can be explicitly selected by an app.", + "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]", + "pub enum Extension {", + ] + + for row in rows: + doc_prefix = "PostgreSQL" + policy = row.get("external-policy") + if isinstance(policy, dict): + upstream = str(policy.get("upstream", "")) + if "pggraph" in upstream: + doc_prefix = "pgGraph" + elif "paradedb" in upstream: + doc_prefix = "ParadeDB" + text.extend( + [ + rust_doc_comment(f"{doc_prefix} `{row['sql-name']}`.", indent=" "), + f" {rust_variant_from_constant(str(row['rust-constant']))},", + ] + ) + + text.extend( + [ + "}", + "", + "/// First-party PostgreSQL 18 extensions generated from the shared catalog.", + f"pub(super) const FIRST_PARTY_PG18_SUPPORTED: &[Extension] = {rust_extension_slice(first_party_rows)};", + "/// Public release-ready PostgreSQL 18 extensions generated from the shared catalog.", + f"pub(super) const RELEASE_READY_PG18_SUPPORTED: &[Extension] = {rust_extension_slice(release_ready_rows)};", + "/// Mobile release-ready PostgreSQL 18 extensions generated from the shared catalog.", + f"pub(super) const MOBILE_RELEASE_READY_PG18_SUPPORTED: &[Extension] = {rust_extension_slice(mobile_ready_rows)};", + "/// External PostgreSQL 18 extension candidates generated from explicit metadata.", + f"pub(super) const EXTERNAL_PG18_SUPPORTED: &[Extension] = {rust_extension_slice(external_rows)};", + "/// All PostgreSQL 18 extension rows known to the Rust SDK.", + f"pub(super) const ALL_PG18_SUPPORTED: &[Extension] = {rust_extension_slice(rows)};", + "", + rust_match("sql_name", "&'static str", rows, lambda row: rust_string_literal(row["sql-name"])), + rust_match( + "native_module_stem", + "Option<&'static str>", + rows, + lambda row: rust_option_string(row["native-module-stem"]), + ), + rust_match( + "creates_extension", + "bool", + rows, + lambda row: "true" if row["creates-extension"] else "false", + ), + rust_match( + "dependencies", + "&'static [Extension]", + rows, + lambda row: rust_extension_dependency_slice( + row["selected-extension-dependencies"], + rows_by_sql_name, + item_indent=" ", + closing_indent=" ", + ), + ), + rust_match( + "desktop_release_ready", + "bool", + rows, + lambda row: "true" if row.get("desktop-release-ready") else "false", + ), + rust_match( + "mobile_release_ready", + "bool", + rows, + lambda row: "true" if row["mobile-release-ready"] else "false", + ), + rust_match( + "required_shared_preload_library", + "Option<&'static str>", + rows, + lambda row: rust_option_string( + row["shared-preload-libraries"][0] + if row["shared-preload-libraries"] + else None + ), + ), + rust_match( + "extension_data_files", + "&'static [&'static str]", + rows, + lambda row: rust_string_slice( + row["runtime-share-data-files"], + item_indent=" ", + closing_indent=" ", + ), + ), + rust_match( + "extension_sql_file_prefixes", + "&'static [&'static str]", + rows, + lambda row: rust_string_slice( + row.get("extension-sql-file-prefixes") or [], + item_indent=" ", + closing_indent=" ", + ), + ), + rust_match( + "extension_sql_file_names", + "&'static [&'static str]", + rows, + lambda row: rust_string_slice( + row.get("extension-sql-file-names") or [], + item_indent=" ", + closing_indent=" ", + ), + ), + rust_match( + "runtime_environment", + "&'static [ExtensionRuntimeEnvironment]", + rows, + lambda row: rust_runtime_environment_slice( + row.get("runtime-environment") or [], + item_indent=" ", + closing_indent=" ", + ), + ), + ] + ) + + artifact_arms = [] + for row in rows: + policy = row.get("external-policy") + if policy is None: + artifact_arms.append(f" {rust_extension_expr(row)} => ExtensionArtifactPolicy::FirstParty,") + continue + artifact_arms.append( + "\n".join( + [ + f" {rust_extension_expr(row)} => ExtensionArtifactPolicy::External {{", + f" upstream: {rust_string_literal(policy['upstream'])},", + f" license: {rust_string_literal(policy['license'])},", + f" source_kind: ExtensionSourceKind::{policy['source-kind']},", + f" redistribution: ExtensionRedistribution::{policy['redistribution']},", + f" requires_shared_preload: {'true' if policy['requires-shared-preload'] else 'false'},", + f" notes: {rust_string_literal(policy['notes'])},", + " },", + ] + ) + ) + text.append( + "/// Generated extension packaging policy accessor.\n" + "pub(super) const fn artifact_policy(extension: Extension) -> ExtensionArtifactPolicy {\n" + " match extension {\n" + + "\n".join(artifact_arms) + + "\n }\n" + "}\n" + ) + + manifest_rows = ",\n".join( + f" manifest_entry({rust_extension_expr(row)})" for row in rows + ) + text.append( + "/// Static native extension manifest generated from the shared catalog.\n" + "pub(super) const NATIVE_EXTENSION_MANIFEST: &[ExtensionManifestEntry] = &[\n" + f"{manifest_rows},\n" + "];\n" + ) + text.append( + "const fn manifest_entry(extension: Extension) -> ExtensionManifestEntry {\n" + " let module = match native_module_stem(extension) {\n" + " Some(stem) => ExtensionModuleAsset::NativeModule { stem },\n" + " None => ExtensionModuleAsset::SqlOnly,\n" + " };\n" + " let sql_assets = if creates_extension(extension) {\n" + " ExtensionSqlAsset::ControlAndSql\n" + " } else {\n" + " ExtensionSqlAsset::LoadableModuleOnly\n" + " };\n" + " let smoke = if creates_extension(extension) {\n" + " ExtensionSmokePlan::CreateExtensionCascade\n" + " } else {\n" + " ExtensionSmokePlan::LoadSharedLibrary\n" + " };\n" + " let mobile_static_link = match module {\n" + " ExtensionModuleAsset::NativeModule { .. } => MobileStaticLinkStatus::PendingRegistry,\n" + " ExtensionModuleAsset::SqlOnly => MobileStaticLinkStatus::NotRequiredSqlOnly,\n" + " };\n" + " ExtensionManifestEntry {\n" + " extension,\n" + " sql_name: sql_name(extension),\n" + " pg_major: 18,\n" + " pg18_supported: true,\n" + " creates_extension: creates_extension(extension),\n" + " sql_assets,\n" + " module,\n" + " dependencies: dependencies(extension),\n" + " data_files: extension_data_files(extension),\n" + " smoke,\n" + " coverage: ExtensionCoverage::GATED_RELEASE_MATRIX,\n" + " mobile_static_link,\n" + " artifact_policy: artifact_policy(extension),\n" + " }\n" + "}\n" + ) + + return format_rust_source("\n".join(text)) + + +def validate_generated_text_file(path: Path, expected: str, write: bool) -> None: + if write: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(expected, encoding="utf-8") + return + if not path.exists(): + 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 {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") + + +def generated_mobile_registry(catalog: dict) -> dict: + rows = [] + for extension in catalog.get("extensions", []): + promotion = extension.get("promotion") or {} + if promotion.get("promoted") is not True: + continue + stem = native_module_stem(extension) + if stem is None: + continue + rows.append( + { + "id": extension.get("id"), + "sql-name": extension.get("sql-name", extension.get("id")), + "native-module-stem": stem, + "data-files": extension_data_files_from_recipe(extension), + "native-dependencies": extension.get("native-dependencies") or [], + "static-registry-required": True, + } + ) + rows.sort(key=lambda row: (str(row["sql-name"]), str(row["id"]))) + return { + "format-version": 1, + "generated-from": [ + {"name": "extension-catalog", "path": rel(CATALOG)}, + {"name": "extension-definitions", "path": "src/extensions/external"}, + ], + "modules": rows, + } + + +def generated_mobile_static_specs(catalog: dict, build_plan: dict) -> str: + plan_by_sql_name = { + row.get("sql-name", row.get("id")): row + for row in build_plan.get("extensions", []) + if isinstance(row, dict) + } + rows = [] + for module in generated_mobile_registry(catalog)["modules"]: + sql_name = module["sql-name"] + plan = plan_by_sql_name.get(sql_name) + if plan is None: + fail(f"mobile static module {sql_name} has no generated build-plan row") + build_kind = plan.get("build-kind") + if build_kind == "postgres-contrib": + contrib_dir = plan.get("contrib-dir") + if not isinstance(contrib_dir, str) or not contrib_dir: + fail(f"mobile static contrib module {sql_name} is missing contrib-dir") + source_kind = "contrib" + source_rel = f"contrib/{contrib_dir}" + else: + source_dir = plan.get("source-dir") + if not isinstance(source_dir, str) or not source_dir: + fail(f"mobile static external module {sql_name} is missing source-dir") + source_kind = "external" + source_rel = source_dir + static_dependencies = ",".join(mobile_static_dependencies(sql_name)) + ios_static_dependencies = ",".join(mobile_static_dependencies(sql_name, "ios_dependencies")) + android_static_dependencies = ",".join(mobile_static_dependencies(sql_name, "android_dependencies")) + include_dependencies = ",".join(mobile_static_include_dependencies(sql_name)) + include_dirs = ",".join(mobile_static_include_dirs(sql_name)) + cflags = ",".join(mobile_static_cflags(sql_name)) + hash_source_dependencies = ",".join(mobile_static_hash_source_dependencies(sql_name)) + ios_hash_source_dependencies = ",".join( + mobile_static_hash_source_dependencies(sql_name, "ios_dependencies") + ) + android_hash_source_dependencies = ",".join( + mobile_static_hash_source_dependencies(sql_name, "android_dependencies") + ) + hash_dirs = ",".join(mobile_static_hash_dirs(sql_name)) + source_files = ",".join(mobile_static_source_files(sql_name)) + source_recursive_dirs = ",".join(mobile_static_source_recursive_dirs(sql_name)) + rows.append( + [ + sql_name, + module["native-module-stem"], + source_kind, + source_rel, + 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, + ] + ) + rows.sort(key=lambda row: row[0]) + lines = [ + 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" + "\tinclude-dependencies\tinclude-dirs\tcflags" + "\thash-source-dependencies\tios-hash-source-dependencies" + "\tandroid-hash-source-dependencies\thash-dirs" + "\tsource-files\tsource-recursive-dirs" + ), + *["\t".join(row).rstrip("\t") for row in rows], + "", + ] + return "\n".join(lines) + + +def generated_wasix_metadata(catalog: dict) -> dict: + rows = [] + for extension in catalog.get("extensions", []): + promotion = extension.get("promotion") or {} + if promotion.get("promoted") is not True: + continue + rows.append( + { + "id": extension.get("id"), + "sql-name": extension.get("sql-name", extension.get("id")), + "archive": promotion.get("archive") or extension.get("archive", ""), + "native-module-file": extension.get("native-module-file") or extension.get("module-file"), + "native-support-modules": target_native_support_modules( + str(extension.get("sql-name", extension.get("id"))), + "wasix", + ), + "dependencies": extension.get("dependencies") or [], + "load-order": extension.get("load-order") or [], + "lifecycle": extension.get("lifecycle") or {}, + } + ) + rows.sort(key=lambda row: (str(row["sql-name"]), str(row["id"]))) + return { + "format-version": 1, + "generated-from": [ + {"name": "extension-catalog", "path": rel(CATALOG)}, + {"name": "extension-definitions", "path": "src/extensions/external"}, + ], + "extensions": rows, + } + + +def validate_generated_file(path: Path, expected: dict, write: bool) -> None: + text = json_text(expected) + if write: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return + if not path.exists(): + 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 {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") + parsed = read_json(path) + if parsed.get("format-version") != 1: + fail(f"{rel(path)} must use format-version 1") + + +def validate_generated_sdk_metadata(catalog: dict, build_plan: dict, write: bool) -> None: + for sdk, path in GENERATED_SDKS.items(): + validate_generated_file(path, generated_sdk_metadata(catalog, sdk), write) + js_metadata = generated_sdk_metadata(catalog, "js") + kotlin_metadata = generated_sdk_metadata(catalog, "kotlin") + rn_metadata = generated_sdk_metadata(catalog, "react-native") + validate_generated_text_file( + GENERATED_RUST_SDK_MODULE, + generated_rust_extension_module(catalog), + write, + ) + validate_generated_text_file( + GENERATED_TS_SDK_MODULE, + generated_typescript_extension_module(js_metadata), + write, + ) + validate_generated_text_file( + GENERATED_RN_SDK_MODULE, + 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_text_file( + GENERATED_KOTLIN_GRADLE_PLUGIN_CATALOG, + generated_kotlin_gradle_plugin_catalog(kotlin_metadata), + write, + ) + validate_generated_file(GENERATED_RN_PLUGIN_METADATA, rn_metadata, write) + validate_generated_file(GENERATED_MOBILE_REGISTRY, generated_mobile_registry(catalog), write) + validate_generated_text_file( + GENERATED_MOBILE_STATIC_SPECS, + generated_mobile_static_specs(catalog, build_plan), + write, + ) + validate_generated_file(GENERATED_WASIX_METADATA, generated_wasix_metadata(catalog), write) + + +def generated_support_table(catalog: dict) -> dict: + rows = [] + for extension in catalog.get("extensions", []): + promotion = extension.get("promotion") or {} + lifecycle = extension.get("lifecycle") or {} + smoke = extension.get("smoke") or {} + sql_name = str(extension.get("sql-name", extension.get("id"))) + rows.append( + { + "id": extension.get("id"), + "sql-name": sql_name, + "display-name": extension.get("display-name", extension.get("id")), + "version": extension_version(extension), + "family": extension_family(extension.get("source-kind")), + "public": bool(promotion.get("promoted")), + "stable": bool(promotion.get("stable")), + "packaged": bool(promotion.get("packaged")), + "promoted": bool(promotion.get("promoted")), + "desktop-release-ready": desktop_release_ready(sql_name, promotion), + "mobile-release-ready": mobile_release_ready(sql_name), + "target-status": extension_target_statuses(sql_name), + "support": extension_support_statuses(sql_name), + "archive": promotion.get("archive") or "", + "blocker": promotion.get("blocker") or "", + "activation": extension_activation(extension), + "dependencies": extension.get("dependencies") or [], + "native-dependencies": extension.get("native-dependencies") or [], + "preload-required": bool(lifecycle.get("preload-required")), + "restart-required": bool(lifecycle.get("restart-required")), + "smoke": { + "direct": smoke.get("direct", "not-run"), + "server": smoke.get("server", "not-run"), + "restart": smoke.get("restart", "not-run"), + "dump-restore": smoke.get("dump-restore", "not-run"), + }, + } + ) + rows.sort(key=lambda row: (str(row["sql-name"]), str(row["id"]))) + return { + "format-version": 1, + "generated-from": [ + {"name": "extension-catalog", "path": rel(CATALOG)}, + {"name": "extension-build-plan", "path": rel(BUILD_PLAN)}, + {"name": "promotion-config", "path": rel(PROMOTED)}, + {"name": "smoke-evidence", "path": rel(SMOKE)}, + ], + "extensions": rows, + } + + +def json_text(value: dict) -> str: + return json.dumps(value, indent=2, sort_keys=True) + "\n" + + +def validate_support_table(catalog: dict, write: bool) -> None: + expected = json_text(generated_support_table(catalog)) + if write: + SUPPORT_TABLE.parent.mkdir(parents=True, exist_ok=True) + SUPPORT_TABLE.write_text(expected, encoding="utf-8") + return + if not SUPPORT_TABLE.exists(): + 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 {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") + if not table.get("extensions"): + fail(f"{rel(SUPPORT_TABLE)} must define extension rows") + + +def public_extensions(catalog: dict) -> list[dict]: + rows = [ + extension + for extension in catalog.get("extensions", []) + if (extension.get("promotion") or {}).get("promoted") is True + ] + rows.sort(key=lambda row: (str(row.get("sql-name", row.get("id"))), str(row.get("id")))) + return rows + + +def write_evidence_files(catalog: dict) -> None: + """Regenerate the claim matrix without mutating observed evidence runs. + + Evidence run JSON is an immutable observation. A source change that makes + an existing run stale must stay red until a runtime harness records a new + run; deriving a fresh `passed` run from catalog metadata would counterfeit + provenance. + """ + public_rows = public_extensions(catalog) + matrix_lines = [ + "format-version = 1", + "source-digest-inputs = [", + *[f' "{path}",' for path in source_digest_inputs()], + "]", + "", + ] + for extension in public_rows: + extension_id = validate_id(extension.get("id"), "public extension id") + matrix_lines.extend( + [ + "[[claims]]", + f'extension = "{extension_id}"', + "postgres-major = 18", + 'artifact-family = "wasix-runtime"', + 'platform-targets = ["portable"]', + 'runtime-modes = ["direct", "server", "restart", "dump-restore"]', + f'evidence-required = ["{WASIX_EVIDENCE_TIER}"]', + "public = true", + "", + ] + ) + EVIDENCE_MATRIX.parent.mkdir(parents=True, exist_ok=True) + EVIDENCE_MATRIX.write_text("\n".join(matrix_lines).rstrip() + "\n", encoding="utf-8") + + +def validate_evidence(catalog: dict, require_current: bool = False) -> dict: + for path in (EVIDENCE_MATRIX, EVIDENCE_RUN_SCHEMA, EVIDENCE_MATRIX_SCHEMA): + if not path.exists(): + fail(f"missing required extension evidence file: {rel(path)}") + matrix = read_toml(EVIDENCE_MATRIX) + if matrix.get("format-version") != 1: + fail(f"{rel(EVIDENCE_MATRIX)} must use format-version = 1") + digest_inputs = normalized_rel_list( + matrix.get("source-digest-inputs"), + f"{rel(EVIDENCE_MATRIX)} source-digest-inputs", + ) + if digest_inputs != source_digest_inputs(): + fail(f"{rel(EVIDENCE_MATRIX)} source-digest-inputs must match the checker contract") + public_ids = {validate_id(row.get("id"), "public catalog extension") for row in public_extensions(catalog)} + claims = matrix.get("claims") + if not isinstance(claims, list) or not claims: + fail(f"{rel(EVIDENCE_MATRIX)} must declare [[claims]]") + claim_ids: set[str] = set() + for claim in claims: + extension_id = validate_id(claim.get("extension"), f"{rel(EVIDENCE_MATRIX)} claim extension") + if claim.get("public") is not True: + continue + if extension_id in claim_ids: + fail(f"{rel(EVIDENCE_MATRIX)} has duplicate public claim for {extension_id}") + claim_ids.add(extension_id) + if claim.get("postgres-major") != 18: + fail(f"{rel(EVIDENCE_MATRIX)} claim {extension_id} must target postgres-major = 18") + for field in ("artifact-family", "platform-targets", "runtime-modes", "evidence-required"): + if field not in claim: + fail(f"{rel(EVIDENCE_MATRIX)} claim {extension_id} is missing {field}") + missing_claims = sorted(public_ids - claim_ids) + extra_claims = sorted(claim_ids - public_ids) + if missing_claims: + fail(f"{rel(EVIDENCE_MATRIX)} is missing public claims for {missing_claims}") + if extra_claims: + fail(f"{rel(EVIDENCE_MATRIX)} claims public support for non-public extensions {extra_claims}") + + current_digest = source_digest(digest_inputs) + evidence: dict[tuple[str, str, str, str], dict[str, str]] = {} + latest: dict[tuple[str, str, str, str], dict] = {} + latest_order: dict[tuple[str, str, str, str], tuple[str, str, str]] = {} + run_files = sorted(EVIDENCE_RUNS.glob("*.json")) + if not run_files: + fail(f"{rel(EVIDENCE_RUNS)} must contain evidence run JSON files") + for run_file in run_files: + run = read_json(run_file) + if run.get("schema") != "oliphaunt-extension-evidence-v1": + fail(f"{rel(run_file)} has unsupported evidence schema") + run_digest = run.get("sourceDigest") + if not isinstance(run_digest, str) or re.fullmatch(r"sha256:[0-9a-f]{64}", run_digest) is None: + fail(f"{rel(run_file)} must define a valid sourceDigest") + run_digest_inputs = normalized_rel_list( + run.get("sourceDigestInputs"), + f"{rel(run_file)} sourceDigestInputs", + ) + run_id = run.get("id") + if not isinstance(run_id, str) or not run_id: + fail(f"{rel(run_file)} must define a non-empty id") + observed_at = run.get("observedAt") + if not isinstance(observed_at, str) or not observed_at: + fail(f"{rel(run_file)} must define a non-empty observedAt") + collector = run.get("collector") + if not isinstance(collector, str) or not collector: + fail(f"{rel(run_file)} must define a non-empty collector") + run_status = run.get("status") + if run_status not in {"passed", "failed", "blocked"}: + fail(f"{rel(run_file)} has unsupported status {run_status!r}") + tier = run.get("evidenceTier") + if not isinstance(tier, str) or not tier: + fail(f"{rel(run_file)} must define evidenceTier") + source_commit = run.get("sourceCommit") + source_tree = run.get("sourceTree") + github = run.get("github") + if tier == WASIX_EVIDENCE_TIER: + if not isinstance(source_commit, str) or re.fullmatch(r"[0-9a-f]{40}", source_commit) is None: + fail(f"{rel(run_file)} {tier} evidence must define a full sourceCommit") + if not isinstance(source_tree, str) or re.fullmatch(r"[0-9a-f]{40}", source_tree) is None: + fail(f"{rel(run_file)} {tier} evidence must define a full sourceTree") + if not isinstance(github, dict): + fail(f"{rel(run_file)} {tier} evidence must define GitHub run provenance") + for field in ("repository", "workflow", "job"): + if not isinstance(github.get(field), str) or not github[field]: + fail(f"{rel(run_file)} github.{field} must be a non-empty string") + for field in ("runId", "runAttempt"): + if not isinstance(github.get(field), int) or isinstance(github[field], bool) or github[field] < 1: + fail(f"{rel(run_file)} github.{field} must be a positive integer") + results = run.get("results") + if not isinstance(results, list) or not results: + fail(f"{rel(run_file)} must define evidence results") + run_results: dict[tuple[str, str, str], dict[str, str]] = {} + for result in results: + extension_id = validate_id(result.get("extension"), f"{rel(run_file)} result extension") + sql_name = result.get("sqlName") + if not isinstance(sql_name, str) or not sql_name: + fail(f"{rel(run_file)} result {extension_id} must define sqlName") + if result.get("postgresMajor") != 18: + continue + family = result.get("artifactFamily") + target = result.get("platformTarget") + statuses = result.get("runtimeModeStatuses") + if not isinstance(family, str) or not isinstance(target, str) or not isinstance(statuses, dict): + fail(f"{rel(run_file)} result {extension_id} must define family, target, and runtimeModeStatuses") + if not statuses or any( + not isinstance(mode, str) + or not mode + or status not in SMOKE_STATUSES + for mode, status in statuses.items() + ): + fail(f"{rel(run_file)} result {extension_id} has invalid runtimeModeStatuses") + run_key = (extension_id, family, target) + if run_key in run_results: + fail( + f"{rel(run_file)} has duplicate result for " + f"{extension_id} {family}/{target}" + ) + run_results[run_key] = statuses + + # Evidence observations are immutable history. A run for an older + # semantic input set remains valid history, but it cannot satisfy a + # current exact-candidate support claim. + if ( + run_digest != current_digest + or run_digest_inputs != digest_inputs + or run_status != "passed" + ): + continue + if tier == WASIX_EVIDENCE_TIER: + current_commit, current_tree = current_git_identity() + if source_commit != current_commit or source_tree != current_tree: + continue + for (extension_id, family, target), statuses in run_results.items(): + key = (extension_id, tier, family, target) + order = (observed_at, run_id, rel(run_file)) + if order <= latest_order.get(key, ("", "", "")): + continue + evidence[key] = statuses + latest_order[key] = order + latest[key] = { + "run-id": run_id, + "run-path": rel(run_file), + "evidence-tier": tier, + "artifact-family": family, + "platform-target": target, + "source-digest": current_digest, + "source-commit": source_commit, + "source-tree": source_tree, + "github": github, + "observed-at": observed_at, + "runtime-mode-statuses": statuses, + } + + claim_rows = [] + for claim in claims: + if claim.get("public") is not True: + continue + extension_id = claim["extension"] + tiers = claim["evidence-required"] + targets = claim["platform-targets"] + modes = claim["runtime-modes"] + family = claim["artifact-family"] + if not isinstance(tiers, list) or not isinstance(targets, list) or not isinstance(modes, list): + fail(f"{rel(EVIDENCE_MATRIX)} claim {extension_id} has invalid evidence target arrays") + accepted = [] + missing = [] + for tier in tiers: + for target in targets: + statuses = evidence.get((extension_id, tier, family, target)) + missing_modes = [mode for mode in modes if statuses is None or statuses.get(mode) != "passed"] + if missing_modes: + missing.append( + { + "evidence-tier": tier, + "artifact-family": family, + "platform-target": target, + "runtime-modes": missing_modes, + } + ) + continue + accepted.append(latest[(extension_id, tier, family, target)]) + if require_current and missing: + first = missing[0] + fail( + f"public extension claim {extension_id} lacks current exact-candidate " + f"{first['evidence-tier']} evidence for " + f"{first['artifact-family']}/{first['platform-target']} modes " + f"{first['runtime-modes']}" + ) + catalog_row = next((row for row in catalog.get("extensions", []) if row.get("id") == extension_id), {}) + claim_rows.append( + { + "extension": extension_id, + "sql-name": catalog_row.get("sql-name", extension_id), + "public": True, + "postgres-major": claim.get("postgres-major"), + "artifact-family": family, + "platform-targets": targets, + "runtime-modes": modes, + "evidence-required": tiers, + "qualification-status": "qualified" if not missing else "requires-exact-candidate-ci", + "latest-accepted-evidence": accepted, + "missing-current-evidence": missing, + } + ) + + claim_rows.sort(key=lambda row: (str(row["sql-name"]), str(row["extension"]))) + qualified = all(row["qualification-status"] == "qualified" for row in claim_rows) + return { + "format-version": 1, + "qualification-authority": { + "kind": "exact-sha-ci", + "collector": "src/extensions/tools/collect-wasix-evidence.sh", + "current-source-status": "qualified" if qualified else "requires-exact-candidate-ci", + }, + "generated-from": [ + {"name": "extension-catalog", "path": rel(CATALOG)}, + {"name": "evidence-matrix", "path": rel(EVIDENCE_MATRIX)}, + {"name": "evidence-runs", "path": rel(EVIDENCE_RUNS)}, + ], + "source-digest": current_digest, + "source-digest-inputs": digest_inputs, + "claims": claim_rows, + } + + +def record_wasix_evidence_run(catalog: dict, run_id: str, observed_at: str) -> None: + if re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}Z-[a-z0-9-]+", run_id) is None: + fail( + "--record-wasix-evidence-run must use " + "YYYY-MM-DDTHHMMSSZ-lower-kebab-case" + ) + if re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z", observed_at) is None: + fail("--observed-at must use UTC YYYY-MM-DDTHH:MM:SSZ") + output = EVIDENCE_RUNS / f"{run_id}.json" + if output.exists(): + fail(f"refusing to overwrite immutable evidence run {rel(output)}") + + required_github = { + "repository": os.environ.get("GITHUB_REPOSITORY", ""), + "workflow": os.environ.get("GITHUB_WORKFLOW", ""), + "job": os.environ.get("GITHUB_JOB", ""), + "runId": os.environ.get("GITHUB_RUN_ID", ""), + "runAttempt": os.environ.get("GITHUB_RUN_ATTEMPT", ""), + } + if os.environ.get("GITHUB_ACTIONS") != "true": + fail("full WASIX release evidence may only be recorded by the exact-candidate GitHub Actions collector") + for field in ("repository", "workflow", "job"): + if not required_github[field]: + fail(f"GitHub Actions evidence is missing {field}") + for field in ("runId", "runAttempt"): + if not str(required_github[field]).isdigit() or int(str(required_github[field])) < 1: + fail(f"GitHub Actions evidence has invalid {field}") + source_commit, source_tree = current_git_identity() + candidate_sha = os.environ.get("CI_HEAD_SHA", "") + if candidate_sha != source_commit: + fail(f"GitHub Actions CI_HEAD_SHA {candidate_sha!r} does not match checkout HEAD {source_commit}") + require_clean_evidence_inputs() + + results = [] + for extension in public_extensions(catalog): + results.append( + { + "extension": extension.get("id"), + "sqlName": extension.get("sql-name", extension.get("id")), + "postgresMajor": 18, + "artifactFamily": "wasix-runtime", + "platformTarget": "portable", + "runtimeModeStatuses": { + "direct": "passed", + "server": "passed", + "restart": "passed", + "dump-restore": "passed", + }, + } + ) + run = { + "schema": "oliphaunt-extension-evidence-v1", + "id": run_id, + "evidenceTier": WASIX_EVIDENCE_TIER, + "status": "passed", + "sourceDigest": source_digest(), + "sourceDigestInputs": source_digest_inputs(), + "sourceCommit": source_commit, + "sourceTree": source_tree, + "observedAt": observed_at, + "collector": "src/extensions/tools/collect-wasix-evidence.sh", + "github": { + "repository": required_github["repository"], + "workflow": required_github["workflow"], + "runId": int(str(required_github["runId"])), + "runAttempt": int(str(required_github["runAttempt"])), + "job": required_github["job"], + }, + "notes": ( + "Recorded only after the full WASIX public-extension direct, server, " + "restart, materialization, and dump/restore suites succeeded." + ), + "results": results, + } + EVIDENCE_RUNS.mkdir(parents=True, exist_ok=True) + output.write_text(json_text(run), encoding="utf-8") + + +def evidence_table_text(catalog: dict, require_current: bool = False) -> str: + table = validate_evidence(catalog, require_current=require_current) + if table.get("format-version") != 1: + fail(f"generated {rel(EVIDENCE_TABLE)} must use format-version 1") + if not table.get("claims"): + fail(f"generated {rel(EVIDENCE_TABLE)} must define public evidence claims") + return json_text(table) + + +def write_evidence_summary(catalog: dict, require_current: bool = False) -> None: + """Rewrite only the deterministic evidence summary. + + The claim matrix and observed evidence run records are inputs to this + projection. They are never created, updated, or removed here. A stale run + remains immutable history and cannot become current by regenerating this + summary. + """ + expected = evidence_table_text(catalog, require_current=require_current) + EVIDENCE_TABLE.parent.mkdir(parents=True, exist_ok=True) + EVIDENCE_TABLE.write_text(expected, encoding="utf-8") + + +def validate_evidence_table(catalog: dict, write: bool, require_current: bool = False) -> None: + expected = evidence_table_text(catalog, require_current=require_current) + if write: + EVIDENCE_TABLE.parent.mkdir(parents=True, exist_ok=True) + EVIDENCE_TABLE.write_text(expected, encoding="utf-8") + return + if not EVIDENCE_TABLE.exists(): + fail( + f"{rel(EVIDENCE_TABLE)} is missing; run " + f"{CHECK_EXTENSION_MODEL_WRITE_EVIDENCE_SUMMARY_COMMAND}" + ) + actual = EVIDENCE_TABLE.read_text(encoding="utf-8") + if actual != expected: + fail( + f"{rel(EVIDENCE_TABLE)} is stale; run " + f"{CHECK_EXTENSION_MODEL_WRITE_EVIDENCE_SUMMARY_COMMAND}" + ) + table = read_json(EVIDENCE_TABLE) + if table.get("format-version") != 1: + fail(f"{rel(EVIDENCE_TABLE)} must use format-version 1") + if not table.get("claims"): + fail(f"{rel(EVIDENCE_TABLE)} must define public evidence claims") + + +def run_xtask_check() -> None: + result = subprocess.run( + ["cargo", "run", "-p", "xtask", "--", "extensions", "check"], + cwd=ROOT, + check=False, + ) + if result.returncode != 0: + raise SystemExit(result.returncode) + + +def self_test() -> None: + digest_inputs = set(source_digest_inputs()) + for path in [ + "src/extensions/external/vector/.release-semantic-inputs.json", + "src/extensions/external/vector/VERSION", + "src/extensions/external/vector/CHANGELOG.md", + "src/extensions/external/vector/release.toml", + "src/extensions/external/vector/targets/artifacts.toml", + ]: + if path in digest_inputs: + fail(f"self-test expected release/package envelope metadata to be excluded from source digest inputs: {path}") + for path in [ + "src/extensions/external/postgis/recipe.toml", + "src/extensions/external/postgis/deps.toml", + ]: + if path not in digest_inputs: + fail(f"self-test expected source recipe input to stay in source digest inputs: {path}") + + with TemporaryDirectory() as tmp: + bad = Path(tmp) / "bad.toml" + bad.write_text( + 'format-version = 1\n\n[[extensions]]\nid = "vector"\n\n[[extensions]]\nid = "vector"\n', + encoding="utf-8", + ) + try: + original = globals()["PROMOTED"] + globals()["PROMOTED"] = bad + validate_catalog_rows() + except SystemExit: + pass + else: + fail("self-test expected duplicate extension id to fail") + finally: + globals()["PROMOTED"] = original + + originals = { + "EVIDENCE_MATRIX": globals()["EVIDENCE_MATRIX"], + "EVIDENCE_RUN_SCHEMA": globals()["EVIDENCE_RUN_SCHEMA"], + "EVIDENCE_MATRIX_SCHEMA": globals()["EVIDENCE_MATRIX_SCHEMA"], + "EVIDENCE_RUNS": globals()["EVIDENCE_RUNS"], + "EVIDENCE_TABLE": globals()["EVIDENCE_TABLE"], + } + catalog = {"extensions": [{"id": "vector", "sql-name": "vector", "promotion": {"promoted": True}}]} + try: + with TemporaryDirectory() as tmp: + root = Path(tmp) + globals()["EVIDENCE_MATRIX"] = root / "missing.toml" + globals()["EVIDENCE_RUN_SCHEMA"] = root / "run.schema.json" + globals()["EVIDENCE_MATRIX_SCHEMA"] = root / "matrix.schema.json" + globals()["EVIDENCE_RUNS"] = root / "runs" + globals()["EVIDENCE_RUN_SCHEMA"].write_text("{}\n", encoding="utf-8") + globals()["EVIDENCE_MATRIX_SCHEMA"].write_text("{}\n", encoding="utf-8") + globals()["EVIDENCE_RUNS"].mkdir() + try: + validate_evidence(catalog) + except SystemExit: + pass + else: + fail("self-test expected missing evidence matrix to fail") + + with TemporaryDirectory() as tmp: + root = Path(tmp) + globals()["EVIDENCE_MATRIX"] = root / "matrix.toml" + globals()["EVIDENCE_RUN_SCHEMA"] = root / "run.schema.json" + globals()["EVIDENCE_MATRIX_SCHEMA"] = root / "matrix.schema.json" + globals()["EVIDENCE_RUNS"] = root / "runs" + globals()["EVIDENCE_TABLE"] = root / "generated" / "extension-evidence.json" + globals()["EVIDENCE_RUNS"].mkdir() + globals()["EVIDENCE_RUN_SCHEMA"].write_text("{}\n", encoding="utf-8") + globals()["EVIDENCE_MATRIX_SCHEMA"].write_text("{}\n", encoding="utf-8") + globals()["EVIDENCE_MATRIX"].write_text( + "\n".join( + [ + "format-version = 1", + "source-digest-inputs = [", + *[f' "{path}",' for path in source_digest_inputs()], + "]", + "", + "[[claims]]", + 'extension = "vector"', + "postgres-major = 18", + 'artifact-family = "wasix-runtime"', + 'platform-targets = ["portable"]', + 'runtime-modes = ["direct"]', + 'evidence-required = ["self-test"]', + "public = true", + "", + ] + ), + encoding="utf-8", + ) + stale_run = globals()["EVIDENCE_RUNS"] / "stale.json" + stale_run.write_text( + json_text( + { + "schema": "oliphaunt-extension-evidence-v1", + "id": "stale", + "evidenceTier": "self-test", + "status": "passed", + "sourceDigest": f"sha256:{'0' * 64}", + "sourceDigestInputs": source_digest_inputs(), + "observedAt": "2026-01-01T00:00:00Z", + "collector": "self-test", + "results": [ + { + "extension": "vector", + "sqlName": "vector", + "postgresMajor": 18, + "artifactFamily": "wasix-runtime", + "platformTarget": "portable", + "runtimeModeStatuses": {"direct": "passed"}, + } + ], + } + ), + encoding="utf-8", + ) + table = validate_evidence(catalog) + if table["qualification-authority"]["current-source-status"] != "requires-exact-candidate-ci": + fail("self-test expected stale evidence to remain history without qualifying current source") + try: + validate_evidence(catalog, require_current=True) + except SystemExit: + pass + else: + fail("self-test expected exact-candidate qualification to reject stale evidence") + + matrix_before = globals()["EVIDENCE_MATRIX"].read_bytes() + run_before = stale_run.read_bytes() + write_evidence_summary(catalog) + if globals()["EVIDENCE_MATRIX"].read_bytes() != matrix_before: + fail("self-test evidence-summary write mutated the claim matrix") + if stale_run.read_bytes() != run_before: + fail("self-test evidence-summary write mutated an observed run") + written = read_json(globals()["EVIDENCE_TABLE"]) + if written != table: + fail("self-test evidence-summary write did not persist the validated projection") + actual_files = { + path.relative_to(root).as_posix() + for path in root.rglob("*") + if path.is_file() + } + expected_files = { + "generated/extension-evidence.json", + "matrix.schema.json", + "matrix.toml", + "run.schema.json", + "runs/stale.json", + } + if actual_files != expected_files: + fail( + "self-test evidence-summary write changed the evidence file inventory: " + f"missing={sorted(expected_files - actual_files)}, " + f"extra={sorted(actual_files - expected_files)}" + ) + finally: + for name, value in originals.items(): + globals()[name] = value + + +def main() -> None: + parser = argparse.ArgumentParser() + mutation = parser.add_mutually_exclusive_group() + mutation.add_argument( + "--write", + action="store_true", + help="regenerate all derived extension metadata", + ) + mutation.add_argument( + "--write-evidence", + action="store_true", + help=( + "regenerate the evidence claim matrix and deterministic summary; observed run files " + "are immutable" + ), + ) + mutation.add_argument( + "--write-evidence-summary", + action="store_true", + help=( + "rewrite only generated extension-evidence.json from the claim matrix and immutable " + "observed runs; never mutate either input" + ), + ) + mutation.add_argument( + "--record-wasix-evidence-run", + metavar="RUN_ID", + help="record an immutable full WASIX lifecycle run after the collector succeeds", + ) + parser.add_argument("--observed-at", help="UTC timestamp for --record-wasix-evidence-run") + parser.add_argument( + "--require-current-evidence", + action="store_true", + help="fail unless every public claim has passing evidence for the current semantic source digest", + ) + parser.add_argument("--check", action="store_true", help="validate generated files without writing") + parser.add_argument("--self-test", action="store_true", help="run negative validation tests") + args = parser.parse_args() + + if args.self_test: + self_test() + + for path in ( + RECIPE_SCHEMA, + SUPPORT_SCHEMA, + PROMOTED, + SMOKE, + SOURCE_CATALOG, + CATALOG, + BUILD_PLAN, + CONTRIB_RECIPE, + ): + if not path.exists(): + fail(f"missing required extension model file: {rel(path)}") + + validate_catalog_rows() + catalog = read_json(CATALOG) + build_plan = read_json(BUILD_PLAN) + if args.record_wasix_evidence_run: + if not args.observed_at: + fail("--record-wasix-evidence-run requires --observed-at") + record_wasix_evidence_run(catalog, args.record_wasix_evidence_run, args.observed_at) + write_evidence_files(catalog) + elif args.observed_at: + fail("--observed-at requires --record-wasix-evidence-run") + if args.write_evidence: + write_evidence_files(catalog) + validate_extension_release_metadata() + validate_contrib_recipe(build_plan) + validate_external_recipes() + validate_support_table(catalog, write=args.write) + evidence_summary_pending = args.write_evidence_summary + if evidence_summary_pending: + # Validate the complete projection now, but defer the only write until + # every other model and xtask check has passed. + evidence_table_text(catalog, require_current=args.require_current_evidence) + else: + validate_evidence_table( + catalog, + write=args.write or args.write_evidence or bool(args.record_wasix_evidence_run), + require_current=args.require_current_evidence, + ) + validate_generated_sdk_metadata(catalog, build_plan, write=args.write) + if not args.write: + run_xtask_check() + if evidence_summary_pending: + write_evidence_summary(catalog, require_current=args.require_current_evidence) + print("extension model checks passed") + + +if __name__ == "__main__": + main() diff --git a/src/extensions/tools/check-extension-tree.mjs b/src/extensions/tools/check-extension-tree.mjs new file mode 100755 index 00000000..cfd79596 --- /dev/null +++ b/src/extensions/tools/check-extension-tree.mjs @@ -0,0 +1,283 @@ +#!/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'; +const EXTENSION_ARTIFACT_TARGET_PROFILE_SCHEMA = 'oliphaunt-extension-artifact-target-profiles-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`); + } + const release = resolve(path, 'release.toml'); + if (!existsSync(release)) { + fail(`${rel(path)} must own the linked contrib bundle release.toml`); + } + const releaseData = await parseToml(release); + if (releaseData.kind !== 'exact-extension-bundle') { + fail(`${rel(release)} must declare kind = 'exact-extension-bundle'`); + } + const expectedSqlNames = data.extensions.map((row) => row?.['sql-name']).sort(); + const declaredSqlNames = releaseData.extension_sql_names; + if ( + !Array.isArray(declaredSqlNames) || + JSON.stringify(declaredSqlNames) !== JSON.stringify(expectedSqlNames) + ) { + fail(`${rel(release)} extension_sql_names must exactly match sorted postgres18.toml SQL names`); + } + 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 checkContribMember(path) { + 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`); + } + const release = resolve(path, 'release.toml'); + const version = resolve(path, 'VERSION'); + const changelog = resolve(path, 'CHANGELOG.md'); + for (const stale of [release, version, changelog]) { + if (existsSync(stale)) { + fail(`${rel(stale)} is stale per-member release metadata; contrib members are versioned by src/extensions/contrib/release.toml`); + } + } + const artifactTargets = resolve(path, 'targets', 'artifacts.toml'); + if (!existsSync(artifactTargets)) { + fail(`${rel(path)} must preserve fail-closed member target evidence in targets/artifacts.toml`); + } + await checkArtifactTargetOverride(artifactTargets); + 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)}`); + } + const hasTargets = Array.isArray(targetData.targets) && targetData.targets.length > 0; + const hasProfiles = Array.isArray(targetData.profiles) && targetData.profiles.length > 0; + if (!hasTargets && !hasProfiles) { + fail(`${rel(artifactTargets)} must opt into at least one canonical profile or define [[targets]] rows`); + } + if (targetData.profiles !== undefined) { + await checkArtifactTargetProfiles(artifactTargets, targetData); + } +} + +async function checkArtifactTargetProfiles(artifactTargets, targetData) { + const relative = rel(artifactTargets); + if ( + !Array.isArray(targetData.profiles) || + targetData.profiles.some((profile) => typeof profile !== 'string' || profile.length === 0) + ) { + fail(`${relative} profiles must be a list of non-empty profile ids`); + } + const profilePath = resolve(ROOT, 'tools/release/extension-target-profiles.toml'); + const profileData = await parseToml(profilePath); + if (profileData.schema !== EXTENSION_ARTIFACT_TARGET_PROFILE_SCHEMA || !Array.isArray(profileData.profiles)) { + fail(`${rel(profilePath)} must use schema ${EXTENSION_ARTIFACT_TARGET_PROFILE_SCHEMA} and define [[profiles]]`); + } + const knownProfiles = new Set(); + for (const profile of profileData.profiles) { + if (!isRecord(profile) || typeof profile.id !== 'string' || profile.id.length === 0 || !Array.isArray(profile.targets) || profile.targets.length === 0) { + fail(`${rel(profilePath)} profiles must define non-empty id and targets`); + } + if (knownProfiles.has(profile.id)) { + fail(`${rel(profilePath)} profile ${profile.id} must be unique`); + } + knownProfiles.add(profile.id); + } + if (!isRecord(targetData.evidence)) { + fail(`${relative} must define [evidence.] for every selected profile`); + } + const selectedProfiles = new Set(); + for (const profileId of targetData.profiles) { + if (selectedProfiles.has(profileId)) { + fail(`${relative} selects duplicate profile ${profileId}`); + } + selectedProfiles.add(profileId); + if (!knownProfiles.has(profileId)) { + fail(`${relative} selects unknown extension target profile ${profileId}`); + } + const evidence = targetData.evidence[profileId]; + if (!isRecord(evidence) || typeof evidence.kind !== 'string' || evidence.kind.length === 0 || typeof evidence.reference !== 'string' || evidence.reference.length === 0) { + fail(`${relative} is missing non-empty evidence kind/reference for profile ${profileId}`); + } + } + const staleEvidence = Object.keys(targetData.evidence).filter((profileId) => !selectedProfiles.has(profileId)).sort(); + if (staleEvidence.length > 0) { + fail(`${relative} defines evidence for unselected profiles: ${staleEvidence.join(', ')}`); + } +} + +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 checkContribMember(path); + } 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/collect-wasix-evidence.sh b/src/extensions/tools/collect-wasix-evidence.sh new file mode 100755 index 00000000..919787af --- /dev/null +++ b/src/extensions/tools/collect-wasix-evidence.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +root="$(git -C "$script_dir" rev-parse --show-toplevel)" +cd "$root" + +for command in pnpm python3 rustfmt; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "missing required evidence command: $command" >&2 + exit 1 + fi +done + +for name in GITHUB_ACTIONS GITHUB_REPOSITORY GITHUB_WORKFLOW GITHUB_RUN_ID GITHUB_RUN_ATTEMPT GITHUB_JOB CI_HEAD_SHA; do + if [ -z "${!name:-}" ]; then + echo "$name is required; full release evidence is recorded only by exact-candidate GitHub Actions" >&2 + exit 1 + fi +done +if [ "$GITHUB_ACTIONS" != "true" ] || [ "$(git rev-parse 'HEAD^{commit}')" != "$CI_HEAD_SHA" ]; then + echo "the evidence collector checkout must equal CI_HEAD_SHA exactly" >&2 + exit 1 +fi + +run_id="${1:-$(date -u +%Y-%m-%dT%H%M%SZ)-wasix-full-lifecycle}" +observed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +# This command exercises every promoted extension in direct, server, restart, +# materialization, and dump/restore modes. The record command is deliberately +# after it so a failing or interrupted run cannot produce passed evidence. +bash src/runtimes/liboliphaunt/wasix/tools/runtime-smoke.sh regression +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs \ + --record-wasix-evidence-run "$run_id" \ + --observed-at "$observed_at" +tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check --require-current-evidence + +echo "recorded immutable WASIX extension evidence run: $run_id" diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 3a3c750b..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -#![doc = include_str!("../README.md")] -#![deny(unsafe_code)] - -mod pglite; -mod protocol; - -#[cfg(feature = "extensions")] -pub use pglite::extensions; - -#[cfg(feature = "extensions")] -pub use pglite::PgDumpOptions; -pub use pglite::{ - DataDirArchiveFormat, DataTransferContainer, DescribeQueryParam, DescribeQueryResult, - DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, GlobalListenerHandle, - ListenerHandle, NoticeCallback, ParserMap, Pglite, PgliteBuilder, PgliteError, PgliteServer, - PgliteServerBuilder, PostgresConfig, QueryOptions, QueryTemplate, Results, RowMode, Serializer, - SerializerMap, TemplatedQuery, Transaction, TypeParser, format_query, quote_identifier, -}; -pub use protocol::messages::{BackendMessage, DatabaseError, NoticeMessage}; - -#[doc(hidden)] -pub use pglite::{ - DebugLevel, FsTraceSnapshot, InstallOptions, InstallOutcome, MountInfo, PgDataTemplate, - PgDataTemplateManifest, PglitePaths, PgliteProxy, PhaseTiming, ProtocolStatsSnapshot, - build_pgdata_template, capture_phase_timings, disable_protocol_stats, ensure_cluster, - fs_trace_snapshot, install_and_init, install_and_init_in, install_default, - install_extension_archive, install_extension_bytes, install_into, install_with_options, - measure_phase, preload_runtime_module, protocol_stats_snapshot, record_phase_timing, - reset_fs_trace, reset_protocol_stats, -}; diff --git a/src/pglite/aot.rs b/src/pglite/aot.rs deleted file mode 100644 index 9a92f758..00000000 --- a/src/pglite/aot.rs +++ /dev/null @@ -1,743 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::io::{Cursor, Read}; -use std::path::{Path, PathBuf}; -use std::sync::{Mutex, OnceLock}; - -use anyhow::{Context, Result, bail, ensure}; -use directories::ProjectDirs; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use wasmer::sys::{EngineBuilder, Features, NativeEngineExt}; -use wasmer::{Engine, Module}; -use zstd::stream::read::Decoder as ZstdDecoder; - -#[cfg(feature = "extensions")] -use super::extensions::Extension; -use super::timing; - -const RUNTIME_ARTIFACT: &str = "runtime:pglite"; -const EXPECTED_AOT_ENGINE: &str = "llvm-opta"; -const EXPECTED_WASMER_VERSION: &str = "7.2.0-alpha.3"; -const EXPECTED_WASMER_WASIX_VERSION: &str = "0.702.0-alpha.3"; -const AOT_ENGINE_ID: &str = concat!( - "engine=", - "llvm-opta", - ";wasmer=", - "7.2.0-alpha.3", - ";wasmer-wasix=", - "0.702.0-alpha.3", - ";cpu=generic-baseline" -); -const ZSTD_MAGIC: &[u8] = &[0x28, 0xb5, 0x2f, 0xfd]; -const CACHE_RECEIPT_FORMAT_VERSION: u32 = 1; -static AOT_INSTALL_LOCK: OnceLock> = OnceLock::new(); -static HEADLESS_ENGINE: OnceLock = OnceLock::new(); -static INSTALLED_ARTIFACTS: OnceLock>> = OnceLock::new(); -static MODULE_CACHE: OnceLock>> = OnceLock::new(); - -#[derive(Debug, Clone)] -struct InstalledArtifact { - path: PathBuf, - sha256: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum AotVerifyMode { - Fast, - Full, -} - -pub(crate) fn headless_engine() -> Engine { - HEADLESS_ENGINE - .get_or_init(|| { - let _phase = timing::phase("wasmer.headless_engine"); - let mut features = Features::new(); - features.exceptions(true); - EngineBuilder::headless() - .set_features(Some(features)) - .engine() - .into() - }) - .clone() -} - -pub(crate) fn load_runtime_module() -> Result<(Engine, Module)> { - let engine = headless_engine(); - let module = load_artifact_module(&engine, RUNTIME_ARTIFACT)?; - Ok((engine, module)) -} - -pub(crate) fn engine_identity() -> &'static str { - AOT_ENGINE_ID -} - -pub(crate) fn preload_runtime_artifact() -> Result<()> { - let _ = load_runtime_module()?; - Ok(()) -} - -#[cfg(feature = "extensions")] -pub(crate) fn preload_extension_artifact(extension: Extension) -> Result<()> { - let engine = headless_engine(); - let _ = load_extension_module(&engine, extension)?; - Ok(()) -} - -#[cfg(feature = "extensions")] -pub(crate) fn load_extension_module( - engine: &Engine, - extension: Extension, -) -> Result> { - let Some(aot_name) = extension.aot_name() else { - return Ok(None); - }; - load_artifact_module(engine, aot_name).map(Some) -} - -pub(crate) fn load_artifact_module(engine: &Engine, artifact_name: &str) -> Result { - let artifact = install_artifact(artifact_name)?; - let cache_key = format!("{artifact_name}:{}", artifact.sha256); - let module_cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new())); - let mut modules = module_cache.lock().expect("AOT module cache poisoned"); - if let Some(module) = modules.get(&cache_key) { - return Ok(module.clone()); - } - - let module = match deserialize_headless(engine, &artifact.path) { - Ok(module) => module, - Err(err) if aot_verify_mode()? == AotVerifyMode::Fast => { - let _phase = timing::phase("aot.rebuild_after_deserialize_failure"); - forget_installed_artifact(artifact_name); - remove_cached_artifact(&artifact.path)?; - let artifact = rebuild_artifact(artifact_name).with_context(|| { - format!("rebuild AOT artifact '{artifact_name}' after deserialize failure") - })?; - deserialize_headless(engine, &artifact.path).with_context(|| { - format!( - "deserialize rebuilt Wasmer AOT artifact '{}' after initial failure: {err:#}", - artifact.path.display() - ) - })? - } - Err(err) => return Err(err), - }; - modules.insert(cache_key, module.clone()); - Ok(module) -} - -#[cfg(feature = "extensions")] -pub(crate) fn load_pg_dump_module(engine: &Engine) -> Result { - load_artifact_module(engine, "tool:pg_dump") -} - -#[cfg(feature = "extensions")] -#[allow(dead_code)] -pub(crate) fn load_initdb_module(engine: &Engine) -> Result { - load_artifact_module(engine, "tool:initdb") -} - -fn install_artifact(name: &str) -> Result { - if let Some(artifact) = installed_artifact(name) { - return Ok(artifact); - } - - let _guard = AOT_INSTALL_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .expect("AOT install lock poisoned"); - if let Some(artifact) = installed_artifact(name) { - return Ok(artifact); - } - - let manifest_artifact = { - let _phase = timing::phase("aot.manifest_validation"); - target_manifest_artifact(name)? - }; - let verify_mode = aot_verify_mode()?; - - if let Some(artifact) = cached_raw_artifact(name, &manifest_artifact, verify_mode)? { - remember_installed_artifact(name, artifact.clone()); - return Ok(artifact); - } - - let artifact = materialize_artifact(name, &manifest_artifact, verify_mode)?; - remember_installed_artifact(name, artifact.clone()); - Ok(artifact) -} - -fn rebuild_artifact(name: &str) -> Result { - let _guard = AOT_INSTALL_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .expect("AOT install lock poisoned"); - forget_installed_artifact(name); - let manifest_artifact = { - let _phase = timing::phase("aot.manifest_validation"); - target_manifest_artifact(name)? - }; - let artifact = materialize_artifact(name, &manifest_artifact, aot_verify_mode()?)?; - remember_installed_artifact(name, artifact.clone()); - Ok(artifact) -} - -fn materialize_artifact( - name: &str, - manifest_artifact: &AotManifestArtifact, - verify_mode: AotVerifyMode, -) -> Result { - let _phase = timing::phase("aot.materialize"); - let raw = artifact_raw_bytes(name, manifest_artifact, verify_mode)?; - let hash = expected_raw_hash(name, manifest_artifact, &raw, verify_mode)?; - let cache_path = cache_path(name, &hash)?; - remove_cached_artifact(&cache_path)?; - - if let Some(parent) = cache_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("create AOT cache directory {}", parent.display()))?; - } - let tmp_path = - cache_path.with_extension(format!("bin.{}.{}.tmp", std::process::id(), tmp_suffix())); - fs::write(&tmp_path, raw) - .with_context(|| format!("write AOT artifact {}", tmp_path.display()))?; - if let Err(err) = fs::rename(&tmp_path, &cache_path) { - remove_file_if_exists(&tmp_path).ok(); - return Err(err).with_context(|| { - format!( - "promote AOT artifact {} -> {}", - tmp_path.display(), - cache_path.display() - ) - }); - } - - write_cache_receipt(name, manifest_artifact, &cache_path, &hash)?; - Ok(InstalledArtifact { - path: cache_path, - sha256: hash, - }) -} - -fn cached_raw_artifact( - name: &str, - manifest_artifact: &AotManifestArtifact, - verify_mode: AotVerifyMode, -) -> Result> { - let Some(raw_sha256) = manifest_artifact.raw_sha256.as_deref() else { - return Ok(None); - }; - let cache_path = cache_path(name, raw_sha256)?; - if !cache_path.exists() { - return Ok(None); - } - - match verify_mode { - AotVerifyMode::Fast => { - let _phase = timing::phase("aot.cache_receipt_verify"); - if !cache_receipt_matches(name, manifest_artifact, &cache_path, raw_sha256)? { - remove_cached_artifact(&cache_path)?; - return Ok(None); - } - } - AotVerifyMode::Full => { - let _phase = timing::phase("aot.raw_cache_verify"); - let (actual, actual_size) = sha256_file_with_len(&cache_path)?; - if !actual.eq_ignore_ascii_case(raw_sha256) { - remove_cached_artifact(&cache_path)?; - return Ok(None); - } - if let Some(raw_size) = manifest_artifact.raw_size { - ensure!( - actual_size == raw_size, - "cached AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", - actual_size - ); - } - } - } - Ok(Some(InstalledArtifact { - path: cache_path, - sha256: raw_sha256.to_owned(), - })) -} - -fn installed_artifact(name: &str) -> Option { - INSTALLED_ARTIFACTS - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .expect("installed AOT artifact cache poisoned") - .get(name) - .filter(|artifact| artifact.path.exists()) - .cloned() -} - -fn remember_installed_artifact(name: &str, artifact: InstalledArtifact) { - INSTALLED_ARTIFACTS - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .expect("installed AOT artifact cache poisoned") - .insert(name.to_string(), artifact); -} - -fn forget_installed_artifact(name: &str) { - INSTALLED_ARTIFACTS - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .expect("installed AOT artifact cache poisoned") - .remove(name); -} - -fn remove_cached_artifact(path: &Path) -> Result<()> { - remove_file_if_exists(path)?; - remove_file_if_exists(&receipt_path(path)) -} - -fn remove_file_if_exists(path: &Path) -> Result<()> { - match fs::remove_file(path) { - Ok(()) => Ok(()), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(err) => Err(err).with_context(|| format!("remove {}", path.display())), - } -} - -fn tmp_suffix() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or_default() -} - -fn artifact_raw_bytes( - name: &str, - manifest_artifact: &AotManifestArtifact, - verify_mode: AotVerifyMode, -) -> Result> { - let Some(bytes) = target_artifact_bytes(name) else { - bail!( - "no Wasmer LLVM AOT artifact named '{name}' is available for target {}; rebuild assets or disable this unsupported target", - target_triple() - ); - }; - if verify_mode == AotVerifyMode::Full { - validate_compressed_artifact_manifest(name, manifest_artifact, bytes)?; - } - - if bytes.starts_with(ZSTD_MAGIC) { - let _phase = timing::phase("aot.decompress"); - let mut decoder = ZstdDecoder::new(Cursor::new(bytes)) - .with_context(|| format!("decode compressed AOT artifact '{name}'"))?; - let mut raw = Vec::new(); - decoder - .read_to_end(&mut raw) - .with_context(|| format!("decompress AOT artifact '{name}'"))?; - ensure!( - !raw.is_empty(), - "AOT artifact '{name}' decompressed to zero bytes" - ); - Ok(raw) - } else { - Ok(bytes.to_vec()) - } -} - -fn expected_raw_hash( - name: &str, - manifest_artifact: &AotManifestArtifact, - raw: &[u8], - verify_mode: AotVerifyMode, -) -> Result { - if let Some(raw_size) = manifest_artifact.raw_size { - ensure!( - raw.len() as u64 == raw_size, - "AOT artifact '{name}' raw size mismatch: manifest={raw_size} actual={}", - raw.len() - ); - } - - let Some(raw_sha256) = &manifest_artifact.raw_sha256 else { - ensure!( - verify_mode == AotVerifyMode::Full, - "AOT artifact '{name}' is missing raw-sha256 metadata; rebuild assets or set PGLITE_OXIDE_AOT_VERIFY=full for strict hash-derived cache keys" - ); - return Ok(sha256_hex(raw)); - }; - if verify_mode == AotVerifyMode::Full { - let actual = sha256_hex(raw); - ensure!( - actual.eq_ignore_ascii_case(raw_sha256), - "AOT artifact '{name}' raw hash mismatch: manifest={raw_sha256} actual={actual}" - ); - } - Ok(raw_sha256.clone()) -} - -fn target_manifest_artifact(name: &str) -> Result { - let manifest = target_aot_manifest()?; - ensure!( - manifest.target_triple == target_triple(), - "AOT manifest target mismatch: manifest={} actual={}", - manifest.target_triple, - target_triple() - ); - ensure!( - manifest.engine == EXPECTED_AOT_ENGINE, - "AOT manifest engine mismatch: manifest={} expected={EXPECTED_AOT_ENGINE}", - manifest.engine - ); - ensure!( - manifest.wasmer_version == EXPECTED_WASMER_VERSION, - "AOT manifest Wasmer version mismatch: manifest={} expected={EXPECTED_WASMER_VERSION}", - manifest.wasmer_version - ); - ensure!( - manifest.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION, - "AOT manifest wasmer-wasix version mismatch: manifest={} expected={EXPECTED_WASMER_WASIX_VERSION}", - manifest.wasmer_wasix_version - ); - - let artifact = manifest - .artifacts - .into_iter() - .find(|artifact| artifact.name == name) - .ok_or_else(|| anyhow::anyhow!("AOT manifest does not list artifact '{name}'"))?; - #[cfg(feature = "bundled")] - { - let expected_module = super::assets::expected_module_sha256(name)?; - ensure!( - expected_module.eq_ignore_ascii_case(&artifact.module_sha256), - "AOT artifact '{name}' source module hash mismatch: manifest={} assets={expected_module}", - artifact.module_sha256 - ); - } - - Ok(artifact) -} - -fn validate_compressed_artifact_manifest( - name: &str, - artifact: &AotManifestArtifact, - bytes: &[u8], -) -> Result<()> { - let actual_hash = sha256_hex(bytes); - ensure!( - actual_hash.eq_ignore_ascii_case(&artifact.sha256), - "AOT artifact '{name}' hash mismatch: manifest={} actual={actual_hash}", - artifact.sha256 - ); - Ok(()) -} - -fn target_aot_manifest() -> Result { - let Some(json) = target_aot_manifest_json() else { - bail!( - "no Wasmer LLVM AOT manifest is available for target {}; rebuild assets or disable this unsupported target", - target_triple() - ); - }; - serde_json::from_str(json).context("parse bundled AOT manifest") -} - -fn cache_path(name: &str, hash: &str) -> Result { - let safe_name = name.replace([':', '/', '\\'], "-"); - let dirs = ProjectDirs::from("dev", "pglite-oxide", "pglite-oxide") - .context("could not resolve pglite-oxide cache directory")?; - Ok(dirs - .cache_dir() - .join("wasmer-aot") - .join(target_triple()) - .join(format!("{safe_name}-{hash}.bin"))) -} - -fn receipt_path(cache_path: &Path) -> PathBuf { - cache_path.with_extension("receipt.json") -} - -fn cache_receipt_matches( - name: &str, - manifest_artifact: &AotManifestArtifact, - cache_path: &Path, - raw_sha256: &str, -) -> Result { - let Some(raw_size) = manifest_artifact.raw_size else { - return Ok(false); - }; - let metadata = match fs::metadata(cache_path) { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), - Err(err) => return Err(err).with_context(|| format!("stat {}", cache_path.display())), - }; - if metadata.len() != raw_size { - return Ok(false); - } - - let receipt_path = receipt_path(cache_path); - let receipt = match fs::read(&receipt_path) { - Ok(bytes) => match serde_json::from_slice::(&bytes) { - Ok(receipt) => receipt, - Err(_) => return Ok(false), - }, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false), - Err(err) => return Err(err).with_context(|| format!("read {}", receipt_path.display())), - }; - - Ok(receipt.format_version == CACHE_RECEIPT_FORMAT_VERSION - && receipt.artifact_name == name - && receipt.target_triple == target_triple() - && receipt.engine == EXPECTED_AOT_ENGINE - && receipt.wasmer_version == EXPECTED_WASMER_VERSION - && receipt.wasmer_wasix_version == EXPECTED_WASMER_WASIX_VERSION - && receipt.raw_sha256.eq_ignore_ascii_case(raw_sha256) - && receipt.raw_size == raw_size - && receipt - .compressed_sha256 - .eq_ignore_ascii_case(&manifest_artifact.sha256) - && receipt - .module_sha256 - .eq_ignore_ascii_case(&manifest_artifact.module_sha256)) -} - -fn write_cache_receipt( - name: &str, - manifest_artifact: &AotManifestArtifact, - cache_path: &Path, - raw_sha256: &str, -) -> Result<()> { - let Some(raw_size) = manifest_artifact.raw_size else { - return Ok(()); - }; - let receipt = AotCacheReceipt { - format_version: CACHE_RECEIPT_FORMAT_VERSION, - artifact_name: name.to_owned(), - target_triple: target_triple().to_owned(), - engine: EXPECTED_AOT_ENGINE.to_owned(), - wasmer_version: EXPECTED_WASMER_VERSION.to_owned(), - wasmer_wasix_version: EXPECTED_WASMER_WASIX_VERSION.to_owned(), - raw_sha256: raw_sha256.to_owned(), - raw_size, - compressed_sha256: manifest_artifact.sha256.clone(), - module_sha256: manifest_artifact.module_sha256.clone(), - }; - - let path = receipt_path(cache_path); - let tmp_path = path.with_extension(format!( - "receipt.{}.{}.tmp", - std::process::id(), - tmp_suffix() - )); - let bytes = serde_json::to_vec(&receipt).context("serialize AOT cache receipt")?; - fs::write(&tmp_path, bytes).with_context(|| format!("write {}", tmp_path.display()))?; - if let Err(err) = fs::rename(&tmp_path, &path) { - remove_file_if_exists(&tmp_path).ok(); - return Err(err).with_context(|| format!("promote AOT cache receipt {}", path.display())); - } - Ok(()) -} - -fn sha256_file_with_len(path: &Path) -> Result<(String, u64)> { - let mut file = fs::File::open(path).with_context(|| format!("open {}", path.display()))?; - let mut hasher = Sha256::new(); - let mut len = 0u64; - let mut buffer = [0u8; 128 * 1024]; - loop { - let read = file - .read(&mut buffer) - .with_context(|| format!("read {}", path.display()))?; - if read == 0 { - break; - } - hasher.update(&buffer[..read]); - len += read as u64; - } - Ok((format!("{:x}", hasher.finalize()), len)) -} - -fn aot_verify_mode() -> Result { - let Some(value) = std::env::var_os("PGLITE_OXIDE_AOT_VERIFY") else { - return Ok(AotVerifyMode::Fast); - }; - let value = value.to_string_lossy().to_ascii_lowercase(); - match value.as_str() { - "" | "fast" | "metadata" | "receipt" | "0" | "false" | "off" => Ok(AotVerifyMode::Fast), - "full" | "sha" | "sha256" | "strict" | "1" | "true" | "on" => Ok(AotVerifyMode::Full), - other => bail!("unsupported PGLITE_OXIDE_AOT_VERIFY={other}; use `fast` or `full`"), - } -} - -#[allow(unsafe_code)] -fn deserialize_headless(engine: &Engine, path: &Path) -> Result { - let _phase = timing::phase("aot.deserialize"); - deserialize_headless_mmap(engine, path) -} - -#[allow(unsafe_code)] -fn deserialize_headless_mmap(engine: &Engine, path: &Path) -> Result { - let _phase = timing::phase("aot.deserialize.mmap"); - // SAFETY: same artifact ownership and cache-key constraints as the file - // deserializer below. This path avoids reading the complete native artifact - // into a Rust Vec before Wasmer deserializes it. - unsafe { - engine - .deserialize_from_mmapped_file(path) - .with_context(|| format!("mmap-deserialize Wasmer AOT artifact {}", path.display())) - } -} - -fn sha256_hex(bytes: &[u8]) -> String { - let mut hasher = Sha256::new(); - hasher.update(bytes); - format!("{:x}", hasher.finalize()) -} - -fn target_triple() -> &'static str { - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] - { - return "aarch64-apple-darwin"; - } - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - { - return "x86_64-unknown-linux-gnu"; - } - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - { - return "aarch64-unknown-linux-gnu"; - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - { - return "x86_64-pc-windows-msvc"; - } - #[allow(unreachable_code)] - "unsupported" -} - -fn target_artifact_bytes(_name: &str) -> Option<&'static [u8]> { - #[cfg(all(feature = "bundled", target_os = "macos", target_arch = "aarch64"))] - { - return pglite_oxide_aot_aarch64_apple_darwin::artifact_bytes(_name); - } - #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "x86_64"))] - { - return pglite_oxide_aot_x86_64_unknown_linux_gnu::artifact_bytes(_name); - } - #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "aarch64"))] - { - return pglite_oxide_aot_aarch64_unknown_linux_gnu::artifact_bytes(_name); - } - #[cfg(all(feature = "bundled", target_os = "windows", target_arch = "x86_64"))] - { - return pglite_oxide_aot_x86_64_pc_windows_msvc::artifact_bytes(_name); - } - #[allow(unreachable_code)] - None -} - -fn target_aot_manifest_json() -> Option<&'static str> { - #[cfg(all(feature = "bundled", target_os = "macos", target_arch = "aarch64"))] - { - return Some(pglite_oxide_aot_aarch64_apple_darwin::MANIFEST_JSON); - } - #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "x86_64"))] - { - return Some(pglite_oxide_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON); - } - #[cfg(all(feature = "bundled", target_os = "linux", target_arch = "aarch64"))] - { - return Some(pglite_oxide_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON); - } - #[cfg(all(feature = "bundled", target_os = "windows", target_arch = "x86_64"))] - { - return Some(pglite_oxide_aot_x86_64_pc_windows_msvc::MANIFEST_JSON); - } - #[allow(unreachable_code)] - None -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -struct AotManifest { - target_triple: String, - engine: String, - wasmer_version: String, - wasmer_wasix_version: String, - artifacts: Vec, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] -struct AotManifestArtifact { - name: String, - sha256: String, - #[allow(dead_code)] - module_sha256: String, - raw_sha256: Option, - raw_size: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -struct AotCacheReceipt { - format_version: u32, - artifact_name: String, - target_triple: String, - engine: String, - wasmer_version: String, - wasmer_wasix_version: String, - raw_sha256: String, - raw_size: u64, - compressed_sha256: String, - module_sha256: String, -} - -#[cfg(test)] -mod tests { - use super::*; - - const ASSET_SOURCES: &str = include_str!("../../assets/sources.toml"); - - #[test] - fn runtime_aot_versions_match_asset_toolchain() { - assert_eq!( - EXPECTED_WASMER_VERSION, - toolchain_value("wasmer"), - "runtime AOT Wasmer expectation must match assets/sources.toml" - ); - assert_eq!( - EXPECTED_WASMER_WASIX_VERSION, - toolchain_value("wasmer-wasix"), - "runtime AOT WASIX expectation must match assets/sources.toml" - ); - } - - #[test] - fn engine_identity_matches_runtime_aot_versions() { - assert!( - AOT_ENGINE_ID.contains(EXPECTED_AOT_ENGINE), - "engine identity must include the validated AOT engine" - ); - assert!( - AOT_ENGINE_ID.contains(EXPECTED_WASMER_VERSION), - "engine identity must include the validated Wasmer version" - ); - assert!( - AOT_ENGINE_ID.contains(EXPECTED_WASMER_WASIX_VERSION), - "engine identity must include the validated WASIX version" - ); - } - - fn toolchain_value(key: &str) -> &str { - let rest = ASSET_SOURCES - .split_once("[toolchain]") - .expect("sources manifest has a [toolchain] section") - .1; - let section = rest.split_once("\n[").map_or(rest, |(section, _)| section); - - for line in section.lines() { - let Some((line_key, value)) = line.trim().split_once('=') else { - continue; - }; - if line_key.trim() == key { - return value.trim().trim_matches('"'); - } - } - panic!("sources manifest has toolchain.{key}"); - } -} diff --git a/src/pglite/assets.rs b/src/pglite/assets.rs deleted file mode 100644 index bfe0ffee..00000000 --- a/src/pglite/assets.rs +++ /dev/null @@ -1,145 +0,0 @@ -#[cfg(feature = "bundled")] -use anyhow::{Context, Result, anyhow}; -#[cfg(feature = "bundled")] -use std::sync::{Arc, OnceLock}; - -#[cfg(feature = "bundled")] -static ASSET_MANIFEST: OnceLock< - std::result::Result, String>, -> = OnceLock::new(); - -#[cfg(feature = "bundled")] -fn asset_manifest() -> Result> { - ASSET_MANIFEST - .get_or_init(|| { - pglite_oxide_assets::manifest() - .map(Arc::new) - .map_err(|err| err.to_string()) - }) - .clone() - .map_err(|message| anyhow!(message)) -} - -#[cfg(feature = "bundled")] -pub(crate) fn runtime_archive() -> Option<&'static [u8]> { - pglite_oxide_assets::runtime_archive() -} - -#[cfg(not(feature = "bundled"))] -pub(crate) fn runtime_archive() -> Option<&'static [u8]> { - None -} - -#[cfg(feature = "bundled")] -pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { - pglite_oxide_assets::pgdata_template_archive() -} - -#[cfg(not(feature = "bundled"))] -pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { - None -} - -#[cfg(feature = "bundled")] -pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { - pglite_oxide_assets::pgdata_template_manifest() -} - -#[cfg(not(feature = "bundled"))] -pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { - None -} - -#[cfg(feature = "bundled")] -#[allow(dead_code)] -pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { - pglite_oxide_assets::pg_dump_wasm() -} - -#[cfg(not(feature = "bundled"))] -#[allow(dead_code)] -pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { - None -} - -#[cfg(feature = "bundled")] -#[allow(dead_code)] -pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { - pglite_oxide_assets::initdb_wasm() -} - -#[cfg(not(feature = "bundled"))] -#[allow(dead_code)] -pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { - None -} - -#[cfg(feature = "extensions")] -pub(crate) fn extension_archive(sql_name: &str) -> Option<&'static [u8]> { - pglite_oxide_assets::extension_archive(sql_name) -} - -#[cfg(feature = "bundled")] -pub(crate) fn expected_runtime_archive_sha256() -> Result { - Ok(asset_manifest() - .context("parse embedded asset manifest")? - .runtime - .sha256 - .clone()) -} - -#[cfg(feature = "extensions")] -pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result { - asset_manifest() - .context("parse embedded asset manifest")? - .extensions - .iter() - .find(|extension| extension.sql_name == sql_name) - .map(|extension| extension.sha256.clone()) - .ok_or_else(|| anyhow!("extension asset '{sql_name}' is missing from asset manifest")) -} - -#[cfg(feature = "bundled")] -pub(crate) fn expected_module_sha256(name: &str) -> Result { - let manifest = asset_manifest().context("parse embedded asset manifest")?; - if name == "runtime:pglite" { - return Ok(manifest.runtime.module_sha256.clone()); - } - if let Some(name) = name.strip_prefix("runtime-support:") { - return manifest - .runtime_support - .iter() - .find(|module| module.name == name) - .map(|module| module.module_sha256.clone()) - .ok_or_else(|| { - anyhow!("runtime support module '{name}' is missing from asset manifest") - }); - } - if name == "tool:pg_dump" { - return manifest - .pg_dump - .as_ref() - .map(|module| module.module_sha256.clone()) - .ok_or_else(|| anyhow!("pg_dump is missing from asset manifest")); - } - if name == "tool:initdb" { - return manifest - .initdb - .as_ref() - .map(|module| module.module_sha256.clone()) - .ok_or_else(|| anyhow!("initdb is missing from asset manifest")); - } - if let Some(sql_name) = name.strip_prefix("extension:") { - let module_sha256 = manifest - .extensions - .iter() - .find(|extension| extension.sql_name == sql_name) - .map(|extension| extension.module_sha256.clone()) - .ok_or_else(|| anyhow!("extension '{sql_name}' is missing from asset manifest"))?; - if module_sha256.is_empty() { - anyhow::bail!("extension '{sql_name}' has no native module in asset manifest"); - } - return Ok(module_sha256); - } - Err(anyhow!("unknown asset module '{name}'")) -} diff --git a/src/pglite/backend.rs b/src/pglite/backend.rs deleted file mode 100644 index 47919139..00000000 --- a/src/pglite/backend.rs +++ /dev/null @@ -1,331 +0,0 @@ -#[cfg(feature = "extensions")] -use anyhow::{Context, bail}; -use anyhow::{Result, ensure}; -use std::sync::{Mutex, MutexGuard, OnceLock}; - -use crate::pglite::base::InstallOutcome; -use crate::pglite::config::{PostgresConfig, StartupConfig}; -#[cfg(feature = "extensions")] -use crate::pglite::extensions::{Extension, extension_session_setup_sql, extension_setup_sql}; -use crate::pglite::interface::DataTransferContainer; -use crate::pglite::postgres_mod::{ - PostgresMod, ProtocolPumpOutcome, ProtocolStream, StartupProtocolResponse, -}; -use crate::pglite::timing; -use crate::pglite::transport::Transport; -use crate::pglite::wire::raw_protocol_message_len; -#[cfg(feature = "extensions")] -use crate::pglite::wire::{response_contains_error, simple_query_message}; - -static WASIX_BACKEND_OPEN_LOCK: OnceLock> = OnceLock::new(); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum BackendOpenKind { - Direct, - Proxy, -} - -pub(crate) struct BackendSession { - pg: PostgresMod, - transport: Transport, - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - #[cfg(feature = "extensions")] - preinstalled_extensions: Vec, - #[cfg(feature = "extensions")] - preloaded_extensions: Vec, -} - -impl BackendSession { - pub(crate) fn open( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - ) -> Result { - #[cfg(feature = "extensions")] - { - Self::open_with_extension_preload(outcome, postgres_config, startup_config, kind, &[]) - } - #[cfg(not(feature = "extensions"))] - { - Self::open_without_extension_preload(outcome, postgres_config, startup_config, kind) - } - } - - #[cfg(feature = "extensions")] - pub(crate) fn open_with_extension_preload( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - extensions: &[Extension], - ) -> Result { - Self::open_inner(outcome, postgres_config, startup_config, kind, extensions) - } - - #[cfg(not(feature = "extensions"))] - fn open_without_extension_preload( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - ) -> Result { - Self::open_inner(outcome, postgres_config, startup_config, kind) - } - - #[cfg(feature = "extensions")] - fn open_inner( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - extensions: &[Extension], - ) -> Result { - let _open_guard = wasix_backend_open_guard(); - let preinstalled_extensions = outcome.preinstalled_extensions.clone(); - let pg = Self::new_postgres( - outcome.clone(), - postgres_config.clone(), - startup_config.clone(), - kind, - )?; - for extension in extensions { - pg.preload_extension_module(*extension)?; - } - let (pg, transport) = Self::finish_open(pg, kind)?; - Ok(Self { - pg, - transport, - outcome, - postgres_config, - startup_config, - kind, - preinstalled_extensions, - preloaded_extensions: extensions.to_vec(), - }) - } - - #[cfg(not(feature = "extensions"))] - fn open_inner( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - ) -> Result { - let _open_guard = wasix_backend_open_guard(); - let pg = Self::new_postgres( - outcome.clone(), - postgres_config.clone(), - startup_config.clone(), - kind, - )?; - let (pg, transport) = Self::finish_open(pg, kind)?; - Ok(Self { - pg, - transport, - outcome, - postgres_config, - startup_config, - kind, - }) - } - - fn new_postgres( - outcome: InstallOutcome, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - kind: BackendOpenKind, - ) -> Result { - let pg = { - let _phase = timing::phase(match kind { - BackendOpenKind::Direct => "pglite.postgres_new", - BackendOpenKind::Proxy => "proxy.backend_postgres_new", - }); - PostgresMod::new_prepared_with_config( - outcome.paths, - outcome.runtime_layout, - postgres_config, - startup_config, - )? - }; - Ok(pg) - } - - fn finish_open(mut pg: PostgresMod, kind: BackendOpenKind) -> Result<(PostgresMod, Transport)> { - { - let _phase = timing::phase(match kind { - BackendOpenKind::Direct => "pglite.ensure_cluster", - BackendOpenKind::Proxy => "proxy.backend_ensure_cluster", - }); - pg.ensure_cluster()?; - } - let transport = { - let _phase = timing::phase(match kind { - BackendOpenKind::Direct => "pglite.transport_prepare", - BackendOpenKind::Proxy => "proxy.transport_prepare", - }); - Transport::prepare(&mut pg)? - }; - Ok((pg, transport)) - } - - pub(crate) fn paths(&self) -> &crate::pglite::base::PglitePaths { - self.pg.paths() - } - - pub(crate) fn pgdata_template_root(&self) -> Option<&std::path::Path> { - self.pg.pgdata_template_root() - } - - pub(crate) fn startup_config(&self) -> &StartupConfig { - &self.startup_config - } - - #[cfg(debug_assertions)] - pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { - self.pg.guest_bridge_allocation_counts() - } - - pub(crate) fn send_buffered( - &mut self, - message: &[u8], - requested: Option, - ) -> Result> { - self.transport.send(&mut self.pg, message, requested) - } - - pub(crate) fn send_framed_raw_stream( - &mut self, - message: &[u8], - requested: Option, - mut on_data: F, - ) -> Result<()> - where - F: FnMut(&[u8]) -> Result<()>, - { - let mut cursor = 0usize; - while cursor < message.len() { - let frame_len = raw_protocol_message_len(&message[cursor..])?; - let end = cursor + frame_len; - let data = self.send_buffered(&message[cursor..end], requested)?; - if !data.is_empty() { - on_data(&data)?; - } - cursor = end; - } - Ok(()) - } - - pub(crate) fn startup_with_packet( - &mut self, - message: &[u8], - ) -> Result { - self.pg.start_protocol_with_startup_packet(message) - } - - #[cfg(feature = "extensions")] - pub(crate) fn existing_startup_response(&self) -> Option> { - self.pg.existing_startup_response() - } - - #[cfg(feature = "extensions")] - pub(crate) fn preload_extension_module(&mut self, extension: Extension) -> Result<()> { - self.pg.preload_extension_module(extension) - } - - #[cfg(feature = "extensions")] - pub(crate) fn preload_installed_extension(&mut self, extension: Extension) -> Result<()> { - self.preload_extension_module(extension) - } - - #[cfg(feature = "extensions")] - pub(crate) fn enable_extensions(&mut self, extensions: &[Extension]) -> Result<()> { - for extension in extensions { - let setup_sql = if self.has_preinstalled_extension(*extension) { - self.preload_installed_extension(*extension)?; - extension_session_setup_sql(*extension) - } else { - extension_setup_sql(*extension) - }; - for sql in setup_sql { - let response = self - .send_buffered(&simple_query_message(&sql), None) - .with_context(|| { - format!("enable bundled extension '{}'", extension.sql_name()) - })?; - if response_contains_error(&response) { - bail!( - "enable bundled extension '{}' returned a Postgres error", - extension.sql_name() - ); - } - } - } - Ok(()) - } - - #[cfg(feature = "extensions")] - pub(crate) fn has_preinstalled_extension(&self, extension: Extension) -> bool { - self.preinstalled_extensions - .iter() - .any(|sql_name| sql_name == extension.sql_name()) - } - - pub(crate) fn supports_protocol_pump(&self) -> bool { - self.pg.supports_streaming_protocol() - } - - pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> - where - S: ProtocolStream + 'static, - { - self.pg.attach_protocol_stream(stream) - } - - pub(crate) fn send_with_protocol_pump( - &mut self, - message: &[u8], - continuation_prefix: impl FnOnce() -> Vec, - ) -> Result { - ensure!( - self.supports_protocol_pump(), - "WASIX runtime is missing backend-owned protocol pump exports" - ); - self.pg.send_protocol_pump(message, continuation_prefix) - } - - pub(crate) fn shutdown(&mut self) -> Result<()> { - self.pg.shutdown_backend() - } - - pub(crate) fn restart(&mut self) -> Result<()> { - let _open_guard = wasix_backend_open_guard(); - let pg = Self::new_postgres( - self.outcome.clone(), - self.postgres_config.clone(), - self.startup_config.clone(), - self.kind, - )?; - #[cfg(feature = "extensions")] - for extension in &self.preloaded_extensions { - pg.preload_extension_module(*extension)?; - } - let (pg, transport) = Self::finish_open(pg, self.kind)?; - self.pg = pg; - self.transport = transport; - Ok(()) - } -} - -fn wasix_backend_open_guard() -> MutexGuard<'static, ()> { - // Wasmer/WASIX backend startup uses process-wide runtime and module-cache - // state. Serialize creation and `_start`; already-open backends still run - // independently after startup. - WASIX_BACKEND_OPEN_LOCK - .get_or_init(|| Mutex::new(())) - .lock() - .expect("WASIX backend open lock poisoned") -} diff --git a/src/pglite/builder.rs b/src/pglite/builder.rs deleted file mode 100644 index f9ed32a9..00000000 --- a/src/pglite/builder.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::path::PathBuf; - -use anyhow::{Result, bail}; - -use crate::pglite::base::{PreparedRoot, RootPlan, RootSource, RootTarget, prepare_root}; -use crate::pglite::client::Pglite; -use crate::pglite::config::{PostgresConfig, StartupConfig}; -#[cfg(feature = "extensions")] -use crate::pglite::extensions::{Extension, resolve_extension_set}; -use crate::pglite::interface::DebugLevel; - -/// Builder for opening persistent or temporary [`Pglite`] databases. -#[derive(Debug, Clone)] -pub struct PgliteBuilder { - target: Option, - template_cache: bool, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - load_data_dir_archive: Option>, - #[cfg(feature = "extensions")] - extensions: Vec, -} - -#[derive(Debug, Clone)] -enum PgliteTarget { - Path(PathBuf), - AppId { - qualifier: String, - organization: String, - application: String, - }, - Temporary, -} - -impl Default for PgliteBuilder { - fn default() -> Self { - Self { - target: None, - template_cache: true, - postgres_config: PostgresConfig::default(), - startup_config: StartupConfig::default(), - load_data_dir_archive: None, - #[cfg(feature = "extensions")] - extensions: Vec::new(), - } - } -} - -impl PgliteBuilder { - /// Create a builder. Call [`path`](Self::path), [`app_id`](Self::app_id), - /// or [`temporary`](Self::temporary) before [`open`](Self::open). - pub fn new() -> Self { - Self::default() - } - - /// Open a persistent database rooted at `root`. - pub fn path(mut self, root: impl Into) -> Self { - self.target = Some(PgliteTarget::Path(root.into())); - self - } - - /// Open a persistent database under the platform data directory. - pub fn app( - mut self, - qualifier: impl Into, - organization: impl Into, - application: impl Into, - ) -> Self { - self.target = Some(PgliteTarget::AppId { - qualifier: qualifier.into(), - organization: organization.into(), - application: application.into(), - }); - self - } - - /// Open a persistent database under the platform data directory. - pub fn app_id(self, app_id: (&str, &str, &str)) -> Self { - self.app(app_id.0, app_id.1, app_id.2) - } - - /// Open an ephemeral database removed when the instance is dropped. - /// - /// Temporary databases use the process-local template cluster cache by - /// default, avoiding repeated `initdb` work in test suites. - pub fn temporary(mut self) -> Self { - self.target = Some(PgliteTarget::Temporary); - self - } - - /// Control whether new databases are cloned from the process-local or - /// embedded PGDATA template cache. - pub fn template_cache(mut self, enabled: bool) -> Self { - self.template_cache = enabled; - self - } - - /// Open an ephemeral database with a fresh `initdb`. - /// - /// This is a compatibility alias for - /// `temporary().template_cache(false)`. Fresh initdb uses the bundled split - /// WASIX `initdb` module; cached temporary databases remain the production - /// fast path. - pub fn fresh_temporary(self) -> Self { - self.temporary().template_cache(false) - } - - /// Set a PostgreSQL startup GUC for this embedded backend. - pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { - self.postgres_config.insert(name, value); - self - } - - /// Set multiple PostgreSQL startup GUCs for this embedded backend. - pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self - where - K: Into, - V: Into, - { - for (name, value) in settings { - self.postgres_config.insert(name, value); - } - self - } - - /// Connect as a PostgreSQL role. The role must already exist in the - /// cluster. - pub fn username(mut self, username: impl Into) -> Self { - self.startup_config.username = username.into(); - self - } - - /// Connect to a PostgreSQL database. The database must already exist in the - /// cluster. - pub fn database(mut self, database: impl Into) -> Self { - self.startup_config.database = database.into(); - self - } - - /// Enable PostgreSQL debug logging level `0..=5` for the embedded backend. - pub fn debug_level(mut self, level: DebugLevel) -> Self { - self.startup_config.debug_level = Some(level); - self - } - - /// Use lower durability settings for ephemeral or cacheable local - /// workloads. - pub fn relaxed_durability(mut self, enabled: bool) -> Self { - self.startup_config.relaxed_durability = enabled; - self - } - - /// Append an advanced PostgreSQL startup argument. Prefer - /// [`postgres_config`](Self::postgres_config) for GUCs. - pub fn startup_arg(mut self, arg: impl Into) -> Self { - self.startup_config.extra_args.push(arg.into()); - self - } - - /// Append advanced PostgreSQL startup arguments. - pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { - self.startup_config - .extra_args - .extend(args.into_iter().map(Into::into)); - self - } - - /// Load a previously dumped PGDATA tar archive before opening the database. - pub fn load_data_dir_archive(mut self, archive: impl Into>) -> Self { - self.load_data_dir_archive = Some(archive.into()); - self - } - - /// Enable a bundled Postgres extension before returning the database. - #[cfg(feature = "extensions")] - pub fn extension(mut self, extension: Extension) -> Self { - self.extensions.push(extension); - self - } - - /// Enable bundled Postgres extensions before returning the database. - #[cfg(feature = "extensions")] - pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { - self.extensions.extend(extensions); - self - } - - /// Install, initialize, and start the selected database. - pub fn open(self) -> Result { - self.postgres_config.validate()?; - self.startup_config.validate()?; - let target = match self.target.clone() { - Some(PgliteTarget::Path(root)) => RootTarget::Path(root), - Some(PgliteTarget::AppId { - qualifier, - organization, - application, - }) => RootTarget::AppId { - qualifier, - organization, - application, - }, - Some(PgliteTarget::Temporary) => RootTarget::Temporary, - None => { - bail!( - "PgliteBuilder target is not set; call path, app_id, or temporary before open" - ) - } - }; - let source = if let Some(archive) = self.load_data_dir_archive.clone() { - RootSource::DataDirArchive(archive) - } else if self.template_cache { - RootSource::Template - } else { - RootSource::FreshInitdb - }; - #[cfg(feature = "extensions")] - let extensions = resolve_extension_set(&self.extensions)?; - let plan = RootPlan::new(target, source); - #[cfg(feature = "extensions")] - let plan = plan.with_extensions(extensions.clone(), self.postgres_config.clone()); - let prepared = prepare_root(plan)?; - #[cfg(feature = "extensions")] - { - self.open_prepared_root(prepared, extensions) - } - #[cfg(not(feature = "extensions"))] - { - self.open_prepared_root(prepared) - } - } - - fn open_prepared_root( - self, - prepared: PreparedRoot, - #[cfg(feature = "extensions")] extensions: Vec, - ) -> Result { - let PreparedRoot { - temp_dir, - root_lock, - outcome, - .. - } = prepared; - #[cfg(feature = "extensions")] - let preinstalled_extensions = outcome.preinstalled_extensions.clone(); - let mut instance = - Pglite::new_prepared_with_config(outcome, self.postgres_config, self.startup_config)?; - if let Some(lock) = root_lock { - instance.attach_root_lock(lock); - } - if let Some(temp_dir) = temp_dir { - instance.attach_temp_dir(temp_dir); - } - #[cfg(feature = "extensions")] - let mut instance = instance; - #[cfg(feature = "extensions")] - for extension in extensions { - if preinstalled_extensions - .iter() - .any(|sql_name| sql_name == extension.sql_name()) - { - instance.enable_preinstalled_extension(extension)?; - } else { - instance.enable_extension(extension)?; - } - } - Ok(instance) - } -} diff --git a/src/pglite/config.rs b/src/pglite/config.rs deleted file mode 100644 index 081b9960..00000000 --- a/src/pglite/config.rs +++ /dev/null @@ -1,157 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Result, bail, ensure}; - -use crate::pglite::interface::DebugLevel; - -/// PostgreSQL startup configuration applied through normal `postgres -c` GUC -/// handling before the embedded backend starts. -/// -/// Settings added here override `pglite-oxide`'s default startup profile because -/// they are appended after the defaults in the generated PostgreSQL argv. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct PostgresConfig { - settings: BTreeMap, -} - -impl PostgresConfig { - /// Create an empty startup configuration. - pub fn new() -> Self { - Self::default() - } - - /// Set or replace one PostgreSQL GUC. - pub fn set(mut self, name: impl Into, value: impl Into) -> Self { - self.settings.insert(name.into(), value.into()); - self - } - - pub(crate) fn insert(&mut self, name: impl Into, value: impl Into) { - self.settings.insert(name.into(), value.into()); - } - - pub(crate) fn validate(&self) -> Result<()> { - for (name, value) in &self.settings { - validate_guc_name(name)?; - ensure!( - !value.contains('\0'), - "Postgres config value for '{name}' must not contain NUL bytes" - ); - } - Ok(()) - } - - pub(crate) fn iter(&self) -> impl Iterator { - self.settings - .iter() - .map(|(name, value)| (name.as_str(), value.as_str())) - } - - #[cfg(feature = "extensions")] - pub(crate) fn stable_entries(&self) -> Vec<(String, String)> { - self.settings - .iter() - .map(|(name, value)| (name.clone(), value.clone())) - .collect() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct StartupConfig { - pub(crate) username: String, - pub(crate) database: String, - pub(crate) debug_level: Option, - pub(crate) relaxed_durability: bool, - pub(crate) extra_args: Vec, -} - -impl Default for StartupConfig { - fn default() -> Self { - Self { - username: "postgres".to_owned(), - database: "template1".to_owned(), - debug_level: None, - relaxed_durability: false, - extra_args: Vec::new(), - } - } -} - -impl StartupConfig { - pub(crate) fn validate(&self) -> Result<()> { - validate_startup_value("username", &self.username)?; - validate_startup_value("database", &self.database)?; - if let Some(level) = self.debug_level { - ensure!( - level <= 5, - "Postgres debug level must be between 0 and 5, got {level}" - ); - } - for arg in &self.extra_args { - ensure!( - !arg.contains('\0'), - "Postgres startup argument must not contain NUL bytes" - ); - } - Ok(()) - } -} - -fn validate_guc_name(name: &str) -> Result<()> { - ensure!(!name.is_empty(), "Postgres config name must not be empty"); - ensure!( - !name.contains('\0') && !name.contains('='), - "Postgres config name '{name}' must not contain NUL bytes or '='" - ); - - for part in name.split('.') { - if part.is_empty() { - bail!("Postgres config name '{name}' contains an empty identifier part"); - } - let mut chars = part.chars(); - let first = chars.next().expect("part is non-empty"); - if !(first == '_' || first.is_ascii_alphabetic()) { - bail!("Postgres config name '{name}' must start each identifier with a letter or '_'"); - } - if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) { - bail!("Postgres config name '{name}' may only contain letters, digits, '_', and '.'"); - } - } - - Ok(()) -} - -fn validate_startup_value(name: &str, value: &str) -> Result<()> { - ensure!( - !value.is_empty(), - "Postgres startup {name} must not be empty" - ); - ensure!( - !value.contains('\0'), - "Postgres startup {name} must not contain NUL bytes" - ); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::PostgresConfig; - - #[test] - fn validates_builtin_and_extension_guc_names() { - PostgresConfig::new() - .set("synchronous_commit", "off") - .set("pg_stat_statements.track", "all") - .validate() - .unwrap(); - } - - #[test] - fn rejects_invalid_guc_names_before_startup() { - let err = PostgresConfig::new() - .set("bad=name", "off") - .validate() - .expect_err("invalid GUC name should be rejected"); - assert!(err.to_string().contains("must not contain")); - } -} diff --git a/src/pglite/extensions.rs b/src/pglite/extensions.rs deleted file mode 100644 index f3d2ddd1..00000000 --- a/src/pglite/extensions.rs +++ /dev/null @@ -1,625 +0,0 @@ -use std::collections::BTreeSet; - -use anyhow::{Result, bail}; - -#[path = "generated_extensions.rs"] -mod generated; - -pub use generated::*; - -/// A bundled Postgres extension that can be installed into a PGlite database. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Extension { - name: &'static str, - sql_name: &'static str, - archive_name: &'static str, - native_module_file: Option<&'static str>, - aot_name: Option<&'static str>, - dependencies: &'static [&'static str], - setup: ExtensionSetup, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) struct ExtensionSetup { - create_extension: bool, - create_schema: Option<&'static str>, - load_sql: &'static [&'static str], - post_create_sql: &'static [&'static str], -} - -impl ExtensionSetup { - pub(crate) const fn new( - create_extension: bool, - create_schema: Option<&'static str>, - load_sql: &'static [&'static str], - post_create_sql: &'static [&'static str], - ) -> Self { - Self { - create_extension, - create_schema, - load_sql, - post_create_sql, - } - } -} - -impl Extension { - #[allow(dead_code)] - pub(crate) const fn new( - name: &'static str, - sql_name: &'static str, - archive_name: &'static str, - native_module_file: Option<&'static str>, - aot_name: Option<&'static str>, - dependencies: &'static [&'static str], - setup: ExtensionSetup, - ) -> Self { - Self { - name, - sql_name, - archive_name, - native_module_file, - aot_name, - dependencies, - setup, - } - } - - /// Human-facing extension name. - pub const fn name(self) -> &'static str { - self.name - } - - /// SQL extension name used in `CREATE EXTENSION`. - pub const fn sql_name(self) -> &'static str { - self.sql_name - } - - /// Archive path inside the asset manifest. - pub const fn archive_name(self) -> &'static str { - self.archive_name - } - - /// AOT artifact key for the extension side module. - pub const fn aot_name(self) -> Option<&'static str> { - self.aot_name - } - - /// Native side-module file installed into `/lib/postgresql`, when the - /// extension has one. - pub const fn native_module_file(self) -> Option<&'static str> { - self.native_module_file - } - - /// SQL extension names that must be installed before this extension. - pub const fn dependencies(self) -> &'static [&'static str] { - self.dependencies - } - - pub(crate) const fn setup(self) -> ExtensionSetup { - self.setup - } -} - -pub fn by_sql_name(sql_name: &str) -> Option { - ALL.iter() - .copied() - .find(|extension| extension.sql_name == sql_name) -} - -pub(crate) fn candidate_by_sql_name(sql_name: &str) -> Option { - generated::CANDIDATES - .iter() - .copied() - .find(|extension| extension.sql_name == sql_name) -} - -pub(crate) fn resolve_extension_set(extensions: &[Extension]) -> Result> { - let mut visiting = BTreeSet::new(); - let mut visited = BTreeSet::new(); - let mut resolved = Vec::new(); - let mut requested = extensions.to_vec(); - requested.sort_by_key(|extension| extension.sql_name()); - for extension in requested { - visit_extension(extension, &mut visiting, &mut visited, &mut resolved)?; - } - Ok(resolved) -} - -fn visit_extension( - extension: Extension, - visiting: &mut BTreeSet<&'static str>, - visited: &mut BTreeSet<&'static str>, - resolved: &mut Vec, -) -> Result<()> { - if visited.contains(extension.sql_name()) { - return Ok(()); - } - if !visiting.insert(extension.sql_name()) { - bail!( - "cyclic bundled extension dependency involving '{}'", - extension.sql_name() - ); - } - for dependency in extension.dependencies() { - let dependency_extension = candidate_by_sql_name(dependency).ok_or_else(|| { - anyhow::anyhow!( - "bundled extension '{}' depends on missing packaged extension '{}'", - extension.sql_name(), - dependency - ) - })?; - visit_extension(dependency_extension, visiting, visited, resolved)?; - } - visiting.remove(extension.sql_name()); - visited.insert(extension.sql_name()); - resolved.push(extension); - Ok(()) -} - -pub(crate) fn extension_setup_sql(extension: Extension) -> Vec { - let setup = extension.setup(); - let mut statements = Vec::new(); - if setup.create_extension { - if let Some(schema) = setup.create_schema.filter(|schema| *schema != "pg_catalog") { - statements.push(format!( - "CREATE SCHEMA IF NOT EXISTS {};", - crate::pglite::templating::quote_identifier(schema) - )); - } - let mut sql = format!( - "CREATE EXTENSION IF NOT EXISTS {}", - crate::pglite::templating::quote_identifier(extension.sql_name()) - ); - if let Some(schema) = setup.create_schema { - sql.push_str(" WITH SCHEMA "); - sql.push_str(&crate::pglite::templating::quote_identifier(schema)); - } - sql.push(';'); - statements.push(sql); - } - statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); - statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); - statements -} - -pub(crate) fn extension_session_setup_sql(extension: Extension) -> Vec { - let setup = extension.setup(); - let mut statements = Vec::new(); - statements.extend(setup.load_sql.iter().map(|sql| (*sql).to_owned())); - statements.extend(setup.post_create_sql.iter().map(|sql| (*sql).to_owned())); - statements -} - -#[cfg(all(test, feature = "extensions"))] -mod candidate_tests { - use super::*; - use crate::{Pglite, PgliteServer}; - use anyhow::{Context, Result, ensure}; - use sqlx::{Connection, PgConnection}; - use std::collections::BTreeSet; - use std::path::{Path, PathBuf}; - - #[test] - fn public_extensions_pass_direct_and_restart_smoke() -> Result<()> { - run_direct_and_restart_smoke_set(generated::ALL) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn public_extensions_pass_server_smoke() -> Result<()> { - run_server_smoke_set(generated::ALL).await - } - - #[test] - fn public_extensions_materialize_only_requested_libraries() -> Result<()> { - run_lifecycle_materialization_set(generated::ALL) - } - - #[test] - #[ignore = "promotion gate: run manually before marking packaged candidates stable"] - fn packaged_candidate_extensions_pass_direct_and_restart_smoke() -> Result<()> { - run_direct_and_restart_smoke_set(generated::CANDIDATES) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - #[ignore = "promotion gate: run manually before marking packaged candidates stable"] - async fn packaged_candidate_extensions_pass_server_smoke() -> Result<()> { - run_server_smoke_set(generated::CANDIDATES).await - } - - #[test] - #[ignore = "promotion gate: run manually before marking packaged candidates stable"] - fn packaged_candidate_extensions_materialize_only_requested_libraries() -> Result<()> { - run_lifecycle_materialization_set(generated::CANDIDATES) - } - - fn run_direct_and_restart_smoke_set(extensions: &[Extension]) -> Result<()> { - let mut failures = Vec::new(); - for extension in extensions { - if let Err(error) = run_one_direct_and_restart_smoke(*extension) { - failures.push(format!("{}: {error:?}", extension.sql_name())); - } - } - ensure!( - failures.is_empty(), - "extension direct/restart smoke failures:\n{}", - failures.join("\n\n") - ); - Ok(()) - } - - fn run_one_direct_and_restart_smoke(extension: Extension) -> Result<()> { - let name = extension.sql_name(); - { - let mut db = Pglite::builder() - .temporary() - .extension(extension) - .open() - .with_context(|| format!("open temporary database with extension {name}"))?; - run_direct_smoke(&mut db, extension)?; - db.close() - .with_context(|| format!("close temporary database with extension {name}"))?; - } - - let root = tempfile::TempDir::new() - .with_context(|| format!("create restart root for extension {name}"))?; - { - let mut db = Pglite::builder() - .path(root.path()) - .extension(extension) - .open() - .with_context(|| { - format!("open persistent database with extension {name} before restart") - })?; - run_direct_smoke(&mut db, extension)?; - assert_extension_catalog_state(&mut db, extension)?; - db.close() - .with_context(|| format!("close persistent database with extension {name}"))?; - } - { - let mut db = Pglite::builder() - .path(root.path()) - .extension(extension) - .open() - .with_context(|| { - format!("reopen persistent database with extension {name} after restart") - })?; - assert_extension_catalog_state(&mut db, extension)?; - db.close() - .with_context(|| format!("close restarted database with extension {name}"))?; - } - Ok(()) - } - - async fn run_server_smoke_set(extensions: &[Extension]) -> Result<()> { - let mut failures = Vec::new(); - for extension in extensions { - if let Err(error) = run_one_server_smoke(*extension).await { - failures.push(format!("{}: {error:?}", extension.sql_name())); - } - } - ensure!( - failures.is_empty(), - "extension server smoke failures:\n{}", - failures.join("\n\n") - ); - Ok(()) - } - - async fn run_one_server_smoke(extension: Extension) -> Result<()> { - let name = extension.sql_name(); - let server = PgliteServer::builder() - .temporary() - .extension(extension) - .start() - .with_context(|| format!("start server with extension {name}"))?; - let mut conn = PgConnection::connect(&server.database_url()) - .await - .with_context(|| format!("connect server with extension {name}"))?; - run_server_smoke(&mut conn, extension).await?; - drop(conn); - server - .shutdown() - .with_context(|| format!("shutdown server with extension {name}"))?; - Ok(()) - } - - fn run_lifecycle_materialization_set(extensions: &[Extension]) -> Result<()> { - let mut failures = Vec::new(); - for extension in extensions { - if let Err(error) = run_one_lifecycle_materialization(*extension) { - failures.push(format!("{}: {error:?}", extension.sql_name())); - } - } - ensure!( - failures.is_empty(), - "extension lifecycle/materialization failures:\n{}", - failures.join("\n\n") - ); - Ok(()) - } - - fn run_one_lifecycle_materialization(extension: Extension) -> Result<()> { - let name = extension.sql_name(); - let root = tempfile::TempDir::new() - .with_context(|| format!("create lifecycle root for extension {name}"))?; - { - let mut db = Pglite::builder() - .path(root.path()) - .extension(extension) - .open() - .with_context(|| format!("open lifecycle database with extension {name}"))?; - db.close() - .with_context(|| format!("close lifecycle database with extension {name}"))?; - } - assert_only_resolved_extension_libraries_are_materialized(root.path(), extension) - } - - fn run_direct_smoke(db: &mut Pglite, extension: Extension) -> Result<()> { - for statement in smoke_sql(extension.sql_name()) { - db.exec(statement, None).with_context(|| { - format!( - "direct smoke failed for extension {} while running:\n{}", - extension.sql_name(), - statement - ) - })?; - } - Ok(()) - } - - async fn run_server_smoke(conn: &mut PgConnection, extension: Extension) -> Result<()> { - for statement in smoke_sql(extension.sql_name()) { - sqlx::query(statement) - .fetch_all(&mut *conn) - .await - .with_context(|| { - format!( - "server smoke failed for extension {} while running:\n{}", - extension.sql_name(), - statement - ) - })?; - } - Ok(()) - } - - fn assert_extension_catalog_state(db: &mut Pglite, extension: Extension) -> Result<()> { - if extension.setup().create_extension { - let result = db.query( - "SELECT count(*)::int4 AS count FROM pg_extension WHERE extname = $1", - &[serde_json::json!(extension.sql_name())], - None, - )?; - ensure!( - result.rows[0]["count"] == serde_json::json!(1), - "extension {} should survive restart in pg_extension", - extension.sql_name() - ); - } else { - let result = db.query("SELECT 1::int4 AS ok", &[], None)?; - ensure!( - result.rows[0]["ok"] == serde_json::json!(1), - "extension {} should reopen cleanly", - extension.sql_name() - ); - } - Ok(()) - } - - fn assert_only_resolved_extension_libraries_are_materialized( - root: &Path, - extension: Extension, - ) -> Result<()> { - let expected = resolve_extension_set(&[extension])? - .into_iter() - .filter_map(|extension| extension.native_module_file().map(PathBuf::from)) - .collect::>(); - let actual = relative_files(&root.join("tmp/pglite/lib/postgresql")) - .into_iter() - .collect::>(); - ensure!( - actual == expected, - "upper runtime library layer for {} should contain only resolved requested libraries; expected {:?}, got {:?}", - extension.sql_name(), - expected, - actual - ); - Ok(()) - } - - fn relative_files(root: &Path) -> Vec { - fn walk(base: &Path, current: &Path, files: &mut Vec) { - let Ok(entries) = std::fs::read_dir(current) else { - return; - }; - for entry in entries { - let entry = entry.expect("read runtime test directory entry"); - let path = entry.path(); - if path.is_dir() { - walk(base, &path, files); - } else if path.is_file() { - files.push( - path.strip_prefix(base) - .expect("relative extension library path") - .to_path_buf(), - ); - } - } - } - - let mut files = Vec::new(); - walk(root, root, &mut files); - files.sort(); - files - } - - fn smoke_sql(sql_name: &str) -> &'static [&'static str] { - // These are compact Rust ports of the PGlite extension smoke tests in - // assets/checkouts/pglite/packages/pglite/tests. - match sql_name { - "age" => &[ - "SELECT ag_catalog.create_graph('oxide_graph')", - "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM ag_catalog.ag_graph WHERE name = 'oxide_graph') THEN RAISE EXCEPTION 'age graph was not created'; END IF; END $$", - "SELECT * FROM ag_catalog.cypher('oxide_graph', $$ RETURN 1 $$) AS (one agtype)", - ], - "amcheck" => &[ - "CREATE TEMP TABLE oxide_amcheck (id int PRIMARY KEY, value text)", - "INSERT INTO oxide_amcheck SELECT i, 'v' || i::text FROM generate_series(1, 8) AS i", - "SELECT bt_index_check('oxide_amcheck_pkey'::regclass)", - ], - "auto_explain" => &["EXPLAIN SELECT count(*) FROM pg_class"], - "bloom" => &[ - "CREATE TEMP TABLE oxide_bloom (id int, value int)", - "CREATE INDEX oxide_bloom_idx ON oxide_bloom USING bloom (id, value)", - "INSERT INTO oxide_bloom SELECT i, i % 3 FROM generate_series(1, 20) AS i", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_bloom WHERE id = 7 AND value = 1; IF n <> 1 THEN RAISE EXCEPTION 'bloom lookup failed: %', n; END IF; END $$", - ], - "btree_gin" => &[ - "CREATE TEMP TABLE oxide_btree_gin (id int)", - "CREATE INDEX oxide_btree_gin_idx ON oxide_btree_gin USING gin (id)", - "INSERT INTO oxide_btree_gin SELECT generate_series(1, 10)", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gin WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gin lookup failed: %', n; END IF; END $$", - ], - "btree_gist" => &[ - "CREATE TEMP TABLE oxide_btree_gist (id int)", - "CREATE INDEX oxide_btree_gist_idx ON oxide_btree_gist USING gist (id)", - "INSERT INTO oxide_btree_gist SELECT generate_series(1, 10)", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_btree_gist WHERE id = 5; IF n <> 1 THEN RAISE EXCEPTION 'btree_gist lookup failed: %', n; END IF; END $$", - ], - "citext" => &[ - "CREATE TEMP TABLE oxide_citext (value citext)", - "INSERT INTO oxide_citext VALUES ('Postgres')", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_citext WHERE value = 'postgres'; IF n <> 1 THEN RAISE EXCEPTION 'citext comparison failed: %', n; END IF; END $$", - ], - "cube" => &[ - "DO $$ DECLARE d float8; BEGIN SELECT cube(array[1,2,3]) <-> cube(array[1,2,4]) INTO d; IF d <> 1 THEN RAISE EXCEPTION 'cube distance failed: %', d; END IF; END $$", - ], - "dict_int" => &[ - "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('intdict', '40865854'), ',') INTO lex; IF lex <> '408658' THEN RAISE EXCEPTION 'dict_int lexize failed: %', lex; END IF; END $$", - ], - "dict_xsyn" => &[ - "ALTER TEXT SEARCH DICTIONARY xsyn (RULES = 'xsyn_sample', KEEPORIG = true, MATCHORIG = true, KEEPSYNONYMS = true, MATCHSYNONYMS = false)", - "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('xsyn', 'supernova'), ',') INTO lex; IF lex IS NULL OR lex !~ 'sn' THEN RAISE EXCEPTION 'dict_xsyn lexize failed: %', lex; END IF; END $$", - ], - "earthdistance" => &[ - "DO $$ DECLARE d float8; BEGIN SELECT earth_distance(ll_to_earth(0, 0), ll_to_earth(0, 1)) INTO d; IF d <= 0 THEN RAISE EXCEPTION 'earthdistance failed: %', d; END IF; END $$", - ], - "file_fdw" => &[ - "CREATE SERVER oxide_file_server FOREIGN DATA WRAPPER file_fdw", - "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_foreign_data_wrapper WHERE fdwname = 'file_fdw') THEN RAISE EXCEPTION 'file_fdw wrapper missing'; END IF; END $$", - ], - "fuzzystrmatch" => &[ - "DO $$ BEGIN IF levenshtein('kitten', 'sitting') <> 3 THEN RAISE EXCEPTION 'levenshtein failed'; END IF; IF soundex('kitten') <> 'K350' THEN RAISE EXCEPTION 'soundex failed'; END IF; END $$", - ], - "hstore" => &[ - "CREATE TEMP TABLE oxide_hstore (attrs hstore)", - "INSERT INTO oxide_hstore VALUES ('a=>1,b=>2'::hstore)", - "DO $$ DECLARE v text; BEGIN SELECT attrs -> 'b' INTO v FROM oxide_hstore; IF v <> '2' THEN RAISE EXCEPTION 'hstore lookup failed: %', v; END IF; END $$", - ], - "intarray" => &[ - "CREATE TEMP TABLE oxide_intarray (tags int[])", - "INSERT INTO oxide_intarray VALUES (ARRAY[1, 2, 5]), (ARRAY[3, 4])", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_intarray WHERE tags && ARRAY[2, 9]; IF n <> 1 THEN RAISE EXCEPTION 'intarray overlap failed: %', n; END IF; SELECT count(*) INTO n FROM oxide_intarray WHERE tags @@ '1 & (2|3)'::query_int; IF n <> 1 THEN RAISE EXCEPTION 'intarray query_int failed: %', n; END IF; END $$", - ], - "isn" => &[ - "DO $$ BEGIN IF isbn('978-0-393-04002-9')::text <> '0-393-04002-X' THEN RAISE EXCEPTION 'isbn failed'; END IF; IF isbn13('0901690546')::text <> '978-0-901690-54-8' THEN RAISE EXCEPTION 'isbn13 failed'; END IF; IF issn('1436-4522')::text <> '1436-4522' THEN RAISE EXCEPTION 'issn failed'; END IF; END $$", - ], - "lo" => &[ - "CREATE TEMP TABLE oxide_lo (id int, data oid)", - "CREATE TRIGGER oxide_lo_manage BEFORE UPDATE OR DELETE ON oxide_lo FOR EACH ROW EXECUTE FUNCTION lo_manage(data)", - "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'oxide_lo_manage') THEN RAISE EXCEPTION 'lo trigger missing'; END IF; END $$", - ], - "ltree" => &[ - "CREATE TEMP TABLE oxide_ltree (path ltree)", - "INSERT INTO oxide_ltree VALUES ('Top.Science.Astronomy'), ('Top.Collections.Pictures')", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ltree WHERE path <@ 'Top.Science'; IF n <> 1 THEN RAISE EXCEPTION 'ltree ancestor query failed: %', n; END IF; END $$", - ], - "pageinspect" => &[ - "CREATE TEMP TABLE oxide_pageinspect (id int)", - "INSERT INTO oxide_pageinspect SELECT generate_series(1, 5)", - "SELECT * FROM page_header(get_raw_page('oxide_pageinspect', 0))", - ], - "pg_buffercache" => &[ - "SELECT * FROM pg_buffercache_summary()", - "SELECT * FROM pg_buffercache_usage_counts()", - ], - "pg_freespacemap" => &[ - "CREATE TEMP TABLE oxide_fsm (id int, value text)", - "INSERT INTO oxide_fsm SELECT i, repeat('x', 200) FROM generate_series(1, 20) AS i", - "DELETE FROM oxide_fsm WHERE id % 2 = 0", - "SELECT * FROM pg_freespace('oxide_fsm') LIMIT 1", - ], - "pg_hashids" => &[ - "DO $$ BEGIN IF id_encode(1001) <> 'jNl' THEN RAISE EXCEPTION 'pg_hashids encode failed'; END IF; IF id_decode_once('jNl') <> 1001 THEN RAISE EXCEPTION 'pg_hashids decode failed'; END IF; END $$", - ], - "pg_ivm" => &[ - "CREATE TABLE oxide_ivm_orders (id int, amount int)", - "INSERT INTO oxide_ivm_orders VALUES (1, 10), (2, 20)", - "SELECT pgivm.create_immv('oxide_ivm_summary', $$ SELECT id, amount FROM oxide_ivm_orders $$)", - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM oxide_ivm_summary; IF n <> 2 THEN RAISE EXCEPTION 'pg_ivm initial count failed: %', n; END IF; END $$", - ], - "pg_surgery" => &[ - "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'heap_force_kill') THEN RAISE EXCEPTION 'pg_surgery function missing'; END IF; END $$", - ], - "pg_textsearch" => &[ - "DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_am WHERE amname = 'bm25') THEN RAISE EXCEPTION 'bm25 access method missing'; END IF; END $$", - "SELECT to_bm25query('postgres wasm')", - ], - "pg_trgm" => &[ - "DO $$ DECLARE score float8; BEGIN SELECT similarity('postgres', 'postgrex') INTO score; IF score <= 0 THEN RAISE EXCEPTION 'pg_trgm similarity failed: %', score; END IF; END $$", - ], - "pg_uuidv7" => &[ - "DO $$ DECLARE id uuid; ts timestamptz; BEGIN SELECT uuid_generate_v7() INTO id; IF length(id::text) <> 36 THEN RAISE EXCEPTION 'uuidv7 length failed'; END IF; SELECT uuid_v7_to_timestamptz('018570bb-4a7d-7c7e-8df4-6d47afd8c8fc') INTO ts; IF ts IS NULL THEN RAISE EXCEPTION 'uuidv7 timestamp failed'; END IF; END $$", - ], - "pg_visibility" => &[ - "CREATE TEMP TABLE oxide_visibility (id int)", - "INSERT INTO oxide_visibility SELECT generate_series(1, 5)", - "SELECT * FROM pg_visibility('oxide_visibility') LIMIT 1", - "SELECT * FROM pg_visibility_map('oxide_visibility') LIMIT 1", - ], - "pg_walinspect" => &[ - "CREATE TEMP TABLE oxide_walinspect (value text)", - "CREATE TEMP TABLE oxide_walinspect_lsn AS SELECT pg_current_wal_lsn() AS before_lsn", - "INSERT INTO oxide_walinspect SELECT 'row ' || i::text FROM generate_series(1, 5) AS i", - "SELECT * FROM pg_get_wal_block_info((SELECT before_lsn FROM oxide_walinspect_lsn), pg_current_wal_lsn()) ORDER BY start_lsn, block_id LIMIT 20", - ], - "pgtap" => &[ - "BEGIN", - "SELECT plan(1)", - "SELECT pass('pgtap smoke')", - "SELECT * FROM finish()", - "ROLLBACK", - ], - "seg" => &[ - "DO $$ BEGIN IF '7(+-)1'::seg::text <> '6 .. 8' THEN RAISE EXCEPTION 'seg cast failed'; END IF; END $$", - ], - "tablefunc" => &[ - "DO $$ DECLARE n int; BEGIN SELECT count(*) INTO n FROM normal_rand(10, 5, 3); IF n <> 10 THEN RAISE EXCEPTION 'normal_rand failed: %', n; END IF; END $$", - "SELECT * FROM crosstab('SELECT 1, 1, 10 UNION ALL SELECT 1, 2, 20') AS ct(rowid int, c1 int, c2 int)", - ], - "tcn" => &[ - "CREATE TEMP TABLE oxide_tcn (id int PRIMARY KEY, value text)", - "CREATE TRIGGER oxide_tcn_trigger AFTER INSERT OR UPDATE OR DELETE ON oxide_tcn FOR EACH ROW EXECUTE FUNCTION triggered_change_notification()", - "INSERT INTO oxide_tcn VALUES (1, 'one')", - ], - "tsm_system_rows" => &[ - "CREATE TEMP TABLE oxide_tsm_rows AS SELECT i FROM generate_series(1, 20) AS i", - "SELECT * FROM oxide_tsm_rows TABLESAMPLE SYSTEM_ROWS(5)", - ], - "tsm_system_time" => &[ - "CREATE TEMP TABLE oxide_tsm_time AS SELECT i FROM generate_series(1, 20) AS i", - "SELECT * FROM oxide_tsm_time TABLESAMPLE SYSTEM_TIME(50)", - ], - "unaccent" => &[ - "DO $$ DECLARE lex text; BEGIN SELECT array_to_string(ts_lexize('unaccent', 'Hôtel'), ',') INTO lex; IF lex <> 'Hotel' THEN RAISE EXCEPTION 'unaccent failed: %', lex; END IF; END $$", - ], - "vector" => &[ - "CREATE TEMP TABLE oxide_vector (embedding vector(3))", - "INSERT INTO oxide_vector VALUES ('[1,2,3]')", - "DO $$ DECLARE d float8; BEGIN SELECT embedding <-> '[1,2,4]'::vector INTO d FROM oxide_vector; IF d <> 1 THEN RAISE EXCEPTION 'vector distance failed: %', d; END IF; END $$", - ], - other => panic!("missing smoke SQL for packaged extension candidate {other}"), - } - } -} diff --git a/src/pglite/mod.rs b/src/pglite/mod.rs deleted file mode 100644 index 756516ee..00000000 --- a/src/pglite/mod.rs +++ /dev/null @@ -1,52 +0,0 @@ -pub(crate) mod aot; -pub(crate) mod assets; -pub(crate) mod backend; -pub(crate) mod base; -pub(crate) mod builder; -pub(crate) mod client; -pub(crate) mod config; -pub(crate) mod data_dir; -pub(crate) mod errors; -#[cfg(feature = "extensions")] -pub mod extensions; -pub(crate) mod interface; -pub(crate) mod parse; -#[cfg(feature = "extensions")] -pub mod pg_dump; -pub(crate) mod postgres_mod; -pub(crate) mod proxy; -pub(crate) mod server; -pub(crate) mod sync_host_fs; -pub(crate) mod templating; -pub(crate) mod timing; -pub(crate) mod transport; -pub(crate) mod types; -pub(crate) mod wire; - -pub use base::{ - InstallOptions, InstallOutcome, MountInfo, PgDataTemplate, PgDataTemplateManifest, PglitePaths, - build_pgdata_template, ensure_cluster, install_and_init, install_and_init_in, install_default, - install_extension_archive, install_extension_bytes, install_into, install_with_options, - preload_runtime_module, -}; -pub use builder::PgliteBuilder; -pub use client::{GlobalListenerHandle, ListenerHandle, Pglite, Transaction}; -pub use config::PostgresConfig; -pub use data_dir::DataDirArchiveFormat; -pub use errors::PgliteError; -pub use interface::{ - DataTransferContainer, DebugLevel, DescribeQueryParam, DescribeQueryResult, - DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, NoticeCallback, - ParserMap, QueryOptions, Results, RowMode, Serializer, SerializerMap, TypeParser, -}; -#[cfg(feature = "extensions")] -pub use pg_dump::PgDumpOptions; -#[doc(hidden)] -pub use postgres_mod::{FsTraceSnapshot, fs_trace_snapshot, reset_fs_trace}; -pub use proxy::{ - PgliteProxy, ProtocolStatsSnapshot, disable_protocol_stats, protocol_stats_snapshot, - reset_protocol_stats, -}; -pub use server::{PgliteServer, PgliteServerBuilder}; -pub use templating::{QueryTemplate, TemplatedQuery, format_query, quote_identifier}; -pub use timing::{PhaseTiming, capture_phase_timings, measure_phase, record_phase_timing}; diff --git a/src/pglite/parse.rs b/src/pglite/parse.rs deleted file mode 100644 index fd77b757..00000000 --- a/src/pglite/parse.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::collections::HashMap; - -use serde_json::Value; - -use crate::pglite::interface::{FieldInfo, ParserMap, QueryOptions, Results, RowMode}; -use crate::pglite::types::ParserLookup; -use crate::protocol::messages::{ - BackendMessage, CommandCompleteMessage, DataRowMessage, RowDescriptionMessage, -}; - -pub fn parse_results( - messages: &[BackendMessage], - default_parsers: &ParserMap, - options: Option<&QueryOptions>, - blob: Option>, -) -> Vec { - let mut result_sets: Vec = Vec::new(); - let mut current_fields: Vec = Vec::new(); - let mut current_rows: Vec = Vec::new(); - let mut affected_rows = 0usize; - - let empty_parsers = HashMap::new(); - let (row_mode, parsers_override) = options - .map(|opts| (opts.row_mode, &opts.parsers)) - .unwrap_or((None, &empty_parsers)); - - let parser_lookup = ParserLookup::new(default_parsers, parsers_override); - - for message in messages { - match message { - BackendMessage::RowDescription(desc) => { - current_fields = map_fields(desc); - } - BackendMessage::DataRow(row) => { - if current_fields.is_empty() { - continue; - } - let row_value = map_row(row, ¤t_fields, &parser_lookup, row_mode); - current_rows.push(row_value); - } - BackendMessage::CommandComplete(cmd) => { - affected_rows += retrieve_row_count(cmd); - result_sets.push(Results { - rows: std::mem::take(&mut current_rows), - fields: current_fields.clone(), - affected_rows: Some(affected_rows), - blob: blob.clone(), - }); - current_fields.clear(); - } - _ => {} - } - } - - if result_sets.is_empty() { - result_sets.push(Results { - rows: Vec::new(), - fields: Vec::new(), - affected_rows: Some(0), - blob, - }) - } - - result_sets -} - -pub fn parse_describe_statement_results(messages: &[BackendMessage]) -> Vec { - messages - .iter() - .find_map(|msg| match msg { - BackendMessage::ParameterDescription(desc) => Some(desc.data_type_ids.clone()), - _ => None, - }) - .unwrap_or_default() -} - -fn map_fields(desc: &RowDescriptionMessage) -> Vec { - desc.fields - .iter() - .map(|field| FieldInfo { - name: field.name.clone(), - data_type_id: field.data_type_id, - }) - .collect() -} - -fn map_row( - row: &DataRowMessage, - fields: &[FieldInfo], - parsers: &ParserLookup, - row_mode: Option, -) -> Value { - match row_mode { - Some(RowMode::Array) => { - let values: Vec = row - .fields - .iter() - .zip(fields.iter()) - .map(|(value, field)| parse_cell(value.as_deref(), field.data_type_id, parsers)) - .collect(); - Value::Array(values) - } - _ => { - let mut map = serde_json::Map::with_capacity(fields.len()); - for (value, field) in row.fields.iter().zip(fields.iter()) { - let parsed = parse_cell(value.as_deref(), field.data_type_id, parsers); - map.insert(field.name.clone(), parsed); - } - Value::Object(map) - } - } -} - -fn parse_cell(value: Option<&str>, type_id: i32, parsers: &ParserLookup) -> Value { - match value { - None => Value::Null, - Some(text) => parsers.apply(text, type_id), - } -} - -fn retrieve_row_count(msg: &CommandCompleteMessage) -> usize { - let parts: Vec<&str> = msg.text.split(' ').collect(); - match parts.first().copied() { - Some("INSERT") => parts.get(2).and_then(|v| v.parse().ok()).unwrap_or(0), - Some("UPDATE") | Some("DELETE") | Some("COPY") | Some("MERGE") => { - parts.get(1).and_then(|v| v.parse().ok()).unwrap_or(0) - } - _ => 0, - } -} diff --git a/src/pglite/pg_dump.rs b/src/pglite/pg_dump.rs deleted file mode 100644 index f9a9e1d8..00000000 --- a/src/pglite/pg_dump.rs +++ /dev/null @@ -1,962 +0,0 @@ -use std::fmt; -use std::io::{Read, Seek, Write}; -use std::mem::MaybeUninit; -use std::net::Shutdown; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::pin::Pin; -use std::sync::mpsc::{self, Receiver, SyncSender}; -use std::sync::{Arc, Mutex}; -use std::task::{Context as TaskContext, Poll}; -use std::thread; -use std::time::{Duration, Instant}; - -use anyhow::{Context, Result, anyhow, bail}; -use tempfile::TempDir; -use wasmer::Store; -use wasmer_types::ModuleHash; -use wasmer_wasix::runners::wasi::{RuntimeOrEngine, WasiRunner}; -use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; -use wasmer_wasix::virtual_fs::{self, AsyncRead, AsyncSeek, AsyncWrite}; -use wasmer_wasix::virtual_net::tcp_pair::TcpSocketHalf; -use wasmer_wasix::virtual_net::{ - self, InterestHandler, NetworkError, SocketStatus, VirtualConnectedSocket, VirtualIoSource, - VirtualNetworking, VirtualSocket, VirtualTcpSocket, -}; -use wasmer_wasix::{LocalNetworking, PluggableRuntime, VirtualFile}; - -use crate::pglite::sync_host_fs::SyncHostFileSystem; -use crate::pglite::timing; -use crate::pglite::{aot, assets}; - -/// Options for the bundled WASIX `pg_dump` runner. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PgDumpOptions { - args: Vec, - database: String, - username: String, -} - -impl Default for PgDumpOptions { - fn default() -> Self { - Self { - args: Vec::new(), - database: "template1".to_owned(), - username: "postgres".to_owned(), - } - } -} - -impl PgDumpOptions { - pub fn new() -> Self { - Self::default() - } - - /// Add one raw `pg_dump` argument. - pub fn arg(mut self, arg: impl Into) -> Self { - self.args.push(arg.into()); - self - } - - /// Add raw `pg_dump` arguments. - pub fn args(mut self, args: impl IntoIterator>) -> Self { - self.args.extend(args.into_iter().map(Into::into)); - self - } - - /// Select the database to dump. - pub fn database(mut self, database: impl Into) -> Self { - self.database = database.into(); - self - } - - /// Select the user passed to `pg_dump`. - 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'), - "pg_dump {name} must not be empty or contain NUL bytes" - ); - } - for arg in &self.args { - anyhow::ensure!( - !arg.contains('\0'), - "pg_dump argument must not contain NUL bytes" - ); - validate_passthrough_arg(arg)?; - } - Ok(()) - } - - pub(crate) fn database_ref(&self) -> &str { - &self.database - } - - pub(crate) fn username_ref(&self) -> &str { - &self.username - } -} - -fn validate_passthrough_arg(arg: &str) -> Result<()> { - if let Some(flag) = disallowed_pg_dump_flag(arg) { - anyhow::bail!( - "pg_dump argument '{arg}' conflicts with pglite-oxide's managed {flag}; use PgDumpOptions typed setters where available" - ); - } - Ok(()) -} - -fn disallowed_pg_dump_flag(arg: &str) -> Option<&'static str> { - const LONG_FLAGS: &[(&str, &str)] = &[ - ("--file", "output file"), - ("--format", "output format"), - ("--host", "host"), - ("--port", "port"), - ("--username", "username"), - ("--dbname", "database"), - ("--jobs", "job count"), - ]; - 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)] = &[ - ("-f", "output file"), - ("-F", "output format"), - ("-h", "host"), - ("-p", "port"), - ("-U", "username"), - ("-d", "database"), - ("-j", "job count"), - ]; - 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) type PgDumpVirtualSocket = TcpSocketHalf; - -pub(crate) fn dump_direct_sql(options: &PgDumpOptions, serve: F) -> Result -where - F: FnOnce(PgDumpVirtualSocket) -> Result<()>, -{ - options.validate()?; - let (socket_tx, socket_rx) = mpsc::sync_channel(1); - let networking = DirectPgDumpNetworking::new(socket_tx); - let runner_options = options.clone(); - let runner = thread::spawn(move || { - dump_sql_with_networking(DIRECT_PG_DUMP_ADDR, &runner_options, networking) - }); - - let accepted = receive_direct_pg_dump_socket(&socket_rx, &runner) - .context("accept direct pg_dump virtual protocol connection"); - let serve_result = match accepted { - Ok(socket) => serve(socket), - Err(err) => Err(err), - }; - let dump_result = runner - .join() - .map_err(|_| anyhow!("direct pg_dump runner thread panicked"))?; - - match (serve_result, dump_result) { - (Ok(()), Ok(sql)) => Ok(sql), - (Err(err), Ok(_)) => Err(err), - (Ok(()), Err(err)) => Err(err), - (Err(err), Err(dump_err)) => { - Err(err.context(format!("direct pg_dump runner also failed: {dump_err:#}"))) - } - } -} - -fn dump_sql_with_networking( - addr: SocketAddr, - options: &PgDumpOptions, - networking: N, -) -> Result -where - N: VirtualNetworking + Sync, -{ - options.validate()?; - 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"))? - }; - let engine = aot::headless_engine(); - let module = { - let _phase = timing::phase("pg_dump.load_aot"); - aot::load_pg_dump_module(&engine)? - }; - let _store = Store::new(engine.clone()); - - let fs_root = TempDir::new().context("create pg_dump WASIX filesystem root")?; - let runtime = { - let _phase = timing::phase("pg_dump.tokio_runtime"); - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("create Tokio runtime for WASIX pg_dump")? - }; - let (host_fs, wasix_runtime) = { - let _phase = timing::phase("pg_dump.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 output_path = "/host/out.sql"; - 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 = options.args.clone(); - args.extend([ - "-U".to_owned(), - options.username.clone(), - "-h".to_owned(), - host, - "-p".to_owned(), - port, - "--inserts".to_owned(), - "-j".to_owned(), - "1".to_owned(), - "-f".to_owned(), - output_path.to_owned(), - ]); - args.push(options.database.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("/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)))); - { - let _phase = timing::phase("pg_dump.run_wasm"); - runner - .run_wasm( - RuntimeOrEngine::Runtime(Arc::new(wasix_runtime)), - "pg_dump", - 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}; pg_dump stderr: {stderr}") - } - }) - .context("run WASIX pg_dump")?; - } - - { - let _phase = timing::phase("pg_dump.read_output"); - match std::fs::read_to_string(fs_root.path().join("out.sql")) { - Ok(sql) => Ok(sql), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - let stdout = stdout.lock().expect("stdout capture poisoned"); - if stdout.is_empty() { - Err(err).with_context(|| { - format!( - "read pg_dump output {}", - fs_root.path().join("out.sql").display() - ) - }) - } else { - String::from_utf8(stdout.clone()).context("decode pg_dump stdout as UTF-8") - } - } - Err(err) => Err(err).with_context(|| { - format!( - "read pg_dump output {}", - fs_root.path().join("out.sql").display() - ) - }), - } - } -} - -const DIRECT_PG_DUMP_PORT: u16 = 65_432; -const DIRECT_PG_DUMP_SOCKET_BUFFER: usize = 8 * 1024 * 1024; -const DIRECT_PG_DUMP_LOCAL_PORT: u16 = 65_431; -const DIRECT_PG_DUMP_ADDR: SocketAddr = - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_PORT); -const DIRECT_PG_DUMP_LOCAL_ADDR: SocketAddr = - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DIRECT_PG_DUMP_LOCAL_PORT); - -struct DirectPgDumpNetworking { - socket_tx: Mutex>>, -} - -impl DirectPgDumpNetworking { - fn new(socket_tx: SyncSender) -> Self { - Self { - socket_tx: Mutex::new(Some(socket_tx)), - } - } -} - -impl fmt::Debug for DirectPgDumpNetworking { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DirectPgDumpNetworking") - .finish_non_exhaustive() - } -} - -#[async_trait::async_trait] -impl VirtualNetworking for DirectPgDumpNetworking { - async fn connect_tcp( - &self, - addr: SocketAddr, - peer: SocketAddr, - ) -> virtual_net::Result> { - if peer != DIRECT_PG_DUMP_ADDR { - return Err(NetworkError::ConnectionRefused); - } - - let sender = self - .socket_tx - .lock() - .map_err(|_| NetworkError::IOError)? - .take() - .ok_or(NetworkError::ConnectionRefused)?; - let local = if addr.port() == 0 { - DIRECT_PG_DUMP_LOCAL_ADDR - } else { - addr - }; - let (guest, host) = TcpSocketHalf::channel(DIRECT_PG_DUMP_SOCKET_BUFFER, local, peer); - sender - .send(host) - .map_err(|_| NetworkError::ConnectionAborted)?; - Ok(Box::new(DirectPgDumpTcpSocket { - inner: guest, - first_write_ready_probe: true, - })) - } - - async fn resolve( - &self, - host: &str, - _port: Option, - _dns_server: Option, - ) -> virtual_net::Result> { - match host { - "localhost" | "127.0.0.1" => Ok(vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]), - _ => Err(NetworkError::AddressNotAvailable), - } - } -} - -#[derive(Debug)] -struct DirectPgDumpTcpSocket { - inner: TcpSocketHalf, - // WASIX probes writability once while completing a blocking connect. - // `TcpSocketHalf` suppresses an immediate second write-ready poll until a - // write happens, but libpq polls again before its first StartupMessage. - // Keep the adapter level-triggered for that connect-to-first-write handoff. - first_write_ready_probe: bool, -} - -impl VirtualIoSource for DirectPgDumpTcpSocket { - fn remove_handler(&mut self) { - self.inner.remove_handler(); - } - - fn poll_read_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { - self.inner.poll_read_ready(cx) - } - - fn poll_write_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll> { - if self.first_write_ready_probe { - self.first_write_ready_probe = false; - return Poll::Ready(Ok(self.inner.send_buf_size().unwrap_or(1).max(1))); - } - self.inner.poll_write_ready(cx) - } -} - -impl VirtualSocket for DirectPgDumpTcpSocket { - fn set_ttl(&mut self, ttl: u32) -> virtual_net::Result<()> { - self.inner.set_ttl(ttl) - } - - fn ttl(&self) -> virtual_net::Result { - self.inner.ttl() - } - - fn addr_local(&self) -> virtual_net::Result { - self.inner.addr_local() - } - - fn status(&self) -> virtual_net::Result { - self.inner.status() - } - - fn set_handler( - &mut self, - handler: Box, - ) -> virtual_net::Result<()> { - self.inner.set_handler(handler) - } -} - -impl VirtualConnectedSocket for DirectPgDumpTcpSocket { - fn set_linger(&mut self, linger: Option) -> virtual_net::Result<()> { - self.inner.set_linger(linger) - } - - fn linger(&self) -> virtual_net::Result> { - self.inner.linger() - } - - fn try_send(&mut self, data: &[u8]) -> virtual_net::Result { - self.inner.try_send(data) - } - - fn try_flush(&mut self) -> virtual_net::Result<()> { - self.inner.try_flush() - } - - fn close(&mut self) -> virtual_net::Result<()> { - self.inner.close() - } - - fn try_recv(&mut self, buf: &mut [MaybeUninit], peek: bool) -> virtual_net::Result { - self.inner.try_recv(buf, peek) - } -} - -impl VirtualTcpSocket for DirectPgDumpTcpSocket { - fn set_recv_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { - self.inner.set_recv_buf_size(size) - } - - fn recv_buf_size(&self) -> virtual_net::Result { - self.inner.recv_buf_size() - } - - fn set_send_buf_size(&mut self, size: usize) -> virtual_net::Result<()> { - self.inner.set_send_buf_size(size) - } - - fn send_buf_size(&self) -> virtual_net::Result { - self.inner.send_buf_size() - } - - fn set_nodelay(&mut self, reuse: bool) -> virtual_net::Result<()> { - self.inner.set_nodelay(reuse) - } - - fn nodelay(&self) -> virtual_net::Result { - self.inner.nodelay() - } - - fn set_keepalive(&mut self, keepalive: bool) -> virtual_net::Result<()> { - self.inner.set_keepalive(keepalive) - } - - fn keepalive(&self) -> virtual_net::Result { - self.inner.keepalive() - } - - fn set_dontroute(&mut self, keepalive: bool) -> virtual_net::Result<()> { - self.inner.set_dontroute(keepalive) - } - - fn dontroute(&self) -> virtual_net::Result { - self.inner.dontroute() - } - - fn addr_peer(&self) -> virtual_net::Result { - self.inner.addr_peer() - } - - fn shutdown(&mut self, how: Shutdown) -> virtual_net::Result<()> { - self.inner.shutdown(how) - } - - fn is_closed(&self) -> bool { - self.inner.is_closed() - } -} - -fn receive_direct_pg_dump_socket( - socket_rx: &Receiver, - runner: &thread::JoinHandle>, -) -> Result { - let started = Instant::now(); - loop { - match socket_rx.recv_timeout(Duration::from_millis(5)) { - Ok(socket) => return Ok(socket), - Err(mpsc::RecvTimeoutError::Timeout) => { - if runner.is_finished() { - bail!("pg_dump exited before opening the direct virtual protocol connection"); - } - if started.elapsed() > Duration::from_secs(30) { - bail!( - "timed out waiting for pg_dump to open the direct virtual protocol connection" - ); - } - } - Err(mpsc::RecvTimeoutError::Disconnected) => { - bail!("pg_dump direct virtual networking channel closed before connect") - } - } - } -} - -#[derive(Debug)] -struct CaptureFile { - buffer: Arc>>, -} - -impl CaptureFile { - fn new(buffer: Arc>>) -> Self { - Self { buffer } - } -} - -impl VirtualFile for CaptureFile { - fn last_accessed(&self) -> u64 { - 0 - } - - fn last_modified(&self) -> u64 { - 0 - } - - fn created_time(&self) -> u64 { - 0 - } - - fn size(&self) -> u64 { - self.buffer.lock().expect("capture lock poisoned").len() as u64 - } - - fn set_len(&mut self, _new_size: u64) -> Result<(), wasmer_wasix::FsError> { - Err(wasmer_wasix::FsError::PermissionDenied) - } - - fn unlink(&mut self) -> Result<(), wasmer_wasix::FsError> { - Ok(()) - } - - fn poll_read_ready( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - ) -> Poll> { - Poll::Ready(Ok(0)) - } - - fn poll_write_ready( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - ) -> Poll> { - Poll::Ready(Ok(8192)) - } -} - -impl AsyncRead for CaptureFile { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - _buf: &mut tokio::io::ReadBuf<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl AsyncWrite for CaptureFile { - fn poll_write( - mut self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - Poll::Ready(self.write(buf)) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl AsyncSeek for CaptureFile { - fn start_seek(self: Pin<&mut Self>, _position: std::io::SeekFrom) -> std::io::Result<()> { - Ok(()) - } - - fn poll_complete( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - ) -> Poll> { - Poll::Ready(Ok(0)) - } -} - -impl Read for CaptureFile { - fn read(&mut self, _buf: &mut [u8]) -> std::io::Result { - Ok(0) - } -} - -impl Write for CaptureFile { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.buffer - .lock() - .expect("capture lock poisoned") - .extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - Ok(()) - } -} - -impl Seek for CaptureFile { - fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result { - Ok(0) - } -} - -#[cfg(all(test, feature = "extensions"))] -mod tests { - use super::*; - use crate::pglite::Pglite; - use crate::pglite::extensions; - use crate::pglite::server::PgliteServer; - use serde_json::json; - use sqlx::{Connection, Executor, Row}; - - #[test] - fn pg_dump_options_reject_managed_args() { - for arg in [ - "-f", - "-f/tmp/out.sql", - "--file", - "--file=/tmp/out.sql", - "-F", - "-Fc", - "--format", - "--format=custom", - "-h", - "-hlocalhost", - "--host=localhost", - "-p", - "-p5432", - "--port=5432", - "-U", - "-Upostgres", - "--username=postgres", - "-d", - "-dpostgres", - "--dbname=postgres", - "-j", - "-j2", - "--jobs=2", - ] { - let err = PgDumpOptions::new() - .arg(arg) - .validate() - .expect_err("managed pg_dump arg should be rejected"); - assert!( - err.to_string().contains("conflicts with pglite-oxide"), - "unexpected error for {arg}: {err:#}" - ); - } - } - - #[test] - fn pg_dump_options_allow_dump_shaping_args() -> Result<()> { - PgDumpOptions::new() - .args([ - "--schema-only", - "--quote-all-identifiers", - "-n", - "public", - "-t", - "dump_items", - ]) - .validate() - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn pg_dump_round_trip_plain_sql() -> Result<()> { - let server = PgliteServer::temporary_tcp()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()) - .await - .context("connect to PGlite server")?; - conn.execute( - "CREATE TABLE dump_items(id INTEGER PRIMARY KEY, value TEXT); - CREATE INDEX dump_items_value_idx ON dump_items(value); - CREATE SEQUENCE dump_items_seq START WITH 10; - CREATE VIEW dump_item_values AS SELECT value FROM dump_items; - INSERT INTO dump_items(id, value) VALUES (1, 'alpha'), (2, 'beta'); - SELECT nextval('dump_items_seq');", - ) - .await - .context("seed pg_dump source data")?; - drop(conn); - - let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { - let dump = server.dump_sql(PgDumpOptions::default())?; - Ok((server, dump)) - }) - .await - .context("join pg_dump task")??; - - assert!(dump.contains("PostgreSQL database dump")); - assert!( - dump.contains("CREATE TABLE public.dump_items"), - "dump did not contain dump_items table DDL:\n{dump}" - ); - assert!(dump.contains("CREATE INDEX dump_items_value_idx")); - assert!(dump.contains("CREATE SEQUENCE public.dump_items_seq")); - assert!(dump.contains("CREATE VIEW public.dump_item_values")); - assert!(dump.contains("INSERT INTO")); - - let (server, schema_only) = tokio::task::spawn_blocking(move || -> Result<_> { - let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; - Ok((server, dump)) - }) - .await - .context("join schema-only pg_dump task")??; - assert!(schema_only.contains("CREATE TABLE public.dump_items")); - assert!( - !schema_only.contains("INSERT INTO public.dump_items"), - "schema-only dump unexpectedly contained data:\n{schema_only}" - ); - - let (server, quoted) = tokio::task::spawn_blocking(move || -> Result<_> { - let dump = server.dump_sql(PgDumpOptions::new().arg("--quote-all-identifiers"))?; - Ok((server, dump)) - }) - .await - .context("join quoted pg_dump task")??; - assert!(quoted.contains("CREATE TABLE \"public\".\"dump_items\"")); - assert!(quoted.contains("INSERT INTO \"public\".\"dump_items\"")); - - let mut usable = sqlx::PgConnection::connect(&server.database_url()) - .await - .context("reconnect after pg_dump")?; - let row = sqlx::query("SELECT count(*)::int4 AS count FROM public.dump_items") - .fetch_one(&mut usable) - .await - .context("server should remain usable after pg_dump")?; - assert_eq!(row.try_get::("count")?, 2); - usable.close().await?; - - server.shutdown()?; - - tokio::task::spawn_blocking(move || -> Result<()> { - let mut restored = Pglite::builder().temporary().open()?; - restored.exec(&dump, None).context("restore pg_dump SQL")?; - let result = restored.query( - "SELECT value FROM public.dump_items WHERE id = $1", - &[json!(2)], - None, - )?; - let value = result - .rows - .first() - .and_then(|row| row.get("value")) - .cloned(); - assert_eq!(value, Some(json!("beta"))); - let view = restored.query( - "SELECT count(*)::int AS count FROM public.dump_item_values", - &[], - None, - )?; - assert_eq!(view.rows[0]["count"], json!(2)); - let sequence = restored.query( - "SELECT nextval('public.dump_items_seq')::int AS next_value", - &[], - None, - )?; - assert_eq!(sequence.rows[0]["next_value"], json!(11)); - restored.close()?; - Ok(()) - }) - .await - .context("join restore task")??; - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn pg_dump_round_trip_vector_extension() -> Result<()> { - let server = PgliteServer::builder() - .temporary() - .extension(extensions::VECTOR) - .start()?; - let mut conn = sqlx::PgConnection::connect(&server.database_url()) - .await - .context("connect to extension-enabled PGlite server")?; - conn.execute( - "CREATE TABLE vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); - INSERT INTO vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", - ) - .await - .context("seed vector pg_dump source data")?; - drop(conn); - - let (server, dump) = tokio::task::spawn_blocking(move || -> Result<_> { - let dump = server.dump_sql(PgDumpOptions::default())?; - Ok((server, dump)) - }) - .await - .context("join vector pg_dump task")??; - server.shutdown()?; - - assert!( - dump.contains("CREATE EXTENSION IF NOT EXISTS vector"), - "dump did not contain vector extension DDL:\n{dump}" - ); - assert!(dump.contains("CREATE TABLE public.vector_dump_items")); - assert!(dump.contains("'[1,2,3]'")); - - tokio::task::spawn_blocking(move || -> Result<()> { - let mut restored = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - restored - .exec(&dump, None) - .context("restore vector dump SQL")?; - let result = restored.query( - "SELECT embedding <-> '[1,2,4]'::vector AS distance \ - FROM public.vector_dump_items WHERE id = $1", - &[json!(1)], - None, - )?; - let distance = result - .rows - .first() - .and_then(|row| row.get("distance")) - .and_then(|value| value.as_f64()); - assert_eq!(distance, Some(1.0)); - restored.close()?; - Ok(()) - }) - .await - .context("join vector restore task")??; - Ok(()) - } - - #[test] - fn direct_pg_dump_public_api_round_trip() -> Result<()> { - let mut db = Pglite::temporary()?; - db.exec("CREATE TABLE direct_dump_items(value TEXT)", None)?; - db.exec("INSERT INTO direct_dump_items VALUES ('alpha')", None)?; - - let mismatched_database = db - .dump_sql(PgDumpOptions::new().database("other_database")) - .expect_err("direct pg_dump should reject database switching"); - assert!( - mismatched_database - .to_string() - .contains("already-open embedded backend database"), - "unexpected direct pg_dump database mismatch error: {mismatched_database:#}" - ); - - let dump = db.dump_sql(PgDumpOptions::new())?; - assert!(dump.contains("CREATE TABLE public.direct_dump_items")); - assert!(dump.contains("INSERT INTO")); - let source_still_usable = db.query( - "SELECT count(*)::int AS count FROM direct_dump_items", - &[], - None, - )?; - assert_eq!(source_still_usable.rows[0]["count"], json!(1)); - - let mut restored = Pglite::temporary()?; - restored.exec(&dump, None)?; - let result = restored.query("SELECT value FROM public.direct_dump_items", &[], None)?; - assert_eq!(result.rows[0]["value"], json!("alpha")); - - restored.close()?; - db.close()?; - Ok(()) - } - - #[test] - fn direct_pg_dump_round_trip_vector_extension() -> Result<()> { - let mut db = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - db.exec( - "CREATE TABLE direct_vector_dump_items(id INTEGER PRIMARY KEY, embedding vector(3)); - INSERT INTO direct_vector_dump_items(id, embedding) VALUES (1, '[1,2,3]');", - None, - )?; - - let dump = db.dump_sql(PgDumpOptions::new())?; - assert!(dump.contains("CREATE EXTENSION IF NOT EXISTS vector")); - assert!(dump.contains("CREATE TABLE public.direct_vector_dump_items")); - - let mut restored = Pglite::builder() - .temporary() - .extension(extensions::VECTOR) - .open()?; - restored.exec(&dump, None)?; - let result = restored.query( - "SELECT embedding <-> '[1,2,4]'::vector AS distance \ - FROM public.direct_vector_dump_items WHERE id = $1", - &[json!(1)], - None, - )?; - assert_eq!(result.rows[0]["distance"], json!(1.0)); - - restored.close()?; - db.close()?; - Ok(()) - } -} diff --git a/src/pglite/postgres_mod.rs b/src/pglite/postgres_mod.rs deleted file mode 100644 index d4c4d2e5..00000000 --- a/src/pglite/postgres_mod.rs +++ /dev/null @@ -1,3385 +0,0 @@ -#[cfg(debug_assertions)] -use std::cell::Cell; -use std::collections::{HashSet, VecDeque}; -use std::fmt; -use std::fs; -use std::future::Future; -use std::io::{self, Read, Write}; -use std::path::{Component, Path, PathBuf}; -use std::pin::Pin; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, OnceLock}; -use std::task::{Context as TaskContext, Poll}; -use std::time::{Duration, Instant}; - -use anyhow::{Context, Result, ensure}; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use tokio::io::ReadBuf; -use tokio::runtime::Runtime as TokioRuntime; -use tracing::{debug, warn}; -use wasmer::{Engine, Instance, Module, Store, TypedFunction, WasmTypeList}; -use wasmer_config::package::{PackageHash, PackageId}; -use wasmer_types::ModuleHash; -use wasmer_wasix::bin_factory::{BinaryPackage, BinaryPackageCommand, spawn_exec}; -use wasmer_wasix::fs::WasiFsRoot; -use wasmer_wasix::runners::wasi::{PackageOrHash, RuntimeOrEngine, WasiRunner}; -use wasmer_wasix::runtime::module_cache::ModuleCache; -use wasmer_wasix::runtime::module_cache::SharedCache; -use wasmer_wasix::runtime::task_manager::VirtualTaskManagerExt; -use wasmer_wasix::runtime::task_manager::tokio::TokioTaskManager; -use wasmer_wasix::runtime::{PluggableRuntime, Runtime}; -use wasmer_wasix::virtual_fs::null_file::NullFile; -use wasmer_wasix::{WasiError, WasiFunctionEnv, virtual_fs}; -use webc::metadata::Command as WebcCommand; -use webc::metadata::annotations::{WASI_RUNNER_URI, Wasi}; - -use super::aot; -use super::base::{PglitePaths, RuntimeLayout}; -use super::config::{PostgresConfig, StartupConfig}; -#[cfg(feature = "extensions")] -use super::extensions::Extension; -use super::sync_host_fs::SyncHostFileSystem; -use super::timing; - -const PGLITE_EXE_PATH: &str = "/bin/pglite"; -const PGDATA_DIR: &str = "/base"; -const WASM_PREFIX: &str = "/"; -const RUNTIME_SIDE_MODULES: &[(&str, &str)] = &[ - ("plpgsql.so", "runtime-support:plpgsql"), - ("dict_snowball.so", "runtime-support:dict_snowball"), -]; -const PGLITE_EXIT_ALIVE: i32 = 99; -const POSTGRES_MAIN_LONGJMP: i32 = 100; -const WASIX_DEVICE_FILES: &[&str] = &[ - "null", "zero", "urandom", "stdin", "stdout", "stderr", "tty", -]; - -#[derive(Debug, Default)] -struct TailCaptureState { - bytes: VecDeque, -} - -#[derive(Debug, Clone)] -struct TailCaptureFile { - inner: Arc>, - limit: usize, -} - -#[derive(Debug, Clone)] -struct TailCaptureHandle { - inner: Arc>, -} - -impl TailCaptureFile { - fn new(limit: usize) -> (Self, TailCaptureHandle) { - let inner = Arc::new(Mutex::new(TailCaptureState::default())); - ( - Self { - inner: inner.clone(), - limit, - }, - TailCaptureHandle { inner }, - ) - } - - fn push_tail(&self, bytes: &[u8]) { - let Ok(mut state) = self.inner.lock() else { - return; - }; - for byte in bytes { - state.bytes.push_back(*byte); - while state.bytes.len() > self.limit { - state.bytes.pop_front(); - } - } - } -} - -impl TailCaptureHandle { - fn text(&self) -> String { - let Ok(state) = self.inner.lock() else { - return "".to_owned(); - }; - let bytes = state.bytes.iter().copied().collect::>(); - String::from_utf8_lossy(&bytes).into_owned() - } -} - -impl virtual_fs::AsyncSeek for TailCaptureFile { - fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { - Ok(()) - } - - fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(0)) - } -} - -impl virtual_fs::AsyncRead for TailCaptureFile { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - _buf: &mut ReadBuf<'_>, - ) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl virtual_fs::AsyncWrite for TailCaptureFile { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - self.push_tail(buf); - Poll::Ready(Ok(buf.len())) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn poll_write_vectored( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - bufs: &[io::IoSlice<'_>], - ) -> Poll> { - let mut total = 0; - for buf in bufs { - self.push_tail(buf); - total += buf.len(); - } - Poll::Ready(Ok(total)) - } - - fn is_write_vectored(&self) -> bool { - true - } -} - -#[async_trait::async_trait] -impl virtual_fs::VirtualFile for TailCaptureFile { - fn last_accessed(&self) -> u64 { - 0 - } - - fn last_modified(&self) -> u64 { - 0 - } - - fn created_time(&self) -> u64 { - 0 - } - - fn size(&self) -> u64 { - self.inner - .lock() - .map(|state| state.bytes.len() as u64) - .unwrap_or(0) - } - - fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { - Ok(()) - } - - fn unlink(&mut self) -> virtual_fs::Result<()> { - Ok(()) - } - - fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(0)) - } - - fn poll_write_ready( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - ) -> Poll> { - Poll::Ready(Ok(self.limit)) - } -} -const BACKEND_C_TIMINGS: &[(i32, &str)] = &[ - (1, "postgres.backend.c.main_pre"), - (2, "postgres.backend.c.restart_single_user_main"), - (3, "postgres.backend.c.async_single_user_main"), - (4, "postgres.backend.c.standalone_process"), - (5, "postgres.backend.c.guc_init"), - (6, "postgres.backend.c.switch_parse"), - (7, "postgres.backend.c.config_files"), - (8, "postgres.backend.c.data_dir_lock"), - (9, "postgres.backend.c.control_file"), - (10, "postgres.backend.c.preload_libraries"), - (11, "postgres.backend.c.shared_memory"), - (12, "postgres.backend.c.base_init"), - (13, "postgres.backend.c.init_postgres"), - (14, "postgres.backend.c.post_init"), - (15, "postgres.backend.c.message_contexts"), - (16, "postgres.backend.c.postmaster_environment"), - (17, "postgres.backend.c.init_proc_phase2"), - (18, "postgres.backend.c.startup_xlog"), - (19, "postgres.backend.c.relcache_catcache_init"), - (20, "postgres.backend.c.transaction_snapshot"), - (21, "postgres.backend.c.session_user"), - (22, "postgres.backend.c.database_lookup"), - (23, "postgres.backend.c.database_lock_recheck"), - (24, "postgres.backend.c.database_path"), - (25, "postgres.backend.c.relcache_phase3"), - (26, "postgres.backend.c.check_my_database"), - (27, "postgres.backend.c.startup_options"), - (28, "postgres.backend.c.process_settings"), - (29, "postgres.backend.c.session_initialization"), - (30, "postgres.backend.c.session_preload_libraries"), - (31, "postgres.backend.c.init_max_backends"), - (32, "postgres.backend.c.create_shared_memory"), - (33, "postgres.backend.c.init_process"), - (34, "postgres.backend.c.relation_cache_phase3"), - (35, "postgres.backend.c.initialize_acl"), - (36, "postgres.backend.c.exec_simple_query"), - (37, "postgres.backend.c.exec_start_xact"), - (38, "postgres.backend.c.exec_drop_unnamed"), - (39, "postgres.backend.c.exec_parse"), - (40, "postgres.backend.c.exec_snapshot"), - (41, "postgres.backend.c.exec_analyze_rewrite"), - (42, "postgres.backend.c.exec_plan"), - (43, "postgres.backend.c.exec_portal_start"), - (44, "postgres.backend.c.exec_dest_receiver"), - (45, "postgres.backend.c.exec_portal_run"), - (46, "postgres.backend.c.exec_finish_xact"), - (47, "postgres.backend.c.exec_command_counter"), - (48, "postgres.backend.c.exec_end_command"), -]; - -static FS_TRACE: FsTraceState = FsTraceState::new(); - -#[derive(Debug, Clone, Copy, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FsTraceSnapshot { - enabled: bool, - open_count: u64, - read_count: u64, - read_bytes: u64, - write_count: u64, - write_bytes: u64, - seek_count: u64, - metadata_count: u64, - read_dir_count: u64, - create_dir_count: u64, - remove_file_count: u64, - remove_dir_count: u64, - rename_count: u64, - set_len_count: u64, - unlink_count: u64, - total_elapsed_micros: u64, - read_elapsed_micros: u64, - write_elapsed_micros: u64, - seek_elapsed_micros: u64, -} - -struct FsTraceState { - open_count: AtomicU64, - read_count: AtomicU64, - read_bytes: AtomicU64, - write_count: AtomicU64, - write_bytes: AtomicU64, - seek_count: AtomicU64, - metadata_count: AtomicU64, - read_dir_count: AtomicU64, - create_dir_count: AtomicU64, - remove_file_count: AtomicU64, - remove_dir_count: AtomicU64, - rename_count: AtomicU64, - set_len_count: AtomicU64, - unlink_count: AtomicU64, - total_elapsed_micros: AtomicU64, - read_elapsed_micros: AtomicU64, - write_elapsed_micros: AtomicU64, - seek_elapsed_micros: AtomicU64, -} - -impl FsTraceState { - const fn new() -> Self { - Self { - open_count: AtomicU64::new(0), - read_count: AtomicU64::new(0), - read_bytes: AtomicU64::new(0), - write_count: AtomicU64::new(0), - write_bytes: AtomicU64::new(0), - seek_count: AtomicU64::new(0), - metadata_count: AtomicU64::new(0), - read_dir_count: AtomicU64::new(0), - create_dir_count: AtomicU64::new(0), - remove_file_count: AtomicU64::new(0), - remove_dir_count: AtomicU64::new(0), - rename_count: AtomicU64::new(0), - set_len_count: AtomicU64::new(0), - unlink_count: AtomicU64::new(0), - total_elapsed_micros: AtomicU64::new(0), - read_elapsed_micros: AtomicU64::new(0), - write_elapsed_micros: AtomicU64::new(0), - seek_elapsed_micros: AtomicU64::new(0), - } - } - - fn reset(&self) { - for counter in [ - &self.open_count, - &self.read_count, - &self.read_bytes, - &self.write_count, - &self.write_bytes, - &self.seek_count, - &self.metadata_count, - &self.read_dir_count, - &self.create_dir_count, - &self.remove_file_count, - &self.remove_dir_count, - &self.rename_count, - &self.set_len_count, - &self.unlink_count, - &self.total_elapsed_micros, - &self.read_elapsed_micros, - &self.write_elapsed_micros, - &self.seek_elapsed_micros, - ] { - counter.store(0, Ordering::Relaxed); - } - } - - fn record_total(&self, elapsed: Duration) { - self.total_elapsed_micros.fetch_add( - elapsed.as_micros().min(u64::MAX as u128) as u64, - Ordering::Relaxed, - ); - } - - fn snapshot(&self) -> FsTraceSnapshot { - FsTraceSnapshot { - enabled: fs_trace_enabled(), - open_count: self.open_count.load(Ordering::Relaxed), - read_count: self.read_count.load(Ordering::Relaxed), - read_bytes: self.read_bytes.load(Ordering::Relaxed), - write_count: self.write_count.load(Ordering::Relaxed), - write_bytes: self.write_bytes.load(Ordering::Relaxed), - seek_count: self.seek_count.load(Ordering::Relaxed), - metadata_count: self.metadata_count.load(Ordering::Relaxed), - read_dir_count: self.read_dir_count.load(Ordering::Relaxed), - create_dir_count: self.create_dir_count.load(Ordering::Relaxed), - remove_file_count: self.remove_file_count.load(Ordering::Relaxed), - remove_dir_count: self.remove_dir_count.load(Ordering::Relaxed), - rename_count: self.rename_count.load(Ordering::Relaxed), - set_len_count: self.set_len_count.load(Ordering::Relaxed), - unlink_count: self.unlink_count.load(Ordering::Relaxed), - total_elapsed_micros: self.total_elapsed_micros.load(Ordering::Relaxed), - read_elapsed_micros: self.read_elapsed_micros.load(Ordering::Relaxed), - write_elapsed_micros: self.write_elapsed_micros.load(Ordering::Relaxed), - seek_elapsed_micros: self.seek_elapsed_micros.load(Ordering::Relaxed), - } - } -} - -pub fn reset_fs_trace() { - FS_TRACE.reset(); -} - -pub fn fs_trace_snapshot() -> FsTraceSnapshot { - FS_TRACE.snapshot() -} -static WASIX_PROCESS_RUNTIME: OnceLock, String>> = - OnceLock::new(); -static SEEDED_SIDE_MODULES: OnceLock>> = OnceLock::new(); - -struct WasixProcessRuntime { - tokio_runtime: Arc, - wasix_module_cache: Arc, - wasix_runtime: Arc, -} - -pub struct PostgresMod { - #[cfg_attr(not(feature = "extensions"), allow(dead_code))] - engine: Engine, - #[cfg_attr(not(feature = "extensions"), allow(dead_code))] - tokio_runtime: Arc, - #[cfg_attr(not(feature = "extensions"), allow(dead_code))] - wasix_module_cache: Arc, - _wasix_runtime: Arc, - store: Store, - _instance: Instance, - env: WasiFunctionEnv, - guest_allocator: GuestAllocator, - io: WasixPgliteIo, - lifecycle: PgliteLifecycleExports, - protocol: WasixProtocolExports, - protocol_stdio: Option, - protocol_stdio_file: ProtocolStdioFile, - wasi_stderr: TailCaptureHandle, - protocol_stdio_attachment: Option, - paths: PglitePaths, - pgdata_template_root: Option, - startup_config: StartupConfig, - startup_response: Option>, - cluster_ready: bool, - backend_started: bool, - started: bool, -} - -pub(crate) struct StartupProtocolResponse { - pub(crate) output: Vec, - pub(crate) accepted: bool, -} - -#[derive(Debug)] -pub(crate) struct StartupErrorResponse { - output: Vec, - summary: String, -} - -impl StartupErrorResponse { - fn new(output: Vec) -> Self { - let summary = summarize_protocol(&output); - Self { output, summary } - } - - pub(crate) fn output(&self) -> &[u8] { - &self.output - } -} - -impl fmt::Display for StartupErrorResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Postgres startup returned a protocol ErrorResponse: {}", - self.summary - ) - } -} - -impl std::error::Error for StartupErrorResponse {} - -pub(crate) fn startup_error_response_output(err: &anyhow::Error) -> Option<&[u8]> { - err.downcast_ref::() - .map(StartupErrorResponse::output) -} - -pub(crate) enum ProtocolPumpOutcome { - Buffered(Vec), - Streamed, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ProtocolTransportMode { - Buffered = 0, - Stream = 1, - Hybrid = 2, -} - -impl ProtocolTransportMode { - fn from_i32(value: i32) -> Result { - match value { - 0 => Ok(Self::Buffered), - 1 => Ok(Self::Stream), - 2 => Ok(Self::Hybrid), - other => anyhow::bail!("invalid WASIX protocol transport mode {other}"), - } - } -} - -struct PgliteLifecycleExports { - wasi_start: TypedFunction<(), ()>, - set_force_host_error_recovery: Option>, - set_active: TypedFunction, - start_pglite: TypedFunction<(), ()>, - #[cfg_attr(not(feature = "extensions"), allow(dead_code))] - run_atexit_funcs: Option>, - backend_timing_reset: Option>, - backend_timing_elapsed_us: Option>, -} - -struct WasixProtocolExports { - get_port: TypedFunction<(), i32>, - process_startup: TypedFunction<(i32, i32, i32), i32>, - send_conn_data: TypedFunction<(), ()>, - pq_flush: TypedFunction<(), ()>, - pq_buffer_remaining_data: TypedFunction<(), i32>, - main_loop: TypedFunction<(), ()>, - send_ready: TypedFunction<(), ()>, - recover_error: TypedFunction<(), ()>, -} - -#[derive(Clone)] -struct WasixProtocolStdioExports { - set_protocol_transport: TypedFunction, - protocol_stream_active: TypedFunction<(), i32>, -} - -struct WasixPgliteIo { - input_reset: TypedFunction<(), i32>, - input_write: TypedFunction<(i32, i32), i32>, - input_available: TypedFunction<(), i32>, - output_reset: TypedFunction<(), i32>, - output_len: TypedFunction<(), i32>, - output_read: TypedFunction<(i32, i32), i32>, -} - -struct GuestAllocator { - malloc: TypedFunction, - free: TypedFunction, - #[cfg(debug_assertions)] - allocations: Cell, - #[cfg(debug_assertions)] - frees: Cell, -} - -pub(crate) trait ProtocolStream: Read + Write + Send { - fn read_ready(&mut self) -> io::Result; -} - -#[derive(Clone)] -struct ProtocolStdioFile { - state: Arc, -} - -struct ProtocolStdioState { - inner: Mutex, -} - -#[derive(Default)] -struct ProtocolStdioInner { - stream: Option>, - prefix: Vec, - prefix_offset: usize, -} - -struct ProtocolStdioAttachment { - file: ProtocolStdioFile, -} - -impl ProtocolStdioFile { - fn new() -> Self { - Self { - state: Arc::new(ProtocolStdioState { - inner: Mutex::new(ProtocolStdioInner::default()), - }), - } - } - - fn attach(&self, stream: S) -> Result - where - S: ProtocolStream + 'static, - { - let mut guard = self - .state - .inner - .lock() - .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; - ensure!( - guard.stream.is_none(), - "WASIX protocol stdio stream is already attached" - ); - guard.stream = Some(Box::new(stream)); - guard.prefix.clear(); - guard.prefix_offset = 0; - Ok(ProtocolStdioAttachment { file: self.clone() }) - } - - fn detach(&self) { - if let Ok(mut guard) = self.state.inner.lock() { - guard.stream = None; - guard.prefix.clear(); - guard.prefix_offset = 0; - } - } - - fn set_prefix(&self, prefix: Vec) -> Result<()> { - let mut guard = self - .state - .inner - .lock() - .map_err(|_| anyhow::anyhow!("protocol stdio lock poisoned"))?; - guard.prefix = prefix; - guard.prefix_offset = 0; - Ok(()) - } - - fn clear_prefix(&self) -> Result<()> { - self.set_prefix(Vec::new()) - } - - fn with_inner( - &self, - f: impl FnOnce(&mut ProtocolStdioInner) -> io::Result, - ) -> io::Result { - let mut guard = self - .state - .inner - .lock() - .map_err(|_| io::Error::other("protocol stdio lock poisoned"))?; - f(&mut guard) - } -} - -impl Drop for ProtocolStdioAttachment { - fn drop(&mut self) { - self.file.detach(); - } -} - -impl fmt::Debug for ProtocolStdioFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ProtocolStdioFile").finish_non_exhaustive() - } -} - -impl virtual_fs::VirtualFile for ProtocolStdioFile { - fn last_accessed(&self) -> u64 { - 0 - } - - fn last_modified(&self) -> u64 { - 0 - } - - fn created_time(&self) -> u64 { - 0 - } - - fn size(&self) -> u64 { - 0 - } - - fn set_len(&mut self, _new_size: u64) -> virtual_fs::Result<()> { - Err(virtual_fs::FsError::PermissionDenied) - } - - fn unlink(&mut self) -> virtual_fs::Result<()> { - Ok(()) - } - - fn is_open(&self) -> bool { - self.state - .inner - .lock() - .map(|inner| inner.stream.is_some()) - .unwrap_or(false) - } - - fn poll_read_ready(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - match self.with_inner(|inner| { - if inner.prefix_offset < inner.prefix.len() { - return Ok(true); - } - let stream = inner.stream.as_mut().ok_or_else(|| { - io::Error::new( - io::ErrorKind::BrokenPipe, - "WASIX protocol stdio stream is not attached", - ) - })?; - stream.read_ready() - }) { - Ok(true) => Poll::Ready(Ok(1)), - Ok(false) => Poll::Pending, - Err(err) => Poll::Ready(Err(err)), - } - } - - fn poll_write_ready( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - ) -> Poll> { - match self.with_inner(|inner| { - if inner.stream.is_some() { - Ok(8192) - } else { - Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "WASIX protocol stdio stream is not attached", - )) - } - }) { - Ok(ready) => Poll::Ready(Ok(ready)), - Err(err) => Poll::Ready(Err(err)), - } - } -} - -impl virtual_fs::AsyncRead for ProtocolStdioFile { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - if buf.remaining() == 0 { - return Poll::Ready(Ok(())); - } - let read = self.with_inner(|inner| { - let unfilled = buf.initialize_unfilled(); - if inner.prefix_offset < inner.prefix.len() { - let remaining = &inner.prefix[inner.prefix_offset..]; - let read = remaining.len().min(unfilled.len()); - unfilled[..read].copy_from_slice(&remaining[..read]); - inner.prefix_offset += read; - if inner.prefix_offset == inner.prefix.len() { - inner.prefix.clear(); - inner.prefix_offset = 0; - } - return Ok(read); - } - let stream = inner.stream.as_mut().ok_or_else(|| { - io::Error::new( - io::ErrorKind::BrokenPipe, - "WASIX protocol stdio stream is not attached", - ) - })?; - stream.read(unfilled) - }); - match read { - Ok(read) => { - buf.advance(read); - Poll::Ready(Ok(())) - } - Err(err) => Poll::Ready(Err(err)), - } - } -} - -impl virtual_fs::AsyncWrite for ProtocolStdioFile { - fn poll_write( - self: Pin<&mut Self>, - _cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - let written = self - .state - .inner - .lock() - .map_err(|_| io::Error::other("protocol stdio lock poisoned")) - .and_then(|mut inner| match inner.stream.as_mut() { - Some(stream) => stream.write(buf), - None => Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "WASIX protocol stdio stream is not attached", - )), - }); - Poll::Ready(written) - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - let flushed = self - .state - .inner - .lock() - .map_err(|_| io::Error::other("protocol stdio lock poisoned")) - .and_then(|mut inner| match inner.stream.as_mut() { - Some(stream) => stream.flush(), - None => Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "WASIX protocol stdio stream is not attached", - )), - }); - Poll::Ready(flushed) - } - - fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(())) - } -} - -impl virtual_fs::AsyncSeek for ProtocolStdioFile { - fn start_seek(self: Pin<&mut Self>, _position: io::SeekFrom) -> io::Result<()> { - Ok(()) - } - - fn poll_complete(self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll> { - Poll::Ready(Ok(0)) - } -} - -impl PostgresMod { - pub(crate) fn preload_module(module_path: &std::path::Path) -> Result<()> { - let runtime_root = module_path - .parent() - .and_then(Path::parent) - .context("runtime module path must be under bin/pglite")?; - let (engine, _) = aot::load_runtime_module()?; - let process_runtime = process_wasix_runtime(&engine)?; - preload_runtime_side_modules( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - runtime_root, - ) - } - - pub(crate) fn new_prepared(paths: PglitePaths, runtime_layout: RuntimeLayout) -> Result { - Self::new_prepared_with_config( - paths, - runtime_layout, - PostgresConfig::default(), - StartupConfig::default(), - ) - } - - pub(crate) fn new_prepared_with_config( - paths: PglitePaths, - runtime_layout: RuntimeLayout, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - ) -> Result { - postgres_config.validate()?; - startup_config.validate()?; - ensure_runtime_dirs(&paths)?; - #[cfg(feature = "extensions")] - let runtime_root = runtime_layout.local_root.clone(); - let module_runtime_root = runtime_layout.module_root.clone(); - ensure!( - module_runtime_root.join("bin/pglite").exists(), - "WASIX PGlite executable not found at {}", - module_runtime_root.join("bin/pglite").display() - ); - - let (engine, module) = aot::load_runtime_module()?; - let process_runtime = process_wasix_runtime(&engine)?; - { - let _phase = timing::phase("wasix.preload_runtime_side_modules"); - preload_runtime_side_modules( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &module_runtime_root, - )?; - } - #[cfg(feature = "extensions")] - { - let _phase = timing::phase("wasix.preload_installed_extension_side_modules"); - preload_installed_extension_side_modules( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &runtime_root, - )?; - } - let mut store = Store::new(engine.clone()); - - let _phase = timing::phase("wasix.instance_create"); - let (instance, env, protocol_stdio_file, wasi_stderr) = - instantiate_wasix_module(WasixInstantiateInput { - runtime: &process_runtime.tokio_runtime, - wasix_runtime: &process_runtime.wasix_runtime, - store: &mut store, - paths: &paths, - runtime_layout: &runtime_layout, - postgres_config: &postgres_config, - startup_config: &startup_config, - module: module.clone(), - })?; - seed_exported_c_string_value(&mut store, &instance, &env, "my_exec_path", PGLITE_EXE_PATH)?; - - let (guest_allocator, io, lifecycle, protocol, protocol_stdio) = { - let _phase = timing::phase("wasix.export_load"); - let guest_allocator = GuestAllocator::load(&mut store, &instance)?; - let io = WasixPgliteIo::new(&mut store, &instance)?; - ensure_integrated_pglite_contract(&instance)?; - let lifecycle = PgliteLifecycleExports::load(&mut store, &instance)?; - let protocol = WasixProtocolExports::load(&mut store, &instance)?; - let protocol_stdio = WasixProtocolStdioExports::load(&mut store, &instance)?; - (guest_allocator, io, lifecycle, protocol, protocol_stdio) - }; - - let pg = Self { - engine, - tokio_runtime: process_runtime.tokio_runtime.clone(), - wasix_module_cache: process_runtime.wasix_module_cache.clone(), - _wasix_runtime: process_runtime.wasix_runtime.clone(), - store, - _instance: instance, - env, - guest_allocator, - io, - lifecycle, - protocol, - protocol_stdio, - protocol_stdio_file, - wasi_stderr, - protocol_stdio_attachment: None, - paths, - pgdata_template_root: runtime_layout.pgdata_template_root.clone(), - startup_config, - startup_response: None, - cluster_ready: false, - backend_started: false, - started: false, - }; - Ok(pg) - } - - pub fn paths(&self) -> &PglitePaths { - &self.paths - } - - pub(crate) fn pgdata_template_root(&self) -> Option<&Path> { - self.pgdata_template_root.as_deref() - } - - #[cfg(debug_assertions)] - pub(crate) fn guest_bridge_allocation_counts(&self) -> (u64, u64) { - self.guest_allocator.allocation_counts() - } - - pub fn ensure_cluster(&mut self) -> Result<()> { - self.initialize_cluster()?; - self.start_backend() - } - - pub fn initialize_cluster(&mut self) -> Result<()> { - if self.cluster_ready { - return Ok(()); - } - - ensure!( - self.paths.is_cluster_initialized(), - "PGDATA is not initialized; install the WASIX runtime assets and PGDATA template before opening" - ); - self.cluster_ready = true; - Ok(()) - } - - fn start_backend(&mut self) -> Result<()> { - if self.backend_started { - return Ok(()); - } - let _phase = timing::phase("postgres.backend_start"); - self.reset_backend_c_timings()?; - self.configure_host_error_recovery()?; - { - let _phase = timing::phase("postgres.backend_start.set_active"); - self.lifecycle - .set_active - .call(&mut self.store, 1) - .context("pgl_setPGliteActive(1)")?; - } - { - let _phase = timing::phase("postgres.backend_start.single_user_main"); - match self.lifecycle.wasi_start.call(&mut self.store) { - Ok(()) => {} - Err(err) if runtime_error_exit_code(&err) == Some(PGLITE_EXIT_ALIVE) => {} - Err(err) => return self.startup_failure(err, "_start PGlite single-user backend"), - } - } - if let Err(err) = self.lifecycle.start_pglite.call(&mut self.store) { - return self.startup_failure(err, "pgl_startPGlite"); - } - self.record_backend_c_timings()?; - self.backend_started = true; - Ok(()) - } - - fn configure_host_error_recovery(&mut self) -> Result<()> { - let force = host_requires_process_exit_error_recovery(); - let Some(set_force) = &self.lifecycle.set_force_host_error_recovery else { - if force { - anyhow::bail!( - "WASIX runtime does not export pgl_set_force_host_error_recovery required by this host" - ); - } - return Ok(()); - }; - - set_force - .call(&mut self.store, i32::from(force)) - .context("pgl_set_force_host_error_recovery")?; - Ok(()) - } - - fn startup_failure(&mut self, err: wasmer::RuntimeError, context: &str) -> Result<()> { - if let Some(output) = self.take_startup_output_after_failure() { - if protocol_response_contains_error(&output) { - return Err(StartupErrorResponse::new(output).into()); - } - return Err(err).context(format!( - "{context}{}", - self.startup_failure_detail(Some(&output)) - )); - } - Err(err).context(format!("{context}{}", self.startup_failure_detail(None))) - } - - fn take_startup_output_after_failure(&mut self) -> Option> { - let _ = self.protocol.pq_flush.call(&mut self.store); - match self - .io - .take_output(&mut self.store, &self.env, &self.guest_allocator) - { - Ok(output) if !output.is_empty() => Some(output), - Ok(_) => None, - Err(err) => { - warn!("failed to read startup output after backend failure: {err}"); - None - } - } - } - - fn startup_failure_detail(&self, output: Option<&[u8]>) -> String { - let mut detail = String::new(); - let stderr = self.wasi_stderr.text(); - if !stderr.trim().is_empty() { - detail.push_str("\nWASIX stderr tail:\n"); - detail.push_str(stderr.trim_end()); - } - if let Some(output) = output { - detail.push_str("\nWASIX startup output tail:\n"); - detail.push_str(&format_output_tail(output)); - } - detail - } - - #[cfg_attr(not(feature = "extensions"), allow(dead_code))] - pub(crate) fn shutdown_backend(&mut self) -> Result<()> { - let _phase = timing::phase("postgres.backend_shutdown"); - self.lifecycle - .set_active - .call(&mut self.store, 0) - .context("pgl_setPGliteActive(0)")?; - if let Some(run_atexit_funcs) = &self.lifecycle.run_atexit_funcs { - run_atexit_funcs - .call(&mut self.store) - .context("pgl_run_atexit_funcs")?; - } - self.backend_started = false; - self.started = false; - self.startup_response = None; - self.cluster_ready = false; - Ok(()) - } - - fn record_backend_c_timings(&mut self) -> Result<()> { - let Some(elapsed) = &self.lifecycle.backend_timing_elapsed_us else { - return Ok(()); - }; - - for &(id, name) in BACKEND_C_TIMINGS { - let elapsed_micros = elapsed - .call(&mut self.store, id) - .with_context(|| format!("pgl_backend_timing_elapsed_us({id})"))?; - if elapsed_micros > 0 { - timing::record_phase_timing(name, Duration::from_micros(elapsed_micros as u64)); - } - } - Ok(()) - } - - fn reset_backend_c_timings(&mut self) -> Result<()> { - let Some(reset) = &self.lifecycle.backend_timing_reset else { - return Ok(()); - }; - - reset - .call(&mut self.store) - .context("pgl_backend_timing_reset")?; - Ok(()) - } - - #[cfg(feature = "extensions")] - pub fn preload_extension_module(&self, extension: Extension) -> Result<()> { - let Some(module_file) = extension.native_module_file() else { - return Ok(()); - }; - let Some(aot_name) = extension.aot_name() else { - return Ok(()); - }; - let runtime_root = self.paths.runtime_root(); - let library = runtime_root - .join("lib") - .join("postgresql") - .join(module_file); - ensure!( - library.exists(), - "extension library for '{}' is not installed at {}", - extension.sql_name(), - library.display() - ); - - seed_side_module_cache( - &self.tokio_runtime, - &self.engine, - &self.wasix_module_cache, - &library, - aot_name, - &format!("extension '{}'", extension.sql_name()), - )?; - Ok(()) - } - - #[cfg(feature = "extensions")] - pub(crate) fn preload_extension_module_from_paths( - paths: &PglitePaths, - extension: Extension, - ) -> Result<()> { - let Some(module_file) = extension.native_module_file() else { - return Ok(()); - }; - let Some(aot_name) = extension.aot_name() else { - return Ok(()); - }; - let runtime_root = paths.runtime_root(); - let library = runtime_root - .join("lib") - .join("postgresql") - .join(module_file); - ensure!( - library.exists(), - "extension library for '{}' is not installed at {}", - extension.sql_name(), - library.display() - ); - - let (engine, _) = aot::load_runtime_module()?; - let process_runtime = process_wasix_runtime(&engine)?; - seed_side_module_cache( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &library, - aot_name, - &format!("extension '{}'", extension.sql_name()), - ) - } - - pub(crate) fn run_split_initdb( - paths: &PglitePaths, - runtime_layout: &RuntimeLayout, - ) -> Result<()> { - run_split_initdb(paths, runtime_layout) - } - - pub fn send_protocol(&mut self, payload: &[u8]) -> Result> { - { - let _phase = timing::phase("postgres.protocol.ensure_started"); - self.start_protocol()?; - } - if payload.is_empty() { - return Ok(Vec::new()); - } - self.send_protocol_inner(payload) - } - - pub(crate) fn attach_protocol_stream(&mut self, stream: S) -> Result<()> - where - S: ProtocolStream + 'static, - { - ensure!( - self.protocol_stdio.is_some(), - "WASIX runtime does not export protocol stream transport" - ); - if self.protocol_stdio_attachment.is_none() { - let attachment = self.protocol_stdio_file.attach(stream)?; - self.protocol_stdio_attachment = Some(attachment); - } - Ok(()) - } - - pub(crate) fn set_protocol_stream_prefix(&mut self, prefix: Vec) -> Result<()> { - self.protocol_stdio_file.set_prefix(prefix) - } - - pub(crate) fn clear_protocol_stream_prefix(&mut self) -> Result<()> { - self.protocol_stdio_file.clear_prefix() - } - - pub(crate) fn send_protocol_pump( - &mut self, - payload: &[u8], - continuation_prefix: impl FnOnce() -> Vec, - ) -> Result { - { - let _phase = timing::phase("postgres.protocol.ensure_started"); - self.start_protocol()?; - } - if payload.is_empty() { - return Ok(ProtocolPumpOutcome::Buffered(Vec::new())); - } - ensure!( - self.protocol_stdio_attachment.is_some(), - "WASIX protocol pump requires an attached stream" - ); - let previous_mode = self.set_protocol_transport(ProtocolTransportMode::Hybrid)?; - ensure!( - previous_mode == ProtocolTransportMode::Buffered, - "WASIX protocol transport was not buffered before protocol pump" - ); - let result = self.send_protocol_inner(payload); - let active = self.protocol_stream_active().unwrap_or(false); - if active { - self.set_protocol_stream_prefix(continuation_prefix())?; - let stream_result = result.and_then(|_| self.serve_protocol_stream_inner()); - let restore_result = self.restore_protocol_transport(previous_mode); - let clear_result = self.clear_protocol_stream_prefix(); - stream_result.and(restore_result).and(clear_result)?; - Ok(ProtocolPumpOutcome::Streamed) - } else { - let output = result; - let restore_result = self.restore_protocol_transport(previous_mode); - restore_result?; - let output = output?; - Ok(ProtocolPumpOutcome::Buffered(output)) - } - } - - fn send_protocol_inner(&mut self, payload: &[u8]) -> Result> { - self.reset_backend_c_timings()?; - - { - let _phase = timing::phase("postgres.protocol.input_reset"); - self.io.reset(&mut self.store)?; - } - { - let _phase = timing::phase("postgres.protocol.input_write"); - self.io - .push_input(&mut self.store, &self.env, &self.guest_allocator, payload)?; - } - - { - let _phase = timing::phase("postgres.protocol.dispatch_buffer"); - let max_attempts = (payload.len() / 5).saturating_add(2).max(1); - let mut attempts = 0usize; - let mut recovered_protocol_error = false; - while self.protocol_input_remaining()? > 0 { - attempts += 1; - ensure!( - attempts <= max_attempts, - "Postgres protocol dispatch did not drain buffered input after {attempts} attempts" - ); - if let Err(err) = self.protocol.main_loop.call(&mut self.store) { - if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) { - debug!( - "PostgresMainLoopOnce used host longjmp fallback; recovering protocol error" - ); - self.recover_protocol_error(payload.len())?; - recovered_protocol_error = true; - } else if is_wasm_uncaught_exception(&err) { - debug!( - "PostgresMainLoopOnce trapped for PostgreSQL error; recovering protocol state: {err}" - ); - self.recover_protocol_error(payload.len())?; - recovered_protocol_error = true; - } else { - warn!("PostgresMainLoopOnce trapped; attempting protocol recovery: {err}"); - self.recover_protocol_error(payload.len())?; - recovered_protocol_error = true; - } - } - } - - { - let _phase = timing::phase("postgres.protocol.send_ready"); - self.protocol - .send_ready - .call(&mut self.store) - .context("PostgresSendReadyForQueryIfNecessary")?; - } - { - let _phase = timing::phase("postgres.protocol.pq_flush"); - self.protocol - .pq_flush - .call(&mut self.store) - .context("pgl_pq_flush after protocol buffer")?; - } - let output = { - let _phase = timing::phase("postgres.protocol.output_read"); - self.io - .take_output(&mut self.store, &self.env, &self.guest_allocator) - .context("take backend output after protocol buffer")? - }; - if !recovered_protocol_error && protocol_response_contains_error(&output) { - self.recover_non_trapping_protocol_error()?; - } - self.record_backend_c_timings()?; - Ok(output) - } - } - - pub(crate) fn supports_streaming_protocol(&self) -> bool { - self.protocol_stdio.is_some() - } - - fn serve_protocol_stream_inner(&mut self) -> Result<()> { - self.reset_backend_c_timings()?; - loop { - if let Err(err) = self.protocol.main_loop.call(&mut self.store) { - if runtime_error_exit_code(&err) == Some(PGLITE_EXIT_ALIVE) { - break; - } - if runtime_error_exit_code(&err) == Some(POSTGRES_MAIN_LONGJMP) { - debug!( - "PostgresMainLoopOnce used host longjmp fallback while serving streaming protocol" - ); - self.protocol.recover_error.call(&mut self.store).context( - "recover Postgres main-loop error while serving streaming protocol", - )?; - } else if is_wasm_uncaught_exception(&err) { - debug!( - "PostgresMainLoopOnce trapped for PostgreSQL error while serving streaming protocol: {err}" - ); - self.protocol.recover_error.call(&mut self.store).context( - "recover Postgres main-loop error while serving streaming protocol", - )?; - } else { - return Err(err).context("PostgresMainLoopOnce streaming protocol"); - } - } - self.protocol - .send_ready - .call(&mut self.store) - .context("PostgresSendReadyForQueryIfNecessary streaming protocol")?; - self.protocol - .pq_flush - .call(&mut self.store) - .context("pgl_pq_flush streaming protocol")?; - } - self.record_backend_c_timings()?; - Ok(()) - } - - fn set_protocol_transport( - &mut self, - mode: ProtocolTransportMode, - ) -> Result { - let stdio = self - .protocol_stdio - .as_ref() - .context("WASIX runtime does not export protocol stdio switching")?; - let previous = stdio - .set_protocol_transport - .call(&mut self.store, mode as i32) - .context("pgl_set_protocol_transport")?; - ProtocolTransportMode::from_i32(previous) - } - - fn restore_protocol_transport(&mut self, previous_mode: ProtocolTransportMode) -> Result<()> { - let current = self.set_protocol_transport(previous_mode)?; - ensure!( - current != previous_mode, - "pgl_set_protocol_transport restore observed unchanged current mode" - ); - Ok(()) - } - - fn protocol_stream_active(&mut self) -> Result { - let stdio = self - .protocol_stdio - .as_ref() - .context("WASIX runtime does not export protocol stream state")?; - Ok(stdio - .protocol_stream_active - .call(&mut self.store) - .context("pgl_protocol_stream_active")? - != 0) - } - - fn start_protocol(&mut self) -> Result<()> { - if self.started { - return Ok(()); - } - let startup = startup_packet(&self.startup_config.username, &self.startup_config.database); - let response = self.start_protocol_with_startup_packet(&startup)?; - ensure!( - response.accepted, - "PGlite WASIX startup packet was rejected: {}", - summarize_protocol(&response.output) - ); - ensure!( - !protocol_response_contains_error(&response.output), - "PGlite WASIX startup packet returned an error: {}", - summarize_protocol(&response.output) - ); - Ok(()) - } - - pub(crate) fn start_protocol_with_startup_packet( - &mut self, - startup: &[u8], - ) -> Result { - self.ensure_cluster()?; - ensure!( - !self.started, - "PGlite WASIX protocol startup has already completed for this backend" - ); - - let _phase = timing::phase("postgres.startup_packet"); - { - let _phase = timing::phase("postgres.startup_packet.input_reset"); - self.io.reset(&mut self.store)?; - } - { - let _phase = timing::phase("postgres.startup_packet.input_write"); - self.io - .push_input(&mut self.store, &self.env, &self.guest_allocator, startup)?; - } - - // The upstream lifecycle is already running by this point. These calls - // open the Rust-owned direct wire-protocol transport on top of that - // lifecycle; they must not grow into a second backend lifecycle. - let port = { - let _phase = timing::phase("postgres.startup_packet.get_port"); - self.protocol - .get_port - .call(&mut self.store) - .context("pgl_getMyProcPort")? - }; - ensure!(port > 0, "pgl_getMyProcPort returned null"); - - let status = { - let _phase = timing::phase("postgres.startup_packet.process_startup"); - self.protocol - .process_startup - .call(&mut self.store, port, 1, 1) - .context("ProcessStartupPacket")? - }; - if status != 0 { - let _ = self.protocol.pq_flush.call(&mut self.store); - let output = self - .io - .take_output(&mut self.store, &self.env, &self.guest_allocator)?; - return Ok(StartupProtocolResponse { - output, - accepted: false, - }); - } - let output = { - let _phase = timing::phase("postgres.startup_packet.ready"); - { - let _phase = timing::phase("postgres.startup_packet.send_conn_data"); - self.protocol - .send_conn_data - .call(&mut self.store) - .context("pgl_sendConnData")?; - } - { - let _phase = timing::phase("postgres.startup_packet.pq_flush"); - self.protocol - .pq_flush - .call(&mut self.store) - .context("pgl_pq_flush after startup")?; - } - { - let _phase = timing::phase("postgres.startup_packet.output_read"); - self.io - .take_output(&mut self.store, &self.env, &self.guest_allocator)? - } - }; - self.started = true; - self.startup_response = Some(output.clone()); - Ok(StartupProtocolResponse { - output, - accepted: true, - }) - } - - #[cfg(feature = "extensions")] - pub(crate) fn existing_startup_response(&self) -> Option> { - self.startup_response.clone() - } - - fn recover_protocol_error(&mut self, payload_len: usize) -> Result<()> { - self.protocol - .recover_error - .call(&mut self.store) - .context("PostgresMainLongJmp after protocol trap")?; - - // PostgreSQL extended-query errors skip messages until Sync. If Sync was - // already in this host buffer, re-enter the loop to drain it and produce - // ReadyForQuery from PostgreSQL rather than inventing one in Rust. - let max_drain_attempts = (payload_len / 5).saturating_add(2).max(1); - let mut drain_attempts = 0usize; - while self.protocol_input_remaining()? > 0 { - drain_attempts += 1; - ensure!( - drain_attempts <= max_drain_attempts, - "Postgres protocol recovery did not drain buffered input after {drain_attempts} attempts" - ); - if let Err(drain_err) = self.protocol.main_loop.call(&mut self.store) { - if runtime_error_exit_code(&drain_err) == Some(POSTGRES_MAIN_LONGJMP) - || is_wasm_uncaught_exception(&drain_err) - { - debug!( - "PostgresMainLoopOnce trapped while draining after PostgreSQL error recovery: {drain_err}" - ); - } else { - warn!( - "PostgresMainLoopOnce trapped while draining after recovery: {drain_err}" - ); - } - self.protocol - .recover_error - .call(&mut self.store) - .context("PostgresMainLongJmp while draining after protocol trap")?; - } - } - Ok(()) - } - - fn recover_non_trapping_protocol_error(&mut self) -> Result<()> { - self.protocol - .recover_error - .call(&mut self.store) - .context("PostgresMainLongJmp after backend ErrorResponse")?; - self.protocol - .send_ready - .call(&mut self.store) - .context("PostgresSendReadyForQueryIfNecessary after backend ErrorResponse")?; - self.protocol - .pq_flush - .call(&mut self.store) - .context("pgl_pq_flush after backend ErrorResponse recovery")?; - let _ = self - .io - .take_output(&mut self.store, &self.env, &self.guest_allocator)?; - Ok(()) - } - - fn protocol_input_remaining(&mut self) -> Result { - let host_remaining = self.io.available(&mut self.store)?; - if host_remaining > 0 { - return Ok(host_remaining); - } - self.protocol - .pq_buffer_remaining_data - .call(&mut self.store) - .context("pq_buffer_remaining_data") - } -} - -fn process_wasix_runtime(engine: &Engine) -> Result> { - WASIX_PROCESS_RUNTIME - .get_or_init(|| { - let _phase = timing::phase("wasix.runtime_construct"); - let tokio_runtime = { - let _phase = timing::phase("wasix.runtime_construct.tokio"); - Arc::new( - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .context("create Tokio runtime for Wasmer/WASIX filesystem") - .map_err(|err| format!("{err:#}"))?, - ) - }; - let wasix_module_cache = { - let _phase = timing::phase("wasix.runtime_construct.module_cache"); - Arc::new(SharedCache::new()) - }; - let wasix_runtime = { - let _phase = timing::phase("wasix.runtime_construct.pluggable_runtime"); - build_wasix_runtime(&tokio_runtime, engine, wasix_module_cache.clone()) - }; - - Ok(Arc::new(WasixProcessRuntime { - tokio_runtime, - wasix_module_cache, - wasix_runtime, - })) - }) - .clone() - .map_err(|message| anyhow::anyhow!(message)) -} - -struct WasixInstantiateInput<'a> { - runtime: &'a TokioRuntime, - wasix_runtime: &'a Arc, - store: &'a mut Store, - paths: &'a PglitePaths, - runtime_layout: &'a RuntimeLayout, - postgres_config: &'a PostgresConfig, - startup_config: &'a StartupConfig, - module: Module, -} - -fn instantiate_wasix_module( - input: WasixInstantiateInput<'_>, -) -> Result<( - Instance, - WasiFunctionEnv, - ProtocolStdioFile, - TailCaptureHandle, -)> { - let _phase = timing::phase("wasix.instantiate"); - let _guard = input.runtime.enter(); - let root_fs = { - let _phase = timing::phase("wasix.instantiate.root_fs"); - if input.runtime_layout.uses_shared_overlay() { - mountfs_overlay_wasi_root(input.paths, input.runtime_layout)? - } else { - host_wasi_root(&input.paths.runtime_root())? - } - }; - - let mut runner = WasiRunner::new(); - runner.with_current_dir("/"); - let protocol_stdio_file = ProtocolStdioFile::new(); - let (stderr_file, stderr_capture) = TailCaptureFile::new(16 * 1024); - runner.with_stdin(Box::new(protocol_stdio_file.clone())); - runner.with_stdout(Box::new(protocol_stdio_file.clone())); - runner.with_stderr(Box::new(stderr_file)); - let wasi = Wasi::new(PGLITE_EXE_PATH); - let mut builder = { - let _phase = timing::phase("wasix.instantiate.prepare_env"); - runner - .prepare_webc_env( - PGLITE_EXE_PATH, - &wasi, - PackageOrHash::Hash(ModuleHash::random()), - RuntimeOrEngine::Runtime(input.wasix_runtime.clone()), - Some(root_fs), - ) - .context("prepare Wasmer/WASIX runner environment")? - }; - { - let _phase = timing::phase("wasix.instantiate.pgdata_preopen"); - add_pgdata_preopen(&mut builder)?; - } - add_pglite_env(&mut builder, input.startup_config); - add_pglite_args(&mut builder, input.postgres_config, input.startup_config)?; - - { - let _phase = timing::phase("wasix.instantiate.module"); - builder - .instantiate(input.module, input.store) - .context("instantiate PGlite WASIX module") - .map(|(instance, env)| (instance, env, protocol_stdio_file, stderr_capture)) - } -} - -fn add_pgdata_preopen(builder: &mut wasmer_wasix::WasiEnvBuilder) -> Result<()> { - builder - .add_preopen_build(|preopen| { - preopen - .directory(PGDATA_DIR) - .alias(PGDATA_DIR.trim_start_matches('/')) - .read(true) - .write(true) - .create(true) - }) - .context("preopen PGDATA directory for Wasmer/WASIX")?; - Ok(()) -} - -fn host_wasi_root(runtime_root: &Path) -> Result { - let root = maybe_trace_filesystem(host_filesystem(runtime_root)?); - Ok(WasiFsRoot::from_filesystem(wasi_root_with_devices(root)?)) -} - -fn mountfs_overlay_wasi_root( - paths: &PglitePaths, - runtime_layout: &RuntimeLayout, -) -> Result { - let _phase = timing::phase("wasix.mountfs_overlay_construct"); - let runtime_root = paths.runtime_root(); - let primary = - virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem(&runtime_root)?)); - let secondary = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( - &runtime_layout.module_root, - )?)); - let overlay = Arc::new(virtual_fs::OverlayFileSystem::new(primary, [secondary])); - let root: Arc = - if let Some(pgdata) = pgdata_overlay_filesystem(paths, runtime_layout)? { - wasi_root_with_pgdata_mount(overlay, pgdata)? - } else { - overlay - }; - - Ok(WasiFsRoot::from_filesystem(wasi_root_with_devices(root)?)) -} - -fn pgdata_overlay_filesystem( - paths: &PglitePaths, - runtime_layout: &RuntimeLayout, -) -> Result>> { - if let Some(pgdata_template_root) = &runtime_layout.pgdata_template_root { - let fs = - EagerCopyOverlayFileSystem::new(paths.pgdata.clone(), pgdata_template_root.clone())?; - return Ok(Some(maybe_trace_filesystem(Arc::new(fs)))); - } - Ok(None) -} - -fn wasi_root_with_pgdata_mount( - root: Arc, - pgdata: Arc, -) -> virtual_fs::Result> { - let mount = virtual_fs::MountFileSystem::new(); - mount.mount(Path::new("/"), root)?; - mount.mount(Path::new(PGDATA_DIR), pgdata)?; - Ok(Arc::new(mount)) -} - -fn wasi_root_with_devices( - root: Arc, -) -> virtual_fs::Result> { - let devices: Arc = - Arc::new(virtual_fs::RootFileSystemBuilder::default().build_tmp_ext(&[])); - let root_with_default_dirs: Arc = - Arc::new(virtual_fs::OverlayFileSystem::new( - virtual_fs::ArcFileSystem::new(root), - [virtual_fs::ArcFileSystem::new(devices.clone())], - )); - let mount = virtual_fs::MountFileSystem::new(); - mount.mount(Path::new("/"), root_with_default_dirs)?; - for name in WASIX_DEVICE_FILES { - let path = Path::new("/dev").join(name); - mount.mount_with_source(&path, &path, devices.clone())?; - } - Ok(Arc::new(mount)) -} - -struct EagerCopyOverlayFileSystem { - upper_root: PathBuf, - lower_root: PathBuf, - overlay: - virtual_fs::OverlayFileSystem, -} - -impl fmt::Debug for EagerCopyOverlayFileSystem { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("EagerCopyOverlayFileSystem") - .field("upper_root", &self.upper_root) - .field("lower_root", &self.lower_root) - .finish_non_exhaustive() - } -} - -impl EagerCopyOverlayFileSystem { - fn new(upper_root: PathBuf, lower_root: PathBuf) -> Result { - fs::create_dir_all(&upper_root) - .with_context(|| format!("create PGDATA overlay upper {}", upper_root.display()))?; - let upper_root = upper_root.canonicalize().with_context(|| { - format!("canonicalize PGDATA overlay upper {}", upper_root.display()) - })?; - let lower_root = lower_root.canonicalize().with_context(|| { - format!("canonicalize PGDATA overlay lower {}", lower_root.display()) - })?; - let upper = virtual_fs::ArcFileSystem::new(host_filesystem(&upper_root)?); - let lower = virtual_fs::ArcFileSystem::new(host_filesystem(&lower_root)?); - Ok(Self { - upper_root, - lower_root, - overlay: virtual_fs::OverlayFileSystem::new(upper, [lower]), - }) - } - - fn ensure_upper_copy( - &self, - path: &Path, - conf: &virtual_fs::OpenOptionsConfig, - ) -> virtual_fs::Result<()> { - let Some(relative) = normalize_overlay_path(path)? else { - return Ok(()); - }; - - let upper = self.upper_root.join(&relative); - if upper.exists() { - return Ok(()); - } - - let lower = self.lower_root.join(&relative); - let metadata = match fs::symlink_metadata(&lower) { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - if conf.create || conf.create_new { - self.ensure_upper_parent(&relative)?; - } - return Ok(()); - } - Err(err) => return Err(err.into()), - }; - - if conf.create_new { - return Err(virtual_fs::FsError::AlreadyExists); - } - if metadata.is_dir() { - return Ok(()); - } - if !metadata.is_file() { - return Err(virtual_fs::FsError::Unsupported); - } - - if let Some(parent) = upper.parent() { - fs::create_dir_all(parent).map_err(virtual_fs::FsError::from)?; - } - if conf.truncate && !conf.read && !conf.append { - fs::File::create(&upper).map_err(virtual_fs::FsError::from)?; - } else { - fs::copy(&lower, &upper).map_err(virtual_fs::FsError::from)?; - } - Ok(()) - } - - fn ensure_upper_parent(&self, relative: &Path) -> virtual_fs::Result<()> { - let Some(parent) = relative.parent() else { - return Ok(()); - }; - if parent.as_os_str().is_empty() { - return Ok(()); - } - - let upper_parent = self.upper_root.join(parent); - if upper_parent.is_dir() { - return Ok(()); - } - - let lower_parent = self.lower_root.join(parent); - let metadata = match fs::symlink_metadata(&lower_parent) { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(virtual_fs::FsError::EntryNotFound); - } - Err(err) => return Err(err.into()), - }; - if !metadata.is_dir() { - return Err(virtual_fs::FsError::BaseNotDirectory); - } - - fs::create_dir_all(upper_parent).map_err(virtual_fs::FsError::from) - } -} - -impl virtual_fs::FileSystem for EagerCopyOverlayFileSystem { - fn readlink(&self, path: &Path) -> virtual_fs::Result { - self.overlay.readlink(path) - } - - fn read_dir(&self, path: &Path) -> virtual_fs::Result { - self.overlay.read_dir(path) - } - - fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { - if let Some(relative) = normalize_overlay_path(path)? { - self.ensure_upper_parent(&relative)?; - } - self.overlay.create_dir(path) - } - - fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { - if let Some(relative) = normalize_overlay_path(target)? { - self.ensure_upper_parent(&relative)?; - } - self.overlay.create_symlink(source, target) - } - - fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { - self.overlay.remove_dir(path) - } - - fn rename<'a>( - &'a self, - from: &'a Path, - to: &'a Path, - ) -> Pin> + Send + 'a>> { - Box::pin(async move { - self.ensure_upper_copy(from, &mutating_open_config())?; - if let Some(relative) = normalize_overlay_path(to)? { - self.ensure_upper_parent(&relative)?; - } - self.overlay.rename(from, to).await - }) - } - - fn metadata(&self, path: &Path) -> virtual_fs::Result { - self.overlay.metadata(path) - } - - fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { - self.overlay.symlink_metadata(path) - } - - fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { - self.overlay.remove_file(path) - } - - fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { - virtual_fs::OpenOptions::new(self) - } -} - -impl virtual_fs::FileOpener for EagerCopyOverlayFileSystem { - fn open( - &self, - path: &Path, - conf: &virtual_fs::OpenOptionsConfig, - ) -> virtual_fs::Result> { - if conf.would_mutate() { - self.ensure_upper_copy(path, conf)?; - } - virtual_fs::FileSystem::new_open_options(&self.overlay) - .options(conf.clone()) - .open(path) - } -} - -fn normalize_overlay_path(path: &Path) -> virtual_fs::Result> { - let mut relative = PathBuf::new(); - for component in path.components() { - match component { - Component::RootDir | Component::CurDir => {} - Component::Normal(part) => relative.push(part), - Component::ParentDir | Component::Prefix(_) => { - return Err(virtual_fs::FsError::PermissionDenied); - } - } - } - if relative.as_os_str().is_empty() { - Ok(None) - } else { - Ok(Some(relative)) - } -} - -fn mutating_open_config() -> virtual_fs::OpenOptionsConfig { - virtual_fs::OpenOptionsConfig { - read: true, - write: true, - create_new: false, - create: false, - append: false, - truncate: false, - } -} - -fn host_filesystem(host_path: &Path) -> Result> { - let host_fs = SyncHostFileSystem::new(host_path) - .with_context(|| format!("create host fs rooted at {}", host_path.display()))?; - Ok(Arc::new(host_fs) as Arc) -} - -fn fs_trace_enabled() -> bool { - env_flag_enabled("PGLITE_OXIDE_WASIX_FS_TRACE") -} - -fn env_flag_enabled(name: &str) -> bool { - let Some(value) = std::env::var_os(name) else { - return false; - }; - !matches!( - value.to_string_lossy().to_ascii_lowercase().as_str(), - "" | "0" | "false" | "off" | "no" - ) -} - -fn maybe_trace_filesystem( - inner: Arc, -) -> Arc { - if fs_trace_enabled() { - Arc::new(TracedFileSystem { inner }) as Arc - } else { - inner - } -} - -#[derive(Debug)] -struct TracedFileSystem { - inner: Arc, -} - -impl TracedFileSystem { - fn record(&self, counter: &AtomicU64, operation: impl FnOnce() -> T) -> T { - counter.fetch_add(1, Ordering::Relaxed); - let started = Instant::now(); - let result = operation(); - FS_TRACE.record_total(started.elapsed()); - result - } -} - -impl virtual_fs::FileSystem for TracedFileSystem { - fn readlink(&self, path: &Path) -> virtual_fs::Result { - self.record(&FS_TRACE.metadata_count, || self.inner.readlink(path)) - } - - fn read_dir(&self, path: &Path) -> virtual_fs::Result { - self.record(&FS_TRACE.read_dir_count, || self.inner.read_dir(path)) - } - - fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> { - self.record(&FS_TRACE.create_dir_count, || self.inner.create_dir(path)) - } - - fn create_symlink(&self, source: &Path, target: &Path) -> virtual_fs::Result<()> { - self.record(&FS_TRACE.create_dir_count, || { - self.inner.create_symlink(source, target) - }) - } - - fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> { - self.record(&FS_TRACE.remove_dir_count, || self.inner.remove_dir(path)) - } - - fn rename<'a>( - &'a self, - from: &'a Path, - to: &'a Path, - ) -> Pin> + Send + 'a>> { - FS_TRACE.rename_count.fetch_add(1, Ordering::Relaxed); - Box::pin(async move { - let started = Instant::now(); - let result = self.inner.rename(from, to).await; - FS_TRACE.record_total(started.elapsed()); - result - }) - } - - fn metadata(&self, path: &Path) -> virtual_fs::Result { - self.record(&FS_TRACE.metadata_count, || self.inner.metadata(path)) - } - - fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result { - self.record(&FS_TRACE.metadata_count, || { - self.inner.symlink_metadata(path) - }) - } - - fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> { - self.record(&FS_TRACE.remove_file_count, || self.inner.remove_file(path)) - } - - fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> { - virtual_fs::OpenOptions::new(self) - } -} - -impl virtual_fs::FileOpener for TracedFileSystem { - fn open( - &self, - path: &Path, - conf: &virtual_fs::OpenOptionsConfig, - ) -> virtual_fs::Result> { - FS_TRACE.open_count.fetch_add(1, Ordering::Relaxed); - let started = Instant::now(); - let file = virtual_fs::FileSystem::new_open_options(&self.inner) - .options(conf.clone()) - .open(path); - FS_TRACE.record_total(started.elapsed()); - file.map(|inner| Box::new(TracedVirtualFile { inner }) as _) - } -} - -#[derive(Debug)] -struct TracedVirtualFile { - inner: Box, -} - -impl virtual_fs::VirtualFile for TracedVirtualFile { - fn last_accessed(&self) -> u64 { - self.inner.last_accessed() - } - - fn last_modified(&self) -> u64 { - self.inner.last_modified() - } - - fn created_time(&self) -> u64 { - self.inner.created_time() - } - - fn set_times(&mut self, atime: Option, mtime: Option) -> virtual_fs::Result<()> { - self.inner.set_times(atime, mtime) - } - - fn size(&self) -> u64 { - self.inner.size() - } - - fn set_len(&mut self, new_size: u64) -> virtual_fs::Result<()> { - FS_TRACE.set_len_count.fetch_add(1, Ordering::Relaxed); - let started = Instant::now(); - let result = self.inner.set_len(new_size); - FS_TRACE.record_total(started.elapsed()); - result - } - - fn unlink(&mut self) -> virtual_fs::Result<()> { - FS_TRACE.unlink_count.fetch_add(1, Ordering::Relaxed); - let started = Instant::now(); - let result = self.inner.unlink(); - FS_TRACE.record_total(started.elapsed()); - result - } - - fn is_open(&self) -> bool { - self.inner.is_open() - } - - fn get_special_fd(&self) -> Option { - self.inner.get_special_fd() - } - - fn write_from_mmap(&mut self, offset: u64, len: u64) -> io::Result<()> { - self.inner.write_from_mmap(offset, len) - } - - fn poll_read_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - Pin::new(&mut *this.inner).poll_read_ready(cx) - } - - fn poll_write_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - Pin::new(&mut *this.inner).poll_write_ready(cx) - } -} - -impl virtual_fs::AsyncRead for TracedVirtualFile { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let this = self.get_mut(); - let before = buf.filled().len(); - let started = Instant::now(); - let result = Pin::new(&mut *this.inner).poll_read(cx, buf); - if let Poll::Ready(Ok(())) = &result { - let bytes = buf.filled().len().saturating_sub(before) as u64; - FS_TRACE.read_count.fetch_add(1, Ordering::Relaxed); - FS_TRACE.read_bytes.fetch_add(bytes, Ordering::Relaxed); - let elapsed = started.elapsed(); - FS_TRACE.record_total(elapsed); - FS_TRACE.read_elapsed_micros.fetch_add( - elapsed.as_micros().min(u64::MAX as u128) as u64, - Ordering::Relaxed, - ); - } - result - } -} - -impl virtual_fs::AsyncWrite for TracedVirtualFile { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut TaskContext<'_>, - buf: &[u8], - ) -> Poll> { - let this = self.get_mut(); - let started = Instant::now(); - let result = Pin::new(&mut *this.inner).poll_write(cx, buf); - if let Poll::Ready(Ok(bytes)) = &result { - FS_TRACE.write_count.fetch_add(1, Ordering::Relaxed); - FS_TRACE - .write_bytes - .fetch_add(*bytes as u64, Ordering::Relaxed); - let elapsed = started.elapsed(); - FS_TRACE.record_total(elapsed); - FS_TRACE.write_elapsed_micros.fetch_add( - elapsed.as_micros().min(u64::MAX as u128) as u64, - Ordering::Relaxed, - ); - } - result - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - Pin::new(&mut *this.inner).poll_flush(cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - Pin::new(&mut *this.inner).poll_shutdown(cx) - } -} - -impl virtual_fs::AsyncSeek for TracedVirtualFile { - fn start_seek(self: Pin<&mut Self>, position: io::SeekFrom) -> io::Result<()> { - let this = self.get_mut(); - FS_TRACE.seek_count.fetch_add(1, Ordering::Relaxed); - let started = Instant::now(); - let result = Pin::new(&mut *this.inner).start_seek(position); - let elapsed = started.elapsed(); - FS_TRACE.record_total(elapsed); - FS_TRACE.seek_elapsed_micros.fetch_add( - elapsed.as_micros().min(u64::MAX as u128) as u64, - Ordering::Relaxed, - ); - result - } - - fn poll_complete(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { - let this = self.get_mut(); - let started = Instant::now(); - let result = Pin::new(&mut *this.inner).poll_complete(cx); - if let Poll::Ready(Ok(_)) = &result { - let elapsed = started.elapsed(); - FS_TRACE.record_total(elapsed); - FS_TRACE.seek_elapsed_micros.fetch_add( - elapsed.as_micros().min(u64::MAX as u128) as u64, - Ordering::Relaxed, - ); - } - result - } -} - -fn build_wasix_runtime( - runtime: &TokioRuntime, - engine: &Engine, - module_cache: Arc, -) -> Arc { - let _guard = runtime.enter(); - let task_manager = Arc::new(TokioTaskManager::new(runtime.handle().clone())); - let mut wasix_runtime = PluggableRuntime::new(task_manager); - wasix_runtime.set_engine(engine.clone()); - wasix_runtime.set_module_cache(module_cache); - Arc::new(wasix_runtime) -} - -fn run_split_initdb(paths: &PglitePaths, runtime_layout: &RuntimeLayout) -> Result<()> { - let _phase = timing::phase("initdb.split_wasix"); - let initdb_module = runtime_layout.module_root.join("bin/initdb"); - let postgres_module = runtime_layout.module_root.join("bin/postgres"); - ensure!( - initdb_module.exists(), - "split WASIX initdb module is not installed at {}; regenerate assets with `xtask assets template`", - initdb_module.display() - ); - ensure!( - postgres_module.exists(), - "WASIX postgres module is not installed at {}", - postgres_module.display() - ); - - fs::create_dir_all(&paths.pgdata) - .with_context(|| format!("create fresh PGDATA {}", paths.pgdata.display()))?; - - let (engine, _) = aot::load_runtime_module()?; - let process_runtime = process_wasix_runtime(&engine)?; - seed_wasix_module_cache( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &initdb_module, - "tool:initdb", - "split initdb command", - )?; - seed_wasix_module_cache( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &postgres_module, - "runtime:pglite", - "initdb child postgres command", - )?; - preload_runtime_side_modules( - &process_runtime.tokio_runtime, - &engine, - &process_runtime.wasix_module_cache, - &runtime_layout.module_root, - )?; - // initdb execs child postgres commands; isolate that command process tree - // from concurrently running backends while keeping the module cache shared. - let initdb_runtime = build_wasix_runtime( - &process_runtime.tokio_runtime, - &engine, - process_runtime.wasix_module_cache.clone(), - ); - - let package = split_initdb_binary_package(&initdb_module, &postgres_module)?; - let root_fs = split_initdb_root_filesystem(paths, runtime_layout)?; - root_fs - .read_dir(Path::new(PGDATA_DIR)) - .with_context(|| format!("verify split initdb {PGDATA_DIR} mount"))?; - - let (stdout_file, stdout_capture) = TailCaptureFile::new(8 * 1024); - let (stderr_file, stderr_capture) = TailCaptureFile::new(8 * 1024); - - let mut runner = WasiRunner::new(); - runner - .with_current_dir("/") - .with_injected_package(package.clone()) - .with_args(split_initdb_args()) - .with_envs([ - ("PGDATA", PGDATA_DIR), - ("PGSYSCONFDIR", PGDATA_DIR), - ("HOME", "/home/postgres"), - ("USER", "postgres"), - ("LOGNAME", "postgres"), - ("PGCLIENTENCODING", "UTF8"), - ("PATH", "/bin"), - ("LC_CTYPE", "C.UTF-8"), - ("TZ", "UTC"), - ("PGTZ", "UTC"), - ("PG_COLOR", "never"), - ]) - .with_stdin(Box::::default()) - .with_stdout(Box::new(stdout_file)) - .with_stderr(Box::new(stderr_file)); - - { - let _phase = timing::phase("initdb.split_wasix.run_command"); - let result = - run_package_command_with_root(&runner, "initdb", &package, initdb_runtime, root_fs); - if let Err(err) = result { - let stdout = stdout_capture.text(); - let stderr = stderr_capture.text(); - let diagnostics = split_initdb_diagnostics(paths, runtime_layout); - return Err(err).with_context(|| { - format!( - "run split WASIX initdb\n{}\ninitdb stdout:\n{}\ninitdb stderr:\n{}", - diagnostics, - if stdout.trim().is_empty() { - "" - } else { - stdout.trim_end() - }, - if stderr.trim().is_empty() { - "" - } else { - stderr.trim_end() - } - ) - }); - } - } - Ok(()) -} - -fn split_initdb_root_filesystem( - paths: &PglitePaths, - runtime_layout: &RuntimeLayout, -) -> Result> { - let root: Arc = - if runtime_layout.uses_shared_overlay() { - let upper = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( - &paths.runtime_root(), - )?)); - let lower = virtual_fs::ArcFileSystem::new(maybe_trace_filesystem(host_filesystem( - &runtime_layout.module_root, - )?)); - Arc::new(virtual_fs::OverlayFileSystem::new(upper, [lower])) - } else { - maybe_trace_filesystem(host_filesystem(&paths.runtime_root())?) - }; - - let pgdata = maybe_trace_filesystem(host_filesystem(&paths.pgdata)?); - // initdb execs a child postgres command during bootstrap. Keep PGDATA inside - // the root filesystem view so both commands inherit the same /base mount. - let root = wasi_root_with_pgdata_mount(root, pgdata)?; - // Wasmer's runner normally starts from a temporary root that provides WASIX - // device files. Keep the real runtime/PGDATA root mounted for database - // writes, but route device paths such as /dev/urandom to virtual devices. - Ok(wasi_root_with_devices(root)?) -} - -fn run_package_command_with_root( - runner: &WasiRunner, - command_name: &str, - package: &BinaryPackage, - runtime: Arc, - root_fs: Arc, -) -> Result<()> { - let cmd = package.get_command(command_name).with_context(|| { - format!("split initdb package does not contain command {command_name:?}") - })?; - let wasi = cmd - .metadata() - .annotation("wasi")? - .unwrap_or_else(|| Wasi::new(command_name)); - let exec_name = wasi.exec_name.as_deref().unwrap_or(command_name); - let mut builder = runner - .prepare_webc_env( - exec_name, - &wasi, - PackageOrHash::Package(package), - RuntimeOrEngine::Runtime(runtime), - Some(WasiFsRoot::from_filesystem(root_fs)), - ) - .with_context(|| format!("prepare WASIX command environment for {command_name:?}"))?; - add_pgdata_preopen(&mut builder)?; - - let env = builder.build()?; - let runtime = env.runtime.clone(); - let tasks = runtime.task_manager().clone(); - let package = package.clone(); - let command_name = command_name.to_owned(); - let exit_code = tasks.spawn_and_block_on(async move { - let mut task_handle = spawn_exec(package, &command_name, env, &runtime) - .await - .with_context(|| format!("spawn WASIX command {command_name:?}"))?; - task_handle - .wait_finished() - .await - .map_err(|err| anyhow::anyhow!("{err}")) - .with_context(|| format!("wait for WASIX command {command_name:?}")) - })??; - - ensure!(exit_code.raw() == 0, "WASI exited with code: {exit_code}"); - Ok(()) -} - -fn split_initdb_diagnostics(paths: &PglitePaths, runtime_layout: &RuntimeLayout) -> String { - let pgdata_parent = paths.pgdata.parent().unwrap_or(&paths.pgdata); - format!( - "initdb diagnostics:\n layout_kind={:?}\n pgdata_host={}\n pgdata_parent={}\n runtime_root={}\n module_root={}\n pgdata_entries={}", - runtime_layout.kind, - path_state(&paths.pgdata), - path_state(pgdata_parent), - path_state(&paths.runtime_root()), - path_state(&runtime_layout.module_root), - dir_entry_sample(&paths.pgdata), - ) -} - -fn path_state(path: &Path) -> String { - match fs::metadata(path) { - Ok(metadata) => format!( - "{} ({})", - path.display(), - if metadata.is_dir() { - "dir" - } else if metadata.is_file() { - "file" - } else { - "other" - } - ), - Err(err) => format!("{} ({})", path.display(), err), - } -} - -fn dir_entry_sample(path: &Path) -> String { - let entries = match fs::read_dir(path) { - Ok(entries) => entries, - Err(err) => return format!(""), - }; - let mut names = entries - .filter_map(|entry| { - entry - .ok() - .map(|entry| entry.file_name().to_string_lossy().into_owned()) - }) - .take(8) - .collect::>(); - names.sort(); - if names.is_empty() { - "".to_owned() - } else { - names.join(", ") - } -} - -fn split_initdb_args() -> Vec<&'static str> { - vec![ - "--allow-group-access", - "--encoding", - "UTF8", - "--locale", - "C.UTF-8", - "--locale-provider", - "libc", - "--auth", - "trust", - "-D", - PGDATA_DIR, - ] -} - -fn split_initdb_binary_package( - initdb_module: &Path, - postgres_module: &Path, -) -> Result { - let initdb_wasm = - fs::read(initdb_module).with_context(|| format!("read {}", initdb_module.display()))?; - let postgres_wasm = - fs::read(postgres_module).with_context(|| format!("read {}", postgres_module.display()))?; - - let mut package_hash = Sha256::new(); - package_hash.update(b"pglite-oxide-split-initdb-package-v1\n"); - package_hash.update(&initdb_wasm); - package_hash.update(&postgres_wasm); - let package_hash: [u8; 32] = package_hash.finalize().into(); - let package_id = PackageId::Hash(PackageHash::from_sha256_bytes(package_hash)); - - Ok(BinaryPackage { - id: package_id.clone(), - package_ids: vec![package_id.clone()], - when_cached: None, - entrypoint_cmd: Some("initdb".to_owned()), - hash: Default::default(), - package_mounts: None, - commands: vec![ - split_initdb_command("initdb", initdb_wasm, &package_id), - split_initdb_command("postgres", postgres_wasm, &package_id), - ], - uses: Vec::new(), - file_system_memory_footprint: 0, - additional_host_mapped_directories: Vec::new(), - }) -} - -fn split_initdb_command(name: &str, wasm: Vec, package_id: &PackageId) -> BinaryPackageCommand { - let hash = ModuleHash::new(&wasm); - let atom: webc::compat::SharedBytes = wasm.into(); - BinaryPackageCommand::new( - name.to_owned(), - WebcCommand { - runner: WASI_RUNNER_URI.to_owned(), - annotations: Default::default(), - }, - atom, - hash, - None, - package_id.clone(), - package_id.clone(), - ) -} - -fn preload_runtime_side_modules( - runtime: &TokioRuntime, - engine: &Engine, - module_cache: &Arc, - runtime_root: &Path, -) -> Result<()> { - let _phase = timing::phase("wasix.seed_runtime_side_modules"); - let lib_dir = runtime_root.join("lib/postgresql"); - for (file_name, artifact_name) in RUNTIME_SIDE_MODULES { - let library = lib_dir.join(file_name); - ensure!( - library.exists(), - "runtime support module '{}' is not installed at {}", - file_name, - library.display() - ); - - seed_side_module_cache( - runtime, - engine, - module_cache, - &library, - artifact_name, - &format!("runtime support module '{file_name}'"), - )?; - } - Ok(()) -} - -#[cfg(feature = "extensions")] -fn preload_installed_extension_side_modules( - runtime: &TokioRuntime, - engine: &Engine, - module_cache: &Arc, - runtime_root: &Path, -) -> Result<()> { - let _phase = timing::phase("wasix.seed_extension_side_modules"); - let lib_dir = runtime_root.join("lib/postgresql"); - for extension in super::extensions::ALL { - let Some(module_file) = extension.native_module_file() else { - continue; - }; - let Some(aot_name) = extension.aot_name() else { - continue; - }; - let library = lib_dir.join(module_file); - if !library.exists() { - continue; - } - seed_side_module_cache( - runtime, - engine, - module_cache, - &library, - aot_name, - &format!("installed extension '{}'", extension.sql_name()), - )?; - } - Ok(()) -} - -fn seed_side_module_cache( - runtime: &TokioRuntime, - engine: &Engine, - module_cache: &Arc, - library: &Path, - artifact_name: &'static str, - label: &str, -) -> Result<()> { - seed_wasix_module_cache(runtime, engine, module_cache, library, artifact_name, label) -} - -fn seed_wasix_module_cache( - runtime: &TokioRuntime, - engine: &Engine, - module_cache: &Arc, - wasm_path: &Path, - artifact_name: &str, - label: &str, -) -> Result<()> { - let wasm = { - let _phase = timing::phase("wasix.seed_side_module.read_wasm"); - fs::read(wasm_path).with_context(|| format!("read WASIX module {}", wasm_path.display()))? - }; - let module_hash = { - let _phase = timing::phase("wasix.seed_side_module.module_hash"); - ModuleHash::new(&wasm) - }; - let seed_key = format!("{artifact_name}:{}:{module_hash}", aot::engine_identity()); - let mut seeded_side_modules = SEEDED_SIDE_MODULES - .get_or_init(|| Mutex::new(HashSet::new())) - .lock() - .expect("seeded side module cache poisoned"); - if seeded_side_modules.contains(&seed_key) { - return Ok(()); - } - - // Keep the process-wide seed check and SharedCache write atomic. Wasmer's - // shared cache is global to all concurrent PGlite instances in this process. - let module = { - let _phase = timing::phase("wasix.seed_side_module.load_aot"); - aot::load_artifact_module(engine, artifact_name)? - }; - { - let _phase = timing::phase("wasix.seed_side_module.save_cache"); - block_on_tokio_runtime(runtime, module_cache.save(module_hash, engine, &module)) - .with_context(|| format!("seed Wasmer module cache for {label} ({module_hash})"))?; - } - seeded_side_modules.insert(seed_key); - Ok(()) -} - -fn block_on_tokio_runtime(runtime: &TokioRuntime, future: F) -> T -where - F: Future + Send, - T: Send, -{ - if tokio::runtime::Handle::try_current().is_ok() { - return std::thread::scope(|scope| { - scope - .spawn(move || runtime.block_on(future)) - .join() - .unwrap_or_else(|payload| std::panic::resume_unwind(payload)) - }); - } - - runtime.block_on(future) -} - -impl PgliteLifecycleExports { - fn load(store: &mut Store, instance: &Instance) -> Result { - let wasi_start = typed_export(store, instance, "_start")?; - let set_force_host_error_recovery = - optional_typed_export(store, instance, "pgl_set_force_host_error_recovery")?; - let set_active = typed_export(store, instance, "pgl_setPGliteActive")?; - let start_pglite = typed_export(store, instance, "pgl_startPGlite")?; - let run_atexit_funcs = optional_typed_export(store, instance, "pgl_run_atexit_funcs")?; - let backend_timing_reset = - optional_typed_export(store, instance, "pgl_backend_timing_reset")?; - let backend_timing_elapsed_us = - optional_typed_export(store, instance, "pgl_backend_timing_elapsed_us")?; - - Ok(Self { - wasi_start, - set_force_host_error_recovery, - set_active, - start_pglite, - run_atexit_funcs, - backend_timing_reset, - backend_timing_elapsed_us, - }) - } -} - -impl WasixProtocolExports { - fn load(store: &mut Store, instance: &Instance) -> Result { - let get_port = typed_export(store, instance, "pgl_getMyProcPort")?; - let process_startup = typed_export(store, instance, "ProcessStartupPacket")?; - let send_conn_data = typed_export(store, instance, "pgl_sendConnData")?; - let pq_flush = typed_export(store, instance, "pgl_pq_flush")?; - let pq_buffer_remaining_data = typed_export(store, instance, "pq_buffer_remaining_data")?; - let main_loop = typed_export(store, instance, "PostgresMainLoopOnce")?; - let send_ready = typed_export(store, instance, "PostgresSendReadyForQueryIfNecessary")?; - let recover_error = typed_export(store, instance, "PostgresMainLongJmp")?; - - Ok(Self { - get_port, - process_startup, - send_conn_data, - pq_flush, - pq_buffer_remaining_data, - main_loop, - send_ready, - recover_error, - }) - } -} - -impl WasixProtocolStdioExports { - fn load(store: &mut Store, instance: &Instance) -> Result> { - let Some(set_protocol_transport) = - optional_typed_export::(store, instance, "pgl_set_protocol_transport")? - else { - return Ok(None); - }; - let protocol_stream_active = - typed_export::<(), i32>(store, instance, "pgl_protocol_stream_active")?; - Ok(Some(Self { - set_protocol_transport, - protocol_stream_active, - })) - } -} - -fn ensure_integrated_pglite_contract(instance: &Instance) -> Result<()> { - for name in [ - "pgl_startPGlite", - "pgl_setPGliteActive", - "PostgresMainLongJmp", - ] { - ensure!( - instance.exports.get_function(name).is_ok() - || instance.exports.get_function(&format!("_{name}")).is_ok(), - "WASIX runtime is missing integrated PGlite lifecycle export {name}" - ); - } - Ok(()) -} - -impl WasixPgliteIo { - fn new(store: &mut Store, instance: &Instance) -> Result { - let io = Self { - input_reset: typed_export(store, instance, "pgl_wasix_input_reset")?, - input_write: typed_export(store, instance, "pgl_wasix_input_write")?, - input_available: typed_export(store, instance, "pgl_wasix_input_available")?, - output_reset: typed_export(store, instance, "pgl_wasix_output_reset")?, - output_len: typed_export(store, instance, "pgl_wasix_output_len")?, - output_read: typed_export(store, instance, "pgl_wasix_output_read")?, - }; - io.reset(store)?; - Ok(io) - } - - fn reset(&self, store: &mut Store) -> Result<()> { - ensure!( - self.input_reset - .call(&mut *store) - .context("pgl_wasix_input_reset")? - == 0, - "pgl_wasix_input_reset failed" - ); - ensure!( - self.output_reset - .call(&mut *store) - .context("pgl_wasix_output_reset")? - == 0, - "pgl_wasix_output_reset failed" - ); - Ok(()) - } - - fn push_input( - &self, - store: &mut Store, - env: &WasiFunctionEnv, - allocator: &GuestAllocator, - bytes: &[u8], - ) -> Result<()> { - if bytes.is_empty() { - return Ok(()); - } - let written = allocator.with_bytes(store, env, bytes, |store, ptr| { - self.input_write - .call(&mut *store, ptr, bytes.len() as i32) - .context("pgl_wasix_input_write") - })?; - ensure!( - written == bytes.len() as i32, - "pgl_wasix_input_write wrote {written}, expected {}", - bytes.len() - ); - Ok(()) - } - - fn available(&self, store: &mut Store) -> Result { - let available = self - .input_available - .call(store) - .context("pgl_wasix_input_available")?; - ensure!( - available >= 0, - "pgl_wasix_input_available returned negative length {available}" - ); - Ok(available) - } - - fn take_output( - &self, - store: &mut Store, - env: &WasiFunctionEnv, - allocator: &GuestAllocator, - ) -> Result> { - let len = self - .output_len - .call(&mut *store) - .context("pgl_wasix_output_len")?; - ensure!( - len >= 0, - "pgl_wasix_output_len returned negative length {len}" - ); - if len == 0 { - return Ok(Vec::new()); - } - let bytes = allocator.with_allocation(store, len, |store, ptr| { - let read = self - .output_read - .call(&mut *store, ptr, len) - .context("pgl_wasix_output_read")?; - ensure!( - read >= 0 && read <= len, - "invalid pgl_wasix_output_read length {read}" - ); - - let mut bytes = vec![0u8; read as usize]; - let view = env - .data(&*store) - .try_memory_view(&*store) - .context("get WASIX memory view")?; - view.read(ptr as u64, &mut bytes) - .with_context(|| format!("read SQL output at 0x{ptr:x}"))?; - Ok(bytes) - })?; - ensure!( - self.output_reset - .call(&mut *store) - .context("pgl_wasix_output_reset after read")? - == 0, - "pgl_wasix_output_reset after read failed" - ); - Ok(bytes) - } -} - -impl GuestAllocator { - fn load(store: &mut Store, instance: &Instance) -> Result { - let malloc = typed_export::(store, instance, "malloc")?; - let free = typed_export::(store, instance, "pg_free") - .or_else(|_| typed_export::(store, instance, "free")) - .context("get pg_free/free export")?; - Ok(Self { - malloc, - free, - #[cfg(debug_assertions)] - allocations: Cell::new(0), - #[cfg(debug_assertions)] - frees: Cell::new(0), - }) - } - - #[cfg(debug_assertions)] - fn allocation_counts(&self) -> (u64, u64) { - (self.allocations.get(), self.frees.get()) - } - - fn with_bytes( - &self, - store: &mut Store, - env: &WasiFunctionEnv, - bytes: &[u8], - f: impl FnOnce(&mut Store, i32) -> Result, - ) -> Result { - let ptr = self.allocate(store, bytes.len() as i32)?; - self.run_and_free(store, ptr, |store, ptr| { - let view = env - .data(&*store) - .try_memory_view(&*store) - .context("get WASIX memory view")?; - view.write(ptr as u64, bytes) - .with_context(|| format!("write guest bytes at 0x{ptr:x}"))?; - f(store, ptr) - }) - } - - fn with_allocation( - &self, - store: &mut Store, - len: i32, - f: impl FnOnce(&mut Store, i32) -> Result, - ) -> Result { - let ptr = self.allocate(store, len)?; - self.run_and_free(store, ptr, f) - } - - fn allocate(&self, store: &mut Store, len: i32) -> Result { - let ptr = self - .malloc - .call(&mut *store, len) - .context("malloc guest allocation")?; - ensure!(ptr > 0, "malloc returned null for guest allocation"); - #[cfg(debug_assertions)] - self.allocations.set(self.allocations.get() + 1); - Ok(ptr) - } - - fn run_and_free( - &self, - store: &mut Store, - ptr: i32, - f: impl FnOnce(&mut Store, i32) -> Result, - ) -> Result { - let result = f(store, ptr); - let free_result = self - .free - .call(&mut *store, ptr) - .with_context(|| format!("free guest allocation at 0x{ptr:x}")); - #[cfg(debug_assertions)] - if free_result.is_ok() { - self.frees.set(self.frees.get() + 1); - } - match (result, free_result) { - (Ok(value), Ok(())) => Ok(value), - (Ok(_), Err(err)) => Err(err), - (Err(err), Ok(())) => Err(err), - (Err(err), Err(free_err)) => Err(err.context(format!( - "failed to free guest allocation at 0x{ptr:x} after previous error: {free_err:#}" - ))), - } - } -} - -fn typed_export( - store: &mut Store, - instance: &Instance, - name: &str, -) -> Result> -where - Args: WasmTypeList, - Rets: WasmTypeList, -{ - instance - .exports - .get_typed_function::(&*store, name) - .or_else(|_| { - instance - .exports - .get_typed_function::(&*store, &format!("_{name}")) - }) - .with_context(|| format!("get {name} export")) -} - -fn optional_typed_export( - store: &mut Store, - instance: &Instance, - name: &str, -) -> Result>> -where - Args: WasmTypeList, - Rets: WasmTypeList, -{ - let underscored_name = format!("_{name}"); - if instance.exports.get_function(name).is_err() - && instance.exports.get_function(&underscored_name).is_err() - { - return Ok(None); - } - typed_export(store, instance, name).map(Some) -} - -fn runtime_error_exit_code(err: &wasmer::RuntimeError) -> Option { - err.downcast_ref::().and_then(|err| match err { - WasiError::Exit(code) => Some(code.raw()), - _ => None, - }) -} - -fn is_wasm_uncaught_exception(err: &wasmer::RuntimeError) -> bool { - // Wasmer reports an uncaught WebAssembly exception when PostgreSQL ERROR - // unwinds across the exported loop boundary. The C recovery export then - // performs the normal Postgres error cleanup and emits ErrorResponse. - err.message().contains("uncaught exception") -} - -fn host_requires_process_exit_error_recovery() -> bool { - // Wasmer does not implement nested WebAssembly exception throws on MSVC - // hosts. The WASIX bridge therefore routes PostgreSQL ERROR longjmps - // through the existing process-exit recovery boundary on that host - // capability, while preserving normal nested unwinding elsewhere. - cfg!(target_env = "msvc") -} - -fn add_pglite_env(builder: &mut wasmer_wasix::WasiEnvBuilder, startup_config: &StartupConfig) { - for (key, value) in [ - ("PREFIX", WASM_PREFIX), - ("PGDATA", PGDATA_DIR), - ("PGUSER", startup_config.username.as_str()), - ("PGDATABASE", startup_config.database.as_str()), - ("MODE", "REACT"), - ("REPL", "N"), - ("PGSYSCONFDIR", PGDATA_DIR), - ("PGCLIENTENCODING", "UTF8"), - ("LC_CTYPE", "C.UTF-8"), - ("TZ", "UTC"), - ("PGTZ", "UTC"), - ("PG_COLOR", "never"), - ] { - builder.add_env(key, value); - } -} - -fn add_pglite_args( - builder: &mut wasmer_wasix::WasiEnvBuilder, - postgres_config: &PostgresConfig, - startup_config: &StartupConfig, -) -> Result<()> { - postgres_config.validate()?; - startup_config.validate()?; - for arg in ["--single", "-F", "-O", "-j"] { - builder.add_arg(arg); - } - if let Some(level) = startup_config.debug_level { - builder.add_arg("-d"); - builder.add_arg(level.to_string()); - } - for (name, value) in DEFAULT_STARTUP_GUCS { - builder.add_arg("-c"); - builder.add_arg(format!("{name}={value}")); - } - if startup_config.relaxed_durability { - builder.add_arg("-c"); - builder.add_arg("synchronous_commit=off"); - } - for (name, value) in postgres_config.iter() { - builder.add_arg("-c"); - builder.add_arg(format!("{name}={value}")); - } - for arg in &startup_config.extra_args { - builder.add_arg(arg); - } - for arg in ["-D", PGDATA_DIR, startup_config.database.as_str()] { - builder.add_arg(arg); - } - Ok(()) -} - -const DEFAULT_STARTUP_GUCS: &[(&str, &str)] = &[ - ("search_path", "public"), - ("exit_on_error", "false"), - ("log_checkpoints", "false"), - ("max_worker_processes", "0"), - ("max_parallel_workers", "0"), - ("max_parallel_workers_per_gather", "0"), - ("wal_buffers", "4MB"), - ("min_wal_size", "80MB"), - ("shared_buffers", "128MB"), -]; - -fn ensure_runtime_dirs(paths: &PglitePaths) -> Result<()> { - for path in [ - paths.runtime_root(), - paths.pgdata.clone(), - paths.runtime_root().join("home"), - paths.runtime_root().join("dev"), - paths.runtime_root().join("dev/shm"), - paths.runtime_root().join("tmp"), - ] { - fs::create_dir_all(&path).with_context(|| format!("create {}", path.display()))?; - } - - Ok(()) -} - -fn startup_packet(user: &str, database: &str) -> Vec { - let mut body = Vec::new(); - body.extend_from_slice(&196608i32.to_be_bytes()); - for (key, value) in [ - ("user", user), - ("database", database), - ("client_encoding", "UTF8"), - ("DateStyle", "ISO, MDY"), - ("TimeZone", "UTC"), - ] { - body.extend_from_slice(key.as_bytes()); - body.push(0); - body.extend_from_slice(value.as_bytes()); - body.push(0); - } - body.push(0); - - let mut packet = Vec::with_capacity(body.len() + 4); - packet.extend_from_slice(&((body.len() + 4) as i32).to_be_bytes()); - packet.extend_from_slice(&body); - packet -} - -fn protocol_response_contains_error(response: &[u8]) -> bool { - let mut cursor = 0usize; - while cursor + 5 <= response.len() { - let tag = response[cursor]; - let len = i32::from_be_bytes(response[cursor + 1..cursor + 5].try_into().unwrap()); - if len < 4 { - return false; - } - let total = 1usize.saturating_add(len as usize); - if cursor + total > response.len() { - return false; - } - if tag == b'E' { - return true; - } - cursor += total; - } - false -} - -fn format_output_tail(bytes: &[u8]) -> String { - const LIMIT: usize = 512; - let skipped = bytes.len().saturating_sub(LIMIT); - let tail = &bytes[skipped..]; - let mut hex = String::new(); - for (index, byte) in tail.iter().enumerate() { - if index > 0 { - hex.push(' '); - } - hex.push_str(&format!("{byte:02x}")); - } - let text = String::from_utf8_lossy(tail); - format!( - "{} bytes total, showing last {} bytes\nhex: {hex}\nutf8-lossy:\n{text}", - bytes.len(), - tail.len() - ) -} - -fn seed_exported_c_string_value( - store: &mut Store, - instance: &Instance, - env: &WasiFunctionEnv, - name: &str, - value: &str, -) -> Result<()> { - let Ok(global) = instance.exports.get_global(name) else { - return Ok(()); - }; - let wasmer::Value::I32(ptr) = global.get(&mut *store) else { - return Ok(()); - }; - if ptr <= 0 { - return Ok(()); - } - let mut bytes = value.as_bytes().to_vec(); - bytes.push(0); - let view = env - .data(&*store) - .try_memory_view(&*store) - .context("get WASIX memory view")?; - view.write(ptr as u64, &bytes) - .with_context(|| format!("seed {name} at 0x{ptr:x}"))?; - Ok(()) -} - -fn summarize_protocol(bytes: &[u8]) -> String { - if bytes.is_empty() { - return "0 bytes".to_owned(); - } - - let mut cursor = 0usize; - let mut messages = Vec::new(); - while cursor + 5 <= bytes.len() { - let tag = bytes[cursor] as char; - let len = i32::from_be_bytes([ - bytes[cursor + 1], - bytes[cursor + 2], - bytes[cursor + 3], - bytes[cursor + 4], - ]); - if len < 4 { - messages.push(format!("{tag}(bad-len:{len})")); - break; - } - let end = cursor + 1 + len as usize; - if end > bytes.len() { - messages.push(format!("{tag}(truncated:{len})")); - break; - } - messages.push(format!("{tag}({} bytes)", len - 4)); - cursor = end; - } - if cursor < bytes.len() { - messages.push(format!("tail:{} bytes", bytes.len() - cursor)); - } - format!("{} bytes [{}]", bytes.len(), messages.join(", ")) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn protocol_stdio_fails_closed_when_detached() -> Result<()> { - use std::task::{Context, Poll, Waker}; - use wasmer_wasix::VirtualFile; - use wasmer_wasix::virtual_fs::AsyncWrite; - - let mut file = ProtocolStdioFile::new(); - let mut cx = Context::from_waker(Waker::noop()); - - match Pin::new(&mut file).poll_write_ready(&mut cx) { - Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), - other => panic!("unexpected detached write-ready result: {other:?}"), - } - match Pin::new(&mut file).poll_write(&mut cx, b"lost bytes") { - Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), - other => panic!("unexpected detached write result: {other:?}"), - } - match Pin::new(&mut file).poll_flush(&mut cx) { - Poll::Ready(Err(err)) => assert_eq!(err.kind(), io::ErrorKind::BrokenPipe), - other => panic!("unexpected detached flush result: {other:?}"), - } - - Ok(()) - } - - #[test] - fn block_on_tokio_runtime_works_inside_tokio_runtime() -> Result<()> { - let worker = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let host = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - - let value = host.block_on(async { block_on_tokio_runtime(&worker, async { 42 }) }); - - assert_eq!(value, 42); - Ok(()) - } - - #[test] - fn mountfs_pgdata_overlay_exposes_lower_template_files() -> Result<()> { - use tokio::io::AsyncWriteExt; - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let _guard = runtime.enter(); - let temp = tempfile::TempDir::new()?; - let runtime_root = temp.path().join("runtime"); - let pgdata_upper = runtime_root.join("base"); - let pgdata_lower = temp.path().join("template"); - fs::create_dir_all(&pgdata_upper)?; - fs::create_dir_all(&pgdata_lower)?; - fs::write(pgdata_lower.join("postgresql.conf"), b"from-template\n")?; - - let root = virtual_fs::MountFileSystem::new(); - root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; - root.mount( - Path::new(PGDATA_DIR), - Arc::new(EagerCopyOverlayFileSystem::new( - pgdata_upper.clone(), - pgdata_lower.clone(), - )?), - )?; - - virtual_fs::FileSystem::metadata(&root, Path::new("/base/postgresql.conf"))?; - virtual_fs::FileSystem::new_open_options(&root) - .read(true) - .open("/base/postgresql.conf")?; - let mut writable = virtual_fs::FileSystem::new_open_options(&root) - .write(true) - .open("/base/postgresql.conf")?; - runtime.block_on(async { - writable.write_all(b"upper-only\n").await?; - writable.flush().await - })?; - assert!(pgdata_upper.join("postgresql.conf").is_file()); - assert_eq!( - fs::read_to_string(pgdata_lower.join("postgresql.conf"))?, - "from-template\n" - ); - Ok(()) - } - - #[test] - fn mountfs_pgdata_overlay_creates_files_in_lower_only_directories() -> Result<()> { - use tokio::io::AsyncWriteExt; - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let _guard = runtime.enter(); - let temp = tempfile::TempDir::new()?; - let runtime_root = temp.path().join("runtime"); - let pgdata_upper = runtime_root.join("base"); - let pgdata_lower = temp.path().join("template"); - fs::create_dir_all(&pgdata_upper)?; - fs::create_dir_all(pgdata_lower.join("global"))?; - - let root = virtual_fs::MountFileSystem::new(); - root.mount(Path::new("/"), host_filesystem(&runtime_root)?)?; - root.mount( - Path::new(PGDATA_DIR), - Arc::new(EagerCopyOverlayFileSystem::new( - pgdata_upper.clone(), - pgdata_lower, - )?), - )?; - - let mut writable = virtual_fs::FileSystem::new_open_options(&root) - .write(true) - .create(true) - .open("/base/global/postmaster.pid")?; - runtime.block_on(async { - writable.write_all(b"lock\n").await?; - writable.flush().await - })?; - - assert_eq!( - fs::read_to_string(pgdata_upper.join("global/postmaster.pid"))?, - "lock\n" - ); - Ok(()) - } - - #[test] - fn mountfs_root_filesystem_routes_pgdata_as_mutable_subtree() -> Result<()> { - use tokio::io::AsyncWriteExt; - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let _guard = runtime.enter(); - let temp = tempfile::TempDir::new()?; - let runtime_root = temp.path().join("runtime"); - let pgdata_upper = runtime_root.join("base"); - let pgdata_lower = temp.path().join("template"); - fs::create_dir_all(&pgdata_upper)?; - fs::create_dir_all(pgdata_lower.join("global"))?; - fs::write(pgdata_lower.join("PG_VERSION"), b"17\n")?; - fs::write(pgdata_lower.join("global/pg_control"), b"control\n")?; - - let root = wasi_root_with_pgdata_mount( - host_filesystem(&runtime_root)?, - Arc::new(EagerCopyOverlayFileSystem::new( - pgdata_upper.clone(), - pgdata_lower, - )?), - )?; - - virtual_fs::FileSystem::metadata(root.as_ref(), Path::new("/base/PG_VERSION"))?; - let mut entries = - virtual_fs::FileSystem::read_dir(root.as_ref(), Path::new("/base/global"))?; - let entry = entries.next().transpose()?.context("expected pg_control")?; - assert_eq!(entry.path, Path::new("/base/global/pg_control")); - - let mut lock_file = virtual_fs::FileSystem::new_open_options(root.as_ref()) - .read(true) - .write(true) - .create_new(true) - .open("/base/postmaster.pid")?; - runtime.block_on(async { - lock_file.write_all(b"lock\n").await?; - lock_file.flush().await - })?; - - assert_eq!( - fs::read_to_string(pgdata_upper.join("postmaster.pid"))?, - "lock\n" - ); - Ok(()) - } -} diff --git a/src/pglite/server.rs b/src/pglite/server.rs deleted file mode 100644 index 852f1673..00000000 --- a/src/pglite/server.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::net::{SocketAddr, TcpListener, TcpStream}; -#[cfg(unix)] -use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::{Path, PathBuf}; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - mpsc::{Receiver, sync_channel}, -}; -use std::thread::{self, JoinHandle}; - -use anyhow::{Context, Result, anyhow}; -use tempfile::TempDir; - -use crate::pglite::base::{PreparedRoot, RootLock, RootPlan, RootSource, RootTarget, prepare_root}; -use crate::pglite::config::{PostgresConfig, StartupConfig}; -#[cfg(feature = "extensions")] -use crate::pglite::extensions::{Extension, resolve_extension_set}; -use crate::pglite::interface::DebugLevel; -#[cfg(feature = "extensions")] -use crate::pglite::pg_dump::{PgDumpOptions, dump_server_sql}; -use crate::pglite::proxy::PgliteProxy; -use crate::pglite::timing; - -/// A supervised local PostgreSQL socket backed by one embedded PGlite runtime. -/// -/// This is the compatibility entry point for code that expects a PostgreSQL URL, -/// such as `tokio-postgres`, SQLx, or tools that speak the wire protocol. The -/// server owns one embedded backend, so downstream pools should use a single -/// connection. -#[derive(Debug)] -pub struct PgliteServer { - root: PathBuf, - _temp_dir: Option, - _root_lock: Option, - endpoint: ServerEndpoint, - startup_config: StartupConfig, - shutdown: Arc, - handle: Option>>, -} - -#[derive(Debug, Clone)] -enum ServerEndpoint { - Tcp(SocketAddr), - #[cfg(unix)] - Unix(PathBuf), -} - -impl PgliteServer { - /// Build a local PGlite server. The default is a cached temporary database - /// served on `127.0.0.1:0`. - pub fn builder() -> PgliteServerBuilder { - PgliteServerBuilder::new() - } - - /// Start a cached temporary database on a random local TCP port. - pub fn temporary_tcp() -> Result { - Self::builder().temporary().start() - } - - /// Return the root directory used for runtime files and cluster data. - pub fn root(&self) -> &Path { - &self.root - } - - /// Return the bound TCP address, if this server is using TCP. - pub fn tcp_addr(&self) -> Option { - match self.endpoint { - ServerEndpoint::Tcp(addr) => Some(addr), - #[cfg(unix)] - ServerEndpoint::Unix(_) => None, - } - } - - /// Return the Unix-domain socket path, if this server is using UDS. - #[cfg(unix)] - pub fn socket_path(&self) -> Option<&Path> { - match &self.endpoint { - ServerEndpoint::Tcp(_) => None, - ServerEndpoint::Unix(path) => Some(path), - } - } - - /// Return a PostgreSQL connection URI for the local server. - pub fn connection_uri(&self) -> String { - match &self.endpoint { - ServerEndpoint::Tcp(addr) => tcp_connection_uri(*addr, &self.startup_config), - #[cfg(unix)] - ServerEndpoint::Unix(path) => { - let host = path.parent().unwrap_or_else(|| Path::new("/tmp")); - let port = parse_unix_socket_port(path).unwrap_or(5432); - format!( - "postgresql://{}@/{}?host={}&port={}&sslmode=disable", - self.startup_config.username, - self.startup_config.database, - percent_encode_query_value(&host.display().to_string()), - port - ) - } - } - } - - /// Alias for [`connection_uri`](Self::connection_uri). - pub fn database_url(&self) -> String { - self.connection_uri() - } - - /// Run the bundled WASIX `pg_dump` against this server and return SQL text. - #[cfg(feature = "extensions")] - pub fn dump_sql(&self, options: PgDumpOptions) -> Result { - let addr = self - .tcp_addr() - .context("pg_dump currently requires a TCP PgliteServer endpoint")?; - dump_server_sql(addr, &options) - } - - /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. - #[cfg(feature = "extensions")] - pub fn dump_bytes(&self, options: PgDumpOptions) -> Result> { - Ok(self.dump_sql(options)?.into_bytes()) - } - - /// Request shutdown and wait for the listener thread to exit. - /// - /// Close database clients before calling this method. The current proxy owns - /// one blocking backend connection at a time, so an open client can keep the - /// worker thread busy until it disconnects. - pub fn shutdown(mut self) -> Result<()> { - self.stop() - } - - fn stop(&mut self) -> Result<()> { - self.shutdown.store(true, Ordering::SeqCst); - { - let _phase = timing::phase("server.shutdown_wake"); - wake_listener(&self.endpoint); - } - if let Some(handle) = self.handle.take() { - let _phase = timing::phase("server.thread_join"); - handle - .join() - .map_err(|_| anyhow!("pglite server thread panicked"))??; - } - Ok(()) - } -} - -impl Drop for PgliteServer { - fn drop(&mut self) { - if let Err(err) = self.stop() { - tracing::warn!("pglite server shutdown during drop failed: {err:#}"); - } - } -} - -/// Builder for [`PgliteServer`]. -#[derive(Debug, Clone)] -pub struct PgliteServerBuilder { - root: ServerRoot, - endpoint: ServerEndpointConfig, - postgres_config: PostgresConfig, - startup_config: StartupConfig, - #[cfg(feature = "extensions")] - extensions: Vec, -} - -#[derive(Debug, Clone)] -enum ServerRoot { - Temporary { template_cache: bool }, - Path(PathBuf), -} - -#[derive(Debug, Clone)] -enum ServerEndpointConfig { - Tcp(SocketAddr), - #[cfg(unix)] - Unix(PathBuf), -} - -impl Default for PgliteServerBuilder { - fn default() -> Self { - Self { - root: ServerRoot::Temporary { - template_cache: true, - }, - endpoint: ServerEndpointConfig::Tcp(SocketAddr::from(([127, 0, 0, 1], 0))), - postgres_config: PostgresConfig::default(), - startup_config: StartupConfig::default(), - #[cfg(feature = "extensions")] - extensions: Vec::new(), - } - } -} - -impl PgliteServerBuilder { - /// Create a builder. Defaults to a cached temporary database on - /// `127.0.0.1:0`. - pub fn new() -> Self { - Self::default() - } - - /// Serve a persistent database rooted at `root`. - pub fn path(mut self, root: impl Into) -> Self { - self.root = ServerRoot::Path(root.into()); - self - } - - /// Serve a temporary database cloned from the process-local template cache. - pub fn temporary(mut self) -> Self { - self.root = ServerRoot::Temporary { - template_cache: true, - }; - self - } - - /// Serve a temporary database initialized without the template cache. - /// - /// This is a compatibility alias for the pre-template-cache public API. - /// Fresh initdb uses the bundled split WASIX `initdb` module; cached - /// temporary databases remain the production fast path. - pub fn fresh_temporary(mut self) -> Self { - self.root = ServerRoot::Temporary { - template_cache: false, - }; - self - } - - /// Bind the server to a TCP address. - pub fn tcp(mut self, addr: SocketAddr) -> Self { - self.endpoint = ServerEndpointConfig::Tcp(addr); - self - } - - /// Bind the server to a Unix-domain socket path. - #[cfg(unix)] - pub fn unix(mut self, path: impl Into) -> Self { - self.endpoint = ServerEndpointConfig::Unix(path.into()); - self - } - - /// Set a PostgreSQL startup GUC for the embedded backend used by this - /// server. - pub fn postgres_config(mut self, name: impl Into, value: impl Into) -> Self { - self.postgres_config.insert(name, value); - self - } - - /// Set multiple PostgreSQL startup GUCs for the embedded backend used by - /// this server. - pub fn postgres_configs(mut self, settings: impl IntoIterator) -> Self - where - K: Into, - V: Into, - { - for (name, value) in settings { - self.postgres_config.insert(name, value); - } - self - } - - /// Default user encoded in [`PgliteServer::database_url`]. - pub fn username(mut self, username: impl Into) -> Self { - self.startup_config.username = username.into(); - self - } - - /// Default database encoded in [`PgliteServer::database_url`]. - pub fn database(mut self, database: impl Into) -> Self { - self.startup_config.database = database.into(); - self - } - - /// Enable PostgreSQL debug logging level `0..=5` for server backends. - pub fn debug_level(mut self, level: DebugLevel) -> Self { - self.startup_config.debug_level = Some(level); - self - } - - /// Use lower durability settings for ephemeral or cacheable local - /// workloads. - pub fn relaxed_durability(mut self, enabled: bool) -> Self { - self.startup_config.relaxed_durability = enabled; - self - } - - /// Append an advanced PostgreSQL startup argument for server backends. - pub fn startup_arg(mut self, arg: impl Into) -> Self { - self.startup_config.extra_args.push(arg.into()); - self - } - - /// Append advanced PostgreSQL startup arguments for server backends. - pub fn startup_args(mut self, args: impl IntoIterator>) -> Self { - self.startup_config - .extra_args - .extend(args.into_iter().map(Into::into)); - self - } - - /// Enable a bundled Postgres extension before serving connections. - #[cfg(feature = "extensions")] - pub fn extension(mut self, extension: Extension) -> Self { - self.extensions.push(extension); - self - } - - /// Enable bundled Postgres extensions before serving connections. - #[cfg(feature = "extensions")] - pub fn extensions(mut self, extensions: impl IntoIterator) -> Self { - self.extensions.extend(extensions); - self - } - - /// Install the runtime if needed, initialize the cluster, and start serving. - pub fn start(self) -> Result { - self.postgres_config.validate()?; - self.startup_config.validate()?; - #[cfg(feature = "extensions")] - let extensions = resolve_extension_set(&self.extensions)?; - let postgres_config = self.postgres_config.clone(); - let startup_config = self.startup_config.clone(); - - let prepared_root = { - let _phase = timing::phase("server.root_prepare"); - match self.root { - ServerRoot::Path(root) => { - let _phase = timing::phase("server.root_prepare.path"); - let plan = RootPlan::new(RootTarget::Path(root), RootSource::Template); - #[cfg(feature = "extensions")] - let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); - prepare_root(plan)? - } - ServerRoot::Temporary { template_cache } => { - let source = if template_cache { - RootSource::Template - } else { - RootSource::FreshInitdb - }; - let phase = if template_cache { - "server.root_prepare.temporary_cached" - } else { - "server.root_prepare.temporary_fresh" - }; - let _phase = timing::phase(phase); - let plan = RootPlan::new(RootTarget::Temporary, source); - #[cfg(feature = "extensions")] - let plan = plan.with_extensions(extensions.clone(), postgres_config.clone()); - run_blocking("pglite-template-cache", move || prepare_root(plan))? - } - } - }; - let PreparedRoot { - root, - temp_dir, - root_lock, - outcome, - } = prepared_root; - - let shutdown = Arc::new(AtomicBool::new(false)); - let proxy = { - let _phase = timing::phase("server.proxy_create"); - PgliteProxy::new(root.clone()).with_prepared_root(outcome) - }; - let proxy = proxy - .with_postgres_config(postgres_config) - .with_startup_config(startup_config.clone()); - #[cfg(feature = "extensions")] - let proxy = proxy.with_extensions(extensions); - - let (endpoint, handle) = match self.endpoint { - ServerEndpointConfig::Tcp(addr) => start_tcp(proxy, addr, shutdown.clone())?, - #[cfg(unix)] - ServerEndpointConfig::Unix(path) => start_unix(proxy, path, shutdown.clone())?, - }; - - Ok(PgliteServer { - root, - _temp_dir: temp_dir, - _root_lock: root_lock, - endpoint, - startup_config, - shutdown, - handle: Some(handle), - }) - } -} - -fn start_tcp( - proxy: PgliteProxy, - addr: SocketAddr, - shutdown: Arc, -) -> Result<(ServerEndpoint, JoinHandle>)> { - let listener = { - let _phase = timing::phase("server.tcp_bind"); - TcpListener::bind(addr).context("bind PGlite TCP server")? - }; - let addr = { - let _phase = timing::phase("server.tcp_local_addr"); - listener.local_addr().context("read PGlite TCP address")? - }; - let (ready_tx, ready_rx) = sync_channel(1); - let recorder = timing::current_recorder(); - let handle = { - let _phase = timing::phase("server.thread_spawn"); - thread::spawn(move || { - timing::with_recorder(recorder, || { - proxy.serve_tcp_listener_until_ready(listener, shutdown, Some(ready_tx)) - }) - }) - }; - { - let _phase = timing::phase("server.wait_ready"); - wait_until_ready(&ready_rx)?; - } - Ok((ServerEndpoint::Tcp(addr), handle)) -} - -fn tcp_connection_uri(addr: SocketAddr, startup: &StartupConfig) -> String { - match addr { - SocketAddr::V4(addr) => { - format!( - "postgresql://{}@{}:{}/{}?sslmode=disable", - startup.username, - addr.ip(), - addr.port(), - startup.database - ) - } - SocketAddr::V6(addr) => { - format!( - "postgresql://{}@[{}]:{}/{}?sslmode=disable", - startup.username, - addr.ip(), - addr.port(), - startup.database - ) - } - } -} - -fn run_blocking(name: &'static str, f: F) -> Result -where - T: Send + 'static, - F: FnOnce() -> Result + Send + 'static, -{ - let recorder = timing::current_recorder(); - thread::Builder::new() - .name(name.to_string()) - .spawn(move || timing::with_recorder(recorder, f)) - .with_context(|| format!("spawn {name} worker"))? - .join() - .map_err(|_| anyhow!("{name} worker panicked"))? -} - -#[cfg(unix)] -fn start_unix( - proxy: PgliteProxy, - path: PathBuf, - shutdown: Arc, -) -> Result<(ServerEndpoint, JoinHandle>)> { - { - let _phase = timing::phase("server.unix_prepare_path"); - if path.exists() { - std::fs::remove_file(&path) - .with_context(|| format!("remove stale socket {}", path.display()))?; - } - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create socket directory {}", parent.display()))?; - } - } - - let listener = { - let _phase = timing::phase("server.unix_bind"); - UnixListener::bind(&path) - .with_context(|| format!("bind PGlite Unix socket {}", path.display()))? - }; - let endpoint = ServerEndpoint::Unix(path); - let (ready_tx, ready_rx) = sync_channel(1); - let recorder = timing::current_recorder(); - let handle = { - let _phase = timing::phase("server.thread_spawn"); - thread::spawn(move || { - timing::with_recorder(recorder, || { - proxy.serve_unix_listener_until_ready(listener, shutdown, Some(ready_tx)) - }) - }) - }; - { - let _phase = timing::phase("server.wait_ready"); - wait_until_ready(&ready_rx)?; - } - Ok((endpoint, handle)) -} - -fn wait_until_ready(ready_rx: &Receiver>) -> Result<()> { - ready_rx - .recv() - .context("PGlite server thread exited before reporting readiness")? -} - -fn wake_listener(endpoint: &ServerEndpoint) { - match endpoint { - ServerEndpoint::Tcp(addr) => { - let _ = TcpStream::connect(addr); - } - #[cfg(unix)] - ServerEndpoint::Unix(path) => { - let _ = UnixStream::connect(path); - } - } -} - -#[cfg(unix)] -fn parse_unix_socket_port(path: &Path) -> Option { - let name = path.file_name()?.to_str()?; - name.strip_prefix(".s.PGSQL.")?.parse().ok() -} - -#[cfg(unix)] -fn percent_encode_query_value(value: &str) -> String { - let mut encoded = String::with_capacity(value.len()); - for byte in value.bytes() { - if matches!( - byte, - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' - ) { - encoded.push(byte as char); - } else { - encoded.push_str(&format!("%{byte:02X}")); - } - } - encoded -} - -#[cfg(all(test, unix))] -mod tests { - use super::percent_encode_query_value; - - #[test] - fn unix_socket_uri_host_is_query_encoded() { - assert_eq!( - percent_encode_query_value("/tmp/Application Support/pglite"), - "/tmp/Application%20Support/pglite" - ); - } -} diff --git a/src/pglite/types.rs b/src/pglite/types.rs deleted file mode 100644 index e5df4061..00000000 --- a/src/pglite/types.rs +++ /dev/null @@ -1,578 +0,0 @@ -use std::collections::HashMap; -use std::str::FromStr; -use std::sync::Arc; -use std::sync::LazyLock; - -use anyhow::{Result, anyhow}; -use serde_json::{Value, json}; - -use super::interface::{ParserMap, Serializer, SerializerMap, TypeParser}; - -macro_rules! const_oid { - ($name:ident = $value:expr) => { - pub const $name: i32 = $value; - }; -} - -const_oid!(BOOL = 16); -const_oid!(BYTEA = 17); -const_oid!(CHAR = 18); -const_oid!(INT8 = 20); -const_oid!(INT2 = 21); -const_oid!(INT4 = 23); -const_oid!(TEXT = 25); -const_oid!(OID = 26); -const_oid!(JSON = 114); -const_oid!(FLOAT4 = 700); -const_oid!(FLOAT8 = 701); -const_oid!(DATE = 1082); -const_oid!(TIMESTAMP = 1114); -const_oid!(TIMESTAMPTZ = 1184); -const_oid!(NUMERIC = 1700); -const_oid!(UUID = 2950); -const_oid!(JSONB = 3802); - -pub static DEFAULT_PARSERS: LazyLock = LazyLock::new(build_default_parsers); -pub static DEFAULT_SERIALIZERS: LazyLock = LazyLock::new(build_default_serializers); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ArrayTypeInfo { - pub element_oid: i32, - pub array_oid: i32, - pub delimiter: char, -} - -impl ArrayTypeInfo { - pub const fn new(element_oid: i32, array_oid: i32, delimiter: char) -> Self { - Self { - element_oid, - array_oid, - delimiter, - } - } -} - -pub struct ParserLookup<'a> { - defaults: &'a ParserMap, - overrides: &'a ParserMap, -} - -impl<'a> ParserLookup<'a> { - pub fn new(defaults: &'a ParserMap, overrides: &'a ParserMap) -> Self { - Self { - defaults, - overrides, - } - } - - pub fn apply(&self, text: &str, type_id: i32) -> Value { - let parser = self - .overrides - .get(&type_id) - .or_else(|| self.defaults.get(&type_id)); - if let Some(parser) = parser { - parser(text, type_id) - } else { - json!(text) - } - } -} - -pub fn serialize_array_value( - value: &Value, - element_serializer: Option, - delimiter: char, -) -> Result { - match value { - Value::Array(items) => { - if items.is_empty() { - return Ok("{}".to_string()); - } - - let mut parts = Vec::with_capacity(items.len()); - for item in items { - match item { - Value::Null => parts.push("null".to_string()), - Value::Array(_) => { - parts.push(serialize_array_value( - item, - element_serializer.clone(), - delimiter, - )?); - } - _ => { - let raw = if let Some(serializer) = element_serializer.as_ref() { - serializer(item)? - } else { - value_to_string(item) - }; - let escaped = raw.replace('\\', "\\\\").replace('"', "\\\""); - parts.push(format!("\"{}\"", escaped)); - } - } - } - let joined = parts.join(&delimiter.to_string()); - Ok(format!("{{{}}}", joined)) - } - Value::Null => Ok("null".to_string()), - _ => Ok(value_to_string(value)), - } -} - -fn value_to_string(value: &Value) -> String { - match value { - Value::String(s) => s.clone(), - Value::Number(n) => n.to_string(), - Value::Bool(b) => { - if *b { - "t".to_string() - } else { - "f".to_string() - } - } - Value::Null => "null".to_string(), - Value::Array(_) => value.to_string(), - _ => value.to_string(), - } -} - -pub fn parse_array_text( - text: &str, - element_parser: Option, - element_type_id: i32, - delimiter: char, -) -> Value { - let Some(start) = text.find('{') else { - return Value::Array(Vec::new()); - }; - let mut parser = ArrayTextParser { - text, - index: start, - element_parser: element_parser.as_ref(), - element_type_id, - delimiter, - }; - parser.parse_array() -} - -struct ArrayTextParser<'a> { - text: &'a str, - index: usize, - element_parser: Option<&'a TypeParser>, - element_type_id: i32, - delimiter: char, -} - -impl ArrayTextParser<'_> { - fn parse_array(&mut self) -> Value { - if self.peek() != Some('{') { - return Value::Array(Vec::new()); - } - self.advance(); - - let mut values = Vec::new(); - loop { - match self.peek() { - Some('}') => { - self.advance(); - return Value::Array(values); - } - Some('{') => values.push(self.parse_array()), - Some('"') => values.push(self.parse_quoted()), - Some(_) => values.push(self.parse_unquoted()), - None => return Value::Array(values), - } - - match self.peek() { - Some(ch) if ch == self.delimiter => { - self.advance(); - } - Some('}') => {} - Some(_) | None => {} - } - } - } - - fn parse_quoted(&mut self) -> Value { - self.advance(); - let mut value = String::new(); - - while let Some(ch) = self.peek() { - self.advance(); - match ch { - '\\' => { - if let Some(escaped) = self.peek() { - self.advance(); - value.push(escaped); - } - } - '"' => { - return apply_element_parser( - &value, - self.element_parser, - self.element_type_id, - true, - ); - } - other => value.push(other), - } - } - - apply_element_parser(&value, self.element_parser, self.element_type_id, true) - } - - fn parse_unquoted(&mut self) -> Value { - let start = self.index; - while let Some(ch) = self.peek() { - if ch == self.delimiter || ch == '}' { - break; - } - self.advance(); - } - - let slice = self.text[start..self.index].trim(); - apply_element_parser(slice, self.element_parser, self.element_type_id, false) - } - - fn peek(&self) -> Option { - self.text[self.index..].chars().next() - } - - fn advance(&mut self) { - if let Some(ch) = self.peek() { - self.index += ch.len_utf8(); - } - } -} - -fn apply_element_parser( - slice: &str, - parser: Option<&TypeParser>, - element_type_id: i32, - quoted: bool, -) -> Value { - if let Some(p) = parser { - p(slice, element_type_id) - } else if !quoted && slice.eq_ignore_ascii_case("NULL") { - Value::Null - } else { - Value::String(slice.to_string()) - } -} - -fn build_default_parsers() -> ParserMap { - let mut map: ParserMap = HashMap::new(); - - map.insert( - TEXT, - Arc::new(|value: &str, _| json!(value.to_string())) as TypeParser, - ); - map.insert(CHAR, Arc::new(|value: &str, _| json!(value.to_string()))); - - map.insert(INT2, Arc::new(|value: &str, _| parse_int(value))); - map.insert(INT4, Arc::new(|value: &str, _| parse_int(value))); - map.insert(INT8, Arc::new(|value: &str, _| parse_bigint(value))); - map.insert(OID, Arc::new(|value: &str, _| parse_int(value))); - map.insert(NUMERIC, Arc::new(|value: &str, _| parse_numeric(value))); - - map.insert(FLOAT4, Arc::new(|value: &str, _| parse_float(value))); - map.insert(FLOAT8, Arc::new(|value: &str, _| parse_float(value))); - - map.insert(BOOL, Arc::new(|value: &str, _| json!(value == "t"))); - - map.insert(JSON, Arc::new(|value: &str, _| parse_json(value))); - map.insert(JSONB, Arc::new(|value: &str, _| parse_json(value))); - - map.insert(BYTEA, Arc::new(|value: &str, _| parse_bytea(value))); - - map.insert(UUID, Arc::new(|value: &str, _| json!(value.to_string()))); - - map.insert( - TIMESTAMP, - Arc::new(|value: &str, _| json!(value.to_string())), - ); - map.insert( - TIMESTAMPTZ, - Arc::new(|value: &str, _| json!(value.to_string())), - ); - map.insert(DATE, Arc::new(|value: &str, _| json!(value.to_string()))); - - register_builtin_array_parsers(&mut map); - map -} - -fn build_default_serializers() -> SerializerMap { - let mut map: SerializerMap = HashMap::new(); - - map.insert( - TEXT, - Arc::new(|value: &Value| serialize_string(value)) as Serializer, - ); - map.insert(CHAR, Arc::new(|value: &Value| serialize_string(value))); - - map.insert(INT2, Arc::new(|value: &Value| serialize_number(value))); - map.insert(INT4, Arc::new(|value: &Value| serialize_number(value))); - map.insert(INT8, Arc::new(|value: &Value| serialize_number(value))); - map.insert(OID, Arc::new(|value: &Value| serialize_number(value))); - map.insert(NUMERIC, Arc::new(|value: &Value| serialize_number(value))); - map.insert(FLOAT4, Arc::new(|value: &Value| serialize_number(value))); - map.insert(FLOAT8, Arc::new(|value: &Value| serialize_number(value))); - - map.insert(BOOL, Arc::new(|value: &Value| serialize_bool(value))); - map.insert(JSON, Arc::new(|value: &Value| serialize_json(value))); - map.insert(JSONB, Arc::new(|value: &Value| serialize_json(value))); - map.insert(BYTEA, Arc::new(|value: &Value| serialize_bytea(value))); - map.insert(UUID, Arc::new(|value: &Value| serialize_string(value))); - map.insert(TIMESTAMP, Arc::new(|value: &Value| serialize_string(value))); - map.insert( - TIMESTAMPTZ, - Arc::new(|value: &Value| serialize_string(value)), - ); - map.insert(DATE, Arc::new(|value: &Value| serialize_string(value))); - - register_builtin_array_serializers(&mut map); - map -} - -pub fn register_array_type( - parsers: &mut ParserMap, - serializers: &mut SerializerMap, - info: ArrayTypeInfo, -) { - register_array_parser(parsers, info); - register_array_serializer(serializers, info); -} - -fn register_array_parser(parsers: &mut ParserMap, info: ArrayTypeInfo) { - let element_parser = parsers.get(&info.element_oid).cloned(); - let element_oid = info.element_oid; - let delimiter = info.delimiter; - let array_parser: TypeParser = Arc::new(move |text: &str, _| { - parse_array_text(text, element_parser.clone(), element_oid, delimiter) - }); - parsers.insert(info.array_oid, array_parser); -} - -fn register_array_serializer(serializers: &mut SerializerMap, info: ArrayTypeInfo) { - let element_serializer = serializers.get(&info.element_oid).cloned(); - let delimiter = info.delimiter; - let array_serializer: Serializer = Arc::new(move |value: &Value| { - serialize_array_value(value, element_serializer.clone(), delimiter) - }); - serializers.insert(info.array_oid, array_serializer); -} - -fn register_builtin_array_parsers(parsers: &mut ParserMap) { - for info in BUILTIN_ARRAY_TYPES { - register_array_parser(parsers, *info); - } -} - -fn register_builtin_array_serializers(serializers: &mut SerializerMap) { - for info in BUILTIN_ARRAY_TYPES { - register_array_serializer(serializers, *info); - } -} - -// Generated from PostgreSQL's built-in pg_type.dat OID assignments for the -// default PGlite/Postgres 17 catalog. Keep this list to built-in types only: -// extension and runtime-created custom arrays are discovered through the direct -// client type cache when they are actually used. -const BUILTIN_ARRAY_TYPES: &[ArrayTypeInfo] = &[ - ArrayTypeInfo::new(16, 1000, ','), - ArrayTypeInfo::new(17, 1001, ','), - ArrayTypeInfo::new(18, 1002, ','), - ArrayTypeInfo::new(19, 1003, ','), - ArrayTypeInfo::new(20, 1016, ','), - ArrayTypeInfo::new(21, 1005, ','), - ArrayTypeInfo::new(22, 1006, ','), - ArrayTypeInfo::new(23, 1007, ','), - ArrayTypeInfo::new(24, 1008, ','), - ArrayTypeInfo::new(25, 1009, ','), - ArrayTypeInfo::new(26, 1028, ','), - ArrayTypeInfo::new(27, 1010, ','), - ArrayTypeInfo::new(28, 1011, ','), - ArrayTypeInfo::new(29, 1012, ','), - ArrayTypeInfo::new(30, 1013, ','), - ArrayTypeInfo::new(114, 199, ','), - ArrayTypeInfo::new(142, 143, ','), - ArrayTypeInfo::new(600, 1017, ','), - ArrayTypeInfo::new(601, 1018, ','), - ArrayTypeInfo::new(602, 1019, ','), - ArrayTypeInfo::new(603, 1020, ';'), - ArrayTypeInfo::new(604, 1027, ','), - ArrayTypeInfo::new(628, 629, ','), - ArrayTypeInfo::new(700, 1021, ','), - ArrayTypeInfo::new(701, 1022, ','), - ArrayTypeInfo::new(718, 719, ','), - ArrayTypeInfo::new(790, 791, ','), - ArrayTypeInfo::new(829, 1040, ','), - ArrayTypeInfo::new(869, 1041, ','), - ArrayTypeInfo::new(650, 651, ','), - ArrayTypeInfo::new(774, 775, ','), - ArrayTypeInfo::new(1033, 1034, ','), - ArrayTypeInfo::new(1042, 1014, ','), - ArrayTypeInfo::new(1043, 1015, ','), - ArrayTypeInfo::new(1082, 1182, ','), - ArrayTypeInfo::new(1083, 1183, ','), - ArrayTypeInfo::new(1114, 1115, ','), - ArrayTypeInfo::new(1184, 1185, ','), - ArrayTypeInfo::new(1186, 1187, ','), - ArrayTypeInfo::new(1266, 1270, ','), - ArrayTypeInfo::new(1560, 1561, ','), - ArrayTypeInfo::new(1562, 1563, ','), - ArrayTypeInfo::new(1700, 1231, ','), - ArrayTypeInfo::new(1790, 2201, ','), - ArrayTypeInfo::new(2202, 2207, ','), - ArrayTypeInfo::new(2203, 2208, ','), - ArrayTypeInfo::new(2204, 2209, ','), - ArrayTypeInfo::new(2205, 2210, ','), - ArrayTypeInfo::new(4191, 4192, ','), - ArrayTypeInfo::new(2206, 2211, ','), - ArrayTypeInfo::new(4096, 4097, ','), - ArrayTypeInfo::new(4089, 4090, ','), - ArrayTypeInfo::new(2950, 2951, ','), - ArrayTypeInfo::new(3220, 3221, ','), - ArrayTypeInfo::new(3614, 3643, ','), - ArrayTypeInfo::new(3642, 3644, ','), - ArrayTypeInfo::new(3615, 3645, ','), - ArrayTypeInfo::new(3734, 3735, ','), - ArrayTypeInfo::new(3769, 3770, ','), - ArrayTypeInfo::new(3802, 3807, ','), - ArrayTypeInfo::new(4072, 4073, ','), - ArrayTypeInfo::new(2970, 2949, ','), - ArrayTypeInfo::new(5038, 5039, ','), - ArrayTypeInfo::new(3904, 3905, ','), - ArrayTypeInfo::new(3906, 3907, ','), - ArrayTypeInfo::new(3908, 3909, ','), - ArrayTypeInfo::new(3910, 3911, ','), - ArrayTypeInfo::new(3912, 3913, ','), - ArrayTypeInfo::new(3926, 3927, ','), - ArrayTypeInfo::new(4451, 6150, ','), - ArrayTypeInfo::new(4532, 6151, ','), - ArrayTypeInfo::new(4533, 6152, ','), - ArrayTypeInfo::new(4534, 6153, ','), - ArrayTypeInfo::new(4535, 6155, ','), - ArrayTypeInfo::new(4536, 6157, ','), - ArrayTypeInfo::new(2275, 1263, ','), -]; - -fn parse_int(value: &str) -> Value { - match value.parse::() { - Ok(int) => json!(int), - Err(_) => json!(value.to_string()), - } -} - -fn parse_bigint(value: &str) -> Value { - match value.parse::() { - Ok(int) => json!(int), - Err(_) => json!(value.to_string()), - } -} - -fn parse_numeric(value: &str) -> Value { - serde_json::Number::from_str(value) - .map(Value::Number) - .unwrap_or_else(|_| json!(value.to_string())) -} - -fn parse_float(value: &str) -> Value { - match value.parse::() { - Ok(float) => json!(float), - Err(_) => json!(value.to_string()), - } -} - -fn parse_json(value: &str) -> Value { - serde_json::from_str(value).unwrap_or_else(|_| json!(value.to_string())) -} - -fn parse_bytea(value: &str) -> Value { - value - .strip_prefix("\\x") - .and_then(|hex| hex::decode(hex).ok()) - .map(Value::from) - .unwrap_or_else(|| json!(value.to_string())) -} - -fn serialize_string(value: &Value) -> Result { - match value { - Value::String(s) => Ok(s.clone()), - other => Ok(other.to_string()), - } -} - -fn serialize_number(value: &Value) -> Result { - match value { - Value::Number(num) => Ok(num.to_string()), - Value::String(s) => Ok(s.clone()), - other => Err(anyhow!("cannot serialize value {other} as number")), - } -} - -fn serialize_bool(value: &Value) -> Result { - match value { - Value::Bool(b) => Ok(if *b { "t" } else { "f" }.to_string()), - Value::Number(num) => Ok(if num.as_i64().unwrap_or(0) != 0 { - "t" - } else { - "f" - } - .to_string()), - Value::String(s) => Ok(match s.as_ref() { - "true" | "t" | "1" => "t".to_string(), - _ => "f".to_string(), - }), - other => Err(anyhow!("cannot serialize value {other} as boolean")), - } -} - -fn serialize_json(value: &Value) -> Result { - if let Some(value) = value.as_str() { - Ok(value.to_string()) - } else { - serde_json::to_string(value).map_err(|err| anyhow!(err)) - } -} - -fn serialize_bytea(value: &Value) -> Result { - match value { - Value::String(s) => Ok(s.clone()), - Value::Array(arr) => { - let bytes: Vec = arr - .iter() - .filter_map(|v| v.as_u64().map(|n| n as u8)) - .collect(); - Ok(format!("\\x{}", hex::encode(bytes))) - } - Value::Null => Ok("\\x".to_string()), - _ => Err(anyhow!("unsupported value for bytea serialization")), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_multidimensional_arrays_without_separator_artifacts() { - let parser: TypeParser = Arc::new(|value, _| parse_float(value)); - let parsed = parse_array_text("{{1.5,2.5},{3.5,4.5}}", Some(parser), FLOAT8, ','); - assert_eq!(parsed, json!([[1.5, 2.5], [3.5, 4.5]])); - } - - #[test] - fn parses_quoted_array_values_and_unquoted_nulls() { - let parsed = parse_array_text( - r#"{"comma,value","quote \" value",NULL,"NULL",""}"#, - None, - TEXT, - ',', - ); - assert_eq!( - parsed, - json!(["comma,value", "quote \" value", null, "NULL", ""]) - ); - } -} diff --git a/src/postgres/versions/18/fetch-source.sh b/src/postgres/versions/18/fetch-source.sh new file mode 100644 index 00000000..34dabe79 --- /dev/null +++ b/src/postgres/versions/18/fetch-source.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env sh + +# Shared, fail-closed transport for the pinned PostgreSQL source archive. +# Keep this file POSIX-compatible: native and WASIX build scripts source it on +# both macOS and Linux. + +oliphaunt_postgresql_sha256_file() ( + oliphaunt_sha_path="${1:?oliphaunt_postgresql_sha256_file requires a path}" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$oliphaunt_sha_path" | awk '{print $1}' + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "$oliphaunt_sha_path" | awk '{print $1}' + else + echo "PostgreSQL source fetch requires shasum or sha256sum" >&2 + return 127 + fi +) + +oliphaunt_fetch_postgresql_source_archive() ( + if [ "$#" -ne 4 ]; then + echo "usage: oliphaunt_fetch_postgresql_source_archive DESTINATION VERSION SHA256 PRIMARY_URL" >&2 + return 2 + fi + + oliphaunt_destination="$1" + oliphaunt_version="$2" + oliphaunt_expected_sha="$(printf '%s' "$3" | tr 'A-F' 'a-f')" + oliphaunt_primary_url="$4" + oliphaunt_fallback_url="https://fossies.org/linux/misc/postgresql-${oliphaunt_version}.tar.bz2" + + if [ -z "$oliphaunt_destination" ]; then + echo "PostgreSQL source destination must not be empty" >&2 + return 2 + fi + case "$oliphaunt_version" in + ""|.*|*.|*..*|*[!0-9.]*) + echo "invalid PostgreSQL source version: $oliphaunt_version" >&2 + return 2 + ;; + esac + if [ "${#oliphaunt_expected_sha}" -ne 64 ]; then + echo "PostgreSQL source SHA-256 must contain exactly 64 hexadecimal characters" >&2 + return 2 + fi + case "$oliphaunt_expected_sha" in + *[!0-9a-f]*) + echo "PostgreSQL source SHA-256 must contain only hexadecimal characters" >&2 + return 2 + ;; + esac + for oliphaunt_url in "$oliphaunt_primary_url" "$oliphaunt_fallback_url"; do + case "$oliphaunt_url" in + https://*) ;; + *) + echo "refusing non-HTTPS PostgreSQL source URL: $oliphaunt_url" >&2 + return 2 + ;; + esac + done + + for oliphaunt_required_command in curl mktemp; do + if ! command -v "$oliphaunt_required_command" >/dev/null 2>&1; then + echo "PostgreSQL source fetch requires $oliphaunt_required_command" >&2 + return 127 + fi + done + if ! command -v shasum >/dev/null 2>&1 && ! command -v sha256sum >/dev/null 2>&1; then + echo "PostgreSQL source fetch requires shasum or sha256sum" >&2 + return 127 + fi + + if [ -f "$oliphaunt_destination" ]; then + oliphaunt_actual_sha="$(oliphaunt_postgresql_sha256_file "$oliphaunt_destination")" + oliphaunt_actual_sha="$(printf '%s' "$oliphaunt_actual_sha" | tr 'A-F' 'a-f')" + if [ "$oliphaunt_actual_sha" = "$oliphaunt_expected_sha" ]; then + return 0 + fi + echo "discarding cached PostgreSQL $oliphaunt_version source with checksum $oliphaunt_actual_sha instead of $oliphaunt_expected_sha" >&2 + rm -f "$oliphaunt_destination" + fi + + oliphaunt_destination_dir="$(dirname "$oliphaunt_destination")" + mkdir -p "$oliphaunt_destination_dir" + umask 077 + oliphaunt_partial="$(mktemp "${oliphaunt_destination}.partial.XXXXXX")" + trap 'rm -f "$oliphaunt_partial"' 0 + trap 'exit 129' HUP + trap 'exit 130' INT + trap 'exit 143' TERM + + for oliphaunt_url in "$oliphaunt_primary_url" "$oliphaunt_fallback_url"; do + rm -f "$oliphaunt_partial" + oliphaunt_curl_status=0 + if curl \ + --location \ + --fail \ + --silent \ + --show-error \ + --retry 4 \ + --retry-all-errors \ + --retry-delay 3 \ + --retry-max-time 90 \ + --connect-timeout 20 \ + --max-time 60 \ + --max-filesize 67108864 \ + --proto '=https' \ + --proto-redir '=https' \ + --tlsv1.2 \ + --output "$oliphaunt_partial" \ + "$oliphaunt_url"; then + oliphaunt_actual_sha="$(oliphaunt_postgresql_sha256_file "$oliphaunt_partial")" + oliphaunt_actual_sha="$(printf '%s' "$oliphaunt_actual_sha" | tr 'A-F' 'a-f')" + if [ "$oliphaunt_actual_sha" = "$oliphaunt_expected_sha" ]; then + mv -f "$oliphaunt_partial" "$oliphaunt_destination" + return 0 + fi + echo "discarding PostgreSQL $oliphaunt_version source from $oliphaunt_url with checksum $oliphaunt_actual_sha instead of $oliphaunt_expected_sha" >&2 + else + oliphaunt_curl_status=$? + echo "PostgreSQL $oliphaunt_version source download from $oliphaunt_url failed after bounded retries (curl exit $oliphaunt_curl_status)" >&2 + fi + done + + rm -f "$oliphaunt_partial" + echo "failed to download verified PostgreSQL $oliphaunt_version source from every pinned HTTPS location" >&2 + return 1 +) diff --git a/src/postgres/versions/18/fetch-source.test.sh b/src/postgres/versions/18/fetch-source.test.sh new file mode 100644 index 00000000..c72e761f --- /dev/null +++ b/src/postgres/versions/18/fetch-source.test.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/fetch-source.sh" + +fail() { + echo "fetch-source.test.sh: $*" >&2 + exit 1 +} + +sha256_file() { + oliphaunt_postgresql_sha256_file "$1" +} + +assert_verified_file() { + local path="$1" + local expected="$2" + [[ -f "$path" ]] || fail "missing verified destination $path" + [[ "$(sha256_file "$path")" == "$expected" ]] || fail "unexpected checksum for $path" +} + +assert_no_partials() { + local root="$1" + local partial + partial="$(find "$root" -type f -name '*.partial.*' -print -quit)" + [[ -z "$partial" ]] || fail "partial transport file was not removed: $partial" +} + +assert_transport_flags() { + local log="$1" + for expected in \ + '--location' \ + '--fail' \ + '--retry 4' \ + '--retry-all-errors' \ + '--retry-delay 3' \ + '--retry-max-time 90' \ + '--connect-timeout 20' \ + '--max-time 60' \ + '--max-filesize 67108864' \ + '--proto =https' \ + '--proto-redir =https' \ + '--tlsv1.2'; do + grep -F -- "$expected" "$log" >/dev/null || fail "curl invocation omitted $expected" + done +} + +work_root="$(mktemp -d)" +cleanup() { + rm -rf "$work_root" +} +trap cleanup EXIT HUP INT TERM + +fixture="$work_root/postgresql-18.4.tar.bz2" +printf 'verified PostgreSQL source fixture\n' > "$fixture" +fixture_sha="$(sha256_file "$fixture")" +fake_bin="$work_root/fake-bin" +mkdir -p "$fake_bin" +cp "$script_dir/testdata/curl" "$fake_bin/curl" +chmod 0755 "$fake_bin/curl" +fake_path="$fake_bin:$PATH" +primary_url="https://primary.invalid/postgresql-18.4.tar.bz2" +fallback_url="https://fossies.org/linux/misc/postgresql-18.4.tar.bz2" + +cached_destination="$work_root/cached/postgresql-18.4.tar.bz2" +mkdir -p "$(dirname "$cached_destination")" +cp "$fixture" "$cached_destination" +cached_log="$work_root/cached-curl.log" +OLIPHAUNT_FETCH_TEST_LOG="$cached_log" \ +OLIPHAUNT_FETCH_TEST_MODE=all-fail \ +OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ +PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$cached_destination" 18.4 "$fixture_sha" "$primary_url" +[[ ! -e "$cached_log" ]] || fail "verified cached source unexpectedly used curl" +assert_verified_file "$cached_destination" "$fixture_sha" + +fallback_destination="$work_root/fallback/postgresql-18.4.tar.bz2" +mkdir -p "$(dirname "$fallback_destination")" +printf 'corrupt cached bytes\n' > "$fallback_destination" +fallback_log="$work_root/fallback-curl.log" +OLIPHAUNT_FETCH_TEST_LOG="$fallback_log" \ +OLIPHAUNT_FETCH_TEST_MODE=primary-fails \ +OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ +OLIPHAUNT_FETCH_TEST_FINAL="$fallback_destination" \ +PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$fallback_destination" 18.4 "$fixture_sha" "$primary_url" +assert_verified_file "$fallback_destination" "$fixture_sha" +[[ "$(wc -l < "$fallback_log" | tr -d ' ')" == 2 ]] || fail "transport did not try exactly the primary and fallback URLs" +grep -F -- "$primary_url" "$fallback_log" >/dev/null || fail "primary URL was not attempted" +grep -F -- "$fallback_url" "$fallback_log" >/dev/null || fail "fallback URL was not attempted" +assert_transport_flags "$fallback_log" +assert_no_partials "$work_root/fallback" + +checksum_destination="$work_root/checksum/postgresql-18.4.tar.bz2" +checksum_log="$work_root/checksum-curl.log" +OLIPHAUNT_FETCH_TEST_LOG="$checksum_log" \ +OLIPHAUNT_FETCH_TEST_MODE=primary-has-wrong-checksum \ +OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ +OLIPHAUNT_FETCH_TEST_FINAL="$checksum_destination" \ +PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$checksum_destination" 18.4 "$fixture_sha" "$primary_url" +assert_verified_file "$checksum_destination" "$fixture_sha" +[[ "$(wc -l < "$checksum_log" | tr -d ' ')" == 2 ]] || fail "checksum mismatch did not advance exactly once to the fallback" +assert_no_partials "$work_root/checksum" + +failed_destination="$work_root/failed/postgresql-18.4.tar.bz2" +failed_log="$work_root/failed-curl.log" +if OLIPHAUNT_FETCH_TEST_LOG="$failed_log" \ + OLIPHAUNT_FETCH_TEST_MODE=all-fail \ + OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ + OLIPHAUNT_FETCH_TEST_FINAL="$failed_destination" \ + PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$failed_destination" 18.4 "$fixture_sha" "$primary_url" >/dev/null 2>&1; then + fail "all-failed transport unexpectedly succeeded" +fi +[[ ! -e "$failed_destination" ]] || fail "failed transport promoted an unverified final destination" +[[ "$(wc -l < "$failed_log" | tr -d ' ')" == 2 ]] || fail "failed transport exceeded or skipped the two bounded URL attempts" +assert_no_partials "$work_root/failed" + +interrupted_destination="$work_root/interrupted/postgresql-18.4.tar.bz2" +interrupted_log="$work_root/interrupted-curl.log" +if OLIPHAUNT_FETCH_TEST_LOG="$interrupted_log" \ + OLIPHAUNT_FETCH_TEST_MODE=interrupt-parent \ + OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ + OLIPHAUNT_FETCH_TEST_FINAL="$interrupted_destination" \ + PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$interrupted_destination" 18.4 "$fixture_sha" "$primary_url" >/dev/null 2>&1; then + fail "interrupted transport unexpectedly succeeded" +fi +[[ ! -e "$interrupted_destination" ]] || fail "interrupted transport promoted an unverified final destination" +[[ "$(wc -l < "$interrupted_log" | tr -d ' ')" == 1 ]] || fail "interrupted transport continued to a fallback URL" +assert_no_partials "$work_root/interrupted" + +rejected_log="$work_root/rejected-curl.log" +if OLIPHAUNT_FETCH_TEST_LOG="$rejected_log" \ + OLIPHAUNT_FETCH_TEST_MODE=all-fail \ + OLIPHAUNT_FETCH_TEST_FIXTURE="$fixture" \ + PATH="$fake_path" \ + oliphaunt_fetch_postgresql_source_archive "$work_root/rejected/archive" 18.4 "$fixture_sha" http://primary.invalid/postgresql-18.4.tar.bz2 >/dev/null 2>&1; then + fail "non-HTTPS primary URL unexpectedly succeeded" +fi +[[ ! -e "$rejected_log" ]] || fail "non-HTTPS URL reached curl" + +if oliphaunt_fetch_postgresql_source_archive "$work_root/rejected/archive" 18.4 not-a-sha "$primary_url" >/dev/null 2>&1; then + fail "invalid SHA-256 unexpectedly succeeded" +fi +if oliphaunt_fetch_postgresql_source_archive "$work_root/rejected/archive" '../18.4' "$fixture_sha" "$primary_url" >/dev/null 2>&1; then + fail "invalid PostgreSQL version unexpectedly succeeded" +fi + +echo "PostgreSQL source fetch transport tests passed" diff --git a/src/postgres/versions/18/moon.yml b/src/postgres/versions/18/moon.yml new file mode 100644 index 00000000..865d311d --- /dev/null +++ b/src/postgres/versions/18/moon.yml @@ -0,0 +1,28 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "postgres18" +language: "unknown" +layer: "configuration" +stack: "systems" +tags: ["postgres", "source", "pg18"] + +project: + title: "PostgreSQL 18 Source" + description: "Shared PostgreSQL 18.4 source pin and checksum." + owner: "oliphaunt" + +owners: + defaultOwner: "@oliphaunt/core" + paths: + "**/*": ["@oliphaunt/core"] + +tasks: + check: + tags: ["quality", "static"] + command: "bun tools/policy/assertions/assert-source-inputs.mjs postgres18" + inputs: + - "/src/postgres/versions/18/**/*" + - "/tools/policy/assertions/assert-source-inputs.mjs" + options: + cache: true + runFromWorkspaceRoot: true diff --git a/src/postgres/versions/18/source.toml b/src/postgres/versions/18/source.toml new file mode 100644 index 00000000..37ba2c32 --- /dev/null +++ b/src/postgres/versions/18/source.toml @@ -0,0 +1,4 @@ +[postgresql] +version = "18.4" +url = "https://ftp.postgresql.org/pub/source/v18.4/postgresql-18.4.tar.bz2" +sha256 = "81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" diff --git a/src/postgres/versions/18/testdata/curl b/src/postgres/versions/18/testdata/curl new file mode 100644 index 00000000..bea20ba8 --- /dev/null +++ b/src/postgres/versions/18/testdata/curl @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${OLIPHAUNT_FETCH_TEST_LOG:?}" +: "${OLIPHAUNT_FETCH_TEST_MODE:?}" +: "${OLIPHAUNT_FETCH_TEST_FIXTURE:?}" + +printf '%s\n' "$*" >> "$OLIPHAUNT_FETCH_TEST_LOG" + +output="" +url="" +expect_output=0 +for argument in "$@"; do + if [[ "$expect_output" == 1 ]]; then + output="$argument" + expect_output=0 + continue + fi + case "$argument" in + --output) expect_output=1 ;; + https://*) url="$argument" ;; + esac +done + +[[ -n "$output" ]] || { + echo "fake curl did not receive --output" >&2 + exit 90 +} +[[ -n "$url" ]] || { + echo "fake curl did not receive an HTTPS URL" >&2 + exit 90 +} +if [[ -n "${OLIPHAUNT_FETCH_TEST_FINAL:-}" && -e "$OLIPHAUNT_FETCH_TEST_FINAL" ]]; then + echo "final destination existed while transport was writing its partial file" >&2 + exit 91 +fi + +is_primary=0 +[[ "$url" == https://primary.invalid/* ]] && is_primary=1 + +case "$OLIPHAUNT_FETCH_TEST_MODE" in + primary-fails) + if [[ "$is_primary" == 1 ]]; then + printf 'partial transport bytes\n' > "$output" + exit 35 + fi + cp "$OLIPHAUNT_FETCH_TEST_FIXTURE" "$output" + ;; + primary-has-wrong-checksum) + if [[ "$is_primary" == 1 ]]; then + printf 'wrong bytes\n' > "$output" + else + cp "$OLIPHAUNT_FETCH_TEST_FIXTURE" "$output" + fi + ;; + all-fail) + printf 'partial transport bytes\n' > "$output" + exit 7 + ;; + interrupt-parent) + printf 'partial transport bytes\n' > "$output" + kill -TERM "$PPID" + sleep 1 + exit 143 + ;; + *) + echo "unsupported fake curl mode: $OLIPHAUNT_FETCH_TEST_MODE" >&2 + exit 92 + ;; +esac diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs deleted file mode 100644 index d8d5054b..00000000 --- a/src/protocol/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![allow(dead_code)] -// The internal protocol layer keeps full PostgreSQL message shapes even when -// the high-level API only consumes a subset of each message today. - -pub(crate) mod buffer_reader; -pub(crate) mod buffer_writer; -pub(crate) mod messages; -pub(crate) mod parser; -pub(crate) mod serializer; -pub(crate) mod string_utils; -pub(crate) mod types; - -#[cfg(test)] -mod tests; - -#[cfg(test)] -use messages::{AuthenticationMessage, BackendMessage, Field, MessageName}; -#[cfg(test)] -use parser::Parser; -#[cfg(test)] -use serializer::{BindConfig, ExecConfig, PortalTarget, Serialize}; -#[cfg(test)] -use string_utils::byte_length_utf8; -#[cfg(test)] -use types::Mode; diff --git a/src/protocol/types.rs b/src/protocol/types.rs deleted file mode 100644 index 04400f86..00000000 --- a/src/protocol/types.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Mode { - Text = 0, - Binary = 1, -} - -impl Mode { - pub fn as_i16(self) -> i16 { - match self { - Mode::Text => 0, - Mode::Binary => 1, - } - } -} - -impl TryFrom for Mode { - type Error = &'static str; - - fn try_from(value: i16) -> Result { - match value { - 0 => Ok(Mode::Text), - 1 => Ok(Mode::Binary), - _ => Err("invalid mode"), - } - } -} - -pub struct Modes; - -impl Modes { - pub const TEXT: Mode = Mode::Text; - pub const BINARY: Mode = Mode::Binary; -} - -pub type BufferParameter<'a> = &'a [u8]; diff --git a/src/runtimes/broker/.release-semantic-inputs.json b/src/runtimes/broker/.release-semantic-inputs.json new file mode 100644 index 00000000..2fcf8cc6 --- /dev/null +++ b/src/runtimes/broker/.release-semantic-inputs.json @@ -0,0 +1,161 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "oliphaunt-broker", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "native-npm-carrier-producer", + "paths": [ + "tools/release/release-product-dry-run.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-product-dry-run.mjs", + "sha256": "5d7f66e141a3d9fbd40e1d6dabb63da82e9120c8265357cbf081d72e8cdecc71" + } + ] + }, + { + "id": "broker-cargo-carrier-producer", + "paths": [ + "tools/release/package_broker_cargo_artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package_broker_cargo_artifacts.mjs", + "sha256": "2a266ce834a2258bb227022991cc46fab84cfe41d204b3c31aa2ee098c86366e" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "shared-native-archive-encoder", + "paths": [ + "tools/release/archive_dir.mjs" + ], + "inputs": [ + { + "path": "tools/release/archive_dir.mjs", + "sha256": "389545cbb7c5512cc762f87edab8c156e7385bdf6449a193186117b48a250fea" + } + ] + }, + { + "id": "broker-release-asset-producers", + "paths": [ + "tools/release/build-linux-broker-baseline.sh", + "tools/release/package-broker-assets.sh" + ], + "inputs": [ + { + "path": "tools/release/build-linux-broker-baseline.sh", + "sha256": "c2207ba48e151346a2c992dc9f9d3f023b2f10b05f5d00034c9851bc7d42b9af" + }, + { + "path": "tools/release/package-broker-assets.sh", + "sha256": "b2b7b45cf84b77e2a6eeda19dcc241d5e5112b2503b40e2d9b81144850606e65" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "runtime-release-checksum-producer", + "paths": [ + "tools/release/write_checksum_manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/write_checksum_manifest.mjs", + "sha256": "acf259694c121cc475efae4a64418bda7817d5d9f32ead3bdeffd65f0b01b840" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "cb4e3b97e9376ad37a3bd815e7dfbb6e61fd6236bacfc69bd2e827500cc33a43" +} diff --git a/src/runtimes/broker/CHANGELOG.md b/src/runtimes/broker/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/runtimes/broker/Cargo.toml b/src/runtimes/broker/Cargo.toml new file mode 100644 index 00000000..fe96513e --- /dev/null +++ b/src/runtimes/broker/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-broker" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Oliphaunt broker helper process for process-isolated embedded PostgreSQL." +readme = "README.md" +repository.workspace = true +homepage.workspace = true +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false + +[[bin]] +name = "oliphaunt-broker" +path = "src/main.rs" + +[dependencies] +oliphaunt = { path = "../../sdks/rust", version = "=0.0.0" } diff --git a/src/runtimes/broker/README.md b/src/runtimes/broker/README.md new file mode 100644 index 00000000..109d296b --- /dev/null +++ b/src/runtimes/broker/README.md @@ -0,0 +1,9 @@ +# oliphaunt-broker + +`oliphaunt-broker` is the helper process used by broker mode. It owns one +native database root per process, serves the Oliphaunt broker IPC protocol, and +is packaged as platform-specific release assets for SDKs that need process +isolation. + +Application developers should use their SDK's broker mode instead of invoking +this binary directly. diff --git a/src/runtimes/broker/crates/linux-arm64-gnu/Cargo.toml b/src/runtimes/broker/crates/linux-arm64-gnu/Cargo.toml new file mode 100644 index 00000000..02077773 --- /dev/null +++ b/src/runtimes/broker/crates/linux-arm64-gnu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-broker-linux-arm64-gnu" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the linux-arm64-gnu oliphaunt-broker helper binary." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_broker_linux_arm64_gnu" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[workspace] diff --git a/src/runtimes/broker/crates/linux-arm64-gnu/README.md b/src/runtimes/broker/crates/linux-arm64-gnu/README.md new file mode 100644 index 00000000..64b4fdb4 --- /dev/null +++ b/src/runtimes/broker/crates/linux-arm64-gnu/README.md @@ -0,0 +1,9 @@ +# oliphaunt-broker-linux-arm64-gnu + +Cargo artifact crate for the `linux-arm64-gnu` `oliphaunt-broker` helper +binary. Applications do not depend on this crate directly; `oliphaunt` selects +it for matching Cargo targets. + +The complete Oliphaunt Linux GNU surface is qualified at glibc 2.38 with a +`GLIBCXX_3.4.30` ceiling. This target ID is an ABI contract, not a claim of +support for every Linux distribution. diff --git a/src/runtimes/broker/crates/linux-arm64-gnu/build.rs b/src/runtimes/broker/crates/linux-arm64-gnu/build.rs new file mode 100644 index 00000000..aeff05c7 --- /dev/null +++ b/src/runtimes/broker/crates/linux-arm64-gnu/build.rs @@ -0,0 +1,46 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +const SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const PRODUCT: &str = "oliphaunt-broker"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "broker-helper"; +const TARGET: &str = "aarch64-unknown-linux-gnu"; +const RELATIVE: &str = "bin/oliphaunt-broker"; + +fn main() { + emit_manifest(); +} + +fn emit_manifest() { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let source = manifest_dir.join("payload").join(RELATIVE); + let checksum = manifest_dir.join("payload").join("sha256"); + println!("cargo::rerun-if-changed={}", source.display()); + println!("cargo::rerun-if-changed={}", checksum.display()); + if !source.is_file() || !checksum.is_file() { + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!( + "missing packaged oliphaunt-broker payload or checksum under {}", + manifest_dir.join("payload").display() + ); + } + return; + } + let sha256 = fs::read_to_string(&checksum) + .expect("read packaged oliphaunt-broker payload checksum"); + let sha256 = sha256.trim(); + if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) { + panic!("invalid packaged oliphaunt-broker payload checksum"); + } + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let text = format!( + "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\n\n[[files]]\nsource = {:?}\nrelative = {RELATIVE:?}\nsha256 = {sha256:?}\nexecutable = true\n", + source.display().to_string(), + ); + fs::write(&manifest, text).expect("write oliphaunt-broker artifact manifest"); + println!("cargo::metadata=manifest={}", manifest.display()); +} diff --git a/src/runtimes/broker/crates/linux-arm64-gnu/src/lib.rs b/src/runtimes/broker/crates/linux-arm64-gnu/src/lib.rs new file mode 100644 index 00000000..ae076701 --- /dev/null +++ b/src/runtimes/broker/crates/linux-arm64-gnu/src/lib.rs @@ -0,0 +1,5 @@ +pub const PRODUCT: &str = "oliphaunt-broker"; +pub const KIND: &str = "broker-helper"; +pub const RELEASE_TARGET: &str = "linux-arm64-gnu"; +pub const CARGO_TARGET: &str = "aarch64-unknown-linux-gnu"; +pub const EXECUTABLE_RELATIVE_PATH: &str = "bin/oliphaunt-broker"; diff --git a/src/runtimes/broker/crates/linux-x64-gnu/Cargo.toml b/src/runtimes/broker/crates/linux-x64-gnu/Cargo.toml new file mode 100644 index 00000000..94f62138 --- /dev/null +++ b/src/runtimes/broker/crates/linux-x64-gnu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-broker-linux-x64-gnu" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the linux-x64-gnu oliphaunt-broker helper binary." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_broker_linux_x64_gnu" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[workspace] diff --git a/src/runtimes/broker/crates/linux-x64-gnu/README.md b/src/runtimes/broker/crates/linux-x64-gnu/README.md new file mode 100644 index 00000000..749e0a95 --- /dev/null +++ b/src/runtimes/broker/crates/linux-x64-gnu/README.md @@ -0,0 +1,9 @@ +# oliphaunt-broker-linux-x64-gnu + +Cargo artifact crate for the `linux-x64-gnu` `oliphaunt-broker` helper binary. +Applications do not depend on this crate directly; `oliphaunt` selects it for +matching Cargo targets. + +The complete Oliphaunt Linux GNU surface is qualified at glibc 2.38 with a +`GLIBCXX_3.4.30` ceiling. This target ID is an ABI contract, not a claim of +support for every Linux distribution. diff --git a/src/runtimes/broker/crates/linux-x64-gnu/build.rs b/src/runtimes/broker/crates/linux-x64-gnu/build.rs new file mode 100644 index 00000000..6f26fae6 --- /dev/null +++ b/src/runtimes/broker/crates/linux-x64-gnu/build.rs @@ -0,0 +1,46 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +const SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const PRODUCT: &str = "oliphaunt-broker"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "broker-helper"; +const TARGET: &str = "x86_64-unknown-linux-gnu"; +const RELATIVE: &str = "bin/oliphaunt-broker"; + +fn main() { + emit_manifest(); +} + +fn emit_manifest() { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let source = manifest_dir.join("payload").join(RELATIVE); + let checksum = manifest_dir.join("payload").join("sha256"); + println!("cargo::rerun-if-changed={}", source.display()); + println!("cargo::rerun-if-changed={}", checksum.display()); + if !source.is_file() || !checksum.is_file() { + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!( + "missing packaged oliphaunt-broker payload or checksum under {}", + manifest_dir.join("payload").display() + ); + } + return; + } + let sha256 = fs::read_to_string(&checksum) + .expect("read packaged oliphaunt-broker payload checksum"); + let sha256 = sha256.trim(); + if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) { + panic!("invalid packaged oliphaunt-broker payload checksum"); + } + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let text = format!( + "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\n\n[[files]]\nsource = {:?}\nrelative = {RELATIVE:?}\nsha256 = {sha256:?}\nexecutable = true\n", + source.display().to_string(), + ); + fs::write(&manifest, text).expect("write oliphaunt-broker artifact manifest"); + println!("cargo::metadata=manifest={}", manifest.display()); +} diff --git a/src/runtimes/broker/crates/linux-x64-gnu/src/lib.rs b/src/runtimes/broker/crates/linux-x64-gnu/src/lib.rs new file mode 100644 index 00000000..72eae5bb --- /dev/null +++ b/src/runtimes/broker/crates/linux-x64-gnu/src/lib.rs @@ -0,0 +1,5 @@ +pub const PRODUCT: &str = "oliphaunt-broker"; +pub const KIND: &str = "broker-helper"; +pub const RELEASE_TARGET: &str = "linux-x64-gnu"; +pub const CARGO_TARGET: &str = "x86_64-unknown-linux-gnu"; +pub const EXECUTABLE_RELATIVE_PATH: &str = "bin/oliphaunt-broker"; diff --git a/src/runtimes/broker/crates/macos-arm64/Cargo.toml b/src/runtimes/broker/crates/macos-arm64/Cargo.toml new file mode 100644 index 00000000..b60e5891 --- /dev/null +++ b/src/runtimes/broker/crates/macos-arm64/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-broker-macos-arm64" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the macos-arm64 oliphaunt-broker helper binary." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_broker_macos_arm64" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[workspace] diff --git a/src/runtimes/broker/crates/macos-arm64/README.md b/src/runtimes/broker/crates/macos-arm64/README.md new file mode 100644 index 00000000..35ddaf22 --- /dev/null +++ b/src/runtimes/broker/crates/macos-arm64/README.md @@ -0,0 +1,5 @@ +# oliphaunt-broker-macos-arm64 + +Cargo artifact crate for the `macos-arm64` `oliphaunt-broker` helper binary. +Applications do not depend on this crate directly; `oliphaunt` selects it for +matching Cargo targets. diff --git a/src/runtimes/broker/crates/macos-arm64/build.rs b/src/runtimes/broker/crates/macos-arm64/build.rs new file mode 100644 index 00000000..19d33f48 --- /dev/null +++ b/src/runtimes/broker/crates/macos-arm64/build.rs @@ -0,0 +1,46 @@ +use std::env; +use std::fs; +use std::path::PathBuf; + +const SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const PRODUCT: &str = "oliphaunt-broker"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "broker-helper"; +const TARGET: &str = "aarch64-apple-darwin"; +const RELATIVE: &str = "bin/oliphaunt-broker"; + +fn main() { + emit_manifest(); +} + +fn emit_manifest() { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let source = manifest_dir.join("payload").join(RELATIVE); + let checksum = manifest_dir.join("payload").join("sha256"); + println!("cargo::rerun-if-changed={}", source.display()); + println!("cargo::rerun-if-changed={}", checksum.display()); + if !source.is_file() || !checksum.is_file() { + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!( + "missing packaged oliphaunt-broker payload or checksum under {}", + manifest_dir.join("payload").display() + ); + } + return; + } + let sha256 = fs::read_to_string(&checksum) + .expect("read packaged oliphaunt-broker payload checksum"); + let sha256 = sha256.trim(); + if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) { + panic!("invalid packaged oliphaunt-broker payload checksum"); + } + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let text = format!( + "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\n\n[[files]]\nsource = {:?}\nrelative = {RELATIVE:?}\nsha256 = {sha256:?}\nexecutable = true\n", + source.display().to_string(), + ); + fs::write(&manifest, text).expect("write oliphaunt-broker artifact manifest"); + println!("cargo::metadata=manifest={}", manifest.display()); +} diff --git a/src/runtimes/broker/crates/macos-arm64/src/lib.rs b/src/runtimes/broker/crates/macos-arm64/src/lib.rs new file mode 100644 index 00000000..8f1c0879 --- /dev/null +++ b/src/runtimes/broker/crates/macos-arm64/src/lib.rs @@ -0,0 +1,5 @@ +pub const PRODUCT: &str = "oliphaunt-broker"; +pub const KIND: &str = "broker-helper"; +pub const RELEASE_TARGET: &str = "macos-arm64"; +pub const CARGO_TARGET: &str = "aarch64-apple-darwin"; +pub const EXECUTABLE_RELATIVE_PATH: &str = "bin/oliphaunt-broker"; diff --git a/src/runtimes/broker/crates/windows-x64-msvc/Cargo.toml b/src/runtimes/broker/crates/windows-x64-msvc/Cargo.toml new file mode 100644 index 00000000..69ec8e91 --- /dev/null +++ b/src/runtimes/broker/crates/windows-x64-msvc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-broker-windows-x64-msvc" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the windows-x64-msvc oliphaunt-broker helper binary." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_broker_windows_x64_msvc" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[workspace] diff --git a/src/runtimes/broker/crates/windows-x64-msvc/README.md b/src/runtimes/broker/crates/windows-x64-msvc/README.md new file mode 100644 index 00000000..f0b9abf6 --- /dev/null +++ b/src/runtimes/broker/crates/windows-x64-msvc/README.md @@ -0,0 +1,11 @@ +# oliphaunt-broker-windows-x64-msvc + +Cargo artifact crate for the `windows-x64-msvc` `oliphaunt-broker` helper +binary. Applications do not depend on this crate directly; `oliphaunt` selects +it for matching Cargo targets. + +The packaged helper includes only its import-derived, app-local Microsoft Visual +C++ runtime DLL closure plus a SHA-256 receipt. Those unmodified files come from +the initialized Visual Studio `VC/Redist/MSVC/.../x64/Microsoft.VC145.CRT` +directory and remain subject to the Microsoft Visual Studio redistribution +terms: . diff --git a/src/runtimes/broker/crates/windows-x64-msvc/build.rs b/src/runtimes/broker/crates/windows-x64-msvc/build.rs new file mode 100644 index 00000000..e5d1899d --- /dev/null +++ b/src/runtimes/broker/crates/windows-x64-msvc/build.rs @@ -0,0 +1,137 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::env; +use std::fs; +use std::path::PathBuf; + +const SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const PRODUCT: &str = "oliphaunt-broker"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "broker-helper"; +const TARGET: &str = "x86_64-pc-windows-msvc"; +const RELATIVE: &str = "bin/oliphaunt-broker.exe"; +const VC_RUNTIME_RECEIPT: &str = "bin/windows-vc-runtime.sha256"; +const ALLOWED_VC_RUNTIME_DLLS: [&str; 3] = + ["msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll"]; + +fn main() { + emit_manifest(); +} + +fn valid_sha256(value: &str) -> bool { + value.len() == 64 + && value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) +} + +fn parse_checksum_lines(text: &str, label: &str) -> BTreeMap { + if !text.ends_with('\n') || text.contains('\r') { + panic!("{label} must use canonical LF-terminated lines"); + } + let mut values = BTreeMap::new(); + let mut previous: Option<&str> = None; + for line in text.lines() { + let (digest, relative) = line + .split_once(" ") + .unwrap_or_else(|| panic!("malformed {label} line")); + if !valid_sha256(digest) + || relative.is_empty() + || relative.contains(" ") + || relative.starts_with('/') + || relative.contains("\\") + || relative + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + { + panic!("invalid {label} entry {relative:?}"); + } + if previous.is_some_and(|value| value >= relative) { + panic!("{label} entries must be unique and bytewise sorted"); + } + previous = Some(relative); + values.insert(relative.to_owned(), digest.to_owned()); + } + values +} + +fn emit_manifest() { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let payload_dir = manifest_dir.join("payload"); + let checksum = payload_dir.join("sha256"); + println!("cargo::rerun-if-changed={}", checksum.display()); + if !checksum.is_file() { + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!( + "missing packaged oliphaunt-broker checksum under {}", + payload_dir.display() + ); + } + return; + } + + let checksum_text = + fs::read_to_string(&checksum).expect("read packaged oliphaunt-broker payload checksums"); + let checksums = parse_checksum_lines(&checksum_text, "payload/sha256"); + let allowed: BTreeSet = [RELATIVE.to_owned(), VC_RUNTIME_RECEIPT.to_owned()] + .into_iter() + .chain( + ALLOWED_VC_RUNTIME_DLLS + .iter() + .map(|name| format!("bin/{name}")), + ) + .collect(); + if !checksums.contains_key(RELATIVE) || !checksums.contains_key(VC_RUNTIME_RECEIPT) { + panic!("Windows broker payload must contain its executable and VC runtime receipt"); + } + if checksums.keys().any(|relative| !allowed.contains(relative)) { + panic!("Windows broker payload checksum contains an undeclared member"); + } + + let receipt_path = payload_dir.join(VC_RUNTIME_RECEIPT); + let receipt_text = + fs::read_to_string(&receipt_path).expect("read packaged Windows VC runtime receipt"); + let receipt = parse_checksum_lines(&receipt_text, VC_RUNTIME_RECEIPT); + if receipt.is_empty() { + panic!("Windows broker VC runtime receipt must not be empty"); + } + for (name, digest) in &receipt { + if !ALLOWED_VC_RUNTIME_DLLS.contains(&name.as_str()) { + panic!("Windows broker VC runtime receipt contains undeclared {name}"); + } + if checksums.get(&format!("bin/{name}")) != Some(digest) { + panic!("Windows broker VC runtime receipt and payload checksum disagree for {name}"); + } + } + let expected_members = receipt + .keys() + .map(|name| format!("bin/{name}")) + .chain([RELATIVE.to_owned(), VC_RUNTIME_RECEIPT.to_owned()]) + .collect::>(); + if checksums.keys().cloned().collect::>() != expected_members { + panic!("Windows broker payload must exactly match its import-derived VC runtime receipt"); + } + + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\n", + ); + for (relative, digest) in checksums { + let source = payload_dir.join(&relative); + println!("cargo::rerun-if-changed={}", source.display()); + if !source.is_file() { + panic!( + "missing packaged oliphaunt-broker payload {}", + source.display() + ); + } + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {relative:?}\nsha256 = {digest:?}\nexecutable = {}\n", + source.display().to_string(), + relative == RELATIVE, + )); + } + fs::write(&manifest, text).expect("write oliphaunt-broker artifact manifest"); + println!("cargo::metadata=manifest={}", manifest.display()); +} diff --git a/src/runtimes/broker/crates/windows-x64-msvc/src/lib.rs b/src/runtimes/broker/crates/windows-x64-msvc/src/lib.rs new file mode 100644 index 00000000..715207c1 --- /dev/null +++ b/src/runtimes/broker/crates/windows-x64-msvc/src/lib.rs @@ -0,0 +1,5 @@ +pub const PRODUCT: &str = "oliphaunt-broker"; +pub const KIND: &str = "broker-helper"; +pub const RELEASE_TARGET: &str = "windows-x64-msvc"; +pub const CARGO_TARGET: &str = "x86_64-pc-windows-msvc"; +pub const EXECUTABLE_RELATIVE_PATH: &str = "bin/oliphaunt-broker.exe"; diff --git a/src/runtimes/broker/moon.yml b/src/runtimes/broker/moon.yml new file mode 100644 index 00000000..a80e6028 --- /dev/null +++ b/src/runtimes/broker/moon.yml @@ -0,0 +1,132 @@ +$schema: "https://moonrepo.dev/schemas/project.json" + +id: "oliphaunt-broker" +language: "rust" +layer: "library" +stack: "systems" +tags: ["runtime", "broker", "native", "postgres", "release-product"] +dependsOn: + - "oliphaunt-rust" + - "liboliphaunt-native" + +project: + title: "Oliphaunt Broker" + description: "Process-isolated broker helper runtime used by Rust and TypeScript SDKs." + owner: "oliphaunt" + release: + component: "oliphaunt-broker" + packagePath: "src/runtimes/broker" + artifactTargets: + preset: "broker-helper" + publishedTargets: + - "linux-arm64-gnu" + - "linux-x64-gnu" + - "macos-arm64" + - "windows-x64-msvc" + +owners: + defaultOwner: "@oliphaunt/broker" + paths: + "**/*.rs": ["@oliphaunt/broker"] + "Cargo.toml": ["@oliphaunt/broker"] + +tasks: + check: + tags: ["quality", "static", "ci-rust"] + command: "cargo check -p oliphaunt-broker --locked" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-broker/check" + deps: + - "oliphaunt-rust:check" + - "liboliphaunt-native:check" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/runtimes/broker/**/*" + - "/src/sdks/rust/**/*" + - "/src/runtimes/liboliphaunt/native/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + test: + tags: ["quality", "unit", "ci-rust"] + command: "cargo test -p oliphaunt-broker --locked" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-broker/test" + deps: + - "oliphaunt-rust:check" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/runtimes/broker/**/*" + - "/src/sdks/rust/**/*" + options: + cache: true + runFromWorkspaceRoot: true + + package: + tags: ["package"] + command: "bash src/runtimes/broker/tools/check-package.sh" + deps: + - "oliphaunt-broker:check" + - "oliphaunt-broker:test" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-broker/package" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/runtimes/broker/**/*" + - "/src/sdks/rust/**/*" + - "/src/runtimes/broker/tools/check-package.sh" + options: + cache: true + runFromWorkspaceRoot: true + + release-check: + tags: ["release", "package"] + command: "true" + deps: + - "oliphaunt-broker:package" + inputs: [] + + release-assets: + tags: ["release", "artifact", "ci-broker-runtime"] + command: "bash tools/release/package-broker-assets.sh" + deps: + - "oliphaunt-broker:check" + - "oliphaunt-broker:test" + env: + CARGO_TARGET_DIR: "target/moon/oliphaunt-broker/release-assets" + OLIPHAUNT_RELEASE_ASSET_PARTIAL: "1" + inputs: + - "/Cargo.lock" + - "/Cargo.toml" + - "/rust-toolchain.toml" + - "/src/runtimes/broker/**/*" + - "/src/sdks/rust/**/*" + - "/tools/release/build-linux-broker-baseline.sh" + - "/tools/release/check-linux-consumer-baseline.sh" + - "/tools/release/linux-abi-baseline.test.mjs" + - "/tools/release/package-broker-assets.sh" + - "/tools/release/check-broker-release-assets.mjs" + - "/tools/release/platform-compatibility-policy.mjs" + - "/tools/release/platform-compatibility-policy.test.mjs" + - "/tools/release/platform-binary-contract.mjs" + - "/tools/release/platform-binary-contract.test.mjs" + - "/tools/release/release-asset-validation.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/policy/moon.mjs" + - "/tools/release/artifact_target_matrix.mjs" + - "/release-please-config.json" + - "/.release-please-manifest.json" + - "/src/**/release.toml" + - "/src/**/targets/**/*.toml" + outputs: + - "/target/oliphaunt-broker/release-assets/**/*" + options: + cache: false + runFromWorkspaceRoot: true diff --git a/src/runtimes/broker/packages/darwin-arm64/README.md b/src/runtimes/broker/packages/darwin-arm64/README.md new file mode 100644 index 00000000..07567690 --- /dev/null +++ b/src/runtimes/broker/packages/darwin-arm64/README.md @@ -0,0 +1,4 @@ +# @oliphaunt/broker-darwin-arm64 + +macOS arm64 broker helper package for `@oliphaunt/ts`. This package is populated +by Oliphaunt release packaging. diff --git a/src/runtimes/broker/packages/darwin-arm64/package.json b/src/runtimes/broker/packages/darwin-arm64/package.json new file mode 100644 index 00000000..5e258cf2 --- /dev/null +++ b/src/runtimes/broker/packages/darwin-arm64/package.json @@ -0,0 +1,38 @@ +{ + "name": "@oliphaunt/broker-darwin-arm64", + "version": "0.0.0", + "description": "macOS arm64 oliphaunt-broker helper binary.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/broker/packages/darwin-arm64" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "optional": true, + "oliphaunt": { + "brokerHelper": "oliphaunt-broker", + "target": "macos-arm64", + "executableRelativePath": "bin/oliphaunt-broker" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./bin/oliphaunt-broker" + ] + }, + "files": [ + "bin", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/broker/packages/linux-arm64-gnu/README.md b/src/runtimes/broker/packages/linux-arm64-gnu/README.md new file mode 100644 index 00000000..293dcb60 --- /dev/null +++ b/src/runtimes/broker/packages/linux-arm64-gnu/README.md @@ -0,0 +1,8 @@ +# @oliphaunt/broker-linux-arm64-gnu + +Linux arm64 glibc broker helper package for `@oliphaunt/ts`. This package is +populated by Oliphaunt release packaging. + +The complete Oliphaunt Linux GNU surface is qualified at glibc 2.38 with a +`GLIBCXX_3.4.30` ceiling. This package name identifies an ABI, not every Linux +distribution. diff --git a/src/runtimes/broker/packages/linux-arm64-gnu/package.json b/src/runtimes/broker/packages/linux-arm64-gnu/package.json new file mode 100644 index 00000000..0402f32d --- /dev/null +++ b/src/runtimes/broker/packages/linux-arm64-gnu/package.json @@ -0,0 +1,41 @@ +{ + "name": "@oliphaunt/broker-linux-arm64-gnu", + "version": "0.0.0", + "description": "Linux arm64 glibc oliphaunt-broker helper binary.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/broker/packages/linux-arm64-gnu" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "oliphaunt": { + "brokerHelper": "oliphaunt-broker", + "target": "linux-arm64-gnu", + "executableRelativePath": "bin/oliphaunt-broker" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./bin/oliphaunt-broker" + ] + }, + "files": [ + "bin", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/broker/packages/linux-x64-gnu/README.md b/src/runtimes/broker/packages/linux-x64-gnu/README.md new file mode 100644 index 00000000..c2e14f4b --- /dev/null +++ b/src/runtimes/broker/packages/linux-x64-gnu/README.md @@ -0,0 +1,8 @@ +# @oliphaunt/broker-linux-x64-gnu + +Linux x64 glibc broker helper package for `@oliphaunt/ts`. This package is +populated by Oliphaunt release packaging. + +The complete Oliphaunt Linux GNU surface is qualified at glibc 2.38 with a +`GLIBCXX_3.4.30` ceiling. This package name identifies an ABI, not every Linux +distribution. diff --git a/src/runtimes/broker/packages/linux-x64-gnu/package.json b/src/runtimes/broker/packages/linux-x64-gnu/package.json new file mode 100644 index 00000000..541fa784 --- /dev/null +++ b/src/runtimes/broker/packages/linux-x64-gnu/package.json @@ -0,0 +1,41 @@ +{ + "name": "@oliphaunt/broker-linux-x64-gnu", + "version": "0.0.0", + "description": "Linux x64 glibc oliphaunt-broker helper binary.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/broker/packages/linux-x64-gnu" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "oliphaunt": { + "brokerHelper": "oliphaunt-broker", + "target": "linux-x64-gnu", + "executableRelativePath": "bin/oliphaunt-broker" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./bin/oliphaunt-broker" + ] + }, + "files": [ + "bin", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/broker/packages/win32-x64-msvc/README.md b/src/runtimes/broker/packages/win32-x64-msvc/README.md new file mode 100644 index 00000000..cec48e73 --- /dev/null +++ b/src/runtimes/broker/packages/win32-x64-msvc/README.md @@ -0,0 +1,9 @@ +# @oliphaunt/broker-win32-x64-msvc + +Windows x64 MSVC broker helper package for `@oliphaunt/ts`. This package is +populated by Oliphaunt release packaging. + +The package includes only the helper's import-derived app-local Microsoft Visual +C++ runtime closure, with a SHA-256 receipt. The unmodified DLLs remain subject +to the [Microsoft Visual Studio redistribution +terms](https://learn.microsoft.com/visualstudio/releases/2022/redistribution). diff --git a/src/runtimes/broker/packages/win32-x64-msvc/package.json b/src/runtimes/broker/packages/win32-x64-msvc/package.json new file mode 100644 index 00000000..54e5bd60 --- /dev/null +++ b/src/runtimes/broker/packages/win32-x64-msvc/package.json @@ -0,0 +1,38 @@ +{ + "name": "@oliphaunt/broker-win32-x64-msvc", + "version": "0.0.0", + "description": "Windows x64 MSVC oliphaunt-broker helper binary.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/broker/packages/win32-x64-msvc" + }, + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "optional": true, + "oliphaunt": { + "brokerHelper": "oliphaunt-broker", + "target": "windows-x64-msvc", + "executableRelativePath": "bin/oliphaunt-broker.exe" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./bin/oliphaunt-broker.exe" + ] + }, + "files": [ + "bin", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/broker/release.toml b/src/runtimes/broker/release.toml new file mode 100644 index 00000000..f3e857f3 --- /dev/null +++ b/src/runtimes/broker/release.toml @@ -0,0 +1,15 @@ +id = "oliphaunt-broker" +owner = "@oliphaunt/broker" +kind = "runtime" +publish_targets = ["github-release-assets", "npm", "crates-io"] +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", +] +release_artifacts = ["broker-helper-binary"] diff --git a/src/runtimes/broker/src/main.rs b/src/runtimes/broker/src/main.rs new file mode 100644 index 00000000..90dde682 --- /dev/null +++ b/src/runtimes/broker/src/main.rs @@ -0,0 +1,599 @@ +use std::env; +use std::fs; +use std::io::{self, Read, Write}; +use std::net::TcpListener; +#[cfg(unix)] +use std::os::unix::net::UnixListener; +use std::path::PathBuf; +use std::process; +use std::sync::Arc; +use std::thread; + +use oliphaunt::{ + BackupArtifact, BackupFormat, BootstrapStrategy, DEFAULT_DATABASE, DEFAULT_USERNAME, + DatabaseRoot, DurabilityProfile, EngineCancel, EngineMode, Extension, NativeDirectConfig, + NativeRuntime, Oliphaunt, OliphauntRuntime, OpenConfig, PostgresStartupGuc, RestoreRequest, + RootLockPolicy, RuntimeFootprintProfile, StorageConfig, +}; + +const ENV_BROKER_AUTH_TOKEN: &str = "OLIPHAUNT_BROKER_AUTH_TOKEN"; + +fn main() { + if let Err(error) = run() { + println!("OLIPHAUNT_BROKER_ERROR {error}"); + process::exit(2); + } +} + +fn run() -> oliphaunt::Result<()> { + let args: Vec = env::args().skip(1).collect(); + if matches!(args.first().map(String::as_str), Some("restore")) { + return RestoreArgs::parse(args.into_iter().skip(1).collect())?.run(); + } + let args = BrokerArgs::parse(args)?; + let config = OpenConfig { + mode: EngineMode::NativeDirect, + storage: StorageConfig { + root: DatabaseRoot::Path(args.root), + bootstrap: args.bootstrap, + lock_policy: RootLockPolicy::ExclusiveProcess, + }, + direct: NativeDirectConfig::default(), + broker: Default::default(), + server: Default::default(), + durability: args.durability, + runtime_footprint: args.runtime_footprint, + startup_gucs: args.startup_gucs, + username: args.username, + database: args.database, + extensions: args.extensions, + }; + config.validate()?; + let mut session = OliphauntRuntime::from_env().open(config)?; + let cancel = session.cancel_handle().ok_or_else(|| { + oliphaunt::Error::Engine( + "native broker direct session does not expose cancellation".to_owned(), + ) + })?; + let listener = BrokerListener::bind(args.endpoint)?; + let cancel_listener = BrokerListener::bind(args.cancel_endpoint)?; + let cancel_ready_endpoint = cancel_listener.ready_endpoint(); + start_cancel_listener(cancel_listener, cancel, args.auth_token.clone()); + println!( + "OLIPHAUNT_BROKER_READY {} cancel={}", + listener.ready_endpoint(), + cancel_ready_endpoint + ); + io::stdout() + .flush() + .map_err(|err| oliphaunt::Error::Engine(format!("flush broker ready line: {err}")))?; + + let mut stream = listener.accept()?; + authenticate_client(&mut stream, &args.auth_token)?; + loop { + let request = oliphaunt::broker_ipc_read_request(&mut stream)?; + match request { + oliphaunt::BrokerIpcRequest::Authenticate(_) => { + oliphaunt::broker_ipc_write_error( + &mut stream, + "broker client is already authenticated".to_owned(), + )?; + break; + } + oliphaunt::BrokerIpcRequest::ExecProtocol(bytes) => { + let response = session.exec_protocol_raw(bytes.into()); + write_broker_response(&mut stream, response.map(|response| response.into_bytes()))?; + } + oliphaunt::BrokerIpcRequest::ExecSimpleQuery(sql) => { + let response = session.exec_simple_query(&sql); + write_broker_response(&mut stream, response.map(|response| response.into_bytes()))?; + } + oliphaunt::BrokerIpcRequest::ExecProtocolStream(bytes) => { + let result = session.exec_protocol_stream(bytes.into(), &mut |chunk| { + oliphaunt::broker_ipc_write_chunk(&mut stream, chunk) + }); + match result { + Ok(()) => oliphaunt::broker_ipc_write_ok(&mut stream, Vec::new())?, + Err(error) => { + oliphaunt::broker_ipc_write_error(&mut stream, error.to_string())? + } + } + } + oliphaunt::BrokerIpcRequest::Checkpoint => { + write_broker_response(&mut stream, session.checkpoint().map(|()| Vec::new()))?; + } + oliphaunt::BrokerIpcRequest::Backup(request) => { + write_broker_response( + &mut stream, + session.backup(request).map(|artifact| artifact.bytes), + )?; + } + oliphaunt::BrokerIpcRequest::Cancel => { + write_broker_response( + &mut stream, + Err(oliphaunt::Error::Engine( + "broker cancellation must use the cancel endpoint".to_owned(), + )), + )?; + } + oliphaunt::BrokerIpcRequest::Close => { + let result = session.close().map(|()| Vec::new()); + write_broker_response(&mut stream, result)?; + break; + } + } + } + Ok(()) +} + +struct RestoreArgs { + root: PathBuf, + artifact: PathBuf, + replace_existing: bool, +} + +impl RestoreArgs { + fn parse(args: Vec) -> oliphaunt::Result { + let mut root = None; + let mut artifact = None; + let mut replace_existing = false; + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--root" => { + root = Some(iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "restore --root requires a filesystem path".to_owned(), + ) + })?); + } + "--artifact" => { + artifact = Some(iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "restore --artifact requires a physical archive path".to_owned(), + ) + })?); + } + "--replace-existing" => replace_existing = true, + _ => { + return Err(oliphaunt::Error::InvalidConfig(format!( + "unknown broker restore argument '{arg}'" + ))); + } + } + } + + Ok(Self { + root: root + .ok_or_else(|| { + oliphaunt::Error::InvalidConfig("restore --root is required".to_owned()) + })? + .into(), + artifact: artifact + .ok_or_else(|| { + oliphaunt::Error::InvalidConfig("restore --artifact is required".to_owned()) + })? + .into(), + replace_existing, + }) + } + + fn run(self) -> oliphaunt::Result<()> { + let bytes = fs::read(&self.artifact).map_err(|err| { + oliphaunt::Error::Engine(format!( + "read restore artifact {}: {err}", + self.artifact.display() + )) + })?; + let artifact = BackupArtifact { + format: BackupFormat::PhysicalArchive, + bytes, + }; + let mut request = RestoreRequest::physical_archive(self.root, artifact); + if self.replace_existing { + request = request.replace_existing(); + } + Oliphaunt::restore_blocking(request)?; + Ok(()) + } +} + +fn start_cancel_listener( + listener: BrokerListener, + cancel: Arc, + expected_token: String, +) { + thread::Builder::new() + .name("oliphaunt-broker-cancel".to_owned()) + .spawn(move || { + loop { + match listener.accept() { + Ok(mut stream) => { + if let Err(error) = + handle_cancel_client(&mut stream, cancel.as_ref(), &expected_token) + { + eprintln!("OLIPHAUNT_BROKER_CANCEL_ERROR {error}"); + } + } + Err(error) => { + eprintln!("OLIPHAUNT_BROKER_CANCEL_ERROR {error}"); + break; + } + } + } + }) + .expect("spawn native broker cancel listener"); +} + +fn handle_cancel_client( + stream: &mut Box, + cancel: &dyn EngineCancel, + expected_token: &str, +) -> oliphaunt::Result<()> { + authenticate_client(stream, expected_token)?; + match oliphaunt::broker_ipc_read_request(stream)? { + oliphaunt::BrokerIpcRequest::Cancel => { + write_broker_response(stream, cancel.cancel().map(|()| Vec::new())) + } + oliphaunt::BrokerIpcRequest::Authenticate(_) => oliphaunt::broker_ipc_write_error( + stream, + "broker cancel client is already authenticated".to_owned(), + ), + _ => oliphaunt::broker_ipc_write_error( + stream, + "broker cancel endpoint only accepts cancellation requests".to_owned(), + ), + } +} + +fn authenticate_client( + stream: &mut Box, + expected_token: &str, +) -> oliphaunt::Result<()> { + match oliphaunt::broker_ipc_read_request(stream)? { + oliphaunt::BrokerIpcRequest::Authenticate(token) if token == expected_token => { + oliphaunt::broker_ipc_write_ok(stream, Vec::new()) + } + oliphaunt::BrokerIpcRequest::Authenticate(_) => { + oliphaunt::broker_ipc_write_error( + stream, + "invalid broker authentication token".to_owned(), + )?; + Err(oliphaunt::Error::Engine( + "invalid broker authentication token".to_owned(), + )) + } + _ => { + oliphaunt::broker_ipc_write_error( + stream, + "broker client must authenticate before sending requests".to_owned(), + )?; + Err(oliphaunt::Error::Engine( + "broker client did not authenticate".to_owned(), + )) + } + } +} + +fn write_broker_response( + stream: &mut impl Write, + result: oliphaunt::Result>, +) -> oliphaunt::Result<()> { + match result { + Ok(bytes) => oliphaunt::broker_ipc_write_ok(stream, bytes), + Err(error) => oliphaunt::broker_ipc_write_error(stream, error.to_string()), + } +} + +struct BrokerArgs { + root: std::path::PathBuf, + endpoint: BrokerListenEndpoint, + cancel_endpoint: BrokerListenEndpoint, + bootstrap: BootstrapStrategy, + durability: DurabilityProfile, + runtime_footprint: RuntimeFootprintProfile, + startup_gucs: Vec, + username: String, + database: String, + extensions: Vec, + auth_token: String, +} + +impl BrokerArgs { + fn parse(args: Vec) -> oliphaunt::Result { + let mut root = None; + let mut endpoint = BrokerListenEndpoint::Tcp("127.0.0.1:0".to_owned()); + let mut cancel_endpoint = BrokerListenEndpoint::Tcp("127.0.0.1:0".to_owned()); + let mut bootstrap = "packaged-template".to_owned(); + let mut initdb = None; + let mut durability = DurabilityProfile::Safe; + let mut runtime_footprint = RuntimeFootprintProfile::Throughput; + let mut startup_gucs = Vec::new(); + let mut username = DEFAULT_USERNAME.to_owned(); + let mut database = DEFAULT_DATABASE.to_owned(); + let mut extensions = Vec::new(); + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_str() { + "--root" => root = iter.next().map(Into::into), + "--listen" => { + let listen = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig("--listen requires an address".to_owned()) + })?; + endpoint = BrokerListenEndpoint::Tcp(listen); + } + "--cancel-listen" => { + let listen = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--cancel-listen requires an address".to_owned(), + ) + })?; + cancel_endpoint = BrokerListenEndpoint::Tcp(listen); + } + "--socket" => { + let socket = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--socket requires a filesystem path".to_owned(), + ) + })?; + endpoint = BrokerListenEndpoint::unix(socket)?; + } + "--cancel-socket" => { + let socket = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--cancel-socket requires a filesystem path".to_owned(), + ) + })?; + cancel_endpoint = BrokerListenEndpoint::unix(socket)?; + } + "--bootstrap" => { + bootstrap = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig("--bootstrap requires a value".to_owned()) + })?; + } + "--initdb" => { + initdb = Some(iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--initdb requires a filesystem path".to_owned(), + ) + })?); + } + "--durability" => { + durability = parse_durability(&iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig("--durability requires a value".to_owned()) + })?)?; + } + "--runtime-footprint" => { + runtime_footprint = + parse_runtime_footprint(&iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--runtime-footprint requires a value".to_owned(), + ) + })?)?; + } + "--startup-guc" => { + let assignment = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--startup-guc requires name=value".to_owned(), + ) + })?; + startup_gucs.push(parse_startup_guc(&assignment)?); + } + "--username" => { + username = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--username requires a PostgreSQL role".to_owned(), + ) + })?; + } + "--database" => { + database = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--database requires a PostgreSQL database name".to_owned(), + ) + })?; + } + "--extension" => { + let sql_name = iter.next().ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--extension requires a SQL extension name".to_owned(), + ) + })?; + let extension = Extension::by_sql_name(&sql_name).ok_or_else(|| { + oliphaunt::Error::InvalidConfig(format!( + "unsupported native extension '{sql_name}'" + )) + })?; + extensions.push(extension); + } + _ => { + return Err(oliphaunt::Error::InvalidConfig(format!( + "unknown broker argument '{arg}'" + ))); + } + } + } + let bootstrap = parse_bootstrap(&bootstrap, initdb)?; + let auth_token = env::var(ENV_BROKER_AUTH_TOKEN).map_err(|_| { + oliphaunt::Error::InvalidConfig(format!("{ENV_BROKER_AUTH_TOKEN} is required")) + })?; + if auth_token.is_empty() { + return Err(oliphaunt::Error::InvalidConfig(format!( + "{ENV_BROKER_AUTH_TOKEN} must not be empty" + ))); + } + + Ok(Self { + root: root + .ok_or_else(|| oliphaunt::Error::InvalidConfig("--root is required".to_owned()))?, + endpoint, + cancel_endpoint, + bootstrap, + durability, + runtime_footprint, + startup_gucs, + username, + database, + extensions, + auth_token, + }) + } +} + +fn parse_bootstrap(value: &str, initdb: Option) -> oliphaunt::Result { + match value { + "packaged-template" => { + if initdb.is_some() { + return Err(oliphaunt::Error::InvalidConfig( + "--initdb is only valid with --bootstrap initdb-tooling-only".to_owned(), + )); + } + Ok(BootstrapStrategy::PackagedTemplate) + } + "existing-only" => { + if initdb.is_some() { + return Err(oliphaunt::Error::InvalidConfig( + "--initdb is only valid with --bootstrap initdb-tooling-only".to_owned(), + )); + } + Ok(BootstrapStrategy::ExistingOnly) + } + "initdb-tooling-only" => { + let initdb = initdb.ok_or_else(|| { + oliphaunt::Error::InvalidConfig( + "--bootstrap initdb-tooling-only requires --initdb".to_owned(), + ) + })?; + Ok(BootstrapStrategy::InitdbToolingOnly { + initdb: initdb.into(), + }) + } + _ => Err(oliphaunt::Error::InvalidConfig(format!( + "unknown bootstrap strategy '{value}'" + ))), + } +} + +fn parse_durability(value: &str) -> oliphaunt::Result { + match value { + "safe" => Ok(DurabilityProfile::Safe), + "balanced" => Ok(DurabilityProfile::Balanced), + "fast-dev" => Ok(DurabilityProfile::FastDev), + _ => Err(oliphaunt::Error::InvalidConfig(format!( + "unknown durability profile '{value}'" + ))), + } +} + +fn parse_runtime_footprint(value: &str) -> oliphaunt::Result { + match value { + "throughput" => Ok(RuntimeFootprintProfile::Throughput), + "balanced-mobile" => Ok(RuntimeFootprintProfile::BalancedMobile), + "small-mobile" => Ok(RuntimeFootprintProfile::SmallMobile), + _ => Err(oliphaunt::Error::InvalidConfig(format!( + "unknown runtime footprint profile '{value}'" + ))), + } +} + +fn parse_startup_guc(value: &str) -> oliphaunt::Result { + let Some((name, guc_value)) = value.split_once('=') else { + return Err(oliphaunt::Error::InvalidConfig( + "--startup-guc requires name=value".to_owned(), + )); + }; + Ok(PostgresStartupGuc::new(name, guc_value)) +} + +enum BrokerListenEndpoint { + Tcp(String), + #[cfg(unix)] + Unix(std::path::PathBuf), +} + +impl BrokerListenEndpoint { + #[cfg(unix)] + fn unix(path: impl Into) -> oliphaunt::Result { + Ok(Self::Unix(path.into())) + } + + #[cfg(not(unix))] + fn unix(_path: impl Into) -> oliphaunt::Result { + Err(oliphaunt::Error::InvalidConfig( + "Unix-domain broker sockets are not supported on this platform".to_owned(), + )) + } +} + +trait BrokerTransport: Read + Write {} + +impl BrokerTransport for T where T: Read + Write {} + +enum BrokerListener { + Tcp(TcpListener), + #[cfg(unix)] + Unix { + listener: UnixListener, + path: std::path::PathBuf, + }, +} + +impl BrokerListener { + fn bind(endpoint: BrokerListenEndpoint) -> oliphaunt::Result { + match endpoint { + BrokerListenEndpoint::Tcp(listen) => { + TcpListener::bind(&listen).map(Self::Tcp).map_err(|err| { + oliphaunt::Error::Engine(format!("bind broker TCP listener {listen}: {err}")) + }) + } + #[cfg(unix)] + BrokerListenEndpoint::Unix(path) => { + if path.exists() { + std::fs::remove_file(&path).map_err(|err| { + oliphaunt::Error::Engine(format!( + "remove stale broker socket {}: {err}", + path.display() + )) + })?; + } + UnixListener::bind(&path) + .map(|listener| Self::Unix { listener, path }) + .map_err(|err| { + oliphaunt::Error::Engine(format!("bind broker Unix socket: {err}")) + }) + } + } + } + + fn ready_endpoint(&self) -> String { + match self { + Self::Tcp(listener) => listener + .local_addr() + .map(|addr| format!("tcp:{addr}")) + .unwrap_or_else(|_| "tcp:".to_owned()), + #[cfg(unix)] + Self::Unix { path, .. } => format!("unix:{}", path.display()), + } + } + + fn accept(&self) -> oliphaunt::Result> { + match self { + Self::Tcp(listener) => listener + .accept() + .map(|(stream, _)| Box::new(stream) as Box) + .map_err(|err| { + oliphaunt::Error::Engine(format!("accept broker TCP client: {err}")) + }), + #[cfg(unix)] + Self::Unix { listener, path } => listener + .accept() + .map(|(stream, _)| Box::new(stream) as Box) + .map_err(|err| { + oliphaunt::Error::Engine(format!( + "accept broker Unix client on {}: {err}", + path.display() + )) + }), + } + } +} diff --git a/src/runtimes/broker/tools/check-package.sh b/src/runtimes/broker/tools/check-package.sh new file mode 100755 index 00000000..9fc4956a --- /dev/null +++ b/src/runtimes/broker/tools/check-package.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" + +out_dir="target/oliphaunt-broker/package" +listing="$out_dir/oliphaunt-broker.package-files.txt" +mkdir -p "$out_dir" + +cargo package --list -p oliphaunt-broker --locked --allow-dirty >"$listing" + +require_entry() { + local entry="$1" + if ! grep -Fxq "$entry" "$listing"; then + echo "oliphaunt-broker package is missing required entry: $entry" >&2 + exit 1 + fi +} + +reject_pattern() { + local pattern="$1" + if grep -Eq "$pattern" "$listing"; then + echo "oliphaunt-broker package contains forbidden entry matching: $pattern" >&2 + grep -E "$pattern" "$listing" >&2 + exit 1 + fi +} + +require_entry "Cargo.toml" +require_entry "README.md" +require_entry "src/main.rs" + +reject_pattern '(^|/)(target|release-assets|release-stage)(/|$)' +reject_pattern '^targets/' +reject_pattern '^src/runtimes/liboliphaunt/' +reject_pattern '^src/sdks/rust/' + +echo "oliphaunt-broker package shape verified: $listing" diff --git a/src/runtimes/liboliphaunt/icu/Cargo.toml b/src/runtimes/liboliphaunt/icu/Cargo.toml new file mode 100644 index 00000000..d146766e --- /dev/null +++ b/src/runtimes/liboliphaunt/icu/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "oliphaunt-icu" +version = "0.0.0" +edition = "2024" +rust-version = "1.93" +description = "Optional ICU data files for Oliphaunt runtimes." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +documentation = "https://docs.rs/oliphaunt-icu" +license = "MIT AND Apache-2.0 AND PostgreSQL AND Unicode-3.0" +links = "oliphaunt_artifact_oliphaunt_icu" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +sha2 = "0.10" +tar = "0.4" +zstd = { version = "0.13", default-features = false } diff --git a/src/runtimes/liboliphaunt/icu/README.md b/src/runtimes/liboliphaunt/icu/README.md new file mode 100644 index 00000000..aa57e962 --- /dev/null +++ b/src/runtimes/liboliphaunt/icu/README.md @@ -0,0 +1,6 @@ +# oliphaunt-icu + +Optional ICU data files for Oliphaunt runtimes. + +Applications add this package only when they need PostgreSQL ICU collations. Base +Oliphaunt runtimes do not carry ICU data. diff --git a/src/runtimes/liboliphaunt/icu/build.rs b/src/runtimes/liboliphaunt/icu/build.rs new file mode 100644 index 00000000..64dc17ae --- /dev/null +++ b/src/runtimes/liboliphaunt/icu/build.rs @@ -0,0 +1,312 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Component, Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +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"); + println!("cargo:rerun-if-env-changed=OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD"); + + 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(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); + write_generated_icu(&out, Some(&archive)); + 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/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")); + for candidate in icu_candidates(&manifest_dir) { + if let Some(root) = canonical_icu_data_root(&candidate) { + return Some(root); + } + } + None +} + +fn icu_candidates(manifest_dir: &Path) -> Vec { + let mut candidates = Vec::new(); + candidates.push(manifest_dir.join("payload/share/icu")); + if let Some(path) = env::var_os("OLIPHAUNT_ICU_DATA_DIR") { + candidates.push(PathBuf::from(path)); + } + if let Some(repo) = repo_root_from_manifest_dir(manifest_dir) { + candidates.push(repo.join("target/oliphaunt-wasix/icu/share/icu")); + candidates.push(repo.join("target/oliphaunt-wasix/wasix-build/work/icu-wasix/share/icu")); + candidates.push(repo.join("target/liboliphaunt-pg18/icu/share/icu")); + candidates.push(repo.join("target/liboliphaunt-pg18/install/share/icu")); + candidates.push(repo.join("target/native-liboliphaunt-pg18/install/share/icu")); + if let Ok(entries) = fs::read_dir(repo.join("target")) { + for entry in entries.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if name.starts_with("liboliphaunt-pg18-") { + candidates.push(path.join("icu/share/icu")); + } + } + } + } + candidates +} + +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/runtimes/liboliphaunt/icu/Cargo.toml") + .is_file() + }) +} + +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()); + } + let entries = fs::read_dir(candidate).ok()?; + let mut dirs = entries + .filter_map(Result::ok) + .map(|entry| entry.path()) + .filter(|path| path.is_dir()) + .collect::>(); + dirs.sort(); + dirs.into_iter().find(|path| icu_root_contains_data(path)) +} + +fn icu_root_contains_data(root: &Path) -> bool { + let Ok(entries) = fs::read_dir(root) else { + return false; + }; + for entry in entries.flatten() { + let path = entry.path(); + let name = entry.file_name().to_string_lossy().into_owned(); + if path.is_file() && name.starts_with("icudt") && name.ends_with(".dat") { + return true; + } + if path.is_dir() && name.starts_with("icudt") && directory_has_file(&path) { + return true; + } + } + false +} + +fn directory_has_file(path: &Path) -> bool { + fs::read_dir(path) + .ok() + .into_iter() + .flatten() + .flatten() + .any(|entry| entry.path().is_file()) +} + +fn emit_rerun_directives(root: &Path) { + println!("cargo:rerun-if-changed={}", root.display()); + visit_files(root, &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_icu_archive(icu_root: &Path, archive: &Path) { + let file = fs::File::create(archive).expect("create ICU data archive"); + let encoder = zstd::stream::write::Encoder::new(file, 19).expect("create zstd encoder"); + let mut builder = tar::Builder::new(encoder); + for source in collect_files(icu_root).expect("collect ICU data files") { + let relative = source + .strip_prefix(icu_root) + .expect("ICU file stays under ICU root"); + let archive_path = Path::new("share/icu").join(relative); + builder + .append_path_with_name(&source, &archive_path) + .expect("append ICU data file"); + } + let encoder = builder.into_inner().expect("finish ICU tar archive"); + encoder.finish().expect("finish ICU zstd archive"); +} + +fn write_generated_icu(out: &Path, archive: Option<&Path>) { + let text = match archive { + Some(archive) => format!( + "pub const HAS_ICU_DATA: bool = true;\n\ + pub fn icu_data_archive() -> Option<&'static [u8]> {{ Some(include_bytes!({archive:?})) }}\n", + archive = archive.to_string_lossy(), + ), + None => "pub const HAS_ICU_DATA: bool = false;\npub fn icu_data_archive() -> Option<&'static [u8]> { None }\n" + .to_owned(), + }; + fs::write(out, text).expect("write generated ICU data module"); +} + +fn emit_artifact_manifest(out_dir: &Path, icu_root: &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 files = collect_files(icu_root).expect("collect ICU data files for manifest"); + 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(icu_root) + .expect("ICU file stays under ICU root") + .to_string_lossy() + .replace('\\', "/"); + let sha256 = sha256_file(&file).expect("hash ICU data file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + format!("share/icu/{relative}"), + sha256, + )); + } + fs::write(&manifest_path, text).expect("write ICU Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +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 path = entry.path(); + if path.is_dir() { + collect_files_inner(&path, files)?; + } else if path.is_file() { + files.push(path); + } + } + Ok(()) +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 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/icu/src/lib.rs b/src/runtimes/liboliphaunt/icu/src/lib.rs new file mode 100644 index 00000000..973fd5f5 --- /dev/null +++ b/src/runtimes/liboliphaunt/icu/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_icu.rs")); diff --git a/src/runtimes/liboliphaunt/native/.release-semantic-inputs.json b/src/runtimes/liboliphaunt/native/.release-semantic-inputs.json new file mode 100644 index 00000000..5ca73e0a --- /dev/null +++ b/src/runtimes/liboliphaunt/native/.release-semantic-inputs.json @@ -0,0 +1,320 @@ +{ + "schema": "oliphaunt-release-semantic-fingerprint-v1", + "product": "liboliphaunt-native", + "ownershipSchema": "oliphaunt-release-semantic-inputs-v1", + "ownershipManifest": "tools/release/release-semantic-inputs.toml", + "rules": [ + { + "id": "cargo-workspace-build-inputs", + "paths": [ + "Cargo.lock", + "Cargo.toml", + "rust-toolchain.toml" + ], + "inputs": [ + { + "path": "Cargo.lock", + "sha256": "2542920dbb45420d4c016bb9633e9039dd6c6401deb09fc90667164b1cd1b787" + }, + { + "path": "Cargo.toml", + "sha256": "e215986659ca938d7e57be512ad38a9100e4e5fa61569b344e8677e2532d0c36" + }, + { + "path": "rust-toolchain.toml", + "sha256": "67da2fa8f0f03d571e24153929d18475dfd95bf1165e7714595ba7113e61b58c" + } + ] + }, + { + "id": "native-npm-carrier-producer", + "paths": [ + "tools/release/release-product-dry-run.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-product-dry-run.mjs", + "sha256": "5d7f66e141a3d9fbd40e1d6dabb63da82e9120c8265357cbf081d72e8cdecc71" + } + ] + }, + { + "id": "maven-runtime-extension-carrier-producer", + "paths": [ + "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "tools/release/build_maven_artifact_manifest.mjs" + ], + "inputs": [ + { + "path": "src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts", + "sha256": "a6e9408e637862ff161232f90339e86e1a53ec3816084e3f7d047e9ab67dea1d" + }, + { + "path": "tools/release/build_maven_artifact_manifest.mjs", + "sha256": "5fe0406a26e99e38b37e8d2c95e002bdbf7241fe8be6404088af0a60099a0f3b" + } + ] + }, + { + "id": "native-cargo-carrier-producer", + "paths": [ + "tools/release/package-liboliphaunt-cargo-artifacts.mjs" + ], + "inputs": [ + { + "path": "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "sha256": "15eb46557603b110e51fa05ee608c5abdd8e6b1fe39cf493481d11bfd343660f" + } + ] + }, + { + "id": "canonical-public-product-target-contract", + "paths": [ + "tools/release/release-artifact-targets.mjs" + ], + "inputs": [ + { + "path": "tools/release/release-artifact-targets.mjs", + "sha256": "b972d94c73cb5e8da4cb086f8fbc5b7be305325004faece4b6f51104d634832f" + } + ] + }, + { + "id": "published-binary-compatibility-contract", + "paths": [ + "tools/release/platform-compatibility-policy.mjs" + ], + "inputs": [ + { + "path": "tools/release/platform-compatibility-policy.mjs", + "sha256": "2da411f95ccf11afa603605b5c15b3775073f9c1f157c70e5d0362902712564c" + } + ] + }, + { + "id": "shared-native-archive-encoder", + "paths": [ + "tools/release/archive_dir.mjs" + ], + "inputs": [ + { + "path": "tools/release/archive_dir.mjs", + "sha256": "389545cbb7c5512cc762f87edab8c156e7385bdf6449a193186117b48a250fea" + } + ] + }, + { + "id": "native-runtime-release-asset-producers", + "paths": [ + "tools/release/package-liboliphaunt-aggregate-assets.sh", + "tools/release/package-liboliphaunt-icu-data.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", + "tools/release/optimize_native_runtime_payload.mjs" + ], + "inputs": [ + { + "path": "tools/release/optimize_native_runtime_payload.mjs", + "sha256": "a9c7b243be75377ee7a846f2a048a6a9059a1cf185a1b60dbdc21be266181711" + }, + { + "path": "tools/release/package-liboliphaunt-aggregate-assets.sh", + "sha256": "f4f20dc5955e56fc615d307a4bc8d05fb3892f2eb146a009762b06c8e3e08209" + }, + { + "path": "tools/release/package-liboliphaunt-icu-data.sh", + "sha256": "8474b4702d55af74fb275da27b4f27e7a1f6a83daf69d1f90edc816bae7a9b86" + }, + { + "path": "tools/release/package-liboliphaunt-linux-assets.sh", + "sha256": "5c243a8a7142834a8f093cbf90e7c05b962e079b0748fd4737dee5402cf3a287" + }, + { + "path": "tools/release/package-liboliphaunt-macos-assets.sh", + "sha256": "f0752106095406fbf06f9f9c50482815560e88c53d9cd81f2d8c12c15e73036b" + }, + { + "path": "tools/release/package-liboliphaunt-mobile-assets.sh", + "sha256": "90582e10667f8b8b636a9089c48ba48dc6b8bf328ccb02c0988f52dc0acca898" + }, + { + "path": "tools/release/package-liboliphaunt-windows-assets.ps1", + "sha256": "0192720359b4e961206867a18bd3b255cd4297bbf47a539bba31551f5d30b65b" + } + ] + }, + { + "id": "native-runtime-resource-rust-producers", + "paths": [ + "src/sdks/rust/src/bin/package_resources.rs", + "src/sdks/rust/src/build_resources.rs", + "src/sdks/rust/src/config.rs", + "src/sdks/rust/src/extension.rs", + "src/sdks/rust/src/generated/extensions.rs", + "src/sdks/rust/src/lib.rs", + "src/sdks/rust/src/liboliphaunt/ffi.rs", + "src/sdks/rust/src/liboliphaunt/mod.rs", + "src/sdks/rust/src/liboliphaunt/root.rs", + "src/sdks/rust/src/liboliphaunt/root/extensions.rs", + "src/sdks/rust/src/liboliphaunt/root/files.rs", + "src/sdks/rust/src/liboliphaunt/root/fingerprint.rs", + "src/sdks/rust/src/liboliphaunt/root/runtime.rs", + "src/sdks/rust/src/liboliphaunt/root/runtime/**", + "src/sdks/rust/src/liboliphaunt/root/template.rs", + "src/sdks/rust/src/runtime_resources.rs", + "src/sdks/rust/src/runtime_resources/package.rs", + "src/sdks/rust/src/runtime_resources/static_registry.rs" + ], + "inputs": [ + { + "path": "src/sdks/rust/src/bin/package_resources.rs", + "sha256": "04e84ab7a6989e215afbd9f76796d31c1e0dbc1cf63b480ca4ecaea3b0554407" + }, + { + "path": "src/sdks/rust/src/build_resources.rs", + "sha256": "9842f5f41c17375a9b219f2715a0825e524fd4c5905e3b368eac76a76e9220b5" + }, + { + "path": "src/sdks/rust/src/config.rs", + "sha256": "747ed909f1c64a4f294da89f3f1af44f1443ec4d528e27b5a031779cb88cd3ce" + }, + { + "path": "src/sdks/rust/src/extension.rs", + "sha256": "ecf3a2c24e42396b28be8f95679e46a53c205b4de68b1d32b8d1d4e4e99f038e" + }, + { + "path": "src/sdks/rust/src/generated/extensions.rs", + "sha256": "349f8409bedf14abbe1c5cee004ae7a23622e8cd9f2d73292c4662387d1b61de" + }, + { + "path": "src/sdks/rust/src/lib.rs", + "sha256": "9183a993a93635914f989fab3e902b78c1a1e69b75058f3d082b807070fc3abf" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/ffi.rs", + "sha256": "647ecf78389cdfcdf125175bdec9c41a257320abc5f624ed7c4cce4f31ad864b" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/mod.rs", + "sha256": "08298b1bf8eeb8aeeda9a440a397b8c276739f8a2877d287bba18cd70ee3bc7e" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root.rs", + "sha256": "240ca95cd30b02bbc45853f60ab61404971aa0b8be18d1f3acfcac4cd52bdbb2" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/extensions.rs", + "sha256": "c789ad9b4c07afd295c5f2b9f12761208c1c4c6180599c888cba6500e4f03183" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/files.rs", + "sha256": "777af53635c4e9836c10bdaf2ae19c838ef8720b8cdcdacf121a2e8fb05fe378" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/fingerprint.rs", + "sha256": "2f3c21a808bf071a820bf44d596e23f42ac33ae5de389bb1dfc9b052b7c21a94" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/runtime.rs", + "sha256": "ba5b7d1529e67864104eeb324aad257fde8f9a817d98d776ed2bbbb650b76f8f" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs", + "sha256": "42fcc4dd03cdd17c6937ef5600122137f2d0a2182d2f5f01a64aac29e7e98064" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/runtime/install.rs", + "sha256": "a92f213dad6f4e61388a42adc7f21eb5619d0615726f81a7468a32d17591c24b" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs", + "sha256": "a512ee9860c95291ea55bf515546242cafb103bd691f2593fe6dbe3fa16a0990" + }, + { + "path": "src/sdks/rust/src/liboliphaunt/root/template.rs", + "sha256": "a8ab44d14d0d63d4694393d2116344ff0167c2bcafa6dd8b1873ed87c3bb57fb" + }, + { + "path": "src/sdks/rust/src/runtime_resources.rs", + "sha256": "b45096dd670660e15634db49457dbb6401b67fb2be774e6835ba7d5df6601ecd" + }, + { + "path": "src/sdks/rust/src/runtime_resources/package.rs", + "sha256": "ad87a6e5d8ad7dade9a87fa15933a32e3887c9bc4b73cb3094cfde5c67da8660" + }, + { + "path": "src/sdks/rust/src/runtime_resources/static_registry.rs", + "sha256": "34b9a67695c477fd943cc5c2e6aa31ede2576b0b412c0e91ac2e0710d6875f35" + } + ] + }, + { + "id": "native-release-binary-byte-postprocessing", + "paths": [ + "tools/release/strip_native_release_binaries.mjs" + ], + "inputs": [ + { + "path": "tools/release/strip_native_release_binaries.mjs", + "sha256": "3ec54818bd6a559c340323aa5c5114f0d7cb723533feb337ae86ea95c9f69cd0" + } + ] + }, + { + "id": "native-release-symlink-materialization", + "paths": [ + "tools/release/materialize-release-symlinks.mjs" + ], + "inputs": [ + { + "path": "tools/release/materialize-release-symlinks.mjs", + "sha256": "ce66a6e78da09461941c015c65fc8020a6662e64b8213638aa4ca00bb7a7f92a" + } + ] + }, + { + "id": "windows-release-runtime-closure", + "paths": [ + "tools/release/native-runtime-payload-policy.json", + "tools/release/windows-vc-runtime-closure.mjs" + ], + "inputs": [ + { + "path": "tools/release/native-runtime-payload-policy.json", + "sha256": "1675781735fd188c7a59a3b66b0926ca3508e4d3405294f91fdeab090a80f5de" + }, + { + "path": "tools/release/windows-vc-runtime-closure.mjs", + "sha256": "d0a887e0b814d15164b149565f72160ebdf6cfe7f1cfdc4e3c5b05bdae5f759d" + } + ] + }, + { + "id": "runtime-release-checksum-producer", + "paths": [ + "tools/release/write_checksum_manifest.mjs" + ], + "inputs": [ + { + "path": "tools/release/write_checksum_manifest.mjs", + "sha256": "acf259694c121cc475efae4a64418bda7817d5d9f32ead3bdeffd65f0b01b840" + } + ] + }, + { + "id": "shared-release-archive-transport", + "paths": [ + "tools/release/tar-command.mjs" + ], + "inputs": [ + { + "path": "tools/release/tar-command.mjs", + "sha256": "fd4fbcb1827bd60e8d04994913b36ce5ad11eb2b81a7c7018afd4d1934d1d385" + } + ] + } + ], + "sha256": "e081d0cd6a5dee5fc20317a936fbf4d1a6443aa39e21ef295d38fd3b32102f81" +} diff --git a/src/runtimes/liboliphaunt/native/CHANGELOG.md b/src/runtimes/liboliphaunt/native/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/src/runtimes/liboliphaunt/native/README.md b/src/runtimes/liboliphaunt/native/README.md new file mode 100644 index 00000000..ffc4d543 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/README.md @@ -0,0 +1,234 @@ +# liboliphaunt + +`liboliphaunt` is the native C boundary for embedded PostgreSQL. It owns the +PostgreSQL 18 source pin, upstreamable patch stack, C ABI header, native shim, +and local smoke/build scripts. + +This directory is intentionally not the Rust SDK. Rust lives in +`src/sdks/rust`; future Swift, Kotlin, React Native, and other targets +should bind to the same C ABI instead of reaching into PostgreSQL internals. + +## Layout + +- `include/oliphaunt.h`: public C ABI. +- `src/liboliphaunt_native.c`: direct-mode lifecycle, backend thread ownership, + and non-query public ABI entrypoints. +- `src/liboliphaunt_runtime.c`: embedded backend argv/default-GUC construction + and backend thread stack sizing policy. +- `src/liboliphaunt_protocol.c`: raw protocol execution, streaming backpressure, + readiness scanning, and embedded backend read/write callbacks. +- `src/liboliphaunt_bootstrap.c`: PGDATA bootstrap, desktop/tooling `initdb` + process execution, runtime-tool discovery, and startup argument copying. + Apple mobile targets compile this path as template-only because apps cannot + rely on spawning `initdb` from app storage. +- `src/liboliphaunt_process.c`: process-wide direct-mode instance guard and + desktop dynamic-extension symbol-scope promotion. +- `src/liboliphaunt_static_extensions.c`: process-wide static extension registry + used by mobile-style builds that link extension modules into the app binary. +- `src/liboliphaunt_trace.c`: low-overhead protocol timing counters. +- `src/liboliphaunt_archive.c`: backup/restore lifecycle over the C ABI. +- `src/liboliphaunt_archive_tar.c`: private ustar read/write implementation for + same-version physical archives. +- `src/liboliphaunt_fs.c`: private filesystem/path helpers shared by archive and + restore code. +- `src/liboliphaunt_internal.h`: private helpers shared between C translation + units; not part of the public ABI. +- `patches/postgresql-18.4/`: minimal PostgreSQL patch stack. +- `postgres18/source.toml`: pinned PostgreSQL source manifest. +- `postgres18/external-extensions.toml`: pinned external PG18 extension + candidate manifest for pgrx-backed extensions such as pgGraph and ParadeDB + `pg_search`. +- `bin/build-postgres18-macos.sh`: macOS build harness. +- `bin/check-external-extension-pins.sh`: no-network source-pin checker for + external extension candidates. +- `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. +- `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. + +## Build + +```sh +src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +``` + +The default output root is `target/liboliphaunt-pg18`. Use `OLIPHAUNT_*` for runtime and build controls. `LIBOLIPHAUNT_PATH` is reserved +for the literal C library artifact path. + +The direct build produces PostgreSQL runtime artifacts without optional +extension artifacts by default. Set `OLIPHAUNT_BUILD_EXTENSIONS=1` only when +refreshing or validating exact extension artifacts; the +`src/runtimes/liboliphaunt/native/tools/check-track.sh extensions` and `full` lanes set that +flag for you. + +External pgrx extensions are not folded into the first-party extension build by +default. Their source pins live in +`src/runtimes/liboliphaunt/native/postgres18/external-extensions.toml`; the native validation wrapper +runs `src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh` without network access and +verifies any local checkout that exists under `target/oliphaunt-sources/checkouts`. Use +`src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh --online` when intentionally +refreshing the pins against upstream refs. + +Build the opt-in pgrx artifacts with: + +```sh +src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh --fetch +src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh +``` + +`--fetch` never changes a durable checkout in place. It fetches only the exact +manifest commit over credential-free HTTPS into a unique sibling stage, with a +wall-clock deadline, low-speed cutoff, shallow/no-submodule transport, and +per-extension source-size bound. A small bounded retry budget uses linear +backoff and a newly initialized stage for every attempt; retries always request +the immutable commit, never its mutable provenance ref. Git object integrity, +the detached `HEAD`, and the clean worktree are verified before a +same-filesystem rename. A failed or interrupted promotion restores the prior +clean checkout. Any tracked, staged, or untracked local state makes fetch fail +without network access or mutation; `OLIPHAUNT_EXTERNAL_PGRX_ALLOW_DIRTY=1` +permits local build experiments but does not authorize source replacement. + +The harness requires the manifest-pinned `cargo-pgrx` version and automatically +uses `target/liboliphaunt-tools/bin/cargo-pgrx` when it exists. It packages each +selected extension once for the normal PostgreSQL server module path and once +with linker flags that bind PostgreSQL symbols to `@rpath/liboliphaunt.dylib` for +direct/broker embedded loading. Use +`OLIPHAUNT_EXTERNAL_PGRX_EXTENSIONS=pggraph` or +`OLIPHAUNT_EXTERNAL_PGRX_EXTENSIONS=paradedb-pg-search` to restrict the build. +The ParadeDB lane is intentionally disk-guarded because `pg_search` pulls a +large DataFusion/Tantivy release build; free target space first, or set +`OLIPHAUNT_EXTERNAL_PGRX_SKIP_DISK_PREFLIGHT=1` only for local experiments. +`src/runtimes/liboliphaunt/native/tools/check-track.sh external-pgrx` runs the no-build +`--check-current` gate for those artifacts. +The currentness fingerprint excludes harness prose and other non-build text. +When only the fingerprint schema changes, use `--refresh-current-stamps` to +validate the existing normal/embedded payloads and restamp them without running +the expensive pgrx packaging step. + +`OLIPHAUNT_STARTUP_TIMEOUT_MS` bounds only initial backend startup readiness. +Normal `oliphaunt_exec_protocol`, `oliphaunt_exec_simple_query`, and streaming +execution do not impose a synthetic query timeout; callers should use +`oliphaunt_cancel` to interrupt long-running SQL. Ordinary SDK close is a +lifecycle detach/wait boundary, not an implicit query cancellation primitive. +The legacy `OLIPHAUNT_TIMEOUT_MS` name remains a startup-time fallback during +the migration. + +The C runtime keeps throughput-oriented PostgreSQL defaults for direct callers: +`shared_buffers=128MB`, `wal_buffers=4MB`, and `min_wal_size=80MB`. SDKs that +need mobile-sized resident footprints do not need a new C ABI; they pass +validated PostgreSQL `-c name=value` startup arguments through +`OliphauntConfig.startup_args`. Later arguments win, so Rust/Swift/Kotlin/RN +can apply balanced/small mobile profiles and benchmark-specific overrides above +the stable C boundary. + +Mobile builds must hydrate PGDATA from a packaged template before calling +`oliphaunt_init`. On Apple mobile platforms the C layer compiles out the +`fork`/`exec` `initdb` fallback and returns a direct error if `PG_VERSION` is +missing. `tools/run-host-c-smoke.mjs` includes a fast iOS simulator syntax +check over the liboliphaunt C shim files. `bin/check-postgres18-ios-simulator.sh` +then validates the upstream PostgreSQL patch touchpoints that matter for the +embedded path: host I/O callbacks, the embedded backend entrypoint, lifecycle +cleanup, static extension lookup, and shell-command exclusion on Apple mobile +SDKs. +`bin/build-postgres18-ios-simulator.sh` is the fast simulator artifact lane for +Expo/RN and Swift validation. `bin/build-postgres18-ios-device.sh` builds the +matching `IOS` device slice, and `bin/build-ios-xcframework.sh` packages both +validated dylibs with public headers as +`target/liboliphaunt-ios-xcframework/out/liboliphaunt.xcframework`. Each lane +cross-builds the patched PostgreSQL backend object graph, tolerates the final +PostgreSQL executable/tool link failure after the embedded objects exist, +links target-specific static ICU code for PostgreSQL collation support, stages +ICU data into the optional ICU package sidecar instead of the base runtime +install, validates the exported C ABI symbols, and reuses the result through +stamped ccache-friendly paths. + +## Static Extension Registry + +Mobile-style packages cannot rely on PostgreSQL dynamically loading every +extension module from the app bundle. `oliphaunt_register_static_extensions` +registers statically linked modules before `oliphaunt_init`, and the PostgreSQL +`dfmgr` patch resolves those entries through the same normal `CREATE +EXTENSION`/`LOAD` path that dynamic modules use. The registry is process-wide, +validates extension names, magic functions, symbol names, duplicate symbols, +and ABI versions, and becomes immutable at backend startup. + +The runtime-resource `--mobile-static-module ` flag is only release +metadata. It must match modules that the platform package actually links and +registers through this C ABI before opening the database. + +The macOS arm64, iOS simulator, iOS device, and Android build lanes also emit +per-extension static archives beside the generated object lists: +`out/extensions//liboliphaunt_extension_.a`. Those archives are the +release artifact boundary for exact mobile extension selection; SDK packaging +can link only the archives for the extensions an app requested instead of +shipping one bundled extension set or rebuilding extension source in the app. +`bin/build-ios-extension-xcframeworks.sh` packages selected macOS arm64, iOS +simulator arm64, and iOS device arm64 archives into per-extension and +per-dependency XCFrameworks for Apple SDK and Xcode consumers without rebuilding +extension sources. Packaging rejects any such XCFramework that lacks one of +those three claimed slices. + +## Root Ownership + +`oliphaunt_init` takes a non-blocking stable sibling filesystem lease for +`` by default and creates +`/.oliphaunt.lock` as the visible root marker. `oliphaunt_restore` +takes the same stable lease before staging or publishing a restored root. This +keeps plain C, Swift, Kotlin, React Native platform adapters, and any future +direct C ABI caller from accidentally opening, replacing, or restoring the same +embedded root concurrently. +Stable lease filenames live beside the root directory and use the same +`.oliphaunt-root-.lock` algorithm as the Rust SDK, so direct C, +Rust, Swift, Kotlin, and React Native platform adapters contend on the same root +identity instead of merely enforcing per-SDK locks. + +Callers that already own an equivalent root coordinator may set +`OLIPHAUNT_CONFIG_EXTERNAL_ROOT_LOCK` in `OliphauntConfig.reserved_flags`. Today the +Rust SDK uses that flag because `oliphaunt` coordinates direct, broker, +server, backup, and restore through its own process registry plus stable +filesystem root leases. SDKs must not set the flag unless they can prove the +same root cannot be opened or replaced concurrently. + +## Physical Archive Contract + +`oliphaunt_backup(..., OLIPHAUNT_BACKUP_FORMAT_PHYSICAL_ARCHIVE, ...)` emits a +same-version concrete root archive. `oliphaunt_backup_ex` uses the same archive +writer and can append generated metadata entries such as `manifest.properties` +or `.oliphaunt/backup-manifest.properties` while the archive is being produced, +which avoids a second full archive copy in SDKs. The C ABI accepts only regular +files and directories under `pgdata`; symlinks, hardlinks, device nodes, FIFOs, +sockets, sparse/special tar records, external tablespaces, and linked WAL +directories are rejected. `oliphaunt_restore` enforces the same rule before +publishing a restored root, so Swift, Kotlin, React Native, and Rust SDK callers +inherit one portable archive contract instead of platform-specific tar behavior. + +## Fast Native Iteration + +For product-track work, prefer the native-only validation wrapper instead of the +workspace-wide WASIX lanes: + +```sh +moon run liboliphaunt-native:host-smoke +src/runtimes/liboliphaunt/native/tools/check-track.sh quick +src/runtimes/liboliphaunt/native/tools/check-track.sh sdks +src/runtimes/liboliphaunt/native/tools/check-track.sh full +``` + +`liboliphaunt-native:host-smoke` is the no-build host C ABI smoke for the current platform. +It reuses the release-runtime artifact produced for macOS, Linux, or Windows +and fails if that artifact is missing or stale. `quick` reuses the existing +native runtime when it is present, then runs the C ABI smoke and Rust native SDK +tests. `sdks` +validates Swift, Kotlin, and React Native package checks against the same +runtime. `full` enables native extension artifacts and the extension matrix; in +the default `missing` policy it first runs the build script's no-build +`--check-extension-artifacts-current` probe and only rebuilds when the extension +fingerprint or required artifacts are stale or absent. Set +`OLIPHAUNT_TRACK_BUILD=never` to prove the command will not rebuild native +PostgreSQL; the native track also runs the no-build +`--check-oliphaunt-current` probe so stale C ABI sources fail before tests trust +an old dylib. Use `OLIPHAUNT_TRACK_BUILD=always` for an intentional rebuild. diff --git a/src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md b/src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..be970c50 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md @@ -0,0 +1,16 @@ +# liboliphaunt Third-Party Notices + +`liboliphaunt` ships native embedded PostgreSQL runtime artifacts, selected SQL +extensions, and supporting runtime resources. + +The PostgreSQL runtime is derived from PostgreSQL 18 source pinned under +`src/postgres/versions/18/` and built with the native patch stack owned by +`src/runtimes/liboliphaunt/native/`. + +- PostgreSQL license: https://www.postgresql.org/about/licence/ +- ICU / Unicode License v3: https://github.com/unicode-org/icu/blob/main/LICENSE + +Third-party source pins for optional external extensions and supporting native +libraries are maintained in `src/sources/third-party/`. Exact SQL extension selection is +modeled in `src/extensions/`; release artifacts must include only the extension +artifacts explicitly selected by the application developer. diff --git a/src/runtimes/liboliphaunt/native/VERSION b/src/runtimes/liboliphaunt/native/VERSION new file mode 100644 index 00000000..77d6f4ca --- /dev/null +++ b/src/runtimes/liboliphaunt/native/VERSION @@ -0,0 +1 @@ +0.0.0 diff --git a/src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh b/src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh new file mode 100755 index 00000000..31a165e7 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh @@ -0,0 +1,605 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +macos_deployment_target="${MACOSX_DEPLOYMENT_TARGET:-11.0}" +case "$macos_deployment_target" in + ""|*[!0-9.]*) + echo "MACOSX_DEPLOYMENT_TARGET must be a numeric dotted version" >&2 + exit 2 + ;; +esac +export MACOSX_DEPLOYMENT_TARGET="$macos_deployment_target" +work_root="${OLIPHAUNT_WORK_ROOT:-$repo_root/target/liboliphaunt-pg18}" +repo_tools_bin="$repo_root/target/liboliphaunt-tools/bin" +install_dir="$work_root/install" +out_dir="$work_root/out" +lib_out="$out_dir/liboliphaunt.dylib" +embedded_modules_dir="$out_dir/modules" +package_root="$work_root/external-pgrx/packages" +target_root="$work_root/external-pgrx/target" +source_stage_root="$work_root/external-pgrx/sources" +pgrx_home="${PGRX_HOME:-$work_root/external-pgrx/pgrx-home}" +stamp_root="$out_dir/external-pgrx" +script_mode="${1:-build}" +selected_extensions="${OLIPHAUNT_EXTERNAL_PGRX_EXTENSIONS:-all}" +build_fingerprint_schema="liboliphaunt-external-pgrx-build-v3" +pinned_git_fetcher="$script_dir/fetch-pinned-git-checkout.sh" + +if [ -x "$repo_tools_bin/cargo-pgrx" ]; then + case ":$PATH:" in + *":$repo_tools_bin:"*) ;; + *) export PATH="$repo_tools_bin:$PATH" ;; + esac +fi + +ids=(pggraph paradedb-pg-search) +sql_names=(graph pg_search) +module_stems=(graph pg_search) +repos=( + https://github.com/evokoa/pggraph.git + https://github.com/paradedb/paradedb.git +) +refs=(HEAD refs/tags/v0.23.4) +commits=( + 4ea3c3206811deda03de136b4f465a2cf9bc8e72 + c07921a78f3d24cbb0251b31a1150a7db600af5a +) +checkouts=( + "$repo_root/target/oliphaunt-sources/checkouts/pggraph" + "$repo_root/target/oliphaunt-sources/checkouts/paradedb" +) +source_subdirs=(graph pg_search) +pgrx_versions=(0.18.0 0.18.0) +pg_features=(pg18 pg18) +min_free_kib=(2097152 12582912) +max_checkout_kib=(524288 4194304) + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh [build|--fetch|--check-current|--refresh-current-stamps|--print-required-artifacts] + +Environment: + OLIPHAUNT_EXTERNAL_PGRX_EXTENSIONS=all|pggraph,paradedb-pg-search + OLIPHAUNT_EXTERNAL_PGRX_SKIP_DISK_PREFLIGHT=1 to bypass disk checks + OLIPHAUNT_EXTERNAL_PGRX_FETCH_TIMEOUT_SECONDS=300 bounds each exact-commit fetch + OLIPHAUNT_EXTERNAL_PGRX_FETCH_ATTEMPTS=3 sets the bounded attempt count (maximum 4) + OLIPHAUNT_EXTERNAL_PGRX_FETCH_RETRY_DELAY_SECONDS=2 sets linear backoff (maximum 5) + +The build mode requires cargo-pgrx. The default fast native validation does not +run this expensive lane; it is the opt-in artifact builder for SDK-known pgrx +extensions. +MSG +} + +run() { + printf '\n==> %s\n' "$*" + "$@" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command for external pgrx extension build: $1" >&2 + exit 1 + fi +} + +available_kib_for_path() { + local path="$1" + mkdir -p "$path" + df -Pk "$path" | awk 'NR == 2 { print $4 }' +} + +format_gib_from_kib() { + awk -v kib="$1" 'BEGIN { printf "%.1f GiB", kib / 1048576 }' +} + +require_free_space_for_candidate() { + local index="$1" + [ "${OLIPHAUNT_EXTERNAL_PGRX_SKIP_DISK_PREFLIGHT:-0}" = "1" ] && return 0 + + local required="${min_free_kib[$index]}" + local available + available="$(available_kib_for_path "$work_root")" + if [ -z "$available" ] || [ "$available" -lt "$required" ]; then + echo "external pgrx build for ${ids[$index]} needs at least $(format_gib_from_kib "$required") free under $work_root; available: $(format_gib_from_kib "${available:-0}")" >&2 + echo "free disk space or set OLIPHAUNT_EXTERNAL_PGRX_SKIP_DISK_PREFLIGHT=1 for a local experiment" >&2 + exit 1 + fi +} + +candidate_selected() { + local id="$1" + local raw="$selected_extensions" + [ "$raw" = "all" ] && return 0 + IFS=',' read -r -a selected <<< "$raw" + local candidate + for candidate in "${selected[@]}"; do + candidate="${candidate#"${candidate%%[![:space:]]*}"}" + candidate="${candidate%"${candidate##*[![:space:]]}"}" + if [ "$candidate" = "$id" ]; then + return 0 + fi + done + return 1 +} + +selected_indices() { + local index + for index in "${!ids[@]}"; do + if candidate_selected "${ids[$index]}"; then + printf '%s\n' "$index" + fi + done +} + +assert_known_selection() { + [ "$selected_extensions" = "all" ] && return 0 + IFS=',' read -r -a selected <<< "$selected_extensions" + local candidate + for candidate in "${selected[@]}"; do + candidate="${candidate#"${candidate%%[![:space:]]*}"}" + candidate="${candidate%"${candidate##*[![:space:]]}"}" + [ -n "$candidate" ] || continue + local found=0 + local id + for id in "${ids[@]}"; do + if [ "$candidate" = "$id" ]; then + found=1 + fi + done + if [ "$found" -eq 0 ]; then + echo "unknown external pgrx extension selection: $candidate" >&2 + exit 2 + fi + done +} + +module_depends_on_liboliphaunt() { + local module="$1" + [ -f "$module" ] || return 1 + case "$(otool -L "$module" 2>/dev/null || true)" in + *"@rpath/liboliphaunt.dylib"*) return 0 ;; + *) return 1 ;; + esac +} + +module_has_postgres_symbols_bound_to_liboliphaunt() { + local module="$1" + nm -m "$module" 2>/dev/null | + awk 'index($0, "(from liboliphaunt)") { found = 1 } END { exit found ? 0 : 1 }' +} + +normal_pgrx_rustflags() { + printf '%s -C link-arg=-Wl,-undefined,dynamic_lookup' "${RUSTFLAGS:-}" +} + +embedded_pgrx_rustflags() { + printf '%s -C link-arg=-L%s -C link-arg=-loliphaunt -C link-arg=-Wl,-rpath,%s' \ + "${RUSTFLAGS:-}" "$out_dir" "$out_dir" +} + +checkout_clean_or_allowed() { + local checkout="$1" + [ "${OLIPHAUNT_EXTERNAL_PGRX_ALLOW_DIRTY:-0}" = "1" ] && return 0 + if [ -n "$(git -C "$checkout" status --porcelain)" ]; then + echo "external extension checkout has local changes: $checkout" >&2 + echo "set OLIPHAUNT_EXTERNAL_PGRX_ALLOW_DIRTY=1 only for local experiments" >&2 + exit 1 + fi +} + +fetch_candidate() { + local index="$1" + local id="${ids[$index]}" + local checkout="${checkouts[$index]}" + local repo="${repos[$index]}" + local ref="${refs[$index]}" + local commit="${commits[$index]}" + + run "$pinned_git_fetcher" \ + "$id" \ + "$repo" \ + "$ref" \ + "$commit" \ + "$checkout" \ + "${max_checkout_kib[$index]}" + echo "external pgrx checkout ready for $id at $commit" +} + +fingerprint_source_state() { + local root="$1" + printf 'checkout_head=%s\n' "$(git -C "$root" rev-parse HEAD)" + if [ -n "$(git -C "$root" status --porcelain=v1)" ]; then + printf 'checkout_dirty=1\n' + git -C "$root" status --porcelain=v1 | LC_ALL=C sort | sed 's/^/checkout_status=/' + git -C "$root" diff --binary HEAD -- | shasum -a 256 | awk '{ print "checkout_diff_sha256=" $1 }' + else + printf 'checkout_dirty=0\n' + fi +} + +prepare_source_stage() { + local index="$1" + local id="${ids[$index]}" + local checkout="${checkouts[$index]}" + local source_subdir="${source_subdirs[$index]}" + local stage="$source_stage_root/$id" + + rm -rf "$stage" + mkdir -p "$stage" + rsync -a \ + --exclude .git \ + --exclude target \ + --exclude .pgrx \ + "$checkout/" "$stage/" + if [ ! -f "$stage/Cargo.toml" ]; then + cat > "$stage/Cargo.toml" < "$tmp" + shasum -a 256 "$tmp" | awk '{print $1}' > "$stamp" + mv "$tmp" "$inputs" +} + +find_one_packaged_file() { + local root="$1" + local name="$2" + find "$root" -type f -name "$name" -print | LC_ALL=C sort | head -n 1 +} + +copy_sql_assets() { + local package_dir="$1" + local sql_name="$2" + local target="$install_dir/share/postgresql/extension" + mkdir -p "$target" + + local control + control="$(find_one_packaged_file "$package_dir" "$sql_name.control")" + if [ -z "$control" ]; then + echo "pgrx package did not produce $sql_name.control under $package_dir" >&2 + exit 1 + fi + cp -p "$control" "$target/$sql_name.control" + + local copied=0 + while IFS= read -r sql_file; do + [ -n "$sql_file" ] || continue + cp -p "$sql_file" "$target/$(basename "$sql_file")" + copied=$((copied + 1)) + done < <(find "$package_dir" -type f -name "$sql_name--*.sql" -print | LC_ALL=C sort) + if [ "$copied" -eq 0 ]; then + echo "pgrx package did not produce any $sql_name--*.sql files under $package_dir" >&2 + exit 1 + fi +} + +find_packaged_module() { + local package_dir="$1" + local module_stem="$2" + local module + module="$(find_one_packaged_file "$package_dir" "$module_stem.dylib")" + if [ -n "$module" ]; then + printf '%s\n' "$module" + return 0 + fi + module="$(find_one_packaged_file "$package_dir" "lib$module_stem.dylib")" + if [ -n "$module" ]; then + printf '%s\n' "$module" + return 0 + fi + return 1 +} + +copy_module_asset() { + local package_dir="$1" + local module_stem="$2" + local target="$3" + local module + if ! module="$(find_packaged_module "$package_dir" "$module_stem")"; then + echo "pgrx package did not produce module $module_stem.dylib under $package_dir" >&2 + exit 1 + fi + mkdir -p "$(dirname "$target")" + cp -p "$module" "$target" +} + +artifact_payload_ready() { + local index="$1" + local sql_name="${sql_names[$index]}" + local module_stem="${module_stems[$index]}" + + [ -f "$install_dir/share/postgresql/extension/$sql_name.control" ] || return 1 + compgen -G "$install_dir/share/postgresql/extension/$sql_name--*.sql" >/dev/null || return 1 + [ -f "$install_dir/lib/postgresql/$module_stem.dylib" ] || return 1 + [ -f "$embedded_modules_dir/$module_stem.dylib" ] || return 1 + module_depends_on_liboliphaunt "$install_dir/lib/postgresql/$module_stem.dylib" && return 1 + module_depends_on_liboliphaunt "$embedded_modules_dir/$module_stem.dylib" || return 1 + module_has_postgres_symbols_bound_to_liboliphaunt "$embedded_modules_dir/$module_stem.dylib" || return 1 +} + +artifact_ready() { + local index="$1" + local id="${ids[$index]}" + local stamp + stamp="$(artifact_stamp "$id")" + + artifact_payload_ready "$index" || return 1 + [ -f "$stamp" ] || return 1 + [ "$(cat "$stamp")" = "$(build_fingerprint "$index")" ] || return 1 +} + +refresh_candidate_stamp() { + local index="$1" + local id="${ids[$index]}" + if ! artifact_payload_ready "$index"; then + echo "external pgrx payload artifacts are missing or invalid for $id; rebuild before refreshing stamps" >&2 + exit 1 + fi + write_artifact_stamp "$index" + echo "external pgrx stamp refreshed for $id" +} + +require_core_runtime() { + if ! "$repo_root/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh" --check-oliphaunt-current >/dev/null; then + echo "native liboliphaunt core runtime is missing or stale; refreshing core runtime first" + OLIPHAUNT_BUILD_EXTENSIONS=0 "$repo_root/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh" + fi + [ -x "$install_dir/bin/pg_config" ] || { + echo "native PostgreSQL install is missing pg_config at $install_dir/bin/pg_config" >&2 + exit 1 + } +} + +require_pgrx_toolchain() { + require_command cargo + require_command rustc + require_command rsync + export PGRX_HOME="$pgrx_home" + cargo pgrx --version >/dev/null 2>&1 || { + cat >&2 <<'MSG' +missing cargo-pgrx. Install the version declared in +src/runtimes/liboliphaunt/native/postgres18/external-extensions.toml, for example: + + cargo install --locked cargo-pgrx --version 0.18.0 --root target/liboliphaunt-tools +MSG + exit 1 + } +} + +ensure_pgrx_home() { + export PGRX_HOME="$pgrx_home" + mkdir -p "$PGRX_HOME" + if [ ! -f "$PGRX_HOME/config.toml" ] || + ! grep -q "$install_dir/bin/pg_config" "$PGRX_HOME/config.toml"; then + run cargo pgrx init --pg18 "$install_dir/bin/pg_config" + fi +} + +verify_pgrx_version() { + local expected="$1" + local actual + actual="$(cargo pgrx --version | awk '{print $2}')" + if [ "$actual" != "$expected" ]; then + echo "cargo-pgrx version mismatch: expected $expected, got $actual" >&2 + exit 1 + fi +} + +build_candidate() { + local index="$1" + local id="${ids[$index]}" + local checkout="${checkouts[$index]}" + local source_dir + local sql_name="${sql_names[$index]}" + local module_stem="${module_stems[$index]}" + local feature="${pg_features[$index]}" + local pgrx_version="${pgrx_versions[$index]}" + local normal_package="$package_root/$id/normal" + local embedded_package="$package_root/$id/embedded" + local normal_target="$target_root/$id/normal" + local embedded_target="$target_root/$id/embedded" + local stamp + stamp="$(artifact_stamp "$id")" + + [ -d "$checkout/.git" ] || { + echo "external pgrx checkout is missing for $id: $checkout" >&2 + echo "run: src/runtimes/liboliphaunt/native/bin/build-external-pgrx-extensions-macos.sh --fetch" >&2 + exit 1 + } + checkout_clean_or_allowed "$checkout" + if [ "$(git -C "$checkout" rev-parse HEAD)" != "${commits[$index]}" ]; then + echo "external pgrx checkout for $id is not at pinned commit ${commits[$index]}" >&2 + exit 1 + fi + [ -f "$checkout/${source_subdirs[$index]}/Cargo.toml" ] || { + echo "external pgrx source for $id is missing Cargo.toml at $checkout/${source_subdirs[$index]}" >&2 + exit 1 + } + + verify_pgrx_version "$pgrx_version" + local desired_hash + desired_hash="$(build_fingerprint "$index")" + if [ "${OLIPHAUNT_FORCE_EXTERNAL_PGRX_REBUILD:-0}" != "1" ] && + [ -f "$stamp" ] && + [ "$(cat "$stamp")" = "$desired_hash" ] && + artifact_ready "$index"; then + echo "reusing external pgrx artifacts for $id" + return + fi + + require_free_space_for_candidate "$index" + rm -rf "$normal_package" "$embedded_package" + mkdir -p "$normal_package" "$embedded_package" "$normal_target" "$embedded_target" "$stamp_root" + source_dir="$(prepare_source_stage "$index")" + + run env CARGO_TARGET_DIR="$normal_target" \ + RUSTFLAGS="$(normal_pgrx_rustflags)" \ + cargo pgrx package \ + --manifest-path "$source_dir/Cargo.toml" \ + --pg-config "$install_dir/bin/pg_config" \ + --out-dir "$normal_package" \ + --no-default-features \ + --features "$feature" + + copy_sql_assets "$normal_package" "$sql_name" + copy_module_asset "$normal_package" "$module_stem" "$install_dir/lib/postgresql/$module_stem.dylib" + if module_depends_on_liboliphaunt "$install_dir/lib/postgresql/$module_stem.dylib"; then + echo "normal server module for $id unexpectedly links against liboliphaunt" >&2 + exit 1 + fi + + run env CARGO_TARGET_DIR="$embedded_target" \ + RUSTFLAGS="$(embedded_pgrx_rustflags)" \ + cargo pgrx package \ + --manifest-path "$source_dir/Cargo.toml" \ + --pg-config "$install_dir/bin/pg_config" \ + --out-dir "$embedded_package" \ + --no-default-features \ + --features "$feature" + + copy_module_asset "$embedded_package" "$module_stem" "$embedded_modules_dir/$module_stem.dylib" + if ! module_depends_on_liboliphaunt "$embedded_modules_dir/$module_stem.dylib"; then + echo "embedded module for $id is not linked against @rpath/liboliphaunt.dylib" >&2 + exit 1 + fi + if ! module_has_postgres_symbols_bound_to_liboliphaunt "$embedded_modules_dir/$module_stem.dylib"; then + echo "embedded module for $id does not bind PostgreSQL symbols to liboliphaunt" >&2 + exit 1 + fi + + write_artifact_stamp "$index" + artifact_ready "$index" || { + echo "external pgrx artifact validation failed for $id after build" >&2 + exit 1 + } +} + +assert_manifest_and_pins() { + run "$repo_root/src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh" +} + +if [ "$(uname -s)" != "Darwin" ]; then + echo "external pgrx extension build currently targets the macOS native liboliphaunt lane" >&2 + exit 2 +fi + +assert_known_selection + +case "$script_mode" in + --print-required-artifacts) + while IFS= read -r index; do + printf 'control:%s\n' "${sql_names[$index]}" + printf 'module:%s\n' "${module_stems[$index]}" + done < <(selected_indices) + exit 0 + ;; + --fetch) + assert_manifest_and_pins + while IFS= read -r index; do + fetch_candidate "$index" + done < <(selected_indices) + assert_manifest_and_pins + exit 0 + ;; + --check-current) + assert_manifest_and_pins + require_core_runtime + require_pgrx_toolchain + ensure_pgrx_home + while IFS= read -r index; do + if ! artifact_ready "$index"; then + echo "external pgrx artifacts are missing or stale for ${ids[$index]}" >&2 + exit 1 + fi + done < <(selected_indices) + echo "external pgrx artifacts are current" + exit 0 + ;; + --refresh-current-stamps) + assert_manifest_and_pins + require_core_runtime + require_pgrx_toolchain + ensure_pgrx_home + while IFS= read -r index; do + refresh_candidate_stamp "$index" + done < <(selected_indices) + echo "external pgrx artifact stamps are current" + exit 0 + ;; + build) + assert_manifest_and_pins + require_core_runtime + require_pgrx_toolchain + ensure_pgrx_home + while IFS= read -r index; do + build_candidate "$index" + done < <(selected_indices) + echo "external pgrx artifacts are ready" + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh b/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh new file mode 100755 index 00000000..b8f29cad --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh @@ -0,0 +1,604 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/mobile-static-extensions.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" + +simulator_out="${OLIPHAUNT_IOS_SIMULATOR_OUT:-$repo_root/target/liboliphaunt-ios-simulator/out}" +device_out="${OLIPHAUNT_IOS_DEVICE_OUT:-$repo_root/target/liboliphaunt-ios-device/out}" +macos_out="${OLIPHAUNT_MACOS_EXTENSION_OUT:-$repo_root/target/liboliphaunt-pg18/macos-extension-archives/out}" +work_root="${OLIPHAUNT_IOS_EXTENSION_XCFRAMEWORK_ROOT:-$repo_root/target/liboliphaunt-ios-extension-xcframeworks}" +out_dir="$work_root/out" +headers_dir="$repo_root/src/runtimes/liboliphaunt/native/include" +runtime_resources_dir="${OLIPHAUNT_IOS_RUNTIME_RESOURCES_DIR:-${OLIPHAUNT_RUNTIME_RESOURCES_DIR:-}}" +manifest_file="$out_dir/manifest.properties" + +usage() { + cat >&2 <] + +Packages selected prebuilt Apple extension archives into per-extension +XCFrameworks with macOS arm64, iOS device arm64, and iOS simulator arm64 +slices. Prefer passing the Rust runtime-resource output so the selected +native modules are derived from runtime/manifest.properties: + + src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh \\ + --runtime-resources target/oliphaunt-resources + +For release automation, OLIPHAUNT_MOBILE_STATIC_EXTENSIONS may still provide a +comma-separated exact extension or module-stem list: + + OLIPHAUNT_MOBILE_STATIC_EXTENSIONS=vector,pg_trgm \\ + src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh + +Inputs: + OLIPHAUNT_IOS_SIMULATOR_OUT default target/liboliphaunt-ios-simulator/out + OLIPHAUNT_IOS_DEVICE_OUT default target/liboliphaunt-ios-device/out + OLIPHAUNT_MACOS_EXTENSION_OUT default target/liboliphaunt-pg18/macos-extension-archives/out + OLIPHAUNT_RUNTIME_RESOURCES_DIR optional Rust runtime-resource output + OLIPHAUNT_IOS_RUNTIME_RESOURCES_DIR optional iOS-specific runtime-resource output +Output: + target/liboliphaunt-ios-extension-xcframeworks/out//liboliphaunt_extension_.xcframework + target/liboliphaunt-ios-extension-xcframeworks/out/dependencies//liboliphaunt_dependency_.xcframework + target/liboliphaunt-ios-extension-xcframeworks/out/manifest.properties +USAGE +} + +mode="build" +while [ "$#" -gt 0 ]; do + case "$1" in + --check-current) + mode="check" + shift + ;; + --runtime-resources) + [ "$#" -ge 2 ] || { + usage + exit 2 + } + runtime_resources_dir="$2" + shift 2 + ;; + --runtime-resources=*) + runtime_resources_dir="${1#--runtime-resources=}" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + usage + exit 2 + ;; + esac +done + +selected_extensions=() +selected_stems=() +selected_dependencies=() + +join_csv() { + local old_ifs="$IFS" + IFS="," + printf '%s' "$*" + IFS="$old_ifs" +} + +runtime_resources_root() { + local root="$1" + if [ -f "$root/oliphaunt/runtime/manifest.properties" ]; then + printf '%s\n' "$root/oliphaunt" + elif [ -f "$root/runtime/manifest.properties" ]; then + printf '%s\n' "$root" + else + echo "iOS extension runtime resources are not an Oliphaunt resource root: $root" >&2 + exit 2 + fi +} + +resource_manifest_value() { + local root="$1" + local key="$2" + local manifest="$root/runtime/manifest.properties" + awk -F '=' -v key="$key" '$1 == key { print substr($0, length(key) + 2); found = 1; exit } END { exit found ? 0 : 1 }' "$manifest" || true +} + +static_registry_manifest_value() { + local root="$1" + local key="$2" + local manifest="$root/static-registry/manifest.properties" + [ -f "$manifest" ] || return 1 + awk -F '=' -v key="$key" '$1 == key { print substr($0, length(key) + 2); found = 1; exit } END { exit found ? 0 : 1 }' "$manifest" || true +} + +selected_raw_from_runtime_resources() { + [ -n "$runtime_resources_dir" ] || return 0 + local package_root schema state raw + package_root="$(runtime_resources_root "$runtime_resources_dir")" + schema="$(resource_manifest_value "$package_root" "schema")" + if [ "$schema" != "oliphaunt-runtime-resources-v1" ]; then + echo "iOS extension runtime resources have unsupported schema '${schema:-}'; expected oliphaunt-runtime-resources-v1" >&2 + exit 2 + fi + state="$(resource_manifest_value "$package_root" "mobileStaticRegistryState")" + raw="$(resource_manifest_value "$package_root" "nativeModuleStems")" + if [ "$state" = "pending" ] && [ -n "$raw" ]; then + echo "runtime resources have pending mobile static-registry modules; rebuild them with --mobile-static-module before iOS extension packaging" >&2 + exit 2 + fi + printf '%s\n' "$raw" +} + +portable_id() { + case "$1" in + ""|*[!A-Za-z0-9._-]*) + return 1 + ;; + *) + [ "${#1}" -le 128 ] + ;; + esac +} + +add_selected_pair() { + local sql_name="$1" + local stem="$2" + portable_id "$sql_name" || { + echo "unsupported iOS mobile static extension name: $sql_name" >&2 + exit 2 + } + portable_id "$stem" || { + echo "unsupported iOS mobile static module stem: $stem" >&2 + exit 2 + } + local index + for index in "${!selected_extensions[@]}"; do + if [ "${selected_extensions[$index]}" = "$sql_name" ]; then + if [ "${selected_stems[$index]}" != "$stem" ]; then + echo "iOS mobile static extension $sql_name maps to multiple module stems: ${selected_stems[$index]},$stem" >&2 + exit 2 + fi + return 0 + fi + done + selected_extensions+=("$sql_name") + selected_stems+=("$stem") + local dependency + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + add_selected_dependency "$dependency" + done < <(oliphaunt_mobile_static_extension_dependencies_for_target "$sql_name" ios || true) +} + +add_selected_extension() { + local extension="$1" + local sql_name stem + extension="$(printf '%s\n' "$extension" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [ -n "$extension" ] || return 0 + if ! oliphaunt_mobile_static_extension_spec "$extension" >/dev/null; then + echo "unsupported iOS mobile static extension from OLIPHAUNT_MOBILE_STATIC_EXTENSIONS: $extension" >&2 + echo "for custom prebuilt extensions, pass --runtime-resources so nativeModuleStems are read from the exact resource manifest" >&2 + printf 'supported built-in iOS mobile static extensions: ' >&2 + oliphaunt_mobile_static_supported_extensions | paste -sd ',' - >&2 + exit 2 + fi + sql_name="$(oliphaunt_mobile_static_extension_sql_name "$extension")" + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + add_selected_pair "$sql_name" "$stem" +} + +add_selected_from_runtime_resources() { + [ -n "$runtime_resources_dir" ] || return 0 + local package_root raw old_ifs stems stem sql_name + package_root="$(runtime_resources_root "$runtime_resources_dir")" + raw="$(selected_raw_from_runtime_resources)" + [ -n "$raw" ] || return 0 + old_ifs="$IFS" + IFS="," + read -r -a stems <<< "$raw" + IFS="$old_ifs" + for stem in "${stems[@]}"; do + stem="$(printf '%s\n' "$stem" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [ -n "$stem" ] || continue + sql_name="$(static_registry_manifest_value "$package_root" "module.$stem.extension" || true)" + [ -n "$sql_name" ] || sql_name="$stem" + add_selected_pair "$sql_name" "$stem" + done +} + +add_selected_dependency() { + local dependency="$1" + dependency="$(printf '%s\n' "$dependency" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + [ -n "$dependency" ] || return 0 + portable_id "$dependency" || { + echo "unsupported iOS mobile static dependency name: $dependency" >&2 + exit 2 + } + local existing + for existing in ${selected_dependencies[@]+"${selected_dependencies[@]}"}; do + [ "$existing" = "$dependency" ] && return 0 + done + selected_dependencies+=("$dependency") +} + +add_selected_dependencies_from_runtime_resources() { + [ -n "$runtime_resources_dir" ] || return 0 + local package_root raw old_ifs dependencies dependency + package_root="$(runtime_resources_root "$runtime_resources_dir")" + raw="$(static_registry_manifest_value "$package_root" "dependencyArchives" || true)" + [ -n "$raw" ] || return 0 + old_ifs="$IFS" + IFS="," + read -r -a dependencies <<< "$raw" + IFS="$old_ifs" + for dependency in "${dependencies[@]}"; do + add_selected_dependency "$dependency" + done +} + +parse_selected_extensions() { + local raw="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + if [ -z "$raw" ]; then + add_selected_from_runtime_resources + return 0 + fi + [ -n "$raw" ] || return 0 + local old_ifs="$IFS" + IFS="," + read -r -a requested <<< "$raw" + IFS="$old_ifs" + local extension + for extension in "${requested[@]}"; do + add_selected_extension "$extension" + done +} + +parse_selected_dependencies() { + add_selected_dependencies_from_runtime_resources +} + +stem_for_extension() { + local extension="$1" + local index + for index in "${!selected_extensions[@]}"; do + if [ "${selected_extensions[$index]}" = "$extension" ]; then + printf '%s\n' "${selected_stems[$index]}" + return 0 + fi + done + echo "internal error: missing iOS mobile static module stem for $extension" >&2 + exit 2 +} + +static_registry_archive_candidate() { + local package_root="$1" + local relative="$2" + [ -n "$relative" ] || return 1 + local candidate="$package_root/static-registry/$relative" + [ -f "$candidate" ] || return 1 + printf '%s\n' "$candidate" +} + +archive_for() { + local platform_out="$1" + local extension="$2" + local platform="$3" + local stem + stem="$(stem_for_extension "$extension")" + if [ -n "$runtime_resources_dir" ]; then + local package_root candidate target + package_root="$(runtime_resources_root "$runtime_resources_dir")" + case "$platform" in + simulator) + for target in ios-simulator iphonesimulator aarch64-apple-ios-sim x86_64-apple-ios-sim; do + candidate="$package_root/static-registry/archives/$target/extensions/$stem/liboliphaunt_extension_$stem.a" + if [ -f "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + ;; + device) + for target in ios-device iphoneos aarch64-apple-ios; do + candidate="$package_root/static-registry/archives/$target/extensions/$stem/liboliphaunt_extension_$stem.a" + if [ -f "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + ;; + macos) + for target in macos-arm64 aarch64-apple-darwin; do + candidate="$package_root/static-registry/archives/$target/extensions/$stem/liboliphaunt_extension_$stem.a" + if [ -f "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + ;; + esac + fi + printf '%s\n' "$platform_out/extensions/$stem/liboliphaunt_extension_$stem.a" +} + +dependency_archive_for() { + local dependency="$1" + local platform="$2" + local package_root candidate relative target search_root platform_out + if [ -n "$runtime_resources_dir" ]; then + package_root="$(runtime_resources_root "$runtime_resources_dir")" + case "$platform" in + simulator) + for target in ios-simulator iphonesimulator aarch64-apple-ios-sim x86_64-apple-ios-sim; do + relative="$(static_registry_manifest_value "$package_root" "dependency.$dependency.archive.$target" || true)" + if candidate="$(static_registry_archive_candidate "$package_root" "$relative")"; then + printf '%s\n' "$candidate" + return 0 + fi + search_root="$package_root/static-registry/archives/$target/dependencies/$dependency" + if [ -d "$search_root" ]; then + candidate="$(find "$search_root" -maxdepth 1 -type f -name '*.a' | sort | head -n 1)" + if [ -n "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + done + ;; + device) + for target in ios-device iphoneos aarch64-apple-ios; do + relative="$(static_registry_manifest_value "$package_root" "dependency.$dependency.archive.$target" || true)" + if candidate="$(static_registry_archive_candidate "$package_root" "$relative")"; then + printf '%s\n' "$candidate" + return 0 + fi + search_root="$package_root/static-registry/archives/$target/dependencies/$dependency" + if [ -d "$search_root" ]; then + candidate="$(find "$search_root" -maxdepth 1 -type f -name '*.a' | sort | head -n 1)" + if [ -n "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + done + ;; + macos) + for target in macos-arm64 aarch64-apple-darwin; do + relative="$(static_registry_manifest_value "$package_root" "dependency.$dependency.archive.$target" || true)" + if candidate="$(static_registry_archive_candidate "$package_root" "$relative")"; then + printf '%s\n' "$candidate" + return 0 + fi + search_root="$package_root/static-registry/archives/$target/dependencies/$dependency" + if [ -d "$search_root" ]; then + candidate="$(find "$search_root" -maxdepth 1 -type f -name '*.a' | sort | head -n 1)" + if [ -n "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + done + ;; + esac + fi + case "$platform" in + simulator) platform_out="$simulator_out" ;; + device) platform_out="$device_out" ;; + macos) platform_out="$macos_out" ;; + *) platform_out="" ;; + esac + if [ -n "$platform_out" ]; then + if candidate="$(oliphaunt_mobile_static_dependency_archive_for_root "$platform_out/dependencies" "$dependency")"; then + printf '%s\n' "$candidate" + return 0 + fi + fi + echo "internal error: missing Apple static dependency archive for $dependency on $platform" >&2 + exit 2 +} + +xcframework_for() { + local extension="$1" + local stem + stem="$(stem_for_extension "$extension")" + printf '%s\n' "$out_dir/$stem/liboliphaunt_extension_$stem.xcframework" +} + +dependency_xcframework_for() { + local dependency="$1" + printf '%s\n' "$out_dir/dependencies/$dependency/liboliphaunt_dependency_$dependency.xcframework" +} + +require_inputs() { + local extension="$1" + local simulator_archive device_archive macos_archive + simulator_archive="$(archive_for "$simulator_out" "$extension" simulator)" + device_archive="$(archive_for "$device_out" "$extension" device)" + macos_archive="$(archive_for "$macos_out" "$extension" macos)" + [ -f "$simulator_archive" ] || { + echo "missing iOS simulator extension archive for $extension: $simulator_archive" >&2 + exit 1 + } + [ -f "$device_archive" ] || { + echo "missing iOS device extension archive for $extension: $device_archive" >&2 + exit 1 + } + [ -f "$macos_archive" ] || { + echo "missing macOS arm64 extension archive for $extension: $macos_archive" >&2 + exit 1 + } +} + +require_dependency_inputs() { + local dependency="$1" + local simulator_archive device_archive macos_archive + simulator_archive="$(dependency_archive_for "$dependency" simulator)" + device_archive="$(dependency_archive_for "$dependency" device)" + macos_archive="$(dependency_archive_for "$dependency" macos)" + [ -f "$simulator_archive" ] || { + echo "missing iOS simulator dependency archive for $dependency: $simulator_archive" >&2 + exit 1 + } + [ -f "$device_archive" ] || { + echo "missing iOS device dependency archive for $dependency: $device_archive" >&2 + exit 1 + } + [ -f "$macos_archive" ] || { + echo "missing macOS arm64 dependency archive for $dependency: $macos_archive" >&2 + exit 1 + } +} + +xcframework_ready() { + local extension="$1" + local xcframework + xcframework="$(xcframework_for "$extension")" + [ -d "$xcframework" ] || return 1 + [ -f "$xcframework/Info.plist" ] || return 1 + plutil -extract AvailableLibraries raw "$xcframework/Info.plist" >/dev/null 2>&1 || return 1 +} + +dependency_xcframework_ready() { + local dependency="$1" + local xcframework + xcframework="$(dependency_xcframework_for "$dependency")" + [ -d "$xcframework" ] || return 1 + [ -f "$xcframework/Info.plist" ] || return 1 + plutil -extract AvailableLibraries raw "$xcframework/Info.plist" >/dev/null 2>&1 || return 1 +} + +build_extension_xcframework() { + local extension="$1" + local stem simulator_archive device_archive macos_archive xcframework + stem="$(stem_for_extension "$extension")" + simulator_archive="$(archive_for "$simulator_out" "$extension" simulator)" + device_archive="$(archive_for "$device_out" "$extension" device)" + macos_archive="$(archive_for "$macos_out" "$extension" macos)" + xcframework="$(xcframework_for "$extension")" + rm -rf "$out_dir/$stem" + mkdir -p "$out_dir/$stem" + xcodebuild -create-xcframework \ + -library "$simulator_archive" -headers "$headers_dir" \ + -library "$device_archive" -headers "$headers_dir" \ + -library "$macos_archive" -headers "$headers_dir" \ + -output "$xcframework" >/dev/null +} + +build_dependency_xcframework() { + local dependency="$1" + local simulator_archive device_archive macos_archive xcframework + simulator_archive="$(dependency_archive_for "$dependency" simulator)" + device_archive="$(dependency_archive_for "$dependency" device)" + macos_archive="$(dependency_archive_for "$dependency" macos)" + xcframework="$(dependency_xcframework_for "$dependency")" + rm -rf "$out_dir/dependencies/$dependency" + mkdir -p "$out_dir/dependencies/$dependency" + xcodebuild -create-xcframework \ + -library "$simulator_archive" -headers "$headers_dir" \ + -library "$device_archive" -headers "$headers_dir" \ + -library "$macos_archive" -headers "$headers_dir" \ + -output "$xcframework" >/dev/null +} + +write_manifest() { + { + printf 'packageLayout=oliphaunt-ios-extension-xcframeworks-v1\n' + printf 'extensions=%s\n' "$(join_csv ${selected_extensions[@]+"${selected_extensions[@]}"})" + printf 'nativeModuleStems=%s\n' "$(join_csv ${selected_stems[@]+"${selected_stems[@]}"})" + printf 'dependencies=%s\n' "$(join_csv ${selected_dependencies[@]+"${selected_dependencies[@]}"})" + printf 'simulatorOut=%s\n' "$simulator_out" + printf 'deviceOut=%s\n' "$device_out" + printf 'macosOut=%s\n' "$macos_out" + printf 'runtimeResources=%s\n' "$runtime_resources_dir" + local extension stem + for extension in ${selected_extensions[@]+"${selected_extensions[@]}"}; do + stem="$(stem_for_extension "$extension")" + printf 'extension.%s.xcframework=%s/liboliphaunt_extension_%s.xcframework\n' "$extension" "$stem" "$stem" + printf 'extension.%s.simulatorArchive=%s\n' "$extension" "$(archive_for "$simulator_out" "$extension" simulator)" + printf 'extension.%s.deviceArchive=%s\n' "$extension" "$(archive_for "$device_out" "$extension" device)" + printf 'extension.%s.macosArchive=%s\n' "$extension" "$(archive_for "$macos_out" "$extension" macos)" + done + local dependency + for dependency in ${selected_dependencies[@]+"${selected_dependencies[@]}"}; do + printf 'dependency.%s.xcframework=dependencies/%s/liboliphaunt_dependency_%s.xcframework\n' "$dependency" "$dependency" "$dependency" + printf 'dependency.%s.simulatorArchive=%s\n' "$dependency" "$(dependency_archive_for "$dependency" simulator)" + printf 'dependency.%s.deviceArchive=%s\n' "$dependency" "$(dependency_archive_for "$dependency" device)" + printf 'dependency.%s.macosArchive=%s\n' "$dependency" "$(dependency_archive_for "$dependency" macos)" + done + } > "$manifest_file" +} + +manifest_ready() { + [ -f "$manifest_file" ] || return 1 + grep -Fx "packageLayout=oliphaunt-ios-extension-xcframeworks-v1" "$manifest_file" >/dev/null || return 1 + grep -Fx "extensions=$(join_csv ${selected_extensions[@]+"${selected_extensions[@]}"})" "$manifest_file" >/dev/null || return 1 + grep -Fx "nativeModuleStems=$(join_csv ${selected_stems[@]+"${selected_stems[@]}"})" "$manifest_file" >/dev/null || return 1 + grep -Fx "dependencies=$(join_csv ${selected_dependencies[@]+"${selected_dependencies[@]}"})" "$manifest_file" >/dev/null || return 1 + grep -Fx "macosOut=$macos_out" "$manifest_file" >/dev/null || return 1 +} + +parse_selected_extensions +parse_selected_dependencies +if [ "$mode" = "build" ]; then + rm -rf "$out_dir" +fi +mkdir -p "$out_dir" + +if [ "${#selected_extensions[@]}" -eq 0 ]; then + case "$mode" in + build) write_manifest ;; + check) manifest_ready || { + echo "iOS extension XCFramework manifest is missing or stale" >&2 + exit 1 + } ;; + esac + printf '%s\n' "$out_dir" + exit 0 +fi + +for extension in "${selected_extensions[@]}"; do + require_inputs "$extension" + case "$mode" in + check) + xcframework_ready "$extension" || { + echo "iOS extension XCFramework for $extension is missing or stale" >&2 + exit 1 + } + ;; + build) + build_extension_xcframework "$extension" + ;; + esac +done + +for dependency in ${selected_dependencies[@]+"${selected_dependencies[@]}"}; do + require_dependency_inputs "$dependency" + case "$mode" in + check) + dependency_xcframework_ready "$dependency" || { + echo "iOS dependency XCFramework for $dependency is missing or stale" >&2 + exit 1 + } + ;; + build) + build_dependency_xcframework "$dependency" + ;; + esac +done + +case "$mode" in + check) + manifest_ready || { + echo "iOS extension XCFramework manifest is missing or stale" >&2 + exit 1 + } + ;; + build) + write_manifest + ;; +esac + +printf '%s\n' "$out_dir" diff --git a/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.test.sh b/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.test.sh new file mode 100755 index 00000000..347d1164 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.test.sh @@ -0,0 +1,64 @@ +#!/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 +} +builder="$root/src/runtimes/liboliphaunt/native/bin/build-ios-extension-xcframeworks.sh" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/oliphaunt-ios-extension-packager-test.XXXXXX")" +trap 'rm -rf "$test_root"' EXIT HUP INT TERM + +fail() { + echo "iOS extension XCFramework packager test failed: $*" >&2 + exit 1 +} + +resources="$test_root/resources" +runtime_manifest="$resources/oliphaunt/runtime/manifest.properties" +work_root="$test_root/output" +mkdir -p "$(dirname "$runtime_manifest")" +printf '%s\n' \ + 'schema=oliphaunt-runtime-resources-v1' \ + 'layout=postgres-runtime-files-v1' \ + 'selectedExtensions=' \ + 'extensions=' \ + >"$runtime_manifest" + +run_builder() { + env \ + OLIPHAUNT_IOS_EXTENSION_XCFRAMEWORK_ROOT="$work_root" \ + OLIPHAUNT_IOS_SIMULATOR_OUT="$test_root/ios-simulator" \ + OLIPHAUNT_IOS_DEVICE_OUT="$test_root/ios-device" \ + OLIPHAUNT_MACOS_EXTENSION_OUT="$test_root/macos" \ + "$builder" "$@" +} + +run_builder --runtime-resources "$resources" >/dev/null +output_manifest="$work_root/out/manifest.properties" +[ -f "$output_manifest" ] || fail "build did not emit a manifest" +grep -Fx 'packageLayout=oliphaunt-ios-extension-xcframeworks-v1' "$output_manifest" >/dev/null || + fail "manifest has the wrong package layout" +grep -Fx 'extensions=' "$output_manifest" >/dev/null || + fail "empty selection was not preserved" +run_builder --check-current --runtime-resources "$resources" >/dev/null + +awk '{ if ($0 == "extensions=") print "extensions=vector"; else print }' \ + "$output_manifest" >"$output_manifest.stale" +mv "$output_manifest.stale" "$output_manifest" +if run_builder --check-current --runtime-resources "$resources" >/dev/null 2>&1; then + fail "check-current accepted a stale selection manifest" +fi + +invalid_resources="$test_root/invalid-resources" +mkdir -p "$invalid_resources/oliphaunt/runtime" +printf '%s\n' \ + 'schema=oliphaunt-runtime-resources-v0' \ + 'layout=postgres-runtime-files-v1' \ + 'extensions=' \ + >"$invalid_resources/oliphaunt/runtime/manifest.properties" +if run_builder --runtime-resources "$invalid_resources" >/dev/null 2>&1; then + fail "build accepted an unsupported runtime-resource schema" +fi + +printf 'iOS extension XCFramework packager behavior verified.\n' diff --git a/src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh b/src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh new file mode 100755 index 00000000..8183bc5d --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/build-output.bash" +script_path="$script_dir/build-ios-xcframework.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +work_root="${OLIPHAUNT_IOS_XCFRAMEWORK_ROOT:-$repo_root/target/liboliphaunt-ios-xcframework}" +out_dir="$work_root/out" +headers_dir="$work_root/include" +xcframework_out="$out_dir/liboliphaunt.xcframework" +stamp="$work_root/.liboliphaunt-ios-xcframework.sha256" +script_mode="${1:-build}" +runtime_resources_root="${OLIPHAUNT_IOS_RUNTIME_RESOURCES_ROOT:-}" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "liboliphaunt iOS XCFramework build requires Darwin" >&2 + exit 2 +fi + +for cmd in install_name_tool nm plutil rg shasum xcodebuild xcrun; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "missing required command: $cmd" >&2 + exit 1 + fi +done + +simulator_script="$repo_root/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh" +device_script="$repo_root/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh" +macos_script="$repo_root/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh" +public_header="$repo_root/src/runtimes/liboliphaunt/native/include/oliphaunt.h" +default_macos_library="$repo_root/target/liboliphaunt-pg18/out/liboliphaunt.dylib" +default_simulator_library="$repo_root/target/liboliphaunt-ios-simulator/out/liboliphaunt.dylib" +default_device_library="$repo_root/target/liboliphaunt-ios-device/out/liboliphaunt.dylib" +default_simulator_static_registry="$repo_root/target/liboliphaunt-ios-simulator/out/liboliphaunt_mobile_static_registry.c" +default_device_static_registry="$repo_root/target/liboliphaunt-ios-device/out/liboliphaunt_mobile_static_registry.c" +xcframework_static_registry="$out_dir/liboliphaunt_mobile_static_registry.c" +framework_root="$work_root/frameworks" +macos_framework="$framework_root/macos-arm64/liboliphaunt.framework" +simulator_framework="$framework_root/ios-arm64-simulator/liboliphaunt.framework" +device_framework="$framework_root/ios-arm64/liboliphaunt.framework" + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh [--check-current] +MSG +} + +desired_hash() { + { + printf 'mobile_static_extensions=%s\n' "${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + printf 'runtime_resources_root=%s\n' "$runtime_resources_root" + printf 'script_sha256=%s\n' "$(shasum -a 256 "$script_path" | awk '{print $1}')" + shasum -a 256 "$macos_script" "$simulator_script" "$device_script" "$public_header" + if [ -d "$runtime_resources_root" ]; then + find "$runtime_resources_root" -type f -print0 | sort -z | xargs -0 shasum -a 256 + fi + if [ -f "$default_simulator_library" ]; then + shasum -a 256 "$default_simulator_library" + fi + if [ -f "$default_device_library" ]; then + shasum -a 256 "$default_device_library" + fi + if [ -f "$default_macos_library" ]; then + shasum -a 256 "$default_macos_library" + fi + if [ -f "$xcframework_static_registry" ]; then + shasum -a 256 "$xcframework_static_registry" + fi + } | shasum -a 256 | awk '{print $1}' +} + +library_platform() { + xcrun vtool -show-build "$1" 2>/dev/null | awk '/platform /{print $2; exit}' +} + +assert_library_slice() { + local library="$1" + local expected_platform="$2" + [ -f "$library" ] || { + echo "missing liboliphaunt library: $library" >&2 + return 1 + } + local platform + platform="$(library_platform "$library")" + [ "$platform" = "$expected_platform" ] || { + echo "liboliphaunt library has platform $platform, expected $expected_platform: $library" >&2 + return 1 + } + local symbols + symbols="$(nm -g "$library" 2>/dev/null || true)" + if [ "$expected_platform" != "MACOS" ]; then + local undefined_symbols + undefined_symbols="$(nm -u "$library" 2>/dev/null || true)" + if printf '%s\n' "$undefined_symbols" | rg -q '_shm(get|ctl|dt)|_shm_open|_sem(get|ctl|op|open|close|unlink|wait|post|trywait|init|destroy)'; then + echo "liboliphaunt library imports mobile-forbidden shared-memory/semaphore APIs: $library" >&2 + return 1 + fi + fi + local symbol + for symbol in \ + _oliphaunt_init \ + _oliphaunt_init_ex \ + _oliphaunt_exec_protocol \ + _oliphaunt_exec_protocol_stream \ + _oliphaunt_backup \ + _oliphaunt_backup_ex \ + _oliphaunt_restore \ + _oliphaunt_cancel \ + _oliphaunt_detach \ + _oliphaunt_close \ + _oliphaunt_register_static_extensions \ + _oliphaunt_last_error \ + _oliphaunt_version \ + _oliphaunt_capabilities \ + _oliphaunt_free_response + do + case "$symbols" in + *"$symbol"*) ;; + *) + echo "liboliphaunt library is missing symbol $symbol: $library" >&2 + return 1 + ;; + esac + done +} + +xcframework_ready() { + [ -d "$xcframework_out" ] || return 1 + [ -f "$xcframework_out/Info.plist" ] || return 1 + plutil -extract AvailableLibraries raw "$xcframework_out/Info.plist" >/dev/null 2>&1 || return 1 + + local ios_library="" + local simulator_library="" + local macos_library="" + while IFS= read -r library; do + case "$(library_platform "$library")" in + MACOS) macos_library="$library" ;; + IOS) ios_library="$library" ;; + IOSSIMULATOR) simulator_library="$library" ;; + esac + done < <(find "$xcframework_out" -type f \( -name 'liboliphaunt.dylib' -o -name 'liboliphaunt' \) ! -path '*/Headers/*' | sort) + + [ -n "$macos_library" ] || return 1 + [ -n "$ios_library" ] || return 1 + [ -n "$simulator_library" ] || return 1 + assert_library_slice "$macos_library" MACOS || return 1 + assert_library_slice "$ios_library" IOS || return 1 + assert_library_slice "$simulator_library" IOSSIMULATOR || return 1 +} + +write_framework_info_plist() { + local plist="$1" + local platform="$2" + local platform_family="$3" + if [ "$platform" = "MacOSX" ]; then + cat >"$plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + liboliphaunt + CFBundleIdentifier + dev.oliphaunt.liboliphaunt + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + liboliphaunt + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.1.0 + CFBundleSupportedPlatforms + + MacOSX + + CFBundleVersion + 1 + MinimumOSVersion + 14.0 + + +PLIST + return + fi + cat >"$plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + liboliphaunt + CFBundleIdentifier + dev.oliphaunt.liboliphaunt + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + liboliphaunt + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.1.0 + CFBundleSupportedPlatforms + + ${platform} + + CFBundleVersion + 1 + MinimumOSVersion + 17.0 + UIDeviceFamily + + ${platform_family} + + + +PLIST +} + +expected_library_platform_for_framework_platform() { + case "$1" in + MacOSX) printf 'MACOS\n' ;; + iPhoneOS) printf 'IOS\n' ;; + iPhoneSimulator) printf 'IOSSIMULATOR\n' ;; + *) echo "unsupported framework platform $1" >&2; return 1 ;; + esac +} + +prepare_framework_slice() { + local library="$1" + local framework="$2" + local platform="$3" + local platform_family="$4" + rm -rf "$framework" + mkdir -p "$framework/Headers" "$framework/Modules" + cp "$library" "$framework/liboliphaunt" + install_name_tool -id "@rpath/liboliphaunt.framework/liboliphaunt" "$framework/liboliphaunt" + rsync -a --delete "$headers_dir/" "$framework/Headers/" + if [ -n "$runtime_resources_root" ]; then + [ -d "$runtime_resources_root" ] || { + echo "OLIPHAUNT_IOS_RUNTIME_RESOURCES_ROOT does not exist: $runtime_resources_root" >&2 + exit 1 + } + mkdir -p "$framework/Resources/oliphaunt" + rsync -a --delete "$runtime_resources_root/" "$framework/Resources/oliphaunt/" + fi + cat >"$framework/Modules/module.modulemap" <<'MODULEMAP' +framework module liboliphaunt { + umbrella header "oliphaunt.h" + export * + module * { export * } +} +MODULEMAP + write_framework_info_plist "$framework/Info.plist" "$platform" "$platform_family" + assert_library_slice "$framework/liboliphaunt" "$(expected_library_platform_for_framework_platform "$platform")" +} + +build_xcframework() { + mkdir -p "$out_dir" "$headers_dir" + rsync -a --delete "$repo_root/src/runtimes/liboliphaunt/native/include/" "$headers_dir/" + + local macos_library="$default_macos_library" + if ! "$macos_script" --check-oliphaunt-current >/dev/null 2>&1 || + ! assert_library_slice "$macos_library" MACOS >/dev/null 2>&1; then + macos_library="$(oliphaunt_capture_build_artifact_path \ + "macOS liboliphaunt build" \ + "$work_root/logs/build-macos.log" \ + "$macos_script")" + fi + + local simulator_library="$default_simulator_library" + local device_library="$default_device_library" + if ! OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" \ + "$simulator_script" --check-current >/dev/null 2>&1 || + ! assert_library_slice "$simulator_library" IOSSIMULATOR >/dev/null 2>&1; then + simulator_library="$(oliphaunt_capture_build_artifact_path \ + "iOS simulator liboliphaunt build" \ + "$work_root/logs/build-ios-simulator.log" \ + env OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" "$simulator_script")" + fi + if ! OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" \ + "$device_script" --check-current >/dev/null 2>&1 || + ! assert_library_slice "$device_library" IOS >/dev/null 2>&1; then + device_library="$(oliphaunt_capture_build_artifact_path \ + "iOS device liboliphaunt build" \ + "$work_root/logs/build-ios-device.log" \ + env OLIPHAUNT_MOBILE_STATIC_EXTENSIONS="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" "$device_script")" + fi + assert_library_slice "$macos_library" MACOS + assert_library_slice "$simulator_library" IOSSIMULATOR + assert_library_slice "$device_library" IOS + prepare_framework_slice "$macos_library" "$macos_framework" "MacOSX" "0" + prepare_framework_slice "$device_library" "$device_framework" "iPhoneOS" "1" + prepare_framework_slice "$simulator_library" "$simulator_framework" "iPhoneSimulator" "1" + + rm -rf "$xcframework_out" + xcodebuild -create-xcframework \ + -framework "$macos_framework" \ + -framework "$device_framework" \ + -framework "$simulator_framework" \ + -output "$xcframework_out" >/dev/null + + if [ -f "$default_simulator_static_registry" ] && [ -f "$default_device_static_registry" ]; then + if ! cmp -s "$default_simulator_static_registry" "$default_device_static_registry"; then + echo "iOS simulator/device static registry sources differ" >&2 + exit 1 + fi + cp "$default_simulator_static_registry" "$xcframework_static_registry" + else + rm -f "$xcframework_static_registry" + fi + + xcframework_ready + desired_hash > "$stamp" + printf '%s\n' "$xcframework_out" +} + +case "$script_mode" in + build) + if xcframework_ready && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + printf '%s\n' "$xcframework_out" + exit 0 + fi + build_xcframework + ;; + --check-current) + if xcframework_ready && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "iOS liboliphaunt XCFramework is current" + exit 0 + fi + echo "iOS liboliphaunt XCFramework is missing or stale" >&2 + exit 1 + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh b/src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh new file mode 100755 index 00000000..c80d44a7 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh @@ -0,0 +1,463 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +script_path="$script_dir/$(basename "${BASH_SOURCE[0]}")" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/mobile-static-extensions.sh" +. "$script_dir/mobile-postgis-extensions.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" + +oliphaunt_mobile_target="macos-arm64" +pg_version="18.4" +runtime_root="${OLIPHAUNT_MACOS_RUNTIME_ROOT:-${OLIPHAUNT_WORK_ROOT:-$repo_root/target/liboliphaunt-pg18}}" +work_root="${OLIPHAUNT_MACOS_EXTENSION_ARCHIVE_ROOT:-$runtime_root/macos-extension-archives}" +build_dir="$runtime_root/postgresql-$pg_version" +install_dir="$runtime_root/install" +out_dir="$work_root/out" +make_log="$work_root/make.log" +stamp="$out_dir/.macos-extension-archives.inputs.sha256" +mobile_static_dependency_root="$out_dir/dependencies" +export OLIPHAUNT_MOBILE_STATIC_DEPENDENCY_ROOT="$mobile_static_dependency_root" +mobile_static_extensions=() +mobile_static_objects=() +mobile_static_dependency_archives=() +script_mode="${1:-build}" + +fail() { + echo "build-macos-extension-archives.sh: $*" >&2 + exit 1 +} + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-macos-extension-archives.sh [--check-current] + +Builds arm64 macOS static extension and dependency archives from the same +canonical source inventory and symbol-prefix contract used by the iOS lanes. +OLIPHAUNT_MOBILE_STATIC_EXTENSIONS must contain the selected SQL names. +MSG +} + +case "$script_mode" in + build|--check-current) ;; + -h|--help) + usage + exit 0 + ;; + *) + usage + exit 2 + ;; +esac + +[ "$(uname -s)" = "Darwin" ] || fail "arm64 macOS extension archives require Darwin" +for cmd in cmake git make nm perl rsync shasum xcrun; do + command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd" +done + +sdk_path="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)" +clang_path="$(xcrun --find --sdk macosx clang 2>/dev/null || true)" +clangxx_path="$(xcrun --find --sdk macosx clang++ 2>/dev/null || true)" +libtool_path="$(xcrun --find --sdk macosx libtool 2>/dev/null || true)" +ar_path="$(xcrun --find --sdk macosx ar 2>/dev/null || true)" +ranlib_path="$(xcrun --find --sdk macosx ranlib 2>/dev/null || true)" +for value in "$sdk_path" "$clang_path" "$clangxx_path" "$libtool_path" "$ar_path" "$ranlib_path"; do + [ -n "$value" ] || fail "macOS SDK compiler tools are unavailable" +done + +min_macos="${MACOSX_DEPLOYMENT_TARGET:-11.0}" +case "$min_macos" in + ""|*[!0-9.]*) fail "MACOSX_DEPLOYMENT_TARGET must be a numeric dotted version" ;; +esac +min_ios="${OLIPHAUNT_IOS_MIN_VERSION:-17.0}" +cc=( + "$clang_path" + -target "arm64-apple-macos${min_macos}" + "-mmacosx-version-min=${min_macos}" + -isysroot "$sdk_path" +) +cxx=( + "$clangxx_path" + -target "arm64-apple-macos${min_macos}" + "-mmacosx-version-min=${min_macos}" + -isysroot "$sdk_path" +) +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + ccache_bin="" + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" +native_cflags="$(oliphaunt_native_release_cflags -fPIC "-mmacosx-version-min=$min_macos" -DOLIPHAUNT_EMBEDDED)" +icu_prefix="$runtime_root/icu-macos" +pg_extension_cflags="$native_cflags $(oliphaunt_icu_cflags "$icu_prefix")" +read -r -a native_cflag_args <<< "$native_cflags" +read -r -a pg_extension_cflag_args <<< "$pg_extension_cflags" +jobs="${OLIPHAUNT_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" + +parse_mobile_static_extensions() { + local raw="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + [ -n "$raw" ] || fail "OLIPHAUNT_MOBILE_STATIC_EXTENSIONS must select at least one native extension" + local extension sql_name + while IFS= read -r extension; do + extension="$(printf '%s' "$extension" | xargs)" + [ -n "$extension" ] || continue + oliphaunt_mobile_static_extension_spec "$extension" >/dev/null || { + printf 'supported static extensions: ' >&2 + oliphaunt_mobile_static_supported_extensions | paste -sd ',' - >&2 + fail "unsupported macOS static extension: $extension" + } + sql_name="$(oliphaunt_mobile_static_extension_sql_name "$extension")" + case " ${mobile_static_extensions[*]-} " in + *" $sql_name "*) fail "duplicate macOS static extension selection: $sql_name" ;; + esac + mobile_static_extensions+=("$sql_name") + done < <(printf '%s\n' "$raw" | tr ',' '\n') + [ "${#mobile_static_extensions[@]}" -gt 0 ] || fail "no native macOS static extensions were selected" +} + +mobile_static_extensions_include() { + local wanted="$1" + local extension + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + [ "$extension" = "$wanted" ] && return 0 + done + return 1 +} + +mobile_static_dependency_selected() { + local wanted="$1" + local extension dependency + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + [ "$dependency" = "$wanted" ] && return 0 + done < <(oliphaunt_mobile_static_extension_dependencies_for_target "$extension" macos-arm64) + done + return 1 +} + +selected_dependencies() { + local extension + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + oliphaunt_mobile_static_extension_dependencies_for_target "$extension" macos-arm64 + done | sed '/^$/d' | LC_ALL=C sort -u +} + +hash_mobile_static_extension_sources() { + local extension file + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r file; do + [ -n "$file" ] || continue + shasum -a 256 "$file" + done < <(oliphaunt_mobile_static_extension_hash_inputs "$repo_root" "$build_dir" "$extension") + done +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'sdk_path=%s\n' "$sdk_path" + printf 'clang_path=%s\n' "$clang_path" + printf 'clangxx_path=%s\n' "$clangxx_path" + printf 'min_macos=%s\n' "$min_macos" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'pg_extension_cflags=%s\n' "$pg_extension_cflags" + printf 'extensions=%s\n' "${mobile_static_extensions[*]-}" + printf 'dependencies=%s\n' "$(selected_dependencies | paste -sd ',' -)" + shasum -a 256 "$script_path" "$script_dir/mobile-static-extensions.sh" "$script_dir/mobile-postgis-extensions.sh" + hash_mobile_static_extension_sources + } | shasum -a 256 | awk '{print $1}' +} + +require_runtime_tree() { + [ -d "$build_dir/src/include" ] || fail "macOS PostgreSQL build tree is missing: $build_dir" + [ -d "$install_dir/share/postgresql" ] || fail "macOS PostgreSQL install tree is missing: $install_dir" + [ -d "$icu_prefix/include" ] || fail "macOS ICU build is missing: $icu_prefix" +} + +build_openssl_dependency() { + mobile_static_dependency_selected openssl || return 0 + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/openssl" + local dependency_dir="$mobile_static_dependency_root/openssl" + local build_root="$work_root/openssl-macos-arm64" + local install_root="$work_root/openssl-macos-arm64-install" + local installed_archive="" + local archive="$dependency_dir/libcrypto.a" + if [ -f "$archive" ] && [ -d "$dependency_dir/include/openssl" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -f "$source_dir/Configure" ] || fail "pinned OpenSSL checkout is missing: $source_dir" + rm -rf "$build_root" "$install_root" "$dependency_dir" + mkdir -p "$build_root" "$dependency_dir/include" + rsync -a --delete --exclude .git "$source_dir/" "$build_root/" + ( + cd "$build_root" + CC="$cc_string" AR="$ar_path" RANLIB="$ranlib_path" \ + CFLAGS="-O2 -fPIC -mmacosx-version-min=${min_macos}" \ + ./Configure darwin64-arm64-cc \ + no-shared no-tests no-apps no-docs no-engine no-module \ + --prefix="$install_root" \ + --openssldir="$install_root/ssl" >> "$make_log" 2>&1 + make -j"$jobs" build_generated libcrypto.a >> "$make_log" 2>&1 + make install_dev >> "$make_log" 2>&1 + ) + for candidate in "$install_root/lib/libcrypto.a" "$install_root/lib64/libcrypto.a"; do + if [ -f "$candidate" ]; then + installed_archive="$candidate" + break + fi + done + [ -n "$installed_archive" ] || fail "OpenSSL macOS arm64 build did not produce libcrypto.a" + cp -R "$install_root/include/openssl" "$dependency_dir/include/" + cp -p "$installed_archive" "$archive" + mobile_static_dependency_archives+=("$archive") +} + +build_uuid_dependency() { + mobile_static_dependency_selected uuid || return 0 + local source_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" + local dependency_dir="$mobile_static_dependency_root/uuid" + local archive="$dependency_dir/lib/libuuid.a" + local object="$dependency_dir/portable_uuid.o" + if [ -f "$archive" ] && [ -d "$dependency_dir/include/uuid" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -f "$source_dir/portable_uuid.c" ] || fail "portable UUID source is missing: $source_dir" + rm -rf "$dependency_dir" + mkdir -p "$dependency_dir/include" "$dependency_dir/lib" + cp -R "$source_dir/include/uuid" "$dependency_dir/include/" + "${cc[@]}" "${native_cflag_args[@]}" \ + -I"$source_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$source_dir/portable_uuid.c" \ + -o "$object" + "$libtool_path" -static -o "$archive" "$object" + [ -s "$archive" ] || fail "portable UUID macOS arm64 build did not produce $archive" + mobile_static_dependency_archives+=("$archive") +} + +build_static_dependencies() { + mkdir -p "$mobile_static_dependency_root" + build_openssl_dependency + build_uuid_dependency + build_postgis_mobile_static_dependencies +} + +archive_mobile_static_extension_objects() { + local extension="$1" + local object_dir="$2" + local objects_file="$3" + local stem archive object + local -a archive_objects=() + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + archive="$object_dir/liboliphaunt_extension_$stem.a" + while IFS= read -r object; do + [ -n "$object" ] && archive_objects+=("$object") + done < "$objects_file" + [ "${#archive_objects[@]}" -gt 0 ] || fail "macOS static extension $extension has no object inputs" + rm -f "$archive" + "$libtool_path" -static -o "$archive" "${archive_objects[@]}" + [ -s "$archive" ] || fail "macOS static extension $extension did not produce $archive" +} + +build_static_extension_objects() { + local extension source source_dir source_rel object object_dir objects_file stem prefix include_dir cflag + local -a compile_args extension_include_args extension_cflags + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + if [ "$extension" = "postgis" ]; then + build_postgis_mobile_static_extension_objects "$extension" + continue + fi + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + source_dir="$(oliphaunt_mobile_static_extension_source_dir "$repo_root" "$build_dir" "$extension")" + extension_include_args=() + while IFS= read -r include_dir; do + [ -n "$include_dir" ] || continue + extension_include_args+=("-I$include_dir") + done < <(oliphaunt_mobile_static_extension_include_dirs "$repo_root" "$build_dir" "$extension") + extension_cflags=() + while IFS= read -r cflag; do + [ -n "$cflag" ] || continue + extension_cflags+=("$cflag") + done < <(oliphaunt_mobile_static_extension_cflags "$extension") + if [ "$(oliphaunt_mobile_static_extension_kind "$extension")" = "contrib" ]; then + ( + cd "$build_dir" + make -C "$(oliphaunt_mobile_static_extension_source_rel "$extension")" \ + CC="$cc_string" \ + CFLAGS="$pg_extension_cflags" \ + all >> "$make_log" 2>&1 || true + ) + fi + object_dir="$out_dir/extensions/$stem" + objects_file="$object_dir/objects.list" + rm -rf "$object_dir" + mkdir -p "$object_dir" + : > "$objects_file" + while IFS= read -r source; do + [ -n "$source" ] || continue + [ -f "$source" ] || fail "macOS static extension source is missing: $source" + source_rel="${source#"$source_dir"/}" + object="$object_dir/${source_rel%.c}.o" + mkdir -p "$(dirname "$object")" + mobile_static_objects+=("$object") + printf '%s\n' "$object" >> "$objects_file" + compile_args=( + "${cc[@]}" "${pg_extension_cflag_args[@]}" + -DPg_magic_func="${prefix}_Pg_magic_func" + -D_PG_init="${prefix}__PG_init" + ) + if [ "${#extension_cflags[@]}" -gt 0 ]; then + compile_args+=("${extension_cflags[@]}") + fi + if [ "${#extension_include_args[@]}" -gt 0 ]; then + compile_args+=("${extension_include_args[@]}") + fi + compile_args+=( + -I"$build_dir/src/include" + -I"$build_dir/src/include/port" + -c "$source" + -o "$object" + ) + "${compile_args[@]}" + done < <(oliphaunt_mobile_static_extension_source_files "$repo_root" "$build_dir" "$extension") + [ -s "$objects_file" ] || fail "macOS static extension $extension did not produce object inputs" + archive_mobile_static_extension_objects "$extension" "$object_dir" "$objects_file" + done +} + +defined_c_symbols() { + local stem="$1" + local prefix="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | + awk ' + $2 == "T" { + symbol = $3 + sub(/^_/, "", symbol) + if (symbol == "" || index(symbol, prefix "_") == 1 || symbol == "Pg_magic_func" || symbol == "_PG_init") next + if (symbol ~ /^[A-Za-z_][A-Za-z0-9_]*$/) print symbol + } + ' prefix="$prefix" | + LC_ALL=C sort -u +} + +module_has_c_symbol() { + local stem="$1" + local symbol="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | awk -v wanted="_$symbol" '$3 == wanted { found = 1 } END { exit found ? 0 : 1 }' +} + +write_registration_symbol_files() { + local extension stem prefix symbols_file + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + defined_c_symbols "$stem" "$prefix" > "$symbols_file" + module_has_c_symbol "$stem" "${prefix}_Pg_magic_func" || + fail "macOS static extension $extension lacks ${prefix}_Pg_magic_func" + if [ ! -s "$symbols_file" ] && ! module_has_c_symbol "$stem" "${prefix}__PG_init"; then + fail "macOS static extension $extension produced neither SQL-visible symbols nor an init hook" + fi + done +} + +dependency_archive() { + case "$1" in + openssl) printf '%s\n' "$mobile_static_dependency_root/openssl/libcrypto.a" ;; + uuid) printf '%s\n' "$mobile_static_dependency_root/uuid/lib/libuuid.a" ;; + geos) printf '%s\n' "$mobile_static_dependency_root/geos/lib/libgeos.a" ;; + geos-c) printf '%s\n' "$mobile_static_dependency_root/geos/lib/libgeos_c.a" ;; + json-c) printf '%s\n' "$mobile_static_dependency_root/json-c/lib/libjson-c.a" ;; + libxml2) printf '%s\n' "$mobile_static_dependency_root/libxml2/lib/libxml2.a" ;; + proj) printf '%s\n' "$mobile_static_dependency_root/proj/lib/libproj.a" ;; + sqlite) printf '%s\n' "$mobile_static_dependency_root/sqlite/lib/libsqlite3.a" ;; + *) fail "unsupported macOS extension dependency identity: $1" ;; + esac +} + +artifacts_ready() { + local extension stem archive dependency + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + archive="$out_dir/extensions/$stem/liboliphaunt_extension_$stem.a" + [ -s "$archive" ] || return 1 + [ -s "$out_dir/extensions/$stem/objects.list" ] || return 1 + [ -f "$out_dir/extensions/$stem/symbols.list" ] || return 1 + done + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + archive="$(dependency_archive "$dependency")" + [ -s "$archive" ] || return 1 + done < <(selected_dependencies) +} + +write_manifest() { + local extension stem dependency archive + { + printf 'packageLayout=oliphaunt-macos-extension-archives-v1\n' + printf 'target=macos-arm64\n' + printf 'minimumMacOS=%s\n' "$min_macos" + printf 'extensions=%s\n' "$(printf '%s\n' "${mobile_static_extensions[@]}" | LC_ALL=C sort | paste -sd ',' -)" + printf 'dependencies=%s\n' "$(selected_dependencies | paste -sd ',' -)" + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + printf 'extension.%s.archive=extensions/%s/liboliphaunt_extension_%s.a\n' "$extension" "$stem" "$stem" + done + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + archive="$(dependency_archive "$dependency")" + printf 'dependency.%s.archive=%s\n' "$dependency" "${archive#"$out_dir"/}" + done < <(selected_dependencies) + } > "$out_dir/manifest.properties" +} + +parse_mobile_static_extensions +require_runtime_tree +mkdir -p "$out_dir" + +if [ "$script_mode" = "--check-current" ]; then + if artifacts_ready && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "macOS arm64 extension archives are current" + exit 0 + fi + fail "macOS arm64 extension archives are missing or stale" +fi + +if artifacts_ready && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "$out_dir" + exit 0 +fi + +rm -rf "$out_dir" +mkdir -p "$out_dir" "$mobile_static_dependency_root" +: > "$make_log" +build_static_dependencies +build_static_extension_objects +write_registration_symbol_files +artifacts_ready || fail "macOS arm64 extension archive set is incomplete" +write_manifest +desired_hash > "$stamp" +echo "$out_dir" diff --git a/src/runtimes/liboliphaunt/native/bin/build-output.bash b/src/runtimes/liboliphaunt/native/bin/build-output.bash new file mode 100644 index 00000000..2ad94b2f --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-output.bash @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +oliphaunt_capture_build_artifact_path() { + local description="${1:?oliphaunt_capture_build_artifact_path requires a description}" + shift + local log_file="${1:?oliphaunt_capture_build_artifact_path requires a log file}" + shift + local log_dir tmp status artifact + + log_dir="$(dirname "$log_file")" + mkdir -p "$log_dir" + tmp="$(mktemp "${TMPDIR:-/tmp}/oliphaunt-build-output.XXXXXX")" + + set +e + "$@" 2>&1 | tee "$tmp" | tee "$log_file" >&2 + status="${PIPESTATUS[0]}" + set -e + + if [ "$status" -ne 0 ]; then + rm -f "$tmp" + echo "error: $description failed; see $log_file" >&2 + return "$status" + fi + + artifact="" + while IFS= read -r line; do + [ -n "$line" ] || continue + if [ -e "$line" ]; then + artifact="$line" + fi + done < "$tmp" + if [ -z "$artifact" ]; then + artifact="$(awk 'NF { line = $0 } END { if (line != "") print line }' "$tmp")" + fi + rm -f "$tmp" + if [ -z "$artifact" ]; then + echo "error: $description did not print an artifact path; see $log_file" >&2 + return 1 + fi + + printf '%s\n' "$artifact" +} diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh new file mode 100755 index 00000000..e4bf8b39 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh @@ -0,0 +1,987 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/mobile-static-extensions.sh" +. "$script_dir/mobile-postgis-extensions.sh" +script_path="$script_dir/$(basename "$0")" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" +android_abi="${OLIPHAUNT_ANDROID_ABI:-arm64-v8a}" +case "$android_abi" in + arm64-v8a) + android_host="aarch64-linux-android" + android_readelf_arch_regex="AArch64" + oliphaunt_mobile_target="android-arm64" + android_work_root="${OLIPHAUNT_ANDROID_WORK_ROOT:-${OLIPHAUNT_ANDROID_ROOT:-${OLIPHAUNT_ANDROID_ARM64_ROOT:-$repo_root/target/liboliphaunt-pg18-android-arm64}}}" + ;; + x86_64) + android_host="x86_64-linux-android" + android_readelf_arch_regex="X86-64|Advanced Micro Devices X86-64" + oliphaunt_mobile_target="android-x86_64" + android_work_root="${OLIPHAUNT_ANDROID_WORK_ROOT:-${OLIPHAUNT_ANDROID_ROOT:-${OLIPHAUNT_ANDROID_X86_64_ROOT:-$repo_root/target/liboliphaunt-pg18-android-x86_64}}}" + ;; + *) + echo "error: unsupported Android ABI '$android_abi'; expected arm64-v8a or x86_64" >&2 + exit 1 + ;; +esac +work_root="$android_work_root" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +out_dir="$work_root/out" +stamp="$build_dir/.liboliphaunt-android-${android_abi}-build.sha256" +configure_log="$work_root/configure.log" +make_log="$work_root/make.log" +objects_rsp="$out_dir/liboliphaunt-android-${android_abi}-objects.rsp" +lib_out="$out_dir/liboliphaunt.so" +mobile_static_registry_source="$out_dir/liboliphaunt_mobile_static_registry.c" +mobile_static_registry_object="$out_dir/liboliphaunt_mobile_static_registry.o" +script_mode="${1:-build}" +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-$oliphaunt_mobile_target-build" +icu_prefix="$work_root/icu-$oliphaunt_mobile_target" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lc++_static -lc++abi" +icu_libs="$icu_static_libs $icu_cpp_libs" + +liboliphaunt_sources=( + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) + +plpgsql_objects=( + src/pl/plpgsql/src/pl_comp.o + src/pl/plpgsql/src/pl_exec.o + src/pl/plpgsql/src/pl_funcs.o + src/pl/plpgsql/src/pl_gram.o + src/pl/plpgsql/src/pl_handler.o + src/pl/plpgsql/src/pl_scanner.o +) + +jit_objects=( + src/backend/jit/jit.o +) + +mobile_static_extensions=() +mobile_static_objects=() +mobile_static_dependency_archives=() +mobile_static_dependency_root="$out_dir/dependencies" +export OLIPHAUNT_MOBILE_STATIC_DEPENDENCY_ROOT="$mobile_static_dependency_root" + +fail() { + echo "error: $*" >&2 + exit 1 +} + +for cmd in curl git patch perl rg shasum; do + command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd" +done + +if [ -z "${ANDROID_HOME:-}" ] && [ -d "$HOME/Library/Android/sdk" ]; then + export ANDROID_HOME="$HOME/Library/Android/sdk" +fi +[ -n "${ANDROID_HOME:-}" ] || fail "ANDROID_HOME is not set" + +ndk_root="${ANDROID_NDK_HOME:-${ANDROID_NDK_ROOT:-}}" +if [ -z "$ndk_root" ]; then + ndk_root="$(find "$ANDROID_HOME/ndk" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort -V | tail -1 || true)" +fi +[ -n "$ndk_root" ] && [ -d "$ndk_root" ] || fail "Android NDK not found under ANDROID_HOME=$ANDROID_HOME" + +android_ndk_prebuilt_candidates() { + case "$(uname -s):$(uname -m)" in + Darwin:arm64 | Darwin:aarch64) + printf '%s\n' darwin-arm64 darwin-x86_64 + ;; + Darwin:x86_64) + printf '%s\n' darwin-x86_64 + ;; + Linux:x86_64 | Linux:amd64) + printf '%s\n' linux-x86_64 + ;; + Linux:aarch64 | Linux:arm64) + printf '%s\n' linux-aarch64 linux-x86_64 + ;; + *) + return 1 + ;; + esac +} + +toolchain_dir="" +while IFS= read -r prebuilt_host; do + candidate="$ndk_root/toolchains/llvm/prebuilt/$prebuilt_host" + if [ -d "$candidate/bin" ]; then + toolchain_dir="$candidate" + break + fi +done < <(android_ndk_prebuilt_candidates || true) +[ -n "$toolchain_dir" ] || fail "Android NDK LLVM toolchain not found under $ndk_root for host $(uname -s)/$(uname -m)" + +android_api="${OLIPHAUNT_ANDROID_API_LEVEL:-24}" +clang_path="$toolchain_dir/bin/${android_host}${android_api}-clang" +clangxx_path="$toolchain_dir/bin/${android_host}${android_api}-clang++" +cpp_path="$clang_path -E" +llvm_nm="$toolchain_dir/bin/llvm-nm" +llvm_ar="$toolchain_dir/bin/llvm-ar" +llvm_ranlib="$toolchain_dir/bin/llvm-ranlib" +[ -x "$clang_path" ] || fail "Android clang not found: $clang_path" +[ -x "$clangxx_path" ] || fail "Android clang++ not found: $clangxx_path" +[ -x "$llvm_nm" ] || fail "Android llvm-nm not found: $llvm_nm" +[ -x "$llvm_ar" ] || fail "Android llvm-ar not found: $llvm_ar" +[ -x "$llvm_ranlib" ] || fail "Android llvm-ranlib not found: $llvm_ranlib" +oliphaunt_icu_require_source "$icu_source_dir" + +cc=("$clang_path") +cxx=("$clangxx_path") +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + ccache_bin="" + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi + +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" +postgres_cppflags="-D_GNU_SOURCE" +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)}" + +parse_mobile_static_extensions() { + local raw="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + [ -n "$raw" ] || return 0 + local extension + while IFS= read -r extension; do + extension="$(printf '%s' "$extension" | xargs)" + [ -n "$extension" ] || continue + if ! oliphaunt_mobile_static_extension_spec "$extension" >/dev/null; then + printf 'supported Android mobile static extensions: ' >&2 + oliphaunt_mobile_static_supported_extensions | paste -sd ',' - >&2 + fail "unsupported Android mobile static extension: $extension" + fi + mobile_static_extensions+=("$(oliphaunt_mobile_static_extension_sql_name "$extension")") + done < <(printf '%s\n' "$raw" | tr ',' '\n') +} + +mobile_static_extensions_include() { + local wanted="$1" + local extension + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + [ "$extension" = "$wanted" ] && return 0 + done + return 1 +} + +mobile_static_dependency_selected() { + local wanted="$1" + local extension dependency + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + [ "$dependency" = "$wanted" ] && return 0 + done < <(oliphaunt_mobile_static_extension_dependencies "$extension") + done + return 1 +} + +hash_mobile_static_extension_sources() { + local extension file + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r file; do + [ -n "$file" ] || continue + shasum -a 256 "$file" + done < <(oliphaunt_mobile_static_extension_hash_inputs "$repo_root" "$build_dir" "$extension") + done +} + +patch_series() { + sed -n '/series = \[/,/\]/p' "$source_manifest" | + sed -n 's/.*"\([^"]*\.patch\)".*/\1/p' +} + +patch_series_hash() { + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + shasum -a 256 "$patch_dir/$patch_name" + done < <(patch_series) | shasum -a 256 | awk '{print $1}' +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'android_abi=%s\n' "$android_abi" + printf 'android_host=%s\n' "$android_host" + printf 'ndk_root=%s\n' "$ndk_root" + printf 'toolchain_dir=%s\n' "$toolchain_dir" + printf 'android_api=%s\n' "$android_api" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'ar=%s\n' "$llvm_ar" + printf 'ranlib=%s\n' "$llvm_ranlib" + printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" + printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'liboliphaunt_cflags=%s\n' "$liboliphaunt_cflags" + printf 'pg_extension_cflags=%s\n' "$pg_extension_cflags" + printf 'mobile_static_extensions=%s\n' "${mobile_static_extensions[*]-}" + printf 'postgis_source_date_epoch=%s\n' "$(oliphaunt_postgis_reproducible_epoch)" + printf 'patch_series_hash=%s\n' "$(patch_series_hash)" + printf 'liboliphaunt_sources=%s\n' "${liboliphaunt_sources[*]}" + printf 'plpgsql_objects=%s\n' "${plpgsql_objects[*]}" + printf 'jit_objects=%s\n' "${jit_objects[*]}" + printf 'script_sha256=%s\n' "$(shasum -a 256 "$script_path" | awk '{print $1}')" + shasum -a 256 "$script_dir/postgres-backend-objects.mk" + shasum -a 256 "$script_dir/mobile-static-extensions.sh" "$script_dir/mobile-postgis-extensions.sh" + shasum -a 256 "$source_manifest" + shasum -a 256 "${liboliphaunt_sources[@]}" + hash_mobile_static_extension_sources + } | shasum -a 256 | awk '{print $1}' +} + +apply_patch_series() { + local patch_name + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_dir/$patch_name" >/dev/null + done < <(patch_series) +} + +patched_source_ready() { + grep -Fq 'OliphauntEmbeddedIO' "$build_dir/src/include/libpq/libpq-be.h" && + grep -Fq 'oliphaunt_embedded_main' "$build_dir/src/backend/tcop/postgres.c" && + grep -Fq 'oliphaunt_embedded_kill' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'oliphaunt_embedded_raise' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'getenv("ICU_DATA")' "$build_dir/src/bin/initdb/initdb.c" && + grep -Fq 'oliphaunt_embedded' "$build_dir/meson_options.txt" && + grep -Fq 'OLIPHAUNT_EMBEDDED' "$build_dir/meson.build" +} + +artifact_ready() { + [ -f "$lib_out" ] || return 1 + oliphaunt_icu_artifacts_ready "$icu_prefix" || return 1 + "$toolchain_dir/bin/llvm-readelf" -h "$lib_out" 2>/dev/null | rg -q "$android_readelf_arch_regex" || return 1 + local symbols + symbols="$("$llvm_nm" -D --defined-only "$lib_out" 2>/dev/null || true)" + local linked_symbols + linked_symbols="$("$llvm_nm" --defined-only "$lib_out" 2>/dev/null || true)" + oliphaunt_icu_linked_symbols_ready "$linked_symbols" || return 1 + local undefined_symbols + undefined_symbols="$("$llvm_nm" -D --undefined-only "$lib_out" 2>/dev/null || true)" + if printf '%s\n' "$undefined_symbols" | rg -q 'shm(get|ctl|dt)|shm_open|sem(get|ctl|op|open|close|unlink|wait|post|trywait|init|destroy)'; then + return 1 + fi + local symbol + for symbol in \ + oliphaunt_init \ + oliphaunt_init_ex \ + oliphaunt_exec_protocol \ + oliphaunt_exec_simple_query \ + oliphaunt_exec_protocol_stream \ + oliphaunt_backup \ + oliphaunt_backup_ex \ + oliphaunt_restore \ + oliphaunt_cancel \ + oliphaunt_detach \ + oliphaunt_close \ + oliphaunt_register_static_extensions \ + oliphaunt_last_error \ + oliphaunt_version \ + oliphaunt_capabilities \ + oliphaunt_free_response \ + oliphaunt_embedded_kill \ + oliphaunt_embedded_raise + do + case "$symbols" in *" T $symbol"*|*" D $symbol"*|*" B $symbol"*) ;; *) return 1 ;; esac + done + if [ "${#mobile_static_extensions[@]}" -gt 0 ]; then + case "$symbols" in *" T liboliphaunt_selected_static_extensions"*|*" D liboliphaunt_selected_static_extensions"*|*" B liboliphaunt_selected_static_extensions"*) ;; *) return 1 ;; esac + local extension stem prefix + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + [ -f "$out_dir/extensions/$stem/liboliphaunt_extension_$stem.a" ] || return 1 + case "$symbols" in *" T ${prefix}_Pg_magic_func"*|*" D ${prefix}_Pg_magic_func"*|*" B ${prefix}_Pg_magic_func"*) ;; *) return 1 ;; esac + done + fi +} + +report_artifact_not_ready() { + echo "Android $android_abi liboliphaunt shared library failed validation" >&2 + if [ ! -f "$lib_out" ]; then + echo "missing shared library: $lib_out" >&2 + return 0 + fi + + echo "shared library: $lib_out" >&2 + "$toolchain_dir/bin/llvm-readelf" -h "$lib_out" >&2 || true + + local symbols undefined_symbols linked_symbols + symbols="$("$llvm_nm" -D --defined-only "$lib_out" 2>/dev/null || true)" + undefined_symbols="$("$llvm_nm" -D --undefined-only "$lib_out" 2>/dev/null || true)" + linked_symbols="$("$llvm_nm" --defined-only "$lib_out" 2>/dev/null || true)" + echo "defined Oliphaunt API symbols:" >&2 + printf '%s\n' "$symbols" | rg ' oliphaunt_| liboliphaunt_selected_static_extensions' >&2 || true + echo "unexpected Android IPC/POSIX shared-memory undefined symbols:" >&2 + printf '%s\n' "$undefined_symbols" | rg 'shm(get|ctl|dt)|shm_open|sem(get|ctl|op|open|close|unlink|wait|post|trywait|init|destroy)' >&2 || true + if ! oliphaunt_icu_linked_symbols_ready "$linked_symbols"; then + echo "ICU static link validation failed" >&2 + fi + if [ -f "$make_log" ]; then + echo "tail of PostgreSQL Android $android_abi make log:" >&2 + tail -120 "$make_log" >&2 || true + fi +} + +backend_objects_ready() { + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o \ + src/backend/port/oliphaunt_embedded_sema.o \ + src/backend/port/oliphaunt_embedded_shmem.o \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a + do + [ -f "$required" ] || return 1 + done + for objfile in src/backend/*/objfiles.txt; do + [ -s "$objfile" ] || return 1 + done + "$llvm_nm" -g src/backend/tcop/postgres.o | rg -q "oliphaunt_embedded_main" || return 1 +} + +plpgsql_objects_ready() { + local object + for object in "${plpgsql_objects[@]}"; do + [ -f "$object" ] || return 1 + done + "$llvm_nm" -g src/pl/plpgsql/src/pl_handler.o | rg -q "plpgsql_call_handler" || return 1 +} + +jit_objects_ready() { + local object + for object in "${jit_objects[@]}"; do + [ -f "$object" ] || return 1 + done + "$llvm_nm" -g src/backend/jit/jit.o | rg -q "pg_jit_available" || return 1 +} + +prepare_source() { + mkdir -p "$source_cache" "$work_root" "$out_dir" + + oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + ( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - + ) >&2 + + local wanted + wanted="$(desired_hash)" + if [ -d "$build_dir" ] && { [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; }; then + rm -rf "$build_dir" + fi + if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" + ( + cd "$build_dir" + git init -q + apply_patch_series + ) + fi + patched_source_ready || fail "PostgreSQL embedded patch verification failed" +} + +configure_source() { + export CC="$cc_string" + export CXX="$cxx_string" + export CPP="$cpp_path" + export AR="$llvm_ar" + export RANLIB="$llvm_ranlib" + export CFLAGS="$native_cflags" + export CPPFLAGS="$postgres_cppflags $icu_cflags" + export LDFLAGS="-L$icu_prefix/lib" + export ICU_CFLAGS="$icu_cflags" + export ICU_LIBS="$icu_libs" + + if [ ! -f "$build_dir/config.status" ]; then + local build_alias + build_alias="$(sh "$build_dir/config/config.guess")" + ( + cd "$build_dir" + ./configure \ + --host="$android_host" \ + --build="$build_alias" \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls + ) > "$configure_log" 2>&1 + fi +} + +build_icu() { + oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$jobs" \ + "$oliphaunt_mobile_target" \ + "$android_host" \ + "$cc_string" \ + "$cxx_string" \ + "$llvm_ar" \ + "$llvm_ranlib" \ + "$native_cflags" \ + "$native_cflags" \ + "" +} + +build_backend_objects() { + ( + cd "$build_dir" + if backend_objects_ready; then + echo "reusing PostgreSQL Android $android_abi backend objects" >&2 + return + fi + + : > "$make_log" + rm -f src/include/nodes/header-stamp src/include/utils/header-stamp + make -C src/backend generated-headers CC="$cc_string" >> "$make_log" 2>&1 + + make -j"$jobs" -C src/backend \ + -f "$script_dir/postgres-backend-objects.mk" \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + CFLAGS="$native_cflags" \ + oliphaunt-backend-objects >> "$make_log" 2>&1 + + make -C src/common \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + libpgcommon_srv.a >> "$make_log" 2>&1 + make -C src/port \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + libpgport_srv.a >> "$make_log" 2>&1 + + if ! backend_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL Android $android_abi backend objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_timezone_objects() { + ( + cd "$build_dir" + make -C src/timezone \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + CFLAGS="$native_cflags" \ + localtime.o pgtz.o strftime.o >> "$make_log" 2>&1 + ) +} + +build_plpgsql_objects() { + ( + cd "$build_dir" + if plpgsql_objects_ready; then + echo "reusing PostgreSQL Android $android_abi PL/pgSQL objects" >&2 + return + fi + + make -C src/pl/plpgsql/src clean >> "$make_log" 2>&1 + make -C src/pl/plpgsql/src \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + CFLAGS="$native_cflags" \ + pl_comp.o pl_exec.o pl_funcs.o pl_gram.o pl_handler.o pl_scanner.o >> "$make_log" 2>&1 + + if ! plpgsql_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL Android $android_abi PL/pgSQL objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_jit_objects() { + ( + cd "$build_dir" + if jit_objects_ready; then + echo "reusing PostgreSQL Android $android_abi JIT stub objects" >&2 + return + fi + + make -C src/backend/jit \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + CFLAGS="$native_cflags" \ + jit.o >> "$make_log" 2>&1 + + if ! jit_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL Android $android_abi JIT stub objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_liboliphaunt_objects() { + local source object + liboliphaunt_objects=() + for source in "${liboliphaunt_sources[@]}"; do + object="$out_dir/$(basename "${source%.c}").o" + liboliphaunt_objects+=("$object") + "${cc[@]}" $liboliphaunt_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -I"$repo_root/src/runtimes/liboliphaunt/native/src" \ + -c "$source" \ + -o "$object" + done +} + +build_openssl_dependency() { + mobile_static_dependency_selected openssl || return 0 + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/openssl" + local dependency_dir="$mobile_static_dependency_root/openssl" + local build_root="$work_root/openssl-$oliphaunt_mobile_target" + local install_root="$work_root/openssl-$oliphaunt_mobile_target-install" + local installed_archive="" + local archive="$dependency_dir/libcrypto.a" + local configure_target + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/openssl" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -d "$source_dir" ] || fail "OpenSSL checkout is missing: $source_dir" + case "$android_abi" in + arm64-v8a) configure_target="android-arm64" ;; + x86_64) configure_target="android-x86_64" ;; + *) fail "unsupported Android ABI for OpenSSL: $android_abi" ;; + esac + + rm -rf "$build_root" "$install_root" "$dependency_dir" + mkdir -p "$(dirname "$build_root")" "$dependency_dir/include" + cp -a "$source_dir/." "$build_root/" + rm -rf "$build_root/.git" + ( + cd "$build_root" + PATH="$toolchain_dir/bin:$PATH" \ + ANDROID_NDK_ROOT="$ndk_root" \ + ./Configure "$configure_target" \ + -D__ANDROID_API__="$android_api" \ + no-shared no-tests no-apps no-docs no-engine no-module \ + --prefix="$install_root" \ + --openssldir="$install_root/ssl" >> "$make_log" 2>&1 + PATH="$toolchain_dir/bin:$PATH" make -j"$jobs" build_generated libcrypto.a >> "$make_log" 2>&1 + PATH="$toolchain_dir/bin:$PATH" make install_dev >> "$make_log" 2>&1 + ) + for candidate in "$install_root/lib/libcrypto.a" "$install_root/lib64/libcrypto.a"; do + if [ -f "$candidate" ]; then + installed_archive="$candidate" + break + fi + done + [ -n "$installed_archive" ] || fail "OpenSSL Android $android_abi build did not produce libcrypto.a" + cp -R "$install_root/include/openssl" "$dependency_dir/include/" + cp -p "$installed_archive" "$archive" + mobile_static_dependency_archives+=("$archive") +} + +build_uuid_dependency() { + mobile_static_dependency_selected uuid || return 0 + local source_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" + local dependency_dir="$mobile_static_dependency_root/uuid" + local archive="$dependency_dir/lib/libuuid.a" + local object="$dependency_dir/portable_uuid.o" + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/uuid" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -f "$source_dir/portable_uuid.c" ] || fail "portable UUID source is missing: $source_dir" + + rm -rf "$dependency_dir" + mkdir -p "$dependency_dir/include" "$dependency_dir/lib" + cp -R "$source_dir/include/uuid" "$dependency_dir/include/" + "${cc[@]}" $pg_extension_cflags \ + -I"$source_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$source_dir/portable_uuid.c" \ + -o "$object" + "$llvm_ar" crs "$archive" "$object" + "$llvm_ranlib" "$archive" + [ -s "$archive" ] || fail "portable UUID Android $android_abi build did not produce $archive" + mobile_static_dependency_archives+=("$archive") +} + +build_mobile_static_dependencies() { + build_openssl_dependency + build_uuid_dependency + build_postgis_mobile_static_dependencies +} + +build_mobile_static_extension_objects() { + local extension source source_dir source_rel object object_dir objects_file stem prefix + local -a compile_args extension_include_args extension_cflags + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + if [ "$extension" = "postgis" ]; then + build_postgis_mobile_static_extension_objects "$extension" + continue + fi + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + source_dir="$(oliphaunt_mobile_static_extension_source_dir "$repo_root" "$build_dir" "$extension")" + extension_include_args=() + while IFS= read -r include_dir; do + [ -n "$include_dir" ] || continue + extension_include_args+=("-I$include_dir") + done < <(oliphaunt_mobile_static_extension_include_dirs "$repo_root" "$build_dir" "$extension") + extension_cflags=() + while IFS= read -r cflag; do + [ -n "$cflag" ] || continue + extension_cflags+=("$cflag") + done < <(oliphaunt_mobile_static_extension_cflags "$extension") + if [ "$(oliphaunt_mobile_static_extension_kind "$extension")" = "contrib" ]; then + ( + cd "$build_dir" + make -C "$(oliphaunt_mobile_static_extension_source_rel "$extension")" \ + CC="$cc_string" \ + AR="$llvm_ar" \ + RANLIB="$llvm_ranlib" \ + CFLAGS="$pg_extension_cflags" \ + all >> "$make_log" 2>&1 || true + ) + fi + object_dir="$out_dir/extensions/$stem" + objects_file="$object_dir/objects.list" + mkdir -p "$object_dir" + : > "$objects_file" + while IFS= read -r source; do + [ -n "$source" ] || continue + source_rel="${source#$source_dir/}" + object="$object_dir/${source_rel%.c}.o" + mkdir -p "$(dirname "$object")" + mobile_static_objects+=("$object") + printf '%s\n' "$object" >> "$objects_file" + compile_args=( + "${cc[@]}" $pg_extension_cflags + -DPg_magic_func="${prefix}_Pg_magic_func" \ + -D_PG_init="${prefix}__PG_init" + ) + if [ "${#extension_cflags[@]}" -gt 0 ]; then + compile_args+=("${extension_cflags[@]}") + fi + if [ "${#extension_include_args[@]}" -gt 0 ]; then + compile_args+=("${extension_include_args[@]}") + fi + compile_args+=( + -I"$build_dir/src/include" + -I"$build_dir/src/include/port" + -c "$source" + -o "$object" + ) + "${compile_args[@]}" + done < <(oliphaunt_mobile_static_extension_source_files "$repo_root" "$build_dir" "$extension") + if [ ! -s "$objects_file" ]; then + fail "mobile static extension $extension did not produce object inputs" + fi + archive_mobile_static_extension_objects "$extension" "$object_dir" "$objects_file" + done +} + +archive_mobile_static_extension_objects() { + local extension="$1" + local object_dir="$2" + local objects_file="$3" + local stem archive + local -a archive_objects + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + archive="$object_dir/liboliphaunt_extension_$stem.a" + while IFS= read -r object; do + [ -n "$object" ] && archive_objects+=("$object") + done < "$objects_file" + rm -f "$archive" + "$llvm_ar" crs "$archive" "${archive_objects[@]}" + "$llvm_ranlib" "$archive" + if [ ! -s "$archive" ]; then + fail "mobile static extension $extension did not produce archive $archive" + fi +} + +defined_c_symbols() { + local stem="$1" + local prefix="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + "$llvm_nm" -g --defined-only $(cat "$objects_file") | + awk ' + $2 == "T" { + symbol = $3 + if (symbol == "" || + index(symbol, prefix "_") == 1 || + symbol == "Pg_magic_func" || + symbol == "_PG_init") { + next + } + if (symbol ~ /^[A-Za-z_][A-Za-z0-9_]*$/) { + print symbol + } + } + ' prefix="$prefix" | + LC_ALL=C sort -u +} + +module_has_c_symbol() { + local stem="$1" + local symbol="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + "$llvm_nm" -g --defined-only $(cat "$objects_file") | awk -v wanted="$symbol" '$3 == wanted { found = 1 } END { exit found ? 0 : 1 }' +} + +write_mobile_static_registry_source() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + local extension stem prefix symbols_file alias_file init_symbol symbols_expr symbol_count_expr + { + cat <<'HEADER' +/* Generated by Oliphaunt mobile build. Do not edit by hand. */ +#include +#include +#include "oliphaunt.h" + +HEADER + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + defined_c_symbols "$stem" "$prefix" > "$symbols_file" + if [ ! -s "$symbols_file" ] && ! module_has_c_symbol "$stem" "${prefix}__PG_init"; then + fail "mobile static extension $extension did not produce exported C symbols or an init hook" + fi + printf 'extern const void *%s_Pg_magic_func(void);\n' "$prefix" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + printf 'extern void %s__PG_init(void);\n' "$prefix" + fi + while IFS= read -r symbol; do + printf 'extern void %s(void);\n' "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r _ linked_symbol; do + [ -n "$linked_symbol" ] || continue + printf 'extern void %s(void);\n' "$linked_symbol" + done < "$alias_file" + fi + printf '\n' + done + + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + [ -s "$symbols_file" ] || [ -s "$alias_file" ] || continue + printf 'static const OliphauntStaticExtensionSymbol %s_symbols[] = {\n' "$prefix" + while IFS= read -r symbol; do + printf ' { .name = "%s", .address = (void *)%s },\n' "$symbol" "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r sql_symbol linked_symbol; do + [ -n "$sql_symbol" ] || continue + [ -n "$linked_symbol" ] || continue + printf ' { .name = "%s", .address = (void *)%s },\n' "$sql_symbol" "$linked_symbol" + done < "$alias_file" + fi + printf '};\n\n' + done + + printf 'static const OliphauntStaticExtension liboliphaunt_static_extensions[] = {\n' + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + init_symbol="NULL" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + init_symbol="${prefix}__PG_init" + fi + symbols_expr="NULL" + symbol_count_expr="0" + if [ -s "$symbols_file" ] || [ -s "$alias_file" ]; then + symbols_expr="${prefix}_symbols" + symbol_count_expr="sizeof(${prefix}_symbols) / sizeof(${prefix}_symbols[0])" + fi + cat < "$mobile_static_registry_source" +} + +build_mobile_static_registry_object() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + "${cc[@]}" $native_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -c "$mobile_static_registry_source" \ + -o "$mobile_static_registry_object" + mobile_static_objects+=("$mobile_static_registry_object") +} + +write_objects_response_file() { + ( + cd "$build_dir" + { + cat src/backend/*/objfiles.txt + printf '%s\n' "${jit_objects[@]}" + printf 'src/timezone/localtime.o src/timezone/pgtz.o src/timezone/strftime.o\n' + printf '%s\n' "${plpgsql_objects[@]}" + } | tr '[:space:]' '\n' | sed '/^$/d' | awk '!seen[$0]++' > "$objects_rsp" + ) +} + +link_liboliphaunt() { + local -a postgis_link_args + local link_arg + while IFS= read -r link_arg; do + [ -n "$link_arg" ] && postgis_link_args+=("$link_arg") + done < <(oliphaunt_postgis_extra_link_args) + ( + cd "$build_dir" + set +u + "${cxx[@]}" -shared \ + -Wl,-soname,liboliphaunt.so \ + -Wl,-z,defs \ + -Wl,-z,max-page-size=16384 \ + -o "$lib_out" \ + "${liboliphaunt_objects[@]}" \ + "${mobile_static_objects[@]}" \ + "${mobile_static_dependency_archives[@]}" \ + @"$objects_rsp" \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a \ + $icu_libs \ + "${postgis_link_args[@]}" \ + -lm \ + -ldl + set -u + ) +} + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh [--check-current] + +Environment: + OLIPHAUNT_ANDROID_ABI=arm64-v8a|x86_64 +MSG +} + +parse_mobile_static_extensions + +case "$script_mode" in + build) + prepare_source + build_icu + configure_source + if artifact_ready && (cd "$build_dir" && backend_objects_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "$lib_out" + exit 0 + fi + build_backend_objects + build_jit_objects + build_timezone_objects + build_plpgsql_objects + build_liboliphaunt_objects + build_mobile_static_dependencies + build_mobile_static_extension_objects + write_mobile_static_registry_source + build_mobile_static_registry_object + write_objects_response_file + link_liboliphaunt + if ! artifact_ready; then + report_artifact_not_ready + exit 1 + fi + desired_hash > "$stamp" + echo "$lib_out" + ;; + --check-current) + if artifact_ready && (cd "$build_dir" && backend_objects_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "Android $android_abi liboliphaunt shared library is current" + exit 0 + fi + report_artifact_not_ready + echo "Android $android_abi liboliphaunt shared library is missing or stale" >&2 + exit 1 + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh new file mode 100755 index 00000000..d3398737 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export OLIPHAUNT_ANDROID_ABI="${OLIPHAUNT_ANDROID_ABI:-x86_64}" +exec "$script_dir/build-postgres18-android-arm64.sh" "$@" diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh new file mode 100755 index 00000000..351261ef --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh @@ -0,0 +1,1053 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/mobile-static-extensions.sh" +. "$script_dir/mobile-postgis-extensions.sh" +script_path="$script_dir/$(basename "$0")" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +oliphaunt_mobile_target="ios-device" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" +work_root="${OLIPHAUNT_IOS_DEVICE_ROOT:-$repo_root/target/liboliphaunt-ios-device}" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +out_dir="$work_root/out" +stamp="$build_dir/.liboliphaunt-ios-device-build.sha256" +configure_log="$work_root/configure.log" +make_log="$work_root/make.log" +objects_rsp="$out_dir/liboliphaunt-ios-objects.rsp" +lib_out="$out_dir/liboliphaunt.dylib" +mobile_static_registry_source="$out_dir/liboliphaunt_mobile_static_registry.c" +mobile_static_registry_object="$out_dir/liboliphaunt_mobile_static_registry.o" +script_mode="${1:-build}" +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-ios-device-build" +icu_prefix="$work_root/icu-ios-device" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lc++" +icu_libs="$icu_static_libs $icu_cpp_libs" +failure_phase="initialization" + +report_failure() { + local status="${1:-1}" + local line="${2:-unknown}" + local log + if [ "$status" -eq 0 ]; then + return 0 + fi + trap - EXIT + printf 'PostgreSQL iOS device build failed during %s (line %s, status %s)\n' \ + "$failure_phase" "$line" "$status" >&2 + for log in "$configure_log" "$make_log"; do + if [ -s "$log" ]; then + printf '%s\n' "--- tail of $log ---" >&2 + tail -160 "$log" >&2 || true + fi + done + exit "$status" +} + +liboliphaunt_sources=( + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) + +plpgsql_objects=( + src/pl/plpgsql/src/pl_comp.o + src/pl/plpgsql/src/pl_exec.o + src/pl/plpgsql/src/pl_funcs.o + src/pl/plpgsql/src/pl_gram.o + src/pl/plpgsql/src/pl_handler.o + src/pl/plpgsql/src/pl_scanner.o +) + +jit_objects=( + src/backend/jit/jit.o +) + +generated_header_stamps=( + src/include/catalog/bki-stamp + src/include/nodes/header-stamp + src/include/utils/header-stamp +) + +generated_header_files=( + src/backend/parser/gram.h + src/include/catalog/pg_proc_d.h + src/include/catalog/schemapg.h + src/include/nodes/nodetags.h + src/include/storage/lwlocknames.h + src/include/utils/errcodes.h + src/include/utils/fmgroids.h + src/include/utils/fmgrprotos.h + src/include/utils/pgstat_wait_event.c + src/include/utils/probes.h + src/include/utils/wait_event_funcs_data.c + src/include/utils/wait_event_types.h +) + +mobile_static_extensions=() +mobile_static_objects=() +mobile_static_dependency_archives=() +mobile_static_dependency_root="$out_dir/dependencies" +export OLIPHAUNT_MOBILE_STATIC_DEPENDENCY_ROOT="$mobile_static_dependency_root" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "PostgreSQL iOS device build requires Darwin" >&2 + exit 2 +fi + +for cmd in curl git nm patch perl rg shasum xcrun; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "missing required command: $cmd" >&2 + exit 1 + fi +done + +sdk_path="$(xcrun --sdk iphoneos --show-sdk-path 2>/dev/null || true)" +clang_path="$(xcrun --find --sdk iphoneos clang 2>/dev/null || true)" +clangxx_path="$(xcrun --find --sdk iphoneos clang++ 2>/dev/null || true)" +ar_path="$(xcrun --find --sdk iphoneos ar 2>/dev/null || true)" +ranlib_path="$(xcrun --find --sdk iphoneos ranlib 2>/dev/null || true)" +libtool_path="$(xcrun --find --sdk iphoneos libtool 2>/dev/null || true)" +if [ -z "$sdk_path" ] || [ -z "$clang_path" ] || [ -z "$clangxx_path" ] || [ -z "$ar_path" ] || [ -z "$ranlib_path" ] || [ -z "$libtool_path" ]; then + echo "iPhoneOS SDK is unavailable" >&2 + exit 1 +fi +oliphaunt_icu_require_source "$icu_source_dir" + +min_ios="${OLIPHAUNT_IOS_MIN_VERSION:-17.0}" +cc=("$clang_path" -target "arm64-apple-ios${min_ios}" "-miphoneos-version-min=${min_ios}" -isysroot "$sdk_path") +cxx=("$clangxx_path" -target "arm64-apple-ios${min_ios}" "-miphoneos-version-min=${min_ios}" -isysroot "$sdk_path") +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + ccache_bin="" + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" +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)}" + +parse_mobile_static_extensions() { + local raw="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + [ -n "$raw" ] || return 0 + local extension + while IFS= read -r extension; do + extension="$(printf '%s' "$extension" | xargs)" + [ -n "$extension" ] || continue + if ! oliphaunt_mobile_static_extension_spec "$extension" >/dev/null; then + echo "unsupported iOS mobile static extension: $extension" >&2 + printf 'supported iOS mobile static extensions: ' >&2 + oliphaunt_mobile_static_supported_extensions | paste -sd ',' - >&2 + exit 2 + fi + mobile_static_extensions+=("$(oliphaunt_mobile_static_extension_sql_name "$extension")") + done < <(printf '%s\n' "$raw" | tr ',' '\n') +} + +mobile_static_extensions_include() { + local wanted="$1" + local extension + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + [ "$extension" = "$wanted" ] && return 0 + done + return 1 +} + +mobile_static_dependency_selected() { + local wanted="$1" + local extension dependency + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + [ "$dependency" = "$wanted" ] && return 0 + done < <(oliphaunt_mobile_static_extension_dependencies "$extension") + done + return 1 +} + +hash_mobile_static_extension_sources() { + local extension file + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r file; do + [ -n "$file" ] || continue + shasum -a 256 "$file" + done < <(oliphaunt_mobile_static_extension_hash_inputs "$repo_root" "$build_dir" "$extension") + done +} + +patch_series() { + sed -n '/series = \[/,/\]/p' "$source_manifest" | + sed -n 's/.*"\([^"]*\.patch\)".*/\1/p' +} + +patch_series_hash() { + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + shasum -a 256 "$patch_dir/$patch_name" + done < <(patch_series) | shasum -a 256 | awk '{print $1}' +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'sdk_path=%s\n' "$sdk_path" + printf 'clang_path=%s\n' "$clang_path" + printf 'clangxx_path=%s\n' "$clangxx_path" + printf 'min_ios=%s\n' "$min_ios" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'ar=%s\n' "$ar_path" + printf 'ranlib=%s\n' "$ranlib_path" + printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" + printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'liboliphaunt_cflags=%s\n' "$liboliphaunt_cflags" + printf 'pg_extension_cflags=%s\n' "$pg_extension_cflags" + printf 'mobile_static_extensions=%s\n' "${mobile_static_extensions[*]-}" + printf 'patch_series_hash=%s\n' "$(patch_series_hash)" + printf 'liboliphaunt_sources=%s\n' "${liboliphaunt_sources[*]}" + printf 'plpgsql_objects=%s\n' "${plpgsql_objects[*]}" + printf 'jit_objects=%s\n' "${jit_objects[*]}" + printf 'script_sha256=%s\n' "$(shasum -a 256 "$script_path" | awk '{print $1}')" + shasum -a 256 "$script_dir/postgres-backend-objects.mk" + shasum -a 256 "$script_dir/mobile-static-extensions.sh" "$script_dir/mobile-postgis-extensions.sh" + shasum -a 256 "$source_manifest" + shasum -a 256 "${liboliphaunt_sources[@]}" + hash_mobile_static_extension_sources + } | shasum -a 256 | awk '{print $1}' +} + +apply_patch_series() { + local patch_name + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_dir/$patch_name" >/dev/null + done < <(patch_series) +} + +patched_source_ready() { + grep -Fq 'OliphauntEmbeddedIO' "$build_dir/src/include/libpq/libpq-be.h" && + grep -Fq 'oliphaunt_embedded_main' "$build_dir/src/backend/tcop/postgres.c" && + grep -Fq 'oliphaunt_embedded_kill' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'oliphaunt_embedded_raise' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'getenv("ICU_DATA")' "$build_dir/src/bin/initdb/initdb.c" && + grep -Fq 'oliphaunt_embedded' "$build_dir/meson_options.txt" && + grep -Fq 'OLIPHAUNT_EMBEDDED' "$build_dir/meson.build" +} + +artifact_ready() { + [ -f "$lib_out" ] || return 1 + oliphaunt_icu_artifacts_ready "$icu_prefix" || return 1 + xcrun vtool -show-build "$lib_out" 2>/dev/null | rg -q "platform IOS" || return 1 + local symbols + symbols="$(nm -g "$lib_out" 2>/dev/null || true)" + local linked_symbols + linked_symbols="$(nm "$lib_out" 2>/dev/null || true)" + oliphaunt_icu_linked_symbols_ready "$linked_symbols" || return 1 + local undefined_symbols + undefined_symbols="$(nm -u "$lib_out" 2>/dev/null || true)" + if printf '%s\n' "$undefined_symbols" | rg -q '_shm(get|ctl|dt)|_shm_open|_sem(get|ctl|op|open|close|unlink|wait|post|trywait|init|destroy)'; then + return 1 + fi + local symbol + for symbol in \ + _oliphaunt_init \ + _oliphaunt_init_ex \ + _oliphaunt_exec_protocol \ + _oliphaunt_exec_protocol_stream \ + _oliphaunt_backup \ + _oliphaunt_backup_ex \ + _oliphaunt_restore \ + _oliphaunt_cancel \ + _oliphaunt_detach \ + _oliphaunt_close \ + _oliphaunt_register_static_extensions \ + _oliphaunt_last_error \ + _oliphaunt_version \ + _oliphaunt_capabilities \ + _oliphaunt_free_response \ + _oliphaunt_embedded_kill \ + _oliphaunt_embedded_raise + do + case "$symbols" in *"$symbol"*) ;; *) return 1 ;; esac + done + if [ "${#mobile_static_extensions[@]}" -gt 0 ]; then + case "$symbols" in *"_liboliphaunt_selected_static_extensions"*) ;; *) return 1 ;; esac + local extension stem prefix + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + [ -f "$out_dir/extensions/$stem/liboliphaunt_extension_$stem.a" ] || return 1 + case "$symbols" in *"_${prefix}_Pg_magic_func"*) ;; *) return 1 ;; esac + done + fi +} + +backend_objects_ready() { + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o \ + src/backend/port/oliphaunt_embedded_sema.o \ + src/backend/port/oliphaunt_embedded_shmem.o + do + [ -f "$required" ] || return 1 + done + for objfile in src/backend/*/objfiles.txt; do + [ -s "$objfile" ] || return 1 + done + nm -g src/backend/tcop/postgres.o | rg -q "_oliphaunt_embedded_main" || return 1 +} + +support_libraries_ready() { + for required in \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a + do + [ -f "$required" ] || return 1 + done + + local symbols + symbols="$(nm -gU src/port/libpgport_srv.a 2>/dev/null || true)" + local symbol + for symbol in _oliphaunt_embedded_kill _oliphaunt_embedded_raise; do + grep -Eq "[[:space:]]${symbol}$" <<< "$symbols" || return 1 + done +} + +plpgsql_objects_ready() { + local object + for object in "${plpgsql_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/pl/plpgsql/src/pl_handler.o | rg -q "_plpgsql_call_handler" || return 1 +} + +jit_objects_ready() { + local object + for object in "${jit_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/backend/jit/jit.o | rg -q "_pg_jit_available" || return 1 +} + +generated_headers_ready() { + local required + for required in "${generated_header_stamps[@]}"; do + [ -f "$required" ] || return 1 + done + for required in "${generated_header_files[@]}"; do + [ -s "$required" ] || return 1 + done +} + +explain_generated_header_state() { + local required + for required in "${generated_header_stamps[@]}"; do + if [ ! -f "$required" ]; then + echo "missing PostgreSQL generated-header stamp: $build_dir/$required" >&2 + fi + done + for required in "${generated_header_files[@]}"; do + if [ ! -s "$required" ]; then + echo "missing or empty PostgreSQL generated header: $build_dir/$required" >&2 + fi + done +} + +prepare_source() { + mkdir -p "$source_cache" "$work_root" "$out_dir" + + oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + ( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - + ) >&2 + + local wanted + wanted="$(desired_hash)" + if [ -d "$build_dir" ] && { [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; }; then + rm -rf "$build_dir" + fi + if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" + ( + cd "$build_dir" + git init -q + apply_patch_series + ) + fi + if ! patched_source_ready; then + echo "PostgreSQL embedded patch verification failed" >&2 + exit 1 + fi +} + +configure_source() { + export CC="$cc_string" + export CXX="$cxx_string" + export CFLAGS="$native_cflags" + export CPPFLAGS="-isysroot $sdk_path $icu_cflags" + export LDFLAGS="-isysroot $sdk_path -L$icu_prefix/lib" + export ICU_CFLAGS="$icu_cflags" + export ICU_LIBS="$icu_libs" + + if [ ! -f "$build_dir/config.status" ]; then + ( + cd "$build_dir" + ./configure \ + --host=aarch64-apple-darwin \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls \ + ac_cv_file__dev_urandom=yes + ) > "$configure_log" 2>&1 + fi +} + +build_icu() { + oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$jobs" \ + "ios-device" \ + "aarch64-apple-darwin" \ + "$cc_string" \ + "$cxx_string" \ + "$ar_path" \ + "$ranlib_path" \ + "$native_cflags" \ + "$native_cflags" \ + "-isysroot $sdk_path" +} + +build_generated_headers() { + ( + cd "$build_dir" + + # PostgreSQL's generated-header targets use stamp files for multi-output + # generators. Invalidate both the stamps and their generated sources so a + # restored or interrupted build cannot silently reuse a stale header. + rm -f \ + src/backend/nodes/node-support-stamp \ + src/backend/nodes/nodetags.h \ + src/backend/storage/lmgr/lwlocknames.h \ + src/backend/utils/errcodes.h \ + src/backend/utils/fmgr-stamp \ + src/backend/utils/fmgroids.h \ + src/backend/utils/fmgrprotos.h \ + src/backend/utils/fmgrtab.c \ + src/backend/utils/pgstat_wait_event.c \ + src/backend/utils/probes.h \ + src/backend/utils/wait_event_funcs_data.c \ + src/backend/utils/wait_event_types.h \ + src/include/catalog/bki-stamp \ + src/include/nodes/header-stamp \ + src/include/nodes/nodetags.h \ + src/include/storage/lwlocknames.h \ + src/include/utils/errcodes.h \ + src/include/utils/fmgroids.h \ + src/include/utils/fmgrprotos.h \ + src/include/utils/header-stamp \ + src/include/utils/pgstat_wait_event.c \ + src/include/utils/probes.h \ + src/include/utils/wait_event_funcs_data.c \ + src/include/utils/wait_event_types.h + + make -C src/backend generated-headers \ + OLIPHAUNT_EMBEDDED_MOBILE_SHMEM=1 \ + CC="$cc_string" >> "$make_log" 2>&1 + + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS device generated headers are incomplete" >&2 + exit 1 + fi + ) +} + +build_backend_objects() { + ( + cd "$build_dir" + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS device backend objects require generated headers" >&2 + exit 1 + fi + if backend_objects_ready; then + echo "reusing PostgreSQL iOS device backend objects" >&2 + return + fi + + make -j"$jobs" -C src/backend \ + -f "$script_dir/postgres-backend-objects.mk" \ + OLIPHAUNT_EMBEDDED_MOBILE_SHMEM=1 \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + oliphaunt-backend-objects >> "$make_log" 2>&1 + + if ! backend_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS device backend objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_support_libraries() { + ( + cd "$build_dir" + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS device support libraries require generated headers" >&2 + exit 1 + fi + if ! support_libraries_ready; then + make -C src/common clean >> "$make_log" 2>&1 + make -C src/port clean >> "$make_log" 2>&1 + fi + make -C src/common \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + libpgcommon_srv.a >> "$make_log" 2>&1 + make -C src/port \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + libpgport_srv.a >> "$make_log" 2>&1 + if ! support_libraries_ready; then + echo "PostgreSQL iOS device support libraries do not provide the embedded signal boundary" >&2 + exit 1 + fi + ) +} + +build_timezone_objects() { + ( + cd "$build_dir" + make -C src/timezone \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + localtime.o pgtz.o strftime.o >> "$make_log" 2>&1 + ) +} + +build_plpgsql_objects() { + ( + cd "$build_dir" + if plpgsql_objects_ready; then + echo "reusing PostgreSQL iOS device PL/pgSQL objects" >&2 + return + fi + + make -C src/pl/plpgsql/src clean >> "$make_log" 2>&1 + make -C src/pl/plpgsql/src \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + pl_comp.o pl_exec.o pl_funcs.o pl_gram.o pl_handler.o pl_scanner.o >> "$make_log" 2>&1 + + if ! plpgsql_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS device PL/pgSQL objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_jit_objects() { + ( + cd "$build_dir" + if jit_objects_ready; then + echo "reusing PostgreSQL iOS device JIT stub objects" >&2 + return + fi + + make -C src/backend/jit \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + jit.o >> "$make_log" 2>&1 + + if ! jit_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS device JIT stub objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_liboliphaunt_objects() { + local source object + liboliphaunt_objects=() + for source in "${liboliphaunt_sources[@]}"; do + object="$out_dir/$(basename "${source%.c}").o" + liboliphaunt_objects+=("$object") + "${cc[@]}" $liboliphaunt_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -I"$repo_root/src/runtimes/liboliphaunt/native/src" \ + -c "$source" \ + -o "$object" + done +} + +build_openssl_dependency() { + mobile_static_dependency_selected openssl || return 0 + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/openssl" + local dependency_dir="$mobile_static_dependency_root/openssl" + local build_root="$work_root/openssl-ios-device" + local install_root="$work_root/openssl-ios-device-install" + local installed_archive="" + local archive="$dependency_dir/libcrypto.a" + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/openssl" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -d "$source_dir" ] || { + echo "OpenSSL checkout is missing: $source_dir" >&2 + exit 1 + } + + rm -rf "$build_root" "$install_root" "$dependency_dir" + mkdir -p "$(dirname "$build_root")" "$dependency_dir/include" + cp -a "$source_dir/." "$build_root/" + rm -rf "$build_root/.git" + ( + cd "$build_root" + CFLAGS="-O2 -fPIC -miphoneos-version-min=${min_ios}" \ + ./Configure ios64-xcrun \ + no-shared no-tests no-apps no-docs no-engine no-module \ + --prefix="$install_root" \ + --openssldir="$install_root/ssl" >> "$make_log" 2>&1 + make -j"$jobs" build_generated libcrypto.a >> "$make_log" 2>&1 + make install_dev >> "$make_log" 2>&1 + ) + for candidate in "$install_root/lib/libcrypto.a" "$install_root/lib64/libcrypto.a"; do + if [ -f "$candidate" ]; then + installed_archive="$candidate" + break + fi + done + if [ -z "$installed_archive" ]; then + echo "OpenSSL iOS device build did not produce libcrypto.a" >&2 + exit 1 + fi + cp -R "$install_root/include/openssl" "$dependency_dir/include/" + cp -p "$installed_archive" "$archive" + mobile_static_dependency_archives+=("$archive") +} + +build_uuid_dependency() { + mobile_static_dependency_selected uuid || return 0 + local source_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" + local dependency_dir="$mobile_static_dependency_root/uuid" + local archive="$dependency_dir/lib/libuuid.a" + local object="$dependency_dir/portable_uuid.o" + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/uuid" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -f "$source_dir/portable_uuid.c" ] || { + echo "portable UUID source is missing: $source_dir" >&2 + exit 1 + } + + rm -rf "$dependency_dir" + mkdir -p "$dependency_dir/include" "$dependency_dir/lib" + cp -R "$source_dir/include/uuid" "$dependency_dir/include/" + "${cc[@]}" $native_cflags \ + -I"$source_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$source_dir/portable_uuid.c" \ + -o "$object" + "$libtool_path" -static -o "$archive" "$object" + [ -s "$archive" ] || { + echo "portable UUID iOS device build did not produce $archive" >&2 + exit 1 + } + mobile_static_dependency_archives+=("$archive") +} + +build_mobile_static_dependencies() { + build_openssl_dependency + build_uuid_dependency + build_postgis_mobile_static_dependencies +} + +build_mobile_static_extension_objects() { + local extension source source_dir source_rel object object_dir objects_file stem prefix + local -a compile_args extension_include_args extension_cflags + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + if [ "$extension" = "postgis" ]; then + build_postgis_mobile_static_extension_objects "$extension" + continue + fi + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + source_dir="$(oliphaunt_mobile_static_extension_source_dir "$repo_root" "$build_dir" "$extension")" + extension_include_args=() + while IFS= read -r include_dir; do + [ -n "$include_dir" ] || continue + extension_include_args+=("-I$include_dir") + done < <(oliphaunt_mobile_static_extension_include_dirs "$repo_root" "$build_dir" "$extension") + extension_cflags=() + while IFS= read -r cflag; do + [ -n "$cflag" ] || continue + extension_cflags+=("$cflag") + done < <(oliphaunt_mobile_static_extension_cflags "$extension") + if [ "$(oliphaunt_mobile_static_extension_kind "$extension")" = "contrib" ]; then + ( + cd "$build_dir" + make -C "$(oliphaunt_mobile_static_extension_source_rel "$extension")" \ + CC="$cc_string" \ + CFLAGS="$pg_extension_cflags" \ + all >> "$make_log" 2>&1 || true + ) + fi + object_dir="$out_dir/extensions/$stem" + objects_file="$object_dir/objects.list" + mkdir -p "$object_dir" + : > "$objects_file" + while IFS= read -r source; do + [ -n "$source" ] || continue + source_rel="${source#$source_dir/}" + object="$object_dir/${source_rel%.c}.o" + mkdir -p "$(dirname "$object")" + mobile_static_objects+=("$object") + printf '%s\n' "$object" >> "$objects_file" + compile_args=( + "${cc[@]}" $pg_extension_cflags + -DPg_magic_func="${prefix}_Pg_magic_func" \ + -D_PG_init="${prefix}__PG_init" + ) + if [ "${#extension_cflags[@]}" -gt 0 ]; then + compile_args+=("${extension_cflags[@]}") + fi + if [ "${#extension_include_args[@]}" -gt 0 ]; then + compile_args+=("${extension_include_args[@]}") + fi + compile_args+=( + -I"$build_dir/src/include" + -I"$build_dir/src/include/port" + -c "$source" + -o "$object" + ) + "${compile_args[@]}" + done < <(oliphaunt_mobile_static_extension_source_files "$repo_root" "$build_dir" "$extension") + if [ ! -s "$objects_file" ]; then + echo "mobile static extension $extension did not produce object inputs" >&2 + exit 1 + fi + archive_mobile_static_extension_objects "$extension" "$object_dir" "$objects_file" + done +} + +archive_mobile_static_extension_objects() { + local extension="$1" + local object_dir="$2" + local objects_file="$3" + local stem archive + local -a archive_objects + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + archive="$object_dir/liboliphaunt_extension_$stem.a" + while IFS= read -r object; do + [ -n "$object" ] && archive_objects+=("$object") + done < "$objects_file" + rm -f "$archive" + "$libtool_path" -static -o "$archive" "${archive_objects[@]}" + if [ ! -s "$archive" ]; then + echo "mobile static extension $extension did not produce archive $archive" >&2 + exit 1 + fi +} + +defined_c_symbols() { + local stem="$1" + local prefix="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | + awk ' + $2 == "T" { + symbol = $3 + sub(/^_/, "", symbol) + if (symbol == "" || + index(symbol, prefix "_") == 1 || + symbol == "Pg_magic_func" || + symbol == "_PG_init") { + next + } + if (symbol ~ /^[A-Za-z_][A-Za-z0-9_]*$/) { + print symbol + } + } + ' prefix="$prefix" | + LC_ALL=C sort -u +} + +module_has_c_symbol() { + local stem="$1" + local symbol="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | awk -v wanted="_$symbol" '$3 == wanted { found = 1 } END { exit found ? 0 : 1 }' +} + +write_mobile_static_registry_source() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + local extension stem prefix symbols_file alias_file init_symbol symbols_expr symbol_count_expr + { + cat <<'HEADER' +/* Generated by Oliphaunt mobile build. Do not edit by hand. */ +#include +#include +#include "oliphaunt.h" + +HEADER + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + defined_c_symbols "$stem" "$prefix" > "$symbols_file" + if [ ! -s "$symbols_file" ] && ! module_has_c_symbol "$stem" "${prefix}__PG_init"; then + echo "mobile static extension $extension did not produce exported C symbols or an init hook" >&2 + exit 1 + fi + printf 'extern const void *%s_Pg_magic_func(void);\n' "$prefix" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + printf 'extern void %s__PG_init(void);\n' "$prefix" + fi + while IFS= read -r symbol; do + printf 'extern void %s(void);\n' "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r _ linked_symbol; do + [ -n "$linked_symbol" ] || continue + printf 'extern void %s(void);\n' "$linked_symbol" + done < "$alias_file" + fi + printf '\n' + done + + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + [ -s "$symbols_file" ] || [ -s "$alias_file" ] || continue + printf 'static const OliphauntStaticExtensionSymbol %s_symbols[] = {\n' "$prefix" + while IFS= read -r symbol; do + printf ' { .name = "%s", .address = (void *)%s },\n' "$symbol" "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r sql_symbol linked_symbol; do + [ -n "$sql_symbol" ] || continue + [ -n "$linked_symbol" ] || continue + printf ' { .name = "%s", .address = (void *)%s },\n' "$sql_symbol" "$linked_symbol" + done < "$alias_file" + fi + printf '};\n\n' + done + + printf 'static const OliphauntStaticExtension liboliphaunt_static_extensions[] = {\n' + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + init_symbol="NULL" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + init_symbol="${prefix}__PG_init" + fi + symbols_expr="NULL" + symbol_count_expr="0" + if [ -s "$symbols_file" ] || [ -s "$alias_file" ]; then + symbols_expr="${prefix}_symbols" + symbol_count_expr="sizeof(${prefix}_symbols) / sizeof(${prefix}_symbols[0])" + fi + cat < "$mobile_static_registry_source" +} + +build_mobile_static_registry_object() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + "${cc[@]}" $native_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -c "$mobile_static_registry_source" \ + -o "$mobile_static_registry_object" + mobile_static_objects+=("$mobile_static_registry_object") +} + +write_objects_response_file() { + ( + cd "$build_dir" + { + cat src/backend/*/objfiles.txt + printf '%s\n' "${jit_objects[@]}" + printf 'src/timezone/localtime.o src/timezone/pgtz.o src/timezone/strftime.o\n' + printf '%s\n' "${plpgsql_objects[@]}" + } | tr '[:space:]' '\n' | sed '/^$/d' | awk '!seen[$0]++' > "$objects_rsp" + ) +} + +link_liboliphaunt() { + local -a postgis_link_args + local link_arg + while IFS= read -r link_arg; do + [ -n "$link_arg" ] && postgis_link_args+=("$link_arg") + done < <(oliphaunt_postgis_extra_link_args) + ( + cd "$build_dir" + set +u + "${cc[@]}" -dynamiclib \ + -Wl,-install_name,@rpath/liboliphaunt.dylib \ + -o "$lib_out" \ + "${liboliphaunt_objects[@]}" \ + "${mobile_static_objects[@]}" \ + "${mobile_static_dependency_archives[@]}" \ + @"$objects_rsp" \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a \ + $icu_libs \ + "${postgis_link_args[@]}" \ + -lpthread + set -u + ) +} + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh [--check-current] +MSG +} + +parse_mobile_static_extensions + +if [ "${OLIPHAUNT_PRINT_DESIRED_HASH:-0}" = "1" ]; then + desired_hash + exit 0 +fi + +case "$script_mode" in + build) + failure_phase="prepare source" + # EXIT is observed by this outer shell even when errexit originates inside + # a build function or subshell. The handler clears the trap before + # returning the original status, so each failure is reported exactly once. + trap 'report_failure "$?" "$LINENO"' EXIT + prepare_source + failure_phase="build ICU" + build_icu + failure_phase="configure PostgreSQL" + configure_source + if artifact_ready && (cd "$build_dir" && generated_headers_ready && backend_objects_ready && support_libraries_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "$lib_out" + exit 0 + fi + : > "$make_log" + failure_phase="generate PostgreSQL headers" + build_generated_headers + failure_phase="build PostgreSQL support libraries" + build_support_libraries + failure_phase="build PostgreSQL backend objects" + build_backend_objects + failure_phase="build PostgreSQL JIT objects" + build_jit_objects + failure_phase="build PostgreSQL timezone objects" + build_timezone_objects + failure_phase="build PL/pgSQL objects" + build_plpgsql_objects + failure_phase="build liboliphaunt objects" + build_liboliphaunt_objects + failure_phase="build mobile static dependencies" + build_mobile_static_dependencies + failure_phase="build mobile static extensions" + build_mobile_static_extension_objects + failure_phase="generate mobile static registry" + write_mobile_static_registry_source + build_mobile_static_registry_object + write_objects_response_file + failure_phase="link liboliphaunt" + link_liboliphaunt + failure_phase="verify liboliphaunt artifact" + artifact_ready + failure_phase="write completion stamp" + desired_hash > "$stamp" + echo "$lib_out" + ;; + --check-current) + if artifact_ready && (cd "$build_dir" && generated_headers_ready && backend_objects_ready && support_libraries_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "iOS device liboliphaunt dylib is current" + exit 0 + fi + echo "iOS device liboliphaunt dylib is missing or stale" >&2 + exit 1 + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh new file mode 100755 index 00000000..db638312 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh @@ -0,0 +1,1053 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/mobile-static-extensions.sh" +. "$script_dir/mobile-postgis-extensions.sh" +script_path="$script_dir/$(basename "$0")" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +oliphaunt_mobile_target="ios-simulator" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" +work_root="${OLIPHAUNT_IOS_SIMULATOR_ROOT:-$repo_root/target/liboliphaunt-ios-simulator}" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +out_dir="$work_root/out" +stamp="$build_dir/.liboliphaunt-ios-simulator-build.sha256" +configure_log="$work_root/configure.log" +make_log="$work_root/make.log" +objects_rsp="$out_dir/liboliphaunt-ios-objects.rsp" +lib_out="$out_dir/liboliphaunt.dylib" +mobile_static_registry_source="$out_dir/liboliphaunt_mobile_static_registry.c" +mobile_static_registry_object="$out_dir/liboliphaunt_mobile_static_registry.o" +script_mode="${1:-build}" +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-ios-simulator-build" +icu_prefix="$work_root/icu-ios-simulator" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lc++" +icu_libs="$icu_static_libs $icu_cpp_libs" +failure_phase="initialization" + +report_failure() { + local status="${1:-1}" + local line="${2:-unknown}" + local log + if [ "$status" -eq 0 ]; then + return 0 + fi + trap - EXIT + printf 'PostgreSQL iOS simulator build failed during %s (line %s, status %s)\n' \ + "$failure_phase" "$line" "$status" >&2 + for log in "$configure_log" "$make_log"; do + if [ -s "$log" ]; then + printf '%s\n' "--- tail of $log ---" >&2 + tail -160 "$log" >&2 || true + fi + done + exit "$status" +} + +liboliphaunt_sources=( + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) + +plpgsql_objects=( + src/pl/plpgsql/src/pl_comp.o + src/pl/plpgsql/src/pl_exec.o + src/pl/plpgsql/src/pl_funcs.o + src/pl/plpgsql/src/pl_gram.o + src/pl/plpgsql/src/pl_handler.o + src/pl/plpgsql/src/pl_scanner.o +) + +jit_objects=( + src/backend/jit/jit.o +) + +generated_header_stamps=( + src/include/catalog/bki-stamp + src/include/nodes/header-stamp + src/include/utils/header-stamp +) + +generated_header_files=( + src/backend/parser/gram.h + src/include/catalog/pg_proc_d.h + src/include/catalog/schemapg.h + src/include/nodes/nodetags.h + src/include/storage/lwlocknames.h + src/include/utils/errcodes.h + src/include/utils/fmgroids.h + src/include/utils/fmgrprotos.h + src/include/utils/pgstat_wait_event.c + src/include/utils/probes.h + src/include/utils/wait_event_funcs_data.c + src/include/utils/wait_event_types.h +) + +mobile_static_extensions=() +mobile_static_objects=() +mobile_static_dependency_archives=() +mobile_static_dependency_root="$out_dir/dependencies" +export OLIPHAUNT_MOBILE_STATIC_DEPENDENCY_ROOT="$mobile_static_dependency_root" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "PostgreSQL iOS simulator build requires Darwin" >&2 + exit 2 +fi + +for cmd in curl git nm patch perl rg shasum xcrun; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "missing required command: $cmd" >&2 + exit 1 + fi +done + +sdk_path="$(xcrun --sdk iphonesimulator --show-sdk-path 2>/dev/null || true)" +clang_path="$(xcrun --find --sdk iphonesimulator clang 2>/dev/null || true)" +clangxx_path="$(xcrun --find --sdk iphonesimulator clang++ 2>/dev/null || true)" +ar_path="$(xcrun --find --sdk iphonesimulator ar 2>/dev/null || true)" +ranlib_path="$(xcrun --find --sdk iphonesimulator ranlib 2>/dev/null || true)" +libtool_path="$(xcrun --find --sdk iphonesimulator libtool 2>/dev/null || true)" +if [ -z "$sdk_path" ] || [ -z "$clang_path" ] || [ -z "$clangxx_path" ] || [ -z "$ar_path" ] || [ -z "$ranlib_path" ] || [ -z "$libtool_path" ]; then + echo "iPhoneSimulator SDK is unavailable" >&2 + exit 1 +fi +oliphaunt_icu_require_source "$icu_source_dir" + +min_ios="${OLIPHAUNT_IOS_SIMULATOR_MIN_VERSION:-17.0}" +cc=("$clang_path" -target "arm64-apple-ios${min_ios}-simulator" "-mios-simulator-version-min=${min_ios}" -isysroot "$sdk_path") +cxx=("$clangxx_path" -target "arm64-apple-ios${min_ios}-simulator" "-mios-simulator-version-min=${min_ios}" -isysroot "$sdk_path") +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + ccache_bin="" + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" +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)}" + +parse_mobile_static_extensions() { + local raw="${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}" + [ -n "$raw" ] || return 0 + local extension + while IFS= read -r extension; do + extension="$(printf '%s' "$extension" | xargs)" + [ -n "$extension" ] || continue + if ! oliphaunt_mobile_static_extension_spec "$extension" >/dev/null; then + echo "unsupported iOS mobile static extension: $extension" >&2 + printf 'supported iOS mobile static extensions: ' >&2 + oliphaunt_mobile_static_supported_extensions | paste -sd ',' - >&2 + exit 2 + fi + mobile_static_extensions+=("$(oliphaunt_mobile_static_extension_sql_name "$extension")") + done < <(printf '%s\n' "$raw" | tr ',' '\n') +} + +mobile_static_extensions_include() { + local wanted="$1" + local extension + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + [ "$extension" = "$wanted" ] && return 0 + done + return 1 +} + +mobile_static_dependency_selected() { + local wanted="$1" + local extension dependency + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r dependency; do + [ -n "$dependency" ] || continue + [ "$dependency" = "$wanted" ] && return 0 + done < <(oliphaunt_mobile_static_extension_dependencies "$extension") + done + return 1 +} + +hash_mobile_static_extension_sources() { + local extension file + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + while IFS= read -r file; do + [ -n "$file" ] || continue + shasum -a 256 "$file" + done < <(oliphaunt_mobile_static_extension_hash_inputs "$repo_root" "$build_dir" "$extension") + done +} + +patch_series() { + sed -n '/series = \[/,/\]/p' "$source_manifest" | + sed -n 's/.*"\([^"]*\.patch\)".*/\1/p' +} + +patch_series_hash() { + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + shasum -a 256 "$patch_dir/$patch_name" + done < <(patch_series) | shasum -a 256 | awk '{print $1}' +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'sdk_path=%s\n' "$sdk_path" + printf 'clang_path=%s\n' "$clang_path" + printf 'clangxx_path=%s\n' "$clangxx_path" + printf 'min_ios=%s\n' "$min_ios" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'ar=%s\n' "$ar_path" + printf 'ranlib=%s\n' "$ranlib_path" + printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" + printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'liboliphaunt_cflags=%s\n' "$liboliphaunt_cflags" + printf 'pg_extension_cflags=%s\n' "$pg_extension_cflags" + printf 'mobile_static_extensions=%s\n' "${mobile_static_extensions[*]-}" + printf 'patch_series_hash=%s\n' "$(patch_series_hash)" + printf 'liboliphaunt_sources=%s\n' "${liboliphaunt_sources[*]}" + printf 'plpgsql_objects=%s\n' "${plpgsql_objects[*]}" + printf 'jit_objects=%s\n' "${jit_objects[*]}" + printf 'script_sha256=%s\n' "$(shasum -a 256 "$script_path" | awk '{print $1}')" + shasum -a 256 "$script_dir/postgres-backend-objects.mk" + shasum -a 256 "$script_dir/mobile-static-extensions.sh" "$script_dir/mobile-postgis-extensions.sh" + shasum -a 256 "$source_manifest" + shasum -a 256 "${liboliphaunt_sources[@]}" + hash_mobile_static_extension_sources + } | shasum -a 256 | awk '{print $1}' +} + +apply_patch_series() { + local patch_name + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_dir/$patch_name" >/dev/null + done < <(patch_series) +} + +patched_source_ready() { + grep -Fq 'OliphauntEmbeddedIO' "$build_dir/src/include/libpq/libpq-be.h" && + grep -Fq 'oliphaunt_embedded_main' "$build_dir/src/backend/tcop/postgres.c" && + grep -Fq 'oliphaunt_embedded_kill' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'oliphaunt_embedded_raise' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'getenv("ICU_DATA")' "$build_dir/src/bin/initdb/initdb.c" && + grep -Fq 'oliphaunt_embedded' "$build_dir/meson_options.txt" && + grep -Fq 'OLIPHAUNT_EMBEDDED' "$build_dir/meson.build" +} + +artifact_ready() { + [ -f "$lib_out" ] || return 1 + oliphaunt_icu_artifacts_ready "$icu_prefix" || return 1 + xcrun vtool -show-build "$lib_out" 2>/dev/null | rg -q "platform IOSSIMULATOR" || return 1 + local symbols + symbols="$(nm -g "$lib_out" 2>/dev/null || true)" + local linked_symbols + linked_symbols="$(nm "$lib_out" 2>/dev/null || true)" + oliphaunt_icu_linked_symbols_ready "$linked_symbols" || return 1 + local undefined_symbols + undefined_symbols="$(nm -u "$lib_out" 2>/dev/null || true)" + if printf '%s\n' "$undefined_symbols" | rg -q '_shm(get|ctl|dt)|_shm_open|_sem(get|ctl|op|open|close|unlink|wait|post|trywait|init|destroy)'; then + return 1 + fi + local symbol + for symbol in \ + _oliphaunt_init \ + _oliphaunt_init_ex \ + _oliphaunt_exec_protocol \ + _oliphaunt_exec_protocol_stream \ + _oliphaunt_backup \ + _oliphaunt_backup_ex \ + _oliphaunt_restore \ + _oliphaunt_cancel \ + _oliphaunt_detach \ + _oliphaunt_close \ + _oliphaunt_register_static_extensions \ + _oliphaunt_last_error \ + _oliphaunt_version \ + _oliphaunt_capabilities \ + _oliphaunt_free_response \ + _oliphaunt_embedded_kill \ + _oliphaunt_embedded_raise + do + case "$symbols" in *"$symbol"*) ;; *) return 1 ;; esac + done + if [ "${#mobile_static_extensions[@]}" -gt 0 ]; then + case "$symbols" in *"_liboliphaunt_selected_static_extensions"*) ;; *) return 1 ;; esac + local extension stem prefix + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + [ -f "$out_dir/extensions/$stem/liboliphaunt_extension_$stem.a" ] || return 1 + case "$symbols" in *"_${prefix}_Pg_magic_func"*) ;; *) return 1 ;; esac + done + fi +} + +backend_objects_ready() { + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o \ + src/backend/port/oliphaunt_embedded_sema.o \ + src/backend/port/oliphaunt_embedded_shmem.o + do + [ -f "$required" ] || return 1 + done + for objfile in src/backend/*/objfiles.txt; do + [ -s "$objfile" ] || return 1 + done + nm -g src/backend/tcop/postgres.o | rg -q "_oliphaunt_embedded_main" || return 1 +} + +support_libraries_ready() { + for required in \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a + do + [ -f "$required" ] || return 1 + done + + local symbols + symbols="$(nm -gU src/port/libpgport_srv.a 2>/dev/null || true)" + local symbol + for symbol in _oliphaunt_embedded_kill _oliphaunt_embedded_raise; do + grep -Eq "[[:space:]]${symbol}$" <<< "$symbols" || return 1 + done +} + +plpgsql_objects_ready() { + local object + for object in "${plpgsql_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/pl/plpgsql/src/pl_handler.o | rg -q "_plpgsql_call_handler" || return 1 +} + +jit_objects_ready() { + local object + for object in "${jit_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/backend/jit/jit.o | rg -q "_pg_jit_available" || return 1 +} + +generated_headers_ready() { + local required + for required in "${generated_header_stamps[@]}"; do + [ -f "$required" ] || return 1 + done + for required in "${generated_header_files[@]}"; do + [ -s "$required" ] || return 1 + done +} + +explain_generated_header_state() { + local required + for required in "${generated_header_stamps[@]}"; do + if [ ! -f "$required" ]; then + echo "missing PostgreSQL generated-header stamp: $build_dir/$required" >&2 + fi + done + for required in "${generated_header_files[@]}"; do + if [ ! -s "$required" ]; then + echo "missing or empty PostgreSQL generated header: $build_dir/$required" >&2 + fi + done +} + +prepare_source() { + mkdir -p "$source_cache" "$work_root" "$out_dir" + + oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + ( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - + ) >&2 + + local wanted + wanted="$(desired_hash)" + if [ -d "$build_dir" ] && { [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; }; then + rm -rf "$build_dir" + fi + if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" + ( + cd "$build_dir" + git init -q + apply_patch_series + ) + fi + if ! patched_source_ready; then + echo "PostgreSQL embedded patch verification failed" >&2 + exit 1 + fi +} + +configure_source() { + export CC="$cc_string" + export CXX="$cxx_string" + export CFLAGS="$native_cflags" + export CPPFLAGS="-isysroot $sdk_path $icu_cflags" + export LDFLAGS="-isysroot $sdk_path -L$icu_prefix/lib" + export ICU_CFLAGS="$icu_cflags" + export ICU_LIBS="$icu_libs" + + if [ ! -f "$build_dir/config.status" ]; then + ( + cd "$build_dir" + ./configure \ + --host=aarch64-apple-darwin \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls \ + ac_cv_file__dev_urandom=yes + ) > "$configure_log" 2>&1 + fi +} + +build_icu() { + oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$jobs" \ + "ios-simulator" \ + "aarch64-apple-darwin" \ + "$cc_string" \ + "$cxx_string" \ + "$ar_path" \ + "$ranlib_path" \ + "$native_cflags" \ + "$native_cflags" \ + "-isysroot $sdk_path" +} + +build_generated_headers() { + ( + cd "$build_dir" + + # PostgreSQL's generated-header targets use stamp files for multi-output + # generators. Invalidate both the stamps and their generated sources so a + # restored or interrupted build cannot silently reuse a stale header. + rm -f \ + src/backend/nodes/node-support-stamp \ + src/backend/nodes/nodetags.h \ + src/backend/storage/lmgr/lwlocknames.h \ + src/backend/utils/errcodes.h \ + src/backend/utils/fmgr-stamp \ + src/backend/utils/fmgroids.h \ + src/backend/utils/fmgrprotos.h \ + src/backend/utils/fmgrtab.c \ + src/backend/utils/pgstat_wait_event.c \ + src/backend/utils/probes.h \ + src/backend/utils/wait_event_funcs_data.c \ + src/backend/utils/wait_event_types.h \ + src/include/catalog/bki-stamp \ + src/include/nodes/header-stamp \ + src/include/nodes/nodetags.h \ + src/include/storage/lwlocknames.h \ + src/include/utils/errcodes.h \ + src/include/utils/fmgroids.h \ + src/include/utils/fmgrprotos.h \ + src/include/utils/header-stamp \ + src/include/utils/pgstat_wait_event.c \ + src/include/utils/probes.h \ + src/include/utils/wait_event_funcs_data.c \ + src/include/utils/wait_event_types.h + + make -C src/backend generated-headers \ + OLIPHAUNT_EMBEDDED_MOBILE_SHMEM=1 \ + CC="$cc_string" >> "$make_log" 2>&1 + + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS simulator generated headers are incomplete" >&2 + exit 1 + fi + ) +} + +build_backend_objects() { + ( + cd "$build_dir" + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS simulator backend objects require generated headers" >&2 + exit 1 + fi + if backend_objects_ready; then + echo "reusing PostgreSQL iOS simulator backend objects" >&2 + return + fi + + make -j"$jobs" -C src/backend \ + -f "$script_dir/postgres-backend-objects.mk" \ + OLIPHAUNT_EMBEDDED_MOBILE_SHMEM=1 \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + oliphaunt-backend-objects >> "$make_log" 2>&1 + + if ! backend_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS simulator backend objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_support_libraries() { + ( + cd "$build_dir" + if ! generated_headers_ready; then + explain_generated_header_state + echo "PostgreSQL iOS simulator support libraries require generated headers" >&2 + exit 1 + fi + if ! support_libraries_ready; then + make -C src/common clean >> "$make_log" 2>&1 + make -C src/port clean >> "$make_log" 2>&1 + fi + make -C src/common \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + libpgcommon_srv.a >> "$make_log" 2>&1 + make -C src/port \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + libpgport_srv.a >> "$make_log" 2>&1 + if ! support_libraries_ready; then + echo "PostgreSQL iOS simulator support libraries do not provide the embedded signal boundary" >&2 + exit 1 + fi + ) +} + +build_timezone_objects() { + ( + cd "$build_dir" + make -C src/timezone \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + localtime.o pgtz.o strftime.o >> "$make_log" 2>&1 + ) +} + +build_plpgsql_objects() { + ( + cd "$build_dir" + if plpgsql_objects_ready; then + echo "reusing PostgreSQL iOS simulator PL/pgSQL objects" >&2 + return + fi + + make -C src/pl/plpgsql/src clean >> "$make_log" 2>&1 + make -C src/pl/plpgsql/src \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + pl_comp.o pl_exec.o pl_funcs.o pl_gram.o pl_handler.o pl_scanner.o >> "$make_log" 2>&1 + + if ! plpgsql_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS simulator PL/pgSQL objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_jit_objects() { + ( + cd "$build_dir" + if jit_objects_ready; then + echo "reusing PostgreSQL iOS simulator JIT stub objects" >&2 + return + fi + + make -C src/backend/jit \ + CC="$cc_string" \ + CFLAGS="$native_cflags" \ + jit.o >> "$make_log" 2>&1 + + if ! jit_objects_ready; then + tail -120 "$make_log" >&2 || true + echo "PostgreSQL iOS simulator JIT stub objects are incomplete" >&2 + exit 1 + fi + ) +} + +build_liboliphaunt_objects() { + local source object + liboliphaunt_objects=() + for source in "${liboliphaunt_sources[@]}"; do + object="$out_dir/$(basename "${source%.c}").o" + liboliphaunt_objects+=("$object") + "${cc[@]}" $liboliphaunt_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -I"$repo_root/src/runtimes/liboliphaunt/native/src" \ + -c "$source" \ + -o "$object" + done +} + +build_openssl_dependency() { + mobile_static_dependency_selected openssl || return 0 + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/openssl" + local dependency_dir="$mobile_static_dependency_root/openssl" + local build_root="$work_root/openssl-ios-simulator" + local install_root="$work_root/openssl-ios-simulator-install" + local installed_archive="" + local archive="$dependency_dir/libcrypto.a" + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/openssl" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -d "$source_dir" ] || { + echo "OpenSSL checkout is missing: $source_dir" >&2 + exit 1 + } + + rm -rf "$build_root" "$install_root" "$dependency_dir" + mkdir -p "$(dirname "$build_root")" "$dependency_dir/include" + cp -a "$source_dir/." "$build_root/" + rm -rf "$build_root/.git" + ( + cd "$build_root" + CFLAGS="-O2 -fPIC -mios-simulator-version-min=${min_ios}" \ + ./Configure iossimulator-arm64-xcrun \ + no-shared no-tests no-apps no-docs no-engine no-module \ + --prefix="$install_root" \ + --openssldir="$install_root/ssl" >> "$make_log" 2>&1 + make -j"$jobs" build_generated libcrypto.a >> "$make_log" 2>&1 + make install_dev >> "$make_log" 2>&1 + ) + for candidate in "$install_root/lib/libcrypto.a" "$install_root/lib64/libcrypto.a"; do + if [ -f "$candidate" ]; then + installed_archive="$candidate" + break + fi + done + if [ -z "$installed_archive" ]; then + echo "OpenSSL iOS simulator build did not produce libcrypto.a" >&2 + exit 1 + fi + cp -R "$install_root/include/openssl" "$dependency_dir/include/" + cp -p "$installed_archive" "$archive" + mobile_static_dependency_archives+=("$archive") +} + +build_uuid_dependency() { + mobile_static_dependency_selected uuid || return 0 + local source_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" + local dependency_dir="$mobile_static_dependency_root/uuid" + local archive="$dependency_dir/lib/libuuid.a" + local object="$dependency_dir/portable_uuid.o" + + if [ -f "$archive" ] && [ -d "$dependency_dir/include/uuid" ]; then + mobile_static_dependency_archives+=("$archive") + return 0 + fi + [ -f "$source_dir/portable_uuid.c" ] || { + echo "portable UUID source is missing: $source_dir" >&2 + exit 1 + } + + rm -rf "$dependency_dir" + mkdir -p "$dependency_dir/include" "$dependency_dir/lib" + cp -R "$source_dir/include/uuid" "$dependency_dir/include/" + "${cc[@]}" $native_cflags \ + -I"$source_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$source_dir/portable_uuid.c" \ + -o "$object" + "$libtool_path" -static -o "$archive" "$object" + [ -s "$archive" ] || { + echo "portable UUID iOS simulator build did not produce $archive" >&2 + exit 1 + } + mobile_static_dependency_archives+=("$archive") +} + +build_mobile_static_dependencies() { + build_openssl_dependency + build_uuid_dependency + build_postgis_mobile_static_dependencies +} + +build_mobile_static_extension_objects() { + local extension source source_dir source_rel object object_dir objects_file stem prefix + local -a compile_args extension_include_args extension_cflags + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + if [ "$extension" = "postgis" ]; then + build_postgis_mobile_static_extension_objects "$extension" + continue + fi + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + source_dir="$(oliphaunt_mobile_static_extension_source_dir "$repo_root" "$build_dir" "$extension")" + extension_include_args=() + while IFS= read -r include_dir; do + [ -n "$include_dir" ] || continue + extension_include_args+=("-I$include_dir") + done < <(oliphaunt_mobile_static_extension_include_dirs "$repo_root" "$build_dir" "$extension") + extension_cflags=() + while IFS= read -r cflag; do + [ -n "$cflag" ] || continue + extension_cflags+=("$cflag") + done < <(oliphaunt_mobile_static_extension_cflags "$extension") + if [ "$(oliphaunt_mobile_static_extension_kind "$extension")" = "contrib" ]; then + ( + cd "$build_dir" + make -C "$(oliphaunt_mobile_static_extension_source_rel "$extension")" \ + CC="$cc_string" \ + CFLAGS="$pg_extension_cflags" \ + all >> "$make_log" 2>&1 || true + ) + fi + object_dir="$out_dir/extensions/$stem" + objects_file="$object_dir/objects.list" + mkdir -p "$object_dir" + : > "$objects_file" + while IFS= read -r source; do + [ -n "$source" ] || continue + source_rel="${source#$source_dir/}" + object="$object_dir/${source_rel%.c}.o" + mkdir -p "$(dirname "$object")" + mobile_static_objects+=("$object") + printf '%s\n' "$object" >> "$objects_file" + compile_args=( + "${cc[@]}" $pg_extension_cflags + -DPg_magic_func="${prefix}_Pg_magic_func" \ + -D_PG_init="${prefix}__PG_init" + ) + if [ "${#extension_cflags[@]}" -gt 0 ]; then + compile_args+=("${extension_cflags[@]}") + fi + if [ "${#extension_include_args[@]}" -gt 0 ]; then + compile_args+=("${extension_include_args[@]}") + fi + compile_args+=( + -I"$build_dir/src/include" + -I"$build_dir/src/include/port" + -c "$source" + -o "$object" + ) + "${compile_args[@]}" + done < <(oliphaunt_mobile_static_extension_source_files "$repo_root" "$build_dir" "$extension") + if [ ! -s "$objects_file" ]; then + echo "mobile static extension $extension did not produce object inputs" >&2 + exit 1 + fi + archive_mobile_static_extension_objects "$extension" "$object_dir" "$objects_file" + done +} + +archive_mobile_static_extension_objects() { + local extension="$1" + local object_dir="$2" + local objects_file="$3" + local stem archive + local -a archive_objects + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + archive="$object_dir/liboliphaunt_extension_$stem.a" + while IFS= read -r object; do + [ -n "$object" ] && archive_objects+=("$object") + done < "$objects_file" + rm -f "$archive" + "$libtool_path" -static -o "$archive" "${archive_objects[@]}" + if [ ! -s "$archive" ]; then + echo "mobile static extension $extension did not produce archive $archive" >&2 + exit 1 + fi +} + +defined_c_symbols() { + local stem="$1" + local prefix="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | + awk ' + $2 == "T" { + symbol = $3 + sub(/^_/, "", symbol) + if (symbol == "" || + index(symbol, prefix "_") == 1 || + symbol == "Pg_magic_func" || + symbol == "_PG_init") { + next + } + if (symbol ~ /^[A-Za-z_][A-Za-z0-9_]*$/) { + print symbol + } + } + ' prefix="$prefix" | + LC_ALL=C sort -u +} + +module_has_c_symbol() { + local stem="$1" + local symbol="$2" + local objects_file="$out_dir/extensions/$stem/objects.list" + # shellcheck disable=SC2046 + nm -g $(cat "$objects_file") | awk -v wanted="_$symbol" '$3 == wanted { found = 1 } END { exit found ? 0 : 1 }' +} + +write_mobile_static_registry_source() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + local extension stem prefix symbols_file alias_file init_symbol symbols_expr symbol_count_expr + { + cat <<'HEADER' +/* Generated by Oliphaunt mobile build. Do not edit by hand. */ +#include +#include +#include "oliphaunt.h" + +HEADER + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + defined_c_symbols "$stem" "$prefix" > "$symbols_file" + if [ ! -s "$symbols_file" ] && ! module_has_c_symbol "$stem" "${prefix}__PG_init"; then + echo "mobile static extension $extension did not produce exported C symbols or an init hook" >&2 + exit 1 + fi + printf 'extern const void *%s_Pg_magic_func(void);\n' "$prefix" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + printf 'extern void %s__PG_init(void);\n' "$prefix" + fi + while IFS= read -r symbol; do + printf 'extern void %s(void);\n' "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r _ linked_symbol; do + [ -n "$linked_symbol" ] || continue + printf 'extern void %s(void);\n' "$linked_symbol" + done < "$alias_file" + fi + printf '\n' + done + + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + [ -s "$symbols_file" ] || [ -s "$alias_file" ] || continue + printf 'static const OliphauntStaticExtensionSymbol %s_symbols[] = {\n' "$prefix" + while IFS= read -r symbol; do + printf ' { .name = "%s", .address = (void *)%s },\n' "$symbol" "$symbol" + done < "$symbols_file" + if [ -s "$alias_file" ]; then + while IFS=$'\t' read -r sql_symbol linked_symbol; do + [ -n "$sql_symbol" ] || continue + [ -n "$linked_symbol" ] || continue + printf ' { .name = "%s", .address = (void *)%s },\n' "$sql_symbol" "$linked_symbol" + done < "$alias_file" + fi + printf '};\n\n' + done + + printf 'static const OliphauntStaticExtension liboliphaunt_static_extensions[] = {\n' + for extension in ${mobile_static_extensions[@]+"${mobile_static_extensions[@]}"}; do + stem="$(oliphaunt_mobile_static_extension_module_stem "$extension")" + prefix="$(oliphaunt_static_symbol_prefix "$stem")" + symbols_file="$out_dir/extensions/$stem/symbols.list" + alias_file="$out_dir/extensions/$stem/symbol-aliases.list" + init_symbol="NULL" + if module_has_c_symbol "$stem" "${prefix}__PG_init"; then + init_symbol="${prefix}__PG_init" + fi + symbols_expr="NULL" + symbol_count_expr="0" + if [ -s "$symbols_file" ] || [ -s "$alias_file" ]; then + symbols_expr="${prefix}_symbols" + symbol_count_expr="sizeof(${prefix}_symbols) / sizeof(${prefix}_symbols[0])" + fi + cat < "$mobile_static_registry_source" +} + +build_mobile_static_registry_object() { + [ "${#mobile_static_extensions[@]}" -gt 0 ] || return 0 + "${cc[@]}" $native_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -c "$mobile_static_registry_source" \ + -o "$mobile_static_registry_object" + mobile_static_objects+=("$mobile_static_registry_object") +} + +write_objects_response_file() { + ( + cd "$build_dir" + { + cat src/backend/*/objfiles.txt + printf '%s\n' "${jit_objects[@]}" + printf 'src/timezone/localtime.o src/timezone/pgtz.o src/timezone/strftime.o\n' + printf '%s\n' "${plpgsql_objects[@]}" + } | tr '[:space:]' '\n' | sed '/^$/d' | awk '!seen[$0]++' > "$objects_rsp" + ) +} + +link_liboliphaunt() { + local -a postgis_link_args + local link_arg + while IFS= read -r link_arg; do + [ -n "$link_arg" ] && postgis_link_args+=("$link_arg") + done < <(oliphaunt_postgis_extra_link_args) + ( + cd "$build_dir" + set +u + "${cc[@]}" -dynamiclib \ + -Wl,-install_name,@rpath/liboliphaunt.dylib \ + -o "$lib_out" \ + "${liboliphaunt_objects[@]}" \ + "${mobile_static_objects[@]}" \ + "${mobile_static_dependency_archives[@]}" \ + @"$objects_rsp" \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a \ + $icu_libs \ + "${postgis_link_args[@]}" \ + -lpthread + set -u + ) +} + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh [--check-current] +MSG +} + +parse_mobile_static_extensions + +if [ "${OLIPHAUNT_PRINT_DESIRED_HASH:-0}" = "1" ]; then + desired_hash + exit 0 +fi + +case "$script_mode" in + build) + failure_phase="prepare source" + # EXIT is observed by this outer shell even when errexit originates inside + # a build function or subshell. The handler clears the trap before + # returning the original status, so each failure is reported exactly once. + trap 'report_failure "$?" "$LINENO"' EXIT + prepare_source + failure_phase="build ICU" + build_icu + failure_phase="configure PostgreSQL" + configure_source + if artifact_ready && (cd "$build_dir" && generated_headers_ready && backend_objects_ready && support_libraries_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "$lib_out" + exit 0 + fi + : > "$make_log" + failure_phase="generate PostgreSQL headers" + build_generated_headers + failure_phase="build PostgreSQL support libraries" + build_support_libraries + failure_phase="build PostgreSQL backend objects" + build_backend_objects + failure_phase="build PostgreSQL JIT objects" + build_jit_objects + failure_phase="build PostgreSQL timezone objects" + build_timezone_objects + failure_phase="build PL/pgSQL objects" + build_plpgsql_objects + failure_phase="build liboliphaunt objects" + build_liboliphaunt_objects + failure_phase="build mobile static dependencies" + build_mobile_static_dependencies + failure_phase="build mobile static extensions" + build_mobile_static_extension_objects + failure_phase="generate mobile static registry" + write_mobile_static_registry_source + build_mobile_static_registry_object + write_objects_response_file + failure_phase="link liboliphaunt" + link_liboliphaunt + failure_phase="verify liboliphaunt artifact" + artifact_ready + failure_phase="write completion stamp" + desired_hash > "$stamp" + echo "$lib_out" + ;; + --check-current) + if artifact_ready && (cd "$build_dir" && generated_headers_ready && backend_objects_ready && support_libraries_ready && plpgsql_objects_ready && jit_objects_ready) && [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "iOS simulator liboliphaunt dylib is current" + exit 0 + fi + echo "iOS simulator liboliphaunt dylib is missing or stale" >&2 + exit 1 + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh new file mode 100755 index 00000000..dfd2883e --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh @@ -0,0 +1,1637 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/postgis-dependency-cache.sh" + +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" + +case "$(uname -s):$(uname -m)" in + Linux:x86_64|Linux:amd64) + target_id="linux-x64-gnu" + linux_host="x86_64-unknown-linux-gnu" + ;; + Linux:aarch64|Linux:arm64) + target_id="linux-arm64-gnu" + linux_host="aarch64-unknown-linux-gnu" + ;; + *) + echo "build-postgres18-linux.sh: unsupported Linux target $(uname -s)/$(uname -m)" >&2 + exit 2 + ;; +esac + +work_root="${OLIPHAUNT_LINUX_WORK_ROOT:-${OLIPHAUNT_WORK_ROOT:-$repo_root/target/liboliphaunt-pg18-$target_id}}" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +out_dir="$work_root/out" +embedded_modules_dir="$out_dir/modules" +objects_rsp="$out_dir/liboliphaunt-$target_id-objects.rsp" +lib_out="$out_dir/liboliphaunt.so" +stamp="$build_dir/.liboliphaunt-$target_id-build.sha256" +runtime_stamp="$install_dir/.oliphaunt-postgres-runtime.sha256" +extension_build_stamp="$out_dir/native-extension-artifacts.sha256" +postgis_dependency_log="$work_root/postgis-native-dependencies.log" +configure_log="$work_root/configure.log" +make_log="$work_root/make.log" +script_mode="${1:-build}" + +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-$target_id-build" +icu_prefix="$work_root/icu-$target_id" +icu_data_dir="$work_root/icu/share/icu" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lstdc++" +icu_libs="$icu_static_libs $icu_cpp_libs" + +liboliphaunt_sources=( + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) + +plpgsql_objects=( + src/pl/plpgsql/src/pl_comp.o + src/pl/plpgsql/src/pl_exec.o + src/pl/plpgsql/src/pl_funcs.o + src/pl/plpgsql/src/pl_gram.o + src/pl/plpgsql/src/pl_handler.o + src/pl/plpgsql/src/pl_scanner.o +) + +jit_objects=(src/backend/jit/jit.o) + +contrib_extensions=( + 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 + pgcrypto + uuid-ossp + pg_surgery + pg_trgm + pg_visibility + pg_walinspect + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent +) + +external_extensions=( + pg_ivm + pg_hashids + pg_uuidv7 + pgtap + pgvector + pg_textsearch + postgis +) + +required_extension_controls=( + amcheck + 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 + pgcrypto + postgis + pg_surgery + pg_textsearch + pg_trgm + pg_uuidv7 + pg_visibility + pg_walinspect + pgtap + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent + uuid-ossp + vector +) + +required_extension_modules=( + _int + amcheck + auto_explain + bloom + btree_gin + btree_gist + citext + cube + dict_int + dict_xsyn + earthdistance + file_fdw + fuzzystrmatch + hstore + isn + lo + ltree + pageinspect + pg_buffercache + pg_freespacemap + pg_hashids + pg_ivm + pgcrypto + postgis-3 + pg_surgery + pg_textsearch + pg_trgm + pg_uuidv7 + pg_visibility + pg_walinspect + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent + uuid-ossp + vector +) + +native_extension_filter_fail() { + echo "build-postgres18-linux.sh: $*" >&2 + exit 1 +} + +native_extensions_include_postgis() { + local extension + for extension in "${external_extensions[@]}"; do + [ "$extension" != "postgis" ] || return 0 + done + return 1 +} + +native_extension_has_control_artifacts() { + case "$1" in + auto_explain) return 1 ;; + *) return 0 ;; + esac +} + +filter_native_extension_selection() { + local raw="${OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES:-${OLIPHAUNT_EXTENSION_SQL_NAMES:-}}" + [ -n "$raw" ] || return 0 + + local contrib_plan="$repo_root/src/extensions/generated/contrib-build.tsv" + local pgxs_plan="$repo_root/src/extensions/generated/pgxs-build.tsv" + local -a selected_contrib=() + local -a selected_external=() + local -a selected_controls=() + local -a selected_modules=() + local sql contrib_dir external_id module_file module_stem + + while IFS= read -r sql; do + sql="$(printf '%s' "$sql" | xargs)" + [ -n "$sql" ] || continue + if native_extension_has_control_artifacts "$sql"; then + selected_controls+=("$sql") + fi + + contrib_dir="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $3; found = 1; exit } END { exit found ? 0 : 1 }' "$contrib_plan" || true)" + if [ -n "$contrib_dir" ]; then + selected_contrib+=("$contrib_dir") + module_file="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $4; found = 1; exit } END { exit found ? 0 : 1 }' "$contrib_plan" || true)" + else + case "$sql" in + postgis) + selected_external+=(postgis) + module_file="postgis-3.so" + ;; + *) + external_id="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $1; found = 1; exit } END { exit found ? 0 : 1 }' "$pgxs_plan" || true)" + [ -n "$external_id" ] || native_extension_filter_fail "unknown native extension selection: $sql" + selected_external+=("$external_id") + module_file="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $4; found = 1; exit } END { exit found ? 0 : 1 }' "$pgxs_plan" || true)" + ;; + esac + fi + + if [ -n "$module_file" ] && [ "$module_file" != "-" ]; then + module_stem="${module_file%.*}" + selected_modules+=("$module_stem") + fi + done < <(printf '%s\n' "$raw" | tr ',' '\n') + + [ "${#selected_controls[@]}" -gt 0 ] || + [ "${#selected_modules[@]}" -gt 0 ] || + native_extension_filter_fail "OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES did not select any extensions" + contrib_extensions=() + external_extensions=() + required_extension_controls=() + required_extension_modules=() + if [ "${#selected_contrib[@]}" -gt 0 ]; then + contrib_extensions=("${selected_contrib[@]}") + fi + if [ "${#selected_external[@]}" -gt 0 ]; then + external_extensions=("${selected_external[@]}") + fi + if [ "${#selected_controls[@]}" -gt 0 ]; then + required_extension_controls=("${selected_controls[@]}") + fi + if [ "${#selected_modules[@]}" -gt 0 ]; then + required_extension_modules=("${selected_modules[@]}") + fi +} + +filter_native_extension_selection + +if [ "$script_mode" = "--print-required-extension-artifacts" ]; then + for extension in "${required_extension_controls[@]}"; do + printf 'control:%s\n' "$extension" + done + for module in "${required_extension_modules[@]}"; do + printf 'module:%s\n' "$module" + done + exit 0 +fi + +fail() { + echo "build-postgres18-linux.sh: $*" >&2 + exit 1 +} + +for cmd in curl git make nm patch perl readelf rg shasum tar; do + command -v "$cmd" >/dev/null 2>&1 || fail "missing required command: $cmd" +done + +[ "$(uname -s)" = "Linux" ] || fail "Linux native build must run on Linux" +oliphaunt_icu_require_source "$icu_source_dir" + +if { [ -n "${OLIPHAUNT_CC:-}" ] && [ -z "${OLIPHAUNT_CXX:-}" ]; } || + { [ -z "${OLIPHAUNT_CC:-}" ] && [ -n "${OLIPHAUNT_CXX:-}" ]; }; then + fail "OLIPHAUNT_CC and OLIPHAUNT_CXX must be set together" +fi +resolve_native_compiler() { + local role="$1" + local compiler="$2" + local resolved + resolved="$(command -v "$compiler" || true)" + [ -n "$resolved" ] || fail "Linux release $role compiler is unavailable: $compiler" + readlink -f "$resolved" +} +native_cc="${OLIPHAUNT_CC:-$(command -v gcc-12 || true)}" +native_cxx="${OLIPHAUNT_CXX:-$(command -v g++-12 || true)}" +[ -n "$native_cc" ] && [ -n "$native_cxx" ] || + fail "Linux release builds require GCC/G++ 12; run .github/scripts/setup-native-build-tools.sh" +native_cc="$(resolve_native_compiler C "$native_cc")" +native_cxx="$(resolve_native_compiler C++ "$native_cxx")" +native_cc_major="$("$native_cc" -dumpfullversion -dumpversion | cut -d. -f1)" +native_cxx_major="$("$native_cxx" -dumpfullversion -dumpversion | cut -d. -f1)" +if [ "$native_cc_major" != "12" ] || [ "$native_cxx_major" != "12" ]; then + fail "Linux release builds require GCC/G++ major 12, got $native_cc_major/$native_cxx_major" +fi +cc=("$native_cc") +cxx=("$native_cxx") +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +ccache_bin="" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" +cmake_compiler_args=( + "-DCMAKE_C_COMPILER=$native_cc" + "-DCMAKE_CXX_COMPILER=$native_cxx" +) +if [ -n "$ccache_bin" ]; then + cmake_compiler_args+=( + "-DCMAKE_C_COMPILER_LAUNCHER=$ccache_bin" + "-DCMAKE_CXX_COMPILER_LAUNCHER=$ccache_bin" + ) +fi +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="" +jobs="${OLIPHAUNT_JOBS:-$(nproc 2>/dev/null || echo 4)}" +portable_uuid_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" +native_uuid_dependency_dir="$work_root/portable-uuid-native" +native_uuid_archive="$native_uuid_dependency_dir/lib/libuuid.a" + +patch_series() { + sed -n '/series = \[/,/\]/p' "$source_manifest" | + sed -n 's/.*"\([^"]*\.patch\)".*/\1/p' +} + +patch_series_hash() { + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + shasum -a 256 "$patch_dir/$patch_name" + done < <(patch_series) | shasum -a 256 | awk '{print $1}' +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'target_id=%s\n' "$target_id" + printf 'linux_host=%s\n' "$linux_host" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" + printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'postgres_embedded_copt=%s\n' "$postgres_embedded_copt" + printf 'liboliphaunt_cflags=%s\n' "$liboliphaunt_cflags" + printf 'embedded_module_be_dllibs=%s\n' "$embedded_module_be_dllibs" + printf 'patch_series_hash=%s\n' "$(patch_series_hash)" + shasum -a 256 "$script_dir/$(basename "$0")" + shasum -a 256 "$script_dir/postgres-backend-objects.mk" + printf 'liboliphaunt_sources=%s\n' "${liboliphaunt_sources[*]}" + shasum -a 256 "$source_manifest" + shasum -a 256 "${liboliphaunt_sources[@]}" + } | shasum -a 256 | awk '{print $1}' +} + +hash_extension_source_tree() { + local source_dir="$1" + [ -d "$source_dir" ] || return 0 + find "$source_dir" -type f \( \ + -name "CMakeLists.txt" -o \ + -name "*.cmake" -o \ + -name "configure" -o \ + -name "configure.ac" -o \ + -name "configure.in" -o \ + -name "*.am" -o \ + -name "*.m4" -o \ + -name "*.c" -o \ + -name "*.cc" -o \ + -name "*.cpp" -o \ + -name "*.h" -o \ + -name "*.hpp" -o \ + -name "*.sql" -o \ + -name "*.control" -o \ + -name "*.in" -o \ + -name "*.awk" -o \ + -name "*.l" -o \ + -name "*.pl" -o \ + -name "*.pm" -o \ + -name "*.py" -o \ + -name "*.sh" -o \ + -name "*.y" -o \ + -name "*.csv" -o \ + -name "*.dat" -o \ + -name "*.json" -o \ + -name "*.txt" -o \ + -name "*.xml" -o \ + -name "Makefile" -o \ + -name "*.mk" -o \ + -name "meson.build" -o \ + -name "meson_options.txt" \ + \) -print | + LC_ALL=C sort | + while IFS= read -r file; do + shasum -a 256 "$file" + done +} + +exact_checkout_identity() { + local label="$1" + local checkout="$2" + local status + [ -d "$checkout" ] || { + echo "extension source checkout is missing: $checkout" >&2 + return 1 + } + git -C "$checkout" rev-parse --verify 'HEAD^{commit}' >/dev/null 2>&1 || { + echo "extension source checkout is not an exact Git commit: $checkout" >&2 + return 1 + } + status="$(git -C "$checkout" status --porcelain=v1 --untracked-files=all)" + if [ -n "$status" ]; then + echo "extension source checkout is dirty: $checkout" >&2 + return 1 + fi + printf '%s.commit=%s\n' "$label" "$(git -C "$checkout" rev-parse --verify 'HEAD^{commit}')" +} + +extension_build_fingerprint() { + local postgis_dependency_hash="" + if native_extensions_include_postgis; then + postgis_dependency_hash="$(native_postgis_dependency_fingerprint)" || return 1 + fi + + { + printf 'schema=oliphaunt-linux-native-extensions-v2\n' + printf 'pg_version=%s\n' "$pg_version" + printf 'target_id=%s\n' "$target_id" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'normal_be_dllibs=%s\n' "$normal_module_be_dllibs" + printf 'embedded_be_dllibs=%s\n' "$embedded_module_be_dllibs" + printf 'base_hash=%s\n' "$(desired_hash)" + shasum -a 256 "$script_dir/$(basename "$0")" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/include/oliphaunt.h" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_internal.h" + printf 'contrib_extensions=%s\n' "${contrib_extensions[*]}" + printf 'external_extensions=%s\n' "${external_extensions[*]}" + local extension dependency source_rel extension_checkout + for extension in "${contrib_extensions[@]}"; do + printf 'contrib:%s\n' "$extension" + hash_extension_source_tree "$build_dir/contrib/$extension" + done + for extension in "${external_extensions[@]}"; do + printf 'external:%s\n' "$extension" + source_rel="$(oliphaunt_native_external_extension_source_rel "$repo_root" "$extension" || true)" + [ -n "$source_rel" ] || { + echo "unknown external extension source mapping: $extension" >&2 + return 1 + } + extension_checkout="$repo_root/$source_rel" + exact_checkout_identity "extension.$extension" "$extension_checkout" || return 1 + hash_extension_source_tree "$extension_checkout" + done + if native_extensions_include_postgis; then + printf 'postgis-dependency-fingerprint=%s\n' "$postgis_dependency_hash" + for dependency in geos proj sqlite json-c libxml2; do + if [ -d "$repo_root/target/oliphaunt-sources/checkouts/$dependency" ]; then + printf 'postgis-dependency:%s\n' "$dependency" + local dependency_checkout="$repo_root/target/oliphaunt-sources/checkouts/$dependency" + exact_checkout_identity "postgis_dependency.$dependency" "$dependency_checkout" || return 1 + hash_extension_source_tree "$dependency_checkout" + fi + done + fi + hash_extension_source_tree "$portable_uuid_dir" + } | shasum -a 256 | awk '{print $1}' +} + +apply_patch_series() { + local patch_name + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_dir/$patch_name" >/dev/null + done < <(patch_series) +} + +patched_source_ready() { + grep -Fq 'OliphauntEmbeddedIO' "$build_dir/src/include/libpq/libpq-be.h" && + grep -Fq 'oliphaunt_embedded_main' "$build_dir/src/backend/tcop/postgres.c" && + grep -Fq 'oliphaunt_embedded_kill' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'oliphaunt_embedded_raise' "$build_dir/src/port/pqsignal.c" && + grep -Fq 'getenv("ICU_DATA")' "$build_dir/src/bin/initdb/initdb.c" && + grep -Fq 'oliphaunt_embedded' "$build_dir/meson_options.txt" && + grep -Fq 'OLIPHAUNT_EMBEDDED' "$build_dir/meson.build" +} + +prepare_source() { + mkdir -p "$source_cache" "$work_root" "$out_dir" + oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + ( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - + ) >&2 + + local wanted + wanted="$(desired_hash)" + if [ -d "$build_dir" ] && { [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; }; then + rm -rf "$build_dir" + fi + if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" + ( + cd "$build_dir" + git init -q + apply_patch_series + ) + fi + patched_source_ready || fail "PostgreSQL embedded patch verification failed" +} + +build_icu() { + oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$jobs" \ + "$target_id" \ + "$linux_host" \ + "$cc_string" \ + "$cxx_string" \ + "ar" \ + "ranlib" \ + "$native_cflags" \ + "$native_cflags -std=c++17" \ + "" +} + +configure_source() { + if [ ! -f "$build_dir/config.status" ]; then + ( + cd "$build_dir" + CPPFLAGS="$icu_cflags" \ + LDFLAGS="-L$icu_prefix/lib" \ + ICU_CFLAGS="$icu_cflags" \ + ICU_LIBS="$icu_libs" \ + CC="$cc_string" \ + CXX="$cxx_string" \ + ./configure \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls + ) > "$configure_log" 2>&1 + fi +} + +runtime_installed() { + [ -x "$install_dir/bin/initdb" ] && + [ -x "$install_dir/bin/postgres" ] && + [ -f "$install_dir/share/postgresql/postgresql.conf.sample" ] && + oliphaunt_icu_files_data_ready "$icu_data_dir" && + [ -f "$runtime_stamp" ] && + [ "$(cat "$runtime_stamp")" = "$(desired_hash)" ] && + "$install_dir/bin/pg_config" --configure 2>/dev/null | rg -q -- "--with-icu" && + { [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" != "0" ] || base_runtime_optional_extensions_absent; } +} + +explain_runtime_install_state() { + local wanted + wanted="$(desired_hash)" + for required in \ + "$install_dir/bin/initdb" \ + "$install_dir/bin/postgres" \ + "$install_dir/bin/pg_config" + do + if [ ! -x "$required" ]; then + echo "missing executable runtime file: $required" >&2 + fi + done + if [ ! -f "$install_dir/share/postgresql/postgresql.conf.sample" ]; then + echo "missing runtime file: $install_dir/share/postgresql/postgresql.conf.sample" >&2 + fi + if ! oliphaunt_icu_files_data_ready "$icu_data_dir"; then + echo "missing native ICU sidecar data files under $icu_data_dir" >&2 + fi + if [ ! -f "$runtime_stamp" ]; then + echo "missing runtime stamp: $runtime_stamp" >&2 + elif [ "$(cat "$runtime_stamp")" != "$wanted" ]; then + echo "stale runtime stamp: $runtime_stamp" >&2 + fi + if [ -x "$install_dir/bin/pg_config" ] && + ! "$install_dir/bin/pg_config" --configure 2>/dev/null | rg -q -- "--with-icu"; then + echo "installed PostgreSQL runtime was not configured with ICU" >&2 + "$install_dir/bin/pg_config" --configure >&2 || true + fi +} + +install_runtime() { + runtime_installed && return 0 + ( + cd "$build_dir" + : > "$make_log" + make clean CC="$cc_string" >> "$make_log" 2>&1 || true + make -j"$jobs" CC="$cc_string" >> "$make_log" 2>&1 + make install CC="$cc_string" >> "$make_log" 2>&1 + ) + oliphaunt_icu_stage_data "$icu_prefix" "$icu_data_dir" + rm -rf "$install_dir/share/icu" + prune_base_runtime_optional_extensions + desired_hash > "$runtime_stamp" + if ! runtime_installed; then + explain_runtime_install_state + tail -120 "$make_log" >&2 || true + fail "PostgreSQL Linux runtime install is incomplete; see $make_log" + fi +} + +backend_objects_ready() { + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a + do + [ -f "$required" ] || return 1 + done + for objfile in src/backend/*/objfiles.txt; do + [ -s "$objfile" ] || return 1 + done +} + +explain_backend_object_state() { + local required objfile + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a + do + if [ ! -f "$required" ]; then + echo "missing backend object/library: $build_dir/$required" >&2 + fi + done + + for objfile in src/backend/*/objfiles.txt; do + if [ ! -s "$objfile" ]; then + echo "missing or empty backend object list: $build_dir/$objfile" >&2 + fi + done + + echo "backend objects were built, but required object files or object lists are incomplete" >&2 +} + +plpgsql_objects_ready() { + local object + for object in "${plpgsql_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/pl/plpgsql/src/pl_handler.o | rg -q "plpgsql_call_handler" || return 1 +} + +jit_objects_ready() { + local object + for object in "${jit_objects[@]}"; do + [ -f "$object" ] || return 1 + done + nm -g src/backend/jit/jit.o | rg -q "pg_jit_available" || return 1 +} + +build_backend_objects() { + ( + cd "$build_dir" + : > "$make_log" + make -C src/backend clean >> "$make_log" 2>&1 || true + make -C src/common clean >> "$make_log" 2>&1 || true + make -C src/port clean >> "$make_log" 2>&1 || true + rm -f src/include/nodes/header-stamp src/include/utils/header-stamp + make -C src/backend generated-headers CC="$cc_string" >> "$make_log" 2>&1 + make -j"$jobs" -C src/backend \ + -f "$script_dir/postgres-backend-objects.mk" \ + CC="$cc_string" \ + CUSTOM_COPT="$postgres_embedded_copt" \ + oliphaunt-backend-objects >> "$make_log" 2>&1 + make -C src/common CC="$cc_string" CUSTOM_COPT="$postgres_embedded_copt" libpgcommon_srv.a >> "$make_log" 2>&1 + make -C src/port CC="$cc_string" CUSTOM_COPT="$postgres_embedded_copt" libpgport_srv.a >> "$make_log" 2>&1 + backend_objects_ready || { + explain_backend_object_state + tail -120 "$make_log" >&2 || true + fail "PostgreSQL Linux backend objects are incomplete" + } + ) +} + +build_timezone_objects() { + ( + cd "$build_dir" + make -C src/timezone clean >> "$make_log" 2>&1 || true + make -C src/timezone CC="$cc_string" CUSTOM_COPT="$postgres_embedded_copt" localtime.o pgtz.o strftime.o >> "$make_log" 2>&1 + ) +} + +build_plpgsql_objects() { + ( + cd "$build_dir" + if plpgsql_objects_ready; then + echo "reusing PostgreSQL $target_id PL/pgSQL objects" >&2 + return + fi + make -C src/pl/plpgsql/src clean >> "$make_log" 2>&1 + make -C src/pl/plpgsql/src \ + CC="$cc_string" \ + CUSTOM_COPT="$postgres_embedded_copt" \ + pl_comp.o pl_exec.o pl_funcs.o pl_gram.o pl_handler.o pl_scanner.o >> "$make_log" 2>&1 + plpgsql_objects_ready || { + tail -120 "$make_log" >&2 || true + fail "PostgreSQL Linux PL/pgSQL objects are incomplete" + } + ) +} + +module_depends_on_liboliphaunt() { + local module="$1" + [ -f "$module" ] || return 1 + readelf -d "$module" 2>/dev/null | + rg -q 'Shared library: \[liboliphaunt\.so\]' +} + +native_extension_artifacts_ready() { + local extension module + for extension in "${required_extension_controls[@]}"; do + [ -f "$install_dir/share/postgresql/extension/$extension.control" ] || return 1 + compgen -G "$install_dir/share/postgresql/extension/$extension--*.sql" >/dev/null || return 1 + done + + for module in "${required_extension_modules[@]}"; do + [ -f "$install_dir/lib/postgresql/$module.so" ] || return 1 + module_depends_on_liboliphaunt "$install_dir/lib/postgresql/$module.so" && return 1 + [ -f "$embedded_modules_dir/$module.so" ] || return 1 + module_depends_on_liboliphaunt "$embedded_modules_dir/$module.so" || return 1 + done + if native_extensions_include_postgis; then + [ -f "$install_dir/share/postgresql/proj/proj.db" ] || return 1 + fi +} + +base_runtime_optional_extensions_absent() { + local extension module + for extension in "${required_extension_controls[@]}"; do + [ ! -f "$install_dir/share/postgresql/extension/$extension.control" ] || return 1 + done + for module in "${required_extension_modules[@]}"; do + [ ! -f "$install_dir/lib/postgresql/$module.so" ] || return 1 + done + [ ! -d "$install_dir/share/postgresql/contrib" ] || return 1 + [ ! -d "$install_dir/share/postgresql/proj" ] || return 1 +} + +prune_base_runtime_optional_extensions() { + [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" = "0" ] || return 0 + + local extension_dir="$install_dir/share/postgresql/extension" + local module_dir="$install_dir/lib/postgresql" + local extension module + if [ -d "$extension_dir" ]; then + for extension in "${required_extension_controls[@]}"; do + rm -f "$extension_dir/$extension.control" + rm -f "$extension_dir/$extension--"*.sql + done + rm -f "$extension_dir/postgis"*.sql "$extension_dir/rtpostgis"*.sql + rm -f "$extension_dir/uninstall_postgis.sql" "$extension_dir/uninstall_legacy.sql" + rm -f "$extension_dir/pgtap-"*.sql "$extension_dir/uninstall_pgtap.sql" + fi + if [ -d "$module_dir" ]; then + for module in "${required_extension_modules[@]}"; do + rm -f "$module_dir/$module.so" + done + fi + if [ -L "$embedded_modules_dir" ] || { [ -e "$embedded_modules_dir" ] && [ ! -d "$embedded_modules_dir" ]; }; then + rm -rf "$embedded_modules_dir" + fi + mkdir -p "$embedded_modules_dir" + if [ -L "$embedded_modules_dir/plpgsql.so" ] || + { [ -e "$embedded_modules_dir/plpgsql.so" ] && [ ! -f "$embedded_modules_dir/plpgsql.so" ]; }; then + rm -rf "$embedded_modules_dir/plpgsql.so" + fi + find "$embedded_modules_dir" -mindepth 1 -maxdepth 1 ! -name plpgsql.so -exec rm -rf {} + + rm -rf "$install_dir/share/postgresql/contrib" "$install_dir/share/postgresql/proj" +} + +base_embedded_module_closure_ready() { + [ -d "$embedded_modules_dir" ] || return 1 + [ ! -L "$embedded_modules_dir" ] || return 1 + [ -f "$embedded_modules_dir/plpgsql.so" ] || return 1 + [ ! -L "$embedded_modules_dir/plpgsql.so" ] || return 1 + + local entry + while IFS= read -r -d '' entry; do + [ "$(basename "$entry")" = plpgsql.so ] || return 1 + done < <(find "$embedded_modules_dir" -mindepth 1 -maxdepth 1 -print0) +} + +native_extension_artifacts_current() { + [ -d "$build_dir" ] || return 1 + [ -x "$install_dir/bin/postgres" ] || return 1 + [ -f "$lib_out" ] || return 1 + [ -f "$extension_build_stamp" ] || return 1 + local wanted + wanted="$(extension_build_fingerprint)" || return 1 + [ "$(cat "$extension_build_stamp")" = "$wanted" ] || return 1 + native_extension_artifacts_ready || return 1 +} + +embedded_plpgsql_module_ready() { + module_depends_on_liboliphaunt "$embedded_modules_dir/plpgsql.so" +} + +build_embedded_plpgsql_module() { + ( + cd "$build_dir" + if embedded_plpgsql_module_ready; then + echo "reusing PostgreSQL $target_id embedded PL/pgSQL module" >&2 + return + fi + make -C src/pl/plpgsql/src clean >> "$make_log" 2>&1 + make -C src/pl/plpgsql/src \ + CC="$cc_string" \ + CUSTOM_COPT="$postgres_embedded_copt" \ + BE_DLLLIBS="$embedded_module_be_dllibs" \ + all >> "$make_log" 2>&1 + mkdir -p "$embedded_modules_dir" + cp -p src/pl/plpgsql/src/plpgsql.so "$embedded_modules_dir/plpgsql.so" + embedded_plpgsql_module_ready || { + tail -120 "$make_log" >&2 || true + fail "PostgreSQL Linux embedded PL/pgSQL module is not linked against liboliphaunt" + } + ) +} + +copy_embedded_modules_from_dir() { + local source_dir="$1" + mkdir -p "$embedded_modules_dir" + while IFS= read -r module; do + [ -n "$module" ] || continue + cp -p "$module" "$embedded_modules_dir/$(basename "$module")" + done < <(find "$source_dir" -maxdepth 1 -type f -name "*.so" -print) +} + +native_openssl_prefix() { + local candidate + for candidate in "${OLIPHAUNT_OPENSSL_PREFIX:-}" /usr/local /usr; do + [ -n "$candidate" ] || continue + if [ -f "$candidate/include/openssl/evp.h" ] && + { [ -f "$candidate/lib/libcrypto.a" ] || [ -f "$candidate/lib64/libcrypto.a" ] || [ -f "$candidate/lib/libcrypto.so" ] || [ -f "$candidate/lib64/libcrypto.so" ]; }; then + printf '%s\n' "$candidate" + return 0 + fi + done + return 1 +} + +configure_pgcrypto_make_args() { + pgcrypto_make_args=() + if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists openssl; then + pgcrypto_make_args=( + "PG_CPPFLAGS=$(pkg-config --cflags openssl)" + "PG_LDFLAGS=$(pkg-config --libs openssl)" + ) + return + fi + + local openssl_prefix lib_dir + openssl_prefix="$(native_openssl_prefix)" || fail "pgcrypto requires OpenSSL headers and libcrypto; install openssl-devel/libssl-dev or set OLIPHAUNT_OPENSSL_PREFIX" + lib_dir="$openssl_prefix/lib" + [ -d "$openssl_prefix/lib64" ] && lib_dir="$openssl_prefix/lib64" + if [ -f "$lib_dir/libcrypto.a" ]; then + pgcrypto_make_args=("PG_CPPFLAGS=-I$openssl_prefix/include" "PG_LDFLAGS=$lib_dir/libcrypto.a") + else + pgcrypto_make_args=("PG_CPPFLAGS=-I$openssl_prefix/include" "PG_LDFLAGS=-L$lib_dir" "LIBS=-lcrypto") + fi +} + +build_native_uuid_dependency() { + local object="$native_uuid_dependency_dir/portable_uuid.o" + if [ -f "$native_uuid_archive" ] && [ -d "$native_uuid_dependency_dir/include/uuid" ]; then + return 0 + fi + [ -f "$portable_uuid_dir/portable_uuid.c" ] || fail "portable UUID source is missing: $portable_uuid_dir" + rm -rf "$native_uuid_dependency_dir" + mkdir -p "$native_uuid_dependency_dir/include" "$native_uuid_dependency_dir/lib" + cp -R "$portable_uuid_dir/include/uuid" "$native_uuid_dependency_dir/include/" + "${cc[@]}" $native_cflags \ + -I"$portable_uuid_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$portable_uuid_dir/portable_uuid.c" \ + -o "$object" + ar crs "$native_uuid_archive" "$object" + ranlib "$native_uuid_archive" + [ -s "$native_uuid_archive" ] || fail "portable UUID native build did not produce $native_uuid_archive" +} + +build_contrib_extension() { + local extension="$1" + local -a extra_make_args=() + local -a embedded_extra_make_args=() + local embedded_pg_ldflags="$embedded_module_be_dllibs" + local arg + if [ "$extension" = "pgcrypto" ]; then + configure_pgcrypto_make_args + extra_make_args=("${pgcrypto_make_args[@]}") + elif [ "$extension" = "uuid-ossp" ]; then + build_native_uuid_dependency + extra_make_args=( + "PG_CPPFLAGS=-I$portable_uuid_dir/include -DHAVE_UUID_E2FS=1 -DHAVE_UUID_UUID_H=1" + "UUID_LIBS=$native_uuid_archive" + ) + fi + for arg in "${extra_make_args[@]}"; do + case "$arg" in + PG_LDFLAGS=*) + embedded_pg_ldflags="${arg#PG_LDFLAGS=} $embedded_pg_ldflags" + ;; + *) + embedded_extra_make_args+=("$arg") + ;; + esac + done + embedded_extra_make_args+=("PG_LDFLAGS=$embedded_pg_ldflags") + + ( + cd "$build_dir" + make -C "contrib/$extension" clean >> "$make_log" 2>&1 || true + make -C "contrib/$extension" \ + CC="$cc_string" \ + BE_DLLLIBS="$normal_module_be_dllibs" \ + "${extra_make_args[@]}" \ + install >> "$make_log" 2>&1 + make -C "contrib/$extension" clean >> "$make_log" 2>&1 || true + make -C "contrib/$extension" \ + CC="$cc_string" \ + CUSTOM_COPT="$postgres_embedded_copt" \ + "${embedded_extra_make_args[@]}" \ + all >> "$make_log" 2>&1 + copy_embedded_modules_from_dir "contrib/$extension" + ) +} + +pgxs_extension_link_args() { + local extension="$1" + local be_dllibs="$2" + [ -z "$be_dllibs" ] || printf '%s\n' "PG_LDFLAGS=$be_dllibs" + case "$extension" in + pg_textsearch|pgvector|vector) + printf '%s\n' "SHLIB_LINK=-lm" + ;; + *) + ;; + esac +} + +pgxs_extension_source_rel() { + local extension="$1" + [ "$extension" != postgis ] || return 1 + oliphaunt_native_external_extension_source_rel "$repo_root" "$extension" +} + +build_pgxs_extension() { + local extension="$1" + local source_rel + source_rel="$(pgxs_extension_source_rel "$extension" || true)" + [ -n "$source_rel" ] || native_extension_filter_fail "unknown PGXS extension source mapping: $extension" + local checkout="$repo_root/$source_rel" + local build_checkout="$work_root/external-$extension" + local -a normal_link_args=() + local -a embedded_link_args=() + while IFS= read -r arg; do + [ -n "$arg" ] || continue + normal_link_args+=("$arg") + done < <(pgxs_extension_link_args "$extension" "$normal_module_be_dllibs") + while IFS= read -r arg; do + [ -n "$arg" ] || continue + embedded_link_args+=("$arg") + done < <(pgxs_extension_link_args "$extension" "$embedded_module_be_dllibs") + [ -d "$checkout" ] || fail "native extension checkout is missing: $checkout" + rm -rf "$build_checkout" + mkdir -p "$(dirname "$build_checkout")" + cp -a "$checkout/." "$build_checkout/" + rm -rf "$build_checkout/.git" + make -C "$build_checkout" PG_CONFIG="$install_dir/bin/pg_config" clean >> "$make_log" 2>&1 || true + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + CC="$cc_string" \ + OPTFLAGS="" \ + "${normal_link_args[@]}" \ + install >> "$make_log" 2>&1 + make -C "$build_checkout" PG_CONFIG="$install_dir/bin/pg_config" clean >> "$make_log" 2>&1 || true + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + CC="$cc_string" \ + CUSTOM_COPT="$postgres_embedded_copt" \ + OPTFLAGS="" \ + "${embedded_link_args[@]}" \ + all >> "$make_log" 2>&1 + copy_embedded_modules_from_dir "$build_checkout" +} + +native_postgis_dependency_root="${OLIPHAUNT_NATIVE_POSTGIS_DEPENDENCY_ROOT:-$work_root/postgis-native-dependencies}" +native_postgis_dependency_build_roots=( + "$work_root/json-c-native-build" + "$work_root/sqlite-native-build" + "$work_root/geos-native-build" + "$work_root/libxml2-native-build" + "$work_root/proj-native-build" +) +native_postgis_dependency_required_outputs=( + "$native_postgis_dependency_root/json-c/lib/libjson-c.a" + "$native_postgis_dependency_root/sqlite/lib/libsqlite3.a" + "$native_postgis_dependency_root/geos/lib/libgeos_c.a" + "$native_postgis_dependency_root/geos/lib/libgeos.a" + "$native_postgis_dependency_root/libxml2/lib/libxml2.a" + "$native_postgis_dependency_root/libxml2/bin/xml2-config" + "$native_postgis_dependency_root/proj/lib/libproj.a" + "$native_postgis_dependency_root/proj/share/proj/proj.db" +) +postgis_configure_args=() +postgis_configure_env=() +postgis_make_args=() +postgis_proj_prefix="" + +native_postgis_require_tools() { + local cmd + for cmd in cmake rsync sqlite3; do + command -v "$cmd" >/dev/null 2>&1 || fail "PostGIS native dependency build missing required command: $cmd" + done +} + +native_postgis_compiler_identity() { + local role="$1" + local compiler="$2" + local resolved + resolved="$(command -v "$compiler")" || fail "PostGIS native dependency build missing $role compiler: $compiler" + resolved="$(readlink -f "$resolved")" + [ -f "$resolved" ] || fail "PostGIS native dependency $role compiler is not a file: $resolved" + + printf '%s.path=%s\n' "$role" "$resolved" + printf '%s.version=%s\n' "$role" "$("$resolved" --version | sed -n '1p')" + printf '%s.machine=%s\n' "$role" "$("$resolved" -dumpmachine)" + printf '%s.dumpversion=%s\n' "$role" "$("$resolved" -dumpfullversion -dumpversion)" + printf '%s.binary=' "$role" + shasum -a 256 "$resolved" +} + +native_postgis_dependency_fingerprint() { + { + printf 'target_id=%s\n' "$target_id" + printf 'native_cflags=%s\n' "$native_cflags" + native_postgis_compiler_identity cc "$native_cc" + native_postgis_compiler_identity cxx "$native_cxx" + printf 'cmake=%s\n' "$(cmake --version | sed -n '1p')" + shasum -a 256 "$script_dir/$(basename "$0")" + shasum -a 256 "$script_dir/postgis-dependency-cache.sh" + shasum -a 256 "$source_manifest" + + local dependency source_dir + for dependency in geos proj sqlite json-c libxml2; do + source_dir="$repo_root/target/oliphaunt-sources/checkouts/$dependency" + exact_checkout_identity "dependency.$dependency" "$source_dir" || return 1 + hash_extension_source_tree "$source_dir" + done + } | shasum -a 256 | awk '{print $1}' +} + +native_postgis_cmake_install() { + local source_dir="$1" + local build_root="$2" + local dependency_dir="$3" + shift 3 + cmake -S "$source_dir" -B "$build_root" \ + -DCMAKE_INSTALL_PREFIX="$dependency_dir" \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + "${cmake_compiler_args[@]}" \ + "$@" >> "$postgis_dependency_log" 2>&1 + cmake --build "$build_root" --target install -- -j"$jobs" >> "$postgis_dependency_log" 2>&1 +} + +build_native_postgis_jsonc_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/json-c" + local dependency_dir="$native_postgis_dependency_root/json-c" + local build_root="$work_root/json-c-native-build" + local archive="$dependency_dir/lib/libjson-c.a" + [ -f "$archive" ] && [ -d "$dependency_dir/include/json-c" ] && return 0 + [ -f "$source_dir/CMakeLists.txt" ] || fail "missing JSON-C checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_APPS=OFF \ + -DBUILD_TESTING=OFF \ + -DDISABLE_WERROR=ON + [ -f "$archive" ] || fail "JSON-C build did not produce $archive" +} + +build_native_postgis_sqlite_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/sqlite" + local dependency_dir="$native_postgis_dependency_root/sqlite" + local build_root="$work_root/sqlite-native-build" + local archive="$dependency_dir/lib/libsqlite3.a" + [ -f "$archive" ] && [ -f "$dependency_dir/include/sqlite3.h" ] && return 0 + [ -x "$source_dir/configure" ] || fail "missing SQLite checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + mkdir -p "$build_root" "$dependency_dir/include" "$dependency_dir/lib" + rsync -a --delete --exclude .git "$source_dir/" "$build_root/" + ( + cd "$build_root" + 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" $(oliphaunt_native_release_cflags -fPIC) \ + -DSQLITE_THREADSAFE=0 \ + -DSQLITE_OMIT_LOAD_EXTENSION \ + -c sqlite3.c \ + -o sqlite3.o >> "$postgis_dependency_log" 2>&1 + ar crs "$archive" sqlite3.o >> "$postgis_dependency_log" 2>&1 + ranlib "$archive" >> "$postgis_dependency_log" 2>&1 + cp -p sqlite3.h sqlite3ext.h "$dependency_dir/include/" + ) + [ -f "$archive" ] || fail "SQLite build did not produce $archive" +} + +build_native_postgis_geos_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/geos" + local dependency_dir="$native_postgis_dependency_root/geos" + local build_root="$work_root/geos-native-build" + [ -f "$dependency_dir/lib/libgeos_c.a" ] && [ -f "$dependency_dir/lib/libgeos.a" ] && [ -f "$dependency_dir/include/geos_c.h" ] && return 0 + [ -f "$source_dir/CMakeLists.txt" ] || fail "missing GEOS checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_BENCHMARKS=OFF \ + -DBUILD_GEOSOP=OFF \ + -DGEOS_BUILD_DEVELOPER=OFF + [ -f "$dependency_dir/lib/libgeos_c.a" ] || fail "GEOS build did not produce libgeos_c.a" + [ -f "$dependency_dir/lib/libgeos.a" ] || fail "GEOS build did not produce libgeos.a" +} + +build_native_postgis_libxml2_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/libxml2" + local dependency_dir="$native_postgis_dependency_root/libxml2" + local build_root="$work_root/libxml2-native-build" + local archive="$dependency_dir/lib/libxml2.a" + [ -f "$archive" ] && [ -x "$dependency_dir/bin/xml2-config" ] && return 0 + [ -f "$source_dir/CMakeLists.txt" ] || fail "missing libxml2 checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + -DLIBXML2_WITH_PROGRAMS=OFF \ + -DLIBXML2_WITH_TESTS=OFF \ + -DLIBXML2_WITH_PYTHON=OFF \ + -DLIBXML2_WITH_THREADS=OFF \ + -DLIBXML2_WITH_MODULES=OFF \ + -DLIBXML2_WITH_ICONV=OFF \ + -DLIBXML2_WITH_ZLIB=OFF \ + -DLIBXML2_WITH_LZMA=OFF \ + -DLIBXML2_WITH_HTTP=OFF + [ -f "$archive" ] || fail "libxml2 build did not produce $archive" +} + +build_native_postgis_proj_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/proj" + local dependency_dir="$native_postgis_dependency_root/proj" + local sqlite_dir="$native_postgis_dependency_root/sqlite" + local build_root="$work_root/proj-native-build" + local archive="$dependency_dir/lib/libproj.a" + [ -f "$archive" ] && [ -f "$dependency_dir/share/proj/proj.db" ] && return 0 + [ -f "$source_dir/CMakeLists.txt" ] || fail "missing PROJ checkout: $source_dir" + [ -f "$sqlite_dir/lib/libsqlite3.a" ] || fail "PROJ dependency requires SQLite archive first" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + "-DSQLite3_INCLUDE_DIR=$sqlite_dir/include" \ + "-DSQLite3_LIBRARY=$sqlite_dir/lib/libsqlite3.a" \ + "-DEXE_SQLITE3=$(command -v sqlite3)" \ + -DENABLE_TIFF=OFF \ + -DENABLE_CURL=OFF \ + -DENABLE_EMSCRIPTEN_FETCH=OFF \ + -DHAVE_LIBDL=OFF \ + -DBUILD_APPS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DEMBED_RESOURCE_FILES=ON \ + -DUSE_ONLY_EMBEDDED_RESOURCE_FILES=ON + mkdir -p "$dependency_dir/share/proj" + if [ -f "$build_root/data/proj.db" ]; then + cp -p "$build_root/data/proj.db" "$dependency_dir/share/proj/proj.db" + fi + [ -f "$archive" ] || fail "PROJ build did not produce $archive" + [ -f "$dependency_dir/share/proj/proj.db" ] || fail "PROJ build did not produce proj.db" +} + +build_native_postgis_dependencies() { + native_postgis_require_tools + local wanted + wanted="$(native_postgis_dependency_fingerprint)" + oliphaunt_postgis_dependency_cache_prepare \ + "$native_postgis_dependency_root" \ + "$wanted" \ + "${native_postgis_dependency_build_roots[@]}" + mkdir -p "$(dirname "$postgis_dependency_log")" + : > "$postgis_dependency_log" + build_native_postgis_jsonc_dependency + build_native_postgis_sqlite_dependency + build_native_postgis_geos_dependency + build_native_postgis_libxml2_dependency + build_native_postgis_proj_dependency + oliphaunt_postgis_dependency_cache_commit \ + "$native_postgis_dependency_root" \ + "$wanted" \ + "${native_postgis_dependency_required_outputs[@]}" +} + +native_postgis_geos_config_script() { + local path="$1" + cat > "$path" < "$path" <> "$make_log" 2>&1 + fi + ./configure \ + --prefix="$install_dir" \ + --with-pgconfig="$install_dir/bin/pg_config" \ + "${postgis_configure_args[@]}" \ + --without-protobuf \ + --without-raster \ + --without-topology \ + --without-sfcgal \ + --without-address-standardizer \ + --without-tiger \ + --disable-nls >> "$make_log" 2>&1 + patch_postgis_generated_makefiles "$postgis_build_dir" + make postgis_revision.h >> "$make_log" 2>&1 + make clean >> "$make_log" 2>&1 || true + make postgis_revision.h >> "$make_log" 2>&1 + make -C doc CC="$native_cc" "${postgis_make_args[@]}" comments-install >> "$make_log" 2>&1 + make -j"$jobs" -C postgis CC="$native_cc" "${postgis_make_args[@]}" install >> "$make_log" 2>&1 + make -j1 -C extensions CC="$native_cc" "${postgis_make_args[@]}" all >> "$make_log" 2>&1 + make -j1 -C extensions CC="$native_cc" "${postgis_make_args[@]}" install >> "$make_log" 2>&1 + make -C postgis clean >> "$make_log" 2>&1 || true + make postgis_revision.h >> "$make_log" 2>&1 + make -j"$jobs" -C postgis CC="$native_cc" CFLAGS="$native_cflags" BE_DLLLIBS="$embedded_module_be_dllibs" "${postgis_make_args[@]}" all >> "$make_log" 2>&1 + ) + copy_embedded_postgis_module "$postgis_build_dir/postgis" + stage_postgis_data_files "$postgis_build_dir" +} + +build_native_extension_artifacts() { + [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" != "0" ] || return 0 + local wanted extension + wanted="$(extension_build_fingerprint)" + if [ "${OLIPHAUNT_FORCE_EXTENSION_REBUILD:-0}" != "1" ] && + [ -f "$extension_build_stamp" ] && + [ "$(cat "$extension_build_stamp")" = "$wanted" ] && + native_extension_artifacts_ready; then + echo "reusing Linux $target_id native extension artifacts" + return 0 + fi + + rm -f "$extension_build_stamp" + mkdir -p "$embedded_modules_dir" + for extension in "${contrib_extensions[@]}"; do + build_contrib_extension "$extension" + done + for extension in "${external_extensions[@]}"; do + if [ "$extension" = "postgis" ]; then + build_postgis_extension + else + build_pgxs_extension "$extension" + fi + done + native_extension_artifacts_ready || { + tail -160 "$make_log" >&2 || true + fail "Linux $target_id native extension build did not produce required artifacts" + } + extension_build_fingerprint > "$extension_build_stamp" +} + +build_jit_objects() { + ( + cd "$build_dir" + if jit_objects_ready; then + echo "reusing PostgreSQL $target_id JIT stub objects" >&2 + return + fi + make -C src/backend/jit CC="$cc_string" CUSTOM_COPT="$postgres_embedded_copt" jit.o >> "$make_log" 2>&1 + jit_objects_ready || { + tail -120 "$make_log" >&2 || true + fail "PostgreSQL Linux JIT stub objects are incomplete" + } + ) +} + +build_liboliphaunt_objects() { + local source object + liboliphaunt_objects=() + for source in "${liboliphaunt_sources[@]}"; do + object="$out_dir/$(basename "${source%.c}").o" + liboliphaunt_objects+=("$object") + "${cc[@]}" $liboliphaunt_cflags \ + -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ + -I"$repo_root/src/runtimes/liboliphaunt/native/src" \ + -c "$source" \ + -o "$object" + done +} + +write_objects_response_file() { + ( + cd "$build_dir" + { + cat src/backend/*/objfiles.txt + printf '%s\n' "${jit_objects[@]}" + printf 'src/timezone/localtime.o src/timezone/pgtz.o src/timezone/strftime.o\n' + printf '%s\n' "${plpgsql_objects[@]}" + } | tr '[:space:]' '\n' | sed '/^$/d' | awk '!seen[$0]++' > "$objects_rsp" + ) +} + +link_liboliphaunt() { + ( + cd "$build_dir" + "${cc[@]}" -shared \ + -Wl,-soname,liboliphaunt.so \ + -Wl,-z,defs \ + -o "$lib_out" \ + "${liboliphaunt_objects[@]}" \ + @"$objects_rsp" \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a \ + $icu_libs \ + -lpthread \ + -lm \ + -ldl + ) +} + +artifact_ready() { + [ -f "$lib_out" ] || return 1 + embedded_plpgsql_module_ready || return 1 + { [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" != "0" ] || base_embedded_module_closure_ready; } || return 1 + oliphaunt_icu_artifacts_ready "$icu_prefix" || return 1 + local dynamic_symbols linked_symbols + dynamic_symbols="$(nm -D --defined-only "$lib_out" 2>/dev/null || true)" + linked_symbols="$(nm -S --defined-only "$lib_out" 2>/dev/null || true)" + oliphaunt_icu_linked_symbols_ready "$linked_symbols" || return 1 + local symbol + for symbol in \ + oliphaunt_init \ + oliphaunt_init_ex \ + oliphaunt_exec_protocol \ + oliphaunt_exec_simple_query \ + oliphaunt_exec_protocol_stream \ + oliphaunt_backup \ + oliphaunt_backup_ex \ + oliphaunt_restore \ + oliphaunt_cancel \ + oliphaunt_detach \ + oliphaunt_close \ + oliphaunt_register_static_extensions \ + oliphaunt_last_error \ + oliphaunt_version \ + oliphaunt_capabilities \ + oliphaunt_free_response \ + oliphaunt_embedded_kill \ + oliphaunt_embedded_raise + do + case "$dynamic_symbols" in *" $symbol"*) ;; *) return 1 ;; esac + done +} + +usage() { + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh [--runtime-only|--print-required-extension-artifacts|--check-current|--check-extension-artifacts-current] +MSG +} + +case "$script_mode" in + build) + prepare_source + build_icu + configure_source + install_runtime + prune_base_runtime_optional_extensions + if artifact_ready && + (cd "$build_dir" && backend_objects_ready && plpgsql_objects_ready && jit_objects_ready) && + [ -f "$stamp" ] && + [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "$lib_out" + exit 0 + fi + build_backend_objects + build_jit_objects + build_timezone_objects + build_plpgsql_objects + build_liboliphaunt_objects + write_objects_response_file + link_liboliphaunt + build_embedded_plpgsql_module + build_native_extension_artifacts + artifact_ready || fail "Linux liboliphaunt shared library did not pass export checks" + desired_hash > "$stamp" + echo "$lib_out" + ;; + --runtime-only) + prepare_source + build_icu + configure_source + install_runtime + echo "$install_dir" + ;; + --check-current) + if artifact_ready && + (cd "$build_dir" && backend_objects_ready && plpgsql_objects_ready && jit_objects_ready) && + runtime_installed && + [ -f "$stamp" ] && + [ "$(cat "$stamp")" = "$(desired_hash)" ]; then + echo "Linux $target_id liboliphaunt shared library is current" + exit 0 + fi + echo "Linux $target_id liboliphaunt shared library is missing or stale" >&2 + exit 1 + ;; + --check-extension-artifacts-current) + if native_extension_artifacts_current; then + echo "Linux $target_id native extension artifacts are current" + exit 0 + fi + echo "Linux $target_id native extension artifacts are missing or stale" >&2 + exit 1 + ;; + *) + usage + exit 2 + ;; +esac diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh new file mode 100755 index 00000000..c15c6eda --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh @@ -0,0 +1,2398 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +script_path="$script_dir/$(basename "${BASH_SOURCE[0]}")" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +. "$script_dir/postgis-dependency-cache.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +macos_deployment_target="${MACOSX_DEPLOYMENT_TARGET:-11.0}" +case "$macos_deployment_target" in + ""|*[!0-9.]*) + echo "MACOSX_DEPLOYMENT_TARGET must be a numeric dotted version" >&2 + exit 2 + ;; +esac +export MACOSX_DEPLOYMENT_TARGET="$macos_deployment_target" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" +work_root="${OLIPHAUNT_WORK_ROOT:-$repo_root/target/liboliphaunt-pg18}" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +out_dir="$work_root/out" +embedded_modules_dir="$out_dir/modules" +generation_stamp="$work_root/.oliphaunt-macos-generation.sha256" +postgis_dependency_log="$work_root/postgis-native-dependencies.log" +liboliphaunt_build_stamp="$out_dir/liboliphaunt.dylib.inputs.sha256" +extension_build_stamp="$out_dir/native-extension-artifacts.sha256" +embedded_plpgsql_build_stamp="$out_dir/plpgsql.dylib.inputs.sha256" +postgres_runtime_stamp="$install_dir/.oliphaunt-postgres-runtime.sha256" +macos_module_nm_audit="$repo_root/src/runtimes/liboliphaunt/native/tools/audit-macos-module-nm.awk" +macos_provider_collision_audit="$repo_root/src/runtimes/liboliphaunt/native/tools/audit-macos-provider-collisions.awk" +liboliphaunt_sources=( + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c" + "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) +liboliphaunt_objects=() +lib_out="$out_dir/liboliphaunt.dylib" +objects_rsp="$out_dir/liboliphaunt_objects.rsp" +build_stamp="$build_dir/.liboliphaunt-build.sha256" +script_mode="${1:-build}" +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-macos-build" +icu_prefix="$work_root/icu-macos" +icu_data_dir="$work_root/icu/share/icu" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lc++" +icu_libs="$icu_static_libs $icu_cpp_libs" + +contrib_extensions=( + 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 + pgcrypto + uuid-ossp + pg_surgery + pg_trgm + pg_visibility + pg_walinspect + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent +) + +external_extensions=( + pg_ivm + pg_hashids + pg_uuidv7 + pgtap + pgvector + pg_textsearch + postgis +) + +required_extension_controls=( + amcheck + 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 + pgcrypto + postgis + pg_surgery + pg_textsearch + pg_trgm + pg_uuidv7 + pg_visibility + pg_walinspect + pgtap + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent + uuid-ossp + vector +) + +required_extension_modules=( + _int + amcheck + auto_explain + bloom + btree_gin + btree_gist + citext + cube + dict_int + dict_xsyn + earthdistance + file_fdw + fuzzystrmatch + hstore + isn + lo + ltree + pageinspect + pg_buffercache + pg_freespacemap + pg_hashids + pg_ivm + pgcrypto + postgis-3 + pg_surgery + pg_textsearch + pg_trgm + pg_uuidv7 + pg_visibility + pg_walinspect + seg + tablefunc + tcn + tsm_system_rows + tsm_system_time + unaccent + uuid-ossp + vector +) + +native_extension_filter_fail() { + echo "build-postgres18-macos.sh: $*" >&2 + exit 1 +} + +native_extensions_include_postgis() { + local extension + for extension in "${external_extensions[@]}"; do + [ "$extension" != "postgis" ] || return 0 + done + return 1 +} + +native_extension_has_control_artifacts() { + case "$1" in + auto_explain) return 1 ;; + *) return 0 ;; + esac +} + +filter_native_extension_selection() { + local raw="${OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES:-${OLIPHAUNT_EXTENSION_SQL_NAMES:-}}" + [ -n "$raw" ] || return 0 + + local contrib_plan="$repo_root/src/extensions/generated/contrib-build.tsv" + local pgxs_plan="$repo_root/src/extensions/generated/pgxs-build.tsv" + local -a selected_contrib=() + local -a selected_external=() + local -a selected_controls=() + local -a selected_modules=() + local sql contrib_dir external_id module_file module_stem + + while IFS= read -r sql; do + sql="$(printf '%s' "$sql" | xargs)" + [ -n "$sql" ] || continue + if native_extension_has_control_artifacts "$sql"; then + selected_controls+=("$sql") + fi + + contrib_dir="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $3; found = 1; exit } END { exit found ? 0 : 1 }' "$contrib_plan" || true)" + if [ -n "$contrib_dir" ]; then + selected_contrib+=("$contrib_dir") + module_file="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $4; found = 1; exit } END { exit found ? 0 : 1 }' "$contrib_plan" || true)" + else + case "$sql" in + postgis) + selected_external+=(postgis) + module_file="postgis-3.so" + ;; + *) + external_id="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $1; found = 1; exit } END { exit found ? 0 : 1 }' "$pgxs_plan" || true)" + [ -n "$external_id" ] || native_extension_filter_fail "unknown native extension selection: $sql" + selected_external+=("$external_id") + module_file="$(awk -F '\t' -v sql="$sql" 'NR > 1 && $2 == sql { print $4; found = 1; exit } END { exit found ? 0 : 1 }' "$pgxs_plan" || true)" + ;; + esac + fi + + if [ -n "$module_file" ] && [ "$module_file" != "-" ]; then + module_stem="${module_file%.*}" + selected_modules+=("$module_stem") + fi + done < <(printf '%s\n' "$raw" | tr ',' '\n') + + [ "${#selected_controls[@]}" -gt 0 ] || + [ "${#selected_modules[@]}" -gt 0 ] || + native_extension_filter_fail "OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES did not select any extensions" + contrib_extensions=() + external_extensions=() + required_extension_controls=() + required_extension_modules=() + if [ "${#selected_contrib[@]}" -gt 0 ]; then + contrib_extensions=("${selected_contrib[@]}") + fi + if [ "${#selected_external[@]}" -gt 0 ]; then + external_extensions=("${selected_external[@]}") + fi + if [ "${#selected_controls[@]}" -gt 0 ]; then + required_extension_controls=("${selected_controls[@]}") + fi + if [ "${#selected_modules[@]}" -gt 0 ]; then + required_extension_modules=("${selected_modules[@]}") + fi +} + +filter_native_extension_selection + +if [ "$script_mode" = "--print-required-extension-artifacts" ]; then + for extension in "${required_extension_controls[@]}"; do + printf 'control:%s\n' "$extension" + done + for module in "${required_extension_modules[@]}"; do + printf 'module:%s\n' "$module" + done + exit 0 +fi + +verify_source_manifest() { + grep -q "version = \"$pg_version\"" "$source_manifest" && + grep -q "url = \"$pg_url\"" "$source_manifest" && + grep -q "sha256 = \"$pg_sha256\"" "$source_manifest" +} + +patch_series_hash() { + ( + export LC_ALL=C + local patch + for patch in "$patch_dir"/*.patch; do + printf '%s %s\n' "$(basename "$patch")" "$(shasum -a 256 "$patch" | awk '{print $1}')" + done + ) | shasum -a 256 | awk '{print $1}' +} + +native_command_identity() { + local role="$1" + local command_line="$2" + local command_name="${command_line%% *}" + local resolved + resolved="$(command -v "$command_name" 2>/dev/null || true)" + printf '%s.command=%s\n' "$role" "$command_line" + if [ -z "$resolved" ]; then + printf '%s.state=missing\n' "$role" + return 0 + fi + printf '%s.path=%s\n' "$role" "$resolved" + if [ -f "$resolved" ]; then + printf '%s.binary=%s\n' "$role" "$(shasum -a 256 "$resolved" | awk '{print $1}')" + fi +} + +native_brew_prefix() { + local formula="$1" + local candidate + if command -v brew >/dev/null 2>&1; then + candidate="$(brew --prefix "$formula" 2>/dev/null || true)" + if [ -n "$candidate" ] && [ -d "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + fi + return 1 +} + +native_openssl_prefix() { + local candidate + for candidate in \ + "${OLIPHAUNT_OPENSSL_PREFIX:-}" \ + "${OPENSSL_PREFIX:-}" \ + /opt/homebrew/opt/openssl@3 \ + /usr/local/opt/openssl@3 \ + /opt/homebrew/opt/openssl \ + /usr/local/opt/openssl + do + [ -n "$candidate" ] || continue + if [ -f "$candidate/include/openssl/evp.h" ] && + { [ -f "$candidate/lib/libcrypto.a" ] || [ -f "$candidate/lib/libcrypto.dylib" ]; }; then + printf '%s\n' "$candidate" + return 0 + fi + done + if command -v brew >/dev/null 2>&1; then + for candidate in "$(brew --prefix openssl@3 2>/dev/null || true)" "$(brew --prefix openssl 2>/dev/null || true)"; do + [ -n "$candidate" ] || continue + if [ -f "$candidate/include/openssl/evp.h" ] && + { [ -f "$candidate/lib/libcrypto.a" ] || [ -f "$candidate/lib/libcrypto.dylib" ]; }; then + printf '%s\n' "$candidate" + return 0 + fi + done + fi + echo "pgcrypto requires OpenSSL headers and libcrypto; set OLIPHAUNT_OPENSSL_PREFIX to a prefix containing include/openssl/evp.h and lib/libcrypto.{a,dylib}" >&2 + return 1 +} + +native_dependency_prefix() { + local env_var="$1" + local formula="$2" + shift 2 + local override="${!env_var:-}" + local candidate + for candidate in "$override" "$@"; do + [ -n "$candidate" ] || continue + if [ -d "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + if native_brew_prefix "$formula"; then + return 0 + fi + return 1 +} + +native_dependency_tool() { + local env_var="$1" + local tool="$2" + local formula="$3" + shift 3 + local override="${!env_var:-}" + local candidate + if [ -n "$override" ] && [ -x "$override" ]; then + printf '%s\n' "$override" + return 0 + fi + if command -v "$tool" >/dev/null 2>&1; then + command -v "$tool" + return 0 + fi + for candidate in "$@"; do + [ -n "$candidate" ] || continue + if [ -x "$candidate" ]; then + printf '%s\n' "$candidate" + return 0 + fi + done + candidate="$(native_brew_prefix "$formula" || true)" + if [ -n "$candidate" ] && [ -x "$candidate/bin/$tool" ]; then + printf '%s\n' "$candidate/bin/$tool" + return 0 + fi + return 1 +} + +native_postgis_bison_version_major() { + local tool="$1" + "$tool" --version 2>/dev/null | awk ' + NR == 1 { + for (field_index = 1; field_index <= NF; field_index += 1) { + if ($field_index ~ /^[0-9]+([.][0-9]+)*$/) { + split($field_index, version, ".") + print version[1] + exit + } + } + } + ' +} + +native_postgis_find_modern_bison() { + local candidate + local resolved + local major + local brew_prefix + local -a candidates=() + if [ -n "${OLIPHAUNT_BISON:-}" ]; then + candidates+=("$OLIPHAUNT_BISON") + fi + if [ -n "${BISON:-}" ]; then + candidates+=("$BISON") + fi + candidates+=( + /opt/homebrew/opt/bison/bin/bison + /usr/local/opt/bison/bin/bison + ) + brew_prefix="$(native_brew_prefix bison || true)" + if [ -n "$brew_prefix" ]; then + candidates+=("$brew_prefix/bin/bison") + fi + candidates+=(bison) + + for candidate in "${candidates[@]}"; do + [ -n "$candidate" ] || continue + resolved="" + if [ -x "$candidate" ]; then + resolved="$candidate" + elif command -v "$candidate" >/dev/null 2>&1; then + resolved="$(command -v "$candidate")" + fi + [ -n "$resolved" ] || continue + major="$(native_postgis_bison_version_major "$resolved")" + if [ -n "$major" ] && [ "$major" -ge 3 ]; then + printf '%s\n' "$resolved" + return 0 + fi + done + + return 1 +} + +native_dependency_tree_identity() { + local label="$1" + local root="$2" + local resolved_root + [ -d "$root" ] || { + echo "native dependency identity root is missing: $root" >&2 + return 1 + } + resolved_root="$(cd "$root" && pwd -P)" + printf '%s.root=%s\n' "$label" "$resolved_root" + local file relative + while IFS= read -r file; do + relative="${file#"$resolved_root"/}" + printf '%s.file=%s\n' "$label" "$relative" + printf '%s.sha256=%s\n' "$label" "$(shasum -a 256 "$file" | awk '{print $1}')" + done < <(find "$resolved_root" -type f -print | LC_ALL=C sort) +} + +native_dependency_library_identity() { + local label="$1" + local prefix="$2" + local pattern="$3" + local resolved_prefix + resolved_prefix="$(cd "$prefix" && pwd -P)" + local matched=0 + local file + while IFS= read -r file; do + matched=1 + printf '%s.file=%s\n' "$label" "${file#"$resolved_prefix"/}" + printf '%s.sha256=%s\n' "$label" "$(shasum -a 256 "$file" | awk '{print $1}')" + done < <(find "$resolved_prefix/lib" -maxdepth 1 -type f -name "$pattern" -print | LC_ALL=C sort) + if [ "$matched" -eq 0 ]; then + echo "native dependency library identity matched no files: $prefix/lib/$pattern" >&2 + return 1 + fi +} + +native_contrib_extensions_include() { + local wanted="$1" + local extension + for extension in "${contrib_extensions[@]}"; do + [ "$extension" != "$wanted" ] || return 0 + done + return 1 +} + +openssl_dependency_identity() { + local prefix + prefix="$(native_openssl_prefix)" || return 1 + printf 'openssl.prefix=%s\n' "$(cd "$prefix" && pwd -P)" + native_dependency_tree_identity openssl.headers "$prefix/include/openssl" || return 1 + native_dependency_library_identity openssl.crypto "$prefix" 'libcrypto.*' || return 1 +} + +apple_toolchain_identity() { + printf 'arch=%s\n' "$(uname -m)" + printf 'os.version=%s\n' "$(sw_vers -productVersion)" + printf 'os.build=%s\n' "$(sw_vers -buildVersion)" + printf 'xcode=%s\n' "$(xcodebuild -version | tr '\n' ';')" + printf 'sdk.path=%s\n' "$(xcrun --sdk macosx --show-sdk-path)" + printf 'sdk.version=%s\n' "$(xcrun --sdk macosx --show-sdk-version)" + printf 'sdk.build=%s\n' "$(xcrun --sdk macosx --show-sdk-build-version)" + native_command_identity cc "$native_cc" + native_command_identity cxx "$native_cxx" + native_command_identity clang "$(xcrun --find clang)" + native_command_identity linker "$(xcrun --find ld)" +} + +exact_checkout_identity() { + local label="$1" + local checkout="$2" + local status + [ -d "$checkout" ] || { + echo "extension source checkout is missing: $checkout" >&2 + return 1 + } + git -C "$checkout" rev-parse --verify 'HEAD^{commit}' >/dev/null 2>&1 || { + echo "extension source checkout is not an exact Git commit: $checkout" >&2 + return 1 + } + status="$(git -C "$checkout" status --porcelain=v1 --untracked-files=all)" + if [ -n "$status" ]; then + echo "extension source checkout is dirty: $checkout" >&2 + return 1 + fi + printf '%s.commit=%s\n' "$label" "$(git -C "$checkout" rev-parse --verify 'HEAD^{commit}')" +} + +postgis_host_dependency_identity() { + local bison_tool + bison_tool="$(native_postgis_find_modern_bison)" || { + echo "PostGIS requires GNU bison 3 or newer" >&2 + return 1 + } + native_command_identity postgis_bison "$bison_tool" + printf 'postgis.bison.version=%s\n' "$("$bison_tool" --version | sed -n '1p')" + + if [ "${OLIPHAUNT_POSTGIS_USE_PINNED_DEPS:-0}" = "1" ]; then + printf 'postgis.dependency_mode=pinned\n' + return 0 + fi + + local geos_config proj_prefix json_prefix + geos_config="$(native_dependency_tool \ + OLIPHAUNT_GEOS_CONFIG \ + geos-config \ + geos \ + /opt/homebrew/opt/geos/bin/geos-config \ + /usr/local/opt/geos/bin/geos-config || true)" + proj_prefix="$(native_dependency_prefix \ + OLIPHAUNT_PROJ_PREFIX \ + proj \ + /opt/homebrew/opt/proj \ + /usr/local/opt/proj || true)" + json_prefix="$(native_dependency_prefix \ + OLIPHAUNT_JSONC_PREFIX \ + json-c \ + /opt/homebrew/opt/json-c \ + /usr/local/opt/json-c || true)" + if [ -z "$geos_config" ] || [ -z "$proj_prefix" ] || [ -z "$json_prefix" ]; then + printf 'postgis.dependency_mode=pinned-fallback\n' + return 0 + fi + + local geos_prefix pkg_config xml2_config xml2_prefix + geos_prefix="$("$geos_config" --prefix)" || return 1 + pkg_config="$(command -v pkg-config || true)" + [ -n "$pkg_config" ] || { + echo "PostGIS host dependency identity could not resolve pkg-config" >&2 + return 1 + } + xml2_config="$(command -v xml2-config || true)" + [ -n "$xml2_config" ] || { + echo "PostGIS host dependency identity could not resolve xml2-config" >&2 + return 1 + } + xml2_prefix="$("$xml2_config" --prefix)" || return 1 + + printf 'postgis.dependency_mode=host\n' + native_command_identity postgis_geos_config "$geos_config" + native_command_identity postgis_pkg_config "$pkg_config" + native_command_identity postgis_xml2_config "$xml2_config" + printf 'postgis.pkg_config.version=%s\n' "$("$pkg_config" --version)" + native_dependency_tree_identity postgis.geos.headers "$geos_prefix/include" || return 1 + native_dependency_library_identity postgis.geos.library "$geos_prefix" 'libgeos*' || return 1 + native_dependency_tree_identity postgis.proj.headers "$proj_prefix/include" || return 1 + native_dependency_library_identity postgis.proj.library "$proj_prefix" 'libproj.*' || return 1 + [ -f "$proj_prefix/share/proj/proj.db" ] || { + echo "PostGIS PROJ identity is missing proj.db under $proj_prefix" >&2 + return 1 + } + printf 'postgis.proj.data=%s\n' "$(shasum -a 256 "$proj_prefix/share/proj/proj.db" | awk '{print $1}')" + native_dependency_tree_identity postgis.jsonc.headers "$json_prefix/include/json-c" || return 1 + native_dependency_library_identity postgis.jsonc.library "$json_prefix" 'libjson-c.*' || return 1 + native_dependency_tree_identity postgis.libxml2.headers "$xml2_prefix/include/libxml2" || return 1 + native_dependency_library_identity postgis.libxml2.library "$xml2_prefix" 'libxml2.*' || return 1 +} + +module_depends_on_liboliphaunt() { + local module="$1" + [ -f "$module" ] || return 1 + case "$(otool -L "$module" 2>/dev/null || true)" in + *"@rpath/liboliphaunt.dylib"*) return 0 ;; + *) return 1 ;; + esac +} + +module_has_postgres_symbols_bound_to_liboliphaunt() { + local module="$1" + nm -m "$module" 2>/dev/null | + awk 'index($0, "(from liboliphaunt)") { found = 1 } END { exit found ? 0 : 1 }' +} + +module_avoids_main_executable_bindings() { + local module="$1" + nm -m "$module" 2>/dev/null | awk -f "$macos_module_nm_audit" +} + +liboliphaunt_artifact_ready() { + [ -f "$lib_out" ] || return 1 + local symbols + symbols="$(nm -gU "$lib_out" 2>/dev/null || true)" + local symbol + for symbol in \ + _oliphaunt_init \ + _oliphaunt_init_ex \ + _oliphaunt_exec_protocol \ + _oliphaunt_exec_simple_query \ + _oliphaunt_exec_protocol_stream \ + _oliphaunt_backup \ + _oliphaunt_backup_ex \ + _oliphaunt_restore \ + _oliphaunt_cancel \ + _oliphaunt_detach \ + _oliphaunt_close \ + _oliphaunt_register_static_extensions \ + _oliphaunt_last_error \ + _oliphaunt_version \ + _oliphaunt_capabilities \ + _oliphaunt_free_response \ + _oliphaunt_embedded_kill \ + _oliphaunt_embedded_raise \ + _oliphaunt_pg_hash_create \ + _oliphaunt_pg_hash_destroy \ + _oliphaunt_pg_hash_search + do + grep -Eq "[[:space:]]${symbol}$" <<< "$symbols" || return 1 + done + for symbol in _hash_create _hash_destroy _hash_search; do + if grep -Eq "[[:space:]]${symbol}$" <<< "$symbols"; then + return 1 + fi + done + oliphaunt_icu_linked_symbols_ready "$symbols" || return 1 +} + +postgres_install_icu_ready() { + [ -x "$install_dir/bin/pg_config" ] || return 1 + local configure_args + configure_args="$("$install_dir/bin/pg_config" --configure 2>/dev/null || true)" + case "$configure_args" in + *"--with-icu"*) ;; + *) return 1 ;; + esac + case "$configure_args" in + *"U_STATIC_IMPLEMENTATION"*) ;; + *) return 1 ;; + esac + [ -f "$install_dir/include/pg_config.h" ] || return 1 + grep -Eq '^#define USE_ICU 1\b' "$install_dir/include/pg_config.h" && + grep -q 'U_STATIC_IMPLEMENTATION' "$install_dir/include/pg_config.h" +} + +hash_object_response_file_inputs() { + [ -f "$objects_rsp" ] || return 1 + while IFS= read -r object; do + [ -n "$object" ] || continue + if [ ! -f "$object" ]; then + echo "liboliphaunt link object is missing: $object" >&2 + return 1 + fi + stat -f '%m %z %N' "$object" + done < "$objects_rsp" +} + +liboliphaunt_build_fingerprint() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'cc=%s\n' "$CC" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'build_hash=%s\n' "$desired_build_hash" + printf 'install_name=%s\n' '@rpath/liboliphaunt.dylib' + printf 'liboliphaunt_sources=%s\n' "${liboliphaunt_sources[*]}" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/include/oliphaunt.h" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_internal.h" + local source + for source in "${liboliphaunt_sources[@]}"; do + shasum -a 256 "$source" + done + shasum -a 256 "$objects_rsp" + hash_object_response_file_inputs + stat -f '%m %z %N' src/common/libpgcommon_srv.a + stat -f '%m %z %N' src/port/libpgport_srv.a + } | shasum -a 256 | awk '{print $1}' +} + +liboliphaunt_artifacts_current() { + [ -d "$build_dir" ] || return 1 + [ -f "$build_stamp" ] || return 1 + [ "$(cat "$build_stamp")" = "$desired_build_hash" ] || return 1 + oliphaunt_icu_artifacts_ready "$icu_prefix" || return 1 + [ -f "$liboliphaunt_build_stamp" ] || return 1 + [ -x "$install_dir/bin/initdb" ] || return 1 + [ -x "$install_dir/bin/postgres" ] || return 1 + postgres_install_icu_ready || return 1 + [ -f "$objects_rsp" ] || return 1 + [ -f src/common/libpgcommon_srv.a ] || return 1 + [ -f src/port/libpgport_srv.a ] || return 1 + liboliphaunt_artifact_ready || return 1 + + local desired_liboliphaunt_hash + if ! desired_liboliphaunt_hash="$(liboliphaunt_build_fingerprint)"; then + return 1 + fi + [ "$(cat "$liboliphaunt_build_stamp")" = "$desired_liboliphaunt_hash" ] || return 1 +} + +hash_extension_source_tree() { + local source_dir="$1" + [ -d "$source_dir" ] || return 0 + find "$source_dir" -type f \( \ + -name "CMakeLists.txt" -o \ + -name "*.cmake" -o \ + -name "configure" -o \ + -name "configure.ac" -o \ + -name "configure.in" -o \ + -name "*.am" -o \ + -name "*.m4" -o \ + -name "*.c" -o \ + -name "*.cc" -o \ + -name "*.cpp" -o \ + -name "*.h" -o \ + -name "*.hpp" -o \ + -name "*.sql" -o \ + -name "*.control" -o \ + -name "*.in" -o \ + -name "*.awk" -o \ + -name "*.l" -o \ + -name "*.pl" -o \ + -name "*.pm" -o \ + -name "*.py" -o \ + -name "*.sh" -o \ + -name "*.y" -o \ + -name "*.csv" -o \ + -name "*.dat" -o \ + -name "*.json" -o \ + -name "*.txt" -o \ + -name "*.xml" -o \ + -name "Makefile" -o \ + -name "*.mk" -o \ + -name "meson.build" -o \ + -name "meson_options.txt" \ + \) -print | + LC_ALL=C sort | + while IFS= read -r file; do + shasum -a 256 "$file" + done +} + +native_postgis_dependency_fingerprint() { + { + printf 'schema=oliphaunt-macos-postgis-dependencies-v1\n' + printf 'arch=%s\n' "$(uname -m)" + printf 'deployment_target=%s\n' "$macos_deployment_target" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'apple_toolchain=%s\n' "$apple_toolchain_hash" + native_command_identity postgis_cc "$postgis_cc" + native_command_identity postgis_cxx "$native_cxx" + native_command_identity cmake cmake + native_command_identity make make + native_command_identity ar ar + native_command_identity ranlib ranlib + native_command_identity sqlite3 sqlite3 + shasum -a 256 "$script_path" + shasum -a 256 "$script_dir/postgis-dependency-cache.sh" + shasum -a 256 "$source_manifest" + + local dependency source_dir + for dependency in geos proj sqlite json-c libxml2; do + source_dir="$repo_root/target/oliphaunt-sources/checkouts/$dependency" + exact_checkout_identity "postgis_dependency.$dependency" "$source_dir" || return 1 + hash_extension_source_tree "$source_dir" + done + } | shasum -a 256 | awk '{print $1}' +} + +extension_build_fingerprint() { + local postgis_dependency_hash="" + if native_extensions_include_postgis; then + postgis_dependency_hash="$(native_postgis_dependency_fingerprint)" || return 1 + fi + + { + printf 'schema=oliphaunt-macos-native-extensions-v2\n' + printf 'pg_version=%s\n' "$pg_version" + printf 'cc=%s\n' "$CC" + printf 'postgis_cc=%s\n' "$postgis_cc" + printf 'postgis_use_pinned_deps=%s\n' "${OLIPHAUNT_POSTGIS_USE_PINNED_DEPS:-0}" + printf 'native_extension_sql_names=%s\n' "${OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES:-${OLIPHAUNT_EXTENSION_SQL_NAMES:-}}" + printf 'build_hash=%s\n' "$desired_build_hash" + printf 'normal_be_dllibs=%s\n' "$normal_module_be_dllibs" + printf 'embedded_be_dllibs=%s\n' "$embedded_module_be_dllibs" + stat -f '%m %z %N' "$install_dir/bin/postgres" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/include/oliphaunt.h" + shasum -a 256 "$repo_root/src/runtimes/liboliphaunt/native/src/liboliphaunt_internal.h" + local source + for source in "${liboliphaunt_sources[@]}"; do + shasum -a 256 "$source" + done + printf 'contrib_extensions=%s\n' "${contrib_extensions[*]}" + printf 'external_extensions=%s\n' "${external_extensions[*]}" + shasum -a 256 "$repo_root/src/extensions/generated/contrib-build.tsv" + shasum -a 256 "$repo_root/src/extensions/generated/pgxs-build.tsv" + native_command_identity extension_make make + native_command_identity extension_ar ar + native_command_identity extension_ranlib ranlib + native_command_identity extension_perl perl + native_command_identity extension_autoconf autoconf + native_command_identity extension_automake automake + if native_contrib_extensions_include pgcrypto; then + openssl_dependency_identity || return 1 + fi + + local extension source_rel extension_checkout + for extension in "${contrib_extensions[@]}"; do + printf 'contrib:%s\n' "$extension" + hash_extension_source_tree "$build_dir/contrib/$extension" + done + for extension in "${external_extensions[@]}"; do + printf 'external:%s\n' "$extension" + source_rel="$(oliphaunt_native_external_extension_source_rel "$repo_root" "$extension" || true)" + [ -n "$source_rel" ] || { + echo "unknown external extension source mapping: $extension" >&2 + return 1 + } + extension_checkout="$repo_root/$source_rel" + exact_checkout_identity "extension.$extension" "$extension_checkout" || return 1 + hash_extension_source_tree "$extension_checkout" + done + if native_extensions_include_postgis; then + local dependency + printf 'postgis-dependency-fingerprint=%s\n' "$postgis_dependency_hash" + for dependency in geos proj sqlite json-c libxml2; do + if [ -d "$repo_root/target/oliphaunt-sources/checkouts/$dependency" ]; then + printf 'postgis-dependency:%s\n' "$dependency" + local dependency_checkout="$repo_root/target/oliphaunt-sources/checkouts/$dependency" + exact_checkout_identity "postgis_dependency.$dependency" "$dependency_checkout" || return 1 + hash_extension_source_tree "$dependency_checkout" + fi + done + postgis_host_dependency_identity || return 1 + fi + hash_extension_source_tree "$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" + } | shasum -a 256 | awk '{print $1}' +} + +packaged_extension_modules_avoid_provider_collisions() { + local engine_symbols + local module + local module_path + engine_symbols="$(mktemp "${TMPDIR:-/tmp}/oliphaunt-engine-symbols.XXXXXX")" + if ! nm -gU "$lib_out" > "$engine_symbols"; then + rm -f "$engine_symbols" + return 1 + fi + for module in "${required_extension_modules[@]}"; do + module_path="$install_dir/lib/postgresql/$module.dylib" + [ -f "$module_path" ] || continue + if ! nm -m "$module_path" 2>/dev/null | + awk -v require_namespaced_dynahash=1 \ + -f "$macos_provider_collision_audit" "$engine_symbols" -; then + echo "packaged extension module binds liboliphaunt engine symbols to another Mach-O provider: $module_path" >&2 + rm -f "$engine_symbols" + return 1 + fi + done + rm -f "$engine_symbols" +} + +embedded_extension_modules_avoid_provider_collisions() { + local engine_symbols + local module + local module_path + engine_symbols="$(mktemp "${TMPDIR:-/tmp}/oliphaunt-engine-symbols.XXXXXX")" + if ! nm -gU "$lib_out" > "$engine_symbols"; then + rm -f "$engine_symbols" + return 1 + fi + for module in "${required_extension_modules[@]}" plpgsql; do + module_path="$embedded_modules_dir/$module.dylib" + [ -f "$module_path" ] || continue + if ! nm -m "$module_path" 2>/dev/null | + awk -v allowed_engine_provider=liboliphaunt \ + -v require_namespaced_dynahash=1 \ + -f "$macos_provider_collision_audit" "$engine_symbols" -; then + echo "embedded extension module has an invalid liboliphaunt symbol provider: $module_path" >&2 + rm -f "$engine_symbols" + return 1 + fi + done + rm -f "$engine_symbols" +} + +embedded_plpgsql_avoids_provider_collisions() { + local engine_symbols + local module_path="$embedded_modules_dir/plpgsql.dylib" + [ -f "$module_path" ] || return 1 + engine_symbols="$(mktemp "${TMPDIR:-/tmp}/oliphaunt-engine-symbols.XXXXXX")" + if ! nm -gU "$lib_out" > "$engine_symbols"; then + rm -f "$engine_symbols" + return 1 + fi + if ! nm -m "$module_path" 2>/dev/null | + awk -v allowed_engine_provider=liboliphaunt \ + -v require_namespaced_dynahash=1 \ + -f "$macos_provider_collision_audit" "$engine_symbols" -; then + rm -f "$engine_symbols" + return 1 + fi + rm -f "$engine_symbols" +} + +native_extension_artifacts_ready() { + local extension + for extension in "${required_extension_controls[@]}"; do + if [ ! -f "$install_dir/share/postgresql/extension/$extension.control" ]; then + return 1 + fi + if ! compgen -G "$install_dir/share/postgresql/extension/$extension--*.sql" >/dev/null; then + return 1 + fi + done + + local module + for module in "${required_extension_modules[@]}"; do + if [ ! -f "$install_dir/lib/postgresql/$module.dylib" ]; then + return 1 + fi + if module_depends_on_liboliphaunt "$install_dir/lib/postgresql/$module.dylib"; then + return 1 + fi + if ! module_avoids_main_executable_bindings "$install_dir/lib/postgresql/$module.dylib"; then + return 1 + fi + if [ ! -f "$embedded_modules_dir/$module.dylib" ]; then + return 1 + fi + done + [ -f "$embedded_modules_dir/plpgsql.dylib" ] || return 1 + if native_extensions_include_postgis; then + [ -f "$install_dir/share/postgresql/proj/proj.db" ] || return 1 + fi + packaged_extension_modules_avoid_provider_collisions || return 1 + embedded_extension_modules_avoid_provider_collisions || return 1 +} + +base_runtime_optional_extensions_absent() { + local extension + for extension in "${required_extension_controls[@]}"; do + if [ -f "$install_dir/share/postgresql/extension/$extension.control" ]; then + return 1 + fi + done + + local module + for module in "${required_extension_modules[@]}"; do + if [ -f "$install_dir/lib/postgresql/$module.dylib" ]; then + return 1 + fi + done + + [ ! -d "$install_dir/share/postgresql/contrib" ] || return 1 + [ ! -d "$install_dir/share/postgresql/proj" ] || return 1 +} + +prune_base_runtime_optional_extensions() { + if [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" != "0" ]; then + return 0 + fi + + local extension_dir="$install_dir/share/postgresql/extension" + local module_dir="$install_dir/lib/postgresql" + local extension + if [ -d "$extension_dir" ]; then + for extension in "${required_extension_controls[@]}"; do + rm -f "$extension_dir/$extension.control" + rm -f "$extension_dir/$extension--"*.sql + done + rm -f "$extension_dir/postgis"*.sql "$extension_dir/rtpostgis"*.sql + rm -f "$extension_dir/uninstall_postgis.sql" "$extension_dir/uninstall_legacy.sql" + rm -f "$extension_dir/pgtap-"*.sql "$extension_dir/uninstall_pgtap.sql" + fi + + local module + if [ -d "$module_dir" ]; then + for module in "${required_extension_modules[@]}"; do + rm -f "$module_dir/$module.dylib" + done + fi + + if [ -L "$embedded_modules_dir" ] || { [ -e "$embedded_modules_dir" ] && [ ! -d "$embedded_modules_dir" ]; }; then + rm -rf "$embedded_modules_dir" + fi + mkdir -p "$embedded_modules_dir" + if [ -L "$embedded_modules_dir/plpgsql.dylib" ] || + { [ -e "$embedded_modules_dir/plpgsql.dylib" ] && [ ! -f "$embedded_modules_dir/plpgsql.dylib" ]; }; then + rm -rf "$embedded_modules_dir/plpgsql.dylib" + fi + find "$embedded_modules_dir" -mindepth 1 -maxdepth 1 ! -name plpgsql.dylib -exec rm -rf {} + + + rm -rf "$install_dir/share/postgresql/contrib" "$install_dir/share/postgresql/proj" +} + +base_embedded_module_closure_ready() { + [ -d "$embedded_modules_dir" ] || return 1 + [ ! -L "$embedded_modules_dir" ] || return 1 + [ -f "$embedded_modules_dir/plpgsql.dylib" ] || return 1 + [ ! -L "$embedded_modules_dir/plpgsql.dylib" ] || return 1 + + local entry + while IFS= read -r -d '' entry; do + [ "$(basename "$entry")" = plpgsql.dylib ] || return 1 + done < <(find "$embedded_modules_dir" -mindepth 1 -maxdepth 1 -print0) +} + +native_extension_artifacts_current() { + [ -d "$build_dir" ] || return 1 + [ -f "$build_stamp" ] || return 1 + [ "$(cat "$build_stamp")" = "$desired_build_hash" ] || return 1 + [ -x "$install_dir/bin/postgres" ] || return 1 + [ -f "$lib_out" ] || return 1 + [ -d "$build_dir/contrib" ] || return 1 + [ -f "$extension_build_stamp" ] || return 1 + + local desired_extension_hash + if ! desired_extension_hash="$(extension_build_fingerprint)"; then + return 1 + fi + [ "$(cat "$extension_build_stamp")" = "$desired_extension_hash" ] || return 1 + native_extension_artifacts_ready || return 1 +} + +if [ "$(uname -s)" != "Darwin" ]; then + echo "native PostgreSQL 18 liboliphaunt build currently targets macOS only" >&2 + exit 2 +fi + +if ! verify_source_manifest; then + echo "native liboliphaunt source manifest does not match build constants: $source_manifest" >&2 + exit 1 +fi +oliphaunt_icu_require_source "$icu_source_dir" + +native_cc="${OLIPHAUNT_CC:-cc}" +native_cxx="${OLIPHAUNT_CXX:-c++}" +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + if [ "$ccache_mode" != "auto" ]; then + ccache_bin="$ccache_mode" + else + ccache_bin="$(command -v ccache || true)" + fi + if [ -n "$ccache_bin" ]; then + export CC="$ccache_bin $native_cc" + export CXX="$ccache_bin $native_cxx" + else + export CC="$native_cc" + export CXX="$native_cxx" + fi +else + export CC="$native_cc" + export CXX="$native_cxx" +fi + +native_cflags="$(oliphaunt_native_release_cflags -fPIC "-mmacosx-version-min=$macos_deployment_target" -DOLIPHAUNT_EMBEDDED)" +apple_toolchain_hash="$(apple_toolchain_identity | shasum -a 256 | awk '{print $1}')" +desired_patch_hash="$(patch_series_hash)" +desired_build_hash="$( + { + printf 'generation_schema=2\n' + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'patches=%s\n' "$desired_patch_hash" + printf 'cc=%s\n' "$CC" + printf 'cxx=%s\n' "$CXX" + printf 'macos_deployment_target=%s\n' "$macos_deployment_target" + printf 'native_cflags=%s\n' "$native_cflags" + printf 'apple_toolchain=%s\n' "$apple_toolchain_hash" + 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' + printf 'build_script=%s\n' "$(shasum -a 256 "$script_path" | awk '{print $1}')" + printf 'backend_objects_makefile=%s\n' "$(shasum -a 256 "$script_dir/postgres-backend-objects.mk" | awk '{print $1}')" + printf 'common_script=%s\n' "$(shasum -a 256 "$script_dir/common.sh" | awk '{print $1}')" + printf 'fetch_script=%s\n' "$(shasum -a 256 "$repo_root/src/postgres/versions/18/fetch-source.sh" | awk '{print $1}')" + printf 'source_manifest=%s\n' "$(shasum -a 256 "$source_manifest" | awk '{print $1}')" + printf 'module_audit=%s\n' "$(shasum -a 256 "$macos_module_nm_audit" | awk '{print $1}')" + printf 'provider_audit=%s\n' "$(shasum -a 256 "$macos_provider_collision_audit" | awk '{print $1}')" + } | shasum -a 256 | awk '{print $1}' +)" +current_generation_hash="" +if [ -L "$generation_stamp" ] || + { [ -e "$generation_stamp" ] && [ ! -f "$generation_stamp" ]; }; then + echo "discarding malformed macOS native generation stamp: $generation_stamp" >&2 + rm -rf "$generation_stamp" +fi +if [ -f "$generation_stamp" ]; then + current_generation_hash="$(cat "$generation_stamp")" +fi + +# Exact extension carriers are shared by standalone-server and embedded +# profiles. Darwin's -bundle_loader records PostgreSQL imports against the +# main-executable ordinal, which makes an embedded host search node/bun/deno +# instead of the process-global liboliphaunt image. Flat dynamic lookup works +# in both profiles and is backed by the runtime's RTLD_GLOBAL contract. +normal_module_be_dllibs="-undefined dynamic_lookup" +embedded_module_be_dllibs="-L$out_dir -loliphaunt -Wl,-rpath,$out_dir" +postgis_cc="${OLIPHAUNT_POSTGIS_CC:-$native_cc}" +portable_uuid_dir="$repo_root/src/runtimes/liboliphaunt/native/portable-uuid" +native_uuid_dependency_dir="$work_root/portable-uuid-native" +native_uuid_archive="$native_uuid_dependency_dir/lib/libuuid.a" + +if [ "$script_mode" = "--check-oliphaunt-current" ]; then + if [ -d "$build_dir" ] && (cd "$build_dir" && liboliphaunt_artifacts_current); then + echo "native liboliphaunt dylib is current" + exit 0 + fi + echo "native liboliphaunt dylib is missing or stale" >&2 + exit 1 +fi + +if [ "$script_mode" = "--check-extension-artifacts-current" ]; then + if native_extension_artifacts_current; then + echo "native extension artifacts are current" + exit 0 + fi + echo "native extension artifacts are missing or stale" >&2 + exit 1 +fi + +if [ "$script_mode" != "build" ] && [ "$script_mode" != "--runtime-only" ]; then + cat >&2 <<'MSG' +usage: src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh [--runtime-only|--print-required-extension-artifacts|--check-oliphaunt-current|--check-extension-artifacts-current] +MSG + exit 2 +fi + +jobs="${OLIPHAUNT_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" +mkdir -p "$source_cache" + +macos_generation_roots_coherent() { + local root + local present=0 + for root in "$build_dir" "$install_dir" "$out_dir"; do + if [ -L "$root" ] || { [ -e "$root" ] && [ ! -d "$root" ]; }; then + return 1 + fi + if [ -d "$root" ]; then + present=$((present + 1)) + fi + done + [ "$present" -eq 0 ] || [ "$present" -eq 3 ] +} + +if [ "$current_generation_hash" != "$desired_build_hash" ] || ! macos_generation_roots_coherent; then + echo "invalidating stale macOS native build generation" + rm -rf \ + "$build_dir" \ + "$install_dir" \ + "$out_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$work_root/icu" + generation_stamp_stage="$generation_stamp.tmp.$$" + rm -rf "$generation_stamp_stage" + printf '%s\n' "$desired_build_hash" > "$generation_stamp_stage" + mv -f "$generation_stamp_stage" "$generation_stamp" +fi +mkdir -p "$out_dir" + +icu_host="$(sh "$icu_source_dir/config.guess")" +oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "$jobs" \ + "macos" \ + "$icu_host" \ + "$CC" \ + "$CXX" \ + "ar" \ + "ranlib" \ + "$native_cflags" \ + "$native_cflags -std=c++17" \ + "" + +oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + +( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - +) + +postgres_source_configure_complete() { + [ -f "$build_dir/config.status" ] && + [ -f "$build_dir/src/include/pg_config.h" ] +} + +postgres_source_configure_reusable() { + if postgres_source_configure_complete; then + return 0 + fi + [ ! -f "$build_dir/config.status" ] && + [ ! -f "$build_dir/config.log" ] +} + +if [ -d "$build_dir" ] && ! postgres_source_configure_reusable; then + echo "discarding incomplete PostgreSQL configure tree at $build_dir" >&2 + rm -rf "$build_dir" +fi + +if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" +fi + +cd "$build_dir" + +patches_applied() { + grep -q 'OliphauntEmbeddedIO' src/include/libpq/libpq-be.h && + grep -q 'oliphaunt_io' src/backend/libpq/be-secure.c && + grep -q 'oliphaunt_embedded_main' src/backend/tcop/postgres.c && + grep -q 'oliphaunt_embedded' meson_options.txt && + grep -q 'OLIPHAUNT_EMBEDDED' meson.build && + grep -q 'OLIPHAUNT_EMBEDDED' src/include/tcop/tcopprot.h && + grep -q 'oliphaunt_embedded_proc_exit' src/include/storage/ipc.h && + grep -q 'original_cwd' src/backend/tcop/postgres.c && + grep -q 'oliphaunt_static_extension_lookup' src/backend/utils/fmgr/dfmgr.c && + grep -q 'getenv("ICU_DATA")' src/bin/initdb/initdb.c && + grep -q 'OLIPHAUNT_EMBEDDED_NO_SHELL_COMMANDS' src/backend/archive/shell_archive.c && + grep -q 'OLIPHAUNT_EMBEDDED_NO_SHELL_COMMANDS' src/backend/access/transam/xlogarchive.c && + grep -q 'oliphaunt_pg_hash_create' src/include/utils/hsearch.h && + grep -q 'oliphaunt_embedded_kill' src/port/pqsignal.c && + grep -q 'oliphaunt_embedded_raise' src/port/pqsignal.c +} + +if ! patches_applied; then + git init -q + for patch_file in "$patch_dir"/*.patch; do + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_file" + done + printf '%s\n' "$desired_build_hash" > "$build_stamp" +fi + +if ! patches_applied; then + echo "PostgreSQL embedded patch verification failed" >&2 + exit 1 +fi + +if [ ! -f "$build_stamp" ]; then + printf '%s\n' "$desired_build_hash" > "$build_stamp" +fi + +if [ ! -f config.status ]; then + echo "Using CC=$CC" + CPPFLAGS="$icu_cflags" \ + LDFLAGS="-L$icu_prefix/lib" \ + ICU_CFLAGS="$icu_cflags" \ + ICU_LIBS="$icu_libs" \ + ./configure \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls +fi + +if ! postgres_source_configure_complete; then + echo "PostgreSQL configure did not produce config.status and src/include/pg_config.h" >&2 + exit 1 +fi + +runtime_installed() { + [ -x "$install_dir/bin/initdb" ] && + [ -x "$install_dir/bin/postgres" ] && + [ -f "$install_dir/share/postgresql/postgresql.conf.sample" ] && + oliphaunt_icu_files_data_ready "$icu_data_dir" && + [ -f "$postgres_runtime_stamp" ] && + [ "$(cat "$postgres_runtime_stamp")" = "$desired_build_hash" ] && + postgres_install_icu_ready && + normal_runtime_avoids_embedded_signal_providers && + { [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" != "0" ] || base_runtime_optional_extensions_absent; } +} + +normal_runtime_avoids_embedded_signal_providers() { + [ -x "$install_dir/bin/postgres" ] || return 1 + local symbols + if ! symbols="$(nm -gU "$install_dir/bin/postgres" 2>/dev/null)"; then + return 1 + fi + local symbol + for symbol in _oliphaunt_embedded_kill _oliphaunt_embedded_raise; do + if grep -Eq "[[:space:]]${symbol}$" <<< "$symbols"; then + return 1 + fi + done +} + +install_normal_plpgsql_module() { + make -C src/pl/plpgsql/src clean + make -C src/pl/plpgsql/src \ + CC="$CC" \ + BE_DLLLIBS="$normal_module_be_dllibs" \ + install +} + +copy_embedded_modules_from_dir() { + local source_dir="$1" + mkdir -p "$embedded_modules_dir" + while IFS= read -r module; do + cp -p "$module" "$embedded_modules_dir/$(basename "$module")" + done < <(find "$source_dir" -maxdepth 1 -type f -name "*.dylib" -print) +} + +macos_embedded_module_link_args() { + local pg_ldflags="$1" + local be_dllibs="$2" + [ -z "$pg_ldflags" ] || printf '%s\n' "PG_LDFLAGS=$pg_ldflags" + [ -z "$be_dllibs" ] || printf '%s\n' "BE_DLLLIBS=$be_dllibs" +} + +compile_liboliphaunt_objects() { + local index + for index in "${!liboliphaunt_sources[@]}"; do + $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]}" \ + -o "${liboliphaunt_objects[$index]}" + done +} + +link_liboliphaunt_dylib() { + $CC -dynamiclib -undefined dynamic_lookup \ + -Wl,-install_name,@rpath/liboliphaunt.dylib \ + -o "$lib_out" \ + "${liboliphaunt_objects[@]}" \ + @"$objects_rsp" \ + src/common/libpgcommon_srv.a \ + src/port/libpgport_srv.a \ + $icu_libs \ + -lpthread +} + +build_liboliphaunt_dylib() { + local desired_liboliphaunt_hash + desired_liboliphaunt_hash="$(liboliphaunt_build_fingerprint)" + + if [ "${OLIPHAUNT_FORCE_RELINK:-0}" != "1" ] && + [ -f "$liboliphaunt_build_stamp" ] && + [ "$(cat "$liboliphaunt_build_stamp")" = "$desired_liboliphaunt_hash" ] && + liboliphaunt_artifact_ready; then + echo "reusing native liboliphaunt dylib" + return + fi + + rm -f "$liboliphaunt_build_stamp" + compile_liboliphaunt_objects + link_liboliphaunt_dylib + if ! liboliphaunt_artifact_ready; then + echo "native liboliphaunt dylib did not export the required C ABI symbols" >&2 + exit 1 + fi + printf '%s\n' "$desired_liboliphaunt_hash" > "$liboliphaunt_build_stamp" +} + +audit_embedded_extension_modules() { + if ! embedded_extension_modules_avoid_provider_collisions; then + exit 1 + fi +} + +audit_packaged_extension_modules() { + local module + local module_path + for module in "${required_extension_modules[@]}"; do + module_path="$install_dir/lib/postgresql/$module.dylib" + [ -f "$module_path" ] || continue + if module_depends_on_liboliphaunt "$module_path"; then + echo "packaged extension module unexpectedly links directly against liboliphaunt: $module_path" >&2 + exit 1 + fi + if ! module_avoids_main_executable_bindings "$module_path"; then + echo "packaged extension module binds unresolved symbols to the host main executable: $module_path" >&2 + echo "Darwin extension carriers must leave those imports dynamically resolvable by a standalone or embedded host" >&2 + exit 1 + fi + done + if ! packaged_extension_modules_avoid_provider_collisions; then + exit 1 + fi +} + +native_postgis_dependency_root="${OLIPHAUNT_NATIVE_POSTGIS_DEPENDENCY_ROOT:-$work_root/postgis-native-dependencies}" +native_postgis_dependency_build_roots=( + "$work_root/json-c-native-build" + "$work_root/sqlite-native-build" + "$work_root/geos-native-build" + "$work_root/libxml2-native-build" + "$work_root/proj-native-build" +) +native_postgis_dependency_required_outputs=( + "$native_postgis_dependency_root/json-c/lib/libjson-c.a" + "$native_postgis_dependency_root/sqlite/lib/libsqlite3.a" + "$native_postgis_dependency_root/geos/lib/libgeos_c.a" + "$native_postgis_dependency_root/geos/lib/libgeos.a" + "$native_postgis_dependency_root/libxml2/lib/libxml2.a" + "$native_postgis_dependency_root/libxml2/bin/xml2-config" + "$native_postgis_dependency_root/proj/lib/libproj.a" + "$native_postgis_dependency_root/proj/share/proj/proj.db" +) +postgis_configure_env=() +postgis_make_args=() +postgis_bison_tool="" + +invalidate_native_extension_dependency_cache() { + rm -rf "$native_uuid_dependency_dir" "$work_root/postgis-native-dependency-scripts" +} + +native_postgis_fail() { + echo "PostGIS native dependency build: $*" >&2 + exit 1 +} + +native_postgis_require_tools() { + local cmd + for cmd in cmake rsync sqlite3; do + command -v "$cmd" >/dev/null 2>&1 || native_postgis_fail "missing required command: $cmd" + done +} + +configure_postgis_parser_tools() { + postgis_bison_tool="$(native_postgis_find_modern_bison || true)" + if [ -z "$postgis_bison_tool" ]; then + native_postgis_fail "PostGIS requires GNU bison 3 or newer; install Homebrew bison or set OLIPHAUNT_BISON" + fi + postgis_configure_env+=( + "BISON=$postgis_bison_tool" + "YACC=$postgis_bison_tool -y" + ) + postgis_make_args+=( + "BISON=$postgis_bison_tool" + "YACC=$postgis_bison_tool -y" + ) +} + +native_postgis_dependency_archive() { + local name="$1" + local archive="$2" + [ -s "$archive" ] && [ ! -L "$archive" ] || + native_postgis_fail "missing, empty, or linked dependency archive for $name: $archive" +} + +native_postgis_cmake_install() { + local source_dir="$1" + local build_root="$2" + local dependency_dir="$3" + shift 3 + cmake -S "$source_dir" -B "$build_root" \ + -DCMAKE_INSTALL_PREFIX="$dependency_dir" \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DCMAKE_C_COMPILER="$native_cc" \ + -DCMAKE_CXX_COMPILER="$native_cxx" \ + "$@" >> "$postgis_dependency_log" 2>&1 + cmake --build "$build_root" --target install -- -j"$jobs" >> "$postgis_dependency_log" 2>&1 +} + +build_native_postgis_jsonc_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/json-c" + local dependency_dir="$native_postgis_dependency_root/json-c" + local build_root="$work_root/json-c-native-build" + local archive="$dependency_dir/lib/libjson-c.a" + if [ -f "$archive" ] && [ -d "$dependency_dir/include/json-c" ]; then + native_postgis_dependency_archive json-c "$archive" + return 0 + fi + [ -f "$source_dir/CMakeLists.txt" ] || native_postgis_fail "missing JSON-C checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_STATIC_LIBS=ON \ + -DBUILD_APPS=OFF \ + -DBUILD_TESTING=OFF \ + -DDISABLE_WERROR=ON + [ -f "$archive" ] || native_postgis_fail "JSON-C build did not produce $archive" + native_postgis_dependency_archive json-c "$archive" +} + +build_native_postgis_sqlite_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/sqlite" + local dependency_dir="$native_postgis_dependency_root/sqlite" + local build_root="$work_root/sqlite-native-build" + local archive="$dependency_dir/lib/libsqlite3.a" + if [ -f "$archive" ] && [ -f "$dependency_dir/include/sqlite3.h" ]; then + native_postgis_dependency_archive sqlite "$archive" + return 0 + fi + [ -x "$source_dir/configure" ] || native_postgis_fail "missing SQLite checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + mkdir -p "$build_root" "$dependency_dir/include" "$dependency_dir/lib" + rsync -a --delete --exclude .git "$source_dir/" "$build_root/" + ( + cd "$build_root" + 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" $(oliphaunt_native_release_cflags -fPIC) \ + -DSQLITE_THREADSAFE=0 \ + -DSQLITE_OMIT_LOAD_EXTENSION \ + -c sqlite3.c \ + -o sqlite3.o >> "$postgis_dependency_log" 2>&1 + ar crs "$archive" sqlite3.o >> "$postgis_dependency_log" 2>&1 + ranlib "$archive" >> "$postgis_dependency_log" 2>&1 + cp -p sqlite3.h sqlite3ext.h "$dependency_dir/include/" + ) + [ -f "$archive" ] || native_postgis_fail "SQLite build did not produce $archive" + native_postgis_dependency_archive sqlite "$archive" +} + +build_native_postgis_geos_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/geos" + local dependency_dir="$native_postgis_dependency_root/geos" + local build_root="$work_root/geos-native-build" + local geos_c_archive="$dependency_dir/lib/libgeos_c.a" + local geos_archive="$dependency_dir/lib/libgeos.a" + if [ -f "$geos_c_archive" ] && [ -f "$geos_archive" ] && [ -f "$dependency_dir/include/geos_c.h" ]; then + native_postgis_dependency_archive geos-c "$geos_c_archive" + native_postgis_dependency_archive geos "$geos_archive" + return 0 + fi + [ -f "$source_dir/CMakeLists.txt" ] || native_postgis_fail "missing GEOS checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_BENCHMARKS=OFF \ + -DBUILD_GEOSOP=OFF \ + -DGEOS_BUILD_DEVELOPER=OFF + [ -f "$geos_c_archive" ] || native_postgis_fail "GEOS build did not produce $geos_c_archive" + [ -f "$geos_archive" ] || native_postgis_fail "GEOS build did not produce $geos_archive" + native_postgis_dependency_archive geos-c "$geos_c_archive" + native_postgis_dependency_archive geos "$geos_archive" +} + +build_native_postgis_libxml2_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/libxml2" + local dependency_dir="$native_postgis_dependency_root/libxml2" + local build_root="$work_root/libxml2-native-build" + local archive="$dependency_dir/lib/libxml2.a" + if [ -f "$archive" ] && [ -x "$dependency_dir/bin/xml2-config" ]; then + native_postgis_dependency_archive libxml2 "$archive" + return 0 + fi + [ -f "$source_dir/CMakeLists.txt" ] || native_postgis_fail "missing libxml2 checkout: $source_dir" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + -DLIBXML2_WITH_PROGRAMS=OFF \ + -DLIBXML2_WITH_TESTS=OFF \ + -DLIBXML2_WITH_PYTHON=OFF \ + -DLIBXML2_WITH_THREADS=OFF \ + -DLIBXML2_WITH_MODULES=OFF \ + -DLIBXML2_WITH_ICONV=OFF \ + -DLIBXML2_WITH_ZLIB=OFF \ + -DLIBXML2_WITH_LZMA=OFF \ + -DLIBXML2_WITH_HTTP=OFF + [ -f "$archive" ] || native_postgis_fail "libxml2 build did not produce $archive" + native_postgis_dependency_archive libxml2 "$archive" +} + +build_native_postgis_proj_dependency() { + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/proj" + local dependency_dir="$native_postgis_dependency_root/proj" + local sqlite_dir="$native_postgis_dependency_root/sqlite" + local build_root="$work_root/proj-native-build" + local archive="$dependency_dir/lib/libproj.a" + if [ -f "$archive" ] && [ -f "$dependency_dir/share/proj/proj.db" ]; then + native_postgis_dependency_archive proj "$archive" + return 0 + fi + [ -f "$source_dir/CMakeLists.txt" ] || native_postgis_fail "missing PROJ checkout: $source_dir" + [ -f "$sqlite_dir/lib/libsqlite3.a" ] || native_postgis_fail "PROJ dependency requires SQLite archive first" + rm -rf "$build_root" "$dependency_dir" + native_postgis_cmake_install "$source_dir" "$build_root" "$dependency_dir" \ + -DBUILD_SHARED_LIBS=OFF \ + "-DSQLite3_INCLUDE_DIR=$sqlite_dir/include" \ + "-DSQLite3_LIBRARY=$sqlite_dir/lib/libsqlite3.a" \ + "-DEXE_SQLITE3=$(command -v sqlite3)" \ + -DENABLE_TIFF=OFF \ + -DENABLE_CURL=OFF \ + -DENABLE_EMSCRIPTEN_FETCH=OFF \ + -DHAVE_LIBDL=OFF \ + -DBUILD_APPS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_EXAMPLES=OFF \ + -DEMBED_RESOURCE_FILES=ON \ + -DUSE_ONLY_EMBEDDED_RESOURCE_FILES=ON + mkdir -p "$dependency_dir/share/proj" + if [ -f "$build_root/data/proj.db" ]; then + cp -p "$build_root/data/proj.db" "$dependency_dir/share/proj/proj.db" + fi + [ -f "$archive" ] || native_postgis_fail "PROJ build did not produce $archive" + [ -f "$dependency_dir/share/proj/proj.db" ] || native_postgis_fail "PROJ build did not produce proj.db" + native_postgis_dependency_archive proj "$archive" +} + +build_native_postgis_dependencies() { + native_postgis_require_tools + local wanted + wanted="$(native_postgis_dependency_fingerprint)" + oliphaunt_postgis_dependency_cache_prepare \ + "$native_postgis_dependency_root" \ + "$wanted" \ + "${native_postgis_dependency_build_roots[@]}" + mkdir -p "$(dirname "$postgis_dependency_log")" + : > "$postgis_dependency_log" + build_native_postgis_jsonc_dependency + build_native_postgis_sqlite_dependency + build_native_postgis_geos_dependency + build_native_postgis_libxml2_dependency + build_native_postgis_proj_dependency + oliphaunt_postgis_dependency_cache_commit \ + "$native_postgis_dependency_root" \ + "$wanted" \ + "${native_postgis_dependency_required_outputs[@]}" +} + +native_postgis_geos_config_script() { + local path="$1" + cat > "$path" < "$path" <&2 + use_native_postgis_dependency_bundle + configure_postgis_parser_tools + return + fi + + postgis_proj_prefix="$proj_prefix" + postgis_configure_args=( + "--with-geosconfig=$geos_config" + "--with-projdir=$proj_prefix" + "--with-jsondir=$json_prefix" + ) + configure_postgis_parser_tools +} + +build_native_uuid_dependency() { + local object="$native_uuid_dependency_dir/portable_uuid.o" + + if [ -f "$native_uuid_archive" ] && [ -d "$native_uuid_dependency_dir/include/uuid" ]; then + return 0 + fi + [ -f "$portable_uuid_dir/portable_uuid.c" ] || { + echo "portable UUID source is missing: $portable_uuid_dir" >&2 + exit 1 + } + + rm -rf "$native_uuid_dependency_dir" + mkdir -p "$native_uuid_dependency_dir/include" "$native_uuid_dependency_dir/lib" + cp -R "$portable_uuid_dir/include/uuid" "$native_uuid_dependency_dir/include/" + $CC $native_cflags \ + -I"$portable_uuid_dir/include" \ + -I"$build_dir/src/include" \ + -I"$build_dir/src/include/port" \ + -c "$portable_uuid_dir/portable_uuid.c" \ + -o "$object" + ar crs "$native_uuid_archive" "$object" + ranlib "$native_uuid_archive" + [ -s "$native_uuid_archive" ] || { + echo "portable UUID native build did not produce $native_uuid_archive" >&2 + exit 1 + } +} + +build_contrib_extension() { + local extension="$1" + local -a extra_make_args + local -a embedded_extra_make_args + local extra_make_args_count=0 + local embedded_extra_make_args_count=0 + local embedded_pg_ldflags="$embedded_module_be_dllibs" + local arg + if [ "$extension" = "pgcrypto" ]; then + configure_pgcrypto_make_args + extra_make_args=("${pgcrypto_make_args[@]}") + extra_make_args_count="${#pgcrypto_make_args[@]}" + elif [ "$extension" = "uuid-ossp" ]; then + build_native_uuid_dependency + extra_make_args=( + "PG_CPPFLAGS=-I$portable_uuid_dir/include -DHAVE_UUID_E2FS=1 -DHAVE_UUID_UUID_H=1" + "UUID_LIBS=$native_uuid_archive" + ) + extra_make_args_count=2 + fi + if [ "$extra_make_args_count" -gt 0 ]; then + for arg in "${extra_make_args[@]}"; do + case "$arg" in + PG_LDFLAGS=*) + embedded_pg_ldflags="${arg#PG_LDFLAGS=} $embedded_pg_ldflags" + ;; + *) + embedded_extra_make_args+=("$arg") + embedded_extra_make_args_count=$((embedded_extra_make_args_count + 1)) + ;; + esac + done + fi + while IFS= read -r arg; do + [ -n "$arg" ] || continue + embedded_extra_make_args+=("$arg") + embedded_extra_make_args_count=$((embedded_extra_make_args_count + 1)) + done < <(macos_embedded_module_link_args "$embedded_pg_ldflags" "$embedded_module_be_dllibs") + + make -C "contrib/$extension" clean + if [ "$extra_make_args_count" -gt 0 ]; then + make -C "contrib/$extension" \ + CC="$CC" \ + BE_DLLLIBS="$normal_module_be_dllibs" \ + "${extra_make_args[@]}" \ + install + else + make -C "contrib/$extension" \ + CC="$CC" \ + BE_DLLLIBS="$normal_module_be_dllibs" \ + install + fi + make -C "contrib/$extension" clean + if [ "$embedded_extra_make_args_count" -gt 0 ]; then + make -C "contrib/$extension" \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + "${embedded_extra_make_args[@]}" \ + all + else + make -C "contrib/$extension" \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + all + fi + copy_embedded_modules_from_dir "contrib/$extension" +} + +pgxs_extension_link_args() { + local extension="$1" + local link_kind="$2" + local link_flags="$3" + local be_dllibs="$link_flags" + case "$extension" in + pg_textsearch|pgvector|vector) + if [ -n "$be_dllibs" ]; then + be_dllibs="$be_dllibs -lm" + else + printf '%s\n' "SHLIB_LINK=-lm" + fi + ;; + *) + ;; + esac + if [ -n "$link_flags" ]; then + case "$link_kind" in + normal) + printf '%s\n' "BE_DLLLIBS=$be_dllibs" + ;; + embedded) + macos_embedded_module_link_args "$link_flags" "$be_dllibs" + ;; + *) + echo "unknown PGXS extension link kind: $link_kind" >&2 + exit 1 + ;; + esac + fi +} + +pgxs_extension_source_rel() { + local extension="$1" + [ "$extension" != postgis ] || return 1 + oliphaunt_native_external_extension_source_rel "$repo_root" "$extension" +} + +build_pgxs_extension() { + local extension="$1" + local source_rel + source_rel="$(pgxs_extension_source_rel "$extension" || true)" + [ -n "$source_rel" ] || native_extension_filter_fail "unknown PGXS extension source mapping: $extension" + local checkout="$repo_root/$source_rel" + local build_checkout="$work_root/external-$extension" + local -a normal_link_args=() + local -a embedded_link_args=() + while IFS= read -r arg; do + [ -n "$arg" ] || continue + normal_link_args+=("$arg") + done < <(pgxs_extension_link_args "$extension" "normal" "$normal_module_be_dllibs") + while IFS= read -r arg; do + [ -n "$arg" ] || continue + embedded_link_args+=("$arg") + done < <(pgxs_extension_link_args "$extension" "embedded" "$embedded_module_be_dllibs") + if [ ! -d "$checkout" ]; then + echo "native extension checkout is missing: $checkout" >&2 + exit 1 + fi + rm -rf "$build_checkout" + mkdir -p "$(dirname "$build_checkout")" + cp -a "$checkout/." "$build_checkout/" + rm -rf "$build_checkout/.git" + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + clean + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + CC="$CC" \ + OPTFLAGS="" \ + "${normal_link_args[@]}" \ + install + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + clean + make -C "$build_checkout" \ + PG_CONFIG="$install_dir/bin/pg_config" \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + OPTFLAGS="" \ + "${embedded_link_args[@]}" \ + all + copy_embedded_modules_from_dir "$build_checkout" +} + +normalize_installed_module_suffix() { + local stem="$1" + local module_dir="$install_dir/lib/postgresql" + if [ ! -f "$module_dir/$stem.dylib" ] && [ -f "$module_dir/$stem.so" ]; then + cp -p "$module_dir/$stem.so" "$module_dir/$stem.dylib" + fi +} + +copy_embedded_postgis_module() { + local source_dir="$1" + local candidate + mkdir -p "$embedded_modules_dir" + for candidate in "$source_dir/postgis-3.dylib" "$source_dir/postgis-3.so"; do + if [ -f "$candidate" ]; then + cp -p "$candidate" "$embedded_modules_dir/postgis-3.dylib" + if ! module_depends_on_liboliphaunt "$embedded_modules_dir/postgis-3.dylib"; then + echo "embedded PostGIS is not linked against liboliphaunt: $embedded_modules_dir/postgis-3.dylib" >&2 + exit 1 + fi + return + fi + done + echo "PostGIS embedded module was not produced under $source_dir" >&2 + exit 1 +} + +stage_postgis_data_files() { + local postgis_build_dir="$1" + local proj_db="" + local candidate + for candidate in \ + "$postgis_build_dir/share/proj/proj.db" \ + "${postgis_proj_prefix:-}/share/proj/proj.db" \ + "${OLIPHAUNT_PROJ_PREFIX:-}/share/proj/proj.db" \ + "${OLIPHAUNT_PROJ_DATADIR:-}/proj.db" \ + /opt/homebrew/opt/proj/share/proj/proj.db \ + /usr/local/opt/proj/share/proj/proj.db \ + /opt/homebrew/share/proj/proj.db \ + /usr/local/share/proj/proj.db + do + [ -n "$candidate" ] || continue + if [ -f "$candidate" ]; then + proj_db="$candidate" + break + fi + done + if [ -z "$proj_db" ]; then + echo "PostGIS requires proj/proj.db; set OLIPHAUNT_PROJ_DATADIR or install PROJ data files" >&2 + exit 1 + fi + mkdir -p "$install_dir/share/postgresql/proj" + cp -p "$proj_db" "$install_dir/share/postgresql/proj/proj.db" +} + +patch_postgis_generated_makefiles() { + local postgis_build_dir="$1" + local makefile + while IFS= read -r makefile; do + perl -0pi -e 's/\$\(LIBTOOL\) --mode=compile \$\(CC\)/\$(LIBTOOL) --tag=CC --mode=compile \$(CC)/g' "$makefile" + done < <(find "$postgis_build_dir" -name Makefile -type f -print) +} + +build_postgis_extension() { + local checkout="$repo_root/target/oliphaunt-sources/checkouts/postgis" + local postgis_build_dir="$work_root/postgis-native" + if [ ! -f "$checkout/configure.ac" ]; then + echo "native PostGIS checkout is missing or incomplete: $checkout" >&2 + echo "run the source-spine fetch before building native PostGIS artifacts" >&2 + exit 1 + fi + configure_postgis_configure_args + + rm -rf "$postgis_build_dir" + mkdir -p "$(dirname "$postgis_build_dir")" + cp -a "$checkout/." "$postgis_build_dir/" + rm -rf "$postgis_build_dir/.git" + + ( + cd "$postgis_build_dir" + local -a embedded_postgis_make_args=() + local embedded_postgis_has_ldflags=0 + local embedded_postgis_ldflags="$embedded_module_be_dllibs" + local arg + for arg in "${postgis_make_args[@]}"; do + case "$arg" in + LDFLAGS=*) + embedded_postgis_make_args+=("LDFLAGS=$embedded_postgis_ldflags ${arg#LDFLAGS=}") + embedded_postgis_has_ldflags=1 + ;; + *) + embedded_postgis_make_args+=("$arg") + ;; + esac + done + if [ "$embedded_postgis_has_ldflags" -eq 0 ]; then + embedded_postgis_make_args+=("LDFLAGS=$embedded_postgis_ldflags") + fi + + export CC="$postgis_cc" + if [ ! -f configure ]; then + ./autogen.sh + fi + if [ "${#postgis_configure_env[@]}" -gt 0 ]; then + export "${postgis_configure_env[@]}" + fi + ./configure \ + --prefix="$install_dir" \ + --with-pgconfig="$install_dir/bin/pg_config" \ + "${postgis_configure_args[@]}" \ + --without-protobuf \ + --without-raster \ + --without-topology \ + --without-sfcgal \ + --without-address-standardizer \ + --without-tiger \ + --disable-nls + + patch_postgis_generated_makefiles "$postgis_build_dir" + # PostGIS' generated revision header is referenced through postgis_config.h. + # Build it before parallel sub-makes so compiler jobs cannot race the + # top-level header generation target. + make postgis_revision.h + make clean || true + make postgis_revision.h + make -C doc CC="$postgis_cc" "${postgis_make_args[@]}" comments-install + make -j"$jobs" -C postgis CC="$postgis_cc" BE_DLLLIBS="$normal_module_be_dllibs" "${postgis_make_args[@]}" install + # PostGIS extension SQL generation has shared raster helper outputs even + # when raster support is disabled, so keep this packaging phase serial. + make -j1 -C extensions CC="$postgis_cc" "${postgis_make_args[@]}" all + make -j1 -C extensions CC="$postgis_cc" "${postgis_make_args[@]}" install + make -C postgis clean || true + make postgis_revision.h + make -j"$jobs" -C postgis CC="$postgis_cc" CFLAGS="$native_cflags" BE_DLLLIBS="$embedded_module_be_dllibs" "${embedded_postgis_make_args[@]}" all + ) + + normalize_installed_module_suffix postgis-3 + copy_embedded_postgis_module "$postgis_build_dir/postgis" + stage_postgis_data_files "$postgis_build_dir" +} + +build_embedded_plpgsql_module() { + local module="$embedded_modules_dir/plpgsql.dylib" + local desired_module_hash + local stamp_stage + desired_module_hash="$( + { + printf 'pg_version=%s\n' "$pg_version" + printf 'build_hash=%s\n' "$desired_build_hash" + printf 'cc=%s\n' "$CC" + printf 'embedded_be_dllibs=%s\n' "$embedded_module_be_dllibs" + shasum -a 256 "$script_path" | awk '{print "script=" $1}' + } | shasum -a 256 | awk '{print $1}' + )" + if [ -f "$embedded_plpgsql_build_stamp" ] && + [ "$(cat "$embedded_plpgsql_build_stamp")" = "$desired_module_hash" ] && + module_depends_on_liboliphaunt "$module" && + module_has_postgres_symbols_bound_to_liboliphaunt "$module" && + embedded_plpgsql_avoids_provider_collisions; then + return + fi + rm -f "$module" "$embedded_plpgsql_build_stamp" + make -C src/pl/plpgsql/src clean + make -C src/pl/plpgsql/src \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + BE_DLLLIBS="$embedded_module_be_dllibs" \ + all + mkdir -p "$embedded_modules_dir" + cp -p src/pl/plpgsql/src/plpgsql.dylib "$module" + if ! module_depends_on_liboliphaunt "$module"; then + echo "embedded plpgsql is not linked against liboliphaunt: $module" >&2 + exit 1 + fi + if ! module_has_postgres_symbols_bound_to_liboliphaunt "$module"; then + echo "embedded plpgsql does not bind PostgreSQL symbols to liboliphaunt: $module" >&2 + exit 1 + fi + if ! embedded_plpgsql_avoids_provider_collisions; then + echo "embedded plpgsql has an invalid liboliphaunt symbol provider: $module" >&2 + exit 1 + fi + stamp_stage="$embedded_plpgsql_build_stamp.tmp.$$" + printf '%s\n' "$desired_module_hash" > "$stamp_stage" + mv -f "$stamp_stage" "$embedded_plpgsql_build_stamp" +} + +build_native_extension_artifacts() { + if [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" = "0" ]; then + return + fi + + local desired_extension_hash + desired_extension_hash="$(extension_build_fingerprint)" + + if [ "${OLIPHAUNT_FORCE_EXTENSION_REBUILD:-0}" != "1" ] && + [ -f "$extension_build_stamp" ] && + [ "$(cat "$extension_build_stamp")" = "$desired_extension_hash" ] && + native_extension_artifacts_ready; then + audit_embedded_extension_modules + echo "reusing native extension artifacts" + return + fi + + rm -f "$extension_build_stamp" + rm -f "$embedded_modules_dir/age.dylib" "$embedded_modules_dir/pg_hashids.dylib" + invalidate_native_extension_dependency_cache + + local extension + for extension in "${contrib_extensions[@]}"; do + build_contrib_extension "$extension" + done + + for extension in "${external_extensions[@]}"; do + if [ "$extension" = "postgis" ]; then + build_postgis_extension + else + build_pgxs_extension "$extension" + fi + done + + audit_packaged_extension_modules + audit_embedded_extension_modules + if ! native_extension_artifacts_ready; then + echo "native extension build did not produce the required normal and embedded artifacts" >&2 + exit 1 + fi + + # Some extension builds generate source/header files inside their checkout or + # contrib directory. Stamp the post-build fingerprint so --check-current + # verifies the actual artifact-producing tree instead of the pre-build input + # shape. + desired_extension_hash="$(extension_build_fingerprint)" + printf '%s\n' "$desired_extension_hash" > "$extension_build_stamp" +} + +# Build and install a normal PostgreSQL tree first. initdb needs the matching +# sibling postgres binary and the installed share/lib tree needs core modules +# such as dict_snowball and plpgsql. Keep this separate from the embedded/PIC +# object pass so the runtime tools stay normal PostgreSQL while backend modules +# use embedded-friendly Darwin symbol lookup. +if ! runtime_installed; then + rm -f "$postgres_runtime_stamp" + # The embedded dylib pass intentionally rebuilds backend objects with + # OLIPHAUNT_EMBEDDED. If the normal runtime install later becomes stale, do + # not let make reuse those objects for the postgres/initdb toolchain. + make clean CC="$CC" + make -j"$jobs" CC="$CC" + make install CC="$CC" + if ! normal_runtime_avoids_embedded_signal_providers; then + echo "normal PostgreSQL runtime unexpectedly exports the embedded signal boundary" >&2 + exit 1 + fi +fi + +if module_depends_on_liboliphaunt "$install_dir/lib/postgresql/plpgsql.dylib"; then + install_normal_plpgsql_module +fi +oliphaunt_icu_stage_data "$icu_prefix" "$icu_data_dir" +rm -rf "$install_dir/share/icu" +prune_base_runtime_optional_extensions +printf '%s\n' "$desired_build_hash" > "$postgres_runtime_stamp" + +if [ "$script_mode" = "--runtime-only" ]; then + echo "$install_dir" + exit 0 +fi + +regenerate_backend_headers() { + rm -f src/include/nodes/header-stamp src/include/utils/header-stamp + make -C src/backend generated-headers CC="$CC" +} + +native_backend_support_libraries_ready() { + [ -f src/common/libpgcommon_srv.a ] || return 1 + [ -f src/port/libpgport_srv.a ] || return 1 + + local symbols + symbols="$(nm -gU src/port/libpgport_srv.a 2>/dev/null || true)" + local symbol + for symbol in _oliphaunt_embedded_kill _oliphaunt_embedded_raise; do + grep -Eq "[[:space:]]${symbol}$" <<< "$symbols" || return 1 + done +} + +build_native_backend_support_libraries() { + # The normal runtime pass leaves non-embedded server archives in this build + # tree. Rebuild both backend support archives before linking embedded backend + # objects so port.h call sites and their pqsignal.c provider use one profile. + make -C src/common clean + make -C src/port clean + make -C src/common \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + libpgcommon_srv.a + make -C src/port \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + libpgport_srv.a + + if ! native_backend_support_libraries_ready; then + echo "native backend support libraries do not provide the embedded signal boundary" >&2 + exit 1 + fi +} + +native_backend_objects_ready() { + for required in \ + src/backend/tcop/postgres.o \ + src/backend/libpq/be-secure.o \ + src/backend/libpq/pqcomm.o + do + if [ ! -f "$required" ]; then + return 1 + fi + done + for objfile in src/backend/*/objfiles.txt; do + if [ ! -s "$objfile" ]; then + return 1 + fi + done + nm -g src/backend/tcop/postgres.o | grep -q '_oliphaunt_embedded_main' || return 1 + native_backend_support_libraries_ready || return 1 +} + +validate_native_objects() { + if ! native_backend_objects_ready; then + echo "native backend object build did not produce the required embedded objects" >&2 + exit 1 + fi +} + +liboliphaunt_objects=() +for source in "${liboliphaunt_sources[@]}"; do + object="$out_dir/$(basename "${source%.c}").o" + liboliphaunt_objects+=("$object") +done + +# Rebuild backend objects for the dylib only when the patched backend object +# tree is missing. C ABI iteration should normally recompile and relink only the +# liboliphaunt translation units above. +if native_backend_objects_ready; then + echo "reusing native PostgreSQL backend objects" +else + make -C src/backend clean + regenerate_backend_headers + build_native_backend_support_libraries + make -j"$jobs" -C src/backend \ + -f "$script_dir/postgres-backend-objects.mk" \ + CC="$CC" \ + CFLAGS="$native_cflags" \ + oliphaunt-backend-objects + validate_native_objects +fi + +make -C src/timezone CC="$CC" CFLAGS="$native_cflags" localtime.o pgtz.o strftime.o + +{ + cat src/backend/*/objfiles.txt + printf 'src/timezone/localtime.o src/timezone/pgtz.o src/timezone/strftime.o\n' +} | tr '[:space:]' '\n' | sed '/^$/d' > "$objects_rsp" + +build_liboliphaunt_dylib +build_embedded_plpgsql_module +if ! embedded_plpgsql_avoids_provider_collisions; then + echo "embedded plpgsql has an invalid liboliphaunt symbol provider: $embedded_modules_dir/plpgsql.dylib" >&2 + exit 1 +fi +if [ "${OLIPHAUNT_BUILD_EXTENSIONS:-0}" = "0" ] && ! base_embedded_module_closure_ready; then + echo "base macOS runtime must contain exactly one regular embedded module: $embedded_modules_dir/plpgsql.dylib" >&2 + exit 1 +fi +build_native_extension_artifacts + +echo "$lib_out" +echo "Set LIBOLIPHAUNT_PATH=$lib_out" +echo "Set OLIPHAUNT_INITDB=$install_dir/bin/initdb" +echo "Set OLIPHAUNT_POSTGRES=$install_dir/bin/postgres" diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 b/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 new file mode 100644 index 00000000..519dddc0 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 @@ -0,0 +1,2942 @@ +param( + [Alias("check-current")] + [switch]$CheckCurrent +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = $null +try { + $RepoRoot = & git -C $ScriptDir rev-parse --show-toplevel 2>$null +} catch { + $RepoRoot = $null +} +if (-not $RepoRoot) { + $RepoRoot = (Resolve-Path (Join-Path $ScriptDir "../../../../..")).Path +} else { + $RepoRoot = (Resolve-Path $RepoRoot).Path +} +$PgVersion = "18.4" +$PgSha256 = "81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +$PgUrl = "https://ftp.postgresql.org/pub/source/v$PgVersion/postgresql-$PgVersion.tar.bz2" +$PgUrls = @( + $PgUrl, + "https://fossies.org/linux/misc/postgresql-$PgVersion.tar.bz2" +) +$SourceManifest = Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/postgres18/source.toml" +$PatchDir = Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/patches/postgresql-$PgVersion" +$TargetId = "windows-x64-msvc" +$WorkRoot = if ($env:OLIPHAUNT_WINDOWS_WORK_ROOT) { + $env:OLIPHAUNT_WINDOWS_WORK_ROOT +} elseif ($env:OLIPHAUNT_WORK_ROOT) { + $env:OLIPHAUNT_WORK_ROOT +} else { + Join-Path $RepoRoot "target/liboliphaunt-pg18-$TargetId" +} +$SourceCache = Join-Path $WorkRoot "source" +$Tarball = Join-Path $SourceCache "postgresql-$PgVersion.tar.bz2" +$BuildDir = Join-Path $WorkRoot "postgresql-$PgVersion" +$RuntimeBuildDir = Join-Path $WorkRoot "meson-runtime" +$EmbeddedBuildDir = Join-Path $WorkRoot "meson-embedded" +$RuntimeNativeFile = Join-Path $WorkRoot "meson-runtime-native.ini" +$EmbeddedNativeFile = Join-Path $WorkRoot "meson-embedded-native.ini" +$InstallDir = Join-Path $WorkRoot "install" +$OutDir = Join-Path $WorkRoot "out" +$ObjDir = Join-Path $OutDir "obj" +$DllOut = Join-Path $OutDir "bin/oliphaunt.dll" +$ImportLibOut = Join-Path $OutDir "lib/oliphaunt.lib" +$EmbeddedModulesDir = Join-Path $OutDir "modules" +$EmbeddedPlpgsqlDllOut = Join-Path $EmbeddedModulesDir "plpgsql.dll" +$VcRuntimeClosureTool = Join-Path $RepoRoot "tools/release/windows-vc-runtime-closure.mjs" +$Stamp = Join-Path $OutDir "oliphaunt-windows.inputs.sha256" +$ExternalCheckoutRoot = Join-Path $RepoRoot "target/oliphaunt-sources/checkouts" +$OpenSslSourceManifest = Join-Path $RepoRoot "src/sources/third-party/shared/openssl.toml" +$PgxsBuildPlan = Join-Path $RepoRoot "src/extensions/generated/pgxs-build.tsv" +$PortableUuidDir = Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/portable-uuid" +$PortableUuidIncludeDir = Join-Path $PortableUuidDir "include" +$OpenSslDependencyPrefix = Join-Path $WorkRoot "windows-dependencies/openssl" +$PostgisDependencyPrefix = Join-Path $WorkRoot "windows-dependencies/postgis" +$OliphauntContribDir = Join-Path $BuildDir "contrib/oliphaunt_external" +$BuildExtensions = if ($env:OLIPHAUNT_BUILD_EXTENSIONS) { $env:OLIPHAUNT_BUILD_EXTENSIONS } else { "0" } +$NativeExtensionSqlNames = if ($env:OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES) { + $env:OLIPHAUNT_NATIVE_EXTENSION_SQL_NAMES +} elseif ($env:OLIPHAUNT_EXTENSION_SQL_NAMES) { + $env:OLIPHAUNT_EXTENSION_SQL_NAMES +} else { + "" +} +$SelectedNativeExtensionSqlNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) +foreach ($name in ($NativeExtensionSqlNames -split ",")) { + $trimmed = $name.Trim() + if ($trimmed) { + [void]$SelectedNativeExtensionSqlNames.Add($trimmed) + } +} +$ExactExtensionCatalogRows = $null + +$LiboliphauntSources = @( + "src/runtimes/liboliphaunt/native/src/liboliphaunt_native.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_runtime.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_protocol.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_bootstrap.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_process.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_trace.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_fs.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_archive.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_archive_tar.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_static_extensions.c", + "src/runtimes/liboliphaunt/native/src/liboliphaunt_builtin_extensions.c" +) | ForEach-Object { Join-Path $RepoRoot $_ } + +function Fail($Message) { + Write-Error "build-postgres18-windows.ps1: $Message" + exit 1 +} + +function Require-Command($Name) { + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + Fail "missing required command: $Name" + } +} + +function Normalize-PathEntry([string]$PathEntry) { + $trimmed = $PathEntry.Trim().TrimEnd([char[]]@('\', '/')) + if (-not $trimmed) { + return "" + } + try { + [System.IO.Path]::GetFullPath($trimmed).TrimEnd([char[]]@('\', '/')).ToLowerInvariant() + } catch { + $trimmed.ToLowerInvariant() + } +} + +function Set-ProcessPath([string[]]$Entries) { + $seen = @{} + $clean = New-Object System.Collections.Generic.List[string] + foreach ($entry in $Entries) { + if ([string]::IsNullOrWhiteSpace($entry)) { + continue + } + $trimmed = $entry.Trim() + $key = Normalize-PathEntry $trimmed + if ($key -and -not $seen.ContainsKey($key)) { + $seen[$key] = $true + $clean.Add($trimmed) | Out-Null + } + } + Set-Item -Path Env:Path -Value ([string]::Join(";", $clean)) +} + +function Prepend-ProcessPath([string[]]$Entries) { + Set-ProcessPath (@($Entries) + @($env:Path -split ";")) +} + +function Is-MsysToolPath([string]$PathEntry) { + $normalized = Normalize-PathEntry $PathEntry + return $normalized -match '\\git\\usr\\bin$' -or + $normalized -match '\\git\\mingw64\\bin$' -or + $normalized -match '\\mingw64\\bin$' +} + +function Resolve-ApplicationPath([string]$Name) { + $command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $command) { + Fail "missing required command: $Name" + } + $command.Source +} + +function Get-PythonCommand { + $python = Get-Command python -ErrorAction SilentlyContinue + if ($python) { + return [PSCustomObject]@{ + Command = $python.Source + Arguments = @() + } + } + $py = Get-Command py -ErrorAction SilentlyContinue + if ($py) { + return [PSCustomObject]@{ + Command = $py.Source + Arguments = @("-3") + } + } + Fail "missing required command: python" +} + +function Invoke-Python([string[]]$Arguments) { + $python = Get-PythonCommand + & $python.Command @($python.Arguments) @Arguments + if ($LASTEXITCODE -ne 0) { + Fail "python command failed: $($Arguments -join ' ')" + } +} + +function Add-PythonUserScriptsToPath { + $python = Get-PythonCommand + $script = @" +import os +import site +import sysconfig + +paths = [] +for scheme in (None, "nt_user"): + try: + path = sysconfig.get_path("scripts", scheme=scheme) if scheme else sysconfig.get_path("scripts") + except Exception: + path = None + if path: + paths.append(path) +user_base = getattr(site, "USER_BASE", None) +if user_base: + paths.append(os.path.join(user_base, "Scripts")) +seen = set() +for path in paths: + normalized = os.path.normcase(os.path.normpath(path)) + if normalized not in seen: + seen.add(normalized) + print(path) +"@ + $scriptPaths = & $python.Command @($python.Arguments) -c $script + foreach ($scripts in $scriptPaths) { + if ($scripts -and (Test-Path $scripts)) { + Prepend-ProcessPath @($scripts) + } + } +} + +function Ensure-MesonTools { + Add-PythonUserScriptsToPath + if (-not (Get-Command meson -ErrorAction SilentlyContinue) -or -not (Get-Command ninja -ErrorAction SilentlyContinue)) { + Invoke-Python @("-m", "pip", "install", "--user", "meson==1.10.0", "ninja==1.13.0") + Add-PythonUserScriptsToPath + } + Require-Command meson + Require-Command ninja +} + +function Import-MsvcEnvironment { + if (Get-Command cl.exe -ErrorAction SilentlyContinue) { + return + } + $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio/Installer/vswhere.exe" + if (-not (Test-Path $vswhere)) { + Fail "vswhere.exe was not found; install Visual Studio Build Tools with MSVC x64 tools" + } + $vsRoot = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vsRoot) { + Fail "Visual Studio Build Tools with MSVC x64 tools were not found" + } + $vsDevCmd = Join-Path $vsRoot "Common7/Tools/VsDevCmd.bat" + if (-not (Test-Path $vsDevCmd)) { + Fail "VsDevCmd.bat was not found at $vsDevCmd" + } + cmd.exe /s /c "`"$vsDevCmd`" -arch=x64 -host_arch=x64 >nul && set" | + ForEach-Object { + if ($_ -match "^(.*?)=(.*)$") { + Set-Item -Path "Env:$($Matches[1])" -Value $Matches[2] + } + } + Require-Command cl.exe + Require-Command link.exe + Require-Command dumpbin.exe +} + +function Configure-MsvcToolchainPath { + if (-not $env:VCToolsInstallDir) { + Fail "VCToolsInstallDir is not set; run from an MSVC developer environment" + } + $msvcBin = Join-Path $env:VCToolsInstallDir "bin/HostX64/x64" + $requiredTools = @("cl.exe", "link.exe", "lib.exe", "dumpbin.exe") + foreach ($tool in $requiredTools) { + $toolPath = Join-Path $msvcBin $tool + if (-not (Test-Path $toolPath)) { + Fail "MSVC tool was not found at $toolPath" + } + } + + $filteredPath = @($env:Path -split ";") | Where-Object { -not (Is-MsysToolPath $_) } + Set-ProcessPath (@($msvcBin) + $filteredPath) + + foreach ($tool in $requiredTools) { + $resolved = Resolve-ApplicationPath $tool + if (-not $resolved.StartsWith($msvcBin, [System.StringComparison]::OrdinalIgnoreCase)) { + Fail "$tool resolved to $resolved instead of the MSVC tool directory $msvcBin" + } + } + + $env:CC = "cl.exe" + $env:CXX = "cl.exe" + $env:AR = "lib.exe" + Write-Host "Using MSVC tools from $msvcBin" +} + +function Prefer-NativePerl { + $candidateDirs = @( + "C:\Strawberry\perl\bin", + "C:\Perl64\bin" + ) + foreach ($dir in $candidateDirs) { + if (Test-Path (Join-Path $dir "perl.exe")) { + Prepend-ProcessPath @($dir) + break + } + } + $perl = Get-Command perl.exe -ErrorAction SilentlyContinue + if (-not $perl) { + Fail "missing required command: perl.exe" + } + if ($perl.Source -like "*\Git\usr\bin\perl.exe") { + Fail "Git/MSYS Perl cannot drive PostgreSQL's MSVC build because it rewrites native tool arguments; install Strawberry Perl or another native Windows Perl" + } +} + +function Get-FileSha256($Path) { + (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLowerInvariant() +} + +function Invoke-VcRuntimeClosure([string[]]$Arguments) { + & bun $VcRuntimeClosureTool @Arguments + if ($LASTEXITCODE -ne 0) { + Fail "Windows x64 app-local VC runtime closure failed: $($Arguments -join ' ')" + } +} + +function Stage-VcRuntimeClosure { + Invoke-VcRuntimeClosure @( + "stage", + "--root", $InstallDir, + "--profile", "provider", + "--destination", (Join-Path $InstallDir "bin") + ) + Invoke-VcRuntimeClosure @( + "stage", + "--root", $OutDir, + "--profile", "provider", + "--destination", (Join-Path $OutDir "bin") + ) + Invoke-VcRuntimeClosure @( + "verify", + "--root", $InstallDir, + "--profile", "provider", + "--search-root", (Join-Path $InstallDir "bin") + ) + Invoke-VcRuntimeClosure @( + "verify", + "--root", $OutDir, + "--profile", "provider", + "--search-root", (Join-Path $OutDir "bin") + ) +} + +function Test-VcRuntimeClosure { + & bun $VcRuntimeClosureTool verify ` + --root $InstallDir ` + --profile provider ` + --search-root (Join-Path $InstallDir "bin") *> $null + if ($LASTEXITCODE -ne 0) { + return $false + } + & bun $VcRuntimeClosureTool verify ` + --root $OutDir ` + --profile provider ` + --search-root (Join-Path $OutDir "bin") *> $null + $LASTEXITCODE -eq 0 +} + +function Test-PostgresSourceArchive([string]$Path) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + return $false + } + (Get-FileSha256 $Path) -eq $PgSha256 +} + +function Download-PostgresSourceArchive { + $partial = "$Tarball.partial.$PID.$([Guid]::NewGuid().ToString('N'))" + try { + foreach ($url in $PgUrls) { + Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue + $curlExit = 1 + try { + # Schannel can report transient revocation-service outages as curl + # error 35. Keep certificate and hostname validation, and still + # reject certificates that are known to be revoked, while allowing + # a download to proceed when only the revocation distribution point + # is offline. Retries and the independent pinned mirror cover the + # remaining bounded transport failures. + $curlArgs = @( + "--location", "--fail", "--silent", "--show-error", + "--retry", "4", "--retry-all-errors", "--retry-delay", "3", + "--retry-max-time", "90", "--connect-timeout", "20", "--max-time", "60", + "--max-filesize", "67108864", + "--ssl-revoke-best-effort", + "--proto", "=https", "--proto-redir", "=https", "--remove-on-error", + "--output", $partial, $url + ) + & curl.exe @curlArgs + $curlExit = $LASTEXITCODE + } catch { + Write-Warning "curl failed while downloading PostgreSQL $PgVersion from ${url}: $($_.Exception.Message)" + } + + if ($curlExit -eq 0 -and (Test-PostgresSourceArchive $partial)) { + Move-Item -LiteralPath $partial -Destination $Tarball -Force + return + } + if ($curlExit -eq 0 -and (Test-Path -LiteralPath $partial -PathType Leaf)) { + $actual = Get-FileSha256 $partial + Write-Warning "discarding PostgreSQL $PgVersion from $url with checksum $actual instead of $PgSha256" + } else { + Write-Warning "PostgreSQL $PgVersion download from $url failed after bounded retries (curl exit $curlExit)" + } + } + Fail "failed to download verified PostgreSQL $PgVersion source from every pinned HTTPS location" + } finally { + Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue + } +} + +function NativeExtension-Selected([string]$SqlName) { + if ($BuildExtensions -eq "0") { + return $false + } + if ($SelectedNativeExtensionSqlNames.Count -eq 0) { + return $true + } + $SelectedNativeExtensionSqlNames.Contains($SqlName) +} + +function Assert-WindowsNativeExtensionSelectionSupported { +} + +function Meson-Quote([string]$Value) { + "'" + $Value.Replace("\", "/").Replace("'", "\'") + "'" +} + +function Meson-Path([string]$Path) { + ([System.IO.Path]::GetFullPath($Path)).Replace("\", "/") +} + +function Meson-List([string[]]$Values, [string]$Indent = " ") { + if ($Values.Count -eq 0) { + return "" + } + (($Values | ForEach-Object { "$Indent$(Meson-Quote $_)" }) -join ",`n") +} + +function Meson-DataInstall([string[]]$Files) { + $fileList = Meson-List $Files +@" +install_data( +$fileList, + kwargs: contrib_data_args, +) +"@ +} + +function Copy-SourceTree([string]$Source, [string]$Destination) { + if (-not (Test-Path $Source)) { + Fail "missing source checkout: $Source" + } + Remove-Item -Recurse -Force $Destination -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + Copy-Item -Path (Join-Path $Source "*") -Destination $Destination -Recurse -Force + Remove-Item -Recurse -Force (Join-Path $Destination ".git") -ErrorAction SilentlyContinue +} + +function External-Checkout([string]$CheckoutName) { + Join-Path $ExternalCheckoutRoot $CheckoutName +} + +function Get-PatchSeries { + $inSeries = $false + foreach ($line in Get-Content $SourceManifest) { + if ($line -match "series\s*=\s*\[") { + $inSeries = $true + continue + } + if ($inSeries -and $line -match "\]") { + break + } + if ($inSeries -and $line -match '"([^"]+\.patch)"') { + $Matches[1] + } + } +} + +function Get-DesiredHash { + $parts = New-Object System.Collections.Generic.List[string] + $parts.Add("pg_version=$PgVersion") + $parts.Add("pg_sha256=$PgSha256") + $parts.Add("target_id=$TargetId") + $parts.Add("build_extensions=$BuildExtensions") + $parts.Add("native_extension_sql_names=$NativeExtensionSqlNames") + $parts.Add("script=$(Get-FileSha256 $PSCommandPath)") + $parts.Add("source_manifest=$(Get-FileSha256 $SourceManifest)") + foreach ($patch in Get-PatchSeries) { + $parts.Add("patch:$patch=$(Get-FileSha256 (Join-Path $PatchDir $patch))") + } + foreach ($source in $LiboliphauntSources) { + $parts.Add("source:$source=$(Get-FileSha256 $source)") + } + foreach ($source in @( + $OpenSslSourceManifest, + $PgxsBuildPlan, + (Join-Path $PortableUuidDir "portable_uuid.c"), + (Join-Path $PortableUuidIncludeDir "uuid/uuid.h"), + (Join-Path $RepoRoot "src/extensions/external/pg_hashids/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/pg_ivm/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/pg_textsearch/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/pg_uuidv7/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/deps.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/dependencies/geos/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/dependencies/json-c/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/dependencies/libxml2/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/dependencies/proj/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/postgis/dependencies/sqlite/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/pgtap/source.toml"), + (Join-Path $RepoRoot "src/extensions/external/vector/source.toml") + )) { + if (Test-Path $source) { + $parts.Add("source-input:$source=$(Get-FileSha256 $source)") + } + } + $bytes = [System.Text.Encoding]::UTF8.GetBytes(($parts -join "`n") + "`n") + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + (($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) -join "") + } finally { + $sha.Dispose() + } +} + +function Invoke-Logged([string]$LogName, [scriptblock]$Block) { + $log = Join-Path $WorkRoot $LogName + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $log) | Out-Null + $global:LASTEXITCODE = 0 + & $Block *> $log + if ($LASTEXITCODE -ne 0) { + [Console]::Error.WriteLine("==== $LogName tail ====") + if (Test-Path $log) { + Get-Content $log -Tail 160 | ForEach-Object { [Console]::Error.WriteLine($_) } + } else { + [Console]::Error.WriteLine("(log file was not created: $log)") + } + [Console]::Error.WriteLine("==== end $LogName tail ====") + Fail "$LogName failed; see $log" + } +} + +function Expand-PostgresSourceArchive { + $script = @' +import sys +import tarfile +from pathlib import Path + +archive = Path(sys.argv[1]) +destination = Path(sys.argv[2]).resolve() +with tarfile.open(archive, "r:bz2") as source: + members = source.getmembers() + for member in members: + target = (destination / member.name).resolve() + if target != destination and destination not in target.parents: + raise SystemExit(f"archive member escapes extraction root: {member.name}") + try: + source.extractall(destination, members=members, filter="data") + except TypeError: + source.extractall(destination, members=members) +'@ + Invoke-Python @("-c", $script, $Tarball, $WorkRoot) +} + +function Prepare-Source([string]$DesiredHash) { + New-Item -ItemType Directory -Force -Path $SourceCache, $WorkRoot, $OutDir, $ObjDir | Out-Null + if ((Test-Path -LiteralPath $Tarball -PathType Leaf) -and -not (Test-PostgresSourceArchive $Tarball)) { + $actual = Get-FileSha256 $Tarball + Write-Warning "discarding cached PostgreSQL $PgVersion source with checksum $actual instead of $PgSha256" + Remove-Item -LiteralPath $Tarball -Force + } + if (-not (Test-Path -LiteralPath $Tarball -PathType Leaf)) { + Download-PostgresSourceArchive + } + $actual = Get-FileSha256 $Tarball + if ($actual -ne $PgSha256) { + Fail "PostgreSQL source checksum mismatch: expected $PgSha256, got $actual" + } + $current = if (Test-Path $Stamp) { (Get-Content $Stamp -Raw).Trim() } else { "" } + if ((Test-Path $BuildDir) -and $current -ne $DesiredHash) { + Remove-Item -Recurse -Force $BuildDir, $RuntimeBuildDir, $EmbeddedBuildDir, $InstallDir, $OutDir -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $OutDir, $ObjDir | Out-Null + } + if (-not (Test-Path $BuildDir)) { + Expand-PostgresSourceArchive + Push-Location $BuildDir + try { + git init -q + foreach ($patch in Get-PatchSeries) { + git apply --whitespace=error-all (Join-Path $PatchDir $patch) + if ($LASTEXITCODE -ne 0) { + Fail "failed to apply PostgreSQL patch $patch" + } + } + } finally { + Pop-Location + } + } + Assert-PatchedSource +} + +function Assert-FileContains([string]$Path, [string]$Needle) { + if (-not (Test-Path $Path)) { + Fail "missing patched PostgreSQL source file $Path" + } + $text = Get-Content -Raw -Path $Path + if (-not $text.Contains($Needle)) { + Fail "patched PostgreSQL source file $Path does not contain required marker $Needle" + } +} + +function Assert-PatchedSource { + Assert-FileContains (Join-Path $BuildDir "src/include/libpq/libpq-be.h") "OliphauntEmbeddedIO" + Assert-FileContains (Join-Path $BuildDir "src/backend/tcop/postgres.c") "oliphaunt_embedded_main" + Assert-FileContains (Join-Path $BuildDir "src/port/pqsignal.c") "oliphaunt_embedded_kill" + Assert-FileContains (Join-Path $BuildDir "src/port/pqsignal.c") "oliphaunt_embedded_raise" + Assert-FileContains (Join-Path $BuildDir "src/bin/initdb/initdb.c") 'getenv("ICU_DATA")' + Assert-FileContains (Join-Path $BuildDir "meson_options.txt") "oliphaunt_embedded" + Assert-FileContains (Join-Path $BuildDir "meson_options.txt") "oliphaunt_embedded_module_provider" + Assert-FileContains (Join-Path $BuildDir "meson.build") "OLIPHAUNT_EMBEDDED" + Assert-FileContains (Join-Path $BuildDir "src/backend/meson.build") "oliphaunt_embedded_module_provider" +} + +function Append-OliphauntContribSubdir([string]$Subdir) { + $contribMeson = Join-Path $BuildDir "contrib/meson.build" + $line = "subdir('oliphaunt_external/$Subdir')" + $text = Get-Content -Raw -Path $contribMeson + if (-not $text.Contains($line)) { + Add-Content -Path $contribMeson -Value $line + } +} + +function Write-OliphauntMesonModule( + [string]$Subdir, + [string]$ModuleName, + [string[]]$Sources, + [string[]]$DataFiles, + [string[]]$CArgs = @(), + [string[]]$LinkArgs = @(), + [string[]]$LocalIncludeDirs = @() +) { + $destination = Join-Path $OliphauntContribDir $Subdir + New-Item -ItemType Directory -Force -Path $destination | Out-Null + $variable = $Subdir.Replace("-", "_") + $sourceList = Meson-List $Sources + $extraKwargs = New-Object System.Collections.Generic.List[string] + if ($CArgs.Count -gt 0) { + $extraKwargs.Add("'c_args': [`n$(Meson-List $CArgs " ")`n ]") + } + if ($LinkArgs.Count -gt 0) { + $extraKwargs.Add("'link_args': [`n$(Meson-List $LinkArgs " ")`n ]") + } + $extraKwargsText = "" + if ($extraKwargs.Count -gt 0) { + $extraKwargsText = " + {`n $($extraKwargs -join ",`n ")`n}" + } + $includeText = "" + if ($LocalIncludeDirs.Count -gt 0) { + $includeText = " include_directories: [$((($LocalIncludeDirs | ForEach-Object { "include_directories($(Meson-Quote $_))" }) -join ', '))],`n" + } + $dataInstall = Meson-DataInstall $DataFiles + $meson = @" +$variable = shared_module( + $(Meson-Quote $ModuleName), + files( +$sourceList, + ), + c_pch: pch_postgres_h, +$includeText kwargs: contrib_mod_args$extraKwargsText, +) +contrib_targets += $variable + +$dataInstall +"@ + Set-Content -Path (Join-Path $destination "meson.build") -Value $meson -Encoding UTF8 + Append-OliphauntContribSubdir $Subdir +} + +function Build-WindowsOpenSslDependency { + if (-not (NativeExtension-Selected "pgcrypto")) { + return + } + $includeDir = Join-Path $OpenSslDependencyPrefix "include/openssl" + $libCrypto = Join-Path $OpenSslDependencyPrefix "lib/libcrypto.lib" + if ((Test-Path $includeDir) -and (Test-Path $libCrypto)) { + return + } + Require-Command nmake.exe + $sourceDir = External-Checkout "openssl" + if (-not (Test-Path (Join-Path $sourceDir "Configure"))) { + Fail "missing OpenSSL checkout for pgcrypto: $sourceDir" + } + $buildRoot = Join-Path $WorkRoot "openssl-windows-build" + Remove-Item -Recurse -Force $buildRoot, $OpenSslDependencyPrefix -ErrorAction SilentlyContinue + Copy-SourceTree $sourceDir $buildRoot + Invoke-Logged "openssl-windows-build.log" { + Push-Location $buildRoot + try { + & perl Configure VC-WIN64A no-shared no-tests no-apps no-module no-asm ` + "--prefix=$OpenSslDependencyPrefix" ` + "--openssldir=$(Join-Path $OpenSslDependencyPrefix "ssl")" + if ($LASTEXITCODE -ne 0) { return } + & nmake.exe /nologo build_generated libcrypto.lib + if ($LASTEXITCODE -ne 0) { return } + & nmake.exe /nologo install_sw + } finally { + Pop-Location + } + } + if (-not (Test-Path $includeDir) -or -not (Test-Path $libCrypto)) { + Fail "OpenSSL Windows build did not produce include/openssl and lib/libcrypto.lib under $OpenSslDependencyPrefix" + } +} + +function Find-FirstFileOrNull([string]$Root, [string[]]$Filters) { + if (-not (Test-Path $Root)) { + return $null + } + foreach ($filter in $Filters) { + $item = Get-ChildItem -Path $Root -Recurse -Filter $filter -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($item) { + return $item.FullName + } + } + $null +} + +function Invoke-CmakeInstall( + [string]$Name, + [string]$SourceDir, + [string]$BuildRoot, + [string]$Prefix, + [string[]]$ConfigureArgs = @() +) { + Require-Command cmake + Require-Command ninja + Remove-Item -Recurse -Force $BuildRoot, $Prefix -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $BuildRoot, $Prefix | Out-Null + $cmakeArgs = @( + "-S", $SourceDir, + "-B", $BuildRoot, + "-G", "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_INSTALL_PREFIX=$Prefix", + "-DCMAKE_C_COMPILER=cl.exe", + "-DCMAKE_CXX_COMPILER=cl.exe", + "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL" + ) + $ConfigureArgs + Invoke-Logged "postgis-$Name-cmake-configure.log" { cmake @cmakeArgs } + Invoke-Logged "postgis-$Name-cmake-install.log" { cmake --build $BuildRoot --config Release --target install } +} + +function Build-WindowsPostgisJsonCDependency { + if (-not (NativeExtension-Selected "postgis")) { + return + } + $prefix = Join-Path $PostgisDependencyPrefix "json-c" + $archive = Find-FirstFileOrNull $prefix @("json-c.lib", "json-c-static.lib") + if ((Test-Path (Join-Path $prefix "include/json-c")) -and $archive) { + return + } + $sourceDir = External-Checkout "json-c" + if (-not (Test-Path (Join-Path $sourceDir "CMakeLists.txt"))) { + Fail "missing JSON-C checkout for PostGIS: $sourceDir" + } + Invoke-CmakeInstall "json-c" $sourceDir (Join-Path $WorkRoot "json-c-windows-build") $prefix @( + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-DBUILD_SHARED_LIBS=OFF", + "-DBUILD_STATIC_LIBS=ON", + "-DBUILD_APPS=OFF", + "-DBUILD_TESTING=OFF", + "-DDISABLE_WERROR=ON" + ) + [void](First-File $prefix @("json-c.lib", "json-c-static.lib")) +} + +function Build-WindowsPostgisSqliteDependency { + if (-not (NativeExtension-Selected "postgis")) { + return + } + $prefix = Join-Path $PostgisDependencyPrefix "sqlite" + $archive = Join-Path $prefix "lib/sqlite3.lib" + $shell = Join-Path $prefix "bin/sqlite3.exe" + if ((Test-Path $archive) -and (Test-Path $shell) -and (Test-Path (Join-Path $prefix "include/sqlite3.h"))) { + return + } + Require-Command nmake.exe + $sourceDir = External-Checkout "sqlite" + if (-not (Test-Path (Join-Path $sourceDir "Makefile.msc"))) { + Fail "missing SQLite checkout for PostGIS: $sourceDir" + } + $buildRoot = Join-Path $WorkRoot "sqlite-windows-build" + Remove-Item -Recurse -Force $buildRoot, $prefix -ErrorAction SilentlyContinue + Copy-SourceTree $sourceDir $buildRoot + Invoke-Logged "postgis-sqlite-windows-build.log" { + Push-Location $buildRoot + try { + # SQLite's MSVC makefile still injects /NODEFAULTLIB:msvcrt into + # host-tool links. Let the runner's MSVC/UCRT defaults resolve CRT + # symbols instead, and skip TCL artifacts that PostGIS does not use. + & nmake.exe /nologo /f Makefile.msc libsqlite3.lib sqlite3.exe ` + USE_CRT_DLL=1 NO_TCL=1 LDFLAGS= ` + OPTS="-DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION" + } finally { + Pop-Location + } + } + New-Item -ItemType Directory -Force -Path (Join-Path $prefix "include"), (Join-Path $prefix "lib"), (Join-Path $prefix "bin") | Out-Null + Copy-Item -Force (Join-Path $buildRoot "libsqlite3.lib") $archive + Copy-Item -Force (Join-Path $buildRoot "sqlite3.exe") $shell + Copy-Item -Force (Join-Path $buildRoot "sqlite3.h"), (Join-Path $buildRoot "sqlite3ext.h") (Join-Path $prefix "include") + if (-not (Test-Path $archive) -or -not (Test-Path $shell) -or -not (Test-Path (Join-Path $prefix "include/sqlite3.h"))) { + Fail "SQLite Windows build did not produce sqlite3.lib, sqlite3.exe, and headers under $prefix" + } +} + +function Build-WindowsPostgisGeosDependency { + if (-not (NativeExtension-Selected "postgis")) { + return + } + $prefix = Join-Path $PostgisDependencyPrefix "geos" + if ((Test-Path (Join-Path $prefix "include/geos_c.h")) -and + (Find-FirstFileOrNull $prefix @("geos_c.lib")) -and + (Find-FirstFileOrNull $prefix @("geos.lib"))) { + return + } + $sourceDir = External-Checkout "geos" + if (-not (Test-Path (Join-Path $sourceDir "CMakeLists.txt"))) { + Fail "missing GEOS checkout for PostGIS: $sourceDir" + } + Invoke-CmakeInstall "geos" $sourceDir (Join-Path $WorkRoot "geos-windows-build") $prefix @( + "-DBUILD_SHARED_LIBS=OFF", + "-DBUILD_TESTING=OFF", + "-DBUILD_BENCHMARKS=OFF", + "-DBUILD_GEOSOP=OFF", + "-DGEOS_BUILD_DEVELOPER=OFF" + ) + [void](First-File $prefix @("geos_c.lib")) + [void](First-File $prefix @("geos.lib")) +} + +function Build-WindowsPostgisLibxml2Dependency { + if (-not (NativeExtension-Selected "postgis")) { + return + } + $prefix = Join-Path $PostgisDependencyPrefix "libxml2" + if ((Test-Path (Join-Path $prefix "include/libxml2/libxml/parser.h")) -and + (Find-FirstFileOrNull $prefix @("libxml2s.lib", "libxml2.lib", "xml2.lib"))) { + return + } + $sourceDir = External-Checkout "libxml2" + if (-not (Test-Path (Join-Path $sourceDir "CMakeLists.txt"))) { + Fail "missing libxml2 checkout for PostGIS: $sourceDir" + } + Invoke-CmakeInstall "libxml2" $sourceDir (Join-Path $WorkRoot "libxml2-windows-build") $prefix @( + "-DBUILD_SHARED_LIBS=OFF", + "-DLIBXML2_WITH_PROGRAMS=OFF", + "-DLIBXML2_WITH_TESTS=OFF", + "-DLIBXML2_WITH_PYTHON=OFF", + "-DLIBXML2_WITH_THREADS=OFF", + "-DLIBXML2_WITH_MODULES=OFF", + "-DLIBXML2_WITH_ICONV=OFF", + "-DLIBXML2_WITH_ZLIB=OFF", + "-DLIBXML2_WITH_LZMA=OFF", + "-DLIBXML2_WITH_HTTP=OFF" + ) + [void](First-File $prefix @("libxml2s.lib", "libxml2.lib", "xml2.lib")) +} + +function Build-WindowsPostgisProjDependency { + if (-not (NativeExtension-Selected "postgis")) { + return + } + Build-WindowsPostgisSqliteDependency + $prefix = Join-Path $PostgisDependencyPrefix "proj" + if ((Test-Path (Join-Path $prefix "include/proj.h")) -and + (Test-Path (Join-Path $prefix "share/proj/proj.db")) -and + (Find-FirstFileOrNull $prefix @("proj.lib", "libproj.lib"))) { + return + } + $sourceDir = External-Checkout "proj" + if (-not (Test-Path (Join-Path $sourceDir "CMakeLists.txt"))) { + Fail "missing PROJ checkout for PostGIS: $sourceDir" + } + $sqlitePrefix = Join-Path $PostgisDependencyPrefix "sqlite" + $sqliteInclude = Join-Path $sqlitePrefix "include" + $sqliteLib = Join-Path $sqlitePrefix "lib/sqlite3.lib" + $sqliteExe = Join-Path $sqlitePrefix "bin/sqlite3.exe" + $buildRoot = Join-Path $WorkRoot "proj-windows-build" + Invoke-CmakeInstall "proj" $sourceDir $buildRoot $prefix @( + "-DBUILD_SHARED_LIBS=OFF", + "-DSQLite3_INCLUDE_DIR=$sqliteInclude", + "-DSQLite3_LIBRARY=$sqliteLib", + "-DEXE_SQLITE3=$sqliteExe", + "-DENABLE_TIFF=OFF", + "-DENABLE_CURL=OFF", + "-DENABLE_EMSCRIPTEN_FETCH=OFF", + "-DHAVE_LIBDL=OFF", + "-DBUILD_APPS=OFF", + "-DBUILD_TESTING=OFF", + "-DBUILD_EXAMPLES=OFF", + "-DEMBED_RESOURCE_FILES=ON", + "-DUSE_ONLY_EMBEDDED_RESOURCE_FILES=ON" + ) + $projDb = Join-Path $prefix "share/proj/proj.db" + if (-not (Test-Path $projDb) -and (Test-Path (Join-Path $buildRoot "data/proj.db"))) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $projDb) | Out-Null + Copy-Item -Force (Join-Path $buildRoot "data/proj.db") $projDb + } + [void](First-File $prefix @("proj.lib", "libproj.lib")) + if (-not (Test-Path $projDb)) { + Fail "PROJ Windows build did not produce proj.db under $prefix" + } +} + +function Build-WindowsPostgisDependencies { + if (-not (NativeExtension-Selected "postgis")) { + return + } + Build-WindowsPostgisSqliteDependency + Build-WindowsPostgisJsonCDependency + Build-WindowsPostgisGeosDependency + Build-WindowsPostgisLibxml2Dependency + Build-WindowsPostgisProjDependency +} + +function Read-PostgisVersionConfig([string]$PostgisSourceDir) { + $versionPath = Join-Path $PostgisSourceDir "Version.config" + if (-not (Test-Path $versionPath)) { + Fail "missing PostGIS Version.config: $versionPath" + } + $values = @{} + foreach ($line in Get-Content $versionPath) { + if ($line -match "^([A-Z0-9_]+)=(.*)$") { + $values[$Matches[1]] = $Matches[2].Trim() + } + } + foreach ($key in @("POSTGIS_MAJOR_VERSION", "POSTGIS_MINOR_VERSION", "POSTGIS_MICRO_VERSION")) { + if (-not $values.ContainsKey($key)) { + Fail "PostGIS Version.config does not define $key" + } + } + [PSCustomObject]@{ + Major = $values["POSTGIS_MAJOR_VERSION"] + Minor = $values["POSTGIS_MINOR_VERSION"] + Micro = $values["POSTGIS_MICRO_VERSION"] + Version = "$($values["POSTGIS_MAJOR_VERSION"]).$($values["POSTGIS_MINOR_VERSION"]).$($values["POSTGIS_MICRO_VERSION"])" + MajorMinor = "$($values["POSTGIS_MAJOR_VERSION"]).$($values["POSTGIS_MINOR_VERSION"])" + } +} + +function Get-PostgisSourceRevision([string]$PostgisSourceDir, [string]$FallbackVersion) { + $revision = "" + try { + $revision = (& git -C $PostgisSourceDir describe --always --dirty=never 2>$null | Select-Object -First 1).Trim() + } catch { + $revision = "" + } + if (-not $revision) { + $revision = $FallbackVersion + } + $revision +} + +function Expand-PostgisTemplate([string]$InputPath, [string]$OutputPath, [hashtable]$Values) { + $text = Get-Content -Raw -Path $InputPath + foreach ($key in $Values.Keys) { + $text = $text.Replace("@$key@", [string]$Values[$key]) + } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $OutputPath) | Out-Null + Set-Content -Path $OutputPath -Encoding UTF8 -Value $text +} + +function Initialize-WindowsPostgisGeneratedSource([string]$PostgisDir, [string]$OriginalSourceDir) { + $version = Read-PostgisVersionConfig $PostgisDir + $revision = Get-PostgisSourceRevision $OriginalSourceDir $version.Version + $buildDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss") + $geosVersionNumber = "31401" + $projVersionNumber = "90801" + $libXmlVersion = "2.14.6" + $postgisVersion = "$($version.Major).$($version.Minor) USE_GEOS=1 USE_PROJ=1 USE_STATS=1" + $localeDir = (Meson-Path (Join-Path $InstallDir "share/locale")) + + Set-Content -Path (Join-Path $PostgisDir "postgis_revision.h") -Encoding UTF8 -Value "#define POSTGIS_REVISION $revision" + Set-Content -Path (Join-Path $PostgisDir "postgis_config.h") -Encoding UTF8 -Value @" +/* postgis_config.h. Generated by Oliphaunt's Windows native producer. */ +#ifndef POSTGIS_CONFIG_H +#define POSTGIS_CONFIG_H 1 + +#include "postgis_revision.h" + +#define POSTGIS_DEBUG_LEVEL 0 +/* #undef ENABLE_NLS */ +/* #undef HAVE_GETTEXT */ +/* #undef WORDS_BIGENDIAN */ +/* #undef HAVE_ICONV */ +/* #undef HAVE_ICONVCTL */ +#define HAVE_IEEEFP_H 0 +#define HAVE_LIBGEOS_C 1 +/* #undef HAVE_LIBICONVCTL */ +/* #undef HAVE_LIBPROTOBUF */ +/* #undef LIBPROTOBUF_VERSION */ +#define HAVE_LIBJSON 1 +#define HAVE_LIBPQ 1 +#define HAVE_LIBPROJ 1 +#define HAVE_LIBXML2 1 +#define HAVE_LIBXML_PARSER_H 1 +#define HAVE_LIBXML_TREE_H 1 +#define HAVE_LIBXML_XPATHINTERNALS_H 1 +#define HAVE_LIBXML_XPATH_H 1 +/* #undef HAVE_UNISTD_H */ +/* #undef HAVE_SFCGAL */ +#define LT_OBJDIR ".libs/" +#define PGSQL_LOCALEDIR "$localeDir" +#define POSTGIS_BUILD_DATE "$buildDate" +/* #undef POSTGIS_SFCGAL_VERSION */ +/* #undef POSTGIS_GDAL_VERSION */ +#define POSTGIS_GEOS_VERSION $geosVersionNumber +#define POSTGIS_LIBXML2_VERSION "$libXmlVersion" +#define POSTGIS_LIB_VERSION "$($version.Version)" +#define POSTGIS_MAJOR_VERSION "$($version.Major)" +#define POSTGIS_MINOR_VERSION "$($version.Minor)" +#define POSTGIS_MICRO_VERSION "$($version.Micro)" +#define POSTGIS_PGSQL_VERSION 180 +#define POSTGIS_PROJ_VERSION $projVersionNumber +/* #undef POSTGIS_RASTER_WARN_ON_TRUNCATION */ +#define POSTGIS_SCRIPTS_VERSION "$($version.Version)" +#define POSTGIS_VERSION "$postgisVersion" +#define STDC_HEADERS 1 +#define YYTEXT_POINTER 1 + +#endif /* POSTGIS_CONFIG_H */ +"@ + + $templateValues = @{ + POSTGIS_PGSQL_VERSION = "180" + POSTGIS_PGSQL_HR_VERSION = "18.0" + POSTGIS_GEOS_VERSION = $geosVersionNumber + POSTGIS_PROJ_VERSION = $projVersionNumber + POSTGIS_LIB_VERSION = $version.Version + POSTGIS_LIBXML2_VERSION = $libXmlVersion + POSTGIS_SFCGAL_VERSION = "0" + POSTGIS_VERSION = $postgisVersion + POSTGIS_BUILD_DATE = $buildDate + POSTGIS_SCRIPTS_VERSION = $version.Version + SRID_MAX = "999999" + SRID_USR_MAX = "998999" + POSTGIS_MAJOR_VERSION = $version.Major + POSTGIS_MINOR_VERSION = $version.Minor + } + Expand-PostgisTemplate (Join-Path $PostgisDir "postgis/sqldefines.h.in") (Join-Path $PostgisDir "postgis/sqldefines.h") $templateValues + Expand-PostgisTemplate (Join-Path $PostgisDir "liblwgeom/liblwgeom.h.in") (Join-Path $PostgisDir "liblwgeom/liblwgeom.h") $templateValues + Expand-PostgisTemplate (Join-Path $PostgisDir "extensions/postgis/postgis.control.in") (Join-Path $PostgisDir "extensions/postgis/postgis.control") @{ + EXTVERSION = $version.Version + EXTENSION = "postgis" + MODULEPATH = '$libdir/postgis-3' + } + $version +} + +function Invoke-PostgisSqlPreprocessor([string]$InputPath, [string]$OutputPath, [string[]]$IncludeDirs) { + $script = @' +import pathlib +import re +import sys + +source = pathlib.Path(sys.argv[1]).resolve() +output = pathlib.Path(sys.argv[2]).resolve() +include_dirs = [pathlib.Path(p).resolve() for p in sys.argv[3:]] +macros = {} +result = [] +include_stack = [] +token_re = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") + +def expand_macros(text): + for _ in range(16): + changed = False + def repl(match): + nonlocal changed + name = match.group(0) + if name in macros: + changed = True + return macros[name] + return name + expanded = token_re.sub(repl, text) + text = expanded + if not changed: + break + return text + +def eval_expr(expr): + expr = expand_macros(expr) + expr = token_re.sub("0", expr) + expr = expr.replace("&&", " and ").replace("||", " or ") + if not re.match(r"^[0-9\s<>=!&|()+*/%.\-andor]+$", expr): + return False + try: + return bool(eval(expr, {"__builtins__": {}}, {})) + except Exception: + return False + +def find_include(name, current): + candidates = [current.parent] + include_dirs + for directory in candidates: + path = directory / name + if path.exists(): + return path.resolve() + raise SystemExit(f"could not resolve SQL include {name} from {current}") + +def in_block_comment_after_line(line, in_block_comment): + offset = 0 + while True: + if in_block_comment: + end = line.find("*/", offset) + if end == -1: + return True + in_block_comment = False + offset = end + 2 + continue + start = line.find("/*", offset) + if start == -1: + return False + end = line.find("*/", start + 2) + if end == -1: + return True + offset = end + 2 + +def process(path): + path = path.resolve() + if path in include_stack: + cycle = include_stack[include_stack.index(path):] + [path] + raise SystemExit("recursive SQL include: " + " -> ".join(str(item) for item in cycle)) + include_stack.append(path) + active = True + stack = [] + in_block_comment = False + try: + for raw in path.read_text(encoding="utf-8").splitlines(True): + stripped = raw.lstrip() + directive = None if in_block_comment or not stripped.startswith("#") else stripped[1:].strip() + if directive is None: + if active: + result.append(expand_macros(raw)) + in_block_comment = in_block_comment_after_line(raw, in_block_comment) + continue + + if directive.startswith("include"): + if active: + match = re.match(r'include\s+"([^"]+)"', directive) + if not match: + raise SystemExit(f"unsupported include directive in {path}: {raw.rstrip()}") + process(find_include(match.group(1), path)) + continue + if directive.startswith("define"): + if active: + match = re.match(r"define\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s+(.*?))?\s*$", directive) + if match: + macros[match.group(1)] = match.group(2) if match.group(2) is not None else "1" + continue + if directive.startswith("undef"): + if active: + parts = directive.split() + if len(parts) > 1: + macros.pop(parts[1], None) + continue + if directive.startswith("ifdef"): + name = directive.split(None, 1)[1].strip() + cond = name in macros + stack.append([active, cond]) + active = active and cond + continue + if directive.startswith("ifndef"): + name = directive.split(None, 1)[1].strip() + cond = name not in macros + stack.append([active, cond]) + active = active and cond + continue + if directive.startswith("if"): + cond = eval_expr(directive[2:].strip()) if active else False + stack.append([active, cond]) + active = active and cond + continue + if directive.startswith("elif"): + if not stack: + raise SystemExit(f"orphan #elif in {path}") + parent, taken = stack[-1] + cond = (not taken) and eval_expr(directive[4:].strip()) if parent else False + stack[-1][1] = taken or cond + active = parent and cond + continue + if directive.startswith("else"): + if not stack: + raise SystemExit(f"orphan #else in {path}") + parent, taken = stack[-1] + active = parent and not taken + stack[-1][1] = True + continue + if directive.startswith("endif"): + if not stack: + raise SystemExit(f"orphan #endif in {path}") + parent, _ = stack.pop() + active = parent + continue + + if active: + result.append(raw) + finally: + include_stack.pop() + +process(source) +output.parent.mkdir(parents=True, exist_ok=True) +output.write_text("".join(result), encoding="utf-8") +'@ + $args = @("-c", $script, $InputPath, $OutputPath) + $IncludeDirs + Invoke-Python $args +} + +function New-PostgisSqlFromTemplate( + [string]$InputPath, + [string]$OutputPath, + [string[]]$IncludeDirs, + [string]$ModulePath, + [bool]$StripTransactionBlocks, + [bool]$RemoveExtschemaPrefix +) { + $tmp = "$OutputPath.tmp" + Invoke-PostgisSqlPreprocessor $InputPath $tmp $IncludeDirs + $text = Get-Content -Raw -Path $tmp + Remove-Item -Force $tmp + $text = $text.Replace("MODULE_PATHNAME", $ModulePath) + if ($StripTransactionBlocks) { + $text = $text.Replace("BEGIN;", "").Replace("COMMIT;", "") + } + if ($RemoveExtschemaPrefix) { + $text = $text.Replace("@extschema@.", "") + } + Set-Content -Path $OutputPath -Encoding UTF8 -Value $text +} + +function Invoke-PerlToFile([string[]]$Arguments, [string]$OutputPath) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $OutputPath) | Out-Null + $global:LASTEXITCODE = 0 + & perl @Arguments > $OutputPath + if ($LASTEXITCODE -ne 0) { + Fail "perl command failed: $($Arguments -join ' ')" + } +} + +function Invoke-PerlFromInputFile([string]$InputPath, [string[]]$Arguments, [string]$OutputPath) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $OutputPath) | Out-Null + $global:LASTEXITCODE = 0 + Get-Content -Raw -Path $InputPath | & perl @Arguments > $OutputPath + if ($LASTEXITCODE -ne 0) { + Fail "perl command failed: $($Arguments -join ' ')" + } +} + +function Join-TextFiles([string]$OutputPath, [string[]]$InputPaths, [string]$Prefix = "", [string]$Suffix = "") { + $builder = [System.Text.StringBuilder]::new() + if ($Prefix) { + [void]$builder.Append($Prefix) + if (-not $Prefix.EndsWith("`n")) { + [void]$builder.Append("`n") + } + } + foreach ($path in $InputPaths) { + [void]$builder.Append((Get-Content -Raw -Path $path)) + if (-not $builder.ToString().EndsWith("`n")) { + [void]$builder.Append("`n") + } + } + if ($Suffix) { + [void]$builder.Append($Suffix) + if (-not $Suffix.EndsWith("`n")) { + [void]$builder.Append("`n") + } + } + Set-Content -Path $OutputPath -Encoding UTF8 -Value $builder.ToString() +} + +function New-PostgisRasterUnpackageSql([string]$PostgisDir, [string]$SqlDir, [string[]]$RasterDropSqlFiles) { + $template = Join-Path $PostgisDir "extensions/postgis/unpackage_raster_if_needed.sql" + $prefix = [System.Text.StringBuilder]::new() + $suffix = [System.Text.StringBuilder]::new() + $pastMarker = $false + foreach ($line in Get-Content $template) { + if (-not $pastMarker) { + [void]$prefix.AppendLine($line) + if ($line.Contains("UNPACKAGE_CODE")) { + $pastMarker = $true + } + } else { + [void]$suffix.AppendLine($line) + } + } + $dropSql = Join-Path $SqlDir "raster_drop_all.sql" + Join-TextFiles $dropSql $RasterDropSqlFiles + $unpackageBody = Join-Path $SqlDir "raster_unpackage_body.sql" + Invoke-PerlFromInputFile $dropSql @((Join-Path $PostgisDir "utils/create_extension_unpackage.pl"), "postgis") $unpackageBody + $body = Get-Content -Raw -Path $unpackageBody + Set-Content -Path (Join-Path $SqlDir "raster_unpackage.sql") -Encoding UTF8 -Value ($prefix.ToString() + $body + $suffix.ToString()) +} + +function Convert-PostgisExtensionDropGuards([string]$InputPath, [string]$OutputPath) { + $text = (Get-Content -Raw -Path $InputPath).Replace("BEGIN;", "").Replace("COMMIT;", "") + $lines = New-Object System.Collections.Generic.List[string] + foreach ($line in ($text -split "`r?`n")) { + if ($line -match "^(DROP .*)\;") { + $drop = $Matches[1] + $lines.Add("SELECT @extschema@.postgis_extension_drop_if_exists('postgis', '$drop');") + } + $lines.Add($line) + } + Set-Content -Path $OutputPath -Encoding UTF8 -Value ($lines -join "`n") +} + +function Build-WindowsPostgisSql([string]$PostgisDir, [pscustomobject]$Version) { + $postgisSqlDir = Join-Path $PostgisDir "postgis" + $extensionDir = Join-Path $PostgisDir "extensions/postgis" + $extensionSqlDir = Join-Path $extensionDir "sql" + New-Item -ItemType Directory -Force -Path $extensionSqlDir | Out-Null + $includeDirs = @($postgisSqlDir) + $modulePath = '$libdir/postgis-3' + + New-PostgisSqlFromTemplate (Join-Path $PostgisDir "extensions/postgis_extension_helper.sql.in") (Join-Path $PostgisDir "extensions/postgis_extension_helper.sql") @($PostgisDir, $postgisSqlDir) "" $false $false + New-PostgisSqlFromTemplate (Join-Path $postgisSqlDir "postgis.sql.in") (Join-Path $postgisSqlDir "postgis.sql") $includeDirs $modulePath $false $true + New-PostgisSqlFromTemplate (Join-Path $postgisSqlDir "legacy_minimal.sql.in") (Join-Path $postgisSqlDir "legacy_minimal.sql") $includeDirs $modulePath $false $true + New-PostgisSqlFromTemplate (Join-Path $postgisSqlDir "legacy.sql.in") (Join-Path $postgisSqlDir "legacy.sql") $includeDirs $modulePath $false $true + New-PostgisSqlFromTemplate (Join-Path $postgisSqlDir "legacy_gist.sql.in") (Join-Path $postgisSqlDir "legacy_gist.sql") $includeDirs $modulePath $false $true + + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_upgrade.pl"), (Join-Path $postgisSqlDir "postgis.sql")) (Join-Path $postgisSqlDir "postgis_upgrade.sql.in") + Join-TextFiles (Join-Path $postgisSqlDir "postgis_upgrade.sql") @( + (Join-Path $postgisSqlDir "common_before_upgrade.sql"), + (Join-Path $postgisSqlDir "postgis_before_upgrade.sql"), + (Join-Path $postgisSqlDir "postgis_upgrade.sql.in"), + (Join-Path $postgisSqlDir "postgis_after_upgrade.sql"), + (Join-Path $postgisSqlDir "common_after_upgrade.sql") + ) "BEGIN;" "COMMIT;" + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_uninstall.pl"), (Join-Path $postgisSqlDir "postgis.sql"), "180") (Join-Path $postgisSqlDir "uninstall_postgis.sql") + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_uninstall.pl"), (Join-Path $postgisSqlDir "legacy.sql"), "180") (Join-Path $postgisSqlDir "uninstall_legacy.sql") + + New-PostgisSqlFromTemplate (Join-Path $postgisSqlDir "postgis.sql.in") (Join-Path $extensionSqlDir "postgis_for_extension.sql") $includeDirs $modulePath $true $false + $spatialRefExtension = Join-Path $extensionSqlDir "spatial_ref_sys.sql" + $spatialRefText = (Get-Content -Raw -Path (Join-Path $PostgisDir "spatial_ref_sys.sql")).Replace("BEGIN;", "").Replace("COMMIT;", "") + Set-Content -Path $spatialRefExtension -Encoding UTF8 -Value $spatialRefText + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_spatial_ref_sys_config_dump.pl"), (Join-Path $PostgisDir "spatial_ref_sys.sql")) (Join-Path $extensionSqlDir "spatial_ref_sys_config_dump.sql") + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_upgrade.pl"), (Join-Path $extensionSqlDir "postgis_for_extension.sql")) (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql.in") + Join-TextFiles (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql") @( + (Join-Path $postgisSqlDir "common_before_upgrade.sql"), + (Join-Path $postgisSqlDir "postgis_before_upgrade.sql"), + (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql.in"), + (Join-Path $postgisSqlDir "postgis_after_upgrade.sql"), + (Join-Path $postgisSqlDir "common_after_upgrade.sql") + ) + $upgradeForExtensionText = (Get-Content -Raw -Path (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql")).Replace("BEGIN;", "").Replace("COMMIT;", "") + Set-Content -Path (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql") -Encoding UTF8 -Value $upgradeForExtensionText + Convert-PostgisExtensionDropGuards (Join-Path $extensionSqlDir "postgis_upgrade_for_extension.sql") (Join-Path $extensionSqlDir "postgis_upgrade.sql") + + $rasterDir = Join-Path $PostgisDir "raster/rt_pg" + $rasterIncludeDirs = @($postgisSqlDir, $rasterDir) + $rasterBaseSql = Join-Path $rasterDir "rtpostgis.sql" + New-PostgisSqlFromTemplate (Join-Path $rasterDir "rtpostgis.sql.in") $rasterBaseSql $rasterIncludeDirs '$libdir/rtpostgis-3' $false $true + $rasterDropSql = @() + foreach ($name in @("rtpostgis_upgrade_cleanup", "rtpostgis_drop")) { + $output = Join-Path $rasterDir "$name.sql" + New-PostgisSqlFromTemplate (Join-Path $rasterDir "$name.sql.in") $output $rasterIncludeDirs '$libdir/rtpostgis-3' $false $true + $rasterDropSql += $output + } + $rasterUninstallSql = Join-Path $rasterDir "uninstall_rtpostgis.sql" + Invoke-PerlToFile @((Join-Path $PostgisDir "utils/create_uninstall.pl"), $rasterBaseSql, "180") $rasterUninstallSql + $rasterDropSql += $rasterUninstallSql + New-PostgisRasterUnpackageSql $PostgisDir $extensionSqlDir $rasterDropSql + + $installSql = Join-Path $extensionSqlDir "postgis--$($Version.Version).sql" + Join-TextFiles $installSql @( + (Join-Path $extensionSqlDir "postgis_for_extension.sql"), + (Join-Path $extensionSqlDir "spatial_ref_sys_config_dump.sql"), + (Join-Path $extensionSqlDir "spatial_ref_sys.sql") + ) '\echo Use "CREATE EXTENSION postgis" to load this file. \quit' + + $anyUpgradeSql = Join-Path $extensionSqlDir "postgis--ANY--$($Version.Version).sql" + Join-TextFiles $anyUpgradeSql @( + (Join-Path $PostgisDir "extensions/postgis_extension_helper.sql"), + (Join-Path $extensionSqlDir "raster_unpackage.sql"), + (Join-Path $extensionSqlDir "postgis_upgrade.sql"), + (Join-Path $extensionSqlDir "spatial_ref_sys.sql"), + (Join-Path $extensionSqlDir "spatial_ref_sys_config_dump.sql"), + (Join-Path $PostgisDir "extensions/postgis_extension_helper_uninstall.sql") + ) '\echo Use "CREATE EXTENSION postgis" to load this file. \quit' + + $templatedSql = Join-Path $extensionSqlDir "postgis--TEMPLATED--TO--ANY.sql" + Set-Content -Path $templatedSql -Encoding UTF8 -Value @" +-- Just tag extension postgis version as "ANY" +-- Installed by postgis $($Version.Version) +-- Built on $((Get-Date).ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")) +"@ + Copy-Item -Force $templatedSql (Join-Path $extensionSqlDir "postgis--$($Version.Version)--ANY.sql") + Set-Content -Path (Join-Path $extensionSqlDir "postgis--unpackaged.sql") -Encoding UTF8 -Value "-- Nothing to do here" + $unpackagedVersionSql = Join-Path $extensionSqlDir "postgis--unpackaged--$($Version.Version).sql" + Invoke-PerlFromInputFile $installSql @((Join-Path $PostgisDir "utils/create_unpackaged.pl"), "postgis") $unpackagedVersionSql + Add-Content -Path $unpackagedVersionSql -Encoding UTF8 -Value (Get-Content -Raw -Path $anyUpgradeSql) +} + +function Patch-WindowsPostgisFlatgeobufSource([string]$SourceDir) { + $geometryReader = Join-Path $SourceDir "geometryreader.cpp" + $text = Get-Content -Raw -Path $geometryReader + $pointLiteral = "pt = (POINT4D) { x, y, z, m };" + $pointAssignments = "pt.x = x;`n`tpt.y = y;`n`tpt.z = z;`n`tpt.m = m;" + $arrayLiteral = "pt = (POINT4D) { xv, yv, zv, mv };" + $arrayAssignments = "pt.x = xv;`n`t`tpt.y = yv;`n`t`tpt.z = zv;`n`t`tpt.m = mv;" + foreach ($expected in @($pointLiteral, $arrayLiteral)) { + if (-not $text.Contains($expected)) { + Fail "PostGIS FlatGeobuf geometryreader.cpp is missing expected MSVC patch anchor: $expected" + } + } + $text = $text.Replace($pointLiteral, $pointAssignments) + $text = $text.Replace($arrayLiteral, $arrayAssignments) + Set-Content -Path $geometryReader -Encoding UTF8 -Value $text +} + +function Build-WindowsPostgisFlatgeobufLibrary([string]$PostgisDir) { + $prefix = Join-Path $PostgisDependencyPrefix "flatgeobuf" + $archive = Join-Path $prefix "lib/flatgeobuf.lib" + if (Test-Path $archive) { + return $archive + } + $sourceDir = Join-Path $PostgisDir "deps/flatgeobuf" + Patch-WindowsPostgisFlatgeobufSource $sourceDir + $buildRoot = Join-Path $WorkRoot "postgis-flatgeobuf-windows-build" + Remove-Item -Recurse -Force $buildRoot, $prefix -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $buildRoot, (Split-Path -Parent $archive) | Out-Null + $compatHeader = Join-Path $buildRoot "oliphaunt_flatgeobuf_windows_compat.h" + Set-Content -Path $compatHeader -Encoding UTF8 -Value @" +#ifdef _MSC_VER +#ifndef __attribute__ +#define __attribute__(x) +#endif +#ifndef PROJ_DLL +#define PROJ_DLL +#endif +#endif +"@ + $includeArgs = @( + "/I$(Join-Path $PostgisDir "liblwgeom")", + "/I$sourceDir", + "/I$(Join-Path $sourceDir "include")", + "/I$(Join-Path $PostgisDependencyPrefix "proj/include")" + ) + $objects = New-Object System.Collections.Generic.List[string] + foreach ($source in @("flatgeobuf_c.cpp", "geometrywriter.cpp", "geometryreader.cpp", "packedrtree.cpp")) { + $sourcePath = Join-Path $sourceDir $source + $object = Join-Path $buildRoot ([System.IO.Path]::GetFileNameWithoutExtension($source) + ".obj") + Invoke-Logged "postgis-flatgeobuf-$([System.IO.Path]::GetFileNameWithoutExtension($source)).log" { + cl.exe /nologo /O2 /MD /EHsc /D_CRT_SECURE_NO_WARNINGS /Dflatbuffers=postgis_flatbuffers ` + "/FI$compatHeader" ` + @includeArgs ` + /c $sourcePath "/Fo$object" + } + $objects.Add($object) + } + Invoke-Logged "postgis-flatgeobuf-lib.log" { lib.exe /nologo "/OUT:$archive" @objects } + if (-not (Test-Path $archive)) { + Fail "PostGIS FlatGeobuf Windows build did not produce $archive" + } + $archive +} + +function Copy-WindowsPostgisRuntimeData([string]$PostgisDir) { + $projDb = Join-Path $PostgisDependencyPrefix "proj/share/proj/proj.db" + if (-not (Test-Path $projDb)) { + Fail "PostGIS PROJ dependency did not produce $projDb" + } + $destination = Join-Path $PostgisDir "share/proj" + New-Item -ItemType Directory -Force -Path $destination | Out-Null + Copy-Item -Force $projDb (Join-Path $destination "proj.db") +} + +function Ensure-WindowsPostgisCommentsSql([string]$PostgisDir) { + $comments = Join-Path $PostgisDir "doc/postgis_comments.sql" + if (Test-Path $comments) { + return + } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $comments) | Out-Null + Set-Content -Path $comments -Encoding UTF8 -Value "-- PostGIS SQL comments are optional and are not generated by the Windows native producer." +} + +function Patch-WindowsPostgisSource([string]$PostgisDir) { + $compat = Join-Path $PostgisDir "oliphaunt_postgis_windows_compat.h" + Set-Content -Path $compat -Encoding UTF8 -Value @" +#ifndef OLIPHAUNT_POSTGIS_WINDOWS_COMPAT_H +#define OLIPHAUNT_POSTGIS_WINDOWS_COMPAT_H + +#ifdef _MSC_VER +#ifndef __attribute__ +#define __attribute__(x) +#endif +#ifndef __attribute +#define __attribute(x) +#endif +#ifndef FALLTHROUGH +#define FALLTHROUGH ((void)0) +#endif +#ifndef PROJ_DLL +#define PROJ_DLL +#endif +#ifndef strcasecmp +#define strcasecmp _stricmp +#endif +#ifndef strncasecmp +#define strncasecmp _strnicmp +#endif +#endif + +#endif +"@ + + $declarationPattern = "(?m)^\s*(?!(?:extern\s+)?PGDLLEXPORT\s+)(?:extern\s+)?Datum\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(PG_FUNCTION_ARGS\);\r?$" + $patchedDeclarationCount = 0 + foreach ($subdir in @("postgis", "libpgcommon", "liblwgeom")) { + $root = Join-Path $PostgisDir $subdir + foreach ($file in Get-ChildItem -Path $root -Recurse -File | Where-Object { $_.Extension -in @(".c", ".h") }) { + $text = Get-Content -Raw -Path $file.FullName + $patchedDeclarationCount += [regex]::Matches($text, $declarationPattern).Count + $patched = [regex]::Replace( + $text, + $declarationPattern, + 'extern PGDLLEXPORT Datum $1(PG_FUNCTION_ARGS);' + ) + if ($patched -ne $text) { + Set-Content -Path $file.FullName -Encoding UTF8 -Value $patched + } + } + } + + if ($patchedDeclarationCount -lt 50) { + Fail "PostGIS Windows source patch normalized only $patchedDeclarationCount SQL-callable declarations" + } + + $legacySource = Join-Path $PostgisDir "postgis/postgis_legacy.c" + $legacyText = Get-Content -Raw -Path $legacySource + $legacyDeclarationPattern = "(?m)^([ \t]*)Datum[ \t]+funcname[ \t]*\(PG_FUNCTION_ARGS\);[ \t]*\\\r?$" + $legacyPatched = [regex]::Replace( + $legacyText, + $legacyDeclarationPattern, + '$1extern PGDLLEXPORT Datum funcname(PG_FUNCTION_ARGS); \' + ) + if ($legacyPatched -eq $legacyText) { + Fail "PostGIS Windows source patch did not export POSTGIS_DEPRECATE declarations" + } + Set-Content -Path $legacySource -Encoding UTF8 -Value $legacyPatched + + $requiredDeclarations = @( + @{ + Path = "postgis/lwgeom_accum.c" + Functions = @( + "pgis_geometry_accum_transfn", + "pgis_geometry_collect_finalfn", + "pgis_geometry_polygonize_finalfn", + "pgis_geometry_makeline_finalfn", + "pgis_geometry_clusterintersecting_finalfn", + "pgis_geometry_clusterwithin_finalfn" + ) + }, + @{ + Path = "postgis/lwgeom_union.c" + Functions = @( + "pgis_geometry_union_parallel_transfn", + "pgis_geometry_union_parallel_combinefn", + "pgis_geometry_union_parallel_serialfn", + "pgis_geometry_union_parallel_deserialfn", + "pgis_geometry_union_parallel_finalfn" + ) + }, + @{ + Path = "postgis/lwgeom_spheroid.c" + Functions = @( + "ellipsoid_in", + "ellipsoid_out", + "LWGEOM_length2d_ellipsoid", + "LWGEOM_length_ellipsoid_linestring", + "LWGEOM_distance_ellipsoid", + "LWGEOM_distance_sphere", + "geometry_distance_spheroid" + ) + } + ) + foreach ($required in $requiredDeclarations) { + $text = Get-Content -Raw -Path (Join-Path $PostgisDir $required.Path) + foreach ($functionName in $required.Functions) { + $expected = "extern PGDLLEXPORT Datum $functionName(PG_FUNCTION_ARGS);" + if (-not $text.Contains($expected)) { + Fail "PostGIS Windows source patch did not export $functionName in $($required.Path)" + } + } + } +} + +function Write-PostgisMesonModule([string]$PostgisDir, [pscustomobject]$Version, [string]$FlatgeobufLib) { + $jsonLib = First-File (Join-Path $PostgisDependencyPrefix "json-c") @("json-c.lib", "json-c-static.lib") + $sqliteLib = First-File (Join-Path $PostgisDependencyPrefix "sqlite") @("sqlite3.lib", "libsqlite3.lib") + $geosCLib = First-File (Join-Path $PostgisDependencyPrefix "geos") @("geos_c.lib") + $geosLib = First-File (Join-Path $PostgisDependencyPrefix "geos") @("geos.lib") + $libxml2Lib = First-File (Join-Path $PostgisDependencyPrefix "libxml2") @("libxml2s.lib", "libxml2.lib", "xml2.lib") + $projLib = First-File (Join-Path $PostgisDependencyPrefix "proj") @("proj.lib", "libproj.lib") + + $sources = @( + "postgis/postgis_module.c", + "postgis/lwgeom_accum.c", + "postgis/lwgeom_union.c", + "postgis/lwgeom_spheroid.c", + "postgis/lwgeom_ogc.c", + "postgis/lwgeom_functions_analytic.c", + "postgis/lwgeom_functions_basic.c", + "postgis/lwgeom_inout.c", + "postgis/lwgeom_btree.c", + "postgis/lwgeom_box.c", + "postgis/lwgeom_box3d.c", + "postgis/lwgeom_geos.c", + "postgis/lwgeom_geos_predicates.c", + "postgis/lwgeom_geos_prepared.c", + "postgis/lwgeom_geos_clean.c", + "postgis/lwgeom_geos_relatematch.c", + "postgis/lwgeom_generate_grid.c", + "postgis/lwgeom_export.c", + "postgis/lwgeom_in_gml.c", + "postgis/lwgeom_in_kml.c", + "postgis/lwgeom_in_marc21.c", + "postgis/lwgeom_out_marc21.c", + "postgis/lwgeom_in_geohash.c", + "postgis/lwgeom_in_geojson.c", + "postgis/lwgeom_in_encoded_polyline.c", + "postgis/lwgeom_triggers.c", + "postgis/lwgeom_dump.c", + "postgis/lwgeom_dumppoints.c", + "postgis/lwgeom_functions_lrs.c", + "postgis/lwgeom_functions_temporal.c", + "postgis/lwgeom_rectree.c", + "postgis/lwgeom_itree.c", + "postgis/lwgeom_sqlmm.c", + "postgis/lwgeom_transform.c", + "postgis/lwgeom_window.c", + "postgis/gserialized_typmod.c", + "postgis/gserialized_gist_2d.c", + "postgis/gserialized_gist_nd.c", + "postgis/gserialized_supportfn.c", + "postgis/gserialized_spgist_2d.c", + "postgis/gserialized_spgist_3d.c", + "postgis/gserialized_spgist_nd.c", + "postgis/brin_2d.c", + "postgis/brin_nd.c", + "postgis/brin_common.c", + "postgis/gserialized_estimate.c", + "postgis/geography_inout.c", + "postgis/geography_btree.c", + "postgis/geography_centroid.c", + "postgis/geography_measurement.c", + "postgis/geography_measurement_trees.c", + "postgis/geometry_inout.c", + "postgis/postgis_libprotobuf.c", + "postgis/mvt.c", + "postgis/lwgeom_out_mvt.c", + "postgis/geobuf.c", + "postgis/lwgeom_out_geobuf.c", + "postgis/lwgeom_out_geojson.c", + "postgis/flatgeobuf.c", + "postgis/lwgeom_in_flatgeobuf.c", + "postgis/lwgeom_out_flatgeobuf.c", + "postgis/lwgeom_remove_irrelevant_points_for_view.c", + "postgis/lwgeom_remove_small_parts.c", + "postgis/postgis_legacy.c", + "libpgcommon/gserialized_gist.c", + "libpgcommon/lwgeom_transform.c", + "libpgcommon/lwgeom_cache.c", + "libpgcommon/lwgeom_pg.c", + "libpgcommon/shared_gserialized.c", + "liblwgeom/stringbuffer.c", + "liblwgeom/optionlist.c", + "liblwgeom/stringlist.c", + "liblwgeom/bytebuffer.c", + "liblwgeom/measures.c", + "liblwgeom/measures3d.c", + "liblwgeom/ptarray.c", + "liblwgeom/lookup3.c", + "liblwgeom/lwgeom_api.c", + "liblwgeom/lwgeom.c", + "liblwgeom/lwpoint.c", + "liblwgeom/lwline.c", + "liblwgeom/lwpoly.c", + "liblwgeom/lwtriangle.c", + "liblwgeom/lwmpoint.c", + "liblwgeom/lwmline.c", + "liblwgeom/lwmpoly.c", + "liblwgeom/lwboundingcircle.c", + "liblwgeom/lwcollection.c", + "liblwgeom/lwcircstring.c", + "liblwgeom/lwcompound.c", + "liblwgeom/lwcurvepoly.c", + "liblwgeom/lwmcurve.c", + "liblwgeom/lwmsurface.c", + "liblwgeom/lwpsurface.c", + "liblwgeom/lwtin.c", + "liblwgeom/lwout_wkb.c", + "liblwgeom/lwin_geojson.c", + "liblwgeom/lwin_wkb.c", + "liblwgeom/lwin_twkb.c", + "liblwgeom/lwiterator.c", + "liblwgeom/lwgeom_median.c", + "liblwgeom/lwout_wkt.c", + "liblwgeom/lwout_twkb.c", + "liblwgeom/lwin_wkt_parse.c", + "liblwgeom/lwin_wkt_lex.c", + "liblwgeom/lwin_wkt.c", + "liblwgeom/lwin_encoded_polyline.c", + "liblwgeom/lwutil.c", + "liblwgeom/lwhomogenize.c", + "liblwgeom/intervaltree.c", + "liblwgeom/lwalgorithm.c", + "liblwgeom/lwstroke.c", + "liblwgeom/lwlinearreferencing.c", + "liblwgeom/lwprint.c", + "liblwgeom/gbox.c", + "liblwgeom/gserialized.c", + "liblwgeom/gserialized1.c", + "liblwgeom/gserialized2.c", + "liblwgeom/lwgeodetic.c", + "liblwgeom/lwgeodetic_measures.c", + "liblwgeom/lwgeodetic_tree.c", + "liblwgeom/lwrandom.c", + "liblwgeom/lwtree.c", + "liblwgeom/lwout_gml.c", + "liblwgeom/lwout_kml.c", + "liblwgeom/lwout_geojson.c", + "liblwgeom/lwout_svg.c", + "liblwgeom/lwout_x3d.c", + "liblwgeom/lwout_encoded_polyline.c", + "liblwgeom/lwgeom_debug.c", + "liblwgeom/lwgeom_geos.c", + "liblwgeom/lwgeom_geos_clean.c", + "liblwgeom/lwgeom_geos_cluster.c", + "liblwgeom/lwgeom_geos_node.c", + "liblwgeom/lwgeom_geos_split.c", + "liblwgeom/topo/lwgeom_topo.c", + "liblwgeom/topo/lwgeom_topo_polygonizer.c", + "liblwgeom/topo/lwt_edgeend.c", + "liblwgeom/topo/lwt_edgeend_star.c", + "liblwgeom/topo/lwt_node_edges.c", + "liblwgeom/lwgeom_transform.c", + "liblwgeom/lwgeom_wrapx.c", + "liblwgeom/lwunionfind.c", + "liblwgeom/effectivearea.c", + "liblwgeom/lwchaikins.c", + "liblwgeom/lwmval.c", + "liblwgeom/lwkmeans.c", + "liblwgeom/varint.c", + "liblwgeom/lwgeom_remove_irrelevant_points_for_view.c", + "liblwgeom/lwspheroid.c", + "deps/ryu/d2s.c" + ) + $extensionSqlFiles = @( + "extensions/postgis/postgis.control", + "extensions/postgis/sql/postgis--$($Version.Version).sql", + "extensions/postgis/sql/postgis--ANY--$($Version.Version).sql", + "extensions/postgis/sql/postgis--$($Version.Version)--ANY.sql", + "extensions/postgis/sql/postgis--TEMPLATED--TO--ANY.sql", + "extensions/postgis/sql/postgis--unpackaged.sql", + "extensions/postgis/sql/postgis--unpackaged--$($Version.Version).sql" + ) + $contribDataFiles = @( + "postgis/legacy.sql", + "postgis/legacy_gist.sql", + "postgis/legacy_minimal.sql", + "postgis/postgis.sql", + "postgis/postgis_upgrade.sql", + "spatial_ref_sys.sql", + "postgis/uninstall_legacy.sql", + "postgis/uninstall_postgis.sql", + "doc/postgis_comments.sql" + ) + $includeArgs = @( + "/I$(Meson-Path $PostgisDir)", + # liblwgeom has headers with the same basename as the PostgreSQL module. + # Source-local includes still win for postgis/*.c; this order keeps + # liblwgeom/topo/*.c from accidentally including server-side headers. + "/I$(Meson-Path (Join-Path $PostgisDir "liblwgeom"))", + "/I$(Meson-Path (Join-Path $PostgisDir "postgis"))", + "/I$(Meson-Path (Join-Path $PostgisDir "libpgcommon"))", + "/I$(Meson-Path (Join-Path $PostgisDir "deps"))", + "/I$(Meson-Path (Join-Path $PostgisDir "deps/flatgeobuf"))", + "/I$(Meson-Path (Join-Path $PostgisDir "deps/flatgeobuf/include"))", + "/I$(Meson-Path (Join-Path $PostgisDir "deps/ryu"))", + "/I$(Meson-Path (Join-Path $PostgisDependencyPrefix "geos/include"))", + "/I$(Meson-Path (Join-Path $PostgisDependencyPrefix "proj/include"))", + "/I$(Meson-Path (Join-Path $PostgisDependencyPrefix "json-c/include"))", + "/I$(Meson-Path (Join-Path $PostgisDependencyPrefix "json-c/include/json-c"))", + "/I$(Meson-Path (Join-Path $PostgisDependencyPrefix "libxml2/include/libxml2"))" + ) + $cArgs = @( + "/D_CRT_SECURE_NO_WARNINGS", + "/D_USE_MATH_DEFINES", + "/DLIBXML_STATIC", + "/DRYU_NO_TRAILING_ZEROS", + "/FI$(Meson-Path (Join-Path $PostgisDir "oliphaunt_postgis_windows_compat.h"))" + ) + $includeArgs + $linkArgs = @( + (Meson-Path $FlatgeobufLib), + (Meson-Path $geosCLib), + (Meson-Path $geosLib), + (Meson-Path $projLib), + (Meson-Path $sqliteLib), + (Meson-Path $jsonLib), + (Meson-Path $libxml2Lib), + "ws2_32.lib", + "bcrypt.lib", + "advapi32.lib", + "shell32.lib", + "user32.lib" + ) + + $sourceList = Meson-List $sources + $cArgList = Meson-List $cArgs " " + $linkArgList = Meson-List $linkArgs " " + $extensionDataList = Meson-List $extensionSqlFiles + $contribDataList = Meson-List $contribDataFiles + $meson = @" +postgis = shared_module( + 'postgis-3', + files( +$sourceList, + ), + c_pch: pch_postgres_h, + kwargs: contrib_mod_args + { + 'c_args': [ +$cArgList + ], + 'link_args': [ +$linkArgList + ], + }, +) +contrib_targets += postgis + +install_data( +$extensionDataList, + kwargs: contrib_data_args, +) + +install_data( +$contribDataList, + install_dir: dir_data / 'contrib' / 'postgis-$($Version.MajorMinor)', +) + +install_data( + 'share/proj/proj.db', + install_dir: dir_data / 'proj', +) +"@ + Set-Content -Path (Join-Path $PostgisDir "meson.build") -Encoding UTF8 -Value $meson + Append-OliphauntContribSubdir "postgis" +} + +function Add-PostgisMesonProducer { + if (-not (NativeExtension-Selected "postgis")) { + return + } + Build-WindowsPostgisDependencies + $sourceDir = External-Checkout "postgis" + if (-not (Test-Path (Join-Path $sourceDir "Version.config"))) { + Fail "missing PostGIS checkout for Windows extension artifacts: $sourceDir" + } + $destination = Join-Path $OliphauntContribDir "postgis" + Copy-SourceTree $sourceDir $destination + $version = Initialize-WindowsPostgisGeneratedSource $destination $sourceDir + Build-WindowsPostgisSql $destination $version + Ensure-WindowsPostgisCommentsSql $destination + Patch-WindowsPostgisSource $destination + Copy-WindowsPostgisRuntimeData $destination + $flatgeobufLib = Build-WindowsPostgisFlatgeobufLibrary $destination + Write-PostgisMesonModule $destination $version $flatgeobufLib +} + +function Add-PgcryptoMesonProducer { + if (-not (NativeExtension-Selected "pgcrypto")) { + return + } + Build-WindowsOpenSslDependency + $opensslInclude = Meson-Path (Join-Path $OpenSslDependencyPrefix "include") + $libCrypto = Meson-Path (Join-Path $OpenSslDependencyPrefix "lib/libcrypto.lib") + Write-OliphauntMesonModule ` + "pgcrypto" ` + "pgcrypto" ` + @( + "../../pgcrypto/crypt-blowfish.c", + "../../pgcrypto/crypt-des.c", + "../../pgcrypto/crypt-gensalt.c", + "../../pgcrypto/crypt-md5.c", + "../../pgcrypto/crypt-sha.c", + "../../pgcrypto/mbuf.c", + "../../pgcrypto/openssl.c", + "../../pgcrypto/pgcrypto.c", + "../../pgcrypto/pgp-armor.c", + "../../pgcrypto/pgp-cfb.c", + "../../pgcrypto/pgp-compress.c", + "../../pgcrypto/pgp-decrypt.c", + "../../pgcrypto/pgp-encrypt.c", + "../../pgcrypto/pgp-info.c", + "../../pgcrypto/pgp-mpi.c", + "../../pgcrypto/pgp-mpi-openssl.c", + "../../pgcrypto/pgp-pgsql.c", + "../../pgcrypto/pgp-pubdec.c", + "../../pgcrypto/pgp-pubenc.c", + "../../pgcrypto/pgp-pubkey.c", + "../../pgcrypto/pgp-s2k.c", + "../../pgcrypto/pgp.c", + "../../pgcrypto/px-crypt.c", + "../../pgcrypto/px-hmac.c", + "../../pgcrypto/px.c" + ) ` + @( + "../../pgcrypto/pgcrypto--1.0--1.1.sql", + "../../pgcrypto/pgcrypto--1.1--1.2.sql", + "../../pgcrypto/pgcrypto--1.2--1.3.sql", + "../../pgcrypto/pgcrypto--1.3.sql", + "../../pgcrypto/pgcrypto--1.3--1.4.sql", + "../../pgcrypto/pgcrypto.control" + ) ` + @("/I$opensslInclude") ` + @($libCrypto, "crypt32.lib", "advapi32.lib", "bcrypt.lib", "ws2_32.lib", "user32.lib") +} + +function Add-UuidOsspMesonProducer { + if (-not (NativeExtension-Selected "uuid-ossp")) { + return + } + $destination = Join-Path $OliphauntContribDir "uuid_ossp" + New-Item -ItemType Directory -Force -Path $destination | Out-Null + Copy-Item -Force (Join-Path $PortableUuidDir "portable_uuid.c") (Join-Path $destination "portable_uuid.c") + $portableUuidInclude = Meson-Path $PortableUuidIncludeDir + Write-OliphauntMesonModule ` + "uuid_ossp" ` + "uuid-ossp" ` + @("../../uuid-ossp/uuid-ossp.c", "portable_uuid.c") ` + @( + "../../uuid-ossp/uuid-ossp--1.0--1.1.sql", + "../../uuid-ossp/uuid-ossp--1.1.sql", + "../../uuid-ossp/uuid-ossp.control" + ) ` + @("/I$portableUuidInclude", "/DHAVE_UUID_E2FS=1", "/DHAVE_UUID_UUID_H=1") +} + +function Patch-PgTextsearchWindowsSource([string]$ExtensionDir) { + $compat = Join-Path $ExtensionDir "src/oliphaunt_windows_compat.h" + Set-Content -Path $compat -Encoding UTF8 -Value @" +#ifdef _MSC_VER +#ifndef __attribute__ +#define __attribute__(x) +#endif +#endif +"@ + Set-Content -Path (Join-Path $ExtensionDir "src/unistd.h") -Encoding UTF8 -Value @" +#ifndef OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H +#define OLIPHAUNT_PG_TEXTSEARCH_WINDOWS_UNISTD_H +#endif +"@ + $segmentHeader = Join-Path $ExtensionDir "src/segment/segment.h" + $text = Get-Content -Raw -Path $segmentHeader + $text = $text.Replace("} __attribute__((aligned(4))) TpDictEntry;", "} TpDictEntry;") + $text = $text.Replace( + "typedef struct TpSegmentPosting", + "#ifdef _MSC_VER`n#pragma pack(push, 1)`n#endif`ntypedef struct TpSegmentPosting" + ) + $text = $text.Replace( + "} __attribute__((packed)) TpSegmentPosting;", + "} TpSegmentPosting;`n#ifdef _MSC_VER`n#pragma pack(pop)`n#endif" + ) + $text = $text.Replace( + "typedef struct TpSkipEntry", + "#ifdef _MSC_VER`n#pragma pack(push, 1)`n#endif`ntypedef struct TpSkipEntry" + ) + $text = $text.Replace( + "} __attribute__((packed)) TpSkipEntry;", + "} TpSkipEntry;`n#ifdef _MSC_VER`n#pragma pack(pop)`n#endif" + ) + $text = $text.Replace( + "typedef struct TpCtidMapEntry", + "#ifdef _MSC_VER`n#pragma pack(push, 1)`n#endif`ntypedef struct TpCtidMapEntry" + ) + $text = $text.Replace( + "} __attribute__((packed)) TpCtidMapEntry;", + "} TpCtidMapEntry;`n#ifdef _MSC_VER`n#pragma pack(pop)`n#endif" + ) + Set-Content -Path $segmentHeader -Encoding UTF8 -Value $text + + $amHeader = Join-Path $ExtensionDir "src/am/am.h" + $text = Get-Content -Raw -Path $amHeader + $original = "Datum tp_handler(PG_FUNCTION_ARGS);" + $replacement = "extern PGDLLEXPORT Datum tp_handler(PG_FUNCTION_ARGS);" + if (-not $text.Contains($original)) { + Fail "pg_textsearch am.h is missing expected tp_handler declaration" + } + $text = $text.Replace($original, $replacement) + Set-Content -Path $amHeader -Encoding UTF8 -Value $text + + $vectorHeader = Join-Path $ExtensionDir "src/types/vector.h" + $text = Get-Content -Raw -Path $vectorHeader + foreach ($functionName in @("tpvector_in", "tpvector_out", "tpvector_recv", "tpvector_send", "to_tpvector", "tpvector_eq")) { + $original = "Datum $($functionName)(PG_FUNCTION_ARGS);" + $replacement = "extern PGDLLEXPORT Datum $($functionName)(PG_FUNCTION_ARGS);" + if (-not $text.Contains($original)) { + Fail "pg_textsearch vector.h is missing expected $functionName declaration" + } + $text = $text.Replace($original, $replacement) + } + Set-Content -Path $vectorHeader -Encoding UTF8 -Value $text + + $queryHeader = Join-Path $ExtensionDir "src/types/query.h" + $text = Get-Content -Raw -Path $queryHeader + foreach ($functionName in @( + "tpquery_in", + "tpquery_out", + "tpquery_recv", + "tpquery_send", + "to_tpquery_text", + "to_tpquery_text_index", + "bm25_text_bm25query_score", + "bm25_text_text_score", + "tpquery_eq" + )) { + $original = "Datum $($functionName)(PG_FUNCTION_ARGS);" + $replacement = "extern PGDLLEXPORT Datum $($functionName)(PG_FUNCTION_ARGS);" + if (-not $text.Contains($original)) { + Fail "pg_textsearch query.h is missing expected $functionName declaration" + } + $text = $text.Replace($original, $replacement) + } + Set-Content -Path $queryHeader -Encoding UTF8 -Value $text +} + +function Patch-PgUuidv7WindowsSource([string]$ExtensionDir) { + $source = Join-Path $ExtensionDir "pg_uuidv7.c" + $text = Get-Content -Raw -Path $source + $epochDefine = "#define EPOCH_DIFF_USECS ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * USECS_PER_DAY)" + $compat = @" +#define EPOCH_DIFF_USECS ((POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * USECS_PER_DAY) + +#ifdef _WIN32 +#ifndef CLOCK_REALTIME +#define CLOCK_REALTIME 0 +#endif +static int +oliphaunt_pg_uuidv7_clock_gettime(int clock_id, struct timespec *ts) +{ + TimestampTz unix_usecs; + + if (clock_id != CLOCK_REALTIME || ts == NULL) + return -1; + + unix_usecs = GetCurrentTimestamp() + EPOCH_DIFF_USECS; + ts->tv_sec = (time_t) (unix_usecs / USECS_PER_SEC); + ts->tv_nsec = (long) ((unix_usecs % USECS_PER_SEC) * 1000); + return 0; +} +#define clock_gettime oliphaunt_pg_uuidv7_clock_gettime +#endif +"@ + if (-not $text.Contains($epochDefine)) { + Fail "pg_uuidv7.c is missing expected epoch define" + } + $text = $text.Replace($epochDefine, $compat) + Set-Content -Path $source -Encoding UTF8 -Value $text +} + +function Add-ExternalPgxsMesonProducer( + [string]$SqlName, + [string]$CheckoutName, + [string]$Subdir, + [string]$ModuleName, + [string[]]$Sources, + [string[]]$DataFiles, + [string[]]$CArgs = @(), + [string[]]$LocalIncludeDirs = @() +) { + if (-not (NativeExtension-Selected $SqlName)) { + return + } + $destination = Join-Path $OliphauntContribDir $Subdir + Copy-SourceTree (External-Checkout $CheckoutName) $destination + if ($SqlName -eq "pg_uuidv7") { + Patch-PgUuidv7WindowsSource $destination + } + if ($SqlName -eq "pg_textsearch") { + Patch-PgTextsearchWindowsSource $destination + $compatHeader = Meson-Path (Join-Path $destination "src/oliphaunt_windows_compat.h") + $CArgs = @($CArgs) + @("/FI$compatHeader") + } + if ($SqlName -eq "vector") { + Copy-Item -Force (Join-Path $destination "sql/vector.sql") (Join-Path $destination "sql/vector--0.8.2.sql") + } + Write-OliphauntMesonModule $Subdir $ModuleName $Sources $DataFiles $CArgs @() $LocalIncludeDirs +} + +function Add-ExternalPgxsMesonProducers { + Add-ExternalPgxsMesonProducer ` + "pg_hashids" "pg_hashids" "pg_hashids" "pg_hashids" ` + @("pg_hashids.c", "hashids.c") ` + @( + "pg_hashids--1.3.sql", + "pg_hashids--1.2.1--1.3.sql", + "pg_hashids--1.2--1.3.sql", + "pg_hashids--1.1--1.2.sql", + "pg_hashids--1.0--1.1.sql", + "pg_hashids.control" + ) + Add-ExternalPgxsMesonProducer ` + "pg_ivm" "pg_ivm" "pg_ivm" "pg_ivm" ` + @("createas.c", "matview.c", "pg_ivm.c", "ruleutils.c", "subselect.c") ` + @( + "pg_ivm--1.0.sql", + "pg_ivm--1.0--1.1.sql", + "pg_ivm--1.1--1.2.sql", + "pg_ivm--1.2--1.3.sql", + "pg_ivm--1.3--1.4.sql", + "pg_ivm--1.4--1.5.sql", + "pg_ivm--1.5--1.6.sql", + "pg_ivm--1.6--1.7.sql", + "pg_ivm--1.7--1.8.sql", + "pg_ivm--1.8--1.9.sql", + "pg_ivm--1.9--1.10.sql", + "pg_ivm--1.10.sql", + "pg_ivm--1.10--1.11.sql", + "pg_ivm--1.11--1.12.sql", + "pg_ivm--1.12--1.13.sql", + "pg_ivm.control" + ) + Add-ExternalPgxsMesonProducer ` + "pg_uuidv7" "pg_uuidv7" "pg_uuidv7" "pg_uuidv7" ` + @("pg_uuidv7.c") ` + @( + "sql/pg_uuidv7--1.7.sql", + "pg_uuidv7.control" + ) + Add-ExternalPgxsMesonProducer ` + "pg_textsearch" "pg_textsearch" "pg_textsearch" "pg_textsearch" ` + @( + "src/mod.c", + "src/source.c", + "src/am/handler.c", + "src/am/build.c", + "src/am/build_parallel.c", + "src/am/scan.c", + "src/am/vacuum.c", + "src/memtable/memtable.c", + "src/memtable/posting.c", + "src/memtable/stringtable.c", + "src/memtable/local_memtable.c", + "src/memtable/scan.c", + "src/memtable/source.c", + "src/segment/segment.c", + "src/segment/dictionary.c", + "src/segment/scan.c", + "src/segment/merge.c", + "src/segment/docmap.c", + "src/segment/compression.c", + "src/query/bmw.c", + "src/query/score.c", + "src/types/vector.c", + "src/types/query.c", + "src/state/state.c", + "src/state/registry.c", + "src/state/metapage.c", + "src/state/limit.c", + "src/planner/hooks.c", + "src/planner/cost.c", + "src/debug/dump.c" + ) ` + @( + "sql/pg_textsearch--0.5.1.sql", + "sql/pg_textsearch--0.0.1--0.0.2.sql", + "sql/pg_textsearch--0.0.2--0.0.3.sql", + "sql/pg_textsearch--0.0.3--0.0.4.sql", + "sql/pg_textsearch--0.0.4--0.0.5.sql", + "sql/pg_textsearch--0.0.5--0.1.0.sql", + "sql/pg_textsearch--0.1.0--0.2.0.sql", + "sql/pg_textsearch--0.2.0--0.3.0.sql", + "sql/pg_textsearch--0.3.0--0.4.0.sql", + "sql/pg_textsearch--0.4.0--0.4.1.sql", + "sql/pg_textsearch--0.4.1--0.4.2.sql", + "sql/pg_textsearch--0.4.2--0.5.0.sql", + "sql/pg_textsearch--0.5.0--0.5.1.sql", + "pg_textsearch.control" + ) ` + @("/D_CRT_SECURE_NO_WARNINGS") ` + @("src") + Add-ExternalPgxsMesonProducer ` + "vector" "pgvector" "vector" "vector" ` + @( + "src/bitutils.c", + "src/bitvec.c", + "src/halfutils.c", + "src/halfvec.c", + "src/hnsw.c", + "src/hnswbuild.c", + "src/hnswinsert.c", + "src/hnswscan.c", + "src/hnswutils.c", + "src/hnswvacuum.c", + "src/ivfbuild.c", + "src/ivfflat.c", + "src/ivfinsert.c", + "src/ivfkmeans.c", + "src/ivfscan.c", + "src/ivfutils.c", + "src/ivfvacuum.c", + "src/sparsevec.c", + "src/vector.c" + ) ` + @( + "sql/vector--0.1.0--0.1.1.sql", + "sql/vector--0.1.1--0.1.3.sql", + "sql/vector--0.1.3--0.1.4.sql", + "sql/vector--0.1.4--0.1.5.sql", + "sql/vector--0.1.5--0.1.6.sql", + "sql/vector--0.1.6--0.1.7.sql", + "sql/vector--0.1.7--0.1.8.sql", + "sql/vector--0.1.8--0.2.0.sql", + "sql/vector--0.2.0--0.2.1.sql", + "sql/vector--0.2.1--0.2.2.sql", + "sql/vector--0.2.2--0.2.3.sql", + "sql/vector--0.2.3--0.2.4.sql", + "sql/vector--0.2.4--0.2.5.sql", + "sql/vector--0.2.5--0.2.6.sql", + "sql/vector--0.2.6--0.2.7.sql", + "sql/vector--0.2.7--0.3.0.sql", + "sql/vector--0.3.0--0.3.1.sql", + "sql/vector--0.3.1--0.3.2.sql", + "sql/vector--0.3.2--0.4.0.sql", + "sql/vector--0.4.0--0.4.1.sql", + "sql/vector--0.4.1--0.4.2.sql", + "sql/vector--0.4.2--0.4.3.sql", + "sql/vector--0.4.3--0.4.4.sql", + "sql/vector--0.4.4--0.5.0.sql", + "sql/vector--0.5.0--0.5.1.sql", + "sql/vector--0.5.1--0.6.0.sql", + "sql/vector--0.6.0--0.6.1.sql", + "sql/vector--0.6.1--0.6.2.sql", + "sql/vector--0.6.2--0.7.0.sql", + "sql/vector--0.7.0--0.7.1.sql", + "sql/vector--0.7.1--0.7.2.sql", + "sql/vector--0.7.2--0.7.3.sql", + "sql/vector--0.7.3--0.7.4.sql", + "sql/vector--0.7.4--0.8.0.sql", + "sql/vector--0.8.0--0.8.1.sql", + "sql/vector--0.8.1--0.8.2.sql", + "sql/vector--0.8.2.sql", + "vector.control" + ) ` + @("/fp:fast") +} + +function Prepare-WindowsExtensionInputs { + if ($BuildExtensions -eq "0") { + return + } + Assert-WindowsNativeExtensionSelectionSupported + Add-PgcryptoMesonProducer + Add-UuidOsspMesonProducer + Add-ExternalPgxsMesonProducers + Add-PostgisMesonProducer +} + +function Expand-PgtapSqlTemplate([string]$InputPath, [string]$OutputPath, [string]$ModulePath) { + $text = Get-Content -Raw -Path $InputPath + $text = $text.Replace("MODULE_PATHNAME", $ModulePath) + $text = $text.Replace("__OS__", "MSWin32") + $text = $text.Replace("__VERSION__", "1.3") + Set-Content -Path $OutputPath -Encoding UTF8 -Value $text +} + +function Install-WindowsPgtapExtension { + if (-not (NativeExtension-Selected "pgtap")) { + return + } + $sourceDir = External-Checkout "pgtap" + if (-not (Test-Path (Join-Path $sourceDir "pgtap.control"))) { + Fail "missing pgTAP checkout for Windows extension artifact staging: $sourceDir" + } + $buildDir = Join-Path $WorkRoot "pgtap-windows" + Copy-SourceTree $sourceDir $buildDir + $sqlDir = Join-Path $buildDir "sql" + Expand-PgtapSqlTemplate (Join-Path $sqlDir "pgtap.sql.in") (Join-Path $sqlDir "pgtap.sql") "pgtap" + foreach ($input in Get-ChildItem -Path $sqlDir -Filter "*.sql.in" -File) { + $output = Join-Path $sqlDir ($input.Name.Substring(0, $input.Name.Length - 3)) + if (-not (Test-Path $output)) { + Copy-Item -Force $input.FullName $output + } + } + Expand-PgtapSqlTemplate (Join-Path $sqlDir "pgtap.sql.in") (Join-Path $sqlDir "pgtap-static.sql") '$libdir/pgtap' + $coreSql = Join-Path $sqlDir "pgtap-core.sql" + $schemaSql = Join-Path $sqlDir "pgtap-schema.sql" + & perl (Join-Path $buildDir "compat/gencore") 0 (Join-Path $sqlDir "pgtap-static.sql") > $coreSql + if ($LASTEXITCODE -ne 0) { + Fail "pgTAP core SQL generation failed" + } + & perl (Join-Path $buildDir "compat/gencore") 1 (Join-Path $sqlDir "pgtap-static.sql") > $schemaSql + if ($LASTEXITCODE -ne 0) { + Fail "pgTAP schema SQL generation failed" + } + $uninstallSql = Join-Path $sqlDir "uninstall_pgtap.sql" + & perl -e 'for (grep { /^CREATE /} reverse <>) { chomp; s/CREATE (OR REPLACE )?/DROP /; s/DROP (FUNCTION|VIEW|TYPE) /DROP $1 IF EXISTS /; s/ (DEFAULT|=)[ ]+[a-zA-Z0-9]+//g; print "$_;\n" }' (Join-Path $sqlDir "pgtap.sql") > $uninstallSql + if ($LASTEXITCODE -ne 0) { + Fail "pgTAP uninstall SQL generation failed" + } + Copy-Item -Force (Join-Path $sqlDir "pgtap.sql") (Join-Path $sqlDir "pgtap--1.3.5.sql") + Copy-Item -Force $coreSql (Join-Path $sqlDir "pgtap-core--1.3.5.sql") + Copy-Item -Force $schemaSql (Join-Path $sqlDir "pgtap-schema--1.3.5.sql") + + $extensionDir = Join-Path $InstallDir "share/postgresql/extension" + New-Item -ItemType Directory -Force -Path $extensionDir | Out-Null + Copy-Item -Force (Join-Path $buildDir "pgtap.control") (Join-Path $extensionDir "pgtap.control") + Copy-Item -Force (Join-Path $sqlDir "pgtap*.sql") $extensionDir + Copy-Item -Force $uninstallSql $extensionDir + if (-not (Test-Path (Join-Path $extensionDir "pgtap--1.3.5.sql"))) { + Fail "pgTAP Windows staging did not produce pgtap--1.3.5.sql" + } +} + +function Get-ExactExtensionCatalogRows([string]$Purpose) { + if ($null -eq $script:ExactExtensionCatalogRows) { + Push-Location $RepoRoot + try { + $catalogText = cargo run -p oliphaunt --bin oliphaunt-resources --locked -- --list-extensions + $exitCode = $LASTEXITCODE + } finally { + Pop-Location + } + if ($exitCode -ne 0 -or -not $catalogText) { + Fail "failed to read exact extension catalog for $Purpose" + } + $script:ExactExtensionCatalogRows = @($catalogText | Select-Object -Skip 1) + } + $script:ExactExtensionCatalogRows +} + +function Get-SelectedEmbeddedExtensionModules { + if ($BuildExtensions -eq "0") { + return + } + $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($row in (Get-ExactExtensionCatalogRows "Windows embedded extension module linkage")) { + if (-not $row) { + continue + } + $columns = $row -split "`t", 12 + if ($columns.Count -lt 12) { + Fail "malformed extension catalog row while selecting Windows embedded modules: $row" + } + $sqlName = $columns[0] + $stem = $columns[3] + if (-not (NativeExtension-Selected $sqlName) -or -not $stem -or $stem -eq "-") { + continue + } + if ($seen.Add($stem)) { + [PSCustomObject]@{ + SqlName = $sqlName + Stem = $stem + } + } + } +} + +function Runtime-Installed([string]$DesiredHash) { + return (Test-Path (Join-Path $InstallDir "bin/initdb.exe")) -and + (Test-Path (Join-Path $InstallDir "bin/postgres.exe")) -and + (Test-Path (Join-Path $InstallDir "bin/pg_config.exe")) -and + (Test-Path (Join-Path $InstallDir "share/postgresql/postgresql.conf.sample")) -and + (Test-Path (Join-Path $InstallDir "share/postgresql/timezone/UTC")) -and + (Test-Path (Join-Path $InstallDir ".oliphaunt-postgres-runtime.sha256")) -and + ((Get-Content (Join-Path $InstallDir ".oliphaunt-postgres-runtime.sha256") -Raw).Trim() -eq $DesiredHash) -and + (($BuildExtensions -ne "0") -or (BaseRuntimeOptionalExtensionsAbsent)) +} + +function Build-Runtime([string]$DesiredHash) { + if (Runtime-Installed $DesiredHash) { + return + } + Write-MesonNativeFile $RuntimeNativeFile $false + $options = @( + "--native-file", $RuntimeNativeFile, + "--prefix", $InstallDir, + "--buildtype=release", + "-Db_pch=false", + "-Dreadline=disabled", + "-Dicu=disabled", + "-Dldap=disabled", + "-Dllvm=disabled", + "-Dzlib=disabled", + "-Dzstd=disabled", + "-Dlz4=disabled", + "-Dnls=disabled", + "-Dssl=none", + "-Ddocs=disabled", + "-Dtap_tests=disabled", + "-Dplperl=disabled", + "-Dplpython=disabled", + "-Dpltcl=disabled" + ) + if (-not (Test-Path $RuntimeBuildDir)) { + Invoke-Logged "meson-runtime-setup.log" { meson setup $RuntimeBuildDir $BuildDir @options } + } + Invoke-Logged "meson-runtime-compile.log" { meson compile -C $RuntimeBuildDir } + Invoke-Logged "meson-runtime-install.log" { meson install -C $RuntimeBuildDir } + New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + Install-WindowsPgtapExtension + Prune-BaseRuntimeOptionalExtensions + Set-Content -Path (Join-Path $InstallDir ".oliphaunt-postgres-runtime.sha256") -Value $DesiredHash -NoNewline + if (-not (Runtime-Installed $DesiredHash)) { + Fail "PostgreSQL Windows runtime install is incomplete" + } +} + +function Prune-BaseRuntimeOptionalExtensions { + if ($BuildExtensions -ne "0") { + return + } + + $extensionDir = Join-Path $InstallDir "share/postgresql/extension" + $moduleDir = Join-Path $InstallDir "lib/postgresql" + $shareDir = Join-Path $InstallDir "share/postgresql" + foreach ($row in (Get-ExactExtensionCatalogRows "base Windows runtime pruning")) { + if (-not $row) { + continue + } + $columns = $row -split "`t", 12 + if ($columns.Count -lt 12) { + Fail "malformed extension catalog row while pruning Windows base runtime: $row" + } + + $sqlName = $columns[0] + $stem = $columns[3] + $dataFiles = $columns[10] + if (Test-Path $extensionDir) { + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "$sqlName.control") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "$sqlName--*.sql") + } + if ((Test-Path $moduleDir) -and $stem -and $stem -ne "-") { + foreach ($suffix in @("dll", "so", "dylib")) { + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $moduleDir "$stem.$suffix") + } + } + if ($dataFiles -and $dataFiles -ne "-") { + foreach ($dataFile in $dataFiles.Split(",")) { + if ($dataFile) { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $shareDir $dataFile) + } + } + } + } + + if (Test-Path $extensionDir) { + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "postgis*.sql") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "rtpostgis*.sql") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "uninstall_postgis.sql") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "uninstall_legacy.sql") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "pgtap-*.sql") + Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $extensionDir "uninstall_pgtap.sql") + } + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $shareDir "contrib") + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $shareDir "proj") +} + +function BaseRuntimeOptionalExtensionsAbsent { + if (-not (Test-Path $InstallDir)) { + return $false + } + + $extensionDir = Join-Path $InstallDir "share/postgresql/extension" + $moduleDir = Join-Path $InstallDir "lib/postgresql" + $shareDir = Join-Path $InstallDir "share/postgresql" + foreach ($row in (Get-ExactExtensionCatalogRows "base Windows runtime validation")) { + if (-not $row) { + continue + } + $columns = $row -split "`t", 12 + if ($columns.Count -lt 12) { + Fail "malformed extension catalog row while validating Windows base runtime: $row" + } + + $sqlName = $columns[0] + $stem = $columns[3] + $dataFiles = $columns[10] + if ((Test-Path $extensionDir) -and (Test-Path (Join-Path $extensionDir "$sqlName.control"))) { + return $false + } + if ((Test-Path $extensionDir) -and (Get-ChildItem -Path $extensionDir -Filter "$sqlName--*.sql" -File -ErrorAction SilentlyContinue | Select-Object -First 1)) { + return $false + } + if ((Test-Path $moduleDir) -and $stem -and $stem -ne "-") { + foreach ($suffix in @("dll", "so", "dylib")) { + if (Test-Path (Join-Path $moduleDir "$stem.$suffix")) { + return $false + } + } + } + if ($dataFiles -and $dataFiles -ne "-") { + foreach ($dataFile in $dataFiles.Split(",")) { + if ($dataFile -and (Test-Path (Join-Path $shareDir $dataFile))) { + return $false + } + } + } + } + + if ((Test-Path (Join-Path $shareDir "contrib")) -or (Test-Path (Join-Path $shareDir "proj"))) { + return $false + } + + return $true +} + +function Write-MesonNativeFile([string]$Path, [bool]$UseCrtSecureNoWarnings) { + $content = @( + "[binaries]", + "c = 'cl.exe'", + "cpp = 'cl.exe'", + "ar = 'lib.exe'" + ) + if ($UseCrtSecureNoWarnings) { + $content += @( + "", + "[built-in options]", + "c_args = ['/D_CRT_SECURE_NO_WARNINGS']" + ) + } + Set-Content -Path $Path -Value ($content -join "`n") -Encoding UTF8 +} + +function Build-EmbeddedBackend { + Write-MesonNativeFile $EmbeddedNativeFile $true + $options = @( + "--native-file", $EmbeddedNativeFile, + "--prefix", $InstallDir, + "--buildtype=release", + "-Doliphaunt_embedded=true", + "-Doliphaunt_embedded_module_provider=", + "-Db_pch=false", + "-Dreadline=disabled", + "-Dicu=disabled", + "-Dldap=disabled", + "-Dllvm=disabled", + "-Dzlib=disabled", + "-Dzstd=disabled", + "-Dlz4=disabled", + "-Dnls=disabled", + "-Dssl=none", + "-Ddocs=disabled", + "-Dtap_tests=disabled", + "-Dplperl=disabled", + "-Dplpython=disabled", + "-Dpltcl=disabled" + ) + $previousCflags = $env:CFLAGS + $env:CFLAGS = "" + try { + if (-not (Test-Path $EmbeddedBuildDir)) { + Invoke-Logged "meson-embedded-setup.log" { meson setup $EmbeddedBuildDir $BuildDir @options } + } + Invoke-Logged "meson-embedded-bootstrap-provider.log" { + meson configure $EmbeddedBuildDir "-Doliphaunt_embedded_module_provider=" + } + Invoke-Logged "meson-embedded-postgres-lib.log" { meson compile -C $EmbeddedBuildDir postgres_lib } + Invoke-Logged "meson-embedded-postgres-def.log" { meson compile -C $EmbeddedBuildDir postgres.def } + Invoke-Logged "meson-embedded-plpgsql.log" { meson compile -C $EmbeddedBuildDir plpgsql } + } finally { + $env:CFLAGS = $previousCflags + } +} + +function Assert-SymbolPresent([string]$Binary, [string]$Symbol) { + $stem = [System.IO.Path]::GetFileNameWithoutExtension($Binary) + $log = Join-Path $WorkRoot "dumpbin-symbols-$stem.log" + dumpbin.exe /symbols $Binary *> $log + if ($LASTEXITCODE -ne 0) { + Get-Content $log -Tail 160 | Write-Error + Fail "dumpbin failed while inspecting $Binary" + } + $symbols = Get-Content $log -Raw + if ($symbols -notmatch "(^|[^A-Za-z0-9_])_?$([regex]::Escape($Symbol))([^A-Za-z0-9_]|$)") { + Get-Content $log -Tail 160 | Write-Error + Fail "$Binary does not define required embedded PostgreSQL symbol $Symbol" + } +} + +function First-File([string]$Root, [string[]]$Filters) { + foreach ($filter in $Filters) { + $item = Get-ChildItem -Path $Root -Recurse -Filter $filter -File | Select-Object -First 1 + if ($item) { + return $item.FullName + } + } + Fail "could not find any of $($Filters -join ', ') under $Root" +} + +function First-PostgresArchive([string]$Root, [string]$BaseName) { + $file = First-File $Root @("$BaseName.lib", "$BaseName.a") + if (-not $file) { + Fail "could not find PostgreSQL archive $BaseName under $Root" + } + $file +} + +function First-PlpgsqlObject([string]$Source) { + $root = Join-Path $EmbeddedBuildDir "src/pl/plpgsql/src" + if (-not (Test-Path $root)) { + Fail "could not find embedded PL/pgSQL object root under $root" + } + $matches = New-Object System.Collections.Generic.List[string] + foreach ($filter in @("$Source.c.obj", "meson-generated_*_$Source.c.obj", "*$Source.c.obj")) { + Get-ChildItem -Path $root -Recurse -Filter $filter -File | + ForEach-Object { $matches.Add($_.FullName) | Out-Null } + } + $unique = @($matches | Sort-Object -Unique) + if ($unique.Count -eq 1) { + return $unique[0] + } + if ($unique.Count -eq 0) { + Fail "could not find embedded PL/pgSQL object for $Source under $root" + } + Fail "ambiguous embedded PL/pgSQL object for $Source under $root`: $($unique -join ', ')" +} + +function Embedded-PlpgsqlObjects { + $objects = New-Object System.Collections.Generic.List[string] + foreach ($source in @("pl_comp", "pl_exec", "pl_funcs", "pl_gram", "pl_handler", "pl_scanner")) { + $objects.Add((First-PlpgsqlObject $source)) | Out-Null + } + Assert-SymbolPresent (First-PlpgsqlObject "pl_gram") "plpgsql_yyparse" + Assert-SymbolPresent (First-PlpgsqlObject "pl_handler") "plpgsql_call_handler" + $objects +} + +function Compile-LiboliphauntSources { + Remove-Item -Recurse -Force $ObjDir -ErrorAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $ObjDir | Out-Null + $objects = New-Object System.Collections.Generic.List[string] + foreach ($source in $LiboliphauntSources) { + $object = Join-Path $ObjDir ([System.IO.Path]::GetFileNameWithoutExtension($source) + ".obj") + $sourceName = [System.IO.Path]::GetFileNameWithoutExtension($source) + Invoke-Logged "compile-liboliphaunt-$sourceName.log" { + cl.exe /nologo /O2 /Zi /MD /DOLIPHAUNT_EMBEDDED /DOLIPHAUNT_BUILTIN_PLPGSQL /DOLIPHAUNT_BUILDING_DLL /D_CRT_SECURE_NO_WARNINGS ` + "/I$(Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/include")" ` + "/I$(Join-Path $RepoRoot "src/runtimes/liboliphaunt/native/src")" ` + /c $source "/Fo$object" + } + $objects.Add($object) + } + $objects +} + +function Link-LiboliphauntDll([System.Collections.Generic.List[string]]$Objects) { + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $DllOut), (Split-Path -Parent $ImportLibOut) | Out-Null + $postgresLib = First-PostgresArchive $EmbeddedBuildDir "postgres_lib" + $postgresDef = First-File $EmbeddedBuildDir "postgres.def" + Assert-SymbolPresent $postgresLib "oliphaunt_embedded_main" + $exports = @( + "oliphaunt_init", + "oliphaunt_init_ex", + "oliphaunt_exec_protocol", + "oliphaunt_exec_simple_query", + "oliphaunt_exec_protocol_stream", + "oliphaunt_backup", + "oliphaunt_backup_ex", + "oliphaunt_restore", + "oliphaunt_cancel", + "oliphaunt_detach", + "oliphaunt_close", + "oliphaunt_register_static_extensions", + "oliphaunt_last_error", + "oliphaunt_version", + "oliphaunt_capabilities", + "oliphaunt_free_response", + "oliphaunt_embedded_kill", + "oliphaunt_embedded_raise" + ) + $response = Join-Path $OutDir "link-oliphaunt.rsp" + $lines = @( + "/nologo", + "/DLL", + "/INCREMENTAL:NO", + "/OUT:$DllOut", + "/IMPLIB:$ImportLibOut", + "/PDB:$(Join-Path $OutDir "bin/oliphaunt.pdb")", + "/DEF:$postgresDef", + "/WHOLEARCHIVE:$postgresLib" + ) + foreach ($export in $exports) { + $lines += "/EXPORT:$export" + } + foreach ($object in $Objects) { + $lines += $object + } + foreach ($object in (Embedded-PlpgsqlObjects)) { + $lines += $object + } + $lines += @( + "ws2_32.lib", + "secur32.lib", + "advapi32.lib", + "shell32.lib", + "user32.lib", + "bcrypt.lib" + ) + Set-Content -Path $response -Value ($lines -join "`r`n") + link.exe "@$response" + if ($LASTEXITCODE -ne 0) { + Fail "failed to link $DllOut" + } +} + +function Get-ModuleHostBinding([string]$Binary) { + if (-not (Test-Path -LiteralPath $Binary -PathType Leaf)) { + return "missing" + } + $dependencies = dumpbin.exe /dependents $Binary 2>$null | Out-String + if ($LASTEXITCODE -ne 0) { + return "invalid" + } + $serverBound = $dependencies -match '(?im)^\s*postgres\.exe\s*$' + $embeddedBound = $dependencies -match '(?im)^\s*oliphaunt\.dll\s*$' + if ($serverBound -and $embeddedBound) { + return "crossed" + } + if ($serverBound) { + return "server" + } + if ($embeddedBound) { + return "embedded" + } + return "neutral" +} + +function Test-EmbeddedModuleHostContract([string]$Binary, [bool]$RequireProvider = $false) { + $binding = Get-ModuleHostBinding $Binary + if ($RequireProvider) { + return $binding -eq "embedded" + } + return $binding -eq "embedded" -or $binding -eq "neutral" +} + +function Assert-EmbeddedModuleHostContract([string]$Binary, [bool]$RequireProvider = $false) { + if (-not (Test-EmbeddedModuleHostContract $Binary $RequireProvider)) { + $binding = Get-ModuleHostBinding $Binary + $dependencies = if (Test-Path -LiteralPath $Binary -PathType Leaf) { + (dumpbin.exe /dependents $Binary 2>$null | Out-String).Trim() + } else { + "" + } + $expectation = if ($RequireProvider) { + "must import oliphaunt.dll and must not import postgres.exe" + } else { + "must not import postgres.exe and may be host-neutral or import oliphaunt.dll" + } + Fail "$Binary violates the embedded module host contract: $expectation; observed binding: $binding; dependencies: $dependencies" + } +} + +function Test-ServerModuleHostContract([string]$Binary) { + $binding = Get-ModuleHostBinding $Binary + return $binding -eq "server" -or $binding -eq "neutral" +} + +function Assert-ServerModuleHostContract([string]$Binary) { + if (-not (Test-ServerModuleHostContract $Binary)) { + $binding = Get-ModuleHostBinding $Binary + $dependencies = if (Test-Path -LiteralPath $Binary -PathType Leaf) { + (dumpbin.exe /dependents $Binary 2>$null | Out-String).Trim() + } else { + "" + } + Fail "$Binary violates the server module host contract: must not import oliphaunt.dll and may be host-neutral or import postgres.exe; observed binding: $binding; dependencies: $dependencies" + } +} + +function Test-CompatibleModuleProfiles([string]$ServerBinary, [string]$EmbeddedBinary) { + if (-not (Test-ServerModuleHostContract $ServerBinary) -or + -not (Test-EmbeddedModuleHostContract $EmbeddedBinary)) { + return $false + } + $serverSha256 = Get-FileSha256 $ServerBinary + $embeddedSha256 = Get-FileSha256 $EmbeddedBinary + if ($serverSha256 -ne $embeddedSha256) { + return $true + } + return (Get-ModuleHostBinding $ServerBinary) -eq "neutral" -and + (Get-ModuleHostBinding $EmbeddedBinary) -eq "neutral" +} + +function Assert-CompatibleModuleProfiles([string]$ServerBinary, [string]$EmbeddedBinary) { + Assert-ServerModuleHostContract $ServerBinary + Assert-EmbeddedModuleHostContract $EmbeddedBinary + $serverSha256 = Get-FileSha256 $ServerBinary + $embeddedSha256 = Get-FileSha256 $EmbeddedBinary + if ($serverSha256 -eq $embeddedSha256 -and + ((Get-ModuleHostBinding $ServerBinary) -ne "neutral" -or + (Get-ModuleHostBinding $EmbeddedBinary) -ne "neutral")) { + Fail "Windows host-bound extension module server and embedded profiles must have distinct bytes: $ServerBinary and $EmbeddedBinary both have SHA-256 $serverSha256" + } +} + +function Find-EmbeddedModuleBinary([string]$Stem) { + $matches = @( + Get-ChildItem -Path $EmbeddedBuildDir -Recurse -Filter "$Stem.dll" -File | + Sort-Object -Property FullName + ) + if ($matches.Count -ne 1) { + $observed = if ($matches.Count -eq 0) { "" } else { ($matches.FullName -join ", ") } + Fail "expected exactly one Meson embedded module output for $Stem.dll; observed $observed" + } + $matches[0].FullName +} + +function Remove-EmbeddedModuleStage { + if (-not (Test-Path -LiteralPath $EmbeddedModulesDir)) { + return + } + $embeddedModulesInfo = Get-Item -LiteralPath $EmbeddedModulesDir -Force + if (($embeddedModulesInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + Remove-Item -LiteralPath $EmbeddedModulesDir -Force + } else { + Remove-Item -LiteralPath $EmbeddedModulesDir -Recurse -Force + } +} + +function Build-EmbeddedModules { + if (-not (Test-Path -LiteralPath $ImportLibOut -PathType Leaf)) { + Fail "cannot build embedded extension modules before the host import library exists at $ImportLibOut" + } + + $selectedModules = @(Get-SelectedEmbeddedExtensionModules) + $targetNames = @("plpgsql") + @($selectedModules | ForEach-Object { $_.Stem }) + $targetNames = @($targetNames | Sort-Object -Unique) + $provider = Meson-Path $ImportLibOut + Invoke-Logged "meson-embedded-module-provider.log" { + meson configure $EmbeddedBuildDir "-Doliphaunt_embedded_module_provider=$provider" + } + Invoke-Logged "meson-embedded-modules.log" { + meson compile -C $EmbeddedBuildDir @targetNames + } + + Remove-EmbeddedModuleStage + New-Item -ItemType Directory -Force -Path $EmbeddedModulesDir | Out-Null + $plpgsqlSource = Find-EmbeddedModuleBinary "plpgsql" + Assert-EmbeddedModuleHostContract $plpgsqlSource $true + Copy-Item -LiteralPath $plpgsqlSource -Destination $EmbeddedPlpgsqlDllOut -Force + Assert-EmbeddedModuleHostContract $EmbeddedPlpgsqlDllOut $true + + foreach ($module in $selectedModules) { + $source = Find-EmbeddedModuleBinary $module.Stem + Assert-EmbeddedModuleHostContract $source + $staged = Join-Path $EmbeddedModulesDir "$($module.Stem).dll" + Copy-Item -LiteralPath $source -Destination $staged -Force + Assert-EmbeddedModuleHostContract $staged + } + + $installedModuleDir = Join-Path $InstallDir "lib/postgresql" + foreach ($module in $selectedModules) { + $server = Join-Path $installedModuleDir "$($module.Stem).dll" + $embedded = Join-Path $EmbeddedModulesDir "$($module.Stem).dll" + Assert-CompatibleModuleProfiles $server $embedded + } +} + +function Embedded-ModulesReady { + if (-not (Test-EmbeddedModuleHostContract $EmbeddedPlpgsqlDllOut $true)) { + return $false + } + foreach ($module in @(Get-SelectedEmbeddedExtensionModules)) { + $server = Join-Path $InstallDir "lib/postgresql/$($module.Stem).dll" + $embedded = Join-Path $EmbeddedModulesDir "$($module.Stem).dll" + if (-not (Test-CompatibleModuleProfiles $server $embedded)) { + return $false + } + } + return $true +} + +function Artifact-Ready { + if (-not (Test-Path $DllOut) -or + -not (Test-Path $ImportLibOut) -or + -not (Embedded-ModulesReady)) { + return $false + } + if (-not (Test-VcRuntimeClosure)) { + return $false + } + $exports = dumpbin.exe /exports $DllOut 2>$null | Out-String + foreach ($symbol in @( + "oliphaunt_init", + "oliphaunt_init_ex", + "oliphaunt_exec_protocol", + "oliphaunt_exec_protocol_stream", + "oliphaunt_backup", + "oliphaunt_restore", + "oliphaunt_close", + "oliphaunt_version", + "oliphaunt_capabilities", + "oliphaunt_free_response" + )) { + if ($exports -notmatch "\b$symbol\b") { + return $false + } + } + $true +} + +if (-not $IsWindows) { + Fail "Windows liboliphaunt build must run on Windows" +} + +Require-Command git +Require-Command curl.exe +Require-Command bun +Require-Command cargo +Import-MsvcEnvironment +Prefer-NativePerl +$env:CCACHE_DISABLE = "1" +Ensure-MesonTools +Configure-MsvcToolchainPath + +$desiredHash = Get-DesiredHash +Prepare-Source $desiredHash +Prepare-WindowsExtensionInputs + +if ($CheckCurrent) { + if ((Runtime-Installed $desiredHash) -and (Artifact-Ready) -and (Test-Path $Stamp) -and ((Get-Content $Stamp -Raw).Trim() -eq $desiredHash)) { + Write-Output "Windows $TargetId liboliphaunt DLL is current" + exit 0 + } + Write-Error "Windows $TargetId liboliphaunt DLL is missing or stale" + exit 1 +} + +Build-Runtime $desiredHash +Build-EmbeddedBackend +$objects = Compile-LiboliphauntSources +Link-LiboliphauntDll $objects +Build-EmbeddedModules +Stage-VcRuntimeClosure +if (-not (Artifact-Ready)) { + Fail "Windows liboliphaunt DLL did not pass export checks" +} +Set-Content -Path $Stamp -Value $desiredHash -NoNewline +Write-Output $DllOut diff --git a/src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh b/src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh new file mode 100755 index 00000000..a38bb398 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env sh +set -eu + +script_dir="$(cd "$(dirname "$0")" && pwd)" +. "$script_dir/common.sh" +root="$(oliphaunt_resolve_repo_root "$script_dir")" +manifest="$root/src/runtimes/liboliphaunt/native/postgres18/external-extensions.toml" +online=0 + +case "${1:-}" in + "") + ;; + --online) + online=1 + ;; + *) + echo "usage: src/runtimes/liboliphaunt/native/bin/check-external-extension-pins.sh [--online]" >&2 + exit 2 + ;; +esac + +require_line() { + line="$1" + if ! grep -Fxq "$line" "$manifest"; then + echo "external extension source manifest is missing: $line" >&2 + exit 1 + fi +} + +check_checkout_if_present() { + name="$1" + relative_checkout="$2" + expected_commit="$3" + checkout="$root/$relative_checkout" + + if [ ! -d "$checkout/.git" ]; then + echo "external extension checkout not present for $name: $relative_checkout" + return 0 + fi + + actual_commit="$(git -C "$checkout" rev-parse HEAD)" + if [ "$actual_commit" != "$expected_commit" ]; then + cat >&2 <&2 <} +MSG + exit 1 + fi + echo "external extension remote pin verified for $name: $ref -> $expected_commit" +} + +[ -f "$manifest" ] || { + echo "missing external extension source manifest: $manifest" >&2 + exit 1 +} + +require_line 'schema = "liboliphaunt-external-extensions-v2"' +require_line 'pg_major = 18' + +if grep -Eq '^[[:space:]]*pack[[:space:]]*=' "$manifest"; then + echo "external extension manifest must not declare extension selection aliases; select exact extensions only" >&2 + exit 1 +fi + +require_line 'id = "pggraph"' +require_line 'sql_name = "graph"' +require_line 'module_stem = "graph"' +require_line 'upstream = "https://github.com/evokoa/pggraph.git"' +require_line 'source_ref = "main"' +require_line 'commit = "4ea3c3206811deda03de136b4f465a2cf9bc8e72"' +require_line 'checkout = "target/oliphaunt-sources/checkouts/pggraph"' +require_line 'source_subdir = "graph"' +require_line 'license = "Apache-2.0"' +require_line 'redistribution = "allowed"' +require_line 'pgrx_version = "0.18.0"' +require_line 'pg_feature = "pg18"' + +require_line 'id = "paradedb-pg-search"' +require_line 'sql_name = "pg_search"' +require_line 'module_stem = "pg_search"' +require_line 'upstream = "https://github.com/paradedb/paradedb.git"' +require_line 'source_ref = "v0.23.4"' +require_line 'commit = "c07921a78f3d24cbb0251b31a1150a7db600af5a"' +require_line 'checkout = "target/oliphaunt-sources/checkouts/paradedb"' +require_line 'source_subdir = "pg_search"' +require_line 'license = "AGPL-3.0"' +require_line 'redistribution = "requires-commercial-license"' +require_line 'pgrx_version = "0.18.0"' +require_line 'pg_feature = "pg18"' +require_line 'requires_shared_preload = true' + +check_checkout_if_present \ + pggraph \ + target/oliphaunt-sources/checkouts/pggraph \ + 4ea3c3206811deda03de136b4f465a2cf9bc8e72 + +check_checkout_if_present \ + paradedb-pg-search \ + target/oliphaunt-sources/checkouts/paradedb \ + c07921a78f3d24cbb0251b31a1150a7db600af5a + +check_remote_if_requested \ + pggraph \ + https://github.com/evokoa/pggraph.git \ + HEAD \ + 4ea3c3206811deda03de136b4f465a2cf9bc8e72 + +check_remote_if_requested \ + paradedb-pg-search \ + https://github.com/paradedb/paradedb.git \ + refs/tags/v0.23.4 \ + c07921a78f3d24cbb0251b31a1150a7db600af5a + +echo "external extension source pins passed" diff --git a/src/runtimes/liboliphaunt/native/bin/check-postgres18-ios-simulator.sh b/src/runtimes/liboliphaunt/native/bin/check-postgres18-ios-simulator.sh new file mode 100755 index 00000000..92cb6ea6 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/check-postgres18-ios-simulator.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$script_dir/common.sh" +. "$script_dir/icu.sh" +repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" +. "$repo_root/src/postgres/versions/18/fetch-source.sh" +pg_version="18.4" +pg_sha256="81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" +pg_url="https://ftp.postgresql.org/pub/source/v${pg_version}/postgresql-${pg_version}.tar.bz2" +source_manifest="$repo_root/src/runtimes/liboliphaunt/native/postgres18/source.toml" +patch_dir="$repo_root/src/runtimes/liboliphaunt/native/patches/postgresql-${pg_version}" +work_root="${OLIPHAUNT_IOS_SIMULATOR_CHECK_ROOT:-$repo_root/target/liboliphaunt-ios-simulator-check}" +source_cache="$work_root/source" +tarball="$source_cache/postgresql-${pg_version}.tar.bz2" +build_dir="$work_root/postgresql-${pg_version}" +install_dir="$work_root/install" +stamp="$build_dir/.liboliphaunt-ios-simulator-check.sha256" +make_log="$work_root/make.log" +configure_log="$work_root/configure.log" +icu_source_dir="$(oliphaunt_icu_source_dir "$repo_root")" +icu_native_build_dir="$work_root/icu-native" +icu_build_dir="$work_root/icu-ios-simulator-build" +icu_prefix="$work_root/icu-ios-simulator" +icu_cflags="$(oliphaunt_icu_cflags "$icu_prefix")" +icu_static_libs="$(oliphaunt_icu_static_libs "$icu_prefix")" +icu_cpp_libs="-lc++" +icu_libs="$icu_static_libs $icu_cpp_libs" + +if [ "$(uname -s)" != "Darwin" ]; then + echo "PostgreSQL iOS simulator probe requires Darwin" >&2 + exit 2 +fi + +if ! command -v xcrun >/dev/null 2>&1; then + echo "missing required command: xcrun" >&2 + exit 1 +fi + +if ! command -v rg >/dev/null 2>&1; then + echo "missing required command: rg" >&2 + exit 1 +fi + +sdk_path="$(xcrun --sdk iphonesimulator --show-sdk-path 2>/dev/null || true)" +clang_path="$(xcrun --find --sdk iphonesimulator clang 2>/dev/null || true)" +clangxx_path="$(xcrun --find --sdk iphonesimulator clang++ 2>/dev/null || true)" +ar_path="$(xcrun --find --sdk iphonesimulator ar 2>/dev/null || true)" +ranlib_path="$(xcrun --find --sdk iphonesimulator ranlib 2>/dev/null || true)" +if [ -z "$sdk_path" ] || [ -z "$clang_path" ] || [ -z "$clangxx_path" ] || [ -z "$ar_path" ] || [ -z "$ranlib_path" ]; then + echo "iPhoneSimulator SDK is unavailable" >&2 + exit 1 +fi +oliphaunt_icu_require_source "$icu_source_dir" + +min_ios="${OLIPHAUNT_IOS_SIMULATOR_MIN_VERSION:-17.0}" +cc=("$clang_path" -target "arm64-apple-ios${min_ios}-simulator" "-mios-simulator-version-min=${min_ios}" -isysroot "$sdk_path") +cxx=("$clangxx_path" -target "arm64-apple-ios${min_ios}-simulator" "-mios-simulator-version-min=${min_ios}" -isysroot "$sdk_path") +ccache_mode="${OLIPHAUNT_CCACHE:-auto}" +if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then + ccache_bin="" + if [ "$ccache_mode" = "auto" ]; then + ccache_bin="$(command -v ccache || true)" + else + ccache_bin="$ccache_mode" + fi + if [ -n "$ccache_bin" ]; then + cc=("$ccache_bin" "${cc[@]}") + cxx=("$ccache_bin" "${cxx[@]}") + fi +fi +cc_string="${cc[*]}" +cxx_string="${cxx[*]}" + +patch_series() { + sed -n '/series = \[/,/\]/p' "$source_manifest" | + sed -n 's/.*"\([^"]*\.patch\)".*/\1/p' +} + +patch_series_hash() { + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + shasum -a 256 "$patch_dir/$patch_name" + done < <(patch_series) | shasum -a 256 | awk '{print $1}' +} + +desired_hash() { + { + printf 'pg_version=%s\n' "$pg_version" + printf 'pg_sha256=%s\n' "$pg_sha256" + printf 'sdk_path=%s\n' "$sdk_path" + printf 'clang_path=%s\n' "$clang_path" + printf 'clangxx_path=%s\n' "$clangxx_path" + printf 'min_ios=%s\n' "$min_ios" + printf 'cc=%s\n' "$cc_string" + printf 'cxx=%s\n' "$cxx_string" + printf 'ar=%s\n' "$ar_path" + printf 'ranlib=%s\n' "$ranlib_path" + printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" + printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" + printf 'patch_series_hash=%s\n' "$(patch_series_hash)" + shasum -a 256 "$0" + shasum -a 256 "$source_manifest" + } | shasum -a 256 | awk '{print $1}' +} + +apply_patch_series() { + local patch_name + while IFS= read -r patch_name; do + [ -n "$patch_name" ] || continue + GIT_CEILING_DIRECTORIES="$work_root" git apply --whitespace=error-all "$patch_dir/$patch_name" >/dev/null + done < <(patch_series) +} + +prepare_source() { + mkdir -p "$source_cache" "$work_root" + + oliphaunt_fetch_postgresql_source_archive "$tarball" "$pg_version" "$pg_sha256" "$pg_url" + ( + cd "$source_cache" + printf '%s %s\n' "$pg_sha256" "postgresql-${pg_version}.tar.bz2" | shasum -a 256 -c - + ) + + local wanted + wanted="$(desired_hash)" + if [ -d "$build_dir" ] && { [ ! -f "$stamp" ] || [ "$(cat "$stamp")" != "$wanted" ]; }; then + rm -rf "$build_dir" + fi + if [ ! -d "$build_dir" ]; then + tar -xjf "$tarball" -C "$work_root" + ( + cd "$build_dir" + apply_patch_series + ) + fi + printf '%s\n' "$wanted" > "$stamp" +} + +configure_source() { + export CC="$cc_string" + export CXX="$cxx_string" + export CFLAGS="-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED" + export CPPFLAGS="-isysroot $sdk_path $icu_cflags" + export LDFLAGS="-isysroot $sdk_path -L$icu_prefix/lib" + export ICU_CFLAGS="$icu_cflags" + export ICU_LIBS="$icu_libs" + + if [ ! -f "$build_dir/config.status" ]; then + ( + cd "$build_dir" + ./configure \ + --host=aarch64-apple-darwin \ + --prefix="$install_dir" \ + --without-readline \ + --with-icu \ + --without-llvm \ + --without-pam \ + --with-openssl=no \ + --without-zlib \ + --disable-nls \ + ac_cv_file__dev_urandom=yes + ) > "$configure_log" 2>&1 + fi +} + +build_icu() { + oliphaunt_icu_build_target \ + "$icu_source_dir" \ + "$script_dir" \ + "$icu_native_build_dir" \ + "$icu_build_dir" \ + "$icu_prefix" \ + "${OLIPHAUNT_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" \ + "ios-simulator-check" \ + "aarch64-apple-darwin" \ + "$cc_string" \ + "$cxx_string" \ + "$ar_path" \ + "$ranlib_path" \ + "-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED" \ + "-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED" \ + "-isysroot $sdk_path" +} + +compile_probe_objects() { + : > "$make_log" + ( + cd "$build_dir" + make -C src/backend generated-headers + make -C src/backend/archive shell_archive.o V=1 + make -C src/backend/access/transam xlogarchive.o V=1 + make -C src/backend/libpq be-secure.o V=1 + make -C src/backend/libpq pqcomm.o V=1 + make -C src/backend/tcop postgres.o V=1 + make -C src/backend/storage/ipc ipc.o V=1 + make -C src/backend/utils/fmgr dfmgr.o V=1 + ) > "$make_log" 2>&1 + + if rg -n "warning:|error:" "$make_log" >/dev/null; then + rg -n "warning:|error:" "$make_log" >&2 + echo "PostgreSQL iOS simulator probe must be warning-clean" >&2 + exit 1 + fi +} + +verify_probe_symbols() { + local source_root="$build_dir/src" + rg -q --fixed-strings "OLIPHAUNT_EMBEDDED_NO_SHELL_COMMANDS" "$source_root/backend/archive/shell_archive.c" + rg -q --fixed-strings "OLIPHAUNT_EMBEDDED_NO_SHELL_COMMANDS" "$source_root/backend/access/transam/xlogarchive.c" + rg -q --fixed-strings "oliphaunt_embedded_main" "$source_root/include/tcop/tcopprot.h" + rg -q --fixed-strings "oliphaunt_embedded_kill" "$source_root/port/pqsignal.c" + rg -q --fixed-strings "oliphaunt_embedded_raise" "$source_root/port/pqsignal.c" + rg -q --fixed-strings 'getenv("ICU_DATA")' "$source_root/bin/initdb/initdb.c" + rg -q --fixed-strings "oliphaunt_static_extension_magic(file_scanner->static_extension)" "$source_root/backend/utils/fmgr/dfmgr.c" +} + +prepare_source +build_icu +configure_source +compile_probe_objects +verify_probe_symbols + +echo "PostgreSQL 18 iOS simulator embedded probe passed: $build_dir" diff --git a/src/runtimes/liboliphaunt/native/bin/common.sh b/src/runtimes/liboliphaunt/native/bin/common.sh new file mode 100755 index 00000000..f3ebecb9 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/common.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env sh + +oliphaunt_resolve_repo_root() { + script_dir="${1:?oliphaunt_resolve_repo_root requires a script directory}" + if repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "$repo_root" + return 0 + 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 +} + +oliphaunt_native_external_extension_source_rel() { + [ "$#" -eq 2 ] || { + echo "oliphaunt_native_external_extension_source_rel requires a repository root and extension id" >&2 + return 2 + } + case "$2" in + postgis) + printf '%s\n' 'target/oliphaunt-sources/checkouts/postgis' + ;; + *) + awk -F '\t' -v extension="$2" ' + NR > 1 && ($1 == extension || $3 == "target/oliphaunt-sources/checkouts/" extension) { + print $3 + found = 1 + exit + } + END { exit found ? 0 : 1 } + ' "$1/src/extensions/generated/pgxs-build.tsv" + ;; + esac +} diff --git a/src/runtimes/liboliphaunt/native/bin/fetch-pinned-git-checkout.sh b/src/runtimes/liboliphaunt/native/bin/fetch-pinned-git-checkout.sh new file mode 100755 index 00000000..9a6dd5ee --- /dev/null +++ b/src/runtimes/liboliphaunt/native/bin/fetch-pinned-git-checkout.sh @@ -0,0 +1,360 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 6 ]; then + echo "usage: fetch-pinned-git-checkout.sh