GNUmakefile
Rama main · réplica de lectura del repositorio original.
# GNUmakefile for vpn-switch
# Compatible with FreeBSD and Debian/Linux (uses GNU make features — on
# FreeBSD install 'devel/gmake' and invoke as 'gmake').
#
# Usage:
# make - Show help
# make platform - Set repo platform symlink (do this after clone)
# make test - Run all tests
# make install - Install to PREFIX (default: /usr/local)
# make install PREFIX=/usr - Install to /usr
# make uninstall - Remove installed files
#
# For development:
# make metadata - Regenerate function metadata
# make check - Syntax check
# make shellcheck - Run shellcheck linter
# make lint - Run all linters (check + shellcheck)
# make clean - Remove build artifacts
SHELL := /bin/sh
#-----------------------------------------------------------------------------
# Installation paths (FHS-compliant)
#-----------------------------------------------------------------------------
# Base installation prefix
PREFIX ?= /usr/local
# Staging directory (for package building)
DESTDIR ?=
# Binary directory (main script; user-runnable, never needs root).
# /bin (not /sbin): vpn-switch itself does not require privileges —
# privilege escalation happens per-function via interpreter directives
# (sudo sh) only for the network operations that need it.
BINDIR := $(PREFIX)/bin
# System binary directory (root-only helpers).
# resolvconf-up/down scripts that mutate DNS and reload local_unbound live
# here. Phase scripts call them via their absolute /usr/local/sbin paths.
SBINDIR := $(PREFIX)/sbin
# Package-private library directory (include/, template/).
# Holds both shell modules and template/data — see INSTALL.md for the FHS
# rationale (package-private trees are an accepted FHS pattern).
LIBDIR := $(PREFIX)/lib/vpn-switch
# Documentation directory
DOCDIR := $(PREFIX)/share/doc/vpn-switch
# Man page directory (future)
MANDIR := $(PREFIX)/share/man/man1
# bash-completion looks here for <command> files (lazy-loaded on first Tab)
COMPLETIONDIR := $(PREFIX)/share/bash-completion/completions
# rc.d directory (FreeBSD service script; used by freebsd.mk plugin)
RCDIR := $(PREFIX)/etc/rc.d
#-----------------------------------------------------------------------------
# Source files
#-----------------------------------------------------------------------------
SCRIPT := vpn-switch.sh
BUILD_DIR := .build
INDEX_DIR := $(BUILD_DIR)/index
# Helper scripts (DNS resolvconf integration — currently FreeBSD-specific,
# called by template/phase/{wireguard,base}/.../dns.sh from $(SBINDIR)/).
HELPER_SCRIPTS := \
template/phase/base/dns/resolvconf/wg-resolvconf-up \
template/phase/base/dns/resolvconf/wg-resolvconf-down \
template/phase/base/dns/resolvconf/ovpn-resolvconf-up \
template/phase/base/dns/resolvconf/ovpn-resolvconf-down
# Documentation files
DOC_FILES := \
README.md \
LICENSE \
QUICK_REFERENCE.md
#-----------------------------------------------------------------------------
# Platform detection + plugin loading
#-----------------------------------------------------------------------------
# Platform name — drives both the 'platform' target and the plugin filename.
# Two-level detection: the kernel name (uname) is enough for the BSDs, but it
# can't tell Alpine (busybox + OpenRC) from Debian/Ubuntu (coreutils + systemd)
# — both report "Linux". So for Linux we refine via /etc/os-release's ID.
# Keep this logic in sync with include/validate.sh (_install_validate0) and
# vpn-switch-integration-test.sh (install-layout check).
# (if/else, not case: make counts parens to close $(shell ...), and the ')' in
# case patterns like 'alpine)' would close it prematurely.)
PLATFORM := $(shell kernel=$$(uname -s | tr A-Z a-z); \
if [ "$$kernel" = linux ] && [ -r /etc/os-release ]; then \
id=$$(. /etc/os-release 2>/dev/null; echo $$ID); \
if [ "$$id" = alpine ]; then echo alpine; else echo linux; fi; \
else echo "$$kernel"; fi)
# install command — same name on FreeBSD (BSD install) and Linux (GNU coreutils).
# Per-platform flag differences live in the per-platform .mk plugin if needed.
INSTALL := install
# Per-platform install hooks live in template/platform/$(PLATFORM).mk
# (freebsd.mk, linux.mk). Each provides 'install-platform' and
# 'uninstall-platform' target recipes; this main file just calls them.
#
# 'include' (not '-include'): if the .mk is missing, the build fails loudly
# with a clear path to the missing file. To port to a new OS, drop in a new
# .mk and the rest of the Makefile keeps working unchanged.
include template/platform/$(PLATFORM).mk
#-----------------------------------------------------------------------------
# Targets
#-----------------------------------------------------------------------------
.PHONY: all help platform install install-base install-platform \
uninstall uninstall-base uninstall-platform \
test test-unit test-integration test-arch \
metadata manpage check shellcheck lint clean
# Default target: show help
all: help
help:
@echo "vpn-switch - VPN configuration management"
@echo ""
@echo "Development (operates only in the repo):"
@echo " make platform Set repo's template/platform/default symlink"
@echo " make test Run all tests"
@echo " make test-unit Run unit tests only"
@echo " make test-integration Run integration tests only"
@echo " make test-arch Run architecture tests only"
@echo " make metadata Regenerate function metadata + template/VERSION"
@echo " make manpage Regenerate docs/vpn-switch.{1,html} from .1.md (needs pandoc)"
@echo " make check Syntax check all scripts"
@echo " make shellcheck Run shellcheck linter"
@echo " make lint Run all linters (check + shellcheck)"
@echo " make clean Remove build artifacts"
@echo ""
@echo "Installation (operates on \$$(DESTDIR)\$$(PREFIX)):"
@echo " make install Install to $(PREFIX)"
@echo " make install PREFIX=/usr Install to /usr"
@echo " make install DESTDIR=/staging Stage install for packaging"
@echo " make uninstall Remove installed files"
@echo ""
@echo "Variables:"
@echo " PREFIX = $(PREFIX)"
@echo " DESTDIR = $(DESTDIR)"
@echo " BINDIR = $(BINDIR)"
@echo " SBINDIR = $(SBINDIR)"
@echo " LIBDIR = $(LIBDIR)"
@echo " DOCDIR = $(DOCDIR)"
@echo " PLATFORM = $(PLATFORM)"
@echo ""
@echo "Post-installation:"
@echo " 1. Verify install: vpn-switch validate"
@echo " 2. Initialize database: vpn-switch bootstrap ~/.vpn-switch/db minimal"
@echo " 3. Import VPN config: vpn-switch wireguard import /path/to/config.conf"
#-----------------------------------------------------------------------------
# Repo-only convenience: set platform symlink in the SOURCE TREE so tests
# and ad-hoc './vpn-switch.sh' invocations from a fresh clone work right away.
# Does NOT touch the install destination — see 'install' for that.
#-----------------------------------------------------------------------------
platform:
@echo "Setting repo platform: template/platform/default → $(PLATFORM).sh"
@cd template/platform && rm -f default && ln -s $(PLATFORM).sh default
@ls -l template/platform/default
#-----------------------------------------------------------------------------
# Installation
#
# install = install-base + install-platform (plugin hook from .mk)
# install-base = platform-agnostic file copy + symlink in destination
# install-platform = OS-specific extras (rc.d on FreeBSD, systemd on Linux later)
#-----------------------------------------------------------------------------
install: metadata install-base install-platform
@echo ""
@echo "Installation complete!"
@echo ""
@echo "Next steps:"
@echo " 1. Verify installation: vpn-switch validate"
@echo " 2. Initialize database: vpn-switch bootstrap ~/.vpn-switch/db minimal"
@echo " 3. Import VPN config: vpn-switch wireguard import /path/to/config.conf"
# install-base copies the platform-agnostic file set:
# - Main script to $(BINDIR) (with $(LIBDIR) baked in via sed)
# - resolvconf helpers to $(SBINDIR) (root-only; reload local_unbound).
# Phase scripts call them by absolute /usr/local/sbin paths so this is
# a contract.
# - include/ modules and template/ tree to $(LIBDIR) (package-private)
# - Documentation to $(DOCDIR)
# - Platform symlink in the install destination (NOT the repo — that's
# what the 'platform' target is for)
install-base:
@echo "Installing vpn-switch (base) to $(DESTDIR)$(PREFIX)..."
@$(INSTALL) -d -m 755 $(DESTDIR)$(BINDIR)
@$(INSTALL) -d -m 755 $(DESTDIR)$(SBINDIR)
@$(INSTALL) -d -m 755 $(DESTDIR)$(LIBDIR)
@$(INSTALL) -d -m 755 $(DESTDIR)$(LIBDIR)/include
@$(INSTALL) -d -m 755 $(DESTDIR)$(LIBDIR)/template
@$(INSTALL) -d -m 755 $(DESTDIR)$(DOCDIR)
@$(INSTALL) -d -m 755 $(DESTDIR)$(MANDIR)
@sed 's|: "$${VPN_SWITCH_LIBDIR:=.*}"|: "$${VPN_SWITCH_LIBDIR:=$(LIBDIR)}"|' \
$(SCRIPT) > $(DESTDIR)$(BINDIR)/vpn-switch
@chmod 755 $(DESTDIR)$(BINDIR)/vpn-switch
@for script in $(HELPER_SCRIPTS); do \
$(INSTALL) -m 755 $$script $(DESTDIR)$(SBINDIR)/; \
done
@$(INSTALL) -m 644 include/*.sh $(DESTDIR)$(LIBDIR)/include/
@cp -R template/* $(DESTDIR)$(LIBDIR)/template/
@find $(DESTDIR)$(LIBDIR)/template -type f -exec chmod 644 {} \;
@find $(DESTDIR)$(LIBDIR)/template -type d -exec chmod 755 {} \;
@cd $(DESTDIR)$(LIBDIR)/template/platform && rm -f default && ln -s $(PLATFORM).sh default
@for doc in $(DOC_FILES); do \
if [ -f "$$doc" ]; then $(INSTALL) -m 644 "$$doc" $(DESTDIR)$(DOCDIR)/; fi; \
done
@if [ -f docs/vpn-switch.1 ]; then \
$(INSTALL) -m 644 docs/vpn-switch.1 $(DESTDIR)$(MANDIR)/vpn-switch.1; \
fi
@$(INSTALL) -d -m 755 $(DESTDIR)$(COMPLETIONDIR)
@$(INSTALL) -m 644 completion/vpn-switch.bash $(DESTDIR)$(COMPLETIONDIR)/vpn-switch
@echo " → installed script, helpers, libs, templates, docs, man page, bash completion"
@echo " → platform symlink: $(PLATFORM).sh"
uninstall: uninstall-platform uninstall-base
uninstall-base:
@echo "Uninstalling vpn-switch (base) from $(DESTDIR)$(PREFIX)..."
@rm -f $(DESTDIR)$(BINDIR)/vpn-switch
@for script in $(HELPER_SCRIPTS); do \
rm -f $(DESTDIR)$(SBINDIR)/`basename $$script`; \
done
@rm -rf $(DESTDIR)$(LIBDIR)
@rm -f $(DESTDIR)$(COMPLETIONDIR)/vpn-switch
@rm -rf $(DESTDIR)$(DOCDIR)
@rm -f $(DESTDIR)$(MANDIR)/vpn-switch.1
@echo " → removed script, helpers, libs, templates, docs, man page"
#-----------------------------------------------------------------------------
# Testing
#-----------------------------------------------------------------------------
test: metadata
@echo "Running all tests..."
@echo ""
@echo "=== Unit Tests ==="
@sh vpn-switch-unit-test.sh minimal
@echo ""
@echo "=== Integration Tests ==="
@sh vpn-switch-integration-test.sh minimal
@echo ""
@echo "=== Architecture Tests ==="
@sh vpn-switch-architecture-test.sh minimal
test-unit: metadata
@echo "Running unit tests..."
@sh vpn-switch-unit-test.sh minimal
test-integration: metadata
@echo "Running integration tests..."
@sh vpn-switch-integration-test.sh minimal
test-arch: metadata
@echo "Running architecture tests..."
@sh vpn-switch-architecture-test.sh minimal
#-----------------------------------------------------------------------------
# Development
#-----------------------------------------------------------------------------
metadata:
@echo "Generating function metadata (vpn-switch.sh + embedded test copies)..."
@sh scripts/generate-metadata.sh
@for f in $$(grep -lF '# === AUTO-GENERATED METADATA' *-test.sh scripts/vpn-switch-walkthrough.sh); do \
printf ' + %s\n' "$$f"; \
sh scripts/generate-metadata.sh --target "$$f" >/dev/null; \
done
@echo "Generating template/VERSION..."
@git rev-parse HEAD > template/VERSION 2>/dev/null || cp VERSION template/VERSION 2>/dev/null || echo "unknown" > template/VERSION
@echo " template/VERSION: $$(cat template/VERSION)"
# Regenerate man page (groff) and HTML rendering from the single Markdown
# source. Both outputs are committed so packagers/users don't need pandoc;
# only contributors editing the manpage source need it.
manpage:
@command -v pandoc >/dev/null 2>&1 || { \
echo "pandoc not found. Install with:"; \
echo " FreeBSD: pkg install hs-pandoc"; \
echo " Debian: apt install pandoc"; \
exit 1; \
}
@echo "Regenerating COMMANDS section from #@help corpus..."
@sh scripts/generate-help-manpage.sh
@echo "Generating docs/vpn-switch.1 (groff man page)..."
@pandoc -s -t man docs/vpn-switch.1.md -o docs/vpn-switch.1
@echo "Generating docs/vpn-switch.html (web rendering: mandoc from the man page, the same look as the man page in a terminal)..."
@mandoc -T html -O style=man.css docs/vpn-switch.1 > docs/vpn-switch.html
@cp docs/vpn-switch.html docs/index.html # GitHub Pages landing page (served from /docs)
@echo " → docs/vpn-switch.1"
@echo " → docs/vpn-switch.html"
@echo " → docs/index.html (GitHub Pages)"
@echo " Preview man: man ./docs/vpn-switch.1"
@echo " Preview html: \$$BROWSER docs/vpn-switch.html"
check:
@echo "Syntax check: $(SCRIPT)"
@sh -n $(SCRIPT) && echo " OK"
@echo "Syntax check: vpn-switch-unit-test.sh"
@sh -n vpn-switch-unit-test.sh && echo " OK"
@echo "Syntax check: vpn-switch-integration-test.sh"
@sh -n vpn-switch-integration-test.sh && echo " OK"
@echo "Syntax check: vpn-switch-architecture-test.sh"
@sh -n vpn-switch-architecture-test.sh && echo " OK"
@echo ""
@echo "All syntax checks passed."
# ShellCheck linting (requires shellcheck to be installed)
# Configuration in .shellcheckrc, documentation in docs/SHELLCHECK.md
shellcheck:
@echo "Running shellcheck (using .shellcheckrc)..."
@if command -v shellcheck >/dev/null 2>&1; then \
echo "Checking: $(SCRIPT)"; \
shellcheck $(SCRIPT) && echo " OK" || true; \
echo "Checking: vpn-switch-unit-test.sh"; \
shellcheck vpn-switch-unit-test.sh && echo " OK" || true; \
echo "Checking: vpn-switch-integration-test.sh"; \
shellcheck vpn-switch-integration-test.sh && echo " OK" || true; \
echo "Checking: vpn-switch-architecture-test.sh"; \
shellcheck vpn-switch-architecture-test.sh && echo " OK" || true; \
echo "Checking: include/*.sh"; \
shellcheck include/*.sh && echo " OK" || true; \
echo ""; \
echo "ShellCheck complete."; \
else \
echo "shellcheck not found. Install with:"; \
echo " FreeBSD: pkg install hs-ShellCheck"; \
echo " Debian: apt install shellcheck"; \
exit 1; \
fi
lint: check shellcheck
@echo ""
@echo "All linting complete."
clean:
@echo "Cleaning build artifacts..."
rm -rf $(BUILD_DIR)
find . -name "*.bak" -delete
@echo "Clean complete."
#-----------------------------------------------------------------------------
# Development utilities
#-----------------------------------------------------------------------------
.PHONY: index functions stats
# Generate comprehensive index
index: $(INDEX_DIR)/functions.txt $(INDEX_DIR)/anchors.txt
@echo "Index generated in $(INDEX_DIR)/"
@ls -lh $(INDEX_DIR)/
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
$(INDEX_DIR): $(BUILD_DIR)
@mkdir -p $(INDEX_DIR)
$(INDEX_DIR)/functions.txt: $(SCRIPT) $(INDEX_DIR)
@grep -n '^[_a-zA-Z][_a-zA-Z0-9]*()' $(SCRIPT) > $@ || true
$(INDEX_DIR)/anchors.txt: $(SCRIPT) $(INDEX_DIR)
@grep -n '^__*[a-z][a-z0-9_]*[0-9]()' $(SCRIPT) > $@ || true
functions: $(INDEX_DIR)/functions.txt
@echo "=== All Functions ==="
@cat $(INDEX_DIR)/functions.txt
stats: metadata
@echo "=== Code Statistics ==="
@echo "Total lines: $$(wc -l < $(SCRIPT))"
@echo "Functions: $$(grep -c '^[_a-zA-Z][_a-zA-Z0-9]*()' $(SCRIPT))"
@echo "Anchor functions: $$(grep -c '^__*[a-z][a-z0-9_]*[0-9]()' $(SCRIPT))"
@echo "Include modules: $$(ls include/*.sh 2>/dev/null | wc -l)"
@echo "Template files: $$(find template -type f | wc -l)"
# --- package ----------------------------------------------------------------
# A FreeBSD package of this checkout: 'install' into a staging root, the
# +MANIFEST from the name and the commit count (0.0.<count> until a tag names
# a release), the plist from the staged tree, pkg create into PKGDIR. The
# rescue system of an elebake stage depends on it (elebake stage rescue tool
# package <stage> vpn-switch).
PKGNAME := vpn-switch
PKGVERSION := 0.0.$(shell git rev-list --count HEAD 2>/dev/null || echo 0)
PKGDIR ?= $(CURDIR)/pkg
PKGSTAGE ?= $(CURDIR)/pkgstage
PKGABI := $(shell pkg config ABI 2>/dev/null || echo unknown)
.PHONY: package package-clean
package:
@test ! -d $(PKGSTAGE) || chmod -R u+w $(PKGSTAGE); rm -rf $(PKGSTAGE) && mkdir -p $(PKGDIR)
@$(MAKE) install DESTDIR=$(PKGSTAGE) PREFIX=$(PREFIX) > /dev/null
@printf 'name: "%s"\nversion: "%s"\norigin: "net/%s"\ncomment: "switch between WireGuard and OpenVPN sessions with pf and DNS phases"\n' \
'$(PKGNAME)' '$(PKGVERSION)' '$(PKGNAME)' > $(PKGSTAGE)/+MANIFEST
@printf 'desc: "vpn-switch manages VPN connections as sessions: import configurations, patch them per phase (firewall, vpn, dns), start, stop, switch, dump and export the database as a signed pair. BSD-2-Clause."\n' >> $(PKGSTAGE)/+MANIFEST
@printf 'maintainer: "dr.johannes.bruegmann@gmail.com"\nwww: "https://github.com/enk-ode/vpn-switch"\nprefix: "%s"\narch: "%s"\nlicenselogic: "single"\nlicenses: ["BSD2CLAUSE"]\ndeps: { "wireguard-tools": { origin: "net/wireguard-tools", version: "0" }, openvpn: { origin: "security/openvpn", version: "0" }, gnupg: { origin: "security/gnupg", version: "0" } }\n' \
'$(PREFIX)' '$(PKGABI)' >> $(PKGSTAGE)/+MANIFEST
@(cd $(PKGSTAGE)$(PREFIX) && find . \( -type f -o -type l \) | sed 's|^\./||' | sort) > $(PKGSTAGE)/plist
@pkg create -r $(PKGSTAGE) -M $(PKGSTAGE)/+MANIFEST -p $(PKGSTAGE)/plist -o $(PKGDIR)
@echo "package: $(PKGDIR)/$(PKGNAME)-$(PKGVERSION).pkg ($$(wc -l < $(PKGSTAGE)/plist | tr -d ' ') files)"
package-clean:
@test ! -d $(PKGSTAGE) || chmod -R u+w $(PKGSTAGE); rm -rf $(PKGSTAGE)