diff --git a/.gitea/workflows/freebsd.yml b/.gitea/workflows/freebsd.yml index 4645377..2d15865 100644 --- a/.gitea/workflows/freebsd.yml +++ b/.gitea/workflows/freebsd.yml @@ -22,7 +22,6 @@ env: jobs: build: runs-on: fedora - timeout-minutes: 10 steps: - uses: actions/checkout@v7 diff --git a/.gitea/workflows/race.yml b/.gitea/workflows/race.yml deleted file mode 100644 index b6dbaaa..0000000 --- a/.gitea/workflows/race.yml +++ /dev/null @@ -1,37 +0,0 @@ -# Race, Go. Dispatched by hand, and never a gate on a push or a tag: the release tag is -# cut only after `just gates` has already raced the tree, so this workflow is the -# explicit second opinion, not a step of the release. -# -# The race detector roughly doubles both time and memory, which the shared runner box -# cannot afford on every push. Locally it belongs to `just gates`, which runs it once per -# task; here it is a decision rather than a routine. -# -# Every step is one command, so the step that fails is the gate that failed. -name: Race - -on: - workflow_dispatch: - -env: - # One core: parallelism buys no speed here and costs memory the box does not have. - GOFLAGS: -p=1 - GOMAXPROCS: "2" - -jobs: - race: - runs-on: fedora - timeout-minutes: 20 - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - cache: true - - - name: Install gcc - # The race detector needs cgo and the runner image carries no C compiler. - run: dnf install -y gcc - - - name: Race - run: go test -race -count=1 -timeout 10m ./... diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 7a72f0b..ccb7df5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,20 +1,25 @@ # Release, Go binaries. Runs on version tags (v1.2.3) pushed to main. # -# The module sits at the repository root: the toolchain records a version only for a root -# module, measured on go1.27.1, so a build of a module in a subdirectory reports (devel) -# even at its own /vX.Y.Z tag and this workflow's smoke test can never pass for -# it. A Go repository is one module at the root. +# No gate runs at the tag: the tagged tree was tested on every push to development, +# the full suite is the suite workflow's business, and race never runs in CI at all. +# This pipeline publishes and nothing else. The version contract has no injection +# step: the toolchain records the tag into the binary's build information, so the +# build simply has to happen at the tag, which the trigger guarantees. # -# The version contract these steps implement: nothing is injected. The toolchain records -# the tag into the binary's build information, so the build simply has to happen at the -# tag, which the trigger guarantees. +# The module sits at the repository root: the toolchain records a version only for a +# root module, measured on go1.27.1, so a build of a module in a subdirectory reports +# (devel) even at its own /vX.Y.Z tag and this workflow's smoke test can never +# pass for it. A Go repository is one module at the root. # -# The gates run in their own job, once, before the matrix, minus the race detector: race -# never runs on a push path or a tag, and the local gate raced this tree before the tag -# was cut. Putting the gates inside the matrix would run the whole suite once per target -# on the box that also hosts the forge. Each job validates the tag for itself rather than -# passing a value between jobs, so no workflow feature has to be trusted for the version -# to reach the file name. +# The matrix carries the platforms the project ships: Linux on amd64, arm64, loong64 +# and riscv64. The FreeBSD port compiles in its own dispatched workflow and ships no +# binary. Nothing is installed: the fedora job image carries git, perl and node +# (verified on the runner, 2026-10-04). +# +# Each job validates the tag for itself rather than passing a value between jobs, so +# no workflow feature has to be trusted for the version to reach the file name. Every +# step is one command, and the scripted steps are Perl with builtins only: Perl drives +# curl through a list, so no argument is ever word-split, globbed or quoted wrong. name: Release on: @@ -22,111 +27,16 @@ on: tags: ["v*"] env: - # The box is shared with the forge, so parallelism is bounded on purpose. The gates job - # needs it most; the build jobs inherit it for their parallel compilation. + # The box is shared with the forge, so parallelism is bounded on purpose. GOFLAGS: -p=1 GOMAXPROCS: "2" jobs: - gates: - runs-on: fedora - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - cache: true - - - name: Install Perl - # Perl for the steps below. The install is a no-op where the package - # is already present. - run: dnf install -y perl - - - name: Validate the tag - env: - VERSION: ${{ gitea.ref_name }} - run: | - perl -e ' - my $v = $ENV{VERSION} // q{}; - $v =~ m{^v[0-9]+(\.[0-9]+){0,2}([-+].*)?$} - or die qq{ERROR: expected a semver tag like v1.2.3, got: $v\n}; - print qq{tag $v\n}; - ' - - - name: Security policy names this release - # The supported-versions table is the one part of SECURITY.md that - # carries a version, so it goes stale the moment a tag is cut. Fail - # here rather than publish a policy naming the previous release. - env: - VERSION: ${{ gitea.ref_name }} - run: | - perl -e ' - my $v = $ENV{VERSION} // q{}; - (my $nv = $v) =~ s/^v//; - open(my $f, q{<}, q{SECURITY.md}) or die qq{SECURITY.md: $!\n}; - local $/; - my $t = <$f>; - close $f; - $t =~ m{^\|\s*\Q$nv\E\s*\|\s*yes\s*\|}m - or die qq{ERROR: SECURITY.md does not name $nv as supported; update the table before releasing.\n}; - print qq{SECURITY.md names $nv\n}; - ' - - - name: Build - run: go build ./... - - - name: Format - run: | - perl -e ' - open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!}; - my @bad = <$g>; - close($g); - print @bad; - exit(@bad ? 1 : 0); - ' - - - name: Vet - run: go vet ./... - - - name: Modernise - run: go fix -diff ./... - - - name: Tests - # The same command as in test.yml, so the floor is the same number everywhere. - run: go test -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... - - - name: Tests outside the coverage set - # The same command as in test.yml: the CLI's exit codes and manual-page guard, - # and the debugger's architecture-neutral units, run outside the floor. - run: go test -count=1 -timeout 10m ./cmd/... ./debug/... - - - name: Coverage floor - run: | - perl -e ' - open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!}; - my $total; - while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} } - close($c); - die qq{no total line in coverage.out\n} unless defined $total; - printf qq{Total coverage: %s%%\n}, $total; - exit($total < 80 ? 1 : 0); - ' - build: runs-on: fedora - timeout-minutes: 25 - needs: gates strategy: fail-fast: false matrix: - # Portable targets: amd64, arm64, loong64 and riscv64 on Linux, at the toolchain - # default level. No 32-bit, no wasm, no macOS, no Windows. FreeBSD stays out until - # verify/jit.go ports off syscall.Mprotect: the Go syscall package defines no - # Mprotect for freebsd, and verify/jit.go:50 calls it to drop the write bit from - # the JIT mapping, so every freebsd target fails to build with "undefined: - # syscall.Mprotect" (verified for amd64, arm64 and riscv64 on go1.27.1). include: - goos: linux goarch: amd64 @@ -144,9 +54,6 @@ jobs: go-version-file: go.mod cache: true - - name: Install Perl - run: dnf install -y perl - - name: Validate the tag id: version env: @@ -209,7 +116,6 @@ jobs: release: runs-on: fedora - timeout-minutes: 15 needs: build permissions: # contents: read is required for the checkout: a job that declares any @@ -226,9 +132,6 @@ jobs: with: path: dist - - name: Install Perl - run: dnf install -y perl - - name: Extract the CHANGELOG section env: VERSION: ${{ gitea.ref_name }} diff --git a/.gitea/workflows/suite.yml b/.gitea/workflows/suite.yml new file mode 100644 index 0000000..9c39cff --- /dev/null +++ b/.gitea/workflows/suite.yml @@ -0,0 +1,76 @@ +# Suite, Go. Dispatched by hand, on development. Never a push gate. +# +# The complete gate set minus race: the build, both static gates, the full suite with +# every short-layer skip unskipped, and the coverage floor. It is the pipeline form of +# the local `just test`, for the moments when the tree must be proven end to end and +# nobody is at the keyboard. +# +# Race never runs in CI. It roughly doubles the time and the memory on a box shared +# with the forge, and the local `just gates` races the tree on the machine at the +# keyboard, which is where that gate belongs. +name: Suite + +on: + workflow_dispatch: + +env: + # One core: parallelism buys no speed here and costs memory the box does not have. + GOFLAGS: -p=1 + GOMAXPROCS: "2" + +jobs: + suite: + runs-on: fedora + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-go@v6 + with: + # The module is the source of truth for the version, so it cannot drift. + go-version-file: go.mod + cache: true + + - name: Build + run: go build ./... + + - name: Format + run: | + perl -e ' + open(my $g, q{-|}, q{gofmt}, q{-l}, q{.}) or die qq{gofmt: $!}; + my @bad = <$g>; + close($g); + print @bad; + exit(@bad ? 1 : 0); + ' + + - name: Vet + run: go vet ./... + + - name: Modernise + # Exits non-zero when it has something to rewrite, so it needs no output capture. + run: go fix -diff ./... + + - name: Tests + # The full suite over the logic packages (`packages` in the justfile), every + # short-layer skip lifted, with the coverage profile. No timeout: a run that + # does not finish is a defect to find. + run: go test -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... + + - name: Tests outside the coverage set + # The same second invocation as the local gate: the CLI's exit codes and + # manual-page guard, and the debugger's units, live ptrace sessions included. + # They run without a profile, because a thin main and a ptrace-bound package + # would drag the floor down rather than measure the product. + run: go test -count=1 -timeout 0 ./cmd/... ./debug/... + + - name: Coverage floor + run: | + perl -e ' + open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!}; + my $total; + while (my $l = <$c>) { $total = $1 if $l =~ m{^total:\s+\S+\s+([0-9.]+)%} } + close($c); + die qq{no total line in coverage.out\n} unless defined $total; + printf qq{Total coverage: %s%%\n}, $total; + exit($total < 80 ? 1 : 0); + ' diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 3102197..916e716 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -1,22 +1,20 @@ # Test, Go. Push and pull request to development. Never on main. # -# The gates are the ones the justfile's `gates` recipe runs, minus race: the shared -# runner box cannot afford the race detector on every push, so it lives in race.yml. -# The box is one core and 2 GB beside Gitea, so parallelism is bounded on purpose and -# everything runs in one job. Extra jobs would duplicate the checkout, the Go setup and -# the dependency download three times without buying any parallelism. +# The push path owns a two-minute budget end to end, so it carries exactly the gates +# that fit it: the format check, go vet, the suite's short layer and the coverage +# floor. There is no build step (go test compiles what it runs) and the modernisation +# gate (`go fix -diff`) belongs to the suite workflow, which is dispatched by hand. +# Nothing is installed: the fedora job image carries git, perl and node (verified on +# the runner, 2026-10-04), and no pipeline ever installs gcc or runs the race detector. # -# The budget is part of the contract: a push run is fast and light, about two minutes, -# and nothing that cannot run natively on the runner belongs here. The FreeBSD compile -# gates live in freebsd.yml behind workflow_dispatch for that reason; the GOOBJ link -# parity campaign is an opt-in local verification (just link-parity). +# The live ptrace sessions and the other deliberate-run categories skip under +# testing.Short: they need the machine to themselves and belong to the suite workflow +# and to the local `just test`, never to every push. # -# Every step is one command, so the step that fails is the gate that failed, and no shell -# option has to be trusted for the run to stop. The scripted steps are Perl, not shell and -# not Python: Perl behaves the same on both runner images, there is no bashism to trip over -# on ash, and it is one language instead of two. The Perl uses builtins only, because -# Fedora packages the Perl modules separately and nothing beyond `perl` itself may be -# assumed present. +# Every step is one command, so the step that fails is the gate that failed, and no +# shell option has to be trusted for the run to stop. The scripted steps are Perl with +# builtins only: Fedora packages the Perl modules separately, so nothing beyond `perl` +# itself may be assumed present, and there is no bashism to trip over. name: Test on: @@ -41,7 +39,6 @@ concurrency: jobs: test: runs-on: fedora - timeout-minutes: 10 steps: - uses: actions/checkout@v7 @@ -51,15 +48,6 @@ jobs: go-version-file: go.mod cache: true - # No Install Perl step: the fedora image carries perl (verified by run - # 76: the install degraded into a package upgrade costing ~50 s), and a - # dnf on the push path is network work the budget does not need. - - # The steps follow the `gates` order of the justfile contract: build, format, - # vet, test. The vet gate is go vet and go fix -diff, two steps here. - - name: Build - run: go build ./... - - name: Format run: | perl -e ' @@ -73,37 +61,19 @@ jobs: - name: Vet run: go vet ./... - - name: Modernise - # Exits non-zero when it has something to rewrite, so it needs no output capture. - run: go fix -diff ./... - - name: Tests - # The suite must be fast: a push pipeline that cannot finish in a few minutes moves - # its heavy part behind a dispatch. The inner timeout matches the job's, so a - # hanging test reports its own goroutine dump rather than a silent job kill. - # The pattern is `packages` in the project's justfile: the logic packages, since a - # thin cmd/ would drag the total under the floor. release.yml runs the same - # command, so the floor is the same number everywhere. ./verify/... carries the - # live oracle-parity comparison against `go tool asm` (the TestGroundTruth - # suites); the runner's Go setup provides both the tool and GOROOT. - # -short skips the deliberate-run categories inside the suites (the live - # ptrace sessions above all): they need the machine to themselves and a - # starved single-core runner turns each into a timeout the budget cannot - # carry. The local `just test` gate runs everything, in full. - run: go test -short -count=1 -timeout 10m -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... + # The pattern is `packages` in the project's justfile, so the floor is the + # same number the local gate reports: the logic packages, since a thin cmd/ + # and a ptrace-bound debug package would drag the total under it. No timeout + # anywhere: `-timeout 0` disables go test's own ten-minute default, because a + # run that does not finish is a defect to find and a timeout only hides it. + run: go test -short -count=1 -timeout 0 -coverprofile=coverage.out ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... - name: Tests outside the coverage set - # The CLI and the debugger sit outside `packages` because a thin main and a - # ptrace-bound package pull the total under the floor, but their tests guard - # shipped surfaces: the command exit codes, the manual pages against the - # binary's own help, and the debugger's architecture-neutral units. They run - # here so the floor stays a product measure and nothing is left untested. - # -short skips the debugger's live ptrace sessions, the deliberate-run - # category the runner cannot starve-proof. The live go-tool-asm oracle - # comparison (TestGroundTruth in ./verify/...) runs inside the coverage - # sweep above; it is not re-run as its own step, because every second on - # this box is budget. - run: go test -short -count=1 -timeout 10m ./cmd/... ./debug/... + # The CLI's exit codes and manual-page guard and the debugger's + # architecture-neutral units, still tested but outside the profile the floor + # is computed from. `-short` skips the debugger's live ptrace sessions. + run: go test -short -count=1 -timeout 0 ./cmd/... ./debug/... - name: Coverage floor run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92bc341..29badd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,12 +117,14 @@ Workflows live in `.gitea/workflows/` and run on the project's own runners: | Workflow | Trigger | What it does | |---|---|---| -| Test | push or pull request to `development` | build, format check, vet, modernisation, the test suite with the coverage floor, the CLI and debugger tests outside the profile, then the oracle-parity rerun against `go tool asm` | -| Release | a `v*` tag | the same gates as Test minus the oracle-parity step, then the matrix build, the version smoke test and the release itself; the race detector runs locally in `just gates` before the tag is cut | +| Test | push or pull request to `development` | the format check, `go vet`, and the short layer of the test suite with the coverage profile and the 80 % floor, inside the two-minute budget | +| Suite | dispatched by hand | the complete gate set minus race: the build, both static gates, the full suite and the coverage floor | +| FreeBSD build | dispatched by hand | the three FreeBSD compile gates for amd64, arm64 and riscv64 | +| Release | a `v*` tag | the matrix build, the version smoke test and the release with its assets; no gate runs at the tag | -The local equivalent is `just gates`, which is the same set plus the race detector. The -race detector also has its own workflow, dispatched by hand; it never runs on a push or a -tag, where it would double the time and the memory a shared runner cannot spare. +The local equivalent is `just gates`, which is the same set plus the race detector. Race +never runs in CI, on a push or a tag: it would double the time and the memory a shared +runner cannot spare, so the local `just gates` runs it before the tag is cut. ## Reporting bugs diff --git a/README.md b/README.md index a06faae..7628be4 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,10 @@ other. Encoding parity is the exception: the byte comparison against the toolchain runs on the host for every architecture, so no emulator stands between the claim and the evidence. The debugger is the weakest case: on the three emulated architectures its per-architecture ptrace code has -been compiled and read, never executed. Its architecture-neutral units -run under `go test ./...`, which the race workflow and a manual run -perform; the default `just test` gate does not sweep `./debug/...`. +been compiled and read, never executed. Its units run under +`go test ./...`, which `just test` sweeps in a second invocation beside +the coverage profile; the live ptrace sessions are skipped in short +mode, so they run locally or in the dispatched suite, never on a push. ## The documentation goal diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2d64bf7..02574e0 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -32,12 +32,13 @@ Every recipe in the `justfile`, and what it does. | Recipe | What it does | |---|---| | `default` (bare `just`) | prints the recipe list (`@just --list`) | -| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0` and stripped symbols; zero errors and zero warnings | -| `just test` | the test gate: the suite with `-count=1`, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile | -| `just race` | the same suite under the race detector; the expensive one, so it runs once, inside `gates` | -| `just unit [packages] [run]` | fast, cached, scoped run for iterating: no race and no coverage, so an unchanged package reports instantly | -| `just fuzz [fuzztime]` | time-boxed fuzz of one target; the package is required, because `go test -fuzz` refuses more than one | -| `just bench [packages]` | benchmarks (`-benchmem -count=5`); on an idle machine only | +| `just build` | compiles `bin/gasm` with `CGO_ENABLED=0`, `-trimpath` and `-buildvcs=true`, symbols stripped; zero errors and zero warnings | +| `just test` | the test gate: the full suite with `-count=1` and `-timeout 0` under the 4 GiB memory fence, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile | +| `just race` | the same suite under the race detector, under its own 16 GiB fence; the expensive one, so it runs once, inside `gates` | +| `just unit [packages] [run]` | fast, cached, scoped run for iterating, under the fence: no race and no coverage, so an unchanged package reports instantly | +| `just fuzz [fuzztime]` | time-boxed fuzz of one target, under the fence; the package is required, because `go test -fuzz` refuses more than one | +| `just link-parity` | the opt-in cmd/link GOOBJ parity gate; it costs minutes no pipeline can afford | +| `just bench [packages]` | benchmarks (`-benchmem -count=5`); deliberately unfenced, on an idle machine only | | `just fmt` | formats the tree in place with `gofmt` | | `just fmt-check` | zero diff; prints nothing when everything is formatted, which is the shape the CI step wants | | `just vet` | both static gates: `go vet` and `go fix -diff` | @@ -54,7 +55,7 @@ Every recipe in the `justfile`, and what it does. ### `just test` ```sh -go test -count=1 -timeout 10m -coverprofile=coverage.out \ +go test -count=1 -timeout 0 -coverprofile=coverage.out \ ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \ ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... ``` @@ -67,14 +68,18 @@ would drag the coverage total under the floor. Their tests still run, in a second invocation without a profile: ```sh -go test -count=1 -timeout 10m ./cmd/... ./debug/... +go test -count=1 -timeout 0 ./cmd/... ./debug/... ``` That covers the CLI's exit codes and the guard that compares the manual -pages with the binary's own help, and the debugger's architecture-neutral -units. The floor fails if the total is below 80 %. CI runs the same two -commands with the same ten-minute bound, so -the number is the same everywhere. +pages with the binary's own help, and the debugger's units. The floor +fails if the total is below 80 %. Both invocations run under a 4 GiB +cgroup fence with swap off, so a runaway test dies as a failed run +instead of eating the machine, and no timeout is set: `-timeout 0` +disables the ten-minute default `go test` would otherwise impose. The +push pipeline runs the same two commands with `-short`, the suite +workflow and the local gate run them in full, so the number is the same +everywhere. ### `just run` @@ -136,19 +141,30 @@ crash later. ## Continuous integration Workflows live in `.gitea/workflows/` and run on the project's own runners: -Test on a push or pull request to `development`, race dispatched by hand, and -the release on a `v*` tag. They are written by hand rather than through -`just`, but they enforce the same set of gates minus the race detector, which -the shared runner cannot afford on a push; a green `just gates` locally is -therefore the fastest way to a green pipeline. + +| Workflow | Trigger | What it does | +|---|---|---| +| Test | push or pull request to `development` | the format check, `go vet`, the short layer of the suite with the coverage profile and the 80 % floor, inside the two-minute budget | +| Suite | dispatched by hand | the complete gate set minus race: the build, the format check, `go vet`, `go fix -diff`, the full suite and the coverage floor | +| FreeBSD build | dispatched by hand | the three FreeBSD compile gates: `GOOS=freebsd` for amd64, arm64 and riscv64 | +| Release | a `v*` tag | the matrix build, the version smoke test, and the release with its assets; no gate runs at the tag | + +The pipelines are written by hand rather than through `just`, but they enforce +the same gates: the push path carries the affordable subset and the heavy tests +skip under `testing.Short`, so the dispatched suite is the moment to prove the +tree end to end. Race never runs in CI, on a push or a tag: it roughly doubles +the time and the memory on a shared runner, and the local `just gates` races +the tree on the machine at the keyboard before the tag is cut. ## Releases Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`, -which triggers the release workflow: it builds the portable Linux targets, -takes the notes from the matching `CHANGELOG.md` section and uploads the -assets. `SECURITY.md` carries the supported-versions table, so that table -moves with the release; the pipeline refuses a tag the policy does not name. +which triggers the release workflow: it builds the portable Linux targets +(amd64, arm64, loong64, riscv64), takes the notes from the matching +`CHANGELOG.md` section and uploads the assets. No gate runs at the tag, so +`just gates` must be green on the tree before the tag is cut. `SECURITY.md` +carries the supported-versions table, naming the version being released; the +release commit updates it by hand. The version is never injected. `gasm --version` prints what the toolchain recorded in the build information: the tag on a tagged diff --git a/justfile b/justfile index f703ae7..d8a2ce9 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,9 @@ -# gasm-sdk. +# gasm-sdk: Go's Plan 9 assembler and the tooling around it. # -# Replace gasm-sdk here and the values in the variable block. Everything below the block -# is the standard recipe set: the names and their meanings are fixed, identical in every -# repository, and `gates` is the definition of done. +# A binary: `just install` copies gasm into bindir. Everything below the variable block +# is the standard recipe set: the names and their meanings are fixed, and `gates` is the +# definition of done. link-parity, install-man, uninstall-man and gen are the project +# extensions. binary := "gasm" package := "./cmd/gasm" @@ -12,6 +13,16 @@ package := "./cmd/gasm" # failure, not an empty run. packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... ./lint/... ./lsp/... ./parser/... ./token/... ./verify/..." +# The memory fence for the test recipes: a cgroup ceiling with swap off, so a +# runaway run dies at the ceiling as a failed run and never eats the machine. 4G is the +# default; raise it only with a reason recorded here. +memlimit := "4G" + +# The race recipe's own fence: with the logic packages run in parallel the race +# footprint peaks over 8G (8G died in the parser package, 16G passed, measured +# 2026-10-02), and the wide machine at the keyboard can carry it. +racememlimit := "16G" + bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin") # Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because @@ -21,22 +32,23 @@ export MANDIR := env_var_or_default("MANDIR", env_var("HOME") / ".local" / "shar default: @just --list -# Compile. Zero errors, zero warnings. +# Compile. Zero errors, zero warnings; -trimpath and -buildvcs=true make the binary place-independent and version-stamped. build: - CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/{{binary}} {{package}} + CGO_ENABLED=0 go build -trimpath -buildvcs=true -ldflags "-s -w" -o bin/{{binary}} {{package}} -# The test gate: the suite, no cache, the coverage floor. +# The test gate: the full suite, no cache, the coverage floor, under the memory fence. test: #!/usr/bin/env perl - my @fence = qw{systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0}; - system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m}, + my @fence = (q{systemd-run}, q{--user}, q{--scope}, + q{-p}, q{MemoryMax={{memlimit}}}, q{-p}, q{MemorySwapMax=0}); + system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0}, q{-coverprofile}, q{coverage.out}, qw({{packages}})) == 0 or die qq{the test suite failed\n}; # The packages outside the coverage set carry tests of their own: the CLI's # exit codes and manual-page guard, and the debugger's architecture-neutral # units. They run without a profile, because a thin main and a ptrace-bound # package would drag the floor down rather than measure the product. - system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{10m}, + system(@fence, q{go}, q{test}, q{-count=1}, q{-timeout}, q{0}, q{./cmd/...}, q{./debug/...}) == 0 or die qq{the tests outside the coverage set failed\n}; open(my $c, q{-|}, q{go}, q{tool}, q{cover}, q{-func=coverage.out}) or die qq{cover: $!}; @@ -47,28 +59,25 @@ test: printf qq{Total coverage: %s%%\n}, $total; exit($total < 80 ? 1 : 0); -# The same suite under the race detector. The expensive one. +# The same suite under the race detector. The expensive one, still fenced. race: - # Ceiling 16G, not the default 4G: with every package in parallel the race - # footprint peaks over 8G (8G died in the parser package, 16G passed, - # measured 2026-10-02), and the wide machine at the keyboard can carry it. - systemd-run --user --scope -p MemoryMax=16G -p MemorySwapMax=0 go test -race -count=1 -timeout 10m {{packages}} + systemd-run --user --scope -p MemoryMax={{racememlimit}} -p MemorySwapMax=0 go test -race -count=1 -timeout 0 {{packages}} # Fast scoped run for iterating. This is the one that runs after every edit. unit pkgs=packages run=".*": - systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 go test {{pkgs}} -run '{{run}}' + systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 {{pkgs}} -run '{{run}}' # Time-boxed fuzz of one target in one package. The package is required; never a gate. fuzz target pkg fuzztime="60s": - systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 go test -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}} + systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 go test -timeout 0 -run '^$' -fuzz '{{target}}' -fuzztime={{fuzztime}} {{pkg}} -# The cmd/link GOOBJ parity gate, opt-in: the per-push pipeline cannot afford it. +# The cmd/link GOOBJ parity gate, opt-in: it costs minutes no pipeline can afford. link-parity: - systemd-run --user --scope -p MemoryMax=4G -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 10m -run 'TestGOOBJLinkRegression' ./verify/ + systemd-run --user --scope -p MemoryMax={{memlimit}} -p MemorySwapMax=0 env GASM_LINK_PARITY=1 go test -count=1 -timeout 0 -run 'TestGOOBJLinkRegression' ./verify/ -# Benchmarks. On an idle machine only. +# Benchmarks. On an idle machine only, and deliberately unfenced. bench pkgs=packages: - go test -run '^$' -bench=. -benchmem -count=5 {{pkgs}} + go test -timeout 0 -run '^$' -bench=. -benchmem -count=5 {{pkgs}} # Format in place. fmt: