SHELL := /usr/bin/env bash .SHELLFLAGS := -eu -o pipefail -c .DEFAULT_GOAL := help .DELETE_ON_ERROR: PROJECT := latchd BINARY := latchd PREFIX ?= /usr DESTDIR ?= BINDIR := $(DESTDIR)$(PREFIX)/bin CONFDIR := $(DESTDIR)/etc/$(PROJECT) USER_CONFIG_HOME ?= $(if $(XDG_CONFIG_HOME),$(XDG_CONFIG_HOME),$(HOME)/.config) USER_CONFDIR := $(USER_CONFIG_HOME)/$(PROJECT) SYSTEMD_DIR := $(DESTDIR)/usr/lib/systemd/system BUILD_DIR ?= build DIST_DIR ?= dist CARGO_TARGET_DIR ?= target CARGO ?= cargo GO ?= go TARGET ?= TAG ?= VERSION ?= $(shell sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1) GIT_COMMIT ?= $(shell git rev-parse --short=12 HEAD 2>/dev/null || printf unknown) SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct 2>/dev/null || date +%s) TARGET_TRIPLE ?= $(shell rustc -vV 2>/dev/null | sed -n 's/^host: //p' || printf unknown) TARGET_ARGS := $(if $(TARGET),--target $(TARGET),) RELEASE_DIR := $(CARGO_TARGET_DIR)/$(if $(TARGET),$(TARGET)/)release PACKAGE_NAME := $(PROJECT)-$(VERSION)-$(if $(TARGET),$(TARGET),$(TARGET_TRIPLE)) PACKAGE_FILE := $(DIST_DIR)/$(PACKAGE_NAME).tar.gz PACKAGE_STAGE := $(DIST_DIR)/stage/$(PACKAGE_NAME) CARGO_ENV := LATCHD_VERSION=$(VERSION) LATCHD_COMMIT=$(GIT_COMMIT) LATCHD_BUILD_TIME=$(SOURCE_DATE_EPOCH) SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) COLOR ?= 1 ifeq ($(COLOR),1) RESET := \033[0m DIM := \033[2m CYAN := \033[36m GREEN := \033[32m YELLOW := \033[33m RED := \033[31m else RESET := DIM := CYAN := GREEN := YELLOW := RED := endif .PHONY: all deps build release-build test check fmt lint doc audit docker-test \ integration-test pam-fixture-test system-e2e system-e2e-strict \ package release release-check version install install-user uninstall clean run-preview \ legacy-build legacy-check enable update help all: release-build ## Build the optimized release binary version: ## Print the package, commit, and reproducibility metadata @printf 'version=%s\ncommit=%s\nepoch=%s\ntarget=%s\n' \ '$(VERSION)' '$(GIT_COMMIT)' '$(SOURCE_DATE_EPOCH)' '$(if $(TARGET),$(TARGET),$(TARGET_TRIPLE))' deps: ## Check native dependencies needed for builds and tests @command -v $(CARGO) >/dev/null || { echo 'cargo (Rust 1.88+) is required'; exit 1; } @command -v rustfmt >/dev/null || { echo 'rustfmt is required'; exit 1; } @command -v pkg-config >/dev/null || { echo 'pkg-config is required'; exit 1; } @pkg-config --exists pam || { echo 'libpam development files are required'; exit 1; } @printf '$(GREEN)dependencies ok$(RESET)\n' build: release-check ## Build and copy the optimized binary to build/ @mkdir -p '$(BUILD_DIR)' @printf '$(CYAN)building$(RESET) %s %s\n' '$(VERSION)' '$(if $(TARGET),$(TARGET),$(TARGET_TRIPLE))' @env $(CARGO_ENV) $(CARGO) build --release --locked $(TARGET_ARGS) @install -m755 '$(RELEASE_DIR)/$(BINARY)' '$(BUILD_DIR)/$(BINARY)' @'$(BUILD_DIR)/$(BINARY)' --version release-build: build ## Alias for the optimized release build fmt: ## Check Rust formatting @$(CARGO) fmt --all -- --check test: ## Run all Rust tests @$(CARGO) test --locked --workspace --all-targets lint: ## Run Clippy with warnings denied @$(CARGO) clippy --locked --workspace --all-targets --all-features -- -D warnings doc: ## Build rustdoc with warnings denied @RUSTDOCFLAGS='-D warnings' $(CARGO) doc --locked --workspace --no-deps audit: ## Run cargo-audit when installed @command -v cargo-audit >/dev/null || { echo 'cargo-audit is required; install with cargo install cargo-audit --locked'; exit 1; } @cargo audit check: fmt test lint doc ## Run the complete local verification gate docker-test: ## Run the pinned Docker verification suite @./scripts/test-docker.sh integration-test: build ## Exercise safe, non-authenticating CLI paths @./scripts/test-cli.sh '$(BUILD_DIR)/$(BINARY)' pam-fixture-test: ## Run the disposable PAM fault-injection tests when privileged @./scripts/test-pam-fixture.sh system-e2e: ## Probe PAM, VT, X11, Wayland, and logind without requiring privileges @./scripts/test-system-e2e.sh '$(BUILD_DIR)/$(BINARY)' system-e2e-strict: ## Require all real login-manager capabilities (prepared VM only) @./scripts/test-system-e2e.sh --strict '$(BUILD_DIR)/$(BINARY)' release-check: ## Validate a release version and optional tag @[[ '$(VERSION)' =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$$ ]] || { echo 'invalid VERSION: $(VERSION)'; exit 1; } @cargo_version="$$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -1)"; \ [[ "$$cargo_version" == '$(VERSION)' ]] || { echo "Cargo version $$cargo_version does not match $(VERSION)"; exit 1; } @if [[ -n '$(TAG)' ]]; then \ tag_version="$$(printf %s '$(TAG)' | sed 's/^v//')"; \ [[ "$$tag_version" == '$(VERSION)' ]] || { echo "tag $(TAG) does not match $(VERSION)"; exit 1; }; \ fi package: release-check build ## Create a deterministic install archive and checksum @rm -rf '$(PACKAGE_STAGE)' @mkdir -p '$(PACKAGE_STAGE)/usr/bin' '$(PACKAGE_STAGE)/usr/lib/systemd/system' \ '$(PACKAGE_STAGE)/etc/pam.d' '$(PACKAGE_STAGE)/etc/$(PROJECT)/wms' '$(PACKAGE_STAGE)/etc/$(PROJECT)/wayland' @install -m755 '$(BUILD_DIR)/$(BINARY)' '$(PACKAGE_STAGE)/usr/bin/$(BINARY)' @install -m644 extra/config.toml '$(PACKAGE_STAGE)/etc/$(PROJECT)/config.toml' @install -m644 extra/latchd.pam '$(PACKAGE_STAGE)/etc/pam.d/$(PROJECT)' @install -m644 extra/latchd.service '$(PACKAGE_STAGE)/usr/lib/systemd/system/$(PROJECT).service' @install -m644 README.md LICENSE CHANGELOG.md '$(PACKAGE_STAGE)/' @printf 'version=%s\ncommit=%s\nepoch=%s\ntarget=%s\n' \ '$(VERSION)' '$(GIT_COMMIT)' '$(SOURCE_DATE_EPOCH)' '$(if $(TARGET),$(TARGET),$(TARGET_TRIPLE))' \ > '$(PACKAGE_STAGE)/BUILD-METADATA' @mkdir -p '$(DIST_DIR)' @tar --sort=name --numeric-owner --owner=0 --group=0 --mtime='@$(SOURCE_DATE_EPOCH)' \ -C '$(DIST_DIR)/stage' -czf '$(PACKAGE_FILE)' '$(PACKAGE_NAME)' @(cd '$(DIST_DIR)' && sha256sum '$(notdir $(PACKAGE_FILE))' > '$(notdir $(PACKAGE_FILE)).sha256') @(cd '$(DIST_DIR)' && sha256sum --check '$(notdir $(PACKAGE_FILE)).sha256') @checksum="$$(sha256sum '$(PACKAGE_FILE)' | cut -d' ' -f1)"; \ printf 'package=%s\nsha256=%s\nversion=%s\n' \ '$(PACKAGE_FILE)' "$$checksum" '$(VERSION)' > '$(DIST_DIR)/release.env' @printf '$(GREEN)created$(RESET) %s\n' '$(PACKAGE_FILE)' release: check audit integration-test system-e2e docker-test package ## Run all gates and produce release artifacts install: build ## Install the binary, config, PAM policy, and systemd unit @install -Dm755 '$(BUILD_DIR)/$(BINARY)' '$(BINDIR)/$(BINARY)' @install -Dm644 extra/config.toml '$(CONFDIR)/config.toml' @install -Dm644 extra/latchd.pam '$(DESTDIR)/etc/pam.d/$(PROJECT)' @install -Dm644 extra/latchd.service '$(SYSTEMD_DIR)/$(PROJECT).service' @install -d '$(CONFDIR)/wms' '$(CONFDIR)/wayland' install-user: ## Copy the example config to the current user's config directory @test -n '$(USER_CONFIG_HOME)' || { echo 'HOME or XDG_CONFIG_HOME is required'; exit 1; } @install -d -m700 '$(USER_CONFDIR)' @if [ -e '$(USER_CONFDIR)/config.toml' ]; then \ printf 'configuration at %s was preserved\n' '$(USER_CONFDIR)/config.toml'; \ else \ install -m600 extra/config.toml '$(USER_CONFDIR)/config.toml'; \ printf 'created %s\n' '$(USER_CONFDIR)/config.toml'; \ fi uninstall: ## Remove files installed by make install @rm -f '$(BINDIR)/$(BINARY)' '$(DESTDIR)/etc/pam.d/$(PROJECT)' '$(SYSTEMD_DIR)/$(PROJECT).service' @printf '$(YELLOW)configuration at %s was preserved; remove it manually if no longer needed$(RESET)\n' '$(CONFDIR)' clean: ## Remove generated build, release, and Cargo artifacts @rm -rf '$(BUILD_DIR)' '$(DIST_DIR)' @$(CARGO) clean run-preview: build ## Run the safe non-authenticating preview UI @'$(BUILD_DIR)/$(BINARY)' --preview --no-log legacy-build: ## Build the quarantined Go reference only when explicitly requested @printf '$(YELLOW)legacy Go is reference-only and excluded from release builds$(RESET)\n' @command -v $(GO) >/dev/null || { echo 'go is required for legacy-build'; exit 1; } @mkdir -p '$(BUILD_DIR)' @cd legacy/go && $(GO) build -trimpath -o '../../$(BUILD_DIR)/$(BINARY)-go' . legacy-check: ## Format-check and test the quarantined Go reference @command -v $(GO) >/dev/null || { echo 'go is required for legacy-check'; exit 1; } @command -v gofmt >/dev/null || { echo 'gofmt is required for legacy-check'; exit 1; } @test -z "$$(find legacy/go -type f -name '*.go' -print0 | xargs -0 gofmt -l)" || { echo 'legacy Go files need gofmt'; exit 1; } @cd legacy/go && $(GO) test ./... enable: ## Enable the systemd service after host validation @sudo systemctl enable $(PROJECT).service update: ## Rebuild and reinstall the current checkout without changing git state @sudo $(MAKE) install help: ## Show available targets @printf '$(CYAN)$(PROJECT)$(RESET) $(VERSION) — production build targets\n\n' @awk -F ':|##' '/^[a-zA-Z0-9_-]+:.*##/ { printf " %-18s %s\n", $$1, $$NF }' '$(MAKEFILE_LIST)' | sort