diff --git a/.github/workflows/reusable-homebrew-bottle-publish.yml b/.github/workflows/reusable-homebrew-bottle-publish.yml index 82b19c9133..397585353a 100644 --- a/.github/workflows/reusable-homebrew-bottle-publish.yml +++ b/.github/workflows/reusable-homebrew-bottle-publish.yml @@ -241,6 +241,9 @@ jobs: exit 1 fi bash scripts/dev-shell.sh bash scripts/build-musl.sh + if [ "${{ matrix.arch }}" = "wasm64" ]; then + bash scripts/dev-shell.sh bash scripts/build-musl.sh --arch wasm64posix + fi - name: Build Kandelo kernel shell: bash diff --git a/docs/homebrew-publishing.md b/docs/homebrew-publishing.md index d14760066e..bda7dafca7 100644 --- a/docs/homebrew-publishing.md +++ b/docs/homebrew-publishing.md @@ -115,6 +115,11 @@ For formulae that build Kandelo Wasm artifacts: for Homebrew bottle selection. Update Kandelo `build.toml` `revision` only when the underlying Kandelo package output bytes legitimately change. +Current dependency-root formulae mirror the registry manifests' architecture +support: `openssl`, `libcxx`, and `libxml2` build wasm32 and wasm64 bottles; +`libpng`, `libcurl`, and the hybrid `ncurses` package are wasm32-only until +their registry manifests opt into wasm64. + Formula Ruby should read these `HOMEBREW_KANDELO_*` variables for values that must survive Homebrew environment handling: @@ -123,6 +128,8 @@ HOMEBREW_KANDELO_ROOT HOMEBREW_KANDELO_ARCH HOMEBREW_KANDELO_NODE HOMEBREW_KANDELO_LLVM_BIN +HOMEBREW_KANDELO_HOST_TOOL_PATH +HOMEBREW_KANDELO_PKG_CONFIG ``` Workflow-facing scripts use `KANDELO_HOMEBREW_*` variables outside Formula @@ -215,6 +222,18 @@ path calls `scripts/homebrew-publish-sidecars.sh --status failed` so the failed attempt is durable while the previous successful bottle remains selectable when its fallback fields are complete. +The default trusted sidecar wrapper, +`scripts/homebrew-generate-sidecars-from-env.sh`, derives non-hello link +manifests from package kind. Program Formulae link their installed +`bin/` into the Homebrew prefix. Library Formulae link the declared +`[outputs]` headers, static libraries, and pkg-config files from the keg into +the prefix. Package-specific Node and browser outcome text may be supplied via +`KANDELO_HOMEBREW_NODE_SMOKE_COMMAND`, +`KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS`, and +`KANDELO_HOMEBREW_BROWSER_SMOKE_REASON`; browser compatibility is recorded +only when `KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS=success` and the browser VFS +smoke artifact environment is complete. + ## VFS Planning And Building Homebrew-derived VFS images are built from sidecars and verified bottle bytes, @@ -265,6 +284,33 @@ It clones or reads the tap, builds a Homebrew VFS from published sidecars, runs `/home/linuxbrew/.linuxbrew/bin/hello --version` through `NodeKernelHost`, and checks negative ABI-mismatch and missing-bottle cases. +For the sqlite/bzip2/xz pilot and later non-hello package checks, use the +generic package smoke runner against a generated tap root: + +```bash +npx tsx scripts/homebrew-package-node-smoke.ts \ + --tap-root /path/to/kandelo-homebrew \ + --formula sqlite \ + --formula bzip2 \ + --formula xz \ + --formula openssl \ + --formula libcxx \ + --formula libxml2 \ + --formula libpng \ + --formula libcurl \ + --formula ncurses \ + --arch wasm32 \ + --result-dir test-runs/homebrew-package-node-smoke +``` + +The runner builds Homebrew VFS images from sidecars, writes passed, failed, +and skipped outcome lists, runs program package version smokes from the poured +prefix, and compiles small consumers against poured library headers and static +libraries before running the validation Wasm on Node. Use a separate wasm64 run +for formulae whose registry manifests declare `arches = ["wasm32", "wasm64"]`. +Dry-run bottle evidence remains local evidence until the trusted workflow +publishes GHCR bottle bytes and tap sidecars. + Browser compatibility requires a separate browser smoke. For the current `hello` path, the trusted publisher builds a precomposed wasm32 VFS image, serves it through the browser demo, runs Chromium Playwright against diff --git a/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md b/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md new file mode 100644 index 0000000000..68e3cad4c4 --- /dev/null +++ b/docs/plans/2026-06-29-homebrew-sqlite-bzip2-xz-pilot-design.md @@ -0,0 +1,545 @@ +# Homebrew SQLite, Bzip2, And Xz Pilot Design + +Date: 2026-06-29 + +Tracked work: + +- `kd-1mr` - Port all current Kandelo packages to Homebrew. +- `kd-1mr.2` - Port sqlite, bzip2, and xz Homebrew pilot. +- Source planning evidence: `kd-5yd`, commit `b6cd51d8c`, refreshed the + Homebrew package inventory and selected this pilot after the trusted + `hello` publication path and local `zlib` wasm32/wasm64 proof. + +This is a design artifact for the pilot. It does not implement the Formulae, +publish bottles, change registry package bytes, or update release metadata. + +## Problem Statement + +Kandelo's Homebrew foundation can publish and smoke the first `hello` bottle, +and the follow-up `zlib` proof showed that a dependency-root library can be +bottled locally for wasm32 and wasm64. The next migration step needs a small +pilot that exercises more of the future registry-replacement model without +starting a broad package wave. + +The pilot packages are deliberately mixed: + +- `sqlite` is the next dependency-root library. It declares wasm32 and wasm64, + has real downstream consumers, and has existing upstream SQLite test harnesses + whose status should be visible without making full upstream success the + default bottle gate. +- `bzip2` and `xz` are small leaf CLI packages. They exercise program Formulae, + link manifests, VFS pour/link behavior, and Node/browser smoke commands + without the capacity risk of heavy runtimes. + +The design must preserve the current Homebrew direction: + +- Formulae become authoritative for source, dependency, build, install, and + `test do` behavior. +- Kandelo sidecars remain additive metadata for ABI, cache keys, provenance, + VFS planning, host support, browser gallery status, and test outcomes. +- Package failures remain visible with reasons and artifacts. +- Node and browser hosts are both product surfaces. A missing browser result is + status to publish, not proof of browser support. + +## Non-Goals + +- Do not port the full dependency-root wave or the full small-CLI wave. +- Do not delete or rename `packages/registry` in this pilot. +- Do not revive `sqlite-cli` as a product Formula unless the pilot proves it is + necessary and a focused follow-up is created. +- Do not make upstream SQLite full-suite success a default bottle publication + gate. +- Do not use Formula patches or test skips to hide Kandelo runtime, libc, + syscall, VFS, fork-instrumentation, or host-parity defects. +- Do not publish user-facing guest `brew install` instructions from this work. +- Do not bump `packages/registry/*/build.toml` revisions unless package output + bytes legitimately change. + +## Users And Operator Workflows + +### Package Porter + +The porter authors or regenerates `Formula/sqlite.rb`, `Formula/bzip2.rb`, and +`Formula/xz.rb` in `Automattic/kandelo-homebrew` or the reviewable tap fixture. +They reuse existing package build knowledge, but the Formula DSL owns the +final source, build, install, and test behavior. The existing registry scripts +may be called only where they already honor the resolver-style output contract +or after the pilot makes their side effects explicit. + +### Maintainer Reviewer + +The reviewer checks that the Formulae are normal Homebrew Formulae with the +minimum Kandelo-specific environment wiring, that sidecars describe the bottles +truthfully, that dry-run/local evidence is not presented as trusted +publication, and that package failures are recorded instead of disappearing. + +### Trusted Publisher + +The trusted workflow builds each selected `(formula, arch)` entry, uploads the +bottle to the GHCR-backed Homebrew bottle URL shape, generates sidecars and +provenance, validates the tap payload, then publishes success or failure state. +The workflow must keep failure reports durable without replacing last-green +metadata. + +### Runtime Validator + +The validator materializes Homebrew bottles into VFS images and runs package +smoke commands on both Node and browser hosts. For program packages, the smoke +executes the installed program. For library packages, the smoke compiles or +ships a test-only consumer and runs that consumer against the poured library. +The test consumer is validation evidence, not a product bottle output. + +### Debugger + +When a package fails, the debugger needs enough metadata to classify the fault: +source fetch, Formula generation, cross-compile configure answer, build, +install/link, bottle upload, sidecar generation, VFS pour/link, Node runtime, +browser runtime, upstream test failure, or Kandelo platform behavior. + +## Existing Package Facts + +`sqlite`: + +- `packages/registry/sqlite/package.toml` is a library manifest for SQLite + `3.49.1`, declares `arches = ["wasm32", "wasm64"]`, and outputs + `lib/libsqlite3.a`, `include/sqlite3.h`, `include/sqlite3ext.h`, and + `lib/pkgconfig/sqlite3.pc`. +- `build-sqlite.sh` is resolver-shaped for the library path when + `WASM_POSIX_DEP_OUT_DIR` is set, but it also has a legacy direct-invocation + CLI path. Formulae should not rely on that legacy CLI path for the library + bottle. +- Existing SQLite test tooling includes SQL fixture tests and official + testrunner wrappers for Node and browser. Those are upstream-test status + evidence, not the default bottle availability gate. + +`bzip2`: + +- `packages/registry/bzip2/package.toml` is a wasm32 program manifest for + `1.0.8`. +- `build-bzip2.sh` is an older direct script. It writes `bin/bzip2.wasm`, + installs `libbz2.a` and `bzlib.h` to the repo sysroot, and registers a local + binary. A Formula must not silently rely on those sysroot/local-binary side + effects. +- Existing Vitest coverage checks `bzip2 --version`; round-trip compression is + not covered there. + +`xz`: + +- `packages/registry/xz/package.toml` says version `5.6.2`, while + `build-xz.sh` currently defaults to `5.6.4` and downloads a `.tar.gz` from + the GitHub release path. This version/source mismatch is a pilot blocker + unless the Formula pins one source and the build path is made consistent. +- `build-xz.sh` is an older direct script. It writes `bin/xz.wasm`, installs + `liblzma.a` and headers to the repo sysroot, and registers a local binary. + A Formula should install only the intended bottle contents into the Homebrew + keg. +- Existing Vitest coverage checks `xz --version`; round-trip compression is + not covered there. + +Cross-cutting: + +- Current package manifests have stale `kernel_abi` values. The Homebrew + sidecar path must use live `ABI_VERSION` and computed `cache_key_sha` + evidence. The pilot should not treat stale manifest ABI fields as bottle + compatibility truth. +- `scripts/homebrew-generate-sidecars-from-env.sh` is currently heavily shaped + around the `hello` validation path. The pilot needs a generic sidecar input + path for package-specific validation outcomes, or it must publish non-hello + failure/deferred status with a focused follow-up. + +## Architecture And Data Flow + +The pilot should use the same publication architecture as the existing +Homebrew path: + +```text +Formula/sqlite.rb, Formula/bzip2.rb, Formula/xz.rb + | + v +trusted workflow matrix + scripts/homebrew-plan-matrix.sh + | + v +scripts/homebrew-bottle-build.sh + brew install --build-bottle + brew test + brew bottle --json + brew bottle --merge + | + v +scripts/homebrew-ghcr-upload.sh + | + v +generic sidecar input + cache_key_sha, ABI, bottle URL, sha, bytes + formula revision, bottle rebuild + build/test/node/browser/upstream outcome lists + | + v +cargo xtask homebrew-sidecars +cargo xtask homebrew-validate + | + v +scripts/homebrew-publish-sidecars.sh + success or durable failed attempt + | + v +Homebrew VFS builder and Node/browser smoke +``` + +Control-flow invariants: + +- Formula `test do` must execute the Wasm through Kandelo, not as a host Linux + binary. +- Bottle bytes, formula bottle blocks, sidecars, and provenance must be + generated from the same build attempt. +- `cache_key_sha` must be computed for the Formula's Kandelo package identity + and target arch. A bottle with a wrong cache key is stale even if Homebrew + version selection would accept it. +- `sqlite` wasm64 publication can be attempted only if the Formula and package + build path really honor `HOMEBREW_KANDELO_ARCH=wasm64`. `bzip2` and `xz` + stay wasm32 unless their manifests and build paths are intentionally expanded. +- Browser compatibility can be recorded only after a browser smoke consumes a + precomposed VFS image and runs the package or package-specific consumer + through the normal browser host. +- Complete upstream-test outcome artifacts are package status metadata. They + do not decide default bottle availability unless the implementation + explicitly adds such a gate for one package. + +## Formula Shape + +### Shared Formula Pattern + +Each Formula should: + +1. Read `HOMEBREW_KANDELO_ROOT`, `HOMEBREW_KANDELO_ARCH`, + `HOMEBREW_KANDELO_NODE`, and `HOMEBREW_KANDELO_LLVM_BIN`. +2. Source or route through the worktree-local SDK by prepending + `/sdk/bin`. +3. Set `WASM_POSIX_DEP_VERSION`, `WASM_POSIX_DEP_SOURCE_URL`, + `WASM_POSIX_DEP_SOURCE_SHA256`, `WASM_POSIX_DEP_OUT_DIR`, + `WASM_POSIX_DEP_WORK_DIR`, and `WASM_POSIX_DEP_TARGET_ARCH`. +4. Install only the intended artifacts from the package output dir into the + Homebrew keg. +5. Keep `test do` small, deterministic, and runtime-backed by Kandelo. + +### SQLite Formula + +`sqlite` should be modeled as a library Formula, not as the incomplete +`sqlite-cli` package. + +Bottle contents: + +- `lib/libsqlite3.a` +- `include/sqlite3.h` +- `include/sqlite3ext.h` +- `lib/pkgconfig/sqlite3.pc` + +Formula `test do` should compile `packages/registry/sqlite/test/sqlite_basic.c` +or an equivalent inline test program against the installed keg and run the +result through Kandelo. This proves the installed library can be consumed and +executed without shipping a product CLI in the bottle. + +Sidecar link manifest should include library, header, and pkg-config paths +under the Homebrew prefix, even though no executable link is produced. The VFS +builder should either support library-only bottles directly or the pilot should +record the missing library-only VFS link behavior as a blocker before claiming +sqlite browser compatibility. + +### Bzip2 Formula + +`bzip2` should be a program Formula that installs `bin/bzip2` from the produced +Wasm file. The pilot should decide whether library byproducts are intentionally +part of the bottle: + +- Minimal program-only path: install `bin/bzip2` only. This best matches the + current `[[outputs]]` program manifest. +- Expanded hybrid path: install `bin/bzip2`, `lib/libbz2.a`, and `include/bzlib.h`. + This requires updating the package/Formula contract and treating the library + outputs as deliberate bottle contents. + +The minimal path is recommended for this pilot unless a downstream consumer +needs `libbz2.a` immediately. + +Formula `test do` should run `bzip2 --version` through Kandelo and, if feasible, +perform a file-based round trip that avoids writing compressed bytes to a PTY: +create an input file, compress to an output file, decompress, and compare the +content inside the Kandelo VFS. + +### Xz Formula + +`xz` should be a program Formula that installs `bin/xz` from the produced Wasm +file. Before publication, resolve the current package/source mismatch: + +- either keep package version `5.6.2` and make the build script use the + manifest URL and sha through the resolver-style env vars; +- or intentionally update the package and Formula to `5.6.4` with correct + source URL, sha, revision reasoning, and build-output validation. + +The first option is safer for the pilot because it avoids changing package +output identity beyond the Homebrew path. + +Formula `test do` should run `xz --version` through Kandelo and, if feasible, +perform a file-based compress/decompress round trip similar to `bzip2`. + +## Sidecars And Outcome Metadata + +The pilot needs generic sidecar generation before it can report truthful +package status for these packages. `scripts/homebrew-generate-sidecars-from-env.sh` +currently emits `hello`-specific outcome text and hardcoded browser/gallery +behavior. For this pilot, introduce or use a sidecar input file that records: + +- formula and arch; +- package kind: `library` or `program`; +- selected smoke command or test-consumer command; +- bottle build/install result; +- Formula test result; +- sidecar validation result; +- Node VFS smoke result; +- browser VFS smoke result or skip/failure reason; +- upstream test status for sqlite; +- artifact paths for logs, reports, VFS builder reports, and outcome lists. + +The generated sidecars should preserve the existing schema invariants while +allowing each package to report package-specific outcome lists. If the generic +input path cannot land cleanly inside this pilot, publish the package as +`failed`, `pending`, or `deferred` with a durable reason rather than reusing +misleading `hello` validation text. + +## Node And Browser Smoke Strategy + +Program packages: + +- Build a Homebrew VFS from the selected bottle and sidecars. +- On Node, spawn `/home/linuxbrew/.linuxbrew/bin/bzip2` or + `/home/linuxbrew/.linuxbrew/bin/xz` through `NodeKernelHost`. +- On browser, boot the same style of precomposed VFS image and run the + executable through the browser terminal. +- Prefer a file round-trip smoke for final compatibility status. Version-only + smoke may be recorded as partial if the file round trip is blocked by shell, + PTY, or VFS-image limitations. + +SQLite: + +- Build a Homebrew VFS from the sqlite bottle and sidecars. +- Compile a test-only `sqlite_basic.wasm` consumer against the poured keg, or + include a separately built validation artifact in the smoke image with + provenance that names the source and compiler inputs. +- On Node, run the consumer with `NodeKernelHost`. +- On browser, boot a precomposed smoke image that includes the sqlite keg plus + the test consumer and run the consumer through the browser terminal. +- Mark sqlite browser compatibility only if the consumer runs in the browser + host. A library-only bottle build does not prove browser runtime support. + +Negative smoke should remain part of the reusable harness: + +- ABI mismatch rejects before bottle fetch. +- Missing bottle or sha mismatch rejects before image save. +- Cache-key mismatch rejects before compatibility is recorded. + +## Upstream Test Status + +SQLite is the only pilot package with an explicit upstream-test-status goal. + +Existing runners: + +- `scripts/run-sqlite-tests.sh` runs SQL fixture files through `sqlite3.wasm`. +- `scripts/run-sqlite-upstream-tests.sh` runs individual upstream Tcl tests. +- `scripts/run-sqlite-official-tests.sh` runs SQLite `testrunner.tcl` on Node + or delegates to the browser runner. +- `scripts/run-sqlite-project-unit-tests.sh` combines Node and browser + official-test runs and writes summaries. + +The pilot should not require the full SQLite suite to pass before bottle +availability. It should publish upstream status with: + +- permutation (`veryquick`, `full`, or `all`); +- host (`node`, `browser`, or both); +- total, passed, failed, skipped/omitted, running, ready, timeout, and + incomplete counts when available; +- complete failure list with job/test name, state, case count, error count, + elapsed time, and artifact path; +- complete skipped/omitted list with reasons when the harness exposes them; +- explicit "missing category" notes where current harnesses cannot emit a + required list, plus a focused follow-up before the convoy claims complete + upstream outcome-list support. + +`bzip2` and `xz` should record upstream test status as `unavailable` or +`deferred` with a reason if this pilot only runs smoke tests. Do not imply that +version/round-trip smoke is an upstream full-test result. + +## Implementation Sequence + +1. Confirm the worktree is clean and preflighted for `kd-1mr.2`. +2. Bring the worktree to the intended Homebrew foundation commit if the branch + is missing required `hello`, VFS builder, or sidecar work. +3. Add or update Formulae in the tap fixture or tap worktree for `sqlite`, + `bzip2`, and `xz`. +4. Make the package build paths Formula-safe: + - `sqlite`: use the resolver-style library output path and avoid the legacy + CLI side path. + - `bzip2`: prevent repo sysroot/local-binary side effects from being part + of Formula installation, or isolate them behind `WASM_POSIX_DEP_OUT_DIR`. + - `xz`: resolve the 5.6.2 vs 5.6.4 source mismatch before building. +5. Generalize sidecar input so package-specific validation and outcome lists + are truthful for non-hello packages. +6. Build local dry-run bottles for the selected arches: + - `sqlite`: wasm32 and wasm64 if the build path supports both. + - `bzip2`: wasm32. + - `xz`: wasm32. +7. Validate generated sidecars and provenance with `cargo xtask + homebrew-validate`. +8. Materialize VFS images from the dry-run sidecars and run Node smokes. +9. Run browser smokes or record explicit host-failure status with artifact + paths and next action. +10. Run SQLite upstream-test status jobs with durable outcome lists. +11. If local dry-run evidence is satisfactory, run trusted publication against + `Automattic/kandelo-homebrew`. Keep local dry-run and trusted GHCR/tap + evidence separate in bead notes and sidecars. +12. Create focused follow-up beads only for evidence-backed blockers or for the + next specific package wave. Do not create broad waves from assumptions. + +## Test And Documentation Plan + +Required local checks for implementation work: + +- `bash scripts/test-homebrew-publish-workflow.sh` +- `scripts/verify-homebrew-kandelo-platform-tags.sh` +- Homebrew dry-run bottle build for every selected `(formula, arch)` +- `cargo xtask homebrew-validate --tap-root ` +- VFS builder tests affected by sidecar/link behavior: + `cd host && npx vitest run test/homebrew-vfs-planner.test.ts test/homebrew-vfs-builder.test.ts test/homebrew-vfs-fetch.test.ts` +- Package-specific Node smoke artifacts for sqlite, bzip2, and xz +- Package-specific browser smoke artifacts or explicit failure/skip status +- SQLite upstream status command, preferably: + `scripts/run-sqlite-project-unit-tests.sh --host both --permutation veryquick` + for pilot evidence before considering a broader `full` run + +Required outcome artifacts: + +- Build/install pass/fail list for each Formula/arch. +- Sidecar validation pass/fail list. +- Node smoke passed, failed, and skipped lists. +- Browser smoke passed, failed, and skipped lists with reasons. +- SQLite upstream passed, failed, skipped/omitted, and incomplete lists, or + documented missing categories plus follow-up. +- Bottle build logs, bottle JSON, bottle archive sha/byte report, sidecar + payload, VFS builder report, and browser smoke trace or screenshot artifacts. + +Reference docs after implementation: + +- Update `docs/homebrew-publishing.md` if the pilot generalizes non-hello + sidecar input, non-hello browser smoke, library Formula support, or outcome + metadata semantics. +- Update `docs/package-management.md` only if the package/cache/revision + contract changes. Do not update it for tap-only Formula wording. +- Update package-specific docs or notes if `bzip2`/`xz` build scripts are + converted from direct sysroot writers to resolver-style outputs. + +Because this design artifact is docs-only, it requires only lightweight docs +verification. Runtime and package validation belongs to the implementation +work. + +## Alternatives Considered + +### Port sqlite alone + +Rejected for the pilot. SQLite is the important dependency-root case, but it +does not exercise program link manifests, executable smoke commands, or small +CLI publication capacity. Adding `bzip2` and `xz` gives useful signal without +starting the full small-CLI wave. + +### Port only small CLI packages + +Rejected. The migration needs another dependency-root proof after `zlib`, +especially one with upstream-test status and wasm64 considerations. + +### Treat sqlite as a CLI Formula + +Rejected for this pilot. The current publishable registry package is the +library. `sqlite-cli` is manifest-incomplete and should be revived or removed +through a separate decision. + +### Reuse current bzip2/xz scripts unchanged + +Rejected as the default. They write into the repo sysroot and local-binaries, +which is not a clean Homebrew keg contract. The pilot may reuse their compile +knowledge, but Formula installation must be isolated to declared outputs. + +### Mark browser support from Node VFS smoke + +Rejected. Browser is a peer host. Node VFS materialization is necessary but not +sufficient for browser compatibility. + +### Gate sqlite bottle availability on full upstream tests + +Rejected. Upstream test status must be visible, but full-suite success is not +the default bottle gate. A failing upstream subset should publish a package +status and failure list without hiding a buildable/installable bottle. + +## Risks And Mitigations + +Registry and Formula divergence: + +- Risk: Formulae and `packages/registry` scripts become two independent + recipes during the bridge period. +- Mitigation: keep Formulae authoritative for Homebrew behavior, isolate any + registry-script reuse behind explicit env vars, and record bridge deletion + criteria in the broader replacement docs. + +Misleading sidecars: + +- Risk: reusing `hello`-specific sidecar text for non-hello packages would + claim validation that did not run. +- Mitigation: add generic sidecar outcome input before success publication, or + publish failure/deferred state with a blocker. + +Library smoke ambiguity: + +- Risk: a sqlite bottle can build and pour while no runtime consumer has proven + the library works. +- Mitigation: require a test-only consumer on Node and browser before marking + runtime compatibility. + +Version/source drift: + +- Risk: `xz` package metadata and build script fetch different versions. +- Mitigation: resolve source/version identity before any bottle evidence is + accepted. + +Browser parity drift: + +- Risk: Node smokes pass but browser VFS boot, terminal execution, + SharedArrayBuffer setup, or browser fetch path fails. +- Mitigation: record browser smoke separately and keep host-specific failures + visible. + +Outcome-list gaps: + +- Risk: existing test runners emit summaries and failures but not complete + passed/skipped lists. +- Mitigation: add exporters where reasonable; otherwise record the missing + category and create a focused follow-up before claiming convoy completion. + +Unnecessary rebuild churn: + +- Risk: implementers bump `build.toml.revision` to force Homebrew rebuilds. +- Mitigation: use Homebrew formula revision or bottle rebuild for bottle + selection. Bump Kandelo package revision only when package archive output + bytes legitimately change. + +## Open Questions + +- Should `bzip2` and `xz` remain program-only bottles, or should their library + byproducts become deliberate Homebrew bottle outputs? +- Does the VFS builder already handle library-only bottles well enough for + sqlite browser smoke, or does the pilot need a small extension for + validation-only consumers? +- Should sqlite wasm64 be trusted-published in this pilot, or should wasm64 + stay local evidence until the sidecar/browser story is proven on wasm32? +- What exact generic sidecar input format should replace the current + `hello`-specific outcome generation? +- Which SQLite upstream-test permutation is the right first durable status: + `veryquick`, `full`, or both with different publication meanings? +- Should `sqlite-cli` be revived as a separate Formula after the library pilot, + or removed from the accepted package set before registry deletion? diff --git a/docs/plans/2026-06-30-homebrew-js-db-service-runtime-packages-design.md b/docs/plans/2026-06-30-homebrew-js-db-service-runtime-packages-design.md new file mode 100644 index 0000000000..ccbceefe7c --- /dev/null +++ b/docs/plans/2026-06-30-homebrew-js-db-service-runtime-packages-design.md @@ -0,0 +1,388 @@ +# Homebrew JS, DB, And Service Runtime Package Wave + +Bead: `kd-nlyy` + +Package set: `spidermonkey`, `spidermonkey-node`, `node`, `redis`, `nginx`, +`mariadb` + +## Problem Statement + +Kandelo needs the JavaScript runtime, database, and service-runtime packages +ported to the Homebrew bottle model without weakening the platform contract. +This wave is riskier than small CLI and library waves because the packages +exercise large C/C++ builds, multi-output formulae, threads, fork +instrumentation, TCP services, database bootstrap state, Node-compatible +JavaScript behavior, and browser-runtime claims. + +The implementation must produce either working Homebrew formulae, bottles, +sidecars, provenance, Node/browser smoke evidence, and upstream-test status for +each package, or visible failed/deferred Homebrew status with concrete reasons. +Failures should identify whether the problem is package recipe state, tap +automation, SDK/sysroot inputs, POSIX/kernel behavior, host runtime behavior, +browser limits, or upstream test infrastructure. + +## Non-Goals + +- Do not implement the package ports in this design bead. +- Do not publish live tap state from the main `Automattic/kandelo` repository. + Live formulae, bottle blocks, `Kandelo/` sidecars, provenance, and release + assets belong in `Automattic/kandelo-homebrew`. +- Do not document guest `brew install` as user-facing support until guest + Homebrew install is separately validated. +- Do not mark a package browser-compatible from a successful bottle build or a + Node smoke alone. +- Do not make package-local patches hide fixable Kandelo POSIX, libc, VFS, + kernel, fork, socket, or host-runtime defects. +- Do not treat helper/source entries such as `pcre2-source`, `node-compat`, or + `npm` as standalone formulae in this wave; they are owner-local resources or + sidecar/tooling data per `kd-u4sz`. + +## Users And Operator Workflows + +Maintainers need reviewable formulae and sidecar metadata that explain which +runtime claims are supported, which were skipped or failed, and why. The +metadata must remain useful after workflow logs expire. + +Package porters need a safe sequence that starts with recipe normalization and +small runtime checks before spending CI capacity on long SpiderMonkey and +MariaDB builds. + +Tap publishers need trusted workflow inputs that build through Homebrew, +generate bottle blocks and Kandelo sidecars from the same bottle bytes, and +publish failure status without deleting last-green metadata. + +Node runtime operators need precomposed Homebrew VFS images that can run the +published binaries through `NodeKernelHost`, including service startup and +client request paths. + +Browser operators need conservative compatibility claims. Browser support means +a wasm32 bottle was poured into a precomposed VFS image, booted in the browser +UI, and exercised with a package-appropriate smoke. Browser failures or skips +must stay visible instead of hiding the package from all status reporting. + +## Current Package Inventory + +`spidermonkey` builds Firefox ESR SpiderMonkey as `js.wasm`, depends on +`libcxx`, `openssl`, and `zlib`, disables fork instrumentation, and already has +Node and browser stress tests. It should be the first runtime in this wave +because `spidermonkey-node` and `node` depend on its build output. + +`spidermonkey-node` and `node` both use +`packages/registry/spidermonkey-node/build-spidermonkey-node.sh` and produce +`node.wasm`. Their current `package.toml` files use Kandelo-local source URLs +with zero sha256 placeholders, so implementation must make their formula +provenance explicit. For the first Homebrew pass, keep bottle-producing formulae +self-contained unless the sidecar and VFS builder can represent a safe +formula-alias/dependency-owned binary model. + +`redis` declares version `7.2.5`, but `build-redis.sh` currently builds +`7.2.7`. That mismatch is a blocking recipe-normalization item. The script also +needs the same SDK activation and `WASM_POSIX_DEP_*` source/out-dir contract as +the other Homebrew formulae before bottle publication. Redis installs +`redis-server` and `redis-cli`, so sidecars and link manifests must expose both +links. + +`nginx` uses a wrapper around `build-nginx-local.sh`, applies fork +instrumentation, and has existing Node-side HTTP tests. Its Homebrew smoke +should cover `nginx -t` plus a real static HTTP request through Kandelo, not +just `--version`. + +`mariadb` declares wasm32 and wasm64 support, builds host helper executables, +cross-builds `mariadbd` and `mysqltest`, and carries mysql-test data. It is the +heaviest item in this wave. Publish wasm32 first, then wasm64 if the wasm64 +sysroot and LLVM `-O1` path remain green; otherwise publish a wasm64 failure +status with the exact build or runtime reason. Browser compatibility should +start as skipped or failed unless a dedicated browser database smoke proves the +claim. + +Current checked-in package manifests for these packages still declare +`kernel_abi = 7` while the repository ABI is `16`. Implementation must normalize +ABI metadata before building current bottles and must not confuse this with an +`ABI_VERSION` bump. + +## Dependency And Baseline Requirements + +Implementation should start from `origin/main` after the dependency-root wave +from `kd-zq4n` has landed, or explicitly merge +`origin/gascity/kd-1mr/kd-zq4n-dependency-roots` into the bead branch. That +wave adds formulae for `openssl`, `libcxx`, `libxml2`, `libpng`, `libcurl`, +`ncurses`, extends sidecar generation to derive program links from +`package.toml` outputs, and adds reusable Homebrew package Node smoke tooling. + +The `kd-u4sz` classification is also a design dependency: + +- `pcre2-source` is folded into the owning MariaDB formula as a source/helper + input. +- `node-compat` and `npm` are sidecar/tooling-owned data for the SpiderMonkey + and Node-compatible runtime path. +- Helper entries should not become standalone Homebrew packages unless later + evidence shows a real shared runtime need. + +## Architecture And Data Flow + +The main repository owns package build scripts, validation tooling, reusable +workflows, sidecar schemas, VFS planner/builder code, smoke tests, and reference +docs. The live tap repository owns formulae, bottle blocks, generated +`Kandelo/` sidecars, provenance reports, GHCR bottle URLs, and +`bottles-abi-v` release assets. + +Formulae should call the normal Kandelo build scripts through the trusted +Homebrew workflow. Each formula must set: + +- `HOMEBREW_KANDELO_ROOT` +- `HOMEBREW_KANDELO_ARCH` +- `HOMEBREW_KANDELO_NODE` +- `HOMEBREW_KANDELO_LLVM_BIN` +- `WASM_POSIX_DEP_VERSION` +- `WASM_POSIX_DEP_SOURCE_URL` +- `WASM_POSIX_DEP_SOURCE_SHA256` +- `WASM_POSIX_DEP_OUT_DIR` +- `WASM_POSIX_DEP_WORK_DIR` +- `WASM_POSIX_DEP_TARGET_ARCH` + +Formulae install only the produced Wasm artifacts into the keg. Kandelo VFS +planning data, browser compatibility, cache keys, and validation evidence stay +in generated sidecars, not in Formula Ruby. + +Trusted CI builds each `(formula, arch)` with +`.github/workflows/reusable-homebrew-bottle-publish.yml`, applies the Kandelo +Homebrew bottle-tag patch in a temporary Homebrew worktree, runs `brew test` +through Kandelo, uploads bottle bytes, generates sidecars, validates metadata, +and publishes success or failure status. + +Sidecar generation must use the package output list, not a hard-coded +`bin/` link. This is required for `redis` (`redis-server`, +`redis-cli`) and `mariadb` (`mariadbd`, `mysqltest`), and it avoids future +breakage for shared SpiderMonkey/Node artifacts. + +The VFS planner consumes `Kandelo/metadata.json`, resolves the requested package +closure, rejects ABI/cache-key/path/link drift, and selects only bottles whose +runtime metadata supports the requested host. The Node builder then verifies +bottle bytes and pours/link-manifests into a precomposed Homebrew-prefix VFS. + +Node smokes should boot the poured VFS through `NodeKernelHost`. Browser smokes +should boot a published wasm32 precomposed VFS through the browser UI and run a +package-appropriate command or service probe before setting +`browser_compatible = true`. + +## Package-Specific Design + +### SpiderMonkey + +Add a `spidermonkey` formula that builds `js.wasm` through +`build-spidermonkey.sh` and installs it as `bin/js`. Formula `test do` should +run `js -e 'print(1+1)'` through Kandelo. Node smoke should run a broader shell +sample covering file I/O, shared memory worker startup when available, exception +reporting, and non-zero syntax/runtime failure behavior. + +Browser smoke should adapt the existing SpiderMonkey browser stress path to the +Homebrew VFS image. It should prove repeated `js` launches do not leak +processes and stay inside the memory ceiling before sidecars record browser +support. + +### SpiderMonkey Node Runtime And `node` + +Keep the SpiderMonkey Node-compatible runtime explicit: `spidermonkey-node` +owns the `node.wasm` build path, while `node` preserves the existing package +name and user-facing command. If the implementation chooses separate formulae, +both must have clear provenance and must not pretend to be upstream Node.js. If +it chooses an alias/dependency model, the sidecar and VFS builder must first +prove that dependency-owned binaries and links are represented safely. + +Node smoke should cover: + +- `node --version` +- `node -e` with `process`, `console`, `Buffer`, `path`, `util`, and `assert` +- CommonJS package resolution and symlinked package bins +- ES module shebang behavior and visible exception messages +- selected crypto/zlib parity tests +- npm install only if `npm` helper data is available and the outcome is + recorded as pass/fail/skip with reason + +Browser smoke can start with `node --version` and a small `node -e` sample, but +must not claim npm/browser support unless the browser fetch, filesystem, and +runtime behavior are actually exercised. + +### Redis + +Normalize the recipe before writing the formula: + +- make `package.toml` and `build-redis.sh` agree on the Redis version and + source sha; +- source `sdk/activate.sh`; +- honor `WASM_POSIX_DEP_SOURCE_URL`, `WASM_POSIX_DEP_SOURCE_SHA256`, + `WASM_POSIX_DEP_VERSION`, `WASM_POSIX_DEP_OUT_DIR`, and + `WASM_POSIX_DEP_WORK_DIR`; +- install both `redis-server` and `redis-cli` into the Homebrew keg. + +Formula `test do` should at minimum run `redis-server --version` and +`redis-cli --version` through Kandelo. Node smoke should start `redis-server` +inside the Homebrew VFS, wait for readiness on a loopback port, run +`redis-cli PING`, verify `PONG`, and shut the server down cleanly. + +Browser smoke should be a separate pass. If browser loopback/service wiring is +not ready, publish `browser_compatible = false` with a skipped or failed +browser-smoke outcome explaining the exact missing boundary. + +### nginx + +Add an `nginx` formula that calls the existing build wrapper and installs +`bin/nginx`. The formula must preserve the current fork-instrument-last rule. +Formula `test do` should run `nginx -t` through Kandelo with a minimal config. + +Node smoke should pour the Homebrew VFS, launch nginx with `master_process on`, +send an HTTP request through the TCP bridge, verify the static response, and +stop the server. This should reuse the existing `packages/registry/nginx/test` +coverage rather than creating a formula-only runner that bypasses the normal +kernel path. + +Browser smoke should use the service-worker HTTP injection path only after a +precomposed Homebrew VFS image boots in the browser. A successful browser smoke +should verify the response body and server header; otherwise publish explicit +browser skip/failure status. + +### MariaDB + +Add a `mariadb` formula that builds and installs `bin/mariadbd` and +`bin/mysqltest`. Keep `pcre2-source` as a MariaDB-owned source/helper input, +not a standalone runtime dependency. The formula must preserve per-arch sysroot +and build directories so wasm32 and wasm64 artifacts cannot cross-link. + +Formula `test do` should verify the Wasm headers and run lightweight +Kandelo-executed checks that do not require a long server bootstrap. Node smoke +should do the real service proof: + +1. Build a Homebrew VFS containing the poured MariaDB bottle. +2. Bootstrap system tables in a writable data directory. +3. Start `mariadbd`. +4. Run `mysqltest` or an equivalent Kandelo client query for `SELECT 1`. +5. Record server stderr, startup time, query result, and shutdown status. + +The upstream mysql-test suite is too large for a single unqualified gate. The +implementation should publish a complete upstream-test status artifact naming +which mysql-test commands or subsets ran, which were skipped, and why. If no +upstream subset runs, that must be recorded as skipped with a reason and a +follow-up or blocker bead. + +Browser smoke should initially be conservative. MariaDB uses substantial memory +and thread/service behavior. A browser-compatible claim requires a dedicated +Chromium smoke that boots the Homebrew VFS, starts or bootstraps MariaDB within +browser limits, runs a query, and records artifacts. Otherwise publish +browser-compatible false with the concrete browser limit or missing test path. + +## Alternatives Considered + +Build all six packages in one workflow matrix without pre-normalization. +Rejected because stale ABI metadata, the Redis version mismatch, and +multi-output sidecars would turn predictable recipe problems into long CI +failures. + +Treat `node` as only a tap alias for `spidermonkey-node`. Deferred. It is the +least duplicate model, but the current sidecar and VFS metadata must first +prove alias/dependency-owned executable links are safe and understandable. + +Make service packages Node-only and skip browser work entirely. Rejected as a +default because Kandelo treats browser and Node hosts as peer product surfaces. +Browser support may be skipped or failed with reasons, but the status must be +explicit. + +Use existing package archive `index.toml` data as Homebrew metadata. Rejected. +Homebrew bottle selection belongs to formula bottle blocks and GHCR bottle URLs; +Kandelo package archives and Homebrew sidecars are separate publication +contracts. + +Create standalone formulae for `pcre2-source`, `node-compat`, or `npm`. +Rejected for this wave based on `kd-u4sz`; they are helper/source inputs owned +by MariaDB or SpiderMonkey/Node runtime formulae. + +## Risks And Mitigations + +| Risk | Mitigation | +|---|---| +| Old `kernel_abi` values produce stale or rejected package metadata. | Normalize package manifests to current ABI before bottle builds; do not bump `ABI_VERSION`; report ABI evidence in sidecars. | +| Redis version drift builds bytes that do not match the manifest. | Fix the manifest/script/source-sha mismatch before formula work and make the formula pass manifest values through `WASM_POSIX_DEP_*`. | +| Multi-output packages publish incomplete VFS links. | Require sidecar generation from `package.toml` outputs and add validation that poured VFS images contain every declared executable. | +| Build-time dependencies become accidental runtime VFS dependencies. | Keep helper/source inputs owner-local; use sidecar dependencies only for packages that must be present in the poured runtime image. | +| Browser compatibility is overclaimed. | Require wasm32 precomposed VFS plus Playwright/browser smoke evidence before `browser_compatible = true`. | +| Service smoke tests become demo-specific shortcuts. | Reuse package-owned Node/browser runtime paths and exercise Kandelo kernel networking, fork/thread behavior, and VFS state. | +| Long builds hide root causes in truncated logs. | Preserve full build logs, publish outcome lists, and record failure layer classification per formula and arch. | +| MariaDB wasm64 consumes capacity but fails late. | Build wasm32 first, then wasm64 with explicit `-O1`/sysroot status; publish wasm64 failure metadata if necessary. | +| Duplicate Node-compatible artifacts create maintenance burden. | Start with explicit provenance and self-contained formulae, then create a follow-up for alias/dedup sidecar support if duplicate bottles become costly. | + +## Implementation Sequence + +1. Rebase or merge onto the dependency-root Homebrew wave from `kd-zq4n` or a + main branch that contains it. +2. Run recipe audit scripts for this wave: manifest ABI, version/source sha, + declared outputs, `build.toml` revision policy, SDK activation, and + `WASM_POSIX_DEP_*` compliance. +3. Normalize package recipes without changing runtime behavior: current ABI + metadata, Redis version/source alignment, and any missing out-dir/work-dir + handling. +4. Add or update formulae in the tap fixture/template for `spidermonkey`, + `spidermonkey-node`, `node`, `redis`, `nginx`, and `mariadb`. +5. Extend sidecar generation and validation only where the dependency-root wave + is insufficient for this package set, especially service smoke outcome + fields and package-specific runtime commands. +6. Build and dry-run publish `spidermonkey` first, then + `spidermonkey-node`/`node`, then `redis`, then `nginx`, then `mariadb`. +7. For each successful bottle, generate and validate sidecars, build a + Homebrew VFS image, and run the package-specific Node smoke. +8. For each package and arch, run or explicitly record browser smoke status. + Only successful wasm32 browser smokes may set browser-compatible metadata. +9. Record upstream-test support/status artifacts for every package. Use + complete pass/fail/skipped outcome lists, with skipped reasons. +10. Run the relevant focused checks and then the project full gate required by + the implementation bead before closing or publishing completion claims. + +## Test And Documentation Plan + +Focused package checks: + +- Ruby syntax and Homebrew audit/style checks for each formula. +- `bash -n` for changed build and Homebrew scripts. +- Sidecar generation and `cargo xtask homebrew-validate` over accumulated tap + state. +- VFS planner/builder tests for multi-output links and dependency closure. +- Package-specific Node smokes for every successful formula/arch. +- Browser smokes for wasm32 packages where browser support is claimed. + +Required full-gate commands before implementation closure: + +- `cargo test -p kandelo --target aarch64-apple-darwin --lib` +- `cd host && npx vitest run` +- `scripts/run-libc-tests.sh` +- `scripts/run-posix-tests.sh` +- `bash scripts/check-abi-version.sh` + +Outcome artifacts: + +- bottle build/test passed, failed, and skipped lists; +- sidecar/provenance validation passed, failed, and skipped lists; +- Node smoke passed, failed, and skipped lists; +- browser smoke passed, failed, and skipped lists; +- upstream test passed, failed, and skipped lists with reasons; +- complete failure list before and after any fixes. + +Documentation updates should target `docs/homebrew-publishing.md`, +`docs/package-management.md`, `docs/porting-guide.md`, +`docs/browser-support.md`, and `homebrew/kandelo-homebrew/README.md` if this +wave changes formula authoring, package status, runtime claim rules, browser +gallery behavior, or operational runbooks. Do not update user-facing install +instructions until guest Homebrew install is validated. + +## Open Questions + +1. Should `node` remain a separate bottle-producing formula for v1, or should a + follow-up add explicit alias/dependency-owned executable sidecar support? +2. Should sidecar dependencies distinguish build-time formula inputs from + runtime VFS closure dependencies before MariaDB and SpiderMonkey publish? +3. Which Redis and MariaDB browser smokes are acceptable first claims under + browser memory, networking, and service-worker constraints? +4. Should MariaDB wasm64 be attempted in the same implementation PR as wasm32, + or published as an explicit failed/deferred arch status after wasm32 proves + the core path? +5. What upstream-test subset is considered meaningful for MariaDB without + turning this package wave into a full mysql-test convoy? +6. Should duplicate SpiderMonkey/Node bottle bytes be deduplicated at GHCR or + left as separate Homebrew bottle artifacts for clarity? diff --git a/homebrew/kandelo-homebrew/.github/workflows/publish-hello.yml b/homebrew/kandelo-homebrew/.github/workflows/publish-hello.yml index ea8df7d42b..6e330f24f5 100644 --- a/homebrew/kandelo-homebrew/.github/workflows/publish-hello.yml +++ b/homebrew/kandelo-homebrew/.github/workflows/publish-hello.yml @@ -210,6 +210,9 @@ jobs: exit 1 fi bash scripts/dev-shell.sh bash scripts/build-musl.sh + if [ "${{ matrix.arch }}" = "wasm64" ]; then + bash scripts/dev-shell.sh bash scripts/build-musl.sh --arch wasm64posix + fi - name: Build Kandelo kernel shell: bash diff --git a/homebrew/kandelo-homebrew/Formula/README.md b/homebrew/kandelo-homebrew/Formula/README.md index 17a19c06e6..c4e29e0519 100644 --- a/homebrew/kandelo-homebrew/Formula/README.md +++ b/homebrew/kandelo-homebrew/Formula/README.md @@ -1,8 +1,9 @@ # Formula Directory The `Automattic/kandelo-homebrew` tap places Homebrew formulae here. This -main-repo scaffold includes `hello.rb` so formula, bottle, sidecar, and smoke -logic can be reviewed alongside the Kandelo implementation that consumes it. +main-repo scaffold includes representative program and library formulae so +formula, bottle, sidecar, and smoke logic can be reviewed alongside the Kandelo +implementation that consumes it. Formulae should use normal Homebrew DSL, including `depends_on`, `bottle do`, `revision`, `rebuild`, and `test do`, while any Kandelo-specific VFS planning diff --git a/homebrew/kandelo-homebrew/Formula/bzip2.rb b/homebrew/kandelo-homebrew/Formula/bzip2.rb new file mode 100644 index 0000000000..257fee342e --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/bzip2.rb @@ -0,0 +1,68 @@ +require "shellwords" + +class Bzip2 < Formula + desc "bzip2 compression tool for Kandelo" + homepage "https://sourceware.org/bzip2/" + url "https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz" + sha256 "ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269" + license "bzip2-1.0.6" + + skip_clean "bin/bzip2" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://sourceware.org/pub/bzip2/bzip2-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + + system "bash", "#{root}/packages/registry/bzip2/build-bzip2.sh" + chmod 0755, out_dir/"bzip2.wasm" + bin.install out_dir/"bzip2.wasm" => "bzip2" + chmod 0755, bin/"bzip2" + end + + test do + bzip2 = bin/"bzip2" + assert_equal "\0asm".b, File.binread(bzip2, 4) + + root = kandelo_root + configure_kandelo_environment(root) + + test_wasm = testpath/"bzip2.wasm" + File.binwrite(test_wasm, File.binread(bzip2)) + output = shell_output( + "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape} --help 2>&1", + ) + assert_match "bzip2", output.scrub + end +end diff --git a/homebrew/kandelo-homebrew/Formula/libcurl.rb b/homebrew/kandelo-homebrew/Formula/libcurl.rb new file mode 100644 index 0000000000..39002d8cf3 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/libcurl.rb @@ -0,0 +1,130 @@ +require "shellwords" + +class Libcurl < Formula + desc "URL transfer library for Kandelo" + homepage "https://curl.se/libcurl/" + url "https://curl.se/download/curl-8.11.1.tar.xz" + sha256 "c7ca7db48b0909743eaef34250da02c19bc61d4f1dcedd6603f109409536ab56" + license "curl" + + depends_on "automattic/kandelo-homebrew/openssl" + depends_on "automattic/kandelo-homebrew/zlib" + + skip_clean "lib/libcurl.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + else odie "libcurl is currently packaged for wasm32 only, got #{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://curl.se/download/curl-#{version}.tar.xz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "c7ca7db48b0909743eaef34250da02c19bc61d4f1dcedd6603f109409536ab56" + ENV["WASM_POSIX_DEP_ZLIB_DIR"] = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + ENV["WASM_POSIX_DEP_OPENSSL_DIR"] = Formula["automattic/kandelo-homebrew/openssl"].opt_prefix + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/libcurl/build-libcurl.sh" + + lib.install out_dir/"lib/libcurl.a" + include.install out_dir/"include/curl" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/libcurl.pc" + inreplace lib/"pkgconfig/libcurl.pc", /^prefix=.*/, "prefix=#{prefix}" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + openssl = Formula["automattic/kandelo-homebrew/openssl"].opt_prefix + zlib = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + + test_src = testpath/"libcurl-smoke.c" + test_wasm = testpath/"libcurl-smoke.wasm" + test_src.write <<~C + #include + #include + + int main(void) { + CURLcode rc = curl_global_init(CURL_GLOBAL_DEFAULT); + if (rc != CURLE_OK) { + printf("curl_global_init failed: %d\\n", (int)rc); + return 1; + } + printf("libcurl %s ok\\n", curl_version()); + curl_global_cleanup(); + return 0; + } + C + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}", + "-I#{openssl}/include", + test_src, + "#{lib}/libcurl.a", + "#{openssl}/lib/libssl.a", + "#{openssl}/lib/libcrypto.a", + "#{zlib}/lib/libz.a", + "-ldl", + "-lm", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "libcurl", output + assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/libcurl.pc") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/libcxx.rb b/homebrew/kandelo-homebrew/Formula/libcxx.rb new file mode 100644 index 0000000000..8aac0a898e --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/libcxx.rb @@ -0,0 +1,145 @@ +require "shellwords" + +class Libcxx < Formula + desc "LLVM libc++ and libc++abi static libraries for Kandelo" + homepage "https://libcxx.llvm.org/" + url "https://github.com/llvm/llvm-project/releases/download/llvmorg-21.1.7/llvm-project-21.1.7.src.tar.xz" + sha256 "e5b65fd79c95c343bb584127114cb2d252306c1ada1e057899b6aacdd445899e" + license "Apache-2.0" => { with: "LLVM-exception" } + + depends_on "cmake" => :build + + skip_clean "lib/libc++.a" + skip_clean "lib/libc++abi.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + when "wasm64" then "wasm64posix" + else odie "unsupported HOMEBREW_KANDELO_ARCH=#{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/#{kandelo_arch == "wasm64" ? "sysroot64" : "sysroot"}" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + ENV["LLVM_VERSION"] ||= version.to_s + ENV["WASM_POSIX_LLVM_LIBCXX_SOURCE"] ||= buildpath.to_s + ENV["WASM_POSIX_LLVM_LIBUNWIND_SOURCE"] ||= buildpath.to_s + + %w[ + LLVM_PREFIX + LLVM_VERSION + WASM_POSIX_LLVM_LIBCXX_SOURCE + WASM_POSIX_LLVM_LIBUNWIND_SOURCE + ].each do |key| + odie "#{key} must be provided by scripts/dev-shell.sh for libcxx" if ENV[key].to_s.empty? + end + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/libcxx/build-libcxx.sh" + + lib.install out_dir/"lib/libc++.a" + lib.install out_dir/"lib/libc++abi.a" + (include/"c++").mkpath + (include/"c++").install out_dir/"include/c++/v1" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + test_src = testpath/"libcxx-smoke.cpp" + test_wasm = testpath/"libcxx-smoke.wasm" + test_src.write <<~CPP + #include + #include + #include + #include + + int main() { + try { + std::vector values; + values.push_back("kandelo"); + if (values.size() != 1) throw std::runtime_error("vector failed"); + throw std::runtime_error(values[0]); + } catch (const std::runtime_error& err) { + std::printf("libcxx caught %s\\n", err.what()); + return 0; + } + } + CPP + + system "#{kandelo_tool_prefix}-c++", + "-std=c++20", + "-fexceptions", + "-fwasm-exceptions", + "-mexception-handling", + "-mllvm", + "-wasm-enable-sjlj", + "-mllvm", + "-wasm-use-legacy-eh=false", + "-nostdinc++", + "-isystem", + "#{include}/c++/v1", + test_src, + "#{lib}/libc++.a", + "#{lib}/libc++abi.a", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "libcxx caught kandelo", output + end +end diff --git a/homebrew/kandelo-homebrew/Formula/libpng.rb b/homebrew/kandelo-homebrew/Formula/libpng.rb new file mode 100644 index 0000000000..2fb4d6f880 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/libpng.rb @@ -0,0 +1,117 @@ +require "shellwords" + +class Libpng < Formula + desc "PNG image library for Kandelo" + homepage "http://www.libpng.org/pub/png/libpng.html" + url "https://download.sourceforge.net/libpng/libpng-1.6.43.tar.xz" + sha256 "6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c" + license "libpng-2.0" + + depends_on "automattic/kandelo-homebrew/zlib" + + skip_clean "lib/libpng16.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + else odie "libpng is currently packaged for wasm32 only, got #{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://download.sourceforge.net/libpng/libpng-#{version}.tar.xz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "6a5ca0652392a2d7c9db2ae5b40210843c0bbc081cbd410825ab00cc59f14a6c" + ENV["WASM_POSIX_DEP_ZLIB_DIR"] = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/libpng/build-libpng.sh" + + lib.install out_dir/"lib/libpng16.a" + include.install out_dir/"include/libpng16" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/libpng16.pc" + inreplace lib/"pkgconfig/libpng16.pc", /^prefix=.*/, "prefix=#{prefix}" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + zlib = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + + test_src = testpath/"libpng-smoke.c" + test_wasm = testpath/"libpng-smoke.wasm" + test_src.write <<~C + #include + #include + + int main(void) { + printf("libpng %s ok\\n", png_get_libpng_ver(NULL)); + return 0; + } + C + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}/libpng16", + test_src, + "#{lib}/libpng16.a", + "#{zlib}/lib/libz.a", + "-lm", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "libpng #{version} ok", output + assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/libpng16.pc") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/libxml2.rb b/homebrew/kandelo-homebrew/Formula/libxml2.rb new file mode 100644 index 0000000000..fbd2f4cac9 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/libxml2.rb @@ -0,0 +1,111 @@ +require "shellwords" + +class Libxml2 < Formula + desc "XML parser library for Kandelo" + homepage "https://gitlab.gnome.org/GNOME/libxml2/-/wikis/home" + url "https://download.gnome.org/sources/libxml2/2.13/libxml2-2.13.8.tar.xz" + sha256 "277294cb33119ab71b2bc81f2f445e9bc9435b893ad15bb2cd2b0e859a0ee84a" + license "MIT" + + depends_on "automattic/kandelo-homebrew/zlib" + + skip_clean "lib/libxml2.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + when "wasm64" then "wasm64posix" + else odie "unsupported HOMEBREW_KANDELO_ARCH=#{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/#{kandelo_arch == "wasm64" ? "sysroot64" : "sysroot"}" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://download.gnome.org/sources/libxml2/2.13/libxml2-#{version}.tar.xz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "277294cb33119ab71b2bc81f2f445e9bc9435b893ad15bb2cd2b0e859a0ee84a" + ENV["WASM_POSIX_DEP_ZLIB_DIR"] = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/libxml2/build-libxml2.sh" + + lib.install out_dir/"lib/libxml2.a" + include.install out_dir/"include/libxml" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/libxml-2.0.pc" + inreplace lib/"pkgconfig/libxml-2.0.pc", /^prefix=.*/, "prefix=#{prefix}" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + zlib = Formula["automattic/kandelo-homebrew/zlib"].opt_prefix + + test_src = testpath/"libxml2_basic.c" + test_wasm = testpath/"libxml2_basic.wasm" + FileUtils.cp "#{root}/packages/registry/libxml2/test/libxml2_basic.c", test_src + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}", + "-I#{zlib}/include", + test_src, + "#{lib}/libxml2.a", + "#{zlib}/lib/libz.a", + "-lm", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "PASS", output + assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/libxml-2.0.pc") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/mariadb.rb b/homebrew/kandelo-homebrew/Formula/mariadb.rb new file mode 100644 index 0000000000..e3a10b83c0 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/mariadb.rb @@ -0,0 +1,100 @@ +require "shellwords" + +class Mariadb < Formula + desc "MariaDB server and mysqltest client for Kandelo" + homepage "https://mariadb.org/" + url "https://archive.mariadb.org/mariadb-10.5.28/source/mariadb-10.5.28.tar.gz" + sha256 "0b5070208da0116640f20bd085f1136527f998cc23268715bcbf352e7b7f3cc1" + license "GPL-2.0-only" + + skip_clean "bin/mariadbd" + skip_clean "bin/mysqltest" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/#{kandelo_arch == "wasm64" ? "sysroot64" : "sysroot"}" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null 2>&1") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://archive.mariadb.org/mariadb-#{version}/source/mariadb-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "0b5070208da0116640f20bd085f1136527f998cc23268715bcbf352e7b7f3cc1" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + system "bash", "#{root}/packages/registry/mariadb/build-mariadb.sh" + chmod 0755, out_dir/"mariadbd.wasm" + bin.install out_dir/"mariadbd.wasm" => "mariadbd" + if (out_dir/"mysqltest.wasm").exist? + chmod 0755, out_dir/"mysqltest.wasm" + bin.install out_dir/"mysqltest.wasm" => "mysqltest" + end + chmod 0755, bin/"mariadbd" + chmod 0755, bin/"mysqltest" if (bin/"mysqltest").exist? + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + server = bin/"mariadbd" + assert_equal "\0asm".b, File.binread(server, 4) + output = run_kandelo_wasm(root, server, "--help", "--verbose") + assert_match "MariaDB", output + assert_path_exists bin/"mysqltest" + end +end diff --git a/homebrew/kandelo-homebrew/Formula/ncurses.rb b/homebrew/kandelo-homebrew/Formula/ncurses.rb new file mode 100644 index 0000000000..87254b82c9 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/ncurses.rb @@ -0,0 +1,140 @@ +require "shellwords" + +class Ncurses < Formula + desc "Terminal handling library and utilities for Kandelo" + homepage "https://invisible-island.net/ncurses/" + url "https://ftpmirror.gnu.org/gnu/ncurses/ncurses-6.5.tar.gz" + sha256 "136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6" + license "X11" + + skip_clean "bin/clear" + skip_clean "bin/reset" + skip_clean "bin/tset" + skip_clean "bin/tput" + skip_clean "bin/tabs" + skip_clean "bin/tic" + skip_clean "bin/infocmp" + skip_clean "bin/toe" + skip_clean "bin/captoinfo" + skip_clean "bin/infotocap" + skip_clean "lib/libncursesw.a" + skip_clean "lib/libtinfow.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + else odie "ncurses is currently packaged for wasm32 only, got #{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def ncurses_programs + %w[ + clear reset tset tput tabs tic infocmp toe captoinfo infotocap + ] + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://ftpmirror.gnu.org/gnu/ncurses/ncurses-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/ncurses/build-ncurses.sh" + + lib.install out_dir/"lib/libncursesw.a" + lib.install out_dir/"lib/libtinfow.a" + ln_s "libncursesw.a", lib/"libncurses.a" + ln_s "libtinfow.a", lib/"libtinfo.a" + include.install out_dir/"include/ncursesw" + ln_s "ncursesw", include/"ncurses" + ncurses_programs.each do |program| + chmod 0755, out_dir/"#{program}.wasm" + bin.install out_dir/"#{program}.wasm" => program + chmod 0755, bin/program + end + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + tput_wasm = testpath/"tput.wasm" + File.binwrite(tput_wasm, File.binread(bin/"tput")) + tput_output = run_kandelo_wasm(root, tput_wasm, "-V") + assert_match "ncurses", tput_output + + test_src = testpath/"ncurses-smoke.c" + test_wasm = testpath/"ncurses-smoke.wasm" + test_src.write <<~C + #include + #include + + int main(void) { + printf("%s\\n", curses_version()); + return 0; + } + C + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}", + test_src, + "#{lib}/libncursesw.a", + "#{lib}/libtinfow.a", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "ncurses", output + end +end diff --git a/homebrew/kandelo-homebrew/Formula/nginx.rb b/homebrew/kandelo-homebrew/Formula/nginx.rb new file mode 100644 index 0000000000..4207c8cf1f --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/nginx.rb @@ -0,0 +1,94 @@ +require "shellwords" + +class Nginx < Formula + desc "nginx HTTP server for Kandelo" + homepage "https://nginx.org/" + url "https://nginx.org/download/nginx-1.24.0.tar.gz" + sha256 "77a2541637b92a621e3ee76776c8b7b40cf6d707e69ba53a940283e30ff2f55d" + license "BSD-2-Clause" + + skip_clean "bin/nginx" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null 2>&1") + end + + def install + odie "nginx currently supports wasm32 only" unless kandelo_arch == "wasm32" + + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://nginx.org/download/nginx-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "77a2541637b92a621e3ee76776c8b7b40cf6d707e69ba53a940283e30ff2f55d" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/nginx/build-nginx.sh" + chmod 0755, out_dir/"nginx.wasm" + bin.install out_dir/"nginx.wasm" => "nginx" + chmod 0755, bin/"nginx" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + nginx = bin/"nginx" + assert_equal "\0asm".b, File.binread(nginx, 4) + assert_match "nginx", run_kandelo_wasm(root, nginx, "-v") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/node.rb b/homebrew/kandelo-homebrew/Formula/node.rb new file mode 100644 index 0000000000..6aa4fec7e2 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/node.rb @@ -0,0 +1,96 @@ +require "shellwords" + +class Node < Formula + desc "Node.js compatibility command backed by SpiderMonkey for Kandelo" + homepage "https://github.com/Automattic/kandelo" + url "https://github.com/Automattic/kandelo.git", + revision: "1ab41fe2ad5553f4fa4bb0223f2d804b13149578" + version "0.1.0" + license "MPL-2.0" + + depends_on "automattic/kandelo-homebrew/spidermonkey" + + skip_clean "bin/node" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null") + end + + def install + odie "node currently supports wasm32 only" unless kandelo_arch == "wasm32" + + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + ENV["WASM_POSIX_DEP_SPIDERMONKEY_DIR"] = Formula["automattic/kandelo-homebrew/spidermonkey"].opt_libexec.to_s + system "bash", "#{root}/packages/registry/spidermonkey-node/build-spidermonkey-node.sh" + chmod 0755, out_dir/"node.wasm" + bin.install out_dir/"node.wasm" => "node" + chmod 0755, bin/"node" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + runtime = bin/"node" + assert_equal "\0asm".b, File.binread(runtime, 4) + output = run_kandelo_wasm(root, runtime, "-e", "console.log('hello', process.arch, process.platform, process.version)") + assert_equal "hello wasm32 linux v22.0.0", output.strip + end +end diff --git a/homebrew/kandelo-homebrew/Formula/openssl.rb b/homebrew/kandelo-homebrew/Formula/openssl.rb new file mode 100644 index 0000000000..2d54dc2224 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/openssl.rb @@ -0,0 +1,110 @@ +require "shellwords" + +class Openssl < Formula + desc "TLS and cryptography library for Kandelo" + homepage "https://www.openssl.org/" + url "https://github.com/openssl/openssl/releases/download/openssl-3.3.2/openssl-3.3.2.tar.gz" + sha256 "2e8a40b01979afe8be0bbfb3de5dc1c6709fedb46d6c89c10da114ab5fc3d281" + license "Apache-2.0" + + skip_clean "lib/libssl.a" + skip_clean "lib/libcrypto.a" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + when "wasm64" then "wasm64posix" + else odie "unsupported HOMEBREW_KANDELO_ARCH=#{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/#{kandelo_arch == "wasm64" ? "sysroot64" : "sysroot"}" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv}") + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://github.com/openssl/openssl/releases/download/openssl-#{version}/openssl-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "2e8a40b01979afe8be0bbfb3de5dc1c6709fedb46d6c89c10da114ab5fc3d281" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/openssl/build-openssl.sh" + + lib.install out_dir/"lib/libssl.a" + lib.install out_dir/"lib/libcrypto.a" + include.install out_dir/"include/openssl" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/libssl.pc" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/libcrypto.pc" + inreplace lib/"pkgconfig/libssl.pc", /^prefix=.*/, "prefix=#{prefix}" + inreplace lib/"pkgconfig/libcrypto.pc", /^prefix=.*/, "prefix=#{prefix}" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + test_src = testpath/"ssl_basic.c" + test_wasm = testpath/"ssl_basic.wasm" + FileUtils.cp "#{root}/packages/registry/openssl/test/ssl_basic.c", test_src + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}", + test_src, + "#{lib}/libssl.a", + "#{lib}/libcrypto.a", + "-ldl", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = run_kandelo_wasm(root, test_wasm) + assert_match "PASS", output + assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/libssl.pc") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/redis.rb b/homebrew/kandelo-homebrew/Formula/redis.rb new file mode 100644 index 0000000000..d02c5aa9b0 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/redis.rb @@ -0,0 +1,101 @@ +require "shellwords" + +class Redis < Formula + desc "Redis server and CLI for Kandelo" + homepage "https://redis.io/" + url "https://github.com/redis/redis/archive/refs/tags/7.2.5.tar.gz" + sha256 "98a8502a2e902d2a9785ef46a69a5f8d5e24cbf9ea3ae4d845afcfc6778aa783" + license "BSD-3-Clause" + + skip_clean "bin/redis-server" + skip_clean "bin/redis-cli" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null 2>&1") + end + + def install + odie "redis currently supports wasm32 only" unless kandelo_arch == "wasm32" + + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://github.com/redis/redis/archive/refs/tags/#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "98a8502a2e902d2a9785ef46a69a5f8d5e24cbf9ea3ae4d845afcfc6778aa783" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/redis/build-redis.sh" + chmod 0755, out_dir/"redis-server.wasm" + chmod 0755, out_dir/"redis-cli.wasm" + bin.install out_dir/"redis-server.wasm" => "redis-server" + bin.install out_dir/"redis-cli.wasm" => "redis-cli" + chmod 0755, bin/"redis-server" + chmod 0755, bin/"redis-cli" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + server = bin/"redis-server" + cli = bin/"redis-cli" + assert_equal "\0asm".b, File.binread(server, 4) + assert_equal "\0asm".b, File.binread(cli, 4) + assert_match "Redis server", run_kandelo_wasm(root, server, "--version") + assert_match "redis-cli", run_kandelo_wasm(root, cli, "--version") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/spidermonkey-node.rb b/homebrew/kandelo-homebrew/Formula/spidermonkey-node.rb new file mode 100644 index 0000000000..74029f0238 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/spidermonkey-node.rb @@ -0,0 +1,96 @@ +require "shellwords" + +class SpidermonkeyNode < Formula + desc "SpiderMonkey-backed Node.js compatibility runtime for Kandelo" + homepage "https://github.com/Automattic/kandelo" + url "https://github.com/Automattic/kandelo.git", + revision: "1ab41fe2ad5553f4fa4bb0223f2d804b13149578" + version "140.11.0esr-node.1" + license "MPL-2.0" + + depends_on "automattic/kandelo-homebrew/spidermonkey" + + skip_clean "bin/spidermonkey-node" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null") + end + + def install + odie "spidermonkey-node currently supports wasm32 only" unless kandelo_arch == "wasm32" + + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + ENV["WASM_POSIX_DEP_SPIDERMONKEY_DIR"] = Formula["automattic/kandelo-homebrew/spidermonkey"].opt_libexec.to_s + system "bash", "#{root}/packages/registry/spidermonkey-node/build-spidermonkey-node.sh" + chmod 0755, out_dir/"node.wasm" + bin.install out_dir/"node.wasm" => "spidermonkey-node" + chmod 0755, bin/"spidermonkey-node" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + runtime = bin/"spidermonkey-node" + assert_equal "\0asm".b, File.binread(runtime, 4) + output = run_kandelo_wasm(root, runtime, "--version") + assert_equal "v22.0.0", output.strip + end +end diff --git a/homebrew/kandelo-homebrew/Formula/spidermonkey.rb b/homebrew/kandelo-homebrew/Formula/spidermonkey.rb new file mode 100644 index 0000000000..18df8b64c3 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/spidermonkey.rb @@ -0,0 +1,101 @@ +require "shellwords" + +class Spidermonkey < Formula + desc "SpiderMonkey JavaScript shell for Kandelo" + homepage "https://spidermonkey.dev/" + url "https://ftp.mozilla.org/pub/firefox/releases/140.11.0esr/source/firefox-140.11.0esr.source.tar.xz" + version "140.11.0esr" + sha256 "1b034d2117356fda24807a151055132315c6ba58ad2bdf7ec71ee707fac5e028" + license "MPL-2.0" + + skip_clean "bin/js" + skip_clean "libexec/node.wasm" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + ENV["WASM_POSIX_SYSROOT"] = "#{root}/sysroot" + ENV["WASM_POSIX_GLUE_DIR"] = "#{root}/libc/glue" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (host_tool_path = ENV["HOMEBREW_KANDELO_HOST_TOOL_PATH"]).to_s != "" + host_tool_path.split(File::PATH_SEPARATOR).reverse_each do |dir| + ENV.prepend_path "PATH", dir unless dir.empty? + end + end + if (pkg_config = ENV["HOMEBREW_KANDELO_PKG_CONFIG"]).to_s != "" + ENV["PKG_CONFIG"] = pkg_config + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV["LLVM_PREFIX"] ||= File.expand_path("..", llvm_bin) + ENV.prepend_path "PATH", llvm_bin + end + end + + def run_kandelo_wasm(root, wasm, *args) + wasm_path = testpath/"#{File.basename(wasm.to_s)}.wasm" + FileUtils.cp wasm, wasm_path + argv = [ + "node", + "--experimental-wasm-exnref", + "--import", + "tsx/esm", + "examples/run-example.ts", + wasm_path.to_s, + *args, + ].map(&:shellescape).join(" ") + shell_output("cd #{root.shellescape} && #{argv} < /dev/null") + end + + def install + odie "spidermonkey currently supports wasm32 only" unless kandelo_arch == "wasm32" + + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["CARGO_TARGET_DIR"] = (buildpath/"cargo-target").to_s + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://ftp.mozilla.org/pub/firefox/releases/#{version}/source/firefox-#{version}.source.tar.xz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "1b034d2117356fda24807a151055132315c6ba58ad2bdf7ec71ee707fac5e028" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + system "bash", "#{root}/packages/registry/spidermonkey/build-spidermonkey.sh" + chmod 0755, out_dir/"js.wasm" + bin.install out_dir/"js.wasm" => "js" + if (out_dir/"node.wasm").exist? + libexec.install out_dir/"node.wasm" + end + chmod 0755, bin/"js" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + js = bin/"js" + assert_equal "\0asm".b, File.binread(js, 4) + output = run_kandelo_wasm(root, js, "-e", "print(1 + 1)") + assert_equal "2", output.strip + assert_path_exists libexec/"node.wasm" + end +end diff --git a/homebrew/kandelo-homebrew/Formula/sqlite.rb b/homebrew/kandelo-homebrew/Formula/sqlite.rb new file mode 100644 index 0000000000..914b207a6c --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/sqlite.rb @@ -0,0 +1,89 @@ +require "shellwords" + +class Sqlite < Formula + desc "SQLite static library for Kandelo" + homepage "https://www.sqlite.org/" + url "https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip" + sha256 "6cebd1d8403fc58c30e93939b246f3e6e58d0765a5cd50546f16c00fd805d2c3" + license "blessing" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def kandelo_arch + ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + end + + def kandelo_tool_prefix + case kandelo_arch + when "wasm32" then "wasm32posix" + when "wasm64" then "wasm64posix" + else odie "unsupported HOMEBREW_KANDELO_ARCH=#{kandelo_arch}" + end + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "6cebd1d8403fc58c30e93939b246f3e6e58d0765a5cd50546f16c00fd805d2c3" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = kandelo_arch + + system "bash", "#{root}/packages/registry/sqlite/build-sqlite.sh" + include.install out_dir/"include/sqlite3.h" + include.install out_dir/"include/sqlite3ext.h" + lib.install out_dir/"lib/libsqlite3.a" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/sqlite3.pc" + inreplace lib/"pkgconfig/sqlite3.pc", /^prefix=.*/, "prefix=#{prefix}" + end + + test do + root = kandelo_root + configure_kandelo_environment(root) + + test_src = testpath/"sqlite_basic.c" + test_wasm = testpath/"sqlite_basic.wasm" + FileUtils.cp "#{root}/packages/registry/sqlite/test/sqlite_basic.c", test_src + + system "#{kandelo_tool_prefix}-cc", + "-I#{include}", + test_src, + "#{lib}/libsqlite3.a", + "-lm", + "-o", + test_wasm + assert_equal "\0asm".b, File.binread(test_wasm, 4) + + output = shell_output( + "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape}", + ) + assert_match "PASS", output + assert_match "prefix=#{prefix}", File.read(lib/"pkgconfig/sqlite3.pc") + end +end diff --git a/homebrew/kandelo-homebrew/Formula/xz.rb b/homebrew/kandelo-homebrew/Formula/xz.rb new file mode 100644 index 0000000000..e9f1da378e --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/xz.rb @@ -0,0 +1,68 @@ +require "shellwords" + +class Xz < Formula + desc "XZ Utils compression tool for Kandelo" + homepage "https://tukaani.org/xz/" + url "https://tukaani.org/xz/xz-5.6.2.tar.xz" + sha256 "a9db3bb3d64e248a0fae963f8fb6ba851a26ba1822e504dc0efd18a80c626caf" + license all_of: ["GPL-2.0-or-later", "LGPL-2.1-or-later", "0BSD"] + + skip_clean "bin/xz" + + def kandelo_root + root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if root.to_s.empty? + root + end + + def configure_kandelo_environment(root) + %w[ + CC CXX OBJC OBJCXX CFLAGS CPPFLAGS CXXFLAGS LDFLAGS CPATH + C_INCLUDE_PATH CPLUS_INCLUDE_PATH OBJC_INCLUDE_PATH SDKROOT + MACOSX_DEPLOYMENT_TARGET + ].each { |key| ENV.delete(key) } + + ENV.prepend_path "PATH", "#{root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + end + + def install + root = kandelo_root + configure_kandelo_environment(root) + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://tukaani.org/xz/xz-#{version}.tar.xz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "a9db3bb3d64e248a0fae963f8fb6ba851a26ba1822e504dc0efd18a80c626caf" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + + system "bash", "#{root}/packages/registry/xz/build-xz.sh" + chmod 0755, out_dir/"xz.wasm" + bin.install out_dir/"xz.wasm" => "xz" + chmod 0755, bin/"xz" + end + + test do + xz = bin/"xz" + assert_equal "\0asm".b, File.binread(xz, 4) + + root = kandelo_root + configure_kandelo_environment(root) + + test_wasm = testpath/"xz.wasm" + File.binwrite(test_wasm, File.binread(xz)) + output = shell_output( + "cd #{root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{test_wasm.to_s.shellescape} --version", + ) + assert_match "xz", output.scrub + end +end diff --git a/homebrew/kandelo-homebrew/Formula/zlib.rb b/homebrew/kandelo-homebrew/Formula/zlib.rb new file mode 100644 index 0000000000..8d0924e351 --- /dev/null +++ b/homebrew/kandelo-homebrew/Formula/zlib.rb @@ -0,0 +1,106 @@ +require "shellwords" + +class Zlib < Formula + desc "Compression library for Kandelo" + homepage "https://zlib.net/" + url "https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz" + sha256 "9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23" + license "Zlib" + skip_clean "lib/libz.a" + + def install + kandelo_root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + odie "HOMEBREW_KANDELO_ROOT must point at a Kandelo checkout" if kandelo_root.to_s.empty? + + ENV.prepend_path "PATH", "#{kandelo_root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + if (llvm_bin = ENV["HOMEBREW_KANDELO_LLVM_BIN"]).to_s != "" + ENV["WASM_POSIX_LLVM_DIR"] = llvm_bin + ENV["LLVM_BIN"] = llvm_bin + ENV.prepend_path "PATH", llvm_bin + end + + out_dir = buildpath/"kandelo-package-out" + ENV["WASM_POSIX_DEP_VERSION"] = version.to_s + ENV["WASM_POSIX_DEP_SOURCE_URL"] = "https://github.com/madler/zlib/releases/download/v#{version}/zlib-#{version}.tar.gz" + ENV["WASM_POSIX_DEP_SOURCE_SHA256"] = "9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23" + ENV["WASM_POSIX_DEP_OUT_DIR"] = out_dir + ENV["WASM_POSIX_DEP_WORK_DIR"] = buildpath/"kandelo-package-work" + ENV["WASM_POSIX_DEP_TARGET_ARCH"] = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + + system "bash", "#{kandelo_root}/packages/registry/zlib/build-zlib.sh" + + inreplace out_dir/"lib/pkgconfig/zlib.pc", out_dir.to_s, prefix.to_s + lib.install out_dir/"lib/libz.a" + include.install out_dir/"include/zlib.h", out_dir/"include/zconf.h" + (lib/"pkgconfig").install out_dir/"lib/pkgconfig/zlib.pc" + end + + test do + assert_path_exists lib/"libz.a" + assert_path_exists include/"zlib.h" + assert_path_exists include/"zconf.h" + assert_path_exists lib/"pkgconfig/zlib.pc" + + kandelo_root = ENV["HOMEBREW_KANDELO_ROOT"] || ENV["KANDELO_HOMEBREW_KANDELO_ROOT"] + return if kandelo_root.to_s.empty? + + ENV.prepend_path "PATH", "#{kandelo_root}/sdk/bin" + if (node = ENV["HOMEBREW_KANDELO_NODE"]).to_s != "" + ENV.prepend_path "PATH", File.dirname(node) + end + + arch = ENV.fetch("HOMEBREW_KANDELO_ARCH", ENV.fetch("KANDELO_HOMEBREW_ARCH", "wasm32")) + ENV["WASM_POSIX_SYSROOT"] = "#{kandelo_root}/#{arch == "wasm64" ? "sysroot64" : "sysroot"}" + ENV["WASM_POSIX_GLUE_DIR"] = "#{kandelo_root}/libc/glue" + %w[ + SDKROOT + HOMEBREW_SDKROOT + CPATH + C_INCLUDE_PATH + CPLUS_INCLUDE_PATH + OBJC_INCLUDE_PATH + ].each { |key| ENV.delete(key) } + + compiler = "#{kandelo_root}/sdk/bin/#{arch}posix-cc" + smoke_c = testpath/"zlib-smoke.c" + smoke_wasm = testpath/"zlib-smoke.wasm" + smoke_c.write <<~C + #include + #include + #include + + int main(void) { + const unsigned char input[] = "kandelo zlib smoke"; + unsigned char compressed[128]; + unsigned char output[128]; + unsigned long compressed_len = sizeof(compressed); + unsigned long output_len = sizeof(output); + + if (compress(compressed, &compressed_len, input, sizeof(input)) != Z_OK) { + puts("compress failed"); + return 1; + } + if (uncompress(output, &output_len, compressed, compressed_len) != Z_OK) { + puts("uncompress failed"); + return 1; + } + if (output_len != sizeof(input) || memcmp(input, output, sizeof(input)) != 0) { + puts("roundtrip mismatch"); + return 1; + } + + printf("zlib %s ok\\n", zlibVersion()); + return 0; + } + C + + system compiler, smoke_c, "-I#{include}", "-L#{lib}", "-lz", "-o", smoke_wasm + output = shell_output( + "cd #{kandelo_root.shellescape} && node --experimental-wasm-exnref --import tsx/esm examples/run-example.ts #{smoke_wasm.to_s.shellescape}", + ) + assert_match "zlib #{version} ok", output + end +end diff --git a/homebrew/kandelo-homebrew/README.md b/homebrew/kandelo-homebrew/README.md index 172dbc88e5..e10aa96da5 100644 --- a/homebrew/kandelo-homebrew/README.md +++ b/homebrew/kandelo-homebrew/README.md @@ -27,6 +27,13 @@ Kandelo/ This template currently contains: - `Formula/hello.rb`, the first Kandelo Homebrew formula scaffold; +- `Formula/zlib.rb`, the first static-library formula scaffold; +- `Formula/sqlite.rb`, `Formula/bzip2.rb`, and `Formula/xz.rb`, the first + post-hello pilot package formulae; +- dependency-root and hybrid package formulae for `openssl`, `libcxx`, + `libxml2`, `libpng`, `libcurl`, and `ncurses`; +- JS runtime, database, and service runtime formulae for `spidermonkey`, + `spidermonkey-node`, `node`, `redis`, `nginx`, and `mariadb`; - JSON Schemas for the Kandelo sidecar metadata contract; - `hello` example metadata for schema and validator development. - an `xtask homebrew-sidecars` generator that converts produced bottle bytes diff --git a/packages/registry/bzip2/build-bzip2.sh b/packages/registry/bzip2/build-bzip2.sh index 4248060501..8ee4c9417c 100755 --- a/packages/registry/bzip2/build-bzip2.sh +++ b/packages/registry/bzip2/build-bzip2.sh @@ -4,42 +4,70 @@ set -euo pipefail # Build bzip2 1.0.8 for wasm32-posix-kernel. # # Plain Makefile build with CC/AR/RANLIB overrides. -# Output: packages/registry/bzip2/bin/bzip2.wasm -# Also installs libbz2.a + bzlib.h to sysroot. +# Output: bzip2.wasm. Resolver/Homebrew invocations install only the +# declared program output into WASM_POSIX_DEP_OUT_DIR; direct legacy +# invocations still populate packages/registry/bzip2/bin and sysroot. -BZIP2_VERSION="${BZIP2_VERSION:-1.0.8}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/bzip2-src" -BIN_DIR="$SCRIPT_DIR/bin" -SYSROOT="$REPO_ROOT/sysroot" +source "$REPO_ROOT/sdk/activate.sh" + +BZIP2_VERSION="${WASM_POSIX_DEP_VERSION:-${BZIP2_VERSION:-1.0.8}}" +SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://sourceware.org/pub/bzip2/bzip2-${BZIP2_VERSION}.tar.gz}" +SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/bzip2-src" +BIN_DIR="$WORK_DIR/bin" +INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" + +if [ "$TARGET_ARCH" != "wasm32" ]; then + echo "ERROR: bzip2 is currently packaged for wasm32 only, got $TARGET_ARCH" >&2 + exit 2 +fi + +SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" +export WASM_POSIX_SYSROOT="$SYSROOT" # --- Prerequisites --- if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 + echo "ERROR: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2 exit 1 fi if [ ! -f "$SYSROOT/lib/libc.a" ]; then - echo "ERROR: sysroot not found. Run: bash build.sh && bash scripts/build-musl.sh" >&2 + echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh first." >&2 exit 1 fi -export WASM_POSIX_SYSROOT="$SYSROOT" - # --- Download bzip2 source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$BZIP2_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +SOURCE_MARKER="$SRC_DIR/.kandelo-bzip2-source" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing bzip2 source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading bzip2 $BZIP2_VERSION..." - TARBALL="bzip2-${BZIP2_VERSION}.tar.gz" - URL="https://sourceware.org/pub/bzip2/${TARBALL}" - curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$URL" -o "/tmp/$TARBALL" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-bzip2-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/bzip2-${BZIP2_VERSION}.tar.gz" + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" + if [ -n "$SOURCE_SHA256" ]; then + echo "==> Verifying source sha256..." + echo "$SOURCE_SHA256 $TARBALL" | shasum -a 256 -c - + fi mkdir -p "$SRC_DIR" - tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/$TARBALL" + tar xzf "$TARBALL" -C "$SRC_DIR" --strip-components=1 + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" echo "==> Source extracted to $SRC_DIR" fi cd "$SRC_DIR" +make clean >/dev/null 2>&1 || true # --- Build --- echo "==> Building bzip2..." @@ -64,17 +92,25 @@ else exit 1 fi -# --- Install library to sysroot --- -echo "==> Installing libbz2.a and bzlib.h to sysroot..." -cp "$SRC_DIR/libbz2.a" "$SYSROOT/lib/" -cp "$SRC_DIR/bzlib.h" "$SYSROOT/include/" -echo "==> Installed libbz2.a and bzlib.h" +if [ -n "$INSTALL_DIR" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$BIN_DIR/bzip2.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/bzip2.wasm" + rm -rf "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + cp "$BIN_DIR/bzip2.wasm" "$INSTALL_DIR/bzip2.wasm" + echo "==> Installed bzip2.wasm to $INSTALL_DIR" +else + # --- Install library to sysroot (legacy direct invocation only) --- + echo "==> Installing libbz2.a and bzlib.h to sysroot..." + cp "$SRC_DIR/libbz2.a" "$SYSROOT/lib/" + cp "$SRC_DIR/bzlib.h" "$SYSROOT/include/" + echo "==> Installed libbz2.a and bzlib.h" + + source "$REPO_ROOT/scripts/install-local-binary.sh" + install_local_binary bzip2 "$SCRIPT_DIR/bin/bzip2.wasm" +fi echo "" echo "==> bzip2 built successfully!" echo "Binary: $BIN_DIR/bzip2.wasm" - -# Install into local-binaries/ so the resolver picks the freshly-built -# binary over the fetched release. -source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary bzip2 "$SCRIPT_DIR/bin/bzip2.wasm" diff --git a/packages/registry/libcurl/build-libcurl.sh b/packages/registry/libcurl/build-libcurl.sh index a71f6dcee8..c33e5f486f 100755 --- a/packages/registry/libcurl/build-libcurl.sh +++ b/packages/registry/libcurl/build-libcurl.sh @@ -20,13 +20,28 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/curl-src" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Inputs from resolver, with legacy fallbacks --- CURL_VERSION="${WASM_POSIX_DEP_VERSION:-${CURL_VERSION:-8.11.1}}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://curl.se/download/curl-${CURL_VERSION}.tar.xz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +if [ "$TARGET_ARCH" != "wasm32" ]; then + echo "ERROR: libcurl is currently packaged for wasm32 only, got $TARGET_ARCH" >&2 + exit 2 +fi + +TOOL_PREFIX="wasm32posix" +CC="${TOOL_PREFIX}-cc" +CONFIGURE="${TOOL_PREFIX}-configure" +SRC_DIR="$WORK_DIR/curl-src" +SOURCE_MARKER="$SRC_DIR/.kandelo-curl-source" + # Resolver vs legacy is decided by whether WASM_POSIX_DEP_OUT_DIR is # set. In resolver mode INSTALL_DIR is the cache temp dir; in legacy # mode it's the in-tree bin/ for curl.wasm. @@ -38,8 +53,8 @@ else INSTALL_DIR="$SCRIPT_DIR/bin" fi -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 exit 1 fi @@ -70,9 +85,17 @@ if [ -z "$OPENSSL_PREFIX" ] \ fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$CURL_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing curl source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading curl $CURL_VERSION..." - TARBALL="/tmp/curl-${CURL_VERSION}.tar.xz" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-curl-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/curl-${CURL_VERSION}.tar.xz" curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." @@ -82,7 +105,9 @@ if [ ! -d "$SRC_DIR" ]; then fi mkdir -p "$SRC_DIR" tar xJf "$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "$TARBALL" + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" echo "==> Source extracted to $SRC_DIR" fi @@ -205,7 +230,7 @@ if [ ! -f Makefile ]; then PREFIX_ARGS+=(--prefix="$INSTALL_DIR") fi - wasm32posix-configure \ + "$CONFIGURE" \ ${PREFIX_ARGS[@]+"${PREFIX_ARGS[@]}"} \ --disable-nls \ --disable-shared \ diff --git a/packages/registry/libcxx/build-libcxx.sh b/packages/registry/libcxx/build-libcxx.sh index 96396ed8f7..f393d3794c 100755 --- a/packages/registry/libcxx/build-libcxx.sh +++ b/packages/registry/libcxx/build-libcxx.sh @@ -102,7 +102,7 @@ if [ ! -d "$NIX_LIBUNWIND_SOURCE/libunwind" ]; then exit 1 fi -BUILD_DIR="$SCRIPT_DIR/build-${ARCH}" +BUILD_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR/build-${ARCH}}" LLVM_SRC_DIR="$BUILD_DIR/llvm-source" # --- Verify prerequisites --- diff --git a/packages/registry/libpng/build-libpng.sh b/packages/registry/libpng/build-libpng.sh index 409fb7e5d1..b3dc3668c3 100755 --- a/packages/registry/libpng/build-libpng.sh +++ b/packages/registry/libpng/build-libpng.sh @@ -21,21 +21,37 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/libpng-src" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Inputs from resolver, with legacy fallbacks --- LIBPNG_VERSION="${WASM_POSIX_DEP_VERSION:-${LIBPNG_VERSION:-1.6.43}}" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/libpng-install}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://download.sourceforge.net/libpng/libpng-${LIBPNG_VERSION}.tar.xz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +if [ "$TARGET_ARCH" != "wasm32" ]; then + echo "ERROR: libpng is currently packaged for wasm32 only, got $TARGET_ARCH" >&2 + exit 2 +fi + +TOOL_PREFIX="wasm32posix" +CC="${TOOL_PREFIX}-cc" +AR="${TOOL_PREFIX}-ar" +RANLIB="${TOOL_PREFIX}-ranlib" +SRC_DIR="$WORK_DIR/libpng-src" +SOURCE_MARKER="$SRC_DIR/.kandelo-libpng-source" + # autoconf bakes --prefix into the Makefile. A rerun from a different # INSTALL_DIR would install into the wrong path, so always build in a # fresh dir rather than reusing a stale libpng-build/. -BUILD_DIR="$SCRIPT_DIR/libpng-build" +BUILD_DIR="$WORK_DIR/libpng-build" -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 exit 1 fi @@ -43,6 +59,7 @@ fi # Resolver surfaces the direct-dep install path via contract env var. # Legacy mode falls back to the sysroot artifact. SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" +export WASM_POSIX_SYSROOT="$SYSROOT" ZLIB_PREFIX="${WASM_POSIX_DEP_ZLIB_DIR:-}" if [ -z "$ZLIB_PREFIX" ]; then if [ -f "$SYSROOT/lib/libz.a" ]; then @@ -54,9 +71,17 @@ if [ -z "$ZLIB_PREFIX" ]; then fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$LIBPNG_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing libpng source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading libpng $LIBPNG_VERSION..." - TARBALL="/tmp/libpng-${LIBPNG_VERSION}.tar.xz" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-libpng-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/libpng-${LIBPNG_VERSION}.tar.xz" curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." @@ -66,7 +91,9 @@ if [ ! -d "$SRC_DIR" ]; then fi mkdir -p "$SRC_DIR" tar xf "$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "$TARBALL" + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" fi # Fresh build + install dir each run. The cache path varies per key @@ -85,9 +112,9 @@ echo "==> Configuring libpng for wasm32 (zlib at $ZLIB_PREFIX)..." --enable-static \ --disable-shared \ --with-zlib-prefix="$ZLIB_PREFIX" \ - CC=wasm32posix-cc \ - AR=wasm32posix-ar \ - RANLIB=wasm32posix-ranlib \ + CC="$CC" \ + AR="$AR" \ + RANLIB="$RANLIB" \ CPPFLAGS="-I$ZLIB_PREFIX/include" \ LDFLAGS="-L$ZLIB_PREFIX/lib" diff --git a/packages/registry/libxml2/build-libxml2.sh b/packages/registry/libxml2/build-libxml2.sh index e0d8670d04..bf34b4c4c2 100644 --- a/packages/registry/libxml2/build-libxml2.sh +++ b/packages/registry/libxml2/build-libxml2.sh @@ -26,22 +26,50 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/libxml2-src" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Inputs from resolver, with legacy fallbacks --- LIBXML2_VERSION="${WASM_POSIX_DEP_VERSION:-${LIBXML2_VERSION:-2.13.8}}" LIBXML2_MAJOR_MINOR="${LIBXML2_VERSION%.*}" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/libxml2-install}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://download.gnome.org/sources/libxml2/${LIBXML2_MAJOR_MINOR}/libxml2-${LIBXML2_VERSION}.tar.xz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +case "$TARGET_ARCH" in + wasm32) + TOOL_PREFIX="wasm32posix" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" + ;; + wasm64) + TOOL_PREFIX="wasm64posix" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot64}" + ;; + *) + echo "ERROR: unsupported WASM_POSIX_DEP_TARGET_ARCH=$TARGET_ARCH" >&2 + exit 2 + ;; +esac + +CC="${TOOL_PREFIX}-cc" +AR="${TOOL_PREFIX}-ar" +CONFIGURE="${TOOL_PREFIX}-configure" +SRC_DIR="$WORK_DIR/libxml2-src-$TARGET_ARCH" +SOURCE_MARKER="$SRC_DIR/.kandelo-libxml2-source" +export WASM_POSIX_SYSROOT="$SYSROOT" + +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 exit 1 fi -SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" -export WASM_POSIX_SYSROOT="$SYSROOT" +if [ ! -f "$SYSROOT/lib/libc.a" ]; then + echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh for $TARGET_ARCH first." >&2 + exit 1 +fi # --- Locate zlib --- # Resolver surfaces the direct-dep install path via contract env var. @@ -61,11 +89,23 @@ if [ ! -f "$ZLIB_PREFIX/lib/libz.a" ]; then echo "ERROR: zlib not found at $ZLIB_PREFIX" >&2 exit 1 fi +if [ ! -f "$ZLIB_PREFIX/include/zlib.h" ]; then + echo "ERROR: zlib headers not found at $ZLIB_PREFIX" >&2 + exit 1 +fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$LIBXML2_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing libxml2 source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading libxml2 $LIBXML2_VERSION..." - TARBALL="/tmp/libxml2-${LIBXML2_VERSION}.tar.xz" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-libxml2-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/libxml2-${LIBXML2_VERSION}.tar.xz" curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." @@ -75,18 +115,20 @@ if [ ! -d "$SRC_DIR" ]; then fi mkdir -p "$SRC_DIR" tar xJf "$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "$TARBALL" + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" fi cd "$SRC_DIR" # --- Configure (regenerate config.h against the current ZLIB_PREFIX) --- # Scrub any stale config so probes re-run. Cheap; no object-compile wasted. -echo "==> Configuring libxml2 for Wasm (zlib at $ZLIB_PREFIX)..." +echo "==> Configuring libxml2 for $TARGET_ARCH (zlib at $ZLIB_PREFIX)..." make distclean 2>/dev/null || true rm -f config.h config.status -wasm32posix-configure \ +"$CONFIGURE" \ --disable-shared --enable-static \ --without-python --without-readline --without-iconv \ --without-icu --without-lzma --without-http --without-ftp \ @@ -112,7 +154,7 @@ SOURCES=( schematron.c ) -CFLAGS="-O2 -DHAVE_CONFIG_H -I. -I./include" +CFLAGS="-O2 -DHAVE_CONFIG_H -I. -I./include -I$ZLIB_PREFIX/include" echo "==> Compiling libxml2 source files..." OBJS=() @@ -120,13 +162,13 @@ for src in "${SOURCES[@]}"; do if [ -f "$src" ]; then obj="${src%.c}.o" # shellcheck disable=SC2086 - wasm32posix-cc $CFLAGS -c "$src" -o "$obj" + "$CC" $CFLAGS -c "$src" -o "$obj" OBJS+=("$obj") fi done echo "==> Creating libxml2.a (${#OBJS[@]} objects)..." -wasm32posix-ar rcs libxml2.a "${OBJS[@]}" +"$AR" rcs libxml2.a "${OBJS[@]}" # --- Install --- echo "==> Installing to $INSTALL_DIR..." diff --git a/packages/registry/mariadb/build-mariadb.sh b/packages/registry/mariadb/build-mariadb.sh index 1cf30f85ce..d1468594d4 100755 --- a/packages/registry/mariadb/build-mariadb.sh +++ b/packages/registry/mariadb/build-mariadb.sh @@ -11,7 +11,7 @@ set -euo pipefail # 1. Host build: generates import_executables.cmake (native helper programs) # 2. Cross build: uses CMake toolchain file for wasm32 or wasm64 -MARIADB_VERSION="${MARIADB_VERSION:-10.5.28}" +MARIADB_VERSION="${WASM_POSIX_DEP_VERSION:-${MARIADB_VERSION:-10.5.28}}" MARIADB_MAJOR="10.5" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -19,9 +19,14 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # Worktree-local SDK on PATH (no global npm link required). # shellcheck source=/dev/null source "$REPO_ROOT/sdk/activate.sh" -SRC_DIR="$SCRIPT_DIR/mariadb-src" -HOST_BUILD_DIR="$SCRIPT_DIR/mariadb-host-build" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +DOWNLOAD_DIR="$WORK_DIR/downloads" +SRC_DIR="$WORK_DIR/mariadb-src" +HOST_BUILD_DIR="$WORK_DIR/mariadb-host-build" GLUE_DIR="$REPO_ROOT/libc/glue" +MARIADB_SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://archive.mariadb.org/mariadb-${MARIADB_VERSION}/source/mariadb-${MARIADB_VERSION}.tar.gz}" +MARIADB_SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-0b5070208da0116640f20bd085f1136527f998cc23268715bcbf352e7b7f3cc1}" +SOURCE_MARKER="$SRC_DIR/.kandelo-source" # Default to xtask resolver's WASM_POSIX_DEP_TARGET_ARCH (set per # manifest arch at build-deps time); fall back to wasm32 outside the @@ -40,8 +45,8 @@ while [ $# -gt 0 ]; do done if [ "$WASM_ARCH" = "wasm64" ]; then - CROSS_BUILD_DIR="$SCRIPT_DIR/mariadb-cross-build-64" - INSTALL_DIR="$SCRIPT_DIR/mariadb-install-64" + CROSS_BUILD_DIR="$WORK_DIR/mariadb-cross-build-64" + INSTALL_DIR="$WORK_DIR/mariadb-install-64" TOOLCHAIN_FILE="$SCRIPT_DIR/wasm64-posix-toolchain.cmake" SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot64}" WASM_TARGET="wasm64-unknown-unknown" @@ -49,14 +54,27 @@ if [ "$WASM_ARCH" = "wasm64" ]; then # in table lookups). Use -O1 until the LLVM wasm64 backend matures. : "${MARIADB_OPT_LEVEL:=-O1}" else - CROSS_BUILD_DIR="$SCRIPT_DIR/mariadb-cross-build" - INSTALL_DIR="$SCRIPT_DIR/mariadb-install" + CROSS_BUILD_DIR="$WORK_DIR/mariadb-cross-build" + INSTALL_DIR="$WORK_DIR/mariadb-install" TOOLCHAIN_FILE="$SCRIPT_DIR/wasm32-posix-toolchain.cmake" SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" WASM_TARGET="wasm32-unknown-unknown" fi export WASM_POSIX_SYSROOT="$SYSROOT" +sha256_file() { + python3 - "$1" <<'PY' +import hashlib +import sys + +h = hashlib.sha256() +with open(sys.argv[1], "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) +print(h.hexdigest()) +PY +} + NPROC="$(sysctl -n hw.ncpu 2>/dev/null || nproc)" HOST_HELPERS=( "$HOST_BUILD_DIR/extra/comp_err" @@ -84,6 +102,18 @@ if [ ! -f "$SYSROOT/lib/libc.a" ]; then exit 1 fi +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BASE_SYSROOT="$SYSROOT" + SYSROOT="$WORK_DIR/sysroot" + if [ ! -f "$SYSROOT/lib/libc.a" ]; then + echo "==> Copying sysroot into package work directory..." + rm -rf "$SYSROOT" + mkdir -p "$SYSROOT" + cp -R "$BASE_SYSROOT/." "$SYSROOT" + fi + export WASM_POSIX_SYSROOT="$SYSROOT" +fi + if [ ! -f "$TOOLCHAIN_FILE" ]; then echo "ERROR: Toolchain file not found at $TOOLCHAIN_FILE" >&2 exit 1 @@ -101,14 +131,30 @@ if ! command -v bison &>/dev/null; then fi # --- Download MariaDB source --- -if [ ! -d "$SRC_DIR" ]; then +mkdir -p "$DOWNLOAD_DIR" +TARBALL="mariadb-${MARIADB_VERSION}.tar.gz" +ARCHIVE="$DOWNLOAD_DIR/$TARBALL" +if [ ! -f "$ARCHIVE" ]; then echo "==> Downloading MariaDB $MARIADB_VERSION..." - TARBALL="mariadb-${MARIADB_VERSION}.tar.gz" - URL="https://archive.mariadb.org/mariadb-${MARIADB_VERSION}/source/${TARBALL}" - curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$URL" -o "/tmp/$TARBALL" + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$MARIADB_SOURCE_URL" -o "$ARCHIVE" +fi + +if [ -n "$MARIADB_SOURCE_SHA256" ]; then + actual_sha="$(sha256_file "$ARCHIVE")" + if [ "$actual_sha" != "$MARIADB_SOURCE_SHA256" ]; then + echo "ERROR: source SHA256 mismatch for $ARCHIVE" >&2 + echo " expected: $MARIADB_SOURCE_SHA256" >&2 + echo " actual: $actual_sha" >&2 + exit 1 + fi +fi + +if [ ! -d "$SRC_DIR" ] || [ ! -f "$SOURCE_MARKER" ] || [ "$(cat "$SOURCE_MARKER")" != "$MARIADB_VERSION $MARIADB_SOURCE_SHA256" ]; then + echo "==> Extracting MariaDB $MARIADB_VERSION..." + rm -rf "$SRC_DIR" mkdir -p "$SRC_DIR" - tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/$TARBALL" + tar xzf "$ARCHIVE" -C "$SRC_DIR" --strip-components=1 + printf '%s %s\n' "$MARIADB_VERSION" "$MARIADB_SOURCE_SHA256" > "$SOURCE_MARKER" echo "==> Source extracted to $SRC_DIR" fi @@ -268,9 +314,9 @@ fi # below) and try to link a wasm32 archive into a wasm64 binary, dying # with: "wasm32 object file can't be linked in wasm64 mode". if [ "$WASM_ARCH" = "wasm64" ]; then - PCRE2_BUILD="$SCRIPT_DIR/pcre2-wasm-build-64" + PCRE2_BUILD="$WORK_DIR/pcre2-wasm-build-64" else - PCRE2_BUILD="$SCRIPT_DIR/pcre2-wasm-build" + PCRE2_BUILD="$WORK_DIR/pcre2-wasm-build" fi if [ ! -f "$PCRE2_BUILD/libpcre2-8.a" ]; then echo "==> Building PCRE2 for $WASM_ARCH from source at $PCRE2_SOURCE_DIR..." @@ -317,9 +363,9 @@ echo "==> PCRE2 installed to sysroot from cached source" WASM_COMPILE_FLAGS="--target=$WASM_TARGET -matomics -mbulk-memory -mexception-handling -mllvm -wasm-enable-sjlj -fno-trapping-math --sysroot=$SYSROOT" if [ "$WASM_ARCH" = "wasm64" ]; then - GLUE_OBJ_DIR="$SCRIPT_DIR/mariadb-glue-objs-64" + GLUE_OBJ_DIR="$WORK_DIR/mariadb-glue-objs-64" else - GLUE_OBJ_DIR="$SCRIPT_DIR/mariadb-glue-objs" + GLUE_OBJ_DIR="$WORK_DIR/mariadb-glue-objs" fi mkdir -p "$GLUE_OBJ_DIR" @@ -349,6 +395,7 @@ cmake "$SRC_DIR" \ -DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE" \ -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ -DIMPORT_EXECUTABLES="$HOST_BUILD_DIR/import_executables.cmake" \ + -DKANDELO_GLUE_OBJ_DIR="$GLUE_OBJ_DIR" \ \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_FLAGS_RELEASE="${MARIADB_OPT_LEVEL:--O2} -DNDEBUG" \ @@ -508,11 +555,26 @@ else echo "WARNING: mysql-test directory not found in source tree" >&2 fi -# Install into local-binaries/ so the resolver picks the freshly-built -# binary over the fetched release. Use $INSTALL_DIR (set per WASM_ARCH -# above) — hard-coding mariadb-install/ lost the wasm64 build's output -# at mariadb-install-64/, which then made build-mariadb-vfs.sh's wasm64 -# branch fail with "mariadbd.wasm not found". -source "$REPO_ROOT/scripts/install-local-binary.sh" -WASM_POSIX_DEP_TARGET_ARCH="$WASM_ARCH" install_local_binary mariadb "$INSTALL_DIR/bin/mariadbd.wasm" mariadbd.wasm -[ -f "$INSTALL_DIR/bin/mysqltest.wasm" ] && WASM_POSIX_DEP_TARGET_ARCH="$WASM_ARCH" install_local_binary mariadb "$INSTALL_DIR/bin/mysqltest.wasm" mysqltest.wasm || true +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$INSTALL_DIR/bin/mariadbd.wasm" + wasm_require_fork_instrumentation_if_needed "$INSTALL_DIR/bin/mariadbd.wasm" + rm -rf "$WASM_POSIX_DEP_OUT_DIR" + mkdir -p "$WASM_POSIX_DEP_OUT_DIR" + cp "$INSTALL_DIR/bin/mariadbd.wasm" "$WASM_POSIX_DEP_OUT_DIR/mariadbd.wasm" + if [ -f "$INSTALL_DIR/bin/mysqltest.wasm" ]; then + wasm_require_no_legacy_asyncify "$INSTALL_DIR/bin/mysqltest.wasm" + wasm_require_fork_instrumentation_if_needed "$INSTALL_DIR/bin/mysqltest.wasm" + cp "$INSTALL_DIR/bin/mysqltest.wasm" "$WASM_POSIX_DEP_OUT_DIR/mysqltest.wasm" + fi + echo "==> Installed MariaDB outputs to $WASM_POSIX_DEP_OUT_DIR" +else + # Install into local-binaries/ so the resolver picks the freshly-built + # binary over the fetched release. Use $INSTALL_DIR (set per WASM_ARCH + # above) — hard-coding mariadb-install/ lost the wasm64 build's output + # at mariadb-install-64/, which then made build-mariadb-vfs.sh's wasm64 + # branch fail with "mariadbd.wasm not found". + source "$REPO_ROOT/scripts/install-local-binary.sh" + WASM_POSIX_DEP_TARGET_ARCH="$WASM_ARCH" install_local_binary mariadb "$INSTALL_DIR/bin/mariadbd.wasm" mariadbd.wasm + [ -f "$INSTALL_DIR/bin/mysqltest.wasm" ] && WASM_POSIX_DEP_TARGET_ARCH="$WASM_ARCH" install_local_binary mariadb "$INSTALL_DIR/bin/mysqltest.wasm" mysqltest.wasm || true +fi diff --git a/packages/registry/mariadb/package.toml b/packages/registry/mariadb/package.toml index 08a5ce7aef..777de9e654 100644 --- a/packages/registry/mariadb/package.toml +++ b/packages/registry/mariadb/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "mariadb" version = "10.5.28" -kernel_abi = 7 +kernel_abi = 16 depends_on = ["pcre2-source@10.44", "libcxx@21.1.7"] # Opt into wasm64 — see memory/wasm64-build-policy.md. arches = ["wasm32", "wasm64"] diff --git a/packages/registry/mariadb/wasm32-posix-toolchain.cmake b/packages/registry/mariadb/wasm32-posix-toolchain.cmake index d7cb507882..73586dad59 100644 --- a/packages/registry/mariadb/wasm32-posix-toolchain.cmake +++ b/packages/registry/mariadb/wasm32-posix-toolchain.cmake @@ -114,10 +114,10 @@ string(REPLACE ";" " " WASM32_LINK_FLAGS_STR "${WASM32_LINK_FLAGS}") # crt1.o provides _start; glue objects provide syscall channel + compiler builtins; # libc.a/libc++.a provide C/C++ standard libraries. get_filename_component(_TOOLCHAIN_DIR2 "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) -set(_GLUE_OBJ_DIR "${_TOOLCHAIN_DIR2}/mariadb-glue-objs") +set(KANDELO_GLUE_OBJ_DIR "${_TOOLCHAIN_DIR2}/mariadb-glue-objs" CACHE PATH "Directory containing Kandelo glue objects") set(CMAKE_EXE_LINKER_FLAGS_INIT - "${WASM32_LINK_FLAGS_STR} ${WASM_POSIX_SYSROOT}/lib/crt1.o ${_GLUE_OBJ_DIR}/channel_syscall.o ${_GLUE_OBJ_DIR}/compiler_rt.o -lc++ -lc++abi -lc" + "${WASM32_LINK_FLAGS_STR} ${WASM_POSIX_SYSROOT}/lib/crt1.o ${KANDELO_GLUE_OBJ_DIR}/channel_syscall.o ${KANDELO_GLUE_OBJ_DIR}/compiler_rt.o -lc++ -lc++abi -lc" ) # --- Type sizes for wasm32 ILP32 --- diff --git a/packages/registry/mariadb/wasm64-posix-toolchain.cmake b/packages/registry/mariadb/wasm64-posix-toolchain.cmake index 8a50a50447..97c0b63b4d 100644 --- a/packages/registry/mariadb/wasm64-posix-toolchain.cmake +++ b/packages/registry/mariadb/wasm64-posix-toolchain.cmake @@ -110,10 +110,10 @@ string(REPLACE ";" " " WASM64_LINK_FLAGS_STR "${WASM64_LINK_FLAGS}") # --- Startup objects and runtime libraries --- get_filename_component(_TOOLCHAIN_DIR2 "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) -set(_GLUE_OBJ_DIR "${_TOOLCHAIN_DIR2}/mariadb-glue-objs-64") +set(KANDELO_GLUE_OBJ_DIR "${_TOOLCHAIN_DIR2}/mariadb-glue-objs-64" CACHE PATH "Directory containing Kandelo glue objects") set(CMAKE_EXE_LINKER_FLAGS_INIT - "${WASM64_LINK_FLAGS_STR} ${WASM_POSIX_SYSROOT}/lib/crt1.o ${_GLUE_OBJ_DIR}/channel_syscall.o ${_GLUE_OBJ_DIR}/compiler_rt.o -lc++ -lc++abi -lc" + "${WASM64_LINK_FLAGS_STR} ${WASM_POSIX_SYSROOT}/lib/crt1.o ${KANDELO_GLUE_OBJ_DIR}/channel_syscall.o ${KANDELO_GLUE_OBJ_DIR}/compiler_rt.o -lc++ -lc++abi -lc" ) # --- Type sizes for wasm64 LP64 --- diff --git a/packages/registry/ncurses/build-ncurses.sh b/packages/registry/ncurses/build-ncurses.sh index 9e70790c02..dcb3aa7b9f 100755 --- a/packages/registry/ncurses/build-ncurses.sh +++ b/packages/registry/ncurses/build-ncurses.sh @@ -30,23 +30,52 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/ncurses-src" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Inputs from resolver, with legacy fallbacks --- NCURSES_VERSION="${WASM_POSIX_DEP_VERSION:-${NCURSES_VERSION:-6.5}}" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/ncurses-install}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://ftpmirror.gnu.org/gnu/ncurses/ncurses-${NCURSES_VERSION}.tar.gz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +if [ "$TARGET_ARCH" != "wasm32" ]; then + echo "ERROR: ncurses is currently packaged for wasm32 only, got $TARGET_ARCH" >&2 + exit 2 +fi + +TOOL_PREFIX="wasm32posix" +CC="${TOOL_PREFIX}-cc" +CXX="${TOOL_PREFIX}-c++" +AR="${TOOL_PREFIX}-ar" +RANLIB="${TOOL_PREFIX}-ranlib" +SRC_DIR="$WORK_DIR/ncurses-src" +SOURCE_MARKER="$SRC_DIR/.kandelo-ncurses-source" +RESOLVER_MODE=0 +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + RESOLVER_MODE=1 +fi + +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 exit 1 fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$NCURSES_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing ncurses source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading ncurses $NCURSES_VERSION..." - TARBALL="/tmp/ncurses-${NCURSES_VERSION}.tar.gz" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-ncurses-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/ncurses-${NCURSES_VERSION}.tar.gz" curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." @@ -56,12 +85,14 @@ if [ ! -d "$SRC_DIR" ]; then fi mkdir -p "$SRC_DIR" tar xzf "$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "$TARBALL" + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" echo "==> Source extracted to $SRC_DIR" fi # --- Build host tic + infocmp once (needed to generate fallback.c) --- -HOST_BUILD_DIR="$SCRIPT_DIR/ncurses-host-build" +HOST_BUILD_DIR="$WORK_DIR/ncurses-host-build" HOST_TIC="$HOST_BUILD_DIR/progs/tic" HOST_INFOCMP="$HOST_BUILD_DIR/progs/infocmp" if [ ! -f "$HOST_TIC" ] || [ ! -f "$HOST_INFOCMP" ]; then @@ -89,7 +120,7 @@ fi # --- Compile minimal terminfo DB (build-time intermediate, not a declared output) --- # Fed into MKfallback.sh below to produce the compiled-in fallback table. -TERMINFO_DIR="$SCRIPT_DIR/terminfo" +TERMINFO_DIR="$WORK_DIR/terminfo" if [ ! -f "$TERMINFO_DIR/x/xterm-256color" ]; then echo "==> Compiling host-side terminfo database..." mkdir -p "$TERMINFO_DIR" @@ -102,7 +133,7 @@ fi # cache-miss invocations, and autoconf bakes the prefix into the # Makefile, so reusing a stale wasm-build dir would `make install` # into the wrong path. -WASM_BUILD_DIR="$SCRIPT_DIR/ncurses-wasm-build" +WASM_BUILD_DIR="$WORK_DIR/ncurses-wasm-build" rm -rf "$WASM_BUILD_DIR" "$INSTALL_DIR" mkdir -p "$WASM_BUILD_DIR" @@ -126,11 +157,11 @@ export ac_cv_sizeof_void_p=4 ( cd "$WASM_BUILD_DIR" - CC=wasm32posix-cc \ - CXX=wasm32posix-c++ \ - AR=wasm32posix-ar \ - RANLIB=wasm32posix-ranlib \ - LD=wasm32posix-cc \ + CC="$CC" \ + CXX="$CXX" \ + AR="$AR" \ + RANLIB="$RANLIB" \ + LD="$CC" \ CFLAGS="-O2" \ LDFLAGS="" \ "$SRC_DIR/configure" \ @@ -239,7 +270,7 @@ NCURSES_PROGRAMS=( captoinfo infotocap ) -BIN_DIR="$SCRIPT_DIR/bin" +BIN_DIR="$WORK_DIR/bin" rm -rf "$BIN_DIR" mkdir -p "$BIN_DIR" @@ -251,12 +282,23 @@ for program in "${NCURSES_PROGRAMS[@]}"; do exit 1 fi cp "$source_path" "$BIN_DIR/$program.wasm" + if [ "$RESOLVER_MODE" = "1" ]; then + cp "$source_path" "$INSTALL_DIR/$program.wasm" + fi done -source "$REPO_ROOT/scripts/install-local-binary.sh" -for program in "${NCURSES_PROGRAMS[@]}"; do - install_local_binary ncurses "$BIN_DIR/$program.wasm" -done +if [ "$RESOLVER_MODE" = "1" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + for program in "${NCURSES_PROGRAMS[@]}"; do + wasm_require_no_legacy_asyncify "$BIN_DIR/$program.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/$program.wasm" + done +else + source "$REPO_ROOT/scripts/install-local-binary.sh" + for program in "${NCURSES_PROGRAMS[@]}"; do + install_local_binary ncurses "$BIN_DIR/$program.wasm" + done +fi if [ -f "$INSTALL_DIR/lib/libncursesw.a" ] && [ -f "$INSTALL_DIR/lib/libtinfow.a" ]; then echo "" diff --git a/packages/registry/nginx/build-nginx-local.sh b/packages/registry/nginx/build-nginx-local.sh index 1865d79578..aa6ecb6ce4 100755 --- a/packages/registry/nginx/build-nginx-local.sh +++ b/packages/registry/nginx/build-nginx-local.sh @@ -2,27 +2,77 @@ set -euo pipefail NGINX_VERSION="${NGINX_VERSION:-1.24.0}" +NGINX_SOURCE_URL="${NGINX_SOURCE_URL:-https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz}" +NGINX_SOURCE_SHA256="${NGINX_SOURCE_SHA256:-77a2541637b92a621e3ee76776c8b7b40cf6d707e69ba53a940283e30ff2f55d}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SRC_DIR="$SCRIPT_DIR/nginx-src" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" + +ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" +if [ "$ARCH" != "wasm32" ]; then + echo "ERROR: nginx package currently supports wasm32 only, got '$ARCH'." >&2 + exit 1 +fi + +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +DOWNLOAD_DIR="$WORK_DIR/downloads" +SRC_DIR="$WORK_DIR/nginx-src" BUILD_DIR="$SRC_DIR/objs" +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BIN_DIR="$WORK_DIR/bin" +else + BIN_DIR="$SCRIPT_DIR" +fi +OUT_WASM="$BIN_DIR/nginx.wasm" +SOURCE_MARKER="$SRC_DIR/.kandelo-source" + +sha256_file() { + python3 - "$1" <<'PY' +import hashlib +import sys + +h = hashlib.sha256() +with open(sys.argv[1], "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) +print(h.hexdigest()) +PY +} if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 + echo "ERROR: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2 exit 1 fi -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" SYSROOT="$REPO_ROOT/sysroot" export WASM_POSIX_SYSROOT="$SYSROOT" # Download nginx source -if [ ! -d "$SRC_DIR" ]; then +mkdir -p "$DOWNLOAD_DIR" +TARBALL="nginx-${NGINX_VERSION}.tar.gz" +ARCHIVE="$DOWNLOAD_DIR/$TARBALL" +if [ ! -f "$ARCHIVE" ]; then echo "==> Downloading nginx $NGINX_VERSION..." - TARBALL="nginx-${NGINX_VERSION}.tar.gz" - curl -fsSL "https://nginx.org/download/${TARBALL}" -o "/tmp/${TARBALL}" + curl -fsSL "$NGINX_SOURCE_URL" -o "$ARCHIVE" +fi + +if [ -n "$NGINX_SOURCE_SHA256" ]; then + actual_sha="$(sha256_file "$ARCHIVE")" + if [ "$actual_sha" != "$NGINX_SOURCE_SHA256" ]; then + echo "ERROR: source SHA256 mismatch for $ARCHIVE" >&2 + echo " expected: $NGINX_SOURCE_SHA256" >&2 + echo " actual: $actual_sha" >&2 + exit 1 + fi +fi + +if [ ! -d "$SRC_DIR" ] || [ ! -f "$SOURCE_MARKER" ] || [ "$(cat "$SOURCE_MARKER")" != "$NGINX_VERSION $NGINX_SOURCE_SHA256" ]; then + echo "==> Extracting nginx $NGINX_VERSION..." + rm -rf "$SRC_DIR" mkdir -p "$SRC_DIR" - tar xzf "/tmp/${TARBALL}" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/${TARBALL}" + tar xzf "$ARCHIVE" -C "$SRC_DIR" --strip-components=1 + printf '%s %s\n' "$NGINX_VERSION" "$NGINX_SOURCE_SHA256" > "$SOURCE_MARKER" fi cd "$SRC_DIR" @@ -409,8 +459,9 @@ if [ -f objs/ngx_modules.c ]; then OBJS+=(objs/ngx_modules.o) fi +mkdir -p "$BIN_DIR" echo " Linking nginx.wasm..." -wasm32posix-cc "${OBJS[@]}" -o "$SCRIPT_DIR/nginx.wasm" -lcrypt +wasm32posix-cc "${OBJS[@]}" -o "$OUT_WASM" -lcrypt # Fork instrumentation (master_process on requires fork children to # resume from the fork point rather than re-executing _start). @@ -419,13 +470,23 @@ wasm32posix-cc "${OBJS[@]}" -o "$SCRIPT_DIR/nginx.wasm" -lcrypt # and any later pass reordering globals would corrupt the fork buffer. FORK_INSTRUMENT="$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" echo " Applying fork instrumentation..." -"$FORK_INSTRUMENT" "$SCRIPT_DIR/nginx.wasm" -o "$SCRIPT_DIR/nginx.wasm.instr" -mv "$SCRIPT_DIR/nginx.wasm.instr" "$SCRIPT_DIR/nginx.wasm" +"$FORK_INSTRUMENT" "$OUT_WASM" -o "$OUT_WASM.instr" +mv "$OUT_WASM.instr" "$OUT_WASM" echo "==> nginx.wasm built successfully!" -ls -la "$SCRIPT_DIR/nginx.wasm" - -# Install into local-binaries/ so the resolver picks the freshly-built -# binary over the fetched release. -source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary nginx "$SCRIPT_DIR/nginx.wasm" +ls -la "$OUT_WASM" + +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$OUT_WASM" + wasm_require_fork_instrumentation_if_needed "$OUT_WASM" + rm -rf "$WASM_POSIX_DEP_OUT_DIR" + mkdir -p "$WASM_POSIX_DEP_OUT_DIR" + cp "$OUT_WASM" "$WASM_POSIX_DEP_OUT_DIR/nginx.wasm" + echo "==> Installed nginx.wasm to $WASM_POSIX_DEP_OUT_DIR" +else + # Install into local-binaries/ so the resolver picks the freshly-built + # binary over the fetched release. + source "$REPO_ROOT/scripts/install-local-binary.sh" + install_local_binary nginx "$OUT_WASM" +fi diff --git a/packages/registry/nginx/build-nginx.sh b/packages/registry/nginx/build-nginx.sh index 8d828ebe18..640e3531c4 100755 --- a/packages/registry/nginx/build-nginx.sh +++ b/packages/registry/nginx/build-nginx.sh @@ -12,8 +12,18 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" + +ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" +if [ "$ARCH" != "wasm32" ]; then + echo "ERROR: nginx package currently supports wasm32 only, got '$ARCH'." >&2 + exit 1 +fi # Force the upstream script to use the version this manifest pins. export NGINX_VERSION="${WASM_POSIX_DEP_VERSION:-1.24.0}" +export NGINX_SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz}" +export NGINX_SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-77a2541637b92a621e3ee76776c8b7b40cf6d707e69ba53a940283e30ff2f55d}" bash "$REPO_ROOT/packages/registry/nginx/build-nginx-local.sh" diff --git a/packages/registry/nginx/package.toml b/packages/registry/nginx/package.toml index 56c9f044ba..4e3f4b259c 100644 --- a/packages/registry/nginx/package.toml +++ b/packages/registry/nginx/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "nginx" version = "1.24.0" -kernel_abi = 7 +kernel_abi = 16 depends_on = [] [source] diff --git a/packages/registry/node/package.toml b/packages/registry/node/package.toml index 5b0e544605..c9ece88874 100644 --- a/packages/registry/node/package.toml +++ b/packages/registry/node/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "node" version = "0.1.0" -kernel_abi = 7 +kernel_abi = 16 depends_on = ["spidermonkey@140.11.0esr"] [source] diff --git a/packages/registry/openssl/build-openssl.sh b/packages/registry/openssl/build-openssl.sh index e26e729871..378cf00d4f 100755 --- a/packages/registry/openssl/build-openssl.sh +++ b/packages/registry/openssl/build-openssl.sh @@ -10,31 +10,75 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SRC_DIR="$SCRIPT_DIR/openssl-src" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Resolver contract (with legacy fallbacks) --- OPENSSL_VERSION="${WASM_POSIX_DEP_VERSION:-${OPENSSL_VERSION:-3.3.2}}" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/openssl-install}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +case "$TARGET_ARCH" in + wasm32) + TOOL_PREFIX="wasm32posix" + OPENSSL_TARGET="linux-generic32" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" + ;; + wasm64) + TOOL_PREFIX="wasm64posix" + OPENSSL_TARGET="linux-generic64" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot64}" + ;; + *) + echo "ERROR: unsupported WASM_POSIX_DEP_TARGET_ARCH=$TARGET_ARCH" >&2 + exit 2 + ;; +esac + +CC="${TOOL_PREFIX}-cc" +AR="${TOOL_PREFIX}-ar" +RANLIB="${TOOL_PREFIX}-ranlib" +SRC_DIR="$WORK_DIR/openssl-src-$TARGET_ARCH" +SOURCE_MARKER="$SRC_DIR/.kandelo-openssl-source" +export WASM_POSIX_SYSROOT="$SYSROOT" + +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 + exit 1 +fi + +if [ ! -f "$SYSROOT/lib/libc.a" ]; then + echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh for $TARGET_ARCH first." >&2 exit 1 fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$OPENSSL_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing OpenSSL source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading OpenSSL $OPENSSL_VERSION..." - TARBALL="openssl-${OPENSSL_VERSION}.tar.gz" - curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "/tmp/${TARBALL}" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-openssl-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/openssl-${OPENSSL_VERSION}.tar.gz" + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." - echo "$SOURCE_SHA256 /tmp/${TARBALL}" | shasum -a 256 -c - + echo "$SOURCE_SHA256 $TARBALL" | shasum -a 256 -c - fi mkdir -p "$SRC_DIR" - tar xzf "/tmp/${TARBALL}" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/${TARBALL}" + tar xzf "$TARBALL" -C "$SRC_DIR" --strip-components=1 + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" fi cd "$SRC_DIR" @@ -45,12 +89,13 @@ if [ -f Makefile ]; then fi rm -rf "$INSTALL_DIR" -# Configure for Wasm using linux-generic32 target. -echo "==> Configuring OpenSSL for Wasm..." -CC=wasm32posix-cc \ -AR=wasm32posix-ar \ -RANLIB=wasm32posix-ranlib \ -perl Configure linux-generic32 \ +# Configure for Wasm using OpenSSL's generic Linux targets. The SDK +# wrapper supplies the actual wasm target triple. +echo "==> Configuring OpenSSL for $TARGET_ARCH..." +CC="$CC" \ +AR="$AR" \ +RANLIB="$RANLIB" \ +perl Configure "$OPENSSL_TARGET" \ -DHAVE_FORK=0 \ -DOPENSSL_NO_AFALGENG=1 \ -DOPENSSL_NO_UI_CONSOLE=1 \ @@ -70,12 +115,14 @@ perl Configure linux-generic32 \ --prefix="$INSTALL_DIR" \ --openssldir=/etc/ssl -# Patch Makefile: remove cross-compile prefix + the -m32 that -# linux-generic32 assumes. +# Patch Makefile: remove cross-compile prefix and host-only -m32/-m64 +# switches that the linux-generic* targets assume. echo "==> Patching Makefile..." sed -i.bak 's/^CROSS_COMPILE=.*/CROSS_COMPILE=/' Makefile sed -i.bak 's/ -m32 / /g' Makefile sed -i.bak 's/ -m32$//' Makefile +sed -i.bak 's/ -m64 / /g' Makefile +sed -i.bak 's/ -m64$//' Makefile rm -f Makefile.bak echo "==> Building OpenSSL..." diff --git a/packages/registry/redis/build-redis.sh b/packages/registry/redis/build-redis.sh index 1c4a8d30b3..1d229f3a76 100755 --- a/packages/registry/redis/build-redis.sh +++ b/packages/registry/redis/build-redis.sh @@ -6,35 +6,80 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -VERSION="7.2.7" +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" + +ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" +if [ "$ARCH" != "wasm32" ]; then + echo "ERROR: Redis package currently supports wasm32 only, got '$ARCH'." >&2 + exit 1 +fi + +VERSION="${WASM_POSIX_DEP_VERSION:-7.2.5}" TARBALL="redis-${VERSION}.tar.gz" -SRC_DIR="$SCRIPT_DIR/redis-src" -BIN_DIR="$SCRIPT_DIR/bin" +SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://github.com/redis/redis/archive/refs/tags/${VERSION}.tar.gz}" +SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-98a8502a2e902d2a9785ef46a69a5f8d5e24cbf9ea3ae4d845afcfc6778aa783}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +DOWNLOAD_DIR="$WORK_DIR/downloads" +SRC_DIR="$WORK_DIR/redis-src" +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BIN_DIR="$WORK_DIR/bin" +else + BIN_DIR="$SCRIPT_DIR/bin" +fi +SOURCE_MARKER="$SRC_DIR/.kandelo-source" + +sha256_file() { + python3 - "$1" <<'PY' +import hashlib +import sys + +h = hashlib.sha256() +with open(sys.argv[1], "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) +print(h.hexdigest()) +PY +} # Check SDK if ! command -v wasm32posix-cc &>/dev/null; then - echo "Error: wasm32posix-cc not found. Install the SDK first." >&2 + echo "Error: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2 exit 1 fi +mkdir -p "$DOWNLOAD_DIR" + # Download if needed -if [ ! -f "$SCRIPT_DIR/$TARBALL" ]; then +ARCHIVE="$DOWNLOAD_DIR/$TARBALL" +if [ ! -f "$ARCHIVE" ]; then echo "==> Downloading Redis $VERSION..." # `-f` (--fail) is load-bearing here: without it, curl returns 0 # and writes the error HTML payload to TARBALL on a 5xx response, # which then poisons the tar-extract step downstream. Combined # with --retry to ride out transient mirror outages (#406). curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL \ - -o "$SCRIPT_DIR/$TARBALL" \ - "https://github.com/redis/redis/archive/refs/tags/${VERSION}.tar.gz" + -o "$ARCHIVE" \ + "$SOURCE_URL" +fi + +if [ -n "$SOURCE_SHA256" ]; then + actual_sha="$(sha256_file "$ARCHIVE")" + if [ "$actual_sha" != "$SOURCE_SHA256" ]; then + echo "ERROR: source SHA256 mismatch for $ARCHIVE" >&2 + echo " expected: $SOURCE_SHA256" >&2 + echo " actual: $actual_sha" >&2 + exit 1 + fi fi # Extract if needed -if [ ! -d "$SRC_DIR/src" ]; then +if [ ! -d "$SRC_DIR/src" ] || [ ! -f "$SOURCE_MARKER" ] || [ "$(cat "$SOURCE_MARKER")" != "$VERSION $SOURCE_SHA256" ]; then echo "==> Extracting..." rm -rf "$SRC_DIR" - tar xf "$SCRIPT_DIR/$TARBALL" -C "$SCRIPT_DIR" - mv "$SCRIPT_DIR/redis-${VERSION}" "$SRC_DIR" + tar xf "$ARCHIVE" -C "$WORK_DIR" + mv "$WORK_DIR/redis-${VERSION}" "$SRC_DIR" + printf '%s %s\n' "$VERSION" "$SOURCE_SHA256" > "$SOURCE_MARKER" fi cd "$SRC_DIR" @@ -149,8 +194,29 @@ cp redis-cli "$BIN_DIR/redis-cli.wasm" echo "==> Redis binaries:" ls -lh "$BIN_DIR/" -# Install into local-binaries/ so the resolver picks the freshly-built -# binary over the fetched release. -source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary redis "$SCRIPT_DIR/bin/redis-server.wasm" redis-server.wasm -install_local_binary redis "$SCRIPT_DIR/bin/redis-cli.wasm" redis-cli.wasm +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + if wasm_imports_kernel_fork "$BIN_DIR/redis-server.wasm" && + ! wasm_has_complete_fork_instrumentation "$BIN_DIR/redis-server.wasm"; then + echo " applying wasm-fork-instrument to redis-server.wasm" + "$REPO_ROOT/scripts/run-wasm-fork-instrument.sh" \ + "$BIN_DIR/redis-server.wasm" \ + -o "$BIN_DIR/redis-server.wasm.instr" + mv "$BIN_DIR/redis-server.wasm.instr" "$BIN_DIR/redis-server.wasm" + fi + wasm_require_no_legacy_asyncify "$BIN_DIR/redis-server.wasm" + wasm_require_fork_instrumentation_if_needed "$BIN_DIR/redis-server.wasm" + wasm_require_no_legacy_asyncify "$BIN_DIR/redis-cli.wasm" + wasm_require_fork_instrumentation_if_needed "$BIN_DIR/redis-cli.wasm" + rm -rf "$WASM_POSIX_DEP_OUT_DIR" + mkdir -p "$WASM_POSIX_DEP_OUT_DIR" + cp "$BIN_DIR/redis-server.wasm" "$WASM_POSIX_DEP_OUT_DIR/redis-server.wasm" + cp "$BIN_DIR/redis-cli.wasm" "$WASM_POSIX_DEP_OUT_DIR/redis-cli.wasm" + echo "==> Installed Redis outputs to $WASM_POSIX_DEP_OUT_DIR" +else + # Install into local-binaries/ so the resolver picks the freshly-built + # binary over the fetched release. + source "$REPO_ROOT/scripts/install-local-binary.sh" + install_local_binary redis "$BIN_DIR/redis-server.wasm" redis-server.wasm + install_local_binary redis "$BIN_DIR/redis-cli.wasm" redis-cli.wasm +fi diff --git a/packages/registry/redis/build.toml b/packages/registry/redis/build.toml index 9c22fbc219..b6c000b57f 100644 --- a/packages/registry/redis/build.toml +++ b/packages/registry/redis/build.toml @@ -1,7 +1,7 @@ script_path = "packages/registry/redis/build-redis.sh" repo_url = "https://github.com/brandonpayton/kandelo.git" commit = "8c53383229fab78f97b098c3207a655159c03041" -revision = 1 +revision = 2 [binary] index_url = "https://github.com/Automattic/kandelo/releases/download/binaries-abi-v{abi}/index.toml" diff --git a/packages/registry/redis/package.toml b/packages/registry/redis/package.toml index f44b8c8e2e..04bca04d54 100644 --- a/packages/registry/redis/package.toml +++ b/packages/registry/redis/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "redis" version = "7.2.5" -kernel_abi = 7 +kernel_abi = 16 depends_on = [] [source] diff --git a/packages/registry/spidermonkey-node/build-spidermonkey-node.sh b/packages/registry/spidermonkey-node/build-spidermonkey-node.sh index 7e08253f96..359326de38 100755 --- a/packages/registry/spidermonkey-node/build-spidermonkey-node.sh +++ b/packages/registry/spidermonkey-node/build-spidermonkey-node.sh @@ -19,8 +19,8 @@ SPIDERMONKEY_PREFIX="${WASM_POSIX_DEP_SPIDERMONKEY_DIR:-}" NODE_WASM="" for candidate in \ - "$REPO_ROOT/packages/registry/spidermonkey/bin/node.wasm" \ "${SPIDERMONKEY_PREFIX:+$SPIDERMONKEY_PREFIX/node.wasm}" \ + "$REPO_ROOT/packages/registry/spidermonkey/bin/node.wasm" \ "$REPO_ROOT/local-binaries/programs/$ARCH/spidermonkey-node.wasm"; do if [ -z "$NODE_WASM" ] && [ -f "$candidate" ]; then NODE_WASM="$candidate" @@ -50,15 +50,28 @@ if [ -z "$NODE_WASM" ]; then fi fi -BIN_DIR="$SCRIPT_DIR/bin" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BIN_DIR="$WORK_DIR/bin" +else + BIN_DIR="$SCRIPT_DIR/bin" +fi mkdir -p "$BIN_DIR" cp "$NODE_WASM" "$BIN_DIR/node.wasm" NODE_SIZE="$(wc -c < "$BIN_DIR/node.wasm" | tr -d ' ')" echo "==> SpiderMonkey Node-compatible runtime staged: $BIN_DIR/node.wasm ($NODE_SIZE bytes)" -# shellcheck source=/dev/null -if command -v rustc >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1; then +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$BIN_DIR/node.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/node.wasm" + rm -rf "$WASM_POSIX_DEP_OUT_DIR" + mkdir -p "$WASM_POSIX_DEP_OUT_DIR" + cp "$BIN_DIR/node.wasm" "$WASM_POSIX_DEP_OUT_DIR/node.wasm" + echo " installed $WASM_POSIX_DEP_OUT_DIR/node.wasm (resolver scratch)" +elif command -v rustc >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1; then + # shellcheck source=/dev/null source "$REPO_ROOT/scripts/install-local-binary.sh" WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary spidermonkey-node "$BIN_DIR/node.wasm" WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary node "$BIN_DIR/node.wasm" @@ -69,9 +82,4 @@ else cp "$BIN_DIR/node.wasm" "$dest" echo " installed $dest" done - if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then - mkdir -p "$WASM_POSIX_DEP_OUT_DIR" - cp "$BIN_DIR/node.wasm" "$WASM_POSIX_DEP_OUT_DIR/node.wasm" - echo " installed $WASM_POSIX_DEP_OUT_DIR/node.wasm (resolver scratch)" - fi fi diff --git a/packages/registry/spidermonkey-node/package.toml b/packages/registry/spidermonkey-node/package.toml index 16893b7b13..66657ead2a 100644 --- a/packages/registry/spidermonkey-node/package.toml +++ b/packages/registry/spidermonkey-node/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "spidermonkey-node" version = "140.11.0esr-node.1" -kernel_abi = 7 +kernel_abi = 16 depends_on = ["spidermonkey@140.11.0esr"] [source] diff --git a/packages/registry/spidermonkey/build-spidermonkey.sh b/packages/registry/spidermonkey/build-spidermonkey.sh index ea4ba4f53c..0cab25cce8 100755 --- a/packages/registry/spidermonkey/build-spidermonkey.sh +++ b/packages/registry/spidermonkey/build-spidermonkey.sh @@ -13,7 +13,7 @@ source "$REPO_ROOT/sdk/activate.sh" VERSION="${WASM_POSIX_DEP_VERSION:-$(tr -d '[:space:]' < "$SCRIPT_DIR/VERSION")}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://ftp.mozilla.org/pub/firefox/releases/$VERSION/source/firefox-$VERSION.source.tar.xz}" -SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-1b034d2117356fda24807a151055132315c6ba58ad2bdf7ec71ee707fac5e028}" ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" if [ "$ARCH" != "wasm32" ]; then @@ -22,11 +22,16 @@ if [ "$ARCH" != "wasm32" ]; then fi SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" -BIN_DIR="$SCRIPT_DIR/bin" -DOWNLOAD_DIR="$SCRIPT_DIR/downloads" -SRC_PARENT="$SCRIPT_DIR/source" -OBJ_DIR="$SCRIPT_DIR/obj-wasm32" -MOZCONFIG_PATH="$SCRIPT_DIR/mozconfig-wasm32" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BIN_DIR="$WORK_DIR/bin" +else + BIN_DIR="$SCRIPT_DIR/bin" +fi +DOWNLOAD_DIR="$WORK_DIR/downloads" +SRC_PARENT="$WORK_DIR/source" +OBJ_DIR="$WORK_DIR/obj-wasm32" +MOZCONFIG_PATH="$WORK_DIR/mozconfig-wasm32" HOST_OS="$(uname -s)" MACOS_SDK_DIR="${WASM_POSIX_MACOS_SDK_DIR:-}" @@ -53,6 +58,18 @@ if [ ! -f "$SYSROOT/lib/libc.a" ]; then exit 1 fi +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + BASE_SYSROOT="$SYSROOT" + SYSROOT="$WORK_DIR/sysroot" + if [ ! -f "$SYSROOT/lib/libc.a" ]; then + echo "==> Copying sysroot into package work directory..." + rm -rf "$SYSROOT" + mkdir -p "$SYSROOT" + cp -R "$BASE_SYSROOT/." "$SYSROOT" + fi + export WASM_POSIX_SYSROOT="$SYSROOT" +fi + for required_tool in python3 rustc cargo cbindgen node curl make; do if ! command -v "$required_tool" >/dev/null 2>&1; then echo "ERROR: required host tool '$required_tool' not found in PATH." >&2 @@ -128,7 +145,7 @@ ln -sf "$ZLIB_PREFIX/lib/libz.a" "$SYSROOT/lib/libz.a" rm -rf "$SYSROOT/include/c++/v1" ln -sfn "$LIBCXX_PREFIX/include/c++/v1" "$SYSROOT/include/c++/v1" -mkdir -p "$BIN_DIR" "$DOWNLOAD_DIR" "$SRC_PARENT" +mkdir -p "$BIN_DIR" "$DOWNLOAD_DIR" "$SRC_PARENT" "$WORK_DIR" find_mach_dir() { local mach_path @@ -277,7 +294,7 @@ if [ -n "$MACOS_SDK_DIR" ]; then fi export MOZCONFIG="$MOZCONFIG_PATH" -export MOZBUILD_STATE_PATH="$SCRIPT_DIR/.mozbuild" +export MOZBUILD_STATE_PATH="$WORK_DIR/.mozbuild" export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system TARGET_OS_DEFINES="${WASM_POSIX_TARGET_OS_DEFINES:--D__linux__=1 -D__unix__=1}" @@ -357,8 +374,21 @@ cp "$BIN_DIR/js.wasm" "$BIN_DIR/node.wasm" NODE_SIZE="$(wc -c < "$BIN_DIR/node.wasm" | tr -d ' ')" echo "==> SpiderMonkey Node-compatible runtime staged: $BIN_DIR/node.wasm ($NODE_SIZE bytes)" -# shellcheck source=/dev/null -source "$REPO_ROOT/scripts/install-local-binary.sh" -WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary spidermonkey "$BIN_DIR/js.wasm" -WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary spidermonkey-node "$BIN_DIR/node.wasm" -WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary node "$BIN_DIR/node.wasm" +if [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$BIN_DIR/js.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/js.wasm" + wasm_require_no_legacy_asyncify "$BIN_DIR/node.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/node.wasm" + rm -rf "$WASM_POSIX_DEP_OUT_DIR" + mkdir -p "$WASM_POSIX_DEP_OUT_DIR" + cp "$BIN_DIR/js.wasm" "$WASM_POSIX_DEP_OUT_DIR/js.wasm" + cp "$BIN_DIR/node.wasm" "$WASM_POSIX_DEP_OUT_DIR/node.wasm" + echo "==> Installed SpiderMonkey outputs to $WASM_POSIX_DEP_OUT_DIR" +else + # shellcheck source=/dev/null + source "$REPO_ROOT/scripts/install-local-binary.sh" + WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary spidermonkey "$BIN_DIR/js.wasm" + WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary spidermonkey-node "$BIN_DIR/node.wasm" + WASM_POSIX_INSTALL_FORK_INSTRUMENTATION=disabled install_local_binary node "$BIN_DIR/node.wasm" +fi diff --git a/packages/registry/spidermonkey/package.toml b/packages/registry/spidermonkey/package.toml index e0a5e2408e..b48e06464f 100644 --- a/packages/registry/spidermonkey/package.toml +++ b/packages/registry/spidermonkey/package.toml @@ -1,7 +1,7 @@ kind = "program" name = "spidermonkey" version = "140.11.0esr" -kernel_abi = 7 +kernel_abi = 16 depends_on = ["libcxx@21.1.7", "openssl@3.3.2", "zlib@1.3.1"] [source] diff --git a/packages/registry/sqlite/build-sqlite.sh b/packages/registry/sqlite/build-sqlite.sh index 8503250dbc..e7d205b6c1 100755 --- a/packages/registry/sqlite/build-sqlite.sh +++ b/packages/registry/sqlite/build-sqlite.sh @@ -14,30 +14,63 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/sqlite-src" +source "$REPO_ROOT/sdk/activate.sh" # --- Resolver contract (with legacy fallbacks) --- SQLITE_VERSION="${WASM_POSIX_DEP_VERSION:-${SQLITE_VERSION:-3.49.1}}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/sqlite-src" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/sqlite-install}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" # Legacy default URL uses the packed version form (3.49.1 → 3490100). SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +case "$TARGET_ARCH" in + wasm32) + TOOL_PREFIX="wasm32posix" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" + ;; + wasm64) + TOOL_PREFIX="wasm64posix" + SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot64}" + ;; + *) + echo "ERROR: unsupported WASM_POSIX_DEP_TARGET_ARCH=$TARGET_ARCH" >&2 + exit 2 + ;; +esac +export WASM_POSIX_SYSROOT="$SYSROOT" + # CLI is a consumer artifact, not a library. Skip it when invoked via # the resolver — it would waste cache space and the consumer-side # tooling will build it independently. BUILD_CLI=1 [ -n "${WASM_POSIX_DEP_OUT_DIR:-}" ] && BUILD_CLI=0 -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +if ! command -v "${TOOL_PREFIX}-cc" &>/dev/null; then + echo "ERROR: ${TOOL_PREFIX}-cc not found. Run through scripts/dev-shell.sh." >&2 + exit 1 +fi + +if [ ! -f "$SYSROOT/lib/libc.a" ]; then + echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh for $TARGET_ARCH first." >&2 exit 1 fi # --- Fetch + verify source --- -if [ ! -d "$SRC_DIR/sqlite3.c" ] && [ ! -f "$SRC_DIR/sqlite3.c" ]; then +expected_marker="$(printf '%s\n%s\n%s\n' "$SQLITE_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +SOURCE_MARKER="$SRC_DIR/.kandelo-sqlite-source" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing SQLite source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + +if [ ! -f "$SRC_DIR/sqlite3.c" ]; then echo "==> Downloading SQLite $SQLITE_VERSION..." - TARBALL="/tmp/sqlite-amalgamation.zip" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-sqlite-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + TARBALL="$tmpdir/sqlite-amalgamation.zip" curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." @@ -51,7 +84,9 @@ if [ ! -d "$SRC_DIR/sqlite3.c" ] && [ ! -f "$SRC_DIR/sqlite3.c" ]; then mv "$inner"/* "$SRC_DIR/" rmdir "$inner" fi - rm "$TARBALL" + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" fi SQLITE_CFLAGS="-O2 \ @@ -68,10 +103,10 @@ SQLITE_CFLAGS="-O2 \ # --- Compile library --- echo "==> Compiling SQLite for Wasm..." # shellcheck disable=SC2086 -wasm32posix-cc -c $SQLITE_CFLAGS \ +"${TOOL_PREFIX}-cc" -c $SQLITE_CFLAGS \ "$SRC_DIR/sqlite3.c" -o "$SRC_DIR/sqlite3.o" -wasm32posix-ar rcs "$SRC_DIR/libsqlite3.a" "$SRC_DIR/sqlite3.o" +"${TOOL_PREFIX}-ar" rcs "$SRC_DIR/libsqlite3.a" "$SRC_DIR/sqlite3.o" # --- Install library into INSTALL_DIR --- echo "==> Installing to $INSTALL_DIR..." @@ -98,7 +133,7 @@ if [ "$BUILD_CLI" = "1" ]; then echo "==> Building sqlite3 CLI..." mkdir -p "$INSTALL_DIR/bin" # shellcheck disable=SC2086 - wasm32posix-cc $SQLITE_CFLAGS \ + "${TOOL_PREFIX}-cc" $SQLITE_CFLAGS \ "$SRC_DIR/shell.c" "$SRC_DIR/sqlite3.c" \ -o "$INSTALL_DIR/bin/sqlite3.wasm" -lm diff --git a/packages/registry/xz/build-xz.sh b/packages/registry/xz/build-xz.sh index 92da5fe760..88604d06db 100755 --- a/packages/registry/xz/build-xz.sh +++ b/packages/registry/xz/build-xz.sh @@ -1,42 +1,73 @@ #!/usr/bin/env bash set -euo pipefail -# Build XZ Utils 5.6.4 for wasm32-posix-kernel. +# Build XZ Utils for wasm32-posix-kernel. # # Uses the SDK's wasm32posix-configure wrapper for cross-compilation. # --disable-threads is critical (no pthreads support). -# Output: packages/registry/xz/bin/xz.wasm -# Also installs liblzma.a + headers to sysroot. +# Output: xz.wasm. Resolver/Homebrew invocations install only the declared +# program output into WASM_POSIX_DEP_OUT_DIR; direct legacy invocations still +# populate packages/registry/xz/bin and sysroot. -XZ_VERSION="${XZ_VERSION:-5.6.4}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" -SRC_DIR="$SCRIPT_DIR/xz-src" -BIN_DIR="$SCRIPT_DIR/bin" -SYSROOT="$REPO_ROOT/sysroot" +source "$REPO_ROOT/sdk/activate.sh" + +XZ_VERSION="${WASM_POSIX_DEP_VERSION:-${XZ_VERSION:-5.6.2}}" +SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://tukaani.org/xz/xz-${XZ_VERSION}.tar.xz}" +SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +SRC_DIR="$WORK_DIR/xz-src" +BIN_DIR="$WORK_DIR/bin" +INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" + +if [ "$TARGET_ARCH" != "wasm32" ]; then + echo "ERROR: xz is currently packaged for wasm32 only, got $TARGET_ARCH" >&2 + exit 2 +fi + +SYSROOT="${WASM_POSIX_SYSROOT:-$REPO_ROOT/sysroot}" +export WASM_POSIX_SYSROOT="$SYSROOT" # --- Prerequisites --- if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 + echo "ERROR: wasm32posix-cc not found. Run through scripts/dev-shell.sh." >&2 exit 1 fi if [ ! -f "$SYSROOT/lib/libc.a" ]; then - echo "ERROR: sysroot not found. Run: bash build.sh && bash scripts/build-musl.sh" >&2 + echo "ERROR: sysroot not found at $SYSROOT. Run scripts/build-musl.sh first." >&2 exit 1 fi -export WASM_POSIX_SYSROOT="$SYSROOT" - # --- Download xz source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$XZ_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +SOURCE_MARKER="$SRC_DIR/.kandelo-xz-source" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing xz source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading xz $XZ_VERSION..." - TARBALL="xz-${XZ_VERSION}.tar.gz" - URL="https://github.com/tukaani-project/xz/releases/download/v${XZ_VERSION}/${TARBALL}" - curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$URL" -o "/tmp/$TARBALL" + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-xz-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT + case "$SOURCE_URL" in + *.tar.gz|*.tgz) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar.gz" ;; + *.tar.xz|*.txz) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar.xz" ;; + *) TARBALL="$tmpdir/xz-${XZ_VERSION}.tar" ;; + esac + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$TARBALL" + if [ -n "$SOURCE_SHA256" ]; then + echo "==> Verifying source sha256..." + echo "$SOURCE_SHA256 $TARBALL" | shasum -a 256 -c - + fi mkdir -p "$SRC_DIR" - tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/$TARBALL" + tar xf "$TARBALL" -C "$SRC_DIR" --strip-components=1 + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" echo "==> Source extracted to $SRC_DIR" # Patch: xz excludes __wasm__ from sigprocmask path, but our sysroot has it @@ -98,21 +129,29 @@ else exit 1 fi -# --- Install library to sysroot --- -echo "==> Installing liblzma.a and headers to sysroot..." -if [ -f "$SRC_DIR/src/liblzma/.libs/liblzma.a" ]; then - cp "$SRC_DIR/src/liblzma/.libs/liblzma.a" "$SYSROOT/lib/" - mkdir -p "$SYSROOT/include/lzma" - cp "$SRC_DIR/src/liblzma/api/lzma.h" "$SYSROOT/include/" - cp "$SRC_DIR/src/liblzma/api/lzma/"*.h "$SYSROOT/include/lzma/" - echo "==> Installed liblzma.a and headers" +if [ -n "$INSTALL_DIR" ]; then + source "$REPO_ROOT/scripts/wasm-artifact-guards.sh" + wasm_require_no_legacy_asyncify "$BIN_DIR/xz.wasm" + wasm_require_no_fork_instrumentation "$BIN_DIR/xz.wasm" + rm -rf "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR" + cp "$BIN_DIR/xz.wasm" "$INSTALL_DIR/xz.wasm" + echo "==> Installed xz.wasm to $INSTALL_DIR" +else + # --- Install library to sysroot (legacy direct invocation only) --- + echo "==> Installing liblzma.a and headers to sysroot..." + if [ -f "$SRC_DIR/src/liblzma/.libs/liblzma.a" ]; then + cp "$SRC_DIR/src/liblzma/.libs/liblzma.a" "$SYSROOT/lib/" + mkdir -p "$SYSROOT/include/lzma" + cp "$SRC_DIR/src/liblzma/api/lzma.h" "$SYSROOT/include/" + cp "$SRC_DIR/src/liblzma/api/lzma/"*.h "$SYSROOT/include/lzma/" + echo "==> Installed liblzma.a and headers" + fi + + source "$REPO_ROOT/scripts/install-local-binary.sh" + install_local_binary xz "$SCRIPT_DIR/bin/xz.wasm" fi echo "" echo "==> xz built successfully!" echo "Binary: $BIN_DIR/xz.wasm" - -# Install into local-binaries/ so the resolver picks the freshly-built -# binary over the fetched release. -source "$REPO_ROOT/scripts/install-local-binary.sh" -install_local_binary xz "$SCRIPT_DIR/bin/xz.wasm" diff --git a/packages/registry/zlib/build-zlib.sh b/packages/registry/zlib/build-zlib.sh index 2a5c787139..810a4d8d5d 100644 --- a/packages/registry/zlib/build-zlib.sh +++ b/packages/registry/zlib/build-zlib.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # -# Build zlib for wasm32-posix-kernel. +# Build zlib for wasm32/wasm64-posix-kernel. # # Honors the dep-resolver build-script contract (see -# docs/dependency-management.md). When invoked via +# docs/package-management.md). When invoked via # `cargo xtask build-deps resolve zlib`, these env vars are set by the # resolver and the build installs into the shared cache: # @@ -11,6 +11,8 @@ # WASM_POSIX_DEP_VERSION # upstream version # WASM_POSIX_DEP_SOURCE_URL # tarball URL # WASM_POSIX_DEP_SOURCE_SHA256 # expected sha256 of the tarball +# WASM_POSIX_DEP_TARGET_ARCH # wasm32 or wasm64 +# WASM_POSIX_DEP_WORK_DIR # scratch source/build directory # # For ad-hoc / legacy invocation (`bash build-zlib.sh` with no resolver), # the script falls back to the in-tree `zlib-install/` layout. @@ -18,33 +20,67 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -SRC_DIR="$SCRIPT_DIR/zlib-src" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +# shellcheck source=/dev/null +source "$REPO_ROOT/sdk/activate.sh" # --- Inputs from resolver, with legacy fallbacks --- ZLIB_VERSION="${WASM_POSIX_DEP_VERSION:-${ZLIB_VERSION:-1.3.1}}" INSTALL_DIR="${WASM_POSIX_DEP_OUT_DIR:-$SCRIPT_DIR/zlib-install}" +WORK_DIR="${WASM_POSIX_DEP_WORK_DIR:-$SCRIPT_DIR}" +TARGET_ARCH="${WASM_POSIX_DEP_TARGET_ARCH:-wasm32}" SOURCE_URL="${WASM_POSIX_DEP_SOURCE_URL:-https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz}" SOURCE_SHA256="${WASM_POSIX_DEP_SOURCE_SHA256:-}" -if ! command -v wasm32posix-cc &>/dev/null; then - echo "ERROR: wasm32posix-cc not found. Run 'npm link' in sdk/ first." >&2 +case "$TARGET_ARCH" in + wasm32) + TOOL_PREFIX="wasm32posix" + ;; + wasm64) + TOOL_PREFIX="wasm64posix" + ;; + *) + echo "ERROR: unsupported WASM_POSIX_DEP_TARGET_ARCH=$TARGET_ARCH" >&2 + exit 2 + ;; +esac + +CC="${TOOL_PREFIX}-cc" +AR="${TOOL_PREFIX}-ar" +RANLIB="${TOOL_PREFIX}-ranlib" +SRC_DIR="$WORK_DIR/zlib-src-$TARGET_ARCH" +SOURCE_MARKER="$SRC_DIR/.kandelo-zlib-source" + +if ! command -v "$CC" &>/dev/null; then + echo "ERROR: $CC not found after sourcing sdk/activate.sh." >&2 exit 1 fi # --- Fetch + verify source --- +expected_marker="$(printf '%s\n%s\n%s\n' "$ZLIB_VERSION" "$SOURCE_URL" "$SOURCE_SHA256")" +if [ -d "$SRC_DIR" ] && [ "$(cat "$SOURCE_MARKER" 2>/dev/null || true)" != "$expected_marker" ]; then + echo "==> Existing zlib source does not match requested version/source; cleaning..." + rm -rf "$SRC_DIR" +fi + if [ ! -d "$SRC_DIR" ]; then echo "==> Downloading zlib $ZLIB_VERSION..." + tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/kandelo-zlib-src.XXXXXX")" + trap 'rm -rf "$tmpdir"' EXIT TARBALL="zlib-${ZLIB_VERSION}.tar.gz" - curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "/tmp/$TARBALL" + curl --retry 10 --retry-delay 5 --retry-max-time 300 --retry-all-errors -fsSL "$SOURCE_URL" -o "$tmpdir/$TARBALL" if [ -n "$SOURCE_SHA256" ]; then echo "==> Verifying source sha256..." - echo "$SOURCE_SHA256 /tmp/$TARBALL" | shasum -a 256 -c - + echo "$SOURCE_SHA256 $tmpdir/$TARBALL" | shasum -a 256 -c - else echo "==> (no SOURCE_SHA256 declared; skipping verification)" fi mkdir -p "$SRC_DIR" - tar xzf "/tmp/$TARBALL" -C "$SRC_DIR" --strip-components=1 - rm "/tmp/$TARBALL" + tar xzf "$tmpdir/$TARBALL" -C "$SRC_DIR" --strip-components=1 + printf '%s\n' "$expected_marker" > "$SOURCE_MARKER" + trap - EXIT + rm -rf "$tmpdir" fi cd "$SRC_DIR" @@ -54,18 +90,18 @@ cd "$SRC_DIR" rm -rf "$INSTALL_DIR" echo "==> Configuring zlib for Wasm..." -CC=wasm32posix-cc AR=wasm32posix-ar RANLIB=wasm32posix-ranlib \ - LDSHARED="wasm32posix-cc -shared" \ +CC="$CC" AR="$AR" RANLIB="$RANLIB" \ + LDSHARED="$CC -shared" \ ./configure --static --prefix="$INSTALL_DIR" -# On macOS, zlib's configure uses 'libtool' which is the Xcode one — not wasm-aware. -# Patch the Makefile to use wasm32posix-ar instead. +# On macOS, zlib's configure uses 'libtool' which is the Xcode one, not +# wasm-aware. Patch the Makefile to use the selected Kandelo archive tools. echo "==> Patching Makefile for Wasm ar..." sed -i.bak \ - -e 's|^AR=.*|AR=wasm32posix-ar|' \ + -e "s|^AR=.*|AR=$AR|" \ -e 's|^ARFLAGS=.*|ARFLAGS=rcs|' \ - -e 's|^RANLIB=.*|RANLIB=wasm32posix-ranlib|' \ - -e 's|libtool -o|wasm32posix-ar rcs|g' \ + -e "s|^RANLIB=.*|RANLIB=$RANLIB|" \ + -e "s|libtool -o|$AR rcs|g" \ Makefile && rm -f Makefile.bak echo "==> Building zlib..." diff --git a/scripts/homebrew-bottle-build.sh b/scripts/homebrew-bottle-build.sh index c418f9c82a..a429d1ff0d 100755 --- a/scripts/homebrew-bottle-build.sh +++ b/scripts/homebrew-bottle-build.sh @@ -85,9 +85,10 @@ fi KANDELO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" PATCH_FILE="$KANDELO_ROOT/homebrew/patches/0001-add-kandelo-wasm-bottle-tags.patch" mkdir -p "$OUT_DIR/bottles" -WORK_DIR="$(mktemp -d)" +WORK_DIR="$(mktemp -d "$OUT_DIR/homebrew-work.XXXXXX")" BREW_REPO="" BREW_OVERLAY="" +TAP_SOURCE="$TAP_ROOT" cleanup() { if [ -n "$BREW_REPO" ] && [ -n "$BREW_OVERLAY" ] && [ -d "$BREW_OVERLAY" ]; then @@ -100,7 +101,8 @@ trap cleanup EXIT BREW_REPO="$("$BREW_BIN" --repository)" if [ -f "$PATCH_FILE" ] && git -C "$BREW_REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$BREW_REPO" apply --check "$PATCH_FILE" - BREW_OVERLAY="$WORK_DIR/homebrew-overlay" + BREW_OVERLAY="$OUT_DIR/homebrew-overlay" + rm -rf "$BREW_OVERLAY" git -C "$BREW_REPO" worktree add --detach "$BREW_OVERLAY" HEAD >/dev/null git -C "$BREW_OVERLAY" apply --whitespace=nowarn "$PATCH_FILE" BREW_BIN="$BREW_OVERLAY/bin/brew" @@ -114,13 +116,40 @@ export HOMEBREW_NO_INSTALL_CLEANUP="${HOMEBREW_NO_INSTALL_CLEANUP:-1}" export HOMEBREW_NO_ANALYTICS="${HOMEBREW_NO_ANALYTICS:-1}" export HOMEBREW_DEVELOPER="${HOMEBREW_DEVELOPER:-1}" export KANDELO_HOMEBREW_ARCH="$ARCH" +export HOMEBREW_KANDELO_BOTTLE_TAG="$BOTTLE_TAG" +export KANDELO_HOMEBREW_BOTTLE_TAG="$BOTTLE_TAG" export KANDELO_HOMEBREW_KANDELO_ROOT="$KANDELO_ROOT" export HOMEBREW_KANDELO_ARCH="$ARCH" export HOMEBREW_KANDELO_ROOT="$KANDELO_ROOT" export HOMEBREW_KANDELO_NODE="$(command -v node)" export HOMEBREW_KANDELO_LLVM_BIN="${LLVM_BIN:-${WASM_POSIX_LLVM_DIR:-}}" +export HOMEBREW_KANDELO_PKG_CONFIG="$(command -v pkg-config || true)" +HOST_TOOL_PATH="" +for tool in rustc cargo cbindgen cmake bison pkg-config; do + tool_path="$(command -v "$tool" || true)" + if [ -n "$tool_path" ]; then + tool_dir="$(dirname "$tool_path")" + case ":$HOST_TOOL_PATH:" in + *":$tool_dir:"*) ;; + *) HOST_TOOL_PATH="${HOST_TOOL_PATH:+$HOST_TOOL_PATH:}$tool_dir" ;; + esac + fi +done +export HOMEBREW_KANDELO_HOST_TOOL_PATH="$HOST_TOOL_PATH" + +if [ ! -d "$TAP_SOURCE/.git" ]; then + TAP_SOURCE="$WORK_DIR/tap-source" + mkdir -p "$TAP_SOURCE" + rsync -a --exclude .git "$TAP_ROOT/" "$TAP_SOURCE/" + git -C "$TAP_SOURCE" init -q + git -C "$TAP_SOURCE" config user.name "kandelo-homebrew-local" + git -C "$TAP_SOURCE" config user.email "kandelo-homebrew-local@example.invalid" + git -C "$TAP_SOURCE" add . + git -C "$TAP_SOURCE" commit -q -m "stage Kandelo Homebrew tap" +fi -"$BREW_BIN" tap "$TAP_NAME" "$TAP_ROOT" +"$BREW_BIN" tap "$TAP_NAME" "$TAP_SOURCE" +"$BREW_BIN" trust "$TAP_NAME" FORMULA_REF="$TAP_NAME/$FORMULA" TAPPED_TAP_ROOT="$("$BREW_BIN" --repository "$TAP_NAME")" TAPPED_FORMULA_PATH="$TAPPED_TAP_ROOT/Formula/$FORMULA.rb" @@ -170,8 +199,15 @@ brew_install_build_bottle() { "$BREW_BIN" bottle --json --no-rebuild --root-url "$BOTTLE_ROOT_URL" "$FORMULA_REF" ) -mapfile -t bottle_jsons < <(find "$WORK_DIR" -maxdepth 1 -type f -name '*.bottle.json' -print | sort) -mapfile -t bottle_archives < <(find "$WORK_DIR" -maxdepth 1 -type f \( -name '*.bottle.tar.gz' -o -name '*.bottle.tar.zst' \) -print | sort) +bottle_jsons=() +while IFS= read -r path; do + bottle_jsons+=("$path") +done < <(find "$WORK_DIR" -maxdepth 1 -type f -name '*.bottle.json' -print | sort) + +bottle_archives=() +while IFS= read -r path; do + bottle_archives+=("$path") +done < <(find "$WORK_DIR" -maxdepth 1 -type f \( -name '*.bottle.tar.gz' -o -name '*.bottle.tar.zst' \) -print | sort) if [ "${#bottle_jsons[@]}" -ne 1 ]; then echo "homebrew-bottle-build.sh: expected exactly one .bottle.json, found ${#bottle_jsons[@]}" >&2 @@ -187,9 +223,20 @@ cp "${bottle_archives[0]}" "$OUT_DIR/bottles/" BOTTLE_JSON="$OUT_DIR/bottles/$(basename "${bottle_jsons[0]}")" BOTTLE_ARCHIVE="$OUT_DIR/bottles/$(basename "${bottle_archives[0]}")" +REMOTE_BOTTLE_FILENAME="$( + "$BREW_BIN" ruby -rjson -e ' + json_path, bottle_tag = ARGV + data = JSON.parse(File.read(json_path)) + tag = data.values.first.fetch("bottle").fetch("tags").fetch(bottle_tag) + puts tag.fetch("filename") + ' "$BOTTLE_JSON" "$BOTTLE_TAG" +)" +if [ -n "$REMOTE_BOTTLE_FILENAME" ] && [ "$REMOTE_BOTTLE_FILENAME" != "$(basename "$BOTTLE_ARCHIVE")" ]; then + cp "$BOTTLE_ARCHIVE" "$OUT_DIR/bottles/$REMOTE_BOTTLE_FILENAME" +fi ( - cd "$TAP_ROOT" + cd "$TAPPED_TAP_ROOT" HOMEBREW_KANDELO_BOTTLE_TAG="$BOTTLE_TAG" \ KANDELO_HOMEBREW_BOTTLE_TAG="$BOTTLE_TAG" \ "$BREW_BIN" bottle --merge --write --no-commit "$BOTTLE_JSON" diff --git a/scripts/homebrew-generate-sidecars-from-env.sh b/scripts/homebrew-generate-sidecars-from-env.sh index e77db39103..0738f16528 100755 --- a/scripts/homebrew-generate-sidecars-from-env.sh +++ b/scripts/homebrew-generate-sidecars-from-env.sh @@ -58,7 +58,15 @@ CACHE_KEY_SHA="$( )" SDK_FINGERPRINT="$(shasum -a 256 "$KANDELO_ROOT/sdk/activate.sh" | awk '{print $1}')" -SYSROOT_FINGERPRINT="$(shasum -a 256 "$KANDELO_ROOT/sysroot/lib/libc.a" | awk '{print $1}')" +case "$KANDELO_HOMEBREW_ARCH" in + wasm64) SYSROOT_LIBC="$KANDELO_ROOT/sysroot64/lib/libc.a" ;; + *) SYSROOT_LIBC="$KANDELO_ROOT/sysroot/lib/libc.a" ;; +esac +if [ ! -f "$SYSROOT_LIBC" ]; then + echo "homebrew-generate-sidecars-from-env.sh: sysroot libc not found: $SYSROOT_LIBC" >&2 + exit 2 +fi +SYSROOT_FINGERPRINT="$(shasum -a 256 "$SYSROOT_LIBC" | awk '{print $1}')" BREW_VERSION="$("${HOMEBREW_BREW_FILE:-brew}" --version | head -n 1)" TAP_COMMIT="$(git -C "$KANDELO_HOMEBREW_TAP_ROOT" rev-parse HEAD)" KANDELO_COMMIT="$(git -C "$KANDELO_ROOT" rev-parse HEAD)" @@ -68,6 +76,14 @@ TAP_NAME="$(printf '%s' "$KANDELO_HOMEBREW_TAP_REPOSITORY" | tr '[:upper:]' '[:l INPUT_JSON="$KANDELO_HOMEBREW_SIDECAR_ROOT/sidecars-input.json" mkdir -p "$KANDELO_HOMEBREW_SIDECAR_ROOT" +if [ -d "$KANDELO_HOMEBREW_TAP_ROOT/Formula" ]; then + mkdir -p "$KANDELO_HOMEBREW_SIDECAR_ROOT/Formula" + rsync -a "$KANDELO_HOMEBREW_TAP_ROOT/Formula/" "$KANDELO_HOMEBREW_SIDECAR_ROOT/Formula/" +fi +if [ -d "$KANDELO_HOMEBREW_TAP_ROOT/Kandelo" ]; then + mkdir -p "$KANDELO_HOMEBREW_SIDECAR_ROOT/Kandelo" + rsync -a "$KANDELO_HOMEBREW_TAP_ROOT/Kandelo/" "$KANDELO_HOMEBREW_SIDECAR_ROOT/Kandelo/" +fi export ABI_VERSION CACHE_KEY_SHA SDK_FINGERPRINT SYSROOT_FINGERPRINT BREW_VERSION export TAP_COMMIT KANDELO_COMMIT GENERATED_AT RUN_URL TAP_NAME PACKAGE_DIR KANDELO_ROOT @@ -103,94 +119,170 @@ deps = [] for dep in package_toml.get("depends_on", []): if "@" in dep: name, version = dep.split("@", 1) - deps.append({"name": name, "version": version}) + entry = {"name": name, "version": version} else: - deps.append({"name": dep}) + name = dep + entry = {"name": dep} + dep_package_toml = pathlib.Path(os.environ["KANDELO_ROOT"]) / "packages" / "registry" / name / "package.toml" + if dep_package_toml.exists(): + with dep_package_toml.open("rb") as f: + dep_package = tomllib.load(f) + if dep_package.get("kind") == "source": + continue + deps.append(entry) version = str(bottle_formula["pkg_version"]) +package_kind = package_toml.get("kind", "program") +if package_kind not in {"library", "program"}: + raise SystemExit(f"unsupported Homebrew sidecar package kind for {formula}: {package_kind!r}") + +def package_links_and_env(): + def output_link(kind, rel): + if kind == "headers" and not rel.endswith((".h", ".hpp", ".hh", ".hxx")): + return { + "type": "symlink", + "source": rel, + "target": rel, + } + return { + "type": "file", + "source": rel, + "target": rel, + "mode": "0644", + } + + if package_kind == "library": + outputs = package_toml.get("outputs", {}) + links = [] + for key in ("headers", "libs", "pkgconfig"): + for rel in sorted(outputs.get(key, [])): + links.append(output_link(key, rel)) + if not links: + raise SystemExit(f"library formula {formula} has no declared package outputs to link") + return links, {} + + links = [] + outputs = package_toml.get("outputs", []) + if isinstance(outputs, list): + for output in outputs: + name = output.get("name", formula) + if "wasm" in output: + links.append({"type": "symlink", "source": f"bin/{name}", "target": f"bin/{name}"}) + if not links: + links.append({"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"}) + + if formula == "ncurses": + links.extend([ + {"type": "file", "source": "lib/libncursesw.a", "target": "lib/libncursesw.a", "mode": "0644"}, + {"type": "file", "source": "lib/libtinfow.a", "target": "lib/libtinfow.a", "mode": "0644"}, + {"type": "symlink", "source": "lib/libncurses.a", "target": "lib/libncurses.a"}, + {"type": "symlink", "source": "lib/libtinfo.a", "target": "lib/libtinfo.a"}, + {"type": "symlink", "source": "include/ncursesw", "target": "include/ncursesw"}, + {"type": "symlink", "source": "include/ncurses", "target": "include/ncurses"}, + ]) + + return links, {"PATH_prepend": ["bin"]} + +def package_fork_instrumentation(): + outputs = package_toml.get("outputs", []) + if isinstance(outputs, list): + for output in outputs: + if output.get("name") == formula: + return output.get("fork_instrumentation", "not-required") + return "not-required" + +def default_node_smoke_text(): + if package_kind == "library": + return ( + f"Formula test compiled a {formula} consumer against the installed keg " + "and ran the resulting Wasm through " + "node --import tsx/esm examples/run-example.ts" + ) + if formula == "bzip2": + return ( + "Formula test ran bzip2 --help through " + "node --import tsx/esm examples/run-example.ts" + ) + return ( + f"Formula test ran {formula} --version through " + "node --import tsx/esm examples/run-example.ts" + ) + +def skipped_outcome(name, reason): + return { + "name": name, + "status": "skipped", + "passed": [], + "failed": [], + "skipped": [reason], + "skip_reason": reason, + } + +def failed_outcome(name, reason): + return { + "name": name, + "status": "failed", + "passed": [], + "failed": [reason], + "skipped": [], + } + +links, link_env = package_links_and_env() browser_smoke_status = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS", "skipped") -if browser_smoke_status not in {"success", "skipped"}: +if browser_smoke_status not in {"success", "skipped", "failed"}: raise SystemExit(f"invalid KANDELO_HOMEBREW_BROWSER_SMOKE_STATUS={browser_smoke_status!r}") browser_compatible = browser_smoke_status == "success" if browser_compatible and arch != "wasm32": raise SystemExit("browser smoke can only mark wasm32 bottles browser-compatible") runtime_support = ["node", "browser"] if browser_compatible else ["node"] -browser_smoke_outcome = { - "name": "browser_smoke", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["browser_compatible is false for this bottle"], - "skip_reason": "No successful browser VFS smoke was recorded for this bottle.", -} -vfs_image_outcome = { - "name": "homebrew_vfs_image", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["precomposed browser VFS image was not built"], - "skip_reason": "Browser-compatible gallery publication requires kd-8ho.10 browser smoke.", -} -gallery_outcome = { - "name": "browser_gallery", - "status": "skipped", - "passed": [], - "failed": [], - "skipped": ["browser gallery assets were not generated"], - "skip_reason": "Gallery assets require a successful browser VFS smoke.", -} +browser_reason = os.environ.get( + "KANDELO_HOMEBREW_BROWSER_SMOKE_REASON", + f"No successful browser VFS smoke was recorded for {formula} {arch}.", +) +browser_smoke_outcome = skipped_outcome("browser_smoke", browser_reason) +if browser_smoke_status == "failed": + browser_smoke_outcome = failed_outcome("browser_smoke", browser_reason) if browser_compatible: vfs_image = os.environ.get("KANDELO_HOMEBREW_VFS_IMAGE", "") vfs_report = os.environ.get("KANDELO_HOMEBREW_VFS_REPORT", "") - gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "") browser_url = os.environ.get("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", "") browser_command = os.environ.get( "KANDELO_HOMEBREW_BROWSER_SMOKE_COMMAND", - "/home/linuxbrew/.linuxbrew/bin/hello --version", + f"/home/linuxbrew/.linuxbrew/bin/{formula} --version", ) missing = [ name for name, value in [ ("KANDELO_HOMEBREW_VFS_IMAGE", vfs_image), ("KANDELO_HOMEBREW_VFS_REPORT", vfs_report), - ("KANDELO_HOMEBREW_GALLERY_ROOT", gallery_root), ("KANDELO_HOMEBREW_BROWSER_SMOKE_URL", browser_url), ] if not value ] if missing: raise SystemExit("browser smoke success is missing env: " + ", ".join(missing)) - browser_smoke_outcome = { - "name": "browser_smoke", - "status": "success", - "passed": [ - f"Playwright chromium launched {browser_url}", - f"terminal command passed: {browser_command}", - ], - "failed": [], - "skipped": [], - } - vfs_image_outcome = { - "name": "homebrew_vfs_image", - "status": "success", - "passed": [ - f"built {vfs_image}", - f"wrote report {vfs_report}", - ], - "failed": [], - "skipped": [], - } - gallery_outcome = { - "name": "browser_gallery", - "status": "success", - "passed": [ + browser_passed = [ + f"built precomposed VFS image {vfs_image}", + f"wrote VFS report {vfs_report}", + f"Playwright chromium launched {browser_url}", + f"terminal command passed: {browser_command}", + ] + gallery_root = os.environ.get("KANDELO_HOMEBREW_GALLERY_ROOT", "") + if gallery_root: + browser_passed.extend([ f"generated {gallery_root}/gallery.json", f"generated {gallery_root}/index.toml", "scripts/validate-software-gallery.mjs accepted generated gallery assets", - ], + ]) + browser_smoke_outcome = { + "name": "browser_smoke", + "status": "success", + "passed": browser_passed, "failed": [], "skipped": [], } +node_smoke_text = os.environ.get("KANDELO_HOMEBREW_NODE_SMOKE_COMMAND", default_node_smoke_text()) + manifest = { "schema": 1, "tap_repository": os.environ["KANDELO_HOMEBREW_TAP_REPOSITORY"], @@ -219,7 +311,7 @@ manifest = { "prefix": "/home/linuxbrew/.linuxbrew", "runtime_support": runtime_support, "browser_compatible": browser_compatible, - "fork_instrumentation": "not-required", + "fork_instrumentation": package_fork_instrumentation(), "status": "success", "built_by": os.environ["RUN_URL"], "built_at": os.environ["GENERATED_AT"], @@ -227,11 +319,9 @@ manifest = { "url": os.environ["KANDELO_HOMEBREW_BOTTLE_URL"], "cache_key_sha": os.environ["CACHE_KEY_SHA"], "payload_root": f"{formula}/{version}", - "links": [ - {"type": "symlink", "source": f"bin/{formula}", "target": f"bin/{formula}"} - ], + "links": links, "receipts": [f".brew/{formula}.rb", "INSTALL_RECEIPT.json"], - "env": {"PATH_prepend": ["bin"]}, + "env": link_env, "build": { "github_run": os.environ["RUN_URL"], "job": os.environ.get("GITHUB_JOB", "local"), @@ -260,8 +350,8 @@ manifest = { "status": "skipped", "passed": [], "failed": [], - "skipped": ["brew audit was not part of kd-8ho.5 local verification"], - "skip_reason": "kd-8ho.5 validates the first bottle build and sidecars; tap audit can run in the real tap publication gate.", + "skipped": ["brew audit was not part of this local dry-run verification"], + "skip_reason": "Tap audit can run in the trusted tap publication gate.", }, { "name": "bottle_build", @@ -278,15 +368,11 @@ manifest = { { "name": "node_smoke", "status": "success", - "passed": [ - "Formula test ran hello --version through node --import tsx/esm examples/run-example.ts" - ], + "passed": [node_smoke_text], "failed": [], "skipped": [], }, - vfs_image_outcome, browser_smoke_outcome, - gallery_outcome, ], }, } diff --git a/scripts/homebrew-package-node-smoke.ts b/scripts/homebrew-package-node-smoke.ts new file mode 100644 index 0000000000..8e8f203ac4 --- /dev/null +++ b/scripts/homebrew-package-node-smoke.ts @@ -0,0 +1,1289 @@ +/** + * Node-side smoke coverage for Kandelo Homebrew package sidecars. + * + * The runner consumes generated Kandelo/Homebrew sidecars, materializes each + * requested package into a VFS, and runs a package-specific smoke through + * NodeKernelHost. Program packages execute their poured binary. SQLite + * compiles test-only consumers from the poured headers and static libraries. + */ +import { execFileSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { ABI_VERSION } from "../host/src/generated/abi"; +import { fetchHomebrewBottleBytes } from "../host/src/homebrew-vfs-fetch"; +import { buildHomebrewVfs } from "../host/src/homebrew-vfs-builder"; +import { + planHomebrewVfs, + type HomebrewBottleArch, + type HomebrewTapMetadata, + type HomebrewVfsPackagePlan, +} from "../host/src/homebrew-vfs-planner"; +import { NodeKernelHost } from "../host/src/node-kernel-host"; +import { MemoryFileSystem } from "../host/src/vfs/memory-fs"; +import { saveImage } from "../images/vfs/scripts/vfs-image-helpers"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, ".."); +const PREFIX = "/home/linuxbrew/.linuxbrew"; +const CELLAR = `${PREFIX}/Cellar`; + +type OutcomeStatus = "pass" | "fail" | "skip"; +type FormulaName = + | "sqlite" + | "bzip2" + | "xz" + | "openssl" + | "libcxx" + | "libxml2" + | "libpng" + | "libcurl" + | "ncurses" + | "spidermonkey" + | "spidermonkey-node" + | "node" + | "redis" + | "nginx" + | "mariadb"; + +interface CliOptions { + resultDir: string; + tapRoot: string; + formulas: FormulaName[]; + arch: HomebrewBottleArch; + bottleCache: string; + timeoutMs: number; + maxBytes: number; + beadId: string; +} + +interface Outcome { + name: string; + status: OutcomeStatus; + durationMs: number; + details?: string; + error?: string; +} + +interface BuiltVfs { + fs: MemoryFileSystem; + imageBytes: Uint8Array; + reportPath: string; +} + +class SkipCase extends Error { + constructor(message: string) { + super(message); + this.name = "SkipCase"; + } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)); + mkdirSync(options.resultDir, { recursive: true }); + mkdirSync(join(options.resultDir, "outcome-lists"), { recursive: true }); + mkdirSync(options.bottleCache, { recursive: true }); + + const metadataPath = join(options.tapRoot, "Kandelo", "metadata.json"); + const metadata = readJsonFile(metadataPath); + const tapCommit = gitRevParse(options.tapRoot); + const startedAt = new Date(); + const outcomes: Outcome[] = []; + + writeCurrentRun(options, { + status: "running", + startedAt, + tapCommit, + outcomes, + currentCase: "startup", + }); + + const builtByFormula = new Map(); + for (const formula of options.formulas) { + await runCase(outcomes, options, tapCommit, `homebrew_vfs_build_${formula}`, async () => { + const built = await buildFormulaVfs(metadata, formula, options); + builtByFormula.set(formula, built); + return `report=${built.reportPath}`; + }); + + await runCase(outcomes, options, tapCommit, `node_smoke_${formula}`, async () => { + const built = builtByFormula.get(formula); + if (!built) throw new SkipCase(`requires successful homebrew_vfs_build_${formula}`); + return await runFormulaSmoke(formula, built, options); + }); + } + + writeOutcomeLists(options.resultDir, outcomes); + writeSummary(options, { + startedAt, + completedAt: new Date(), + tapCommit, + outcomes, + }); + writeCurrentRun(options, { + status: outcomes.some((outcome) => outcome.status === "fail") ? "failed" : "complete", + startedAt, + tapCommit, + outcomes, + currentCase: "complete", + }); + + process.exit(outcomes.some((outcome) => outcome.status === "fail") ? 1 : 0); +} + +async function buildFormulaVfs( + metadata: HomebrewTapMetadata, + formula: FormulaName, + options: CliOptions, +): Promise { + const plan = await planHomebrewVfs(metadata, { + packages: [formula], + arch: options.arch, + runtime: "node", + expectedAbi: ABI_VERSION, + loadLinkManifest: (relPath) => readJsonFile(join(options.tapRoot, relPath)), + }); + const fs = createFs(options.maxBytes); + const result = await buildHomebrewVfs(plan, { + fs, + createdBy: "scripts/homebrew-package-node-smoke.ts", + loadBottleBytes: (pkg) => loadBottleBytes(pkg, options), + }); + + const reportPath = join(options.resultDir, `${formula}-${options.arch}-homebrew-vfs-report.json`); + writeFileSync(reportPath, `${JSON.stringify(result.report, null, 2)}\n`); + const imagePath = join(options.resultDir, `${formula}-${options.arch}-homebrew.vfs.zst`); + const imageBytes = await saveImage(fs, imagePath, { + metadata: { + version: 1, + kernelAbi: plan.kandeloAbi, + createdBy: "scripts/homebrew-package-node-smoke.ts", + homebrew: { + tapRepository: plan.tapRepository, + tapCommit: plan.tapCommit, + releaseTag: plan.releaseTag, + packages: plan.packages.map((pkg) => ({ + name: pkg.name, + version: pkg.version, + arch: pkg.arch, + sourceStatus: pkg.sourceStatus, + cacheKeySha: pkg.cacheKeySha, + })), + }, + }, + }); + return { fs, imageBytes, reportPath }; +} + +async function runFormulaSmoke( + formula: FormulaName, + built: BuiltVfs, + options: CliOptions, +): Promise { + switch (formula) { + case "sqlite": + return runSqliteSmoke(built, options); + case "bzip2": + return runProgramVersionSmoke("bzip2", `${PREFIX}/bin/bzip2`, /bzip2/i, built, options, ["--help"]); + case "xz": + return runProgramVersionSmoke("xz", `${PREFIX}/bin/xz`, /xz/i, built, options); + case "openssl": + return runOpenSslSmoke(built, options); + case "libcxx": + return runLibcxxSmoke(built, options); + case "libxml2": + return runLibxml2Smoke(built, options); + case "libpng": + return runLibpngSmoke(built, options); + case "libcurl": + return runLibcurlSmoke(built, options); + case "ncurses": + return runNcursesSmoke(built, options); + case "spidermonkey": + return runSpiderMonkeySmoke(built, options); + case "spidermonkey-node": + return runSpiderMonkeyNodeSmoke("spidermonkey-node", `${PREFIX}/bin/spidermonkey-node`, built, options); + case "node": + return runSpiderMonkeyNodeSmoke("node", `${PREFIX}/bin/node`, built, options); + case "redis": + return runRedisSmoke(built, options); + case "nginx": + return runNginxSmoke(built, options); + case "mariadb": + return runMariadbSmoke(built, options); + } +} + +async function runProgramVersionSmoke( + argv0: string, + guestPath: string, + expected: RegExp, + built: BuiltVfs, + options: CliOptions, + args: string[] = ["--version"], +): Promise { + const programBytes = readVfsFile(built.fs, guestPath); + const result = await runWasm(programBytes, [argv0, ...args], built.imageBytes, options); + if (result.exitCode !== 0) { + throw new Error(`${argv0} ${args.join(" ")} exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`); + } + const combined = `${result.stdout}\n${result.stderr}`; + if (!expected.test(combined)) { + throw new Error(`unexpected ${argv0} ${args.join(" ")} output: ${JSON.stringify(combined)}`); + } + return combined.trim().split("\n").find((line) => line.trim() !== "") ?? `${argv0} ${args.join(" ")} passed`; +} + +async function runSqliteSmoke(built: BuiltVfs, options: CliOptions): Promise { + const stage = join(options.resultDir, "sqlite-consumer-build", options.arch); + rmSync(stage, { recursive: true, force: true }); + mkdirSync(join(stage, "include"), { recursive: true }); + mkdirSync(join(stage, "lib"), { recursive: true }); + + const version = findPackageVersion(built.fs, "sqlite"); + writeFileSync( + join(stage, "include", "sqlite3.h"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3.h`), + ); + writeFileSync( + join(stage, "include", "sqlite3ext.h"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/include/sqlite3ext.h`), + ); + writeFileSync( + join(stage, "lib", "libsqlite3.a"), + readVfsFile(built.fs, `${CELLAR}/sqlite/${version}/lib/libsqlite3.a`), + ); + + const testSrc = join(repoRoot, "packages", "registry", "sqlite", "test", "sqlite_basic.c"); + const outWasm = join(stage, "sqlite_basic.wasm"); + const cc = join(repoRoot, "sdk", "bin", `${options.arch}posix-cc`); + execFileSync(cc, [ + `-I${join(stage, "include")}`, + testSrc, + join(stage, "lib", "libsqlite3.a"), + "-lm", + "-o", + outWasm, + ], { + cwd: repoRoot, + env: { + ...process.env, + PATH: `${join(repoRoot, "sdk", "bin")}:${process.env.PATH ?? ""}`, + WASM_POSIX_SYSROOT: join(repoRoot, options.arch === "wasm64" ? "sysroot64" : "sysroot"), + }, + stdio: "pipe", + }); + + const consumerBytes = new Uint8Array(readFileSync(outWasm)); + const result = await runWasm(consumerBytes, ["sqlite_basic"], built.imageBytes, options); + if (result.exitCode !== 0) { + throw new Error(`sqlite_basic exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`); + } + if (!result.stdout.includes("PASS")) { + throw new Error(`sqlite_basic did not report PASS: ${JSON.stringify(result.stdout)}`); + } + return "sqlite_basic linked against poured sqlite keg and reported PASS"; +} + +async function runOpenSslSmoke(built: BuiltVfs, options: CliOptions): Promise { + const stage = stagePackagePaths(built, options, "openssl", [ + "include/openssl", + "lib/libssl.a", + "lib/libcrypto.a", + ]); + const testSrc = join(repoRoot, "packages", "registry", "openssl", "test", "ssl_basic.c"); + const outWasm = join(stage, "ssl_basic.wasm"); + runCompiler(options, "cc", [ + `-I${join(stage, "include")}`, + testSrc, + join(stage, "lib", "libssl.a"), + join(stage, "lib", "libcrypto.a"), + "-ldl", + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["ssl_basic"], built.imageBytes, options); + assertRunPassed("ssl_basic", result, "PASS"); + return "ssl_basic linked against poured openssl keg and reported PASS"; +} + +async function runLibcxxSmoke(built: BuiltVfs, options: CliOptions): Promise { + const stage = stagePackagePaths(built, options, "libcxx", [ + "include/c++/v1", + "lib/libc++.a", + "lib/libc++abi.a", + ]); + const testSrc = join(stage, "libcxx-smoke.cpp"); + const outWasm = join(stage, "libcxx-smoke.wasm"); + writeFileSync(testSrc, `#include +#include +#include +#include +int main() { + try { + std::vector values; + values.push_back("kandelo"); + if (values.size() != 1) throw std::runtime_error("vector failed"); + throw std::runtime_error(values[0]); + } catch (const std::runtime_error& err) { + std::printf("libcxx caught %s\\n", err.what()); + return 0; + } +} +`); + + runCompiler(options, "c++", [ + "-std=c++20", + "-fexceptions", + "-fwasm-exceptions", + "-mexception-handling", + "-mllvm", + "-wasm-enable-sjlj", + "-mllvm", + "-wasm-use-legacy-eh=false", + "-nostdinc++", + "-isystem", + join(stage, "include", "c++", "v1"), + testSrc, + join(stage, "lib", "libc++.a"), + join(stage, "lib", "libc++abi.a"), + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["libcxx-smoke"], built.imageBytes, options); + assertRunPassed("libcxx-smoke", result, "libcxx caught kandelo"); + return "libcxx consumer linked against poured libcxx keg and caught an exception"; +} + +async function runLibxml2Smoke(built: BuiltVfs, options: CliOptions): Promise { + const xmlStage = stagePackagePaths(built, options, "libxml2", [ + "include/libxml", + "lib/libxml2.a", + ]); + const zlibStage = stagePackagePaths(built, options, "zlib", ["lib/libz.a"], "libxml2-zlib"); + const testSrc = join(repoRoot, "packages", "registry", "libxml2", "test", "libxml2_basic.c"); + const outWasm = join(xmlStage, "libxml2_basic.wasm"); + runCompiler(options, "cc", [ + `-I${join(xmlStage, "include")}`, + testSrc, + join(xmlStage, "lib", "libxml2.a"), + join(zlibStage, "lib", "libz.a"), + "-lm", + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["libxml2_basic"], built.imageBytes, options); + assertRunPassed("libxml2_basic", result, "PASS"); + return "libxml2_basic linked against poured libxml2 and zlib kegs and reported PASS"; +} + +async function runLibpngSmoke(built: BuiltVfs, options: CliOptions): Promise { + const pngStage = stagePackagePaths(built, options, "libpng", [ + "include/libpng16", + "lib/libpng16.a", + ]); + const zlibStage = stagePackagePaths(built, options, "zlib", ["lib/libz.a"], "libpng-zlib"); + const testSrc = join(pngStage, "libpng-smoke.c"); + const outWasm = join(pngStage, "libpng-smoke.wasm"); + writeFileSync(testSrc, `#include +#include +int main(void) { + printf("libpng %s ok\\n", png_get_libpng_ver(NULL)); + return 0; +} +`); + + runCompiler(options, "cc", [ + `-I${join(pngStage, "include", "libpng16")}`, + testSrc, + join(pngStage, "lib", "libpng16.a"), + join(zlibStage, "lib", "libz.a"), + "-lm", + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["libpng-smoke"], built.imageBytes, options); + assertRunPassed("libpng-smoke", result, "libpng"); + return "libpng consumer linked against poured libpng and zlib kegs"; +} + +async function runLibcurlSmoke(built: BuiltVfs, options: CliOptions): Promise { + const curlStage = stagePackagePaths(built, options, "libcurl", [ + "include/curl", + "lib/libcurl.a", + ]); + const opensslStage = stagePackagePaths(built, options, "openssl", [ + "include/openssl", + "lib/libssl.a", + "lib/libcrypto.a", + ], "libcurl-openssl"); + const zlibStage = stagePackagePaths(built, options, "zlib", ["lib/libz.a"], "libcurl-zlib"); + const testSrc = join(curlStage, "libcurl-smoke.c"); + const outWasm = join(curlStage, "libcurl-smoke.wasm"); + writeFileSync(testSrc, `#include +#include +int main(void) { + CURLcode rc = curl_global_init(CURL_GLOBAL_DEFAULT); + if (rc != CURLE_OK) { + printf("curl_global_init failed: %d\\n", (int)rc); + return 1; + } + printf("libcurl %s ok\\n", curl_version()); + curl_global_cleanup(); + return 0; +} +`); + + runCompiler(options, "cc", [ + `-I${join(curlStage, "include")}`, + `-I${join(opensslStage, "include")}`, + testSrc, + join(curlStage, "lib", "libcurl.a"), + join(opensslStage, "lib", "libssl.a"), + join(opensslStage, "lib", "libcrypto.a"), + join(zlibStage, "lib", "libz.a"), + "-ldl", + "-lm", + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["libcurl-smoke"], built.imageBytes, options); + assertRunPassed("libcurl-smoke", result, "libcurl"); + return "libcurl consumer linked against poured libcurl, openssl, and zlib kegs"; +} + +async function runNcursesSmoke(built: BuiltVfs, options: CliOptions): Promise { + const programBytes = readVfsFile(built.fs, `${PREFIX}/bin/tput`); + const programResult = await runWasm(programBytes, ["tput", "-V"], built.imageBytes, options); + assertRunPassed("tput -V", programResult, "ncurses"); + + const stage = stagePackagePaths(built, options, "ncurses", [ + "include/ncursesw", + "lib/libncursesw.a", + "lib/libtinfow.a", + ]); + const testSrc = join(stage, "ncurses-smoke.c"); + const outWasm = join(stage, "ncurses-smoke.wasm"); + writeFileSync(testSrc, `#include +#include +int main(void) { + printf("%s\\n", curses_version()); + return 0; +} +`); + + runCompiler(options, "cc", [ + `-I${join(stage, "include")}`, + testSrc, + join(stage, "lib", "libncursesw.a"), + join(stage, "lib", "libtinfow.a"), + "-o", + outWasm, + ]); + + const result = await runWasm(new Uint8Array(readFileSync(outWasm)), ["ncurses-smoke"], built.imageBytes, options); + assertRunPassed("ncurses-smoke", result, "ncurses"); + return "tput -V ran and an ncurses consumer linked against the poured keg"; +} + +async function runSpiderMonkeySmoke(built: BuiltVfs, options: CliOptions): Promise { + const programBytes = readVfsFile(built.fs, `${PREFIX}/bin/js`); + const result = await runWasm(programBytes, [ + "js", + "-e", + [ + "print(1 + 1)", + "print([3, 1, 2].toSorted().join(','))", + "print(typeof Intl)", + ].join(";"), + ], built.imageBytes, options); + assertRunPassed("js -e", result, "2\n1,2,3\nobject"); + return "SpiderMonkey js shell evaluated arithmetic, modern array syntax, and Intl"; +} + +async function runSpiderMonkeyNodeSmoke( + argv0: "spidermonkey-node" | "node", + guestPath: string, + built: BuiltVfs, + options: CliOptions, +): Promise { + const programBytes = readVfsFile(built.fs, guestPath); + const versionResult = await runWasm(programBytes, [argv0, "--version"], built.imageBytes, options); + assertRunPassed(`${argv0} --version`, versionResult, "v22.0.0"); + + const evalResult = await runWasm(programBytes, [ + argv0, + "-e", + [ + "const assert = require('node:assert')", + "const path = require('path')", + "const util = require('util')", + "const b = Buffer.from('hello')", + "assert.strictEqual(Buffer.isBuffer(b), true)", + "assert.strictEqual(b.toString('hex'), '68656c6c6f')", + "console.log(util.format('%s:%d:%s', path.basename('/usr/bin/node'), b.length, process.platform))", + ].join(";"), + ], built.imageBytes, options); + assertRunPassed(`${argv0} -e`, evalResult, "node:5:linux"); + return `${argv0} reported v22.0.0 and exercised process, Buffer, path, util, and assert`; +} + +async function runRedisSmoke(built: BuiltVfs, options: CliOptions): Promise { + const serverBytes = readVfsFile(built.fs, `${PREFIX}/bin/redis-server`); + const serverResult = await runWasm(serverBytes, ["redis-server", "--version"], built.imageBytes, options); + assertRunPassed("redis-server --version", serverResult, "Redis server"); + + const cliBytes = readVfsFile(built.fs, `${PREFIX}/bin/redis-cli`); + const cliResult = await runWasm(cliBytes, ["redis-cli", "--version"], built.imageBytes, options); + assertRunPassed("redis-cli --version", cliResult, "redis-cli"); + + await runRedisPingSmoke(serverBytes, cliBytes, built.imageBytes, options); + return "redis-server and redis-cli version paths ran, and redis-cli PING returned PONG through a poured Homebrew VFS"; +} + +async function runRedisPingSmoke( + serverBytes: Uint8Array, + cliBytes: Uint8Array, + rootfsImage: Uint8Array, + options: CliOptions, +): Promise { + let stdout = ""; + let stderr = ""; + const host = new NodeKernelHost({ + maxWorkers: 4, + rootfsImage, + enableTcpNetwork: true, + onStdout: (_pid, data) => { stdout += new TextDecoder().decode(data); }, + onStderr: (_pid, data) => { stderr += new TextDecoder().decode(data); }, + }); + + await host.init(); + let serverPid: number | undefined; + let serverExitStatus: number | Error | undefined; + const port = "26379"; + const env = programEnv(); + const serverExit = host.spawn(toArrayBuffer(serverBytes), [ + "redis-server", + "--save", "", + "--appendonly", "no", + "--protected-mode", "no", + "--bind", "127.0.0.1", + "--port", port, + "--dir", "/tmp", + ], { + env, + cwd: "/", + stdin: new Uint8Array(), + onStarted: (pid) => { serverPid = pid; }, + }); + serverExit.then( + (code) => { serverExitStatus = code; }, + (err) => { serverExitStatus = err instanceof Error ? err : new Error(String(err)); }, + ); + + try { + const pid = await waitForStarted(() => serverPid, "redis-server"); + const deadline = Date.now() + Math.max(options.timeoutMs, 15_000); + let lastAttempt = "redis-cli PING was not attempted"; + + while (Date.now() < deadline) { + if (serverExitStatus !== undefined) { + throw new Error(`redis-server exited before PING completed: ${serverExitStatus}; stderr=${JSON.stringify(stderr)}`); + } + + const beforeStdout = stdout.length; + const beforeStderr = stderr.length; + let cliPid: number | undefined; + const cliExit = host.spawn(toArrayBuffer(cliBytes), [ + "redis-cli", + "-h", "127.0.0.1", + "-p", port, + "PING", + ], { + env, + cwd: "/", + stdin: new Uint8Array(), + onStarted: (startedPid) => { cliPid = startedPid; }, + }); + await waitForStarted(() => cliPid, "redis-cli"); + const exitCode = await withTimeout(cliExit, 5_000, "redis-cli PING"); + const newStdout = stdout.slice(beforeStdout); + const newStderr = stderr.slice(beforeStderr); + lastAttempt = `exit=${exitCode} stdout=${JSON.stringify(newStdout)} stderr=${JSON.stringify(newStderr)}`; + if (exitCode === 0 && newStdout.includes("PONG")) return; + await sleep(250); + } + + throw new Error(`redis-cli PING did not return PONG before timeout; ${lastAttempt}; server_pid=${pid}`); + } finally { + if (serverPid !== undefined) { + await host.terminateProcess(serverPid, 0).catch(() => {}); + await Promise.race([serverExit, sleep(1_000)]).catch(() => {}); + } + await host.destroy().catch(() => {}); + } +} + +async function runNginxSmoke(built: BuiltVfs, options: CliOptions): Promise { + const programBytes = readVfsFile(built.fs, `${PREFIX}/bin/nginx`); + const versionResult = await runWasm(programBytes, ["nginx", "-v"], built.imageBytes, options); + if (versionResult.exitCode !== 0) { + throw new Error(`nginx -v exited ${versionResult.exitCode}; stderr=${JSON.stringify(versionResult.stderr)}`); + } + if (!/nginx/i.test(`${versionResult.stdout}\n${versionResult.stderr}`)) { + throw new Error(`nginx -v did not report nginx: ${JSON.stringify(versionResult.stderr || versionResult.stdout)}`); + } + await runNginxHttpSmoke(programBytes, built.imageBytes, options); + return "nginx version path ran, and nginx served a static HTTP response through a poured Homebrew VFS"; +} + +async function runNginxHttpSmoke( + programBytes: Uint8Array, + rootfsImage: Uint8Array, + options: CliOptions, +): Promise { + const stage = join(options.resultDir, "nginx-http-smoke", options.arch); + rmSync(stage, { recursive: true, force: true }); + mkdirSync(join(stage, "html"), { recursive: true }); + mkdirSync(join(stage, "etc"), { recursive: true }); + writeFileSync(join(stage, "etc", "passwd"), [ + "root:x:0:0:root:/root:/bin/sh", + "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin", + "", + ].join("\n")); + writeFileSync(join(stage, "etc", "group"), [ + "root:x:0:", + "nobody:x:65534:", + "", + ].join("\n")); + writeFileSync(join(stage, "html", "index.html"), "kandelo nginx smoke\n"); + writeFileSync(join(stage, "nginx.conf"), [ + "user nobody;", + "worker_processes 1;", + "pid /tmp/nginx-smoke/nginx.pid;", + "error_log stderr notice;", + "events { worker_connections 16; }", + "http {", + " access_log off;", + " server {", + " listen 127.0.0.1:28080;", + " server_name localhost;", + " location / { root /tmp/nginx-smoke/html; }", + " }", + "}", + "", + ].join("\n")); + + let stdout = ""; + let stderr = ""; + const host = new NodeKernelHost({ + maxWorkers: 4, + rootfsImage, + enableTcpNetwork: true, + extraMounts: [ + { mountPoint: "/tmp/nginx-smoke", hostPath: stage }, + { mountPoint: "/etc", hostPath: join(stage, "etc"), readonly: true }, + ], + onStdout: (_pid, data) => { stdout += new TextDecoder().decode(data); }, + onStderr: (_pid, data) => { stderr += new TextDecoder().decode(data); }, + }); + + await host.init(); + let serverPid: number | undefined; + let serverExitStatus: number | Error | undefined; + const serverExit = host.spawn(toArrayBuffer(programBytes), [ + "nginx", + "-p", "/tmp/nginx-smoke", + "-c", "/tmp/nginx-smoke/nginx.conf", + "-g", "daemon off; master_process off;", + ], { + env: programEnv(), + cwd: "/", + stdin: new Uint8Array(), + onStarted: (pid) => { serverPid = pid; }, + }); + serverExit.then( + (code) => { serverExitStatus = code; }, + (err) => { serverExitStatus = err instanceof Error ? err : new Error(String(err)); }, + ); + + try { + await waitForStarted(() => serverPid, "nginx"); + let lastError: unknown = null; + const deadline = Date.now() + Math.max(options.timeoutMs, 15_000); + + while (Date.now() < deadline) { + if (serverExitStatus !== undefined) { + throw new Error(`nginx exited before HTTP smoke completed: ${serverExitStatus}; stdout=${JSON.stringify(stdout)} stderr=${JSON.stringify(stderr)}`); + } + + try { + const response = await host.fetchInKernel( + 28080, + { method: "GET", url: "/", headers: { Host: "localhost" }, body: null }, + { timeoutMs: 5_000 }, + ); + const body = new TextDecoder().decode(response.body); + if (response.status !== 200 || body !== "kandelo nginx smoke\n") { + throw new Error(`unexpected nginx response status=${response.status} body=${JSON.stringify(body)}`); + } + return; + } catch (err) { + lastError = err; + await sleep(100); + } + } + + throw new Error(`nginx HTTP smoke did not complete before timeout: ${lastError}; stdout=${JSON.stringify(stdout)} stderr=${JSON.stringify(stderr)}`); + } finally { + if (serverPid !== undefined) { + await host.terminateProcess(serverPid, 0).catch(() => {}); + await Promise.race([serverExit, sleep(1_000)]).catch(() => {}); + } + await host.destroy().catch(() => {}); + } +} + +async function runMariadbSmoke(built: BuiltVfs, options: CliOptions): Promise { + const serverBytes = readVfsFile(built.fs, `${PREFIX}/bin/mariadbd`); + const serverResult = await runWasm(serverBytes, ["mariadbd", "--help", "--verbose"], built.imageBytes, options); + if (serverResult.exitCode !== 0) { + throw new Error(`mariadbd --help --verbose exited ${serverResult.exitCode}; stderr=${JSON.stringify(serverResult.stderr)}`); + } + const combined = `${serverResult.stdout}\n${serverResult.stderr}`; + if (!/MariaDB|mariadbd/i.test(combined)) { + throw new Error(`mariadbd help did not mention MariaDB: ${JSON.stringify(combined.slice(0, 4000))}`); + } + + readVfsFile(built.fs, `${PREFIX}/bin/mysqltest`); + return "mariadbd help ran through a poured Homebrew VFS and mysqltest was present"; +} + +function stagePackagePaths( + built: BuiltVfs, + options: CliOptions, + formula: string, + relPaths: string[], + label = formula, +): string { + const stage = join(options.resultDir, `${label}-consumer-build`, options.arch); + rmSync(stage, { recursive: true, force: true }); + mkdirSync(stage, { recursive: true }); + const version = findPackageVersion(built.fs, formula); + for (const rel of relPaths) { + copyVfsPath( + built.fs, + `${CELLAR}/${formula}/${version}/${rel}`, + join(stage, rel), + ); + } + return stage; +} + +function copyVfsPath(fs: MemoryFileSystem, vfsPath: string, localPath: string): void { + const st = fs.stat(vfsPath); + if ((st.mode & 0xf000) === 0x4000) { + mkdirSync(localPath, { recursive: true }); + const dh = fs.opendir(vfsPath); + try { + while (true) { + const entry = fs.readdir(dh); + if (!entry) break; + if (entry.name === "." || entry.name === "..") continue; + copyVfsPath(fs, `${vfsPath}/${entry.name}`, join(localPath, entry.name)); + } + } finally { + fs.closedir(dh); + } + return; + } + + mkdirSync(dirname(localPath), { recursive: true }); + writeFileSync(localPath, readVfsFile(fs, vfsPath)); +} + +function runCompiler(options: CliOptions, compiler: "cc" | "c++", args: string[]): void { + const command = join(repoRoot, "sdk", "bin", `${options.arch}posix-${compiler}`); + execFileSync(command, args, { + cwd: repoRoot, + env: { + ...process.env, + PATH: `${join(repoRoot, "sdk", "bin")}:${process.env.PATH ?? ""}`, + WASM_POSIX_SYSROOT: join(repoRoot, options.arch === "wasm64" ? "sysroot64" : "sysroot"), + WASM_POSIX_GLUE_DIR: join(repoRoot, "libc", "glue"), + }, + stdio: "pipe", + }); +} + +function assertRunPassed( + label: string, + result: { exitCode: number; stdout: string; stderr: string }, + expectedStdout: string, +): void { + if (result.exitCode !== 0) { + throw new Error(`${label} exited ${result.exitCode}; stderr=${JSON.stringify(result.stderr)}`); + } + if (!result.stdout.includes(expectedStdout)) { + throw new Error(`${label} did not print ${JSON.stringify(expectedStdout)}: ${JSON.stringify(result.stdout)}`); + } +} + +async function runWasm( + programBytes: Uint8Array, + argv: string[], + rootfsImage: Uint8Array, + options: CliOptions, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + let stdout = ""; + let stderr = ""; + const host = new NodeKernelHost({ + maxWorkers: 4, + rootfsImage, + onStdout: (_pid, data) => { stdout += new TextDecoder().decode(data); }, + onStderr: (_pid, data) => { stderr += new TextDecoder().decode(data); }, + }); + await host.init(); + let timeout: ReturnType | undefined; + try { + const exitPromise = host.spawn(toArrayBuffer(programBytes), argv, { + env: programEnv(), + cwd: "/", + stdin: new Uint8Array(), + }); + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(`${argv[0]} timed out after ${options.timeoutMs}ms`)), + options.timeoutMs, + ); + }); + const exitCode = await Promise.race([exitPromise, timeoutPromise]); + return { exitCode, stdout, stderr }; + } finally { + if (timeout) clearTimeout(timeout); + await host.destroy().catch(() => {}); + } +} + +function programEnv(): string[] { + return [ + "PATH=/home/linuxbrew/.linuxbrew/bin:/usr/bin:/bin", + "HOME=/tmp", + "TMPDIR=/tmp", + ]; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForStarted( + getPid: () => number | undefined, + label: string, +): Promise { + for (let i = 0; i < 200; i += 1) { + const pid = getPid(); + if (pid !== undefined) return pid; + await sleep(10); + } + throw new Error(`${label} did not start`); +} + +async function withTimeout( + promise: Promise, + ms: number, + label: string, +): Promise { + let timeout: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timeout = setTimeout( + () => reject(new Error(`${label} timed out after ${ms}ms`)), + ms, + ); + }), + ]); + } finally { + if (timeout) clearTimeout(timeout); + } +} + +async function runCase( + outcomes: Outcome[], + options: CliOptions, + tapCommit: string, + name: string, + fn: () => Promise, +): Promise { + writeCurrentRun(options, { + status: "running", + tapCommit, + outcomes, + currentCase: name, + }); + const started = Date.now(); + try { + const details = await fn(); + outcomes.push({ name, status: "pass", durationMs: Date.now() - started, details }); + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + outcomes.push({ + name, + status: error instanceof SkipCase ? "skip" : "fail", + durationMs: Date.now() - started, + details: error.message, + error: error.stack ?? error.message, + }); + } + writeOutcomeLists(options.resultDir, outcomes); +} + +async function loadBottleBytes( + pkg: HomebrewVfsPackagePlan, + options: CliOptions, +): Promise { + if (pkg.url.startsWith("file://")) { + return new Uint8Array(readFileSync(fileURLToPath(pkg.url))); + } + + const cachePath = join(options.bottleCache, `${pkg.sha256}.tar.gz`); + if (existsSync(cachePath)) return new Uint8Array(readFileSync(cachePath)); + if (!pkg.url.startsWith("https://")) { + throw new Error( + `package ${pkg.name}@${pkg.version} bottle URL must be https:// or file://, got ${pkg.url}`, + ); + } + + const bytes = await fetchHomebrewBottleBytes(pkg.url); + writeFileSync(cachePath, bytes); + return bytes; +} + +function findPackageVersion(fs: MemoryFileSystem, formula: string): string { + const info = JSON.parse(new TextDecoder().decode(readVfsFile(fs, "/etc/kandelo/homebrew-vfs.json"))); + const pkg = info.packages?.find((candidate: { name?: string }) => candidate.name === formula); + if (!pkg) throw new Error(`package ${formula} missing from /etc/kandelo/homebrew-vfs.json`); + const keg = String(pkg.keg ?? ""); + const prefix = `${CELLAR}/${formula}/`; + if (!keg.startsWith(prefix)) throw new Error(`unexpected ${formula} keg path: ${keg}`); + return keg.slice(prefix.length); +} + +function readVfsFile(fs: MemoryFileSystem, path: string): Uint8Array { + const st = fs.stat(path); + const fd = fs.open(path, 0, 0); + try { + const out = new Uint8Array(st.size); + let offset = 0; + while (offset < out.byteLength) { + const n = fs.read(fd, out.subarray(offset), null, out.byteLength - offset); + if (n <= 0) break; + offset += n; + } + return out.subarray(0, offset); + } finally { + fs.close(fd); + } +} + +function createFs(maxBytes: number): MemoryFileSystem { + const SharedArrayBufferCtor = SharedArrayBuffer as new ( + byteLength: number, + options?: { maxByteLength?: number }, + ) => SharedArrayBuffer; + return MemoryFileSystem.create( + new SharedArrayBufferCtor(maxBytes, { maxByteLength: maxBytes }), + maxBytes, + ); +} + +function parseArgs(args: string[]): CliOptions { + const defaultResultDir = join( + repoRoot, + "test-runs", + "homebrew-package-node-smoke", + new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "Z"), + ); + const options: CliOptions = { + resultDir: process.env.KANDELO_TEST_RESULT_DIR || defaultResultDir, + tapRoot: process.env.KANDELO_HOMEBREW_TAP_ROOT || "", + formulas: [], + arch: "wasm32", + bottleCache: "", + timeoutMs: 30_000, + maxBytes: 512 * 1024 * 1024, + beadId: process.env.KANDELO_BEAD_ID || "kd-1mr.2", + }; + + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + switch (arg) { + case "--result-dir": + options.resultDir = requireValue(args, ++i, arg); + break; + case "--tap-root": + options.tapRoot = requireValue(args, ++i, arg); + break; + case "--formula": + options.formulas.push(parseFormula(requireValue(args, ++i, arg))); + break; + case "--arch": + options.arch = parseArch(requireValue(args, ++i, arg)); + break; + case "--bottle-cache": + options.bottleCache = requireValue(args, ++i, arg); + break; + case "--timeout-ms": + options.timeoutMs = parsePositiveInt(requireValue(args, ++i, arg), arg); + break; + case "--max-bytes": + options.maxBytes = parseByteSize(requireValue(args, ++i, arg)); + break; + case "--bead-id": + options.beadId = requireValue(args, ++i, arg); + break; + case "--help": + case "-h": + usage(0); + break; + default: + usage(2, `unexpected argument ${arg}`); + } + } + + if (!options.tapRoot) usage(2, "--tap-root is required"); + if (options.formulas.length === 0) usage(2, "at least one --formula is required"); + options.resultDir = resolve(options.resultDir); + options.tapRoot = resolve(options.tapRoot); + options.bottleCache = options.bottleCache + ? resolve(options.bottleCache) + : join(options.resultDir, "bottle-cache"); + return options; +} + +function parseFormula(value: string): FormulaName { + if ( + value === "sqlite" || + value === "bzip2" || + value === "xz" || + value === "openssl" || + value === "libcxx" || + value === "libxml2" || + value === "libpng" || + value === "libcurl" || + value === "ncurses" || + value === "spidermonkey" || + value === "spidermonkey-node" || + value === "node" || + value === "redis" || + value === "nginx" || + value === "mariadb" + ) return value; + usage(2, `--formula must be one of sqlite, bzip2, xz, openssl, libcxx, libxml2, libpng, libcurl, ncurses, spidermonkey, spidermonkey-node, node, redis, nginx, mariadb; got ${value}`); +} + +function parseArch(value: string): HomebrewBottleArch { + if (value === "wasm32" || value === "wasm64") return value; + usage(2, `--arch must be wasm32 or wasm64, got ${value}`); +} + +function parsePositiveInt(value: string, flag: string): number { + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed <= 0) usage(2, `${flag} must be a positive integer`); + return parsed; +} + +function parseByteSize(value: string): number { + const match = /^([1-9][0-9]*)([kKmMgG]i?[bB]?|[bB])?$/.exec(value); + if (!match) usage(2, `--max-bytes must be a positive byte size, got ${value}`); + const amount = Number(match[1]); + const suffix = (match[2] ?? "b").toLowerCase(); + const multiplier = suffix.startsWith("g") ? 1024 ** 3 + : suffix.startsWith("m") ? 1024 ** 2 + : suffix.startsWith("k") ? 1024 + : 1; + return amount * multiplier; +} + +function requireValue(args: string[], index: number, flag: string): string { + const value = args[index]; + if (!value || value.startsWith("--")) usage(2, `${flag} requires a value`); + return value; +} + +function usage(code: number, message?: string): never { + if (message) console.error(`homebrew-package-node-smoke: ${message}`); + console.error(`usage: npx tsx scripts/homebrew-package-node-smoke.ts \\ + --tap-root --formula [--formula ...] \\ + [--arch ] [--result-dir ] [--bottle-cache ]`); + process.exit(code); +} + +function writeOutcomeLists(resultDir: string, outcomes: Outcome[]): void { + const listsDir = join(resultDir, "outcome-lists"); + mkdirSync(listsDir, { recursive: true }); + const passed = outcomes.filter((outcome) => outcome.status === "pass"); + const failed = outcomes.filter((outcome) => outcome.status === "fail"); + const skipped = outcomes.filter((outcome) => outcome.status === "skip"); + writeFileSync( + join(listsDir, "passed-tests.tsv"), + ["test\tduration_ms\tdetails", ...passed.map((outcome) => + `${outcome.name}\t${outcome.durationMs}\t${tsv(outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync( + join(listsDir, "failed-tests.tsv"), + ["test\tduration_ms\terror", ...failed.map((outcome) => + `${outcome.name}\t${outcome.durationMs}\t${tsv(outcome.error ?? outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync( + join(listsDir, "skipped-tests.tsv"), + ["test\treason", ...skipped.map((outcome) => + `${outcome.name}\t${tsv(outcome.details ?? "")}`, + )].join("\n") + "\n", + ); + writeFileSync(join(resultDir, "failures.json"), `${JSON.stringify(failed, null, 2)}\n`); +} + +function writeSummary( + options: CliOptions, + data: { + startedAt: Date; + completedAt: Date; + tapCommit: string; + outcomes: Outcome[]; + }, +): void { + const counts = countOutcomes(data.outcomes); + const summary = { + suite: "Homebrew package Node VFS smoke", + bead_id: options.beadId, + started_at: data.startedAt.toISOString(), + completed_at: data.completedAt.toISOString(), + duration_ms: data.completedAt.getTime() - data.startedAt.getTime(), + result_dir: options.resultDir, + tap_root: options.tapRoot, + tap_commit: data.tapCommit, + arch: options.arch, + formulas: options.formulas, + counts, + outcomes: data.outcomes, + artifacts: { + passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"), + failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"), + skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"), + failures: join(options.resultDir, "failures.json"), + current_run: join(options.resultDir, "current-run.json"), + }, + }; + writeFileSync(join(options.resultDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`); + writeFileSync(join(options.resultDir, "summary.md"), [ + "# Homebrew package Node VFS smoke", + "", + `Result dir: \`${options.resultDir}\``, + `Tap commit: \`${data.tapCommit}\``, + `Counts: ${counts.pass} pass, ${counts.fail} fail, ${counts.skip} skip`, + "", + "| Test | Status | Details |", + "|---|---:|---|", + ...data.outcomes.map((outcome) => + `| \`${outcome.name}\` | ${outcome.status} | ${outcome.details ? outcome.details.replace(/\|/g, "\\|") : ""} |`, + ), + "", + ].join("\n")); +} + +function writeCurrentRun( + options: CliOptions, + data: { + status: "running" | "complete" | "failed"; + startedAt?: Date; + tapCommit: string; + outcomes: Outcome[]; + currentCase: string; + }, +): void { + const counts = countOutcomes(data.outcomes); + const currentRun = { + suite: "homebrew-package-node-smoke", + bead_id: options.beadId, + worktree: repoRoot, + result_dir: options.resultDir, + status: data.status, + started_at: data.startedAt?.toISOString(), + updated_at: new Date().toISOString(), + current_case: data.currentCase, + progress: { + completed: data.outcomes.length, + total: options.formulas.length * 2, + pass: counts.pass, + fail: counts.fail, + skip: counts.skip, + }, + tap_root: options.tapRoot, + tap_commit: data.tapCommit, + command: { + cwd: repoRoot, + argv: process.argv, + }, + outcome_lists: { + passed: join(options.resultDir, "outcome-lists", "passed-tests.tsv"), + failed: join(options.resultDir, "outcome-lists", "failed-tests.tsv"), + skipped: join(options.resultDir, "outcome-lists", "skipped-tests.tsv"), + }, + stale_no_runner_threshold_seconds: 600, + expected_next: data.status === "running" + ? { deterministic: true, action: "continue current smoke case" } + : { deterministic: false, action: "suite terminal" }, + }; + const out = join(options.resultDir, "current-run.json"); + const tmp = `${out}.tmp`; + writeFileSync(tmp, `${JSON.stringify(currentRun, null, 2)}\n`); + renameSync(tmp, out); +} + +function countOutcomes(outcomes: Outcome[]): { pass: number; fail: number; skip: number } { + return { + pass: outcomes.filter((outcome) => outcome.status === "pass").length, + fail: outcomes.filter((outcome) => outcome.status === "fail").length, + skip: outcomes.filter((outcome) => outcome.status === "skip").length, + }; +} + +function readJsonFile(path: string): T { + return JSON.parse(readFileSync(path, "utf8")) as T; +} + +function gitRevParse(path: string): string { + return execFileSync("git", ["-C", path, "rev-parse", "HEAD"], { + encoding: "utf8", + }).trim(); +} + +function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; +} + +function tsv(value: string): string { + return value.replace(/\t/g, " ").replace(/\r?\n/g, "\\n"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tools/xtask/src/homebrew_sidecars.rs b/tools/xtask/src/homebrew_sidecars.rs index ee1f0102a1..594cca2298 100644 --- a/tools/xtask/src/homebrew_sidecars.rs +++ b/tools/xtask/src/homebrew_sidecars.rs @@ -244,6 +244,12 @@ impl Generator<'_> { summary.provenance_reports += package_output.provenance_reports; package_values.push(package_output.metadata_value); } + self.merge_previous_packages(&mut package_values); + package_values.sort_by(|a, b| { + let a_name = a.get("name").and_then(Value::as_str).unwrap_or_default(); + let b_name = b.get("name").and_then(Value::as_str).unwrap_or_default(); + a_name.cmp(b_name) + }); let metadata = json!({ "schema": 1, @@ -273,7 +279,8 @@ impl Generator<'_> { json_hashes.insert(rel, sha); } - for mut provenance in self.pending_provenance { + let pending_provenance = std::mem::take(&mut self.pending_provenance); + for mut provenance in pending_provenance { let metadata_sha = required_hash(&json_hashes, METADATA_REL)?; let formula_sha = required_hash(&json_hashes, &provenance.formula_sidecar_path)?; let link_sha = required_hash(&json_hashes, &provenance.link_manifest_path)?; @@ -306,10 +313,125 @@ impl Generator<'_> { &provenance.value, )?; } + self.refresh_provenance_hashes(&package_values, &json_hashes)?; Ok(summary) } + fn merge_previous_packages(&self, package_values: &mut Vec) { + let Some(previous) = self.previous else { + return; + }; + if previous.get("kandelo_abi").and_then(Value::as_u64) != Some(self.input.kandelo_abi) { + return; + } + + let current_names: BTreeSet = package_values + .iter() + .filter_map(|package| package.get("name").and_then(Value::as_str)) + .map(ToOwned::to_owned) + .collect(); + let Some(previous_packages) = previous.get("packages").and_then(Value::as_array) else { + return; + }; + + for package in previous_packages { + let Some(name) = package.get("name").and_then(Value::as_str) else { + continue; + }; + if !current_names.contains(name) { + package_values.push(package.clone()); + } + } + } + + fn refresh_provenance_hashes( + &self, + package_values: &[Value], + json_hashes: &BTreeMap, + ) -> Result<(), String> { + let metadata_sha = + hash_for_rel(self.options.tap_root.as_path(), json_hashes, METADATA_REL)?; + for package in package_values { + let Some(name) = package.get("name").and_then(Value::as_str) else { + continue; + }; + let Some(version) = package.get("version").and_then(Value::as_str) else { + continue; + }; + let Some(rebuild) = package.get("bottle_rebuild").and_then(Value::as_u64) else { + continue; + }; + let Some(formula_sidecar_path) = + package.get("formula_metadata").and_then(Value::as_str) + else { + continue; + }; + let formula_sha = hash_for_rel( + self.options.tap_root.as_path(), + json_hashes, + formula_sidecar_path, + )?; + let Some(bottles) = package.get("bottles").and_then(Value::as_array) else { + continue; + }; + for bottle in bottles { + if bottle.get("status").and_then(Value::as_str) != Some("success") { + continue; + } + let Some(arch) = bottle.get("arch").and_then(Value::as_str) else { + continue; + }; + let Some(link_manifest_path) = bottle.get("link_manifest").and_then(Value::as_str) + else { + continue; + }; + let link_sha = hash_for_rel( + self.options.tap_root.as_path(), + json_hashes, + link_manifest_path, + )?; + let provenance_path = format!( + "Kandelo/reports/{name}-{version}-rebuild{rebuild}-{arch}.provenance.json" + ); + let full_path = self.options.tap_root.join(&provenance_path); + if !full_path.is_file() { + return Err(format!( + "provenance report referenced by metadata does not exist: {}", + full_path.display() + )); + } + let mut provenance = load_json(&full_path)?; + provenance["metadata"] = json!({ + "metadata_json": { + "path": METADATA_REL, + "sha256": metadata_sha, + }, + "formula_json": { + "path": formula_sidecar_path, + "sha256": formula_sha, + }, + "link_manifest_json": { + "path": link_manifest_path, + "sha256": link_sha, + }, + "provenance_json": { + "path": provenance_path, + "sha256": ZERO_SHA256, + }, + }); + let normalized_sha = json_sha256(&provenance)?; + set_pointer( + &mut provenance, + "/metadata/provenance_json/sha256", + json!(normalized_sha), + )?; + write_json(&full_path, &provenance)?; + } + } + Ok(()) + } + fn generate_package( &mut self, package: &PackageInput, @@ -341,6 +463,12 @@ impl Generator<'_> { } bottle_values.push(bottle_value); } + self.merge_previous_bottles(package, &mut bottle_values); + bottle_values.sort_by(|a, b| { + let a_arch = a.get("arch").and_then(Value::as_str).unwrap_or_default(); + let b_arch = b.get("arch").and_then(Value::as_str).unwrap_or_default(); + a_arch.cmp(b_arch) + }); let dependencies = dependencies_json(&package.dependencies); let full_name = package @@ -386,6 +514,47 @@ impl Generator<'_> { }) } + fn merge_previous_bottles(&self, package: &PackageInput, bottle_values: &mut Vec) { + let Some(previous) = self.previous else { + return; + }; + if previous.get("kandelo_abi").and_then(Value::as_u64) != Some(self.input.kandelo_abi) { + return; + } + let Some(previous_packages) = previous.get("packages").and_then(Value::as_array) else { + return; + }; + let Some(previous_package) = previous_packages.iter().find(|candidate| { + candidate.get("name").and_then(Value::as_str) == Some(package.name.as_str()) + && candidate.get("version").and_then(Value::as_str) + == Some(package.version.as_str()) + && candidate.get("formula_revision").and_then(Value::as_u64) + == Some(package.formula_revision) + && candidate.get("bottle_rebuild").and_then(Value::as_u64) + == Some(package.bottle_rebuild) + }) else { + return; + }; + + let current_arches: BTreeSet = bottle_values + .iter() + .filter_map(|bottle| bottle.get("arch").and_then(Value::as_str)) + .map(ToOwned::to_owned) + .collect(); + let Some(previous_bottles) = previous_package.get("bottles").and_then(Value::as_array) + else { + return; + }; + for bottle in previous_bottles { + let Some(arch) = bottle.get("arch").and_then(Value::as_str) else { + continue; + }; + if !current_arches.contains(arch) { + bottle_values.push(bottle.clone()); + } + } + } + fn generate_bottle( &mut self, package: &PackageInput, @@ -720,6 +889,18 @@ fn load_json(path: &Path) -> Result { serde_json::from_str(&text).map_err(|e| format!("parse {}: {e}", path.display())) } +fn hash_for_rel( + tap_root: &Path, + json_hashes: &BTreeMap, + rel: &str, +) -> Result { + if let Some(hash) = json_hashes.get(rel) { + return Ok(hash.clone()); + } + require_relative_path(rel, "sidecar hash path")?; + sha256_file(&tap_root.join(rel)) +} + fn dependencies_json(dependencies: &[DependencyInput]) -> Value { let mut dependencies = dependencies.to_vec(); dependencies.sort_by(|a, b| a.name.cmp(&b.name)); @@ -994,6 +1175,20 @@ mod tests { archive.append_data(&mut header, path, bytes).unwrap(); } + fn copy_tree(from: &Path, to: &Path) { + fs::create_dir_all(to).unwrap(); + for entry in fs::read_dir(from).unwrap() { + let entry = entry.unwrap(); + let entry_path = entry.path(); + let target = to.join(entry.file_name()); + if entry_path.is_dir() { + copy_tree(&entry_path, &target); + } else { + fs::copy(&entry_path, target).unwrap(); + } + } + } + fn fixture_input(bottle_file: &str, status: &str) -> Value { let mut bottle = json!({ "arch": "wasm32", @@ -1201,4 +1396,47 @@ mod tests { ]) .unwrap(); } + + #[test] + fn success_generation_preserves_previous_arch_bottles() { + let previous = Fixture::new("success"); + previous.run(None); + + let current = Fixture::new("success"); + copy_tree( + &previous.tap_root.join("Kandelo"), + ¤t.tap_root.join("Kandelo"), + ); + + let mut input = load_json(¤t.input_path).unwrap(); + input["packages"][0]["bottles"][0]["arch"] = json!("wasm64"); + input["packages"][0]["bottles"][0]["url"] = json!( + "https://example.invalid/kandelo-homebrew/hello-2.12.1-rebuild0-wasm64_kandelo.bottle.tar.gz" + ); + write_json_value(¤t.input_path, &input); + + current.run(Some(&previous.tap_root.join("Kandelo/metadata.json"))); + + let metadata: Value = load_json(¤t.tap_root.join("Kandelo/metadata.json")).unwrap(); + let bottles = metadata["packages"][0]["bottles"].as_array().unwrap(); + let arches: Vec<_> = bottles + .iter() + .map(|bottle| bottle["arch"].as_str().unwrap()) + .collect(); + assert_eq!(arches, vec!["wasm32", "wasm64"]); + assert_eq!( + bottles[0]["link_manifest"], + json!("Kandelo/link/hello-2.12.1-rebuild0-wasm32.json") + ); + assert_eq!( + bottles[1]["link_manifest"], + json!("Kandelo/link/hello-2.12.1-rebuild0-wasm64.json") + ); + + crate::homebrew_validate::run(vec![ + "--tap-root".to_string(), + current.tap_root.to_string_lossy().into_owned(), + ]) + .unwrap(); + } }