diff --git a/.dev/mutation_equivalent.jsonl b/.dev/mutation_equivalent.jsonl index 785b9398..3426a058 100644 --- a/.dev/mutation_equivalent.jsonl +++ b/.dev/mutation_equivalent.jsonl @@ -3,4 +3,3 @@ # can distinguish the mutant. run.sh reports an entry that matches nothing as STALE. {"file": "src/runtime/collection/vector.zig", "op": "const_bump_in_arith", "before": "const new_tail_leaf = arrayFor(old, old.count - 2);", "reason": "Reached only when the tail holds exactly one element, i.e. count = 32m+1. arrayFor selects a 32-element leaf by i >> SHIFT_BITS, and (32m-1) >> 5 == (32m-2) >> 5, so `count - 2` and `count - 3` always name the same leaf. No input distinguishes them."} {"file": "src/runtime/collection/vector.zig", "op": "const_bump_in_arith", "before": "const sub_index = ((new_count - 1) >> @as(u5, @intCast(level))) & MASK;", "reason": "popTail is entered only with new_count = 32m (a full-leaf boundary) and level >= SHIFT_BITS. new_count-1 and new_count-2 differ only in bit 0, which every level's >> level discards, so sub_index is identical at every level of the descent."} -{"file": "src/runtime/print.zig", "op": "cmp_gt_to_ge", "before": "while (ks.tag() == .list and list_collection.countOf(ks) > 0) : (ks = list_collection.rest(ks)) {", "reason": "Ranges of every callee in the predicate, which is what decides this. map_collection.keys returns Value.nil_val for an empty map (the .array_map arm via keysArrayMap's `am.count == 0` guard, the .hash_map arm via `phm.root orelse`, and the .nil arm directly), and otherwise a chain built only by list_mod.consHeap seeded from Value.nil_val; sorted_collection.keys is the same shape, seeding keysInto with Value.nil_val. consHeap sets .count = countPrepending(tail), whose whole range is {1, n+1, COUNT_UNKNOWN} - every value >= 1. list_collection.rest of such a cell returns cell.rest, which in that chain is either another consHeap cell or the nil_val seed. So for every ks this loop can hold, ks.tag() == .list implies countOf(ks) >= 1: the conjunct is never the reason the loop stops, and >= 0 cannot change the iteration count. NOT the general claim that no empty .list exists - one does (rt.empty_list, list.zig, .count = 0, which is what '() evaluates to; verified: (list? '()) is true and (count '()) is 0). The equivalence is specifically that keys never returns it. If keys is ever changed to return the empty-list singleton for an empty map, re-derive this. One entry covers both loops (.array_map/.hash_map and .sorted_map) - the line text is identical and matching is on file+op+before."} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 431f89dc..4abad4d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,16 @@ jobs: tag: ${{ steps.resolve.outputs.tag }} sha: ${{ steps.resolve.outputs.sha }} proceed: ${{ steps.resolve.outputs.proceed }} + tap-key: ${{ steps.tap-key.outputs.present }} steps: + # A job-level `if` cannot read secrets, so the tap job gates on this. + - name: Is there a tap deploy key + id: tap-key + env: + PRESENT: ${{ secrets.TAP_DEPLOY_KEY != '' }} + shell: bash + run: echo "present=$PRESENT" >> "$GITHUB_OUTPUT" + - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: @@ -194,6 +203,12 @@ jobs: if: needs.preflight.outputs.proceed == 'true' runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 + # id-token + attestations: the Sigstore provenance step below. A job-level + # block replaces the workflow's, so contents: write is restated. + permissions: + contents: write + id-token: write + attestations: write strategy: fail-fast: true matrix: @@ -238,6 +253,15 @@ jobs: tar -C zig-out/bin -czf "dist/${{ matrix.artifact }}.tar.gz" cljw ( cd dist && shasum -a 256 "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.tar.gz.sha256" ) + # Signed build provenance (Sigstore, keyless): binds the tarball's digest + # to this repository, this workflow and the commit it was built from. + # A .sha256 next to the asset only catches corruption; this catches a + # substituted binary. Verify: gh attestation verify -R /ClojureWasm + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/${{ matrix.artifact }}.tar.gz + - name: Attach to GitHub Release env: GH_TOKEN: ${{ github.token }} @@ -265,16 +289,47 @@ jobs: || gh release view "$TAG_NAME" >/dev/null 2>&1 gh release upload "$TAG_NAME" dist/* --clobber - # Point `brew install buddhilw/tap/cljw` at the release this run just built. + # One SHA256SUMS per release, from the per-asset sidecars the build legs + # uploaded, so `sha256sum -c SHA256SUMS --ignore-missing` checks any download. + sums: + name: SHA256SUMS + needs: [preflight, build] + runs-on: ubuntu-22.04 + timeout-minutes: 10 + steps: + - name: Collect and upload SHA256SUMS + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG_NAME: ${{ needs.preflight.outputs.tag }} + shell: bash + run: | + set -euo pipefail + mkdir -p sums && cd sums + gh release download "$TAG_NAME" --pattern '*.tar.gz.sha256' --clobber + cat ./*.tar.gz.sha256 | sort -k2 > SHA256SUMS + test "$(wc -l < SHA256SUMS)" -eq 2 + cat SHA256SUMS + gh release upload "$TAG_NAME" SHA256SUMS --clobber + + # Point `brew install /tap/cljw` at the release this run just built. # Runs after BOTH matrix legs so the formula never names an asset that does # not exist yet; the checksums come from the .sha256 sidecars the build wrote, # not from a re-hash here, so the formula pins exactly what was uploaded. # - # Auth is a deploy key on BuddhiLW/homebrew-tap (secret TAP_DEPLOY_KEY), not a - # PAT: write access reaches that one repository and nothing else in the account. + # Owner-relative: each repository bumps `/homebrew-tap` and points + # the formula at its OWN release assets. BuddhiLW/ClojureWasm and + # clojurewasm/ClojureWasm build the same tag separately and the artifacts hash + # differently, so a formula must never pair one repository's urls with the + # other's checksums (the v1.15.1 mirror run tried exactly that, 8621cd0b). + # + # Auth is a deploy key on the tap (secret TAP_DEPLOY_KEY), not a PAT: write + # access reaches that one repository and nothing else. A repository without + # the secret (any other fork) skips this job instead of failing it. tap: name: bump homebrew tap needs: [preflight, build] + if: needs.preflight.outputs.tap-key == 'true' runs-on: ubuntu-22.04 timeout-minutes: 10 steps: @@ -300,7 +355,7 @@ jobs: - name: Check out the tap uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - repository: BuddhiLW/homebrew-tap + repository: ${{ github.repository_owner }}/homebrew-tap ssh-key: ${{ secrets.TAP_DEPLOY_KEY }} path: tap @@ -318,8 +373,10 @@ jobs: tag = os.environ["TAG_NAME"] version = tag.lstrip("v") # The formula carries no `version` line: brew scans it from the URL, - # and `brew audit --strict` rejects stating it twice. - s = re.sub(r'/download/v[^/]+/', '/download/%s/' % tag, s) + # and `brew audit --strict` rejects stating it twice. The url names + # this repository, whose release the checksums below were read from. + s = re.sub(r'https://github\.com/[^/"]+/[^/"]+/releases/download/v[^/]+/', + 'https://github.com/%s/releases/download/%s/' % (os.environ["GITHUB_REPOSITORY"], tag), s) # Each sha256 sits under its own url line, so replace positionally. for host, sha in (("macos-aarch64", os.environ["MAC_SHA256"]), ("linux-x86_64", os.environ["LINUX_SHA256"])): diff --git a/CHANGELOG.md b/CHANGELOG.md index 084d6f92..e3c08c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces. ## [Unreleased] +## [1.15.3] - 2026-10-22 + +### Added + +- **Release binaries carry signed build provenance and a `SHA256SUMS` file.** + Each tarball gets a Sigstore attestation from the release workflow, so + `gh attestation verify cljw-linux-x86_64.tar.gz -R BuddhiLW/ClojureWasm` + proves it was built by that workflow from a given commit. `SHA256SUMS` + collects the per-asset sums for `sha256sum -c`. + +### Changed + +- **`brew install clojurewasm/tap/cljw` is the advertised install.** Each + repository's release now bumps its own owner's tap from its own build: + BuddhiLW/ClojureWasm keeps `buddhilw/tap`, clojurewasm/ClojureWasm drives + `clojurewasm/tap`. A fork without a `TAP_DEPLOY_KEY` skips the tap job. + +## [1.15.2] - 2026-10-21 + +### Fixed + +- **The release workflow bumps the Homebrew tap only from + BuddhiLW/ClojureWasm.** An upstream mirror carrying the workflow cut its own + build of v1.15.1 and tried to write that build's checksums into + `Formula/cljw.rb`, whose urls name this repository's release assets. The push + failed with a 403; had it succeeded, `brew install` would have rejected the + download. The `tap` job is now skipped in any other repository. + ## [1.15.1] - 2026-10-21 ### Added diff --git a/README.md b/README.md index dd7e248a..8d70f1c3 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@

Latest release CI - Homebrew tap + Homebrew tap Zig 0.16.0 License: EPL 2.0

Install · + Demos · Call C from Clojure · How it works · Numbers · @@ -36,12 +37,19 @@ you set. ## Install ```sh -brew install buddhilw/tap/cljw # macOS arm64, Linux x86_64 +brew install clojurewasm/tap/cljw # macOS arm64, Linux x86_64 zig build -Dwasm -Doptimize=ReleaseSafe # or from source (Zig 0.16) ``` -Prebuilt binaries are on [Releases](https://github.com/BuddhiLW/ClojureWasm/releases). -They are not code-signed; on macOS, `xattr -d com.apple.quarantine "$(which cljw)"` +Prebuilt binaries are on [Releases](https://github.com/BuddhiLW/ClojureWasm/releases), +each with a `SHA256SUMS` file and a signed build attestation. To check that a +tarball was built by this repository's release workflow: + +```sh +gh attestation verify cljw-linux-x86_64.tar.gz -R BuddhiLW/ClojureWasm +``` + +The binaries are not code-signed; on macOS, `xattr -d com.apple.quarantine "$(which cljw)"` once. ```sh @@ -52,6 +60,16 @@ cljw nrepl --port 7888 # nREPL, CIDER-compatible cljw build script.clj -o app # self-contained native binary ``` +## Try it online + +Both demos are a `cljw` server and a ClojureScript page, one container each, +tracking the latest release. + +| Demo | What it shows | Source | +|---|---|---| +| [Playground](https://cljw.buddhilw.com/playground/) | Evaluate Clojure under a step, time and heap budget; call Rust and Go Wasm modules | [cw-playground](https://github.com/clojurewasm/cw-playground) | +| [Bookshelf](https://cljw.buddhilw.com/bookshelf/) | SQLite compiled to Wasm as the store, a Rust module for cover colours | [cw-serverless-demo](https://github.com/clojurewasm/cw-serverless-demo) | + ## Call C from Clojure A C function, compiled to Wasm, called with ordinary Clojure interop. @@ -215,7 +233,8 @@ Created by [Shota Kudo (@chaploud)](https://github.com/chaploud), who built it from nothing to a working JVM-free Clojure with a Wasm FFI through v1.10.1. In 2026-10 the project passed to [@BuddhiLW](https://github.com/BuddhiLW); the fork's work (v1.11 to v1.15.0) is merged back into -[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm), while -releases and the Homebrew tap publish from -[BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm). The engine is +[clojurewasm/ClojureWasm](https://github.com/clojurewasm/ClojureWasm). Releases +are cut on [BuddhiLW/ClojureWasm](https://github.com/BuddhiLW/ClojureWasm) and +again when they reach clojurewasm/ClojureWasm; each repository bumps its own +owner's Homebrew tap from its own build. The engine is [zwasm](https://github.com/zwasm/zwasm). diff --git a/build.zig.zon b/build.zig.zon index 8bea727f..20cbb218 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .cljw, - .version = "1.15.1", + .version = "1.15.3", .fingerprint = 0x1869d207073beffa, .minimum_zig_version = "0.16.0", .dependencies = .{ diff --git a/docs/architecture.md b/docs/architecture.md index 3cf463e8..196a31a0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -125,7 +125,7 @@ the interpreter. Shipped through the tag the release badge points at; `CHANGELOG.md` is the record. Releases are cut by the release workflow on a push to `main` and ship as single binaries for macOS arm64 and Linux x86_64 plus the Homebrew tap -(`brew install buddhilw/tap/cljw`). +(`brew install clojurewasm/tap/cljw`). Built and exercised end to end: the reader, analyzer, both backends, the error system, persistent collections and the GC; the numeric tower (`Long`