Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dev/mutation_equivalent.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -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."}
69 changes: 63 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 <tarball> -R <owner>/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 }}
Expand Down Expand Up @@ -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 <owner>/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 `<its owner>/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:
Expand All @@ -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

Expand All @@ -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"])):
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
<p align="center">
<a href="https://github.com/BuddhiLW/ClojureWasm/releases/latest"><img src="https://img.shields.io/github/v/release/BuddhiLW/ClojureWasm?sort=semver&display_name=tag&label=release&color=success" alt="Latest release" /></a>
<a href="https://github.com/BuddhiLW/ClojureWasm/actions/workflows/ci.yml"><img src="https://github.com/BuddhiLW/ClojureWasm/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
<a href="https://github.com/BuddhiLW/homebrew-tap"><img src="https://img.shields.io/badge/brew-buddhilw%2Ftap%2Fcljw-F9A03C?logo=homebrew&logoColor=white" alt="Homebrew tap" /></a>
<a href="https://github.com/clojurewasm/homebrew-tap"><img src="https://img.shields.io/badge/brew-clojurewasm%2Ftap%2Fcljw-F9A03C?logo=homebrew&logoColor=white" alt="Homebrew tap" /></a>
<a href="https://ziglang.org/"><img src="https://img.shields.io/badge/Zig-0.16.0-f7a41d?logo=zig&logoColor=white" alt="Zig 0.16.0" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-EPL_2.0-blue.svg" alt="License: EPL 2.0" /></a>
</p>

<p align="center">
<a href="#install">Install</a> ·
<a href="#try-it-online">Demos</a> ·
<a href="#call-c-from-clojure">Call C from Clojure</a> ·
<a href="#how-it-works">How it works</a> ·
<a href="#numbers">Numbers</a> ·
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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).
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .cljw,
.version = "1.15.1",
.version = "1.15.3",
.fingerprint = 0x1869d207073beffa,
.minimum_zig_version = "0.16.0",
.dependencies = .{
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Loading