git » homepage.git » master » tree

[master] / Makefile

.PHONY: all site assets clean deploy help test tree check-git-clean check-output-path check-pushed check-links check-tools archive-audit archive-techworks-audit archive-personal-audit archive-checksums archive-export archive-fetch-forums archive-fetch-github-inventory archive-bundle-github archive-fetch-archive-org archive-fetch-gist-footprint archive-fetch-sourceforge-jd4x archive-fetch-open-source-evidence \
	fresh container-image container-build container-check-links \
	container-archive-audit verify-reproducible verify-reproducible-inner

.DELETE_ON_ERROR:
.NOTPARALLEL:

SRC := md
OUT ?= out
HEAD := tpl/head.html
FOOT := tpl/foot.html
CSS := tpl/style.css
MD2HTML ?= cmark-gfm
CMARK_FLAGS := --to html --extension table --validate-utf8 --unsafe
PYTHON ?= python3
SITEGEN := tools/sitegen.py
NORMALIZE_OUTPUT := tools/normalize_output.py
VERIFY_OUTPUT := tools/verify_reproducible.py
DEPLOY_HOST ?= arsien23i2@pdx1-shared-a2-13.dreamhost.com:tailrecursion.com/~alan
SITE_BASE_URL ?= https://tailrecursion.com/~alan/
CONTAINER_ENGINE ?= podman
BUILDER_IMAGE ?= localhost/homepage-builder:2026-07-15
SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct HEAD 2>/dev/null)

export SOURCE_DATE_EPOCH

all: check-tools assets $(OUT)/style.css site

check-tools:
	@for command in "$(MD2HTML)" "$(PYTHON)" git rsync; do \
		command -v "$$command" >/dev/null 2>&1 || { \
			echo "Missing build tool: $$command" >&2; \
			echo 'Use make container-build or install the documented local tools.' >&2; \
			exit 1; \
		}; \
	done
	@$(PYTHON) -c 'import sys; assert sys.version_info >= (3, 11), "Python 3.11 or newer is required"'

$(OUT)/style.css: $(CSS)
	mkdir -p $(OUT)
	cp $(CSS) $@

assets:
	mkdir -p $(OUT)
	rsync -a --include='*/' --exclude='*.md' --exclude='*.MD' \
		--exclude='*~' --exclude='.#*' --exclude='#*#' --exclude='*.swp' \
		--exclude='.DS_Store' --prune-empty-dirs $(SRC)/ $(OUT)/
site:
	OUT_DIR="$(OUT)" MD2HTML="$(MD2HTML)" CMARK_FLAGS="$(CMARK_FLAGS)" \
	SITE_BASE_URL="$(SITE_BASE_URL)" $(PYTHON) $(SITEGEN) \
		--src "$(SRC)" --out "$(OUT)" --head "$(HEAD)" --foot "$(FOOT)"

fresh: check-output-path test
	@set -eu; \
		test -n "$(SOURCE_DATE_EPOCH)" || { echo 'SOURCE_DATE_EPOCH is empty' >&2; exit 1; }; \
		stage=$$(mktemp -d "$${TMPDIR:-/tmp}/homepage-out.XXXXXX"); \
		trap 'rm -rf "$$stage"' EXIT HUP INT TERM; \
		$(MAKE) OUT="$$stage" all; \
		$(PYTHON) $(NORMALIZE_OUTPUT) "$$stage" "$(SOURCE_DATE_EPOCH)"; \
		mkdir -p "$(OUT)"; \
	rsync -a --delete --checksum --chmod=F644,D755 "$$stage/" "$(OUT)/"

container-image:
	$(CONTAINER_ENGINE) build --pull=missing --tag "$(BUILDER_IMAGE)" --file Containerfile .

container-build: container-image
	mkdir -p "$(OUT)"
	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
		--env SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \
		"$(BUILDER_IMAGE)" make fresh OUT="$(OUT)"

container-check-links: container-image
	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
		"$(BUILDER_IMAGE)" make check-links

container-archive-audit: container-image
	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
		"$(BUILDER_IMAGE)" make archive-audit

verify-reproducible: container-image
	$(CONTAINER_ENGINE) run --rm --userns=keep-id \
		--volume "$(CURDIR):/workspace:Z" --workdir /workspace \
		--env SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \
		"$(BUILDER_IMAGE)" make verify-reproducible-inner

verify-reproducible-inner: test
	@set -eu; \
		first=$$(mktemp -d "$${TMPDIR:-/tmp}/homepage-a.XXXXXX"); \
		second=$$(mktemp -d "$${TMPDIR:-/tmp}/homepage-b.XXXXXX"); \
		trap 'rm -rf "$$first" "$$second"' EXIT HUP INT TERM; \
		$(MAKE) OUT="$$first" all; \
		$(MAKE) OUT="$$second" all; \
		$(PYTHON) $(NORMALIZE_OUTPUT) "$$first" "$(SOURCE_DATE_EPOCH)"; \
		$(PYTHON) $(NORMALIZE_OUTPUT) "$$second" "$(SOURCE_DATE_EPOCH)"; \
		$(PYTHON) $(VERIFY_OUTPUT) "$$first" "$$second"

deploy: check-git-clean check-pushed
	$(MAKE) container-build
	@if [ -z "$(DEPLOY_HOST)" ]; then \
		echo "DEPLOY_HOST is not set"; \
		exit 1; \
	fi
	rsync -a --delete --delay-updates --chmod=F644,D755 $(OUT)/ $(DEPLOY_HOST)

tree:
	printf 'Markdown sources: ' && find $(SRC) -type f -name '*.md' | wc -l
	printf 'HTML outputs:   ' && if [ -d $(OUT) ]; then find $(OUT) -type f -name '*.html' | wc -l; else echo 0; fi

test:
	PYTHONPATH=tools $(PYTHON) -m unittest discover -s tests -v
	$(PYTHON) -m compileall -q tools tests

clean: check-output-path
	rm -rf $(OUT)

help:
	@echo 'Static site generator targets:'
	@echo '  make container-build       - reproducibly build $(OUT)/ with Podman'
	@echo '  make verify-reproducible   - compare two isolated container builds'
	@echo '  make container-check-links - check outbound links with container tools'
	@echo '  make container-archive-audit - verify all preserved artifacts'
	@echo '  make assets                - copy non-Markdown assets into $(OUT)/'
	@echo '  make                       - build locally (requires cmark-gfm)'
	@echo '  make deploy                - container-build, then publish from the host'
	@echo '  make tree                  - count source and generated files'
	@echo '  make test                  - run standard-library unit tests'
	@echo '  make archive-audit         - verify all preserved artifacts'
	@echo '  make archive-fetch-forums  - refresh wooby forum captures (network)'
	@echo '  make archive-fetch-github-inventory - refresh GitHub metadata (network)'
	@echo '  make archive-bundle-github - preserve all inventoried Git repositories (network)'
	@echo '  make archive-fetch-archive-org - preserve every public Archive.org link (network)'
	@echo '  make archive-fetch-gist-footprint - capture gist use evidence (network)'
	@echo '  make archive-fetch-sourceforge-jd4x - preserve credited JD4X pages and sources (network)'
	@echo '  make archive-fetch-open-source-evidence - preserve pages supporting public role claims (network)'
	@echo '  make archive-export DEST=  - create a portable preservation copy'
	@echo '  make clean                 - remove $(OUT)/'
	@echo '  (deploy requires a clean worktree matching its upstream branch)'

check-git-clean:
	@if [ -n "$$(git status --porcelain)" ]; then \
		echo 'Refusing to build: git worktree has uncommitted changes.'; \
		git status --short; \
		exit 1; \
	fi

check-output-path:
	@case "$(OUT)" in ''|'/'|'.'|'..') \
		echo "Refusing unsafe output path: $(OUT)" >&2; exit 1 ;; \
	esac

check-pushed:
	@upstream=$$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null) || { \
		echo 'Current branch has no upstream.' >&2; exit 1; \
	}; \
	test "$$(git rev-parse HEAD)" = "$$(git rev-parse "$$upstream")" || { \
		echo "Refusing to deploy: HEAD does not match $$upstream. Push first." >&2; \
		exit 1; \
	}

check-links:
	@python3 tools/check_links.py

archive-audit:
	$(MAKE) archive-techworks-audit
	$(MAKE) archive-personal-audit

archive-techworks-audit:
	$(PYTHON) tools/archive_techworks.py audit

archive-personal-audit:
	$(PYTHON) tools/archive_personal.py audit

archive-checksums:
	$(PYTHON) tools/archive_techworks.py checksums
	$(PYTHON) tools/archive_personal.py checksums

archive-export:
	@test -n "$(DEST)" || { echo 'DEST must be an absolute path' >&2; exit 1; }
	$(PYTHON) tools/archive_techworks.py export --dest "$(DEST)"

archive-fetch-forums:
	$(PYTHON) tools/archive_personal.py fetch-hacker-news
	$(PYTHON) tools/archive_personal.py fetch-slashdot
	$(PYTHON) tools/archive_personal.py fetch-halfbakery

archive-fetch-github-inventory:
	$(PYTHON) tools/archive_personal.py fetch-github-inventory

archive-bundle-github:
	$(PYTHON) tools/archive_personal.py bundle-github

archive-fetch-archive-org:
	$(PYTHON) tools/archive_personal.py fetch-archive-org-links

archive-fetch-gist-footprint:
	$(PYTHON) tools/archive_personal.py fetch-gist-footprint

archive-fetch-sourceforge-jd4x:
	$(PYTHON) tools/archive_personal.py fetch-sourceforge-jd4x

archive-fetch-open-source-evidence:
	$(PYTHON) tools/archive_personal.py fetch-open-source-evidence