# Makefile for duet.el # Test targets delegate to tests/Makefile. # setup / deps / compile / coverage / complexity / doctor operate at project root. # Run 'make help' for available commands. EASK ?= eask EMACS_BATCH = $(EASK) emacs --batch # Coverage / test loops need default-directory = tests/ so test files' # relative paths (../duet.el, sibling test files) resolve as they do under # tests/Makefile. EMACS_BATCH_TESTS = $(EASK) emacs --batch --eval '(cd "tests/")' TEST_DIR = tests SOURCE_FILE = duet.el # Transport CLIs DUET shells out to. rsync is the stage-1 default; rclone, # lftp, and unison arrive in later stages but `deps' installs the full set so # a contributor's environment is ready ahead of the code that uses them. TRANSPORT_CLIS = rsync rclone lftp unison # Cyclomatic-complexity gate (scripts/duet-complexity.el). The budget is soft # (the design spec allows a written justification past it); override per run # with `make complexity COMPLEXITY_THRESHOLD=12'. COMPLEXITY_THRESHOLD ?= 10 COMPLEXITY_FILES = $(SOURCE_FILE) # Coverage configuration COVERAGE_DIR = .coverage COVERAGE_FILE = $(COVERAGE_DIR)/simplecov.json # Test-file list used by the coverage loop, mirroring tests/Makefile. # Coverage runs every test file so the report represents the full suite. ALL_TESTS = $(filter-out $(TEST_DIR)/test-bootstrap.el, \ $(wildcard $(TEST_DIR)/test-*.el)) # Include local overrides if present (per-machine knobs, not committed) -include makefile-local .PHONY: help test test-all test-unit test-integration test-file test-one test-name \ count list validate lint check-deps clean clean-compiled clean-tests \ setup deps deps-elisp deps-system compile coverage coverage-summary coverage-clean \ complexity doctor test-live help: @$(MAKE) -C $(TEST_DIR) help # Test target delegations test: @$(MAKE) -C $(TEST_DIR) test test-all: @$(MAKE) -C $(TEST_DIR) test-all test-unit: @$(MAKE) -C $(TEST_DIR) test-unit test-integration: @$(MAKE) -C $(TEST_DIR) test-integration test-file: @$(MAKE) -C $(TEST_DIR) test-file FILE="$(FILE)" test-one: @$(MAKE) -C $(TEST_DIR) test-one TEST="$(TEST)" test-name: @$(MAKE) -C $(TEST_DIR) test-name TEST="$(TEST)" count: @$(MAKE) -C $(TEST_DIR) count list: @$(MAKE) -C $(TEST_DIR) list validate: @$(MAKE) -C $(TEST_DIR) validate lint: @$(MAKE) -C $(TEST_DIR) lint check-deps: @$(MAKE) -C $(TEST_DIR) check-deps # # Dependencies # # Install everything DUET needs: Emacs Lisp deps (via eask) plus the transport # CLIs (via the system package manager). deps: deps-elisp deps-system # Install runtime + development Emacs Lisp dependencies via eask. deps-elisp setup: @if ! command -v $(EASK) >/dev/null 2>&1; then \ echo "[✗] eask not found on PATH"; \ echo " Install: npm install -g @emacs-eask/cli"; \ echo " Or: https://emacs-eask.github.io/Getting-Started/Install-Eask/"; \ exit 1; \ fi @echo "[i] Installing Emacs Lisp dependencies via eask..." @$(EASK) install-deps --dev @echo "[✓] Emacs Lisp dependencies installed in .eask/" # Install the transport CLIs via the detected system package manager. pacman # is checked first (the maintainer's environment); apt/dnf/zypper/brew follow. # An unknown manager is not a failure — it prints the package list so the user # can install by hand. deps-system: @echo "[i] Installing transport CLIs: $(TRANSPORT_CLIS)" @if command -v pacman >/dev/null 2>&1; then \ sudo pacman -S --needed $(TRANSPORT_CLIS); \ elif command -v apt-get >/dev/null 2>&1; then \ sudo apt-get install -y $(TRANSPORT_CLIS); \ elif command -v dnf >/dev/null 2>&1; then \ sudo dnf install -y $(TRANSPORT_CLIS); \ elif command -v zypper >/dev/null 2>&1; then \ sudo zypper install -y $(TRANSPORT_CLIS); \ elif command -v brew >/dev/null 2>&1; then \ brew install $(TRANSPORT_CLIS); \ else \ echo "[!] No supported package manager found."; \ echo " Install these yourself: $(TRANSPORT_CLIS)"; \ fi # Byte-compile duet.el — surfaces free-variable / unused-let / suspicious-call # warnings that checkdoc and elisp-lint don't catch. byte-compile-error-on-warn # is t so any warning fails the build. compile: @echo "[i] Byte-compiling $(SOURCE_FILE)..." @$(EMACS_BATCH) \ --eval "(progn \ (setq byte-compile-error-on-warn t) \ (batch-byte-compile))" $(SOURCE_FILE) @echo "[✓] Compilation complete" # # Cyclomatic complexity (scripts/duet-complexity.el) # # Report per-function complexity and exit non-zero if any function exceeds the # soft budget. Advisory: run on demand, not wired into `make test'. complexity: @echo "[i] Scanning complexity (budget = $(COMPLEXITY_THRESHOLD))..." @emacs -Q --batch -L scripts -l duet-complexity \ --eval "(duet-complexity-batch '($(foreach f,$(COMPLEXITY_FILES),\"$(f)\")) $(COMPLEXITY_THRESHOLD))" # # Doctor — verify the runtime environment # # Report which transport CLIs are present and confirm duet loads. Missing CLIs # are warnings (rclone/lftp/unison land in later stages); a load failure is the # only hard error. doctor: @echo "[i] DUET doctor" @echo "Transport executables:" @for c in $(TRANSPORT_CLIS); do \ if command -v $$c >/dev/null 2>&1; then \ printf " [✓] %-8s %s\n" "$$c" "$$(command -v $$c)"; \ else \ printf " [!] %-8s missing (install via 'make deps-system')\n" "$$c"; \ fi; \ done @echo "Package load:" @if $(EMACS_BATCH) -l $(SOURCE_FILE) --eval "(require 'duet)" >/dev/null 2>&1; then \ echo " [✓] (require 'duet) succeeds"; \ else \ echo " [✗] duet failed to load"; \ exit 1; \ fi # Run env-gated live remote tests. Skipped unless DUET_LIVE_TESTS is set, since # they need real remote hosts and credentials. test-live: @if [ -z "$$DUET_LIVE_TESTS" ]; then \ echo "[i] DUET_LIVE_TESTS is unset — skipping live remote tests."; \ echo " Set DUET_LIVE_TESTS=1 (and the host/path vars the tests document) to run them."; \ else \ $(MAKE) -C $(TEST_DIR) test-name TEST='live'; \ fi # # Coverage (undercover + simplecov JSON) # # Each test file runs in its own Emacs process (matching test-unit); # tests/run-coverage-file.el instruments duet.el before the source is loaded, # and undercover merges per-file results into a single simplecov JSON. coverage: coverage-clean $(COVERAGE_DIR) @echo "[i] Cleaning .elc files so undercover can instrument source..." @find . -name "*.elc" -delete @echo "[i] Running coverage across $(words $(ALL_TESTS)) test file(s)..." @echo " (slower than 'make test' — each file runs in its own Emacs)" @failed=0; \ for test in $(ALL_TESTS); do \ echo " Coverage: $$test..."; \ testfile=$$(basename $$test); \ $(EMACS_BATCH_TESTS) \ -l ert \ -l run-coverage-file.el \ -l ../$(SOURCE_FILE) \ -l $$testfile \ --eval "(ert-run-tests-batch-and-exit t)" || failed=$$((failed + 1)); \ done; \ if [ $$failed -gt 0 ]; then \ echo "[!] $$failed test file(s) failed during coverage run"; \ exit 1; \ fi @coverage_file="$${COVERAGE_FILE_ACTUAL:-$(COVERAGE_FILE)}"; \ [ -n "$$CI" ] && coverage_file="$(COVERAGE_DIR)/coveralls.json"; \ if [ -f "$$coverage_file" ]; then \ echo "[✓] Coverage report: $$coverage_file ($$(du -h $$coverage_file | cut -f1))"; \ else \ echo "[!] No coverage file produced; check that undercover is installed"; \ exit 1; \ fi @# Print the human-readable summary after a local run. CI emits @# coveralls.json (not simplecov.json) and the upload action reports @# instead, so skip the terminal summary there. @if [ -z "$$CI" ] && [ -f $(COVERAGE_FILE) ]; then \ $(MAKE) --no-print-directory coverage-summary; \ fi # Print a human-readable summary of the SimpleCov report. coverage-summary: @if [ ! -f $(COVERAGE_FILE) ]; then \ echo "[!] No coverage report at $(COVERAGE_FILE). Run 'make coverage' first."; \ exit 1; \ fi @$(EMACS_BATCH) -L scripts -l coverage-summary \ --eval '(duet-coverage-print-summary "$(COVERAGE_FILE)" (list "$(SOURCE_FILE)") "$(CURDIR)")' coverage-clean: @rm -f $(COVERAGE_FILE) $(COVERAGE_DIR): @mkdir -p $(COVERAGE_DIR) # # Cleaning # clean: clean-compiled clean-tests @rm -rf $(COVERAGE_DIR) clean-compiled: @rm -f *.elc scripts/*.elc clean-tests: @$(MAKE) -C $(TEST_DIR) clean