Makefile
Rama main · réplica de lectura del repositorio original.
# Homelab Horizon Build Makefile
BINARY_NAME=homelab-horizon
CMD_PATH=./cmd/homelab-horizon
VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
BUILD_TIME=$(shell date -u '+%Y-%m-%d_%H:%M:%S')
LDFLAGS=-ldflags "-s -w -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)"
# Server builds embed the per-arch hz operator-CLI binaries (served at
# /admin/hz/*) behind the 'hzembed' tag; the hz-embed target cross-compiles them
# into this dir first. A plain `go build`/CI omits the tag and needs no binaries.
SERVER_TAGS=hzembed,uiembed
HZ_EMBED_DIR=internal/server/hzbin/bin
# The admin UI is compiled in for release builds, so a release is one file that
# renders its own login page. ui-embed stages ui/dist here first; a plain
# `go build`/CI omits the tag and needs no staged assets.
UI_EMBED_DIR=internal/server/uiembed/dist
# Default target
.PHONY: all
all: build
# Generate TypeScript types from Go structs (requires tygo)
.PHONY: generate
generate:
go run github.com/iodesystems/tygo@v0.2.21-iode.1 generate
# Build frontend (React SPA)
.PHONY: ui
ui: generate
cd ui && pnpm install --frozen-lockfile && pnpm build
# Create stub ui/dist for Go-only builds (no node toolchain required)
ui/dist/index.html:
mkdir -p ui/dist
echo '<!DOCTYPE html><html><body>Run <code>make ui</code> to build the frontend.</body></html>' > ui/dist/index.html
# Build for current platform (includes frontend)
.PHONY: build
build: ui hz-embed ui-embed
CGO_ENABLED=0 go build -tags $(SERVER_TAGS) $(LDFLAGS) -o $(BINARY_NAME) $(CMD_PATH)
# Build Go only (uses stub frontend if ui/dist doesn't exist)
.PHONY: build-go
build-go: ui/dist/index.html hz-embed ui-embed
CGO_ENABLED=0 go build -tags $(SERVER_TAGS) $(LDFLAGS) -o $(BINARY_NAME) $(CMD_PATH)
# Build the hz operator CLI (admin-token client: service CRUD + sync + setup)
.PHONY: build-hz
build-hz:
CGO_ENABLED=0 go build $(LDFLAGS) -o hz ./cmd/hz
# Build hz-probe, the outside-in vantage agent. It runs on a host outside the
# homelab, so the useful build is a cross-compile you scp to that host.
.PHONY: build-probe
build-probe:
CGO_ENABLED=0 go build $(LDFLAGS) -o hz-probe ./cmd/hz-probe
.PHONY: build-probe-all
build-probe-all: dist
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o dist/hz-probe-linux-amd64 ./cmd/hz-probe
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o dist/hz-probe-linux-arm64 ./cmd/hz-probe
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build $(LDFLAGS) -o dist/hz-probe-linux-armv7 ./cmd/hz-probe
# Cross-compile the client binaries the server embeds: hz (served at
# /admin/hz/bin/) and hz-probe (served at /admin/hz-probe/bin/). Always builds
# all served arches regardless of the server's own arch.
.PHONY: ui-embed
ui-embed: ui/dist/index.html
@rm -rf $(UI_EMBED_DIR)
@mkdir -p $(UI_EMBED_DIR)
@cp -r ui/dist/. $(UI_EMBED_DIR)/
@du -sh $(UI_EMBED_DIR) | sed 's/^/ ui embedded: /'
.PHONY: hz-embed
hz-embed:
@rm -rf $(HZ_EMBED_DIR)
@mkdir -p $(HZ_EMBED_DIR)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-linux-amd64 ./cmd/hz
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-linux-arm64 ./cmd/hz
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-linux-arm ./cmd/hz
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-probe-linux-amd64 ./cmd/hz-probe
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-probe-linux-arm64 ./cmd/hz-probe
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build $(LDFLAGS) -o $(HZ_EMBED_DIR)/hz-probe-linux-arm ./cmd/hz-probe
# Run backend + frontend dev servers together (Ctrl-C stops both)
.PHONY: run
run: ui/node_modules
@trap 'kill 0' EXIT; \
cd ui && pnpm dev & \
go run $(CMD_PATH) & \
wait
# Run Go backend only (builds frontend first, serves at /app/)
.PHONY: run-backend
run-backend: ui
go run $(CMD_PATH)
# Run Vite frontend dev server only (proxies API to :8080)
.PHONY: run-frontend
run-frontend: ui/node_modules
cd ui && pnpm dev
# Install frontend dependencies if needed
ui/node_modules: ui/package.json
cd ui && pnpm install
@touch ui/node_modules
# Build for all platforms
.PHONY: build-all
build-all: ui build-linux-amd64 build-linux-arm64 build-linux-arm
# Linux AMD64 (most servers, x86_64)
.PHONY: build-linux-amd64
build-linux-amd64: ui dist hz-embed ui-embed
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags $(SERVER_TAGS) $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-amd64 $(CMD_PATH)
# Linux ARM64 (Raspberry Pi 4/5, modern ARM servers)
.PHONY: build-linux-arm64
build-linux-arm64: ui dist hz-embed ui-embed
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags $(SERVER_TAGS) $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-arm64 $(CMD_PATH)
# Linux ARM (Raspberry Pi 2/3, older 32-bit ARM)
.PHONY: build-linux-arm
build-linux-arm: ui dist hz-embed ui-embed
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -tags $(SERVER_TAGS) $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-armv7 $(CMD_PATH)
# Clean build artifacts
.PHONY: clean
clean:
rm -f $(BINARY_NAME) hz hz-probe
rm -rf dist/
rm -rf ui/dist/
rm -rf $(HZ_EMBED_DIR)
# Run tests
.PHONY: test
test:
go test -v ./...
# Run unit tests only
.PHONY: test-unit
test-unit:
go test -v ./internal/...
# Run integration tests only
.PHONY: test-integration
test-integration:
go test -v ./test/integration/...
# Run tests with coverage
.PHONY: test-coverage
test-coverage:
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Lint (canon X-LINT-1) — install once: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.1
.PHONY: lint
lint:
golangci-lint run ./...
# Check/lint — gofmt-clean check (no mutation) + vet + golangci-lint
.PHONY: check
check: check-generated
@test -z "$$(gofmt -l . )" || { echo "gofmt needed:"; gofmt -l .; exit 1; }
go vet ./...
golangci-lint run ./...
# The generated TypeScript must be BOTH current and valid, and nothing checked either.
#
# tygo emitted `export const X = any /* configmgr.Y */` for eleven cross-package constant
# aliases — `any` is a type, not a value — and the committed .ts simply went stale instead,
# so the breakage sat waiting for whoever next ran `make generate`. Regenerating and
# diffing catches staleness; tsc catches output that parses as Go but not as TypeScript.
.PHONY: check-generated
check-generated:
@set -e; \
tmp=$$(mktemp); \
trap 'rm -f "$$tmp"' EXIT; \
cp ui/src/api/generated-types.ts "$$tmp"; \
go run github.com/iodesystems/tygo@v0.2.21-iode.1 generate >/dev/null; \
if ! diff -q "$$tmp" ui/src/api/generated-types.ts >/dev/null; then \
cp "$$tmp" ui/src/api/generated-types.ts; \
echo "generated-types.ts is STALE — run 'make generate' and commit the result"; exit 1; \
fi; \
cd ui && npx --no-install tsc --noEmit --skipLibCheck --ignoreConfig src/api/generated-types.ts \
|| { echo "generated-types.ts is not valid TypeScript — see the errors above"; exit 1; }; \
echo "generated types: current and valid"
# Run all checks
.PHONY: test-all
test-all: test-unit test-integration check
# Create dist directory
dist:
mkdir -p dist
# DEPLOY-9: assemble a deployable payload — the binary plus the frontend it now
# serves from disk (WEB-5). One tree to copy, so the two cannot arrive
# separately: a binary without its UI answers the API and shows an explanatory
# page where the login form should be.
#
# PAYLOAD_ARCH selects the binary; defaults to this machine's build.
PAYLOAD_ARCH ?=
.PHONY: payload
payload: ui
@rm -rf dist/payload
@mkdir -p dist/payload/public
@if [ -n "$(PAYLOAD_ARCH)" ]; then \
$(MAKE) build-$(PAYLOAD_ARCH) >/dev/null; \
cp dist/$(BINARY_NAME)-$(PAYLOAD_ARCH) dist/payload/$(BINARY_NAME); \
else \
$(MAKE) build >/dev/null; \
cp $(BINARY_NAME) dist/payload/$(BINARY_NAME); \
fi
cp -r ui/dist/. dist/payload/public/
@echo "payload: dist/payload (binary + public/)"
@du -sh dist/payload/public | sed 's/^/ ui: /'
# Build release archives
.PHONY: release
# One file per archive. The UI is compiled into the binary (SERVER_TAGS
# includes uiembed), so there is no second artifact to install correctly and no
# way to land a gateway whose admin page cannot render.
release: clean dist ui build-all
@echo "Creating release archives..."
@for arch in linux-amd64 linux-arm64 linux-armv7; do \
tar -czf dist/$(BINARY_NAME)-$$arch.tar.gz -C dist $(BINARY_NAME)-$$arch; \
done
@echo "Release archives created in dist/"
@ls -la dist/*.tar.gz
# Install locally (requires sudo)
.PHONY: install
install: build
sudo cp $(BINARY_NAME) /usr/local/bin/
@echo "Installed to /usr/local/bin/$(BINARY_NAME)"
# Build Docker demo image (vanilla Ubuntu + binary + demo config)
.PHONY: docker
docker: build-linux-amd64
docker build -t homelab-horizon:demo .
# Run Docker demo container
.PHONY: docker-run
docker-run: docker
docker run --rm -p 8080:8080 --name hz-demo homelab-horizon:demo
# Regenerate README screenshots in a hermetic Docker container (no real network)
.PHONY: screenshots
screenshots:
./bin/screenshots
# End-to-end VPN MFA jail test in a throwaway multipass VM (real kernel,
# real WireGuard/iptables/HAProxy). Needs multipass; takes a few minutes.
.PHONY: e2e
e2e:
./bin/e2e
# Admin account auth: bootstrap, TOTP against oathtool, the second-factor gate,
# the lockout guards, and a passkey ceremony against the admin relying party.
.PHONY: e2e-auth
e2e-auth:
ACCOUNT=1 ACCOUNT_PASSKEY=1 OIDC=1 ./bin/e2e
.PHONY: help
help:
@echo "Homelab Horizon Build Targets:"
@echo ""
@echo " make - Build for current platform (includes frontend)"
@echo " make ui - Build frontend only (React SPA)"
@echo " make build-go - Build Go only (stub frontend)"
@echo " make build-hz - Build the hz operator CLI (admin-token client)"
@echo " make build-probe - Build hz-probe (outside-in vantage agent)"
@echo " make build-probe-all - Cross-compile hz-probe for amd64/arm64/armv7"
@echo " make run - Run backend + frontend dev servers together"
@echo " make run-backend - Run Go backend only (:8080)"
@echo " make run-frontend - Run Vite frontend dev server only (:5173)"
@echo " make build-all - Build for all platforms"
@echo " make release - Build all platforms and create .tar.gz archives"
@echo ""
@echo " make build-linux-amd64 - Build for Linux x86_64"
@echo " make build-linux-arm64 - Build for Linux ARM64 (Raspberry Pi 4/5)"
@echo " make build-linux-arm - Build for Linux ARMv7 (Raspberry Pi 2/3)"
@echo ""
@echo " make install - Install to /usr/local/bin (requires sudo)"
@echo " make clean - Remove build artifacts"
@echo " make test - Run tests"
@echo " make check - Run go vet and fmt"
@echo ""
@echo " make docker - Build Docker demo image"
@echo " make docker-run - Build and run Docker demo on :8080"
@echo " make screenshots - Regenerate README screenshots (hermetic Docker)"
@echo " make payload - Assemble dist/payload (binary + public/) for deploy"
@echo " make e2e - End-to-end MFA jail test (multipass VM)"
@echo " make e2e-auth - End-to-end admin account auth test: accounts, passkeys, SSO"