# ATCR Makefile # Build targets for the ATProto Container Registry .PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \ build-trixie \ generate test test-billing test-race test-verbose integration-test stripe-integration-test \ lint lex-lint clean help install-credential-helper \ develop develop-detached develop-down dev \ docker docker-appview docker-hold docker-scanner .DEFAULT_GOAL := help help: ## Show this help message @echo "ATCR Build Targets:" @echo "" @awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-28s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) all: generate build ## Generate assets and build all binaries (default) # Generated asset files GENERATED_ASSETS = \ pkg/appview/public/js/htmx.min.js \ pkg/appview/public/js/lucide.min.js \ pkg/appview/licenses/spdx-licenses.json generate: ## Run go generate ./... (always — regenerates cbor_gen, icon sprites, vendor assets) @echo "→ Running go generate ./..." go generate ./... # File rule: lazily download missing vendor assets for fast incremental local builds. # Production builds depend on the phony `generate` target instead so generated code # (cbor_gen.go, icon sprites, etc.) is always up to date. $(GENERATED_ASSETS): @echo "→ Generating vendor assets and code..." go generate ./... ##@ Build Targets build: build-appview build-hold build-credential-helper ## Build all binaries # Legal page "Last updated" dates come from the git commit date of the page # templates. Empty values (e.g., Docker builds without .git) fall back to the # hardcoded default in legal.go. LEGAL_PKG := atcr.io/pkg/appview/handlers PRIVACY_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/privacy.html 2>/dev/null) TERMS_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/terms.html 2>/dev/null) APPVIEW_LDFLAGS := -X '$(LEGAL_PKG).privacyLastUpdated=$(PRIVACY_DATE)' -X '$(LEGAL_PKG).termsLastUpdated=$(TERMS_DATE)' build-appview: $(GENERATED_ASSETS) ## Build appview binary only @echo "→ Building appview..." @mkdir -p bin go build -ldflags="$(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview build-hold: $(GENERATED_ASSETS) ## Build hold binary only @echo "→ Building hold..." @mkdir -p bin go build -o bin/atcr-hold ./cmd/hold build-credential-helper: ## Build credential helper only (atcr brand) @echo "→ Building credential helper..." @mkdir -p bin cd cmd/credential-helper/atcr && go build -ldflags="-X main.version=$(shell git describe --tags --always 2>/dev/null || echo dev) -X main.commit=$(shell git rev-parse HEAD 2>/dev/null || echo none)" -o ../../../bin/docker-credential-atcr . build-oauth-helper: ## Build OAuth helper only @echo "→ Building OAuth helper..." @mkdir -p bin go build -o bin/oauth-helper ./cmd/oauth-helper # Trixie cross-build (Debian 13, glibc 2.41) — produces binaries that run on # any glibc ≥ 2.41 target, even when the host glibc is newer (e.g. Fedora's # 2.43, which otherwise stamps sqrtf@GLIBC_2.43 onto cgo-linked output). TRIXIE_BUILDER_IMAGE ?= golang:1-trixie # Billing is OFF by default, and that default is load-bearing. # # pkg/billing sits behind the `billing` build tag, so the tag decides whether a # deployment has Stripe compiled in at all. This target used to hardcode it, # which meant the only supported way to build a non-billing appview was to # bypass the Makefile and drive the container by hand — and every deploy that # forgot to would silently switch billing on for a deployment whose operator # had chosen to run without it. # # Opt in explicitly instead: # # make build-trixie # no billing (default) # make build-trixie BILLING=1 # billing compiled in # # Only the appview is affected; hold, scanner, labeler and the credential # helper never reference pkg/billing. Verify what you built with: # # strings -a bin/atcr-appview | grep -c stripe-go # 0 when billing is off BILLING ?= ifeq ($(BILLING),) APPVIEW_TAGS := else APPVIEW_TAGS := -tags billing endif build-trixie: generate ## Build all production binaries for linux/amd64 in a Debian 13 (glibc 2.41) container. Billing is off unless BILLING=1. @echo "→ Building in $(TRIXIE_BUILDER_IMAGE) for glibc 2.41 compatibility..." @mkdir -p bin docker run --rm \ --user $$(id -u):$$(id -g) \ -v "$(CURDIR)":/src \ -w /src \ -e HOME=/tmp \ -e GOCACHE=/tmp/.gocache \ -e GOMODCACHE=/tmp/.gomodcache \ -e CGO_ENABLED=1 \ -e GOOS=linux \ -e GOARCH=amd64 \ $(TRIXIE_BUILDER_IMAGE) \ bash -c '\ set -e && \ go build -trimpath $(APPVIEW_TAGS) -ldflags="-s -w $(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview && \ go build -trimpath -ldflags="-s -w" -o bin/atcr-hold ./cmd/hold && \ (cd cmd/credential-helper/atcr && go build -trimpath -ldflags="-s -w" -o ../../../bin/docker-credential-atcr .) && \ go build -trimpath -ldflags="-s -w" -o bin/atcr-labeler ./cmd/labeler && \ cd scanner && go build -trimpath -ldflags="-s -w" -o ../bin/atcr-scanner ./cmd/scanner' @echo "✓ Built to bin/ (glibc ≥ 2.41 compatible), appview billing: $(if $(BILLING),ENABLED,disabled)" ##@ Test Targets # Every module in the workspace except the root. `go test ./...` only ever # covers the module it is run from, so a workspace module needs its own # invocation or its tests never run at all. That is how the scanner's suite sat # dark: 17 test files, none of them reached by `make test`. # # Derived from go.work rather than listed by hand, so adding a module to the # workspace is enough to get it tested. Falls back to the known set if the # go tool cannot answer, so a broken workspace fails loudly in the test run # rather than silently testing less. GO_SUBMODULE_DIRS := $(shell go list -m -f '{{.Dir}}' 2>/dev/null | tail -n +2) ifeq ($(strip $(GO_SUBMODULE_DIRS)),) GO_SUBMODULE_DIRS := scanner cmd/credential-helper/atcr cmd/credential-helper/seamark deploy/upcloud endif # $(call test-submodules,) define test-submodules @for dir in $(GO_SUBMODULE_DIRS); do \ echo "→ Testing module $$(basename $$dir)..."; \ (cd $$dir && go test $(1) ./...) || exit 1; \ done endef # Tests run as a `testmode` build. The tag compiles pkg/atproto/indigo_local.go # in place of indigo_prod.go, which is what lets a did:web on a loopback port # (every test server, the integration harness, docker-compose) resolve over # plain HTTP. Production binaries never carry the tag; there is no runtime # switch. Tests that need it fail fast with a message naming the tag. TEST_TAGS := testmode test: test-billing ## Run all tests (every workspace module) @echo "→ Running tests..." go test -tags $(TEST_TAGS) -cover ./... $(call test-submodules,-tags $(TEST_TAGS) -cover) # pkg/billing is behind the `billing` build tag, so `go test ./...` never # compiles it, let alone runs it. Its tests covered the money path and had # never executed in this target or in CI. test-billing: ## Run the billing-tagged tests (skipped by plain `go test ./...`) @echo "→ Running billing-tagged tests..." go test -tags billing,$(TEST_TAGS) -cover ./pkg/billing/... test-race: ## Run tests with race detector (every workspace module) @echo "→ Running tests with race detector..." go test -race -tags $(TEST_TAGS) ./... @echo "→ Running billing-tagged tests with race detector..." go test -race -tags billing,$(TEST_TAGS) ./pkg/billing/... $(call test-submodules,-race -tags $(TEST_TAGS)) test-verbose: ## Run tests with verbose output (every workspace module) @echo "→ Running tests with verbose output..." go test -v -tags $(TEST_TAGS) ./... $(call test-submodules,-v -tags $(TEST_TAGS)) integration-test: ## Run in-process smoke test (no docker, fake PDS + gofakes3 + hold + appview) @echo "→ Running integration smoke test..." go test -tags=integration,$(TEST_TAGS) -count=1 -race -timeout=120s ./test/integration/... stripe-integration-test: ## Run Stripe sandbox-backed billing tests (needs STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY) @echo "→ Running Stripe sandbox integration tests..." @echo " Required env: STRIPE_SECRET_KEY (sk_test_...), STRIPE_WEBHOOK_SECRET (whsec_...)," @echo " STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY" @echo " Optional env: STRIPE_TEST_TIER_NAME (default 'Supporter')," @echo " STRIPE_TEST_EXISTING_CUSTOMER_DID (skips portal search-lag wait)" go test -tags="billing stripe_integration $(TEST_TAGS)" -count=1 -timeout=180s ./test/stripe-integration/... ##@ Quality Targets .PHONY: check-golangci-lint check-golangci-lint: @LINT_PKG=github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \ CUR_GO=$$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1 | sed 's/^go//'); \ if ! command -v golangci-lint > /dev/null 2>&1; then \ echo "→ Installing golangci-lint..."; \ go install $$LINT_PKG; \ else \ LINT_GO=$$(golangci-lint --version 2>&1 | grep -oE 'built with go[0-9]+\.[0-9]+' | head -1 | sed 's/^built with go//'); \ if [ -n "$$LINT_GO" ] && [ "$$LINT_GO" != "$$CUR_GO" ] && \ [ "$$(printf '%s\n%s\n' $$LINT_GO $$CUR_GO | sort -V | head -1)" = "$$LINT_GO" ]; then \ echo "→ golangci-lint built with go$$LINT_GO but project targets go$$CUR_GO — reinstalling..."; \ go install $$LINT_PKG; \ fi; \ fi lint: check-golangci-lint ## Run golangci-lint @echo "→ Running golangci-lint..." golangci-lint run ./... @echo "→ Running golangci-lint (billing tag)..." golangci-lint run --build-tags=billing ./pkg/billing/... @echo "→ Running golangci-lint (testmode tag)..." golangci-lint run --build-tags=testmode ./... @echo "→ Running golangci-lint (integration tags)..." golangci-lint run --build-tags=integration,testmode ./test/integration/... golangci-lint run --build-tags=billing,stripe_integration,testmode ./test/stripe-integration/... lex-lint: ## Lint ATProto lexicon schemas goat lex lint ./lexicons/ ##@ Install Targets install-credential-helper: build-credential-helper ## Install credential helper to /usr/local/sbin @echo "→ Installing credential helper to /usr/local/sbin..." install -m 755 bin/docker-credential-atcr /usr/local/sbin/docker-credential-atcr @echo "✓ Installed docker-credential-atcr to /usr/local/sbin/" ##@ Development Targets # Air's build command appends $GO_TAGS to its -tags list; a local run needs # `testmode` for the same reason the tests do (see TEST_TAGS above). dev: export GO_TAGS ?= testmode dev: $(GENERATED_ASSETS) ## Run AppView locally with Air hot reload (testmode build) @which air > /dev/null || (echo "→ Installing Air..." && go install github.com/air-verse/air@latest) air -c .air.toml ##@ Docker Targets docker: docker-appview docker-hold docker-scanner ## Build all Docker images docker-appview: ## Build appview Docker image @echo "→ Building appview Docker image..." docker build -f Dockerfile.appview \ --build-arg PRIVACY_DATE=$(PRIVACY_DATE) \ --build-arg TERMS_DATE=$(TERMS_DATE) \ -t atcr.io/atcr.io/appview:latest . docker-hold: ## Build hold Docker image @echo "→ Building hold Docker image..." docker build -f Dockerfile.hold -t atcr.io/atcr.io/hold:latest . docker-scanner: ## Build scanner Docker image @echo "→ Building scanner Docker image..." docker build -f Dockerfile.scanner -t atcr.io/atcr.io/scanner:latest . develop: ## Build and start docker-compose with Air hot reload @echo "→ Building Docker images..." docker-compose build @echo "→ Starting docker-compose with hot reload..." docker-compose up develop-detached: ## Build and start docker-compose with hot reload (detached) @echo "→ Building Docker images..." docker-compose build @echo "→ Starting docker-compose with hot reload (detached)..." docker-compose up -d @echo "✓ Services started in background with hot reload" @echo " AppView: http://localhost:5000" @echo " Hold: http://localhost:8080" develop-down: ## Stop docker-compose services @echo "→ Stopping docker-compose..." docker-compose down ##@ Utility Targets clean: ## Remove built binaries and generated assets @echo "→ Cleaning build artifacts..." rm -rf bin/ rm -f pkg/appview/licenses/spdx-licenses.json @echo "✓ Clean complete"