SHELL := /bin/bash
# Detect host architecture for musl target
MUSL_TARGET := $(shell uname -m | sed 's/x86_64/x86_64-unknown-linux-musl/' | sed 's/aarch64/aarch64-unknown-linux-musl/')
.PHONY: build build-debug build-static test test-unit test-e2e test-e2e-all test-e2e-browser test-screenshots test-perf test-e2e-podman test-compose test-compose-validate coverage run fmt clippy check docker-build docker-build-scratch docker-build-all docker-build-cross docker-build-multiarch docker-build-package docker-scan docker-build-scan docker-run docker-run-scratch compose-up compose-down hash-password clean security-scan test-all quick-start init completions manpage bench changelog install-act install-hooks ensure-podman-socket ci-lint ci-test ci-docker-lint ci-e2e ci push push-all release validate validate-docker validate-plain clean-validation validate-ci validate-msrv validate-coverage validate-security setup check-ci-sync
build:
cargo build --release
build-debug:
cargo build
build-static:
rustup target add $(MUSL_TARGET) 2>/dev/null || true
cargo build --release --target $(MUSL_TARGET)
@echo "Static binary: target/$(MUSL_TARGET)/release/sks5"
test:
cargo test --all-targets
test-unit:
cargo test --lib
test-e2e:
cargo test --test '*'
test-e2e-all:
cargo test --test '*'
test-e2e-browser:
@command -v podman >/dev/null 2>&1 || { echo "Error: podman is required for browser E2E tests"; exit 1; }
@podman image exists docker.io/chromedp/headless-shell:latest 2>/dev/null || \
{ echo "Pulling chromedp/headless-shell..."; podman pull docker.io/chromedp/headless-shell:latest; }
@status=0; \
cargo test --test e2e_browser_dashboard -- --nocapture || status=$$?; \
podman ps -aq --filter "name=sks5-chrome" | xargs -r podman stop 2>/dev/null || true; \
exit $$status
test-screenshots:
@command -v podman >/dev/null 2>&1 || { echo "Error: podman is required for screenshot tests"; exit 1; }
@podman image exists docker.io/chromedp/headless-shell:latest 2>/dev/null || \
{ echo "Pulling chromedp/headless-shell..."; podman pull docker.io/chromedp/headless-shell:latest; }
@mkdir -p screenshots
@status=0; \
SCREENSHOT_DIR=screenshots cargo test --test e2e_browser_screenshots -- --nocapture || status=$$?; \
podman ps -aq --filter "name=sks5-chrome" | xargs -r podman stop 2>/dev/null || true; \
exit $$status
test-perf:
cargo test --test e2e_performance -- --nocapture
test-e2e-podman:
./scripts/test-e2e-podman.sh
test-compose:
./scripts/test-compose.sh
test-compose-validate:
podman-compose config
test-all: test security-scan
coverage:
cargo llvm-cov --all-targets
run:
cargo run -- --config config.example.toml
fmt:
cargo fmt
clippy:
cargo clippy --all-targets -- -D warnings
check:
cargo check --all-targets
security-scan:
./scripts/security-scan.sh
validate-msrv:
@rustup toolchain list | grep -q '^1\.88' || rustup toolchain install 1.88
cargo +1.88 check
validate-coverage:
@rustup component add llvm-tools-preview 2>/dev/null || true
@command -v cargo-llvm-cov >/dev/null 2>&1 || cargo install cargo-llvm-cov --locked
cargo llvm-cov --lcov --output-path lcov.info --lib --test unit
validate-security:
@cargo audit
@cargo deny check
docker-build:
podman build -f Containerfile.alpine -t sks5:latest -t sks5:alpine .
docker-build-scratch:
podman build -t sks5:scratch .
docker-build-all: docker-build docker-build-scratch
@echo "Built sks5:latest (alpine, default) and sks5:scratch"
docker-scan: ensure-podman-socket
@command -v trivy >/dev/null 2>&1 || { echo "Install trivy: https://trivy.dev"; exit 1; }
trivy image --image-src podman --exit-code 1 --severity CRITICAL,HIGH,MEDIUM --ignorefile .trivyignore sks5:latest
trivy image --image-src podman --exit-code 1 --severity CRITICAL,HIGH,MEDIUM --ignorefile .trivyignore sks5:scratch
@if command -v grype >/dev/null 2>&1; then \
grype podman:sks5:latest --fail-on medium -c .grype.yaml; \
grype podman:sks5:scratch --fail-on medium -c .grype.yaml; \
else \
echo " -- grype not installed (skipping)"; \
fi
docker-build-scan: docker-build-all docker-scan
docker-build-cross:
./scripts/build-multiarch-cross.sh
docker-build-multiarch:
./scripts/build-multiarch-qemu.sh
docker-build-package:
@echo "Building multi-arch image from pre-built binaries..."
@test -f binaries/amd64/sks5 || { echo "Error: binaries/amd64/sks5 not found. Run 'make build-static' first."; exit 1; }
@test -f binaries/arm64/sks5 || { echo "Error: binaries/arm64/sks5 not found. Cross-compile for aarch64 first."; exit 1; }
podman build -f Containerfile.package --target alpine -t sks5:latest -t sks5:alpine .
podman build -f Containerfile.package --target minimal -t sks5:scratch .
docker-run:
podman run --rm -p 2222:2222 -p 1080:1080 \
-v ./config.example.toml:/etc/sks5/config.toml:ro sks5:latest
docker-run-scratch:
podman run --rm -p 2222:2222 -p 1080:1080 \
-v ./config.example.toml:/etc/sks5/config.toml:ro sks5:scratch
compose-up:
podman-compose up -d
compose-down:
podman-compose down
hash-password:
@read -sp "Enter password: " pass && echo && \
hash=$$(cargo run --quiet -- hash-password --password "$$pass") && \
echo "" && \
echo "password_hash = \"$$hash\""
quick-start:
cargo run -- quick-start --password demo
init:
cargo run -- init --password demo --output config.toml
completions:
@mkdir -p completions
cargo run --quiet -- completions bash > completions/sks5.bash
cargo run --quiet -- completions zsh > completions/_sks5
cargo run --quiet -- completions fish > completions/sks5.fish
@echo "Shell completions generated in completions/"
manpage:
@mkdir -p man
cargo run --quiet -- manpage > man/sks5.1
@echo "Man page generated: man/sks5.1"
bench:
cargo bench
changelog:
git-cliff --output CHANGELOG.md
clean:
cargo clean
# ===========================================================================
# Local CI with act + Podman
# ===========================================================================
setup:
@echo "Installing all development tools..."
@mkdir -p ~/.local/bin
@# act (local CI runner)
@command -v act >/dev/null 2>&1 && echo " ok act" || \
{ echo " .. act"; curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s -- -b ~/.local/bin 2>/dev/null && echo " ok act (installed)" || echo " !! act (failed)"; }
@# cargo tools
@command -v cargo-audit >/dev/null 2>&1 && echo " ok cargo-audit" || \
{ echo " .. cargo-audit"; cargo install cargo-audit --locked 2>/dev/null && echo " ok cargo-audit (installed)" || echo " !! cargo-audit (failed)"; }
@command -v cargo-deny >/dev/null 2>&1 && echo " ok cargo-deny" || \
{ echo " .. cargo-deny"; cargo install cargo-deny --locked 2>/dev/null && echo " ok cargo-deny (installed)" || echo " !! cargo-deny (failed)"; }
@command -v cargo-llvm-cov >/dev/null 2>&1 && echo " ok cargo-llvm-cov" || \
{ echo " .. cargo-llvm-cov"; rustup component add llvm-tools-preview 2>/dev/null; cargo install cargo-llvm-cov --locked 2>/dev/null && echo " ok cargo-llvm-cov (installed)" || echo " !! cargo-llvm-cov (failed)"; }
@# MSRV toolchain
@rustup toolchain list 2>/dev/null | grep -q '^1\.88' && echo " ok MSRV 1.88" || \
{ echo " .. MSRV 1.88"; rustup toolchain install 1.88 2>/dev/null && echo " ok MSRV 1.88 (installed)" || echo " !! MSRV 1.88 (failed)"; }
@# trivy: binary install → podman wrapper → docker wrapper
@command -v trivy >/dev/null 2>&1 && echo " ok trivy (native)" || \
{ echo " .. trivy"; \
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh 2>/dev/null | sh -s -- -b ~/.local/bin 2>/dev/null \
&& echo " ok trivy (installed)" \
|| { if command -v podman >/dev/null 2>&1; then \
echo " .. trivy binary failed, creating podman wrapper"; \
printf '#!/bin/sh\nexec podman run --rm -v "$${XDG_RUNTIME_DIR}/podman/podman.sock:/var/run/docker.sock:ro" ghcr.io/aquasecurity/trivy:latest "$$@"\n' > ~/.local/bin/trivy \
&& chmod +x ~/.local/bin/trivy \
&& echo " ok trivy (podman wrapper)"; \
elif command -v docker >/dev/null 2>&1; then \
echo " .. trivy binary failed, creating docker wrapper"; \
printf '#!/bin/sh\nexec docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro ghcr.io/aquasecurity/trivy:latest "$$@"\n' > ~/.local/bin/trivy \
&& chmod +x ~/.local/bin/trivy \
&& echo " ok trivy (docker wrapper)"; \
else \
echo " !! trivy (no binary, no podman, no docker)"; \
fi; }; }
@# vhs: go install → podman wrapper → docker wrapper
@command -v vhs >/dev/null 2>&1 && echo " ok vhs (native)" || \
{ echo " .. vhs"; \
if command -v go >/dev/null 2>&1; then \
go install github.com/charmbracelet/vhs@latest 2>/dev/null && echo " ok vhs (installed)" && exit 0; \
fi; \
if command -v podman >/dev/null 2>&1; then \
echo " .. creating podman wrapper"; \
printf '#!/bin/sh\nexec podman run --rm -v "$$PWD:/vhs" ghcr.io/charmbracelet/vhs "$$@"\n' > ~/.local/bin/vhs \
&& chmod +x ~/.local/bin/vhs \
&& echo " ok vhs (podman wrapper)"; \
elif command -v docker >/dev/null 2>&1; then \
echo " .. creating docker wrapper"; \
printf '#!/bin/sh\nexec docker run --rm -v "$$PWD:/vhs" ghcr.io/charmbracelet/vhs "$$@"\n' > ~/.local/bin/vhs \
&& chmod +x ~/.local/bin/vhs \
&& echo " ok vhs (docker wrapper)"; \
else \
echo " !! vhs (no binary, no podman, no docker)"; \
fi; }
@# grype: binary install → podman wrapper → docker wrapper
@command -v grype >/dev/null 2>&1 && echo " ok grype (native)" || \
{ echo " .. grype"; \
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh 2>/dev/null | sh -s -- -b ~/.local/bin 2>/dev/null \
&& echo " ok grype (installed)" \
|| { if command -v podman >/dev/null 2>&1; then \
echo " .. grype binary failed, creating podman wrapper"; \
printf '#!/bin/sh\nexec podman run --rm -v "$${XDG_RUNTIME_DIR}/podman/podman.sock:/var/run/docker.sock:ro" -v "$$PWD:$$PWD:ro" -w "$$PWD" docker.io/anchore/grype:latest "$$@"\n' > ~/.local/bin/grype \
&& chmod +x ~/.local/bin/grype \
&& echo " ok grype (podman wrapper)"; \
elif command -v docker >/dev/null 2>&1; then \
echo " .. grype binary failed, creating docker wrapper"; \
printf '#!/bin/sh\nexec docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro -v "$$PWD:$$PWD:ro" -w "$$PWD" docker.io/anchore/grype:latest "$$@"\n' > ~/.local/bin/grype \
&& chmod +x ~/.local/bin/grype \
&& echo " ok grype (docker wrapper)"; \
else \
echo " !! grype (no binary, no podman, no docker)"; \
fi; }; }
@# JetBrains Mono font (required by VHS tapes)
@fc-list | grep -qi "JetBrains Mono" && echo " ok JetBrains Mono" || \
{ echo " .. JetBrains Mono"; \
mkdir -p ~/.local/share/fonts \
&& curl -fsSL "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip" -o /tmp/JetBrainsMono.zip \
&& unzip -oqj /tmp/JetBrainsMono.zip "fonts/ttf/*.ttf" -d ~/.local/share/fonts/ \
&& fc-cache -f ~/.local/share/fonts/ \
&& rm -f /tmp/JetBrainsMono.zip \
&& echo " ok JetBrains Mono (installed)" \
|| echo " !! JetBrains Mono (failed — install manually: https://www.jetbrains.com/lp/mono/)"; }
@# Install git hooks
@$(MAKE) --no-print-directory install-hooks
@echo ""
@echo "Done. Re-run 'make validate-docker' to verify."
install-hooks:
@echo " .. git hooks"
@mkdir -p .git/hooks
@for hook in scripts/hooks/*; do \
name=$$(basename "$$hook"); \
ln -sf "../../$$hook" ".git/hooks/$$name"; \
done
@echo " ok git hooks (symlinked)"
@git config --local core.sshCommand "ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=20"
@echo " ok SSH keepalive (core.sshCommand)"
install-act:
@echo "Installing act to ~/.local/bin..."
@mkdir -p ~/.local/bin
@curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh | bash -s -- -b ~/.local/bin
@echo "act installed: $$(~/.local/bin/act --version)"
ensure-podman-socket:
@systemctl --user is-active podman.socket >/dev/null 2>&1 || \
{ echo "Starting Podman socket..."; systemctl --user start podman.socket; }
@test -S "$${XDG_RUNTIME_DIR}/podman/podman.sock" || \
{ echo "Error: Podman socket not found at $${XDG_RUNTIME_DIR}/podman/podman.sock"; exit 1; }
ci-lint: ensure-podman-socket
DOCKER_HOST="unix://$${XDG_RUNTIME_DIR}/podman/podman.sock" \
act push -j lint \
--container-daemon-socket "$${XDG_RUNTIME_DIR}/podman/podman.sock" \
--eventpath .github/act-event.json \
--bind \
--env RUST_BACKTRACE=1 \
--env CARGO_TARGET_DIR=/tmp/act-target
ci-test: ensure-podman-socket
DOCKER_HOST="unix://$${XDG_RUNTIME_DIR}/podman/podman.sock" \
act push -j test \
--container-daemon-socket "$${XDG_RUNTIME_DIR}/podman/podman.sock" \
--eventpath .github/act-event.json \
--bind \
--env RUST_BACKTRACE=1 \
--env CARGO_TARGET_DIR=/tmp/act-target
ci-docker-lint: ensure-podman-socket
DOCKER_HOST="unix://$${XDG_RUNTIME_DIR}/podman/podman.sock" \
act push -j docker-lint \
--container-daemon-socket "$${XDG_RUNTIME_DIR}/podman/podman.sock" \
--eventpath .github/act-event.json \
--bind
ci-e2e: ensure-podman-socket
DOCKER_HOST="unix://$${XDG_RUNTIME_DIR}/podman/podman.sock" \
act push -j e2e-tests \
--container-daemon-socket "$${XDG_RUNTIME_DIR}/podman/podman.sock" \
--eventpath .github/act-event.json \
--bind \
--env RUST_BACKTRACE=1 \
--env CARGO_TARGET_DIR=/tmp/act-target
ci: ci-lint ci-test ci-e2e ci-docker-lint
@echo "Local CI passed (lint + test + e2e + docker-lint)"
check-ci-sync:
@./scripts/check-ci-test-sync.sh
push:
@./scripts/validate.sh --with-docker --skip-vhs --skip-browser
@git push --no-verify
push-all:
@./scripts/validate.sh --with-docker --skip-vhs --skip-browser
@git push --follow-tags --no-verify
release:
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=x.y.z"; exit 1; }
@sed -i '0,/^version = ".*"/s//version = "$(VERSION)"/' Cargo.toml
@cargo check --quiet 2>/dev/null || true
@git add Cargo.toml Cargo.lock
@git commit -m "chore: bump version to $(VERSION)"
@git tag "v$(VERSION)"
@./scripts/validate.sh --with-docker --skip-vhs --skip-browser \
|| { echo "!! Validation failed — rolling back"; git tag -d "v$(VERSION)"; git reset --soft HEAD~1; exit 1; }
@git push --follow-tags --no-verify
@echo "Released v$(VERSION)"
validate:
@./scripts/validate.sh
validate-docker:
@./scripts/validate.sh --with-docker $(VALIDATE_EXTRA)
validate-plain:
@./scripts/validate.sh --plain
clean-validation:
@echo "Removing parallel validation target dirs..."
rm -rf target-cov target-msrv
@echo "Done."
# Sequential CI reproduction (kept for backwards compatibility)
validate-ci: ci-lint ci-test ci-e2e ci-docker-lint
@echo "CI reproduction passed (via act)"