SHELL := /usr/bin/env bash GO ?= go GOFMT ?= gofmt INSTALL ?= install PREFIX ?= $(HOME)/.local BINDIR ?= $(PREFIX)/bin LIBDIR ?= $(PREFIX)/lib DESTDIR ?= BUILD_DIR ?= build BUILD_BIN_DIR ?= $(BUILD_DIR)/bin BUILD_MANUAL_DIR ?= $(BUILD_DIR)/manual BANGER_BIN ?= $(BUILD_BIN_DIR)/banger BANGERD_BIN ?= $(BUILD_BIN_DIR)/bangerd VSOCK_AGENT_BIN ?= $(BUILD_BIN_DIR)/banger-vsock-agent BINARIES := $(BANGER_BIN) $(BANGERD_BIN) $(VSOCK_AGENT_BIN) GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort) # BUILD_INPUTS is everything that can change a binary's bytes: Go sources # plus embedded assets (catalog.json, future static files). Listing # everything is cheaper than missing a rebuild — go's own cache absorbs # any redundant invocations. BUILD_INPUTS := $(shell find cmd internal -type f | sort) SHELL_SOURCES := $(shell find scripts -type f -name '*.sh' | sort) SMOKE_DIR := $(BUILD_DIR)/smoke SMOKE_BIN_DIR := $(SMOKE_DIR)/bin SMOKE_COVER_DIR := $(SMOKE_DIR)/covdata SMOKE_XDG_DIR := $(SMOKE_DIR)/xdg SMOKE_SCRIPT := scripts/smoke.sh VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev) COMMIT ?= $(shell git rev-parse --verify HEAD 2>/dev/null || echo unknown) BUILT_AT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal/buildinfo.Commit=$(COMMIT) -X banger/internal/buildinfo.BuiltAt=$(BUILT_AT) .DEFAULT_GOAL := help .PHONY: help build banger bangerd test fmt tidy clean install uninstall lint lint-go lint-shell coverage coverage-html coverage-total smoke smoke-build smoke-coverage-html smoke-clean smoke-fresh help: @printf '%s\n' \ 'Targets:' \ ' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \ ' make install Build and install banger, bangerd, and the companion vsock helper' \ ' make uninstall Stop the daemon and remove installed binaries (leaves user state by default)' \ ' make test Run go test ./...' \ ' make coverage Run tests with coverage; print per-package + total' \ ' make coverage-html Open a browsable per-line HTML report (writes coverage.html)' \ ' make coverage-total Print just the total statement coverage (for scripts/CI)' \ ' make lint Run gofmt + go vet + shellcheck (errors)' \ ' make fmt Format Go sources under cmd/ and internal/' \ ' make tidy Run go mod tidy' \ ' make clean Remove built Go binaries and coverage artefacts' \ ' make smoke Build instrumented binaries, run scripts/smoke.sh, report coverage (needs KVM + sudo)' \ ' make smoke-fresh smoke-clean + smoke — forces first-install paths (migrations, image pull) into the coverage stamp' \ ' make smoke-coverage-html HTML coverage report from the last smoke run' \ ' make smoke-clean Remove the smoke build tree' build: $(BINARIES) $(BANGER_BIN): $(BUILD_INPUTS) go.mod go.sum mkdir -p "$(BUILD_BIN_DIR)" $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(BANGER_BIN)" ./cmd/banger $(BANGERD_BIN): $(BUILD_INPUTS) go.mod go.sum mkdir -p "$(BUILD_BIN_DIR)" $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(BANGERD_BIN)" ./cmd/bangerd $(VSOCK_AGENT_BIN): $(BUILD_INPUTS) go.mod go.sum mkdir -p "$(BUILD_BIN_DIR)" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(VSOCK_AGENT_BIN)" ./cmd/banger-vsock-agent test: $(GO) test ./... # Coverage targets use -coverpkg=./... so packages without their own # tests still get counted when another package exercises them (common # for daemon/* subpackages). coverage.out is gitignored. coverage: $(GO) test -coverpkg=./... -coverprofile=coverage.out ./... @echo '' @echo 'Per-package:' @$(GO) tool cover -func=coverage.out | awk -F'\t+' '/^total:/ {total=$$NF; next} {pkg=$$1; sub("banger/", "", pkg); sub("/[^/]+:[0-9]+:$$", "", pkg); pkgs[pkg]+=1; covered[pkg]+=$$NF+0} END {for (p in pkgs) printf " %-40s %.1f%% (avg of %d funcs)\n", p, covered[p]/pkgs[p], pkgs[p] | "sort"; print ""; print "Total statement coverage:", total}' coverage-html: coverage $(GO) tool cover -html=coverage.out -o coverage.html @echo 'wrote coverage.html' coverage-total: @$(GO) test -coverpkg=./... -coverprofile=coverage.out ./... >/dev/null 2>&1 && $(GO) tool cover -func=coverage.out | awk '/^total:/ {print $$NF}' lint: lint-go lint-shell lint-go: @unformatted="$$($(GOFMT) -l $(GO_SOURCES))"; \ if [ -n "$$unformatted" ]; then \ printf 'gofmt: the following files are not formatted:\n%s\n' "$$unformatted" >&2; \ exit 1; \ fi $(GO) vet ./... lint-shell: @command -v shellcheck >/dev/null 2>&1 || { echo 'shellcheck is required for make lint-shell' >&2; exit 1; } shellcheck --severity=error $(SHELL_SOURCES) fmt: $(GOFMT) -w $(GO_SOURCES) tidy: $(GO) mod tidy clean: rm -rf "$(BUILD_BIN_DIR)" coverage.out coverage.html # Smoke test suite. Builds the three banger binaries with -cover # instrumentation under $(SMOKE_BIN_DIR), runs scripts/smoke.sh # with GOCOVERDIR pointed at $(SMOKE_COVER_DIR), and prints the # resulting coverage. The smoke script fully isolates state via # XDG_* env vars pointing at a mktemp'd root, so the invoking # user's real banger install stays untouched. # # Requires a KVM-capable Linux host with sudo; fails fast via # `banger doctor` when either is missing. This is a pre-release # gate, not CI — the Go test suite is what runs everywhere. smoke-build: $(SMOKE_BIN_DIR)/.built $(SMOKE_BIN_DIR)/.built: $(BUILD_INPUTS) go.mod go.sum mkdir -p "$(SMOKE_BIN_DIR)" $(GO) build -cover -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/banger" ./cmd/banger $(GO) build -cover -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/bangerd" ./cmd/bangerd CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -ldflags '$(GO_LDFLAGS)' -o "$(SMOKE_BIN_DIR)/banger-vsock-agent" ./cmd/banger-vsock-agent touch "$@" smoke: smoke-build rm -rf "$(SMOKE_COVER_DIR)" mkdir -p "$(SMOKE_COVER_DIR)" "$(SMOKE_XDG_DIR)" BANGER_SMOKE_BIN_DIR="$(abspath $(SMOKE_BIN_DIR))" \ BANGER_SMOKE_COVER_DIR="$(abspath $(SMOKE_COVER_DIR))" \ BANGER_SMOKE_XDG_DIR="$(abspath $(SMOKE_XDG_DIR))" \ bash "$(SMOKE_SCRIPT)" @echo '' @echo 'Smoke coverage:' @$(GO) tool covdata percent -i="$(SMOKE_COVER_DIR)" smoke-coverage-html: smoke $(GO) tool covdata textfmt -i="$(SMOKE_COVER_DIR)" -o="$(SMOKE_DIR)/cover.out" $(GO) tool cover -html="$(SMOKE_DIR)/cover.out" -o "$(SMOKE_DIR)/cover.html" @echo 'wrote $(SMOKE_DIR)/cover.html' smoke-clean: rm -rf "$(SMOKE_DIR)" # smoke-fresh wipes everything under $(SMOKE_DIR) (instrumented # binaries, coverage pods, persisted XDG state) and runs a full # smoke from scratch. Useful before a release tag: the regular # `make smoke` reuses the XDG state across runs to skip the ~290MB # image pull, which is fast but leaves migrations and image-upsert # paths cold on every run after the first. smoke-fresh pays the # time cost to stamp those paths into the coverage report too. smoke-fresh: smoke-clean smoke install: build mkdir -p "$(DESTDIR)$(BINDIR)" mkdir -p "$(DESTDIR)$(LIBDIR)/banger" $(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger" $(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd" $(INSTALL) -m 0755 "$(VSOCK_AGENT_BIN)" "$(DESTDIR)$(LIBDIR)/banger/banger-vsock-agent" # uninstall stops a running daemon (if any) and removes the installed # binaries. It does NOT touch user data (config, SSH keys, VM state, # image/kernel caches) — rm -rf those paths manually if wanted; they # are printed for convenience. uninstall: @if [ -x "$(DESTDIR)$(BINDIR)/banger" ]; then \ "$(DESTDIR)$(BINDIR)/banger" daemon stop >/dev/null 2>&1 || true; \ fi rm -f "$(DESTDIR)$(BINDIR)/banger" "$(DESTDIR)$(BINDIR)/bangerd" rm -rf "$(DESTDIR)$(LIBDIR)/banger" @printf '\nRemoved binaries. User data is preserved at:\n' @printf ' ~/.config/banger/ (config, ssh keys)\n' @printf ' ~/.local/state/banger/ (VMs, images, kernels, db, logs)\n' @printf ' ~/.cache/banger/ (OCI layer cache)\n' @printf '\nDelete those paths manually if you want a full purge.\n'