8.1 KiB
Development Guide
Repository: sourcedock.dev/petrbalvin/gasm-sdk
Prerequisites
- Go 1.27.1, the exact version the
godirective ingo.moddeclares - just, the command runner; every task below is a just recipe
- A C compiler (
gcc):just raceruns the suite under the race detector, which needs cgo - Perl: the
test,fmt-check,install-mananduninstall-manrecipes are Perl programs gzip:install-mancompresses the man pages with it- A Linux host on amd64, arm64, riscv64 or loong64:
gasm debugneeds ptrace and the JIT checks ofgasm verifyneed executable memory golang.org/x/arch, the one module dependency, which the Go toolchain fetches; nothing else sits outside the standard library
Setup
git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git
cd gasm-sdk
just build # compile bin/gasm, zero errors and zero warnings
just gates # build, fmt-check, vet, test, race: the definition of done
Recipes
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, -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 <target> <pkg> [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 |
just gates |
build, fmt-check, vet, test and race, in that order: the definition of done |
just clean |
removes the build artefacts, bin/ and coverage.out |
just install |
builds, then copies the binary into bindir (~/.local/bin) |
just uninstall |
removes the installed binary from bindir |
just install-man |
installs the man pages under docs/man into ~/.local/share/man/man1 (MANDIR overrides), gzip-compressed; not a gate |
just uninstall-man |
removes the installed man pages |
just run |
runs the CLI with go run -buildvcs=true; the recipe takes no arguments, so flags go through the package instead |
just dev |
the same as run; the project has no watcher to add |
just gen |
regenerates the arch instruction tables from the Go toolchain source; not a gate |
just test
go test -count=1 -timeout 0 -coverprofile=coverage.out \
./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \
./lint/... ./lsp/... ./parser/... ./token/... ./verify/...
The suite runs over the logic packages (-count=1, so no cached pass
counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser,
token, verify. debug traces a live process and cmd/gasm is thin CLI
glue, so both sit outside the profile sweep, and a thin cmd/ in it
would drag the coverage total under the floor. Their tests still run, in
a second invocation without a profile:
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 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
just run
go run -buildvcs=true ./cmd/gasm lint kernel_amd64.s
go run -buildvcs=true ./cmd/gasm verify --ground-truth kernel_amd64.s
The flag on go run is there because it does not stamp the build otherwise,
which --version would then report as (devel).
just gen
Regenerates the architecture instruction tables in arch/ by parsing the Go
toolchain's own assembler source
($GOROOT/src/cmd/internal/obj/<arch>/anames.go). Requires a Go
installation. Output is committed, with no runtime dependency on the
toolchain.
Running a single test
go test -run TestVexGroundTruth ./asm/
go test -run TestGroundTruthBasic ./verify/
go test -run TestGOObjectLinkAndRun ./asm/
go test -run TestFuzzWideCopy ./verify/
Add -v for the sub-test names, and -race when the change touches
concurrency. -count=1 defeats the test cache when a result looks stale.
Coverage
just test
go tool cover -func=coverage.out
The total: line is the number that matters, and it stays at 80 percent or
more.
Debugging the build
go build -gcflags='-m' ./... # inlining decisions
go build -gcflags='-S' ./... # what the compiler generated
go tool asm -S kernel_amd64.s # how the toolchain's assembler encodes a kernel
gasm dis kernel_amd64.s # what gasm makes of the same kernel
gasm tokens kernel_amd64.s # the token stream
gasm profile kernel_amd64.s # the basic blocks of each function
gasm verify --ground-truth is the differential check that ties the two
together: it compares gasm's bytes with go tool asm's, with the relocation
sites masked, so an encoding drift shows up as a byte difference rather than a
crash later.
Continuous integration
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 |
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
(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
checkout, a pseudo-version naming the commit below one, +dirty on a
dirty tree, and (devel) outside version control. There is no
-ldflags "-X" anywhere and no version constant in the source.