# Fenris Makefile # Spec: §10.1-10.6 SHELL := /bin/bash PYTHON := python3 VENV_DIR := /opt/fenris VENDOR_DIR := $(VENV_DIR)/vendor BIN_DIR := /usr/local/bin LIBEXEC_DIR := /usr/libexec/fenris UNIT_DIR := /etc/systemd/system POLKIT_DIR := /usr/share/polkit-1/actions CONF_DIR := /etc/fenris DATA_DIR := /var/lib/fenris # Placement manifest MANIFEST := $(DATA_DIR)/manifest.txt # Legacy history path (IN-4) LEGACY_HISTORY := ./data/history.jsonl .PHONY: help install upgrade uninstall purge update-deps test lint check-python check-smartctl import-legacy stage package-deb package-rpm package-xbps package generate-test-key sign-rpm sign-xbps checksums clearsign release release-run release-dry-run xbps-publish xbps-publish-dry-run clean help: @echo "Fenris NVMe endurance monitor" @echo "" @echo "Targets:" @echo " install - Install Fenris (builds wheel, installs to /opt/fenris)" @echo " upgrade - Upgrade Fenris (reinstall wheel, sync units)" @echo " uninstall - Uninstall Fenris (preserves config and store)" @echo " purge - Remove everything including config and store" @echo " test - Run tests" @echo " lint - Run linter" @echo " update-deps - Update dependency pins" @echo " stage - Stage packaging tree for nfpm" @echo " package - Build deb + rpm packages" @echo " package-deb - Build deb package only" @echo " package-rpm - Build rpm package only" @echo " package-xbps - Build XBPS package only" @echo " sign-xbps - Sign XBPS package with signing key" @echo " generate-test-key - Create throwaway GPG key for CI/testing" @echo " sign-rpm - Sign RPM payload with packaging key" @echo " checksums - Generate SHA256SUMS manifest" @echo " clearsign - Clearsign SHA256SUMS with packaging key" @echo " release - Full release (build, sign, checksum, print upload steps)" @echo " release-run - Execute the full release flow via scripts/release.sh" @echo " release-dry-run - Dry-run of the release flow (prints commands only)" @echo " xbps-publish - Publish XBPS package to distribution repository" @echo " xbps-publish-dry-run - Dry-run of XBPS publication (prints commands only)" @echo " clean - Remove build artifacts" # ─── Pre-install gates ────────────────────────────────────────────────────── check-python: @echo "=== Verifying Python ≥ 3.10 ===" @$(PYTHON) -c "import sys; v=sys.version_info; exit(0 if (v>=(3,10)) else 1)" || { echo "Error: Python 3.10+ required (found $$($(PYTHON) --version 2>&1))"; exit 1; } check-smartctl: @echo "=== Verifying smartctl ===" @smartctl --version 2>/dev/null | head -1 || { echo "Error: smartctl not found (install smartmontools)"; exit 1; } # ─── Build ────────────────────────────────────────────────────────────────── dist/fenris-*.whl: pyproject.toml src/fenris/*.py @mkdir -p dist $(PYTHON) -m pip wheel --no-deps --wheel-dir dist . # ─── Install ──────────────────────────────────────────────────────────────── install: check-python check-smartctl dist/fenris-*.whl @echo "=== Creating directories ===" @sudo mkdir -p $(LIBEXEC_DIR) @sudo mkdir -p $(CONF_DIR) @sudo mkdir -p $(POLKIT_DIR) @echo "=== Creating data directory (root-written, group-read) ===" @sudo groupadd -f fenris @sudo install -d -o root -g fenris -m 2750 $(DATA_DIR) @echo "=== Installing version-neutral runtime packages ===" @sudo rm -rf $(VENV_DIR) @sudo install -d -m 0755 $(VENDOR_DIR) @sudo $(PYTHON) -m pip install --disable-pip-version-check --no-compile --target $(VENDOR_DIR) -r requirements.txt dist/fenris-*.whl --quiet @echo "=== Installing wrapper ===" @sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris @echo "=== Installing helpers ===" @sudo install -m 0755 src/fenris/monitor.py $(LIBEXEC_DIR)/fenris-monitor @sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect @echo "=== Installing systemd units (dormant — not enabled/started) ===" @sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/ @sudo install -m 0644 units/fenris-collect.service $(UNIT_DIR)/ @sudo systemctl daemon-reload @echo "=== Installing runit service files (dormant — not enabled) ===" @sudo install -d -m 0755 /etc/sv/fenris-collect/log @sudo install -d -o root -g fenris -m 2770 /var/log/fenris-collect @sudo install -m 0755 units/runit/fenris-collect/run /etc/sv/fenris-collect/run @sudo install -m 0755 units/runit/fenris-collect/log/run /etc/sv/fenris-collect/log/run @sudo touch /etc/sv/fenris-collect/down @echo "=== Installing polkit policy ===" @sudo install -m 0644 polkit/com.bongbetic.fenris.monitor.policy $(POLKIT_DIR)/ @echo "=== Recording manifest (IN-2, IN-10) ===" @echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null @echo "# Generated by: sudo make install" | sudo tee -a $(MANIFEST) > /dev/null @echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/run" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/log/run" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/down" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "/var/log/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null @echo "=== Install complete ===" @echo "Units installed but NOT enabled or started (dormant — IN-3)." @echo "To start monitoring: fenris monitor resume" @$(MAKE) --no-print-directory import-legacy # ─── Legacy import (IN-4, ST-7) ──────────────────────────────────────────── import-legacy: @if [ -f "$(LEGACY_HISTORY)" ]; then \ echo "=== Detected legacy history: $(LEGACY_HISTORY) ==="; \ echo "Running idempotent import..."; \ PYTHONPATH=$(VENDOR_DIR) $(PYTHON) -c "from fenris.legacy import import_legacy_history; from fenris.store import init_store; from pathlib import Path; conn = init_store(Path('$(DATA_DIR)/observations.db')); r = import_legacy_history(conn, Path('$(LEGACY_HISTORY)')); conn.close(); print(f' Samples imported: {r.get(\"samples_imported\", 0)}'); print(f' Hours imported: {r.get(\"hours_imported\", 0)}'); print(f' Malformed lines: {r.get(\"malformed_lines\", 0)}') if not r.get('skipped') else print(' Skipped: already imported')" || echo " Warning: import failed (non-fatal)"; \ else \ echo "=== No legacy history found at $(LEGACY_HISTORY) ==="; \ fi # ─── Upgrade ──────────────────────────────────────────────────────────────── upgrade: dist/fenris-*.whl @echo "=== Upgrading Fenris ===" @echo "=== Snapshotting database (IN-6) ===" @sudo cp $(DATA_DIR)/observations.db $(DATA_DIR)/observations.db.bak 2>/dev/null || true @echo "=== Installing new version-neutral runtime packages ===" @sudo rm -rf $(VENDOR_DIR) @sudo install -d -m 0755 $(VENDOR_DIR) @sudo $(PYTHON) -m pip install --disable-pip-version-check --no-compile --target $(VENDOR_DIR) -r requirements.txt dist/fenris-*.whl --quiet @echo "=== Syncing units against manifest ===" @sudo install -m 0644 units/fenris-collect.timer $(UNIT_DIR)/ @sudo install -m 0644 units/fenris-collect.service $(UNIT_DIR)/ @sudo install -d -m 0755 /etc/sv/fenris-collect/log @sudo groupadd -f fenris @sudo install -d -o root -g fenris -m 2770 /var/log/fenris-collect @sudo install -m 0755 units/runit/fenris-collect/run /etc/sv/fenris-collect/run @sudo install -m 0755 units/runit/fenris-collect/log/run /etc/sv/fenris-collect/log/run @sudo install -m 0644 polkit/com.bongbetic.fenris.monitor.policy $(POLKIT_DIR)/ @sudo install -m 0755 scripts/fenris $(BIN_DIR)/fenris @sudo install -m 0755 src/fenris/monitor.py $(LIBEXEC_DIR)/fenris-monitor @sudo install -m 0755 src/fenris/collect.py $(LIBEXEC_DIR)/fenris-collect @sudo systemctl daemon-reload @echo "=== Updating manifest ===" @echo "# Fenris placement manifest — do not edit" | sudo tee $(MANIFEST) > /dev/null @echo "# Generated by: sudo make upgrade" | sudo tee -a $(MANIFEST) > /dev/null @echo "# Timestamp: $$(date -u +%%Y-%%m-%%dT%%H:%%M:%%SZ)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(BIN_DIR)/fenris" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(LIBEXEC_DIR)/fenris-monitor" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(LIBEXEC_DIR)/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(UNIT_DIR)/fenris-collect.timer" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(UNIT_DIR)/fenris-collect.service" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/run" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/log/run" | sudo tee -a $(MANIFEST) > /dev/null @echo "/etc/sv/fenris-collect/down" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(VENV_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(DATA_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(CONF_DIR)" | sudo tee -a $(MANIFEST) > /dev/null @echo "/var/log/fenris-collect" | sudo tee -a $(MANIFEST) > /dev/null @echo "$(MANIFEST)" | sudo tee -a $(MANIFEST) > /dev/null @echo "=== Restarting timer only if contents changed and active (IN-5) ===" @for unit in fenris-collect.timer fenris-collect.service; do \ TMPFILE=$$(mktemp); \ sudo systemctl cat $$unit > $$TMPFILE 2>/dev/null || true; \ if ! diff -q $$TMPFILE $(UNIT_DIR)/$$unit > /dev/null 2>&1; then \ if systemctl is-active --quiet $$unit; then \ echo " $$unit changed and active — restarting"; \ sudo systemctl restart $$unit; \ fi; \ fi; \ rm -f $$TMPFILE; \ done @echo "=== Applying forward-only schema migrations (IN-5, IN-6) ===" @sudo env PYTHONPATH=$(VENDOR_DIR) $(PYTHON) -c "from fenris.store import migrate_to_latest; from pathlib import Path; n = migrate_to_latest(Path('$(DATA_DIR)/observations.db')); print(f' Migration steps applied: {n}') if n else print(' Schema already current')" @echo "=== Upgrade complete ===" # ─── Uninstall (IN-7) ────────────────────────────────────────────────────── uninstall: @echo "=== Uninstalling Fenris ===" @if [ -x $(LIBEXEC_DIR)/fenris-monitor ]; then \ echo "=== Performing sanctioned disable (§10.4) ==="; \ sudo $(LIBEXEC_DIR)/fenris-monitor disable --now || true; \ fi @echo "=== Stopping units ===" @sudo systemctl stop fenris-collect.timer 2>/dev/null || true @sudo systemctl disable fenris-collect.timer 2>/dev/null || true @sudo systemctl daemon-reload @-sudo rm -f /var/service/fenris-collect 2>/dev/null || true @-sudo rm -rf /etc/sv/fenris-collect 2>/dev/null || true @-sudo rm -rf /var/log/fenris-collect 2>/dev/null || true @echo "=== Removing installed files (preserving config and store) ===" @-rm -f $(BIN_DIR)/fenris @-rm -f $(LIBEXEC_DIR)/fenris-monitor @-rm -f $(LIBEXEC_DIR)/fenris-collect @-rmdir $(LIBEXEC_DIR) 2>/dev/null || true @-rm -f $(UNIT_DIR)/fenris-collect.timer @-rm -f $(UNIT_DIR)/fenris-collect.service @-rm -f $(POLKIT_DIR)/com.bongbetic.fenris.monitor.policy @sudo rm -rf $(VENV_DIR) @-sudo rm -f $(MANIFEST) @echo "=== Uninstall complete ===" @echo "Config preserved at $(CONF_DIR)" @echo "Store preserved at $(DATA_DIR)" # ─── Purge ────────────────────────────────────────────────────────────────── purge: uninstall @echo "=== Purging Fenris ===" @-sudo rm -rf $(CONF_DIR) @-sudo rm -rf $(DATA_DIR) @echo "=== Purge complete ===" # ─── Test ─────────────────────────────────────────────────────────────────── test: $(PYTHON) -m pytest tests/ -v # ─── Lint ─────────────────────────────────────────────────────────────────── lint: $(PYTHON) -m ruff check src/ tests/ # ─── Dependencies (IN-8) ─────────────────────────────────────────────────── update-deps: $(PYTHON) -m pip compile pyproject.toml -o requirements.txt # ─── Packaging (spec §3, §4, §5) ──────────────────────────────────────────── # Version is sourced from pyproject.toml for both formats FENRIS_VERSION := $(shell sed -n 's/^version = "\(.*\)"/\1/p' pyproject.toml) # GPG signing — packaging key UID (spec §4) PACKAGING_KEY ?= packaging@bongbetic.com stage: @echo "=== Staging packaging tree (v$(FENRIS_VERSION)) ===" $(PYTHON) -m pip wheel --no-deps --wheel-dir dist . bash packaging/stage.sh "$(FENRIS_VERSION)" package-deb: stage @echo "=== Building deb package ===" VERSION="$(FENRIS_VERSION)" nfpm pkg -f packaging/nfpm.yaml -p deb -t dist/ @echo "=== deb package built: dist/fenris_$(FENRIS_VERSION)_amd64.deb ===" package-rpm: stage @echo "=== Building rpm package ===" VERSION="$(FENRIS_VERSION)" nfpm pkg -f packaging/nfpm.yaml -p rpm -t dist/ @echo "=== rpm package built: dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm ===" package: package-deb package-rpm @echo "=== Both packages built in dist/ ===" # ─── XBPS packaging (ADR 0008) ───────────────────────────────────────────── # XBPS signing key (separate from SSH authentication key) XBPS_SIGNING_KEY ?= $(HOME)/.ssh/id_xbps XBPS_REVISION ?= 1 package-xbps: stage @echo "=== Building XBPS package ===" cp packaging/xbps/install.sh build/stage/INSTALL cp packaging/xbps/remove.sh build/stage/REMOVE install -D -m 0644 packaging/fenris.conf build/stage/etc/fenris/fenris.conf chmod 755 build/stage/INSTALL build/stage/REMOVE xbps-create -A x86_64 \ -n fenris-$(FENRIS_VERSION)_$(XBPS_REVISION) \ -s "Fenris NVMe wear monitor" \ -S "NVMe wear monitor with persistent TUI" \ -m "Fenris Packaging " \ -H "https://git.bongbetic.com/xavierk/Fenris" \ -l "MIT" \ -D "python3>=3.10 smartmontools>=0" \ -F "/etc/fenris/fenris.conf" \ build/stage @echo "=== XBPS package built: fenris-$(FENRIS_VERSION)_$(XBPS_REVISION).x86_64.xbps ===" sign-xbps: package-xbps @echo "=== Signing XBPS package ===" xbps-rindex --sign-pkg --privkey $(XBPS_SIGNING_KEY) \ fenris-$(FENRIS_VERSION)_$(XBPS_REVISION).x86_64.xbps @echo "=== XBPS package signed ===" xbps-publish: bash scripts/xbps-publish.sh --publish xbps-publish-dry-run: bash scripts/xbps-publish.sh --dry-run # ─── GPG key management ───────────────────────────────────────────────────── generate-test-key: @echo "=== Generating throwaway test GPG key ===" @echo "This key is for CI/testing only — never use for real releases." printf '%%no-protection\nKey-Type: RSA\nKey-Length: 3072\nName-Real: Fenris Packaging (TESTING ONLY)\nName-Email: packaging-test@bongbetic.com\nExpire-Date: 0\n%%commit\n' | \ gpg --batch --gen-key @echo "=== Test key created. Fingerprint: ===" @gpg --fingerprint packaging-test@bongbetic.com # ─── Signing ──────────────────────────────────────────────────────────────── sign-rpm: package-rpm @echo "=== Signing RPM payload ===" @rpm --import packaging/keys/fenris-packaging.asc 2>/dev/null || true rpmsign --addsign --define "_gpg_name $(PACKAGING_KEY)" \ dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm @echo "=== RPM signed ===" @rpm -Kv dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm checksums: package @echo "=== Generating SHA256SUMS ===" cd dist && sha256sum fenris_$(FENRIS_VERSION)_amd64.deb \ fenris-$(FENRIS_VERSION)-1.x86_64.rpm > SHA256SUMS @echo "=== SHA256SUMS written ===" @cat dist/SHA256SUMS clearsign: checksums @echo "=== Clearsigning SHA256SUMS ===" gpg --batch --yes --clearsign --local-user $(PACKAGING_KEY) \ dist/SHA256SUMS @echo "=== SHA256SUMS.asc written ===" # ─── Release (spec §5) ────────────────────────────────────────────────────── release: package sign-rpm clearsign @echo "" @echo "=== Release v$(FENRIS_VERSION) ===" @echo "" @echo "Artifacts:" @ls -la dist/fenris_$(FENRIS_VERSION)_amd64.deb \ dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm \ dist/SHA256SUMS.asc 2>/dev/null @echo "" @echo "Verify signing (manual):" @echo " rpm -Kv dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm" @echo " gpg --verify dist/SHA256SUMS.asc dist/SHA256SUMS" @echo "" @echo "Upload to registry:" @echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\" @echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/bookworm/main/upload'" @echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\" @echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/jammy/main/upload'" @echo " curl -X PUT -u user:token -T dist/fenris_$(FENRIS_VERSION)_amd64.deb \\" @echo " 'https://git.bongbetic.com/api/packages/xavierk/debian/pool/noble/main/upload'" @echo " curl -X PUT -u user:token -T dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm \\" @echo " 'https://git.bongbetic.com/api/packages/xavierk/rpm/fenris/upload'" @echo "" @echo "Create Gitea release with notes and attach:" @echo " dist/fenris_$(FENRIS_VERSION)_amd64.deb" @echo " dist/fenris-$(FENRIS_VERSION)-1.x86_64.rpm" @echo " dist/SHA256SUMS.asc" @echo "" @echo "Key ceremony: delete the private key after upload." @echo " See docs/install/signing-key-ceremony.md" # ─── Automated release flow (issue #52) ────────────────────────────────────── release-run: bash scripts/release.sh --publish release-dry-run: bash scripts/release.sh --dry-run clean: @echo "=== Cleaning build artifacts ===" rm -rf build/stage dist/fenris-*.deb dist/fenris-*.rpm dist/SHA256SUMS*