# diff.sr.ht — build scaffolding (git.sr.ht / sourcehut-dolt style). # # Go binary + Go-template SSR live under cmd/ and web/. The CSS is built from # the shared sourcehut scss partials with sassc; the frontend diff bundle is # built once with esbuild and vendored into web/static/. Node is a build-time # dependency only. SERVICE=diff.sr.ht BIN=comparesrht PREFIX?=/usr/local BINDIR?=$(PREFIX)/bin SHAREDIR?=$(PREFIX)/share ASSETS?=/usr/share/sourcehut STATICDIR?=$(ASSETS)/$(SERVICE)/static SASSC?=sassc SASSC_INCLUDE=-I$(ASSETS)/scss MINIFY?=minify # The glob web/templates.go resolves at startup (cssGlob). Named once so that # `css`, which removes the previous build's file, and `check-css`, which counts # what is left, cannot drift apart. CSS=web/static/main.min.*.css # TESTFLAGS is the hole `cover` reaches through, so that the suite has exactly # one spelling: `cover` is `test` with the profile flags in it and nothing else # changed. Do not add a second `go test ./...` line to this file. TESTFLAGS?= # Where `cover` writes the profile. CI passes an absolute path OUTSIDE the # checkout ($HOME/cover.out): a profile written into the working tree would be # an untracked file, and Go reads vcs.modified from `git status --porcelain`, # which counts those — the packaged binary would stamp itself "+dirty". # The default is gitignored for the same reason. COVERPROFILE?=cover.out # BENCH_COUNT is `go test -count` for `bench`, and ten is the family's number: # bench.sr.ht's confidence interval for a point becomes finite at six # repetitions and a comparison becomes significant at four, so a -count under # six uploads points the service can only mark "low n". A laptop that only wants # to know the benchmarks still run says `make bench BENCH_COUNT=1`. BENCH_COUNT?=10 # `bench` names its own timeout because it is the one target that can outrun # go test's 10m default: ten counts of every benchmark, each of which builds a # real git repository first, on a builds.sr.ht VM that is some multiple slower # than a laptop. The failure would be `panic: test timed out` over a goroutine # dump, which reads like a hang in the code rather than an unlucky number here. BENCH_TIMEOUT?=20m all: build # Compile the service into ./comparesrht. build: go build -o $(BIN) ./cmd/$(BIN) test: go test $(TESTFLAGS) ./... # The same suite, plus the coverage profile .build.yml uploads to cov.sr.ht. # # It goes through `test` rather than repeating the command, so that CI's run and # a developer's `make test` cannot become two different suites — the only # difference between them is the two flags below. -covermode=atomic because the # service is a concurrent HTTP server and the default `set` mode records "this # statement ran" rather than how often; cov.sr.ht stores the counts. cover: @$(MAKE) test TESTFLAGS="-covermode=atomic -coverprofile=$(COVERPROFILE)" go tool cover -func=$(COVERPROFILE) | tail -1 # The benchmarks of the diffing path, as benchfmt on stdout — the format # .build.yml uploads to bench.srht.bigb.es. -run='^$$' because this is a run # whose point is the benchmarks and the suite has already run in `test`; # -benchmem because B/op and allocs/op are half of what a diff benchmark means # and they cost nothing to collect. bench: go test -run='^$$' -bench=. -benchmem -count=$(BENCH_COUNT) -timeout $(BENCH_TIMEOUT) ./... # CSS pipeline: sassc -> minify -> content-hashed filename. The running # service globs web/static/main.min.*.css at startup, so the hash in the name # is the cache-busting version. Requires the shared scss partials to be # installed at $(ASSETS)/scss (core.sr.ht `make install`, which installs both # base.scss and bootstrap/scss there) and scss/main.scss to exist (phase 2b). # Produces exactly ONE web/static/main.min..css (old ones and the # intermediate main.css are removed). # # The three preflight checks are here because each of their failures is # otherwise illegible. A missing minifier stops the pipeline after sassc has # already written main.css, and the shell's "command not found" names a binary # rather than a package; a missing $(ASSETS)/scss/base.scss is the family's # standing rake — NO PACKAGE MANAGER SHIPS THAT PARTIAL, it is materialized by # core.sr.ht's own `make install` locally and by the `scss` task of .build.yml # in CI, and sassc's report of it is one line about an import. css: @command -v $(SASSC) >/dev/null 2>&1 || { \ echo "error: $(SASSC) not found — install sassc (apk add sassc, brew install sassc)"; \ exit 1; } @command -v $(MINIFY) >/dev/null 2>&1 || { \ echo "error: $(MINIFY) not found — install tdewolff/minify (apk add minify," \ "go install github.com/tdewolff/minify/v2/cmd/minify@latest)"; \ exit 1; } @[ -f $(ASSETS)/scss/base.scss ] || { \ echo "error: no $(ASSETS)/scss/base.scss — the shared partial is not packaged;" \ "materialize it with core.sr.ht's 'make install' (or the scss task of .build.yml)"; \ exit 1; } mkdir -p web/static rm -f web/static/main.css $(CSS) $(SASSC) $(SASSC_INCLUDE) scss/main.scss web/static/main.css $(MINIFY) -o web/static/main.min.css web/static/main.css mv web/static/main.min.css \ web/static/main.min.$$(sha256sum web/static/main.min.css | cut -c1-8).css rm -f web/static/main.css # check-css is the packaging gate: no stylesheet, no release. It exists because # the failure it catches is invisible at build time — `go build` succeeds # perfectly well with an unstyled web/static (//go:embed takes the directory, # not the file), and the first sign of trouble is an unstyled page in # production. # # It answers about the DISK, and that is the limit of it: nothing here says the # file it counted is the file a binary embedded. # # It COUNTS the matches rather than merely asking whether there are any, because # two stylesheets are as wrong as none and quieter: web/templates.go resolves # cssGlob and takes the first match, so a second file makes the served # stylesheet depend on readdir order. `css` removes the previous build for that # reason, which is exactly why this gate must not assume it did — a gate that # promises "exactly one" and checks "at least one" is a promise the next person # builds on. # # `set --` puts the matches in the positional parameters, so the count is $$# # and no `wc` output has to be parsed. It is fine that this splits on # whitespace: every name it can see was produced by the recipe above, out of a # hex digest. check-css: @set -- $$(ls $(CSS) 2>/dev/null); \ if [ $$# -eq 0 ]; then \ echo "error: no $(CSS) — run 'make css' before 'go build' (the CSS is embedded)"; \ exit 1; \ elif [ $$# -gt 1 ]; then \ echo "error: $$# files match $(CSS) — web/ takes the first, so the choice is arbitrary."; \ echo " run 'make css' to get back to one:"; \ for f in "$$@"; do echo " $$f"; done; \ exit 1; \ fi # Frontend diff/tree bundle: built once, output committed to web/static. The # filename carries a content hash (like the CSS) so a deploy busts the browser # cache — a stale bundle.js is otherwise served for up to max-age and can leave # the file tree rendering blank after an upgrade. Produces exactly ONE # web/static/bundle..js (old ones removed). bundle: mkdir -p web/static rm -f web/static/bundle.js web/static/bundle.*.js cd frontend && npm ci && \ npx esbuild src/app.ts --bundle --minify --format=esm \ --outfile=../web/static/bundle.js mv web/static/bundle.js \ web/static/bundle.$$(sha256sum web/static/bundle.js | cut -c1-8).js # Local development run. Requires a ./config.ini in the working directory (or # ../config.ini, /etc/sr.ht/config.ini): copy config.example.ini and fill in the # instance's shared [sr.ht]/[webhooks] keys, plus a [git.sr.ht] repos root and an # api-origin pointing at a real (or stubbed, see contrib/dev-stub) GraphQL API. # See the "Development" section of README.md for the full recipe. run-dev: build ./$(BIN) -b localhost:5090 install: check-css build @$(MAKE) install-files # The copying half of `install`, with nothing to build in front of it — the # target a packaging run calls once it has already built and checked the binary, # so that the files it stages are the very bytes it checked and not a second # compilation of the same sources. # # That distinction is the whole point and it is not theoretical. `build` above is # .PHONY (Go decides staleness itself, a real file target would never rebuild # after a source edit), so `install` recompiles; abuild runs package() in a FRESH # abuild process under fakeroot, which re-sources the APKBUILD and never calls # build(), so the GOCACHE/GOMODCACHE pins are gone and that recompilation comes # from a cold cache. The packaged binary would be one nothing had inspected. # # It is invoked through a sub-make rather than listed as a third prerequisite of # `install` on purpose: /usr/share/abuild/default.conf exports # MAKEFLAGS=-j$(nproc), prerequisites of one target run in parallel under -j, and # `install: check-css build install-files` would let the copying start beside the # build it is supposed to follow. A recipe line always runs after the # prerequisites are done. # # There is no check in front of the copies. `install -Dm755 comparesrht` on a # missing file is already a fatal error naming the file, which is exactly the # right report for the one way this target can be called too early. install-files: install -Dm755 $(BIN) $(DESTDIR)$(BINDIR)/$(BIN) mkdir -p $(DESTDIR)$(STATICDIR) install -Dm644 -t $(DESTDIR)$(STATICDIR) web/static/* .PHONY: all build test cover bench css check-css bundle run-dev install install-files