CARGO ?= cargo
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
DESTDIR ?=
BIN_NAME := nwg-notifications
DBUS_USER_DIR := $(HOME)/.local/share/dbus-1/services
DBUS_SERVICE_NAME := org.freedesktop.Notifications.service
DBUS_SERVICE_TEMPLATE := data/$(DBUS_SERVICE_NAME).in
SONAR_SCANNER ?= /opt/sonar-scanner/bin/sonar-scanner
SONAR_HOST_URL ?= https://sonar.aaru.network
SONAR_TRUSTSTORE ?= /tmp/sonar-truststore.jks
SONAR_TRUSTSTORE_PASSWORD ?= changeit
.PHONY: all build build-release test lint check-tools \
lint-fmt lint-clippy lint-test lint-deny lint-audit \
install install-bin install-dbus uninstall \
upgrade \
sonar clean help
all: build
define HELP_TEXT
Targets:
make build Debug build
make build-release Release build (used by install + upgrade)
make test cargo test + cargo clippy --all-targets
make lint Full local check: fmt + clippy + test + deny + audit
make install Build release + install binary (system-scope) + install-dbus (user-scope)
make install-bin Install binary to $(DESTDIR)$(BINDIR)
make install-dbus Install D-Bus service file to $(DBUS_USER_DIR) — ALWAYS user-scope (no sudo)
make uninstall Remove installed binary (system) and D-Bus service file (user)
make upgrade Resident-aware: capture running args, stop, rebuild, install, restart
make sonar Run SonarQube scan (requires sonar-scanner + .env)
make clean cargo clean
Install-path invocations:
sudo make install # default /usr/local
make install PREFIX=$$HOME/.local BINDIR=$$HOME/.cargo/bin # no-sudo dev
sudo make install PREFIX=/usr # distro-parity
Note: install-dbus runs unprivileged even under `sudo make install` —
it substitutes BIN_PATH into the service file template and drops it
into the invoking user's $(HOME)/.local/share/dbus-1/services/.
endef
export HELP_TEXT
help:
@echo "$$HELP_TEXT"
build:
$(CARGO) build
build-release:
$(CARGO) build --release
test:
$(CARGO) test
$(CARGO) clippy --all-targets
check-tools:
@if ! command -v cargo-deny >/dev/null 2>&1; then \
echo "Installing cargo-deny..."; \
$(CARGO) install cargo-deny; \
fi
@if ! command -v cargo-audit >/dev/null 2>&1; then \
echo "Installing cargo-audit..."; \
$(CARGO) install cargo-audit; \
fi
lint-fmt:
@echo "── Format ──"
$(CARGO) fmt --all --check
lint-clippy:
@echo "── Clippy ──"
$(CARGO) clippy --all-targets -- -D warnings
lint-test:
@echo "── Tests ──"
$(CARGO) test
lint-deny:
@echo "── Cargo Deny (licenses, advisories, bans, sources) ──"
$(CARGO) deny check
lint-audit:
@echo "── Cargo Audit (dependency CVEs) ──"
$(CARGO) audit
lint: check-tools lint-fmt lint-clippy lint-test lint-deny lint-audit
@echo ""
@echo "All local checks passed ✓"
install: build-release install-bin install-dbus
install-bin:
@echo "Installing binary to $(DESTDIR)$(BINDIR)/$(BIN_NAME)"
install -D -m 755 target/release/$(BIN_NAME) "$(DESTDIR)$(BINDIR)/$(BIN_NAME)"
install-dbus:
@TARGET_HOME="$$HOME"; \
if [ -n "$$SUDO_USER" ] && [ "$$(id -u)" -eq 0 ]; then \
TARGET_HOME="$$(getent passwd "$$SUDO_USER" | cut -d: -f6)"; \
test -n "$$TARGET_HOME" || { \
echo "ERROR: cannot resolve home directory for SUDO_USER=$$SUDO_USER"; \
echo " (getent passwd returned nothing — is $$SUDO_USER a real user on this system?)"; \
exit 1; \
}; \
echo "sudo detected — installing D-Bus service file for user $$SUDO_USER (home: $$TARGET_HOME)"; \
fi; \
TARGET_DIR="$$TARGET_HOME/.local/share/dbus-1/services"; \
TARGET_FILE="$$TARGET_DIR/$(DBUS_SERVICE_NAME)"; \
BIN_PATH="$(BINDIR)/$(BIN_NAME)"; \
echo "Installing D-Bus service file to $$TARGET_FILE"; \
echo " (D-Bus Exec path → $$BIN_PATH)"; \
mkdir -p "$$TARGET_DIR" || exit 1; \
sed "s|@BIN_PATH@|$$BIN_PATH|g" "$(DBUS_SERVICE_TEMPLATE)" > "$$TARGET_FILE" || exit 1; \
if [ -n "$$SUDO_USER" ] && [ "$$(id -u)" -eq 0 ]; then \
chown "$$SUDO_USER:" "$$TARGET_FILE" "$$TARGET_DIR" || { \
echo "ERROR: chown to $$SUDO_USER failed; D-Bus user-service would be unmanageable by the target user"; \
exit 1; \
}; \
fi
uninstall:
@echo "Removing binary"
rm -f "$(DESTDIR)$(BINDIR)/$(BIN_NAME)"
@echo "Removing D-Bus service file"
@TARGET_HOME="$$HOME"; \
if [ -n "$$SUDO_USER" ] && [ "$$(id -u)" -eq 0 ]; then \
TARGET_HOME="$$(getent passwd "$$SUDO_USER" | cut -d: -f6)"; \
test -n "$$TARGET_HOME" || { \
echo "ERROR: cannot resolve home directory for SUDO_USER=$$SUDO_USER"; \
echo " (getent passwd returned nothing — D-Bus service file left behind;"; \
echo " remove manually from that user's ~/.local/share/dbus-1/services/)"; \
exit 1; \
}; \
fi; \
rm -f "$$TARGET_HOME/.local/share/dbus-1/services/$(DBUS_SERVICE_NAME)"
@echo "Uninstalled."
upgrade: build-release
@TARGET_USER="$${SUDO_USER:-$$(id -un)}"; \
PGREP_PATTERN="^([^[:space:]]+/)?$(BIN_NAME)([[:space:]]|$$)"; \
RUNNING_PIDS="$$(pgrep -u "$$TARGET_USER" -f "$$PGREP_PATTERN" 2>/dev/null || true)"; \
if [ -n "$$RUNNING_PIDS" ]; then \
INSTALL_TARGET="$(DESTDIR)$(BINDIR)/$(BIN_NAME)"; \
INSTALL_TARGET_REAL="$$(readlink -f "$$INSTALL_TARGET" 2>/dev/null || echo "$$INSTALL_TARGET")"; \
for pid in $$RUNNING_PIDS; do \
RUNNING_EXE="$$(readlink -f "/proc/$$pid/exe" 2>/dev/null)"; \
if [ -z "$$RUNNING_EXE" ]; then \
if [ -d "/proc/$$pid" ]; then \
echo "ERROR: unable to resolve /proc/$$pid/exe for live daemon pid $$pid"; \
echo " (process is alive but its exe symlink is unreadable — refusing to proceed"; \
echo " without install-target validation)"; \
exit 1; \
fi; \
continue; \
fi; \
if [ "$$RUNNING_EXE" != "$$INSTALL_TARGET_REAL" ]; then \
RUNNING_BINDIR="$$(dirname "$$RUNNING_EXE")"; \
echo "ERROR: running daemon (pid $$pid) is installed at"; \
echo " $$RUNNING_EXE"; \
echo " but 'make upgrade' would install to"; \
echo " $$INSTALL_TARGET"; \
echo ""; \
echo " Daemon NOT killed — a prefix-mismatched upgrade would leave"; \
echo " you with a dead notification daemon and no new binary."; \
echo ""; \
echo " Re-run with BINDIR matching the running binary:"; \
echo " make upgrade BINDIR=$$RUNNING_BINDIR"; \
echo " (install-dbus is always user-scope — PREFIX is ignored here"; \
echo " because the D-Bus service file always lands in ~/.local.)"; \
exit 1; \
fi; \
done; \
ARGS_FILE="$$(mktemp)" || exit 1; \
RUNNING_INFO="$$(mktemp)" || exit 1; \
trap 'rm -f "$$ARGS_FILE" "$$RUNNING_INFO"' EXIT; \
for pid in $$RUNNING_PIDS; do \
START_TIME="$$(sed 's/.*) //' "/proc/$$pid/stat" 2>/dev/null | awk '{print $$20}' || true)"; \
test -n "$$START_TIME" || continue; \
if ! DUMP_OUT="$$(target/release/$(BIN_NAME) --dump-args "$$pid" 2>/dev/null)"; then \
ACTUAL_START="$$(sed 's/.*) //' "/proc/$$pid/stat" 2>/dev/null | awk '{print $$20}' || true)"; \
ACTUAL_EXE="$$(readlink -f "/proc/$$pid/exe" 2>/dev/null || true)"; \
if [ -n "$$ACTUAL_START" ] && [ "$$ACTUAL_START" = "$$START_TIME" ] && \
[ "$$ACTUAL_EXE" = "$$INSTALL_TARGET_REAL" ]; then \
echo "ERROR: --dump-args failed for live daemon pid $$pid"; \
exit 1; \
fi; \
continue; \
fi; \
printf "%s\t%s\n" "$$pid" "$$DUMP_OUT" >> "$$ARGS_FILE" || exit 1; \
echo "$$pid $$START_TIME" >> "$$RUNNING_INFO" || exit 1; \
done; \
$(MAKE) install-bin install-dbus || exit 1; \
VALIDATED_PIDS=""; \
while IFS=' ' read -r pid start_time; do \
ACTUAL_START="$$(sed 's/.*) //' "/proc/$$pid/stat" 2>/dev/null | awk '{print $$20}' || true)"; \
if [ -n "$$ACTUAL_START" ] && [ "$$ACTUAL_START" = "$$start_time" ]; then \
kill "$$pid" 2>/dev/null || true; \
VALIDATED_PIDS="$$VALIDATED_PIDS $$pid"; \
else \
echo "Skipping pid $$pid — no longer our daemon (starttime changed or process exited between capture and kill)"; \
fi; \
done < "$$RUNNING_INFO"; \
if [ -n "$$VALIDATED_PIDS" ]; then \
echo "Sent SIGTERM to daemon(s) for $$TARGET_USER:$$VALIDATED_PIDS"; \
sleep 1; \
STILL_RUNNING=""; \
for pid in $$VALIDATED_PIDS; do \
START_TIME="$$(grep "^$$pid " "$$RUNNING_INFO" | awk '{print $$2}')"; \
ACTUAL_START="$$(sed 's/.*) //' "/proc/$$pid/stat" 2>/dev/null | awk '{print $$20}' || true)"; \
if [ -n "$$ACTUAL_START" ] && [ "$$ACTUAL_START" = "$$START_TIME" ]; then \
kill -9 "$$pid" 2>/dev/null || true; \
STILL_RUNNING="$$STILL_RUNNING $$pid"; \
fi; \
done; \
if [ -n "$$STILL_RUNNING" ]; then \
echo "Escalated to SIGKILL:$$STILL_RUNNING"; \
sleep 1; \
FINAL_ALIVE=""; \
for pid in $$STILL_RUNNING; do \
START_TIME="$$(grep "^$$pid " "$$RUNNING_INFO" | awk '{print $$2}')"; \
ACTUAL_START="$$(sed 's/.*) //' "/proc/$$pid/stat" 2>/dev/null | awk '{print $$20}' || true)"; \
if [ -n "$$ACTUAL_START" ] && [ "$$ACTUAL_START" = "$$START_TIME" ]; then \
FINAL_ALIVE="$$FINAL_ALIVE $$pid"; \
fi; \
done; \
test -z "$$FINAL_ALIVE" || { \
echo "ERROR: failed to stop$$FINAL_ALIVE after SIGKILL; binary installed but daemon still holds old mmap"; \
exit 1; \
}; \
fi; \
fi; \
if [ -n "$$VALIDATED_PIDS" ] && [ -s "$$ARGS_FILE" ]; then \
if [ "$$(id -u)" -eq 0 ]; then \
echo "Refusing to replay captured daemon args as root — D-Bus sessions"; \
echo "are per-user; running the daemon in root context won't receive"; \
echo "notifications from the desktop session anyway. Install finished;"; \
echo "restart the daemon manually from your desktop session (or let"; \
echo "D-Bus auto-activate it on the next notify-send)."; \
else \
for pid in $$VALIDATED_PIDS; do \
args="$$(awk -v p="$$pid" 'BEGIN{FS="\t"} $$1==p{sub(/^[^\t]*\t/, ""); print; exit}' "$$ARGS_FILE")"; \
test -n "$$args" || continue; \
echo "Restarting with captured args: $$args"; \
setsid sh -c "$$args" </dev/null >/dev/null 2>&1 & \
done; \
fi; \
fi; \
else \
echo "No running daemon for $$TARGET_USER — installing; next notify-send D-Bus-activates the new binary"; \
$(MAKE) install-bin install-dbus || exit 1; \
fi
@echo "Upgrade complete."
sonar:
@echo "Running SonarQube scan..."
@test -f ./.env || { echo "ERROR: .env not found in repo root"; exit 1; }
@command -v "$(SONAR_SCANNER)" >/dev/null 2>&1 || [ -x "$(SONAR_SCANNER)" ] || { \
echo "ERROR: sonar-scanner not found (looked at $(SONAR_SCANNER))"; exit 1; \
}
@test -r "$(SONAR_TRUSTSTORE)" || { \
echo "ERROR: truststore not found or not readable at $(SONAR_TRUSTSTORE)"; \
echo " (sonar.aaru.network uses a self-signed cert — regenerate with:"; \
echo " openssl s_client -connect sonar.aaru.network:443 -showcerts </dev/null 2>/dev/null \\\\"; \
echo " | awk '/BEGIN CERT/,/END CERT/' > /tmp/sonar-cert.pem && \\\\"; \
echo " keytool -importcert -alias sonar-aaru -file /tmp/sonar-cert.pem \\\\"; \
echo " -keystore $(SONAR_TRUSTSTORE) -storepass $(SONAR_TRUSTSTORE_PASSWORD) -noprompt)"; \
exit 1; \
}
@TOKEN="$$(awk '/^SONAR_TOKEN=/{sub(/^[^=]*=[ \t]*/, ""); sub(/[ \t]+$$/, ""); print; exit}' ./.env)"; \
test -n "$$TOKEN" || { echo "ERROR: SONAR_TOKEN is empty in .env"; exit 1; }; \
SONAR_TOKEN="$$TOKEN" \
SONAR_SCANNER_OPTS="-Djavax.net.ssl.trustStore=$(SONAR_TRUSTSTORE) -Djavax.net.ssl.trustStorePassword=$(SONAR_TRUSTSTORE_PASSWORD)" \
"$(SONAR_SCANNER)" -Dsonar.host.url="$(SONAR_HOST_URL)"
clean:
$(CARGO) clean