# sr-ht-ecore — build scaffolding (bench.sr.ht / cov.sr.ht family style).
#
# This repository is a LIBRARY: no cmd/, no daemon, no stylesheet, no apk. There
# is nothing to install and nothing to package, so the targets a service's
# Makefile carries for those — css, keygen, install, check-version — have no
# meaning here and are absent rather than stubbed.
#
# What is left is what the seven services pinning this module actually need from
# it: that it compiles, that it is formatted and vets clean, that its tests pass,
# and that its coverage profile and its benchmarks come out in the shape
# .build.yml uploads to cov.srht.bigb.es and bench.srht.bigb.es. That is what the
# targets below are, and .build.yml calls them by name — the Go command lines
# live here, once, instead of being copied into the manifest.
GO?=go
GOFMT?=gofmt
# PKG is what to run and COUNT is `go test -count`, both variables so that
# `make test-race COUNT=10 PKG=./bearer/` needs no retyping of the flags that
# get forgotten when a command is retyped by hand.
PKG?=./...
COUNT?=1
# TEST_TIMEOUT is the per-package ceiling handed to `go test`, and -timeout is
# per test binary, so what it has to cover is the slowest package and not the
# suite. `make test` does not need it: 11 s wall for the whole tree on an M4
# Pro, measured with go1.25 at -count=1, no package over 10 s. `make bench`
# is where it earns its keep — measured on that same machine at BENCH_COUNT=10,
# 6 min 06 s wall over 29 benchmark cases, of which grants took 96.4 s, bearer
# 78.5 s and csrf 73.0 s. A sixth of the toolchain's ten-minute default is
# comfortable on a laptop and not obviously comfortable on a builds.sr.ht VM,
# which is a slower machine running the same fixed number of iterations.
# Twenty minutes is the number the sibling services carry, for the same reason:
# what it prevents — `panic: test timed out` over three hundred lines of
# goroutine dump — reads like a hang in the code rather than like a budget.
TEST_TIMEOUT?=20m
# BENCH_COUNT is `go test -count` for the `bench` target. Ten is bench.sr.ht's
# own number (its SPEC ch. 5): a confidence interval becomes finite at six
# repetitions and a comparison becomes significant at four, so anything under
# six uploads points the service can only mark "low n". It is a variable so a
# laptop can say `make bench BENCH_COUNT=1` when it only wants to know that the
# benchmarks still run.
BENCH_COUNT?=10
# Where `make cover` writes the profile. .build.yml overrides it to $HOME —
# that is where builds.sr.ht's artifacts: looks — and the default is the
# checkout, which is what a developer means by `make cover`. It lands untracked
# there; `make clean` is what removes it, and .gitignore does not list it.
COVERPROFILE?=cover.out
# The sibling services default to `build`, and each of them says in the same
# breath that it was `help` while there was nothing to build. Here there is
# nothing to build — `build` is a compile check, not a product — so the default
# stays where theirs started, and a bare `make` spends no time proving something
# the author did not ask about.
.DEFAULT_GOAL := help
help:
@echo "targets:"
@echo " help this list (the default target)"
@echo " build compile every package: go build \$$(PKG)"
@echo " test go test \$$(PKG) with an explicit -timeout"
@echo " test-race the same under -race and repeatable:"
@echo " make test-race COUNT=10 PKG=./bearer/"
@echo " cover test with -covermode=atomic into \$$(COVERPROFILE) and"
@echo " print the total; the profile .build.yml POSTs to cov.sr.ht"
@echo " (COVERPROFILE=$(COVERPROFILE))"
@echo " bench go test -bench=. \$$(PKG) as benchfmt on stdout, the format"
@echo " .build.yml POSTs to bench.sr.ht (BENCH_COUNT=$(BENCH_COUNT));"
@echo " everything that is not benchfmt is filtered out — see"
@echo " BENCH_FILTER, and 'make bench BENCH_FILTER=.' for the raw run"
@echo " vet go vet \$$(PKG)"
@echo " fmt rewrite this module's Go files with gofmt"
@echo " check-fmt the same as a refusal: fail if gofmt would rewrite anything"
@echo " (this is the one CI runs; fmt writes, gates must not)"
@echo " clean remove \$$(COVERPROFILE)"
# There is no binary to link, so this is a compile check and nothing else. It is
# still worth a target: `go vet` compiles too, but a build failure reported by
# vet reads like a vet finding.
build:
$(GO) build $(PKG)
test:
$(GO) test -timeout $(TEST_TIMEOUT) $(PKG)
# The same suite under the race detector, repeatable. Half of what this library
# holds is middleware sitting on every request of every service — chimw's
# logger, middleware's chain, mcphttp's cache — so the run that matters for it
# is this one, not `test`.
test-race:
$(GO) test -race -count=$(COUNT) -timeout $(TEST_TIMEOUT) $(PKG)
# -covermode=atomic and not the default `set`: `set` records whether a statement
# ran, atomic records how often, and cov.sr.ht reads hit counts. The profile is
# the artifact; the printed total is a convenience.
#
# The `test -s` in the middle is not decoration. A profile that came out empty
# is still a valid file, and uploading it would report success over a report
# that covers nothing — the same failure the .build.yml bench task greps
# against.
cover:
$(GO) test -covermode=atomic -coverprofile=$(COVERPROFILE) -timeout $(TEST_TIMEOUT) $(PKG)
@test -s $(COVERPROFILE) || { echo "$(COVERPROFILE) is empty" >&2; exit 1; }
$(GO) tool cover -func=$(COVERPROFILE) | tail -1
# The benchmarks in the form bench.sr.ht ingests: benchfmt on stdout, which is
# what `go test -bench` writes. -run='^$$' keeps the tests out of it — their
# output is noise the parser skips and their runtime is time added to a run
# whose point is the benchmarks. -benchmem because B/op and allocs/op are half
# of what a middleware benchmark means and they cost nothing to collect.
#
# BENCH_FILTER is the part that is not boilerplate, and it is here because a
# benchmark's own stdout is not only the benchmark. `go test` merges the test
# binary's stderr into its stdout, so anything the code under test logs during a
# measured loop lands in the middle of the benchfmt document. Measured on this
# tree at BENCH_COUNT=10: bearer's invalid-token case makes sr-ht-core's
# auth.DecodeBearerToken call log.Printf once per iteration, and an unfiltered
# run was 901 MB and 9.5 million lines deep — around the 290 result lines a full
# run has — when it was stopped, a third of the way through. Filtered, the same
# run is 340 lines and 31 KB. benchfmt ignores lines it cannot parse, so the
# unfiltered file would upload "successfully": a POST of a gigabyte of noise,
# whatever bench.sr.ht's byte budget makes of it.
#
# So the target emits the benchfmt grammar (its configuration lines and its
# result lines) plus the words that say a run failed, and nothing else. The
# filter is a variable: `make bench BENCH_FILTER=.` is the same run with every
# line of it. It is a defence and not a cure — the cure is for a benchmark that
# provokes logging to silence the logger first, the way middleware's discardLog
# and chimw's io.Discard handler already do.
#
# The exit status travels in a file because the grep is a pipe: taking the
# pipeline's status would report grep's opinion of the output instead of whether
# the benchmarks ran, which is the failure mode that makes a red suite read
# green.
BENCH_FILTER?=^(goos|goarch|pkg|cpu): |^Benchmark|^(PASS|FAIL|ok|--- |panic: )
bench:
@echo "+ $(GO) test -run='^$$' -bench=. -benchmem -count=$(BENCH_COUNT) -timeout $(TEST_TIMEOUT) $(PKG)" >&2
@st=$$(mktemp); \
{ $(GO) test -run='^$$' -bench=. -benchmem -count=$(BENCH_COUNT) \
-timeout $(TEST_TIMEOUT) $(PKG) 2>&1; echo $$? > "$$st"; } \
| grep -E '$(BENCH_FILTER)' || true; \
rc=$$(cat "$$st"); rm -f "$$st"; \
test -n "$$rc" || rc=1; \
exit "$$rc"
vet:
$(GO) vet $(PKG)
# GOFMT_DIRS is `go list`'s answer and not a literal `.`, and that is the whole
# point of the two targets below. The repository convention here is to create
# git worktrees under .worktrees/<branch>, so a plain `gofmt -l .` at the root
# would walk into another checkout of this same module and report — or, for
# `fmt`, rewrite — files that are not in this working tree at all. A worktree
# carries its own go.mod, so it is a different module to `go list`, and ./...
# never leaves this one.
GOFMT_DIRS=$$($(GO) list -f '{{.Dir}}' $(PKG))
fmt:
$(GOFMT) -l -w $(GOFMT_DIRS)
# `gofmt -l` prints the offending files and STILL EXITS 0, so it passes any &&
# chain and any CI task that only looks at the status. This target is that check
# written so a failure stops the build, and it is the one .build.yml calls; fmt
# above rewrites and must never be what a gate runs.
check-fmt:
@out=$$($(GOFMT) -l $(GOFMT_DIRS)); \
if [ -n "$$out" ]; then \
echo "gofmt would rewrite:" >&2; \
echo "$$out" >&2; \
exit 1; \
fi; \
echo "gofmt: clean"
# Only the profile: `bench` writes to stdout and creates no file of its own, so
# there is nothing else here to remove.
clean:
rm -f $(COVERPROFILE)
.PHONY: help build test test-race cover bench vet fmt check-fmt clean