commit 5804adb9c84acf6eb8e80dc06626dd3354df8ce5
parent 0696a381fa35277134e0c1dd22511fc2de886e96
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 21 Apr 2026 08:58:22 -0700
build: pull podman image once per arch, run with --pull=never
Prevents silent registry round-trips on every `podman run` — even a
digest-pinned reference was re-resolving the manifest. One explicit
`podman pull` per arch registers the image in the local store via a
build/$(ARCH)/.image stamp; every other invocation sets --pull=never
so cross-arch leaks or a bumped digest fail loudly instead of quietly
re-pulling.
Also drops stale kaem-minimal references left behind when that program
was removed.
Diffstat:
| M | Makefile | | | 39 | ++++++++++++++++++++++++++------------- |
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/Makefile b/Makefile
@@ -23,20 +23,18 @@
PROG ?= hello
ARCH ?= aarch64
-# Programs live in src/ (real: lisp, kaem-minimal) or tests/ (smoke:
-# hello, demo). Resolve PROG.M1 against both so the user doesn't have
-# to remember which bucket a program is in.
+# Programs live in src/ (real: lisp) or tests/ (smoke: hello, demo).
+# Resolve PROG.M1 against both so the user doesn't have to remember
+# which bucket a program is in.
PROG_SRC := $(firstword $(wildcard src/$(PROG).M1 tests/$(PROG).M1))
ifeq ($(PROG_SRC),)
$(error PROG '$(PROG)' not found — no src/$(PROG).M1 or tests/$(PROG).M1)
endif
# Per-program runtime arguments. The lisp interpreter reads its script
-# from argv[1] (docs/LISP.md step 8); kaem-minimal reads its build
-# script. Default each to its smoke fixture so `make run-all` stays a
-# smoke test.
+# from argv[1] (docs/LISP.md step 8). Default to its smoke fixture so
+# `make run-all` stays a smoke test.
RUN_ARGS_lisp ?= tests/lisp/00-identity.scm
-RUN_ARGS_kaem-minimal ?= tests/kaem.run
RUN_ARGS := $(RUN_ARGS_$(PROG))
# Map P1 ARCH -> Linux-platform tag for the container.
@@ -83,11 +81,22 @@ UPSTREAM_STAMP := $(UPSTREAM_DIR)/.stamp
# Single podman view: curdir mounted at /work. Toolchain build, assembly,
# link, and run all share this view. Keeping it narrow means nothing
# outside the repo is visible to the container.
-PODMAN := podman run --rm --platform $(PLATFORM) \
+#
+# --pull=never: the image is fetched exactly once per arch by the
+# $(IMAGE_STAMP) rule below. Every other podman invocation must find it
+# already in the local store, otherwise fail loudly — so cross-arch pulls
+# or a bumped digest show up as errors, not silent registry traffic.
+PODMAN := podman run --rm --pull=never --platform $(PLATFORM) \
-v $(CURDIR):/work \
-w /work \
$(RUNTIME_IMAGE)
+# Per-arch image stamp. One explicit `podman pull` registers the
+# digest-pinned image in the local store; subsequent runs use --pull=never
+# and never contact the registry. Bumping $(RUNTIME_IMAGE) requires
+# `make clean` (or removing build/$(ARCH)/.image) to repull.
+IMAGE_STAMP := $(OUT_DIR)/.image
+
# --- Targets ---------------------------------------------------------------
.PHONY: all toolchain populate-upstream run run-all test-lisp test-lisp-all clean
@@ -101,6 +110,10 @@ populate-upstream: $(UPSTREAM_STAMP)
$(OUT_DIR) $(TOOLS_DIR):
mkdir -p $@
+$(IMAGE_STAMP): | $(OUT_DIR)
+ podman pull --platform $(PLATFORM) $(RUNTIME_IMAGE)
+ @touch $@
+
# Mirror the upstream seed + hex0/1/2/catm/M0/ELF files we need from
# $(UPSTREAM) into build/upstream/. Host-side so the container mount stays
# minimal. The stamp doubles as an order marker and avoids re-copying on
@@ -118,7 +131,7 @@ $(UPSTREAM_DIR)/%: $(UPSTREAM_STAMP) ;
# One shot per arch — see bootstrap.sh for the phase-by-phase chain.
#
# Grouped target (&:) so all five outputs come from a single recipe run.
-$(TOOLS_DIR)/M0 $(TOOLS_DIR)/hex2-0 $(TOOLS_DIR)/catm $(TOOLS_DIR)/hex0 $(TOOLS_DIR)/hex1 &: bootstrap.sh $(UPSTREAM_STAMP) | $(TOOLS_DIR)
+$(TOOLS_DIR)/M0 $(TOOLS_DIR)/hex2-0 $(TOOLS_DIR)/catm $(TOOLS_DIR)/hex0 $(TOOLS_DIR)/hex1 &: bootstrap.sh $(UPSTREAM_STAMP) | $(TOOLS_DIR) $(IMAGE_STAMP)
$(PODMAN) sh bootstrap.sh $(ARCH) /work/$(TOOLS_DIR)
# Assemble: lint first, then combine per-arch defs + program and feed to M0.
@@ -130,7 +143,7 @@ $(TOOLS_DIR)/M0 $(TOOLS_DIR)/hex2-0 $(TOOLS_DIR)/catm $(TOOLS_DIR)/hex0 $(TOOLS_
# M0 takes a single positional input (no -f flag), so we catm the two
# sources together first. The intermediate .combined.M1 is kept in OUT_DIR
# so it gets cleaned along with everything else.
-$(OUT_DIR)/$(PROG).hex2: $(PROG_SRC) $(OUT_DIR)/p1_$(ARCH).M1 lint.sh $(TOOLS_DIR)/M0 $(TOOLS_DIR)/catm | $(OUT_DIR)
+$(OUT_DIR)/$(PROG).hex2: $(PROG_SRC) $(OUT_DIR)/p1_$(ARCH).M1 lint.sh $(TOOLS_DIR)/M0 $(TOOLS_DIR)/catm | $(OUT_DIR) $(IMAGE_STAMP)
./lint.sh $(OUT_DIR)/p1_$(ARCH).M1 $(PROG_SRC)
$(PODMAN) sh -ec ' \
$(TOOLS_DIR)/catm $(OUT_DIR)/$(PROG).combined.M1 $(OUT_DIR)/p1_$(ARCH).M1 $(PROG_SRC) ; \
@@ -142,12 +155,12 @@ $(OUT_DIR)/$(PROG).hex2: $(PROG_SRC) $(OUT_DIR)/p1_$(ARCH).M1 lint.sh $(TOOLS_DI
# base address 0x00600000 (no --base-address flag), which is why the ELF
# header references `&ELF_base` symbolically rather than baking in a
# concrete VA — the header travels to whatever base the linker chose.
-$(OUT_DIR)/$(PROG): $(OUT_DIR)/$(PROG).hex2 $(UPSTREAM_DIR)/$(ARCH_DIR)/ELF-$(ARCH).hex2 $(TOOLS_DIR)/hex2-0 $(TOOLS_DIR)/catm
+$(OUT_DIR)/$(PROG): $(OUT_DIR)/$(PROG).hex2 $(UPSTREAM_DIR)/$(ARCH_DIR)/ELF-$(ARCH).hex2 $(TOOLS_DIR)/hex2-0 $(TOOLS_DIR)/catm | $(IMAGE_STAMP)
$(PODMAN) sh -ec ' \
$(TOOLS_DIR)/catm $(OUT_DIR)/$(PROG).linked.hex2 $(UPSTREAM_DIR)/$(ARCH_DIR)/ELF-$(ARCH).hex2 $(OUT_DIR)/$(PROG).hex2 ; \
$(TOOLS_DIR)/hex2-0 $(OUT_DIR)/$(PROG).linked.hex2 $(OUT_DIR)/$(PROG)'
-run: $(OUT_DIR)/$(PROG)
+run: $(OUT_DIR)/$(PROG) | $(IMAGE_STAMP)
$(PODMAN) ./$(OUT_DIR)/$(PROG) $(RUN_ARGS)
# `-` prefix: continue past non-zero exit. demo.M1 exits with the computed
@@ -167,7 +180,7 @@ run-all:
# final fixnum payload), so we ignore exit codes and only diff stdout.
LISP_TESTS := $(sort $(wildcard tests/lisp/*.scm))
-test-lisp:
+test-lisp: | $(IMAGE_STAMP)
@$(MAKE) --no-print-directory PROG=lisp ARCH=$(ARCH) build/$(ARCH)/lisp
@pass=0; fail=0; \
for scm in $(LISP_TESTS); do \