feat: ship Rust login manager and GitLab release pipeline

Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
This commit is contained in:
2026-09-25 21:33:46 +05:30
parent 1723ab53b8
commit faf5b27328
49 changed files with 6795 additions and 747 deletions

272
Makefile
View File

@@ -1,146 +1,168 @@
BINARY := latchd
SRC_DIR := src
CMD_DIR := $(SRC_DIR)
BUILD_DIR := build
SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
.DELETE_ON_ERROR:
PREFIX ?= /usr
BINDIR := $(DESTDIR)$(PREFIX)/bin
CONFDIR := $(DESTDIR)/etc/$(BINARY)
SYSTEMD := $(DESTDIR)/usr/lib/systemd/system
PROJECT := latchd
BINARY := latchd
PREFIX ?= /usr
DESTDIR ?=
BINDIR := $(DESTDIR)$(PREFIX)/bin
CONFDIR := $(DESTDIR)/etc/$(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 ?= 0.1.0
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILDDATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
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)
LDFLAGS := -s -w \
-X main.Version=$(VERSION) \
-X main.Commit=$(COMMIT) \
-X main.BuildTime=$(BUILDDATE)
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)
GO := go
GOFLAGS := -trimpath -ldflags="$(LDFLAGS)"
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
## ansi helpers
BOLD := \033[1m
DIM := \033[2m
RED := \033[31m
GREEN := \033[32m
YELLOW := \033[33m
BLUE := \033[34m
CYAN := \033[36m
WHITE := \033[37m
RST := \033[0m
.PHONY: all deps build release-build test check fmt lint doc audit docker-test \
package release release-check version install uninstall clean run-preview \
legacy-build legacy-check enable update help
## ─── targets ────────────────────────────────────────────────
all: release-build ## Build the optimized release binary
.PHONY: all deps build install uninstall clean test lint fmt vet \
run-preview release enable update help
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: ## install system dependencies
@printf " $(BLUE)%-10s$(RST) checking dependencies...\n" "DEPS"
@if ! dpkg -s libpam0g-dev >/dev/null 2>&1; then \
printf " $(YELLOW)%-10s$(RST) sudo apt install -y libpam0g-dev\n" ""; \
sudo apt install -y libpam0g-dev; \
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
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
@if ! dpkg -s build-essential >/dev/null 2>&1; then \
printf " $(YELLOW)%-10s$(RST) sudo apt install -y build-essential\n" ""; \
sudo apt install -y build-essential; \
fi
@if ! command -v go >/dev/null 2>&1; then \
printf " $(RED)%-10s$(RST) go not found — install go 1.22+\n" ""; \
exit 1; \
fi
@printf " $(GREEN)%-10s$(RST) dependencies ok\n" "DEPS"
all: build ## build the release binary
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)'
build: $(BUILD_DIR)/$(BINARY) ## compile latchd
release: check audit docker-test package ## Run all gates and produce release artifacts
$(BUILD_DIR)/$(BINARY): $(shell find $(SRC_DIR) -name '*.go')
@printf " $(CYAN)%-10s$(RST) %s\n" "GO" "$@"
@mkdir -p $(BUILD_DIR)
@cd $(SRC_DIR) && $(GO) build $(GOFLAGS) -o ../$@ .
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: build ## install to $(PREFIX)
@printf " $(GREEN)%-10s$(RST) %s\n" "INSTALL" "$(BINDIR)/$(BINARY)"
@install -Dm755 $(BUILD_DIR)/$(BINARY) $(BINDIR)/$(BINARY)
@install -Dm644 extra/config.toml $(CONFDIR)/config.toml
@install -Dm755 extra/xsetup.sh $(CONFDIR)/xsetup.sh
@install -Dm644 extra/latchd.pam $(DESTDIR)/etc/pam.d/$(BINARY)
@install -Dm644 extra/latchd.service $(SYSTEMD)/$(BINARY).service
@mkdir -p $(CONFDIR)/wms $(CONFDIR)/wayland
@printf " $(GREEN)%-10s$(RST) done — run 'systemctl enable $(BINARY)' to activate\n" "INSTALL"
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)'
uninstall: ## remove installed files
@printf " $(RED)%-10s$(RST) %s\n" "RM" "$(BINDIR)/$(BINARY)"
@rm -f $(BINDIR)/$(BINARY)
@rm -rf $(CONFDIR)
@rm -f $(DESTDIR)/etc/pam.d/$(BINARY)
@rm -f $(SYSTEMD)/$(BINARY).service
clean: ## Remove generated build, release, and Cargo artifacts
@rm -rf '$(BUILD_DIR)' '$(DIST_DIR)'
@$(CARGO) clean
clean: ## remove build artifacts
@printf " $(RED)%-10s$(RST) %s\n" "RM" "$(BUILD_DIR)"
@rm -rf $(BUILD_DIR)
run-preview: build ## Run the safe non-authenticating preview UI
@'$(BUILD_DIR)/$(BINARY)' --preview --no-log
test: ## run tests
@printf " $(YELLOW)%-10s$(RST) running tests...\n" "TEST"
@cd $(SRC_DIR) && $(GO) test ./...
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' .
lint: ## run linter
@printf " $(YELLOW)%-10s$(RST) linting...\n" "LINT"
@golangci-lint run $(SRC_DIR)/... 2>/dev/null || printf " $(DIM)%-10s$(RST) golangci-lint not found; skipping\n" ""
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 ./...
fmt: ## format source
@printf " $(BLUE)%-10s$(RST) formatting...\n" "FMT"
@cd $(SRC_DIR) && $(GO) fmt ./...
enable: ## Enable the systemd service after host validation
@sudo systemctl enable $(PROJECT).service
vet: ## run go vet
@printf " $(YELLOW)%-10s$(RST) vetting...\n" "VET"
@cd $(SRC_DIR) && $(GO) vet ./...
run-preview: build ## launch preview mode
@printf " $(GREEN)%-10s$(RST) starting preview...\n" "RUN"
@./$(BUILD_DIR)/$(BINARY) --preview --no-log
release: clean ## build with version info
@printf " $(BOLD)$(CYAN)╔══════════════════════════════════════╗$(RST)\n"
@printf " $(BOLD)$(CYAN)║ latchd $(VERSION) — release build ║$(RST)\n"
@printf " $(BOLD)$(CYAN)╚══════════════════════════════════════╝$(RST)\n"
@printf "\n"
@printf " $(DIM)commit$(RST) $(COMMIT)\n"
@printf " $(DIM)built$(RST) $(BUILDDATE)\n"
@printf "\n"
@$(MAKE) build
enable: ## disable other DM + enable latchd
@printf " $(YELLOW)%-10s$(RST) switching to latchd...\n" "ENABLE"
@if systemctl is-active display-manager.service >/dev/null 2>&1; then \
printf " $(DIM)%-10s$(RST) stopping current display manager\n" ""; \
sudo systemctl stop display-manager.service; \
fi
@if [ -L /etc/systemd/system/display-manager.service ] \
&& ! readlink /etc/systemd/system/display-manager.service | grep -q $(BINARY); then \
printf " $(DIM)%-10s$(RST) disabling aliased display manager\n" ""; \
sudo systemctl disable display-manager.service; \
fi
@sudo systemctl enable $(BINARY).service
@printf " $(GREEN)%-10s$(RST) latchd enabled — reboot or run 'sudo systemctl start latchd'\n" "ENABLE"
update: ## uninstall → git pull → rebuild → reinstall
@printf " $(YELLOW)%-10s$(RST) removing previous install...\n" "UPDATE"
@sudo $(MAKE) uninstall
@printf " $(CYAN)%-10s$(RST) pulling latest...\n" "UPDATE"
@git pull
@printf " $(BLUE)%-10s$(RST) rebuilding...\n" "UPDATE"
update: ## Rebuild and reinstall the current checkout without changing git state
@sudo $(MAKE) install
@printf " $(GREEN)%-10s$(RST) updated — run 'sudo make enable' to switch DMs\n" "UPDATE"
help: ## print this help
@printf " $(BOLD)latchd$(RST) — tui display manager\n"
@printf " $(DIM)version $(VERSION) commit $(COMMIT)$(RST)\n"
@printf "\n"
@printf " $(WHITE)targets:$(RST)\n"
@awk -F ':|##' '/^[a-zA-Z].*:.*##/ { \
printf " $(CYAN)%-16s$(RST) %s\n", $$1, $$NF }' $(MAKEFILE_LIST) \
| sort
@printf "\n"
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