Files
gasm-sdk/docs/DEVELOPMENT.md
T

174 lines
8.1 KiB
Markdown
Raw Normal View History

# Development Guide
Repository: [sourcedock.dev/petrbalvin/gasm-sdk](https://sourcedock.dev/petrbalvin/gasm-sdk)
## Prerequisites
- **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares
- **just**, the command runner; every task below is a just recipe
- **A C compiler** (`gcc`): `just race` runs the suite under the race detector,
which needs cgo
- **Perl**: the `test`, `fmt-check`, `install-man` and `uninstall-man` recipes
are Perl programs
- **`gzip`**: `install-man` compresses the man pages with it
- A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace
and the JIT checks of `gasm verify` need executable memory
- **`golang.org/x/arch`**, the one module dependency, which the Go toolchain
fetches; nothing else sits outside the standard library
## Setup
```sh
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`
```sh
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:
```sh
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`
```sh
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
```sh
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
```sh
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
```sh
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.