Enforce macro canonical-vs-chapter-copy identity via sync script and CI

The repository ships a copy of apple-linux-convergence.S in each
chapter directory that demonstrates assembly (11 copies at last count,
plus the canonical one in macros/) so that readers browsing or
downloading a single chapter from GitHub have the macros sitting
right next to the sources that use them. That self-containment is
worth keeping. Manual synchronization of 12 copies on every macro
edit is not: all 11 are currently byte-identical to the canonical,
but the first drift is a matter of when, not if, and diagnosing
"which chapter broke when I added a new macro" after the fact is
a bad time.

This commit turns "the copies are in sync" from a hope into a
machine-enforced invariant:

- scripts/sync-macros.sh: walks macros/*.S, finds every file with
  the same basename anywhere else in the repo (excluding .git/ and
  macros/ itself), and overwrites any copy that differs. Idempotent;
  prints only the files it actually changed plus a summary. Uses
  only POSIX tools (find, cmp, cp, basename) plus bash builtins
  under a #!/usr/bin/env bash shebang. Verified working under both
  macOS bash 3.2.57 and zsh 5.9 on clean-tree and drift-repair
  paths.

- .github/workflows/check-macros.yml: runs the sync script on every
  push and pull request, then fails the job if git diff --exit-code
  shows the script produced any uncommitted change. The failure
  message tells the author exactly what to do (run the script
  locally, commit the result).

- macros/README.md: new "Source of truth" section marking the
  chapter copies as derived artifacts, pointing editors at the
  sync script, and stating that CI enforces the invariant.

Rejected alternatives:

- Symlinking each chapter copy to macros/apple-linux-convergence.S.
  Cheapest option (zero infrastructure) and git handles symlinks
  natively, but Windows checkouts without Developer Mode replace
  the symlink with a plain-text file containing the target path.
  This book's audience is overwhelmingly Linux and Apple Silicon,
  so the Windows hazard is mostly theoretical, but a sync-and-check
  approach works in every clone environment and makes the
  source-of-truth relationship explicit rather than implicit in a
  filesystem feature.

- Having each chapter .include the canonical file via a relative
  path. Breaks the "self-contained chapter" property the copies
  exist to preserve; a reader who downloads one chapter gets a
  broken build because macros/ is not beside it.

- Making the copies build-time artifacts (generated by make, not
  committed). Same problem: a reader browsing one chapter on
  GitHub no longer sees the macro file they need.

Tests:

- ./scripts/sync-macros.sh run on the current tree reports
  "macros already in sync (11 chapter copies checked)" and exits 0.
- Injecting a trailing-line perturbation into a chapter copy and
  re-running the script: detects the drift, reports "synced: <path>",
  and restores the file to canonical. Verified under both bash and
  zsh, both paths.
This commit is contained in:
Perry Kivolowitz 2026-04-19 02:20:34 -05:00
parent 88c8f496c4
commit 3144bc6dbb
3 changed files with 132 additions and 0 deletions

41
.github/workflows/check-macros.yml vendored Normal file
View file

@ -0,0 +1,41 @@
# check-macros.yml
#
# Verifies that every chapter-level copy of a macro file is byte-identical
# to its canonical version in macros/. Runs scripts/sync-macros.sh and
# fails the job if the script produced any change that was not already
# committed.
#
# Rationale: the repo ships one copy of each macro file per chapter so
# that each chapter directory remains self-contained on GitHub. Those
# copies are derived artifacts; macros/*.S is the single source of
# truth. This check turns "copies are in sync" from a convention into
# a machine-enforced invariant, so drift cannot sneak into main.
#
# To fix a failure locally:
# ./scripts/sync-macros.sh
# git add -u
# git commit --amend --no-edit # or a new commit, whichever you prefer
name: Check macro sync
on:
push:
branches: [main]
pull_request:
jobs:
check-macros:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run sync script
run: ./scripts/sync-macros.sh
- name: Fail if any chapter copy drifted from canonical
run: |
if ! git diff --exit-code; then
echo "::error::Chapter macro copies drifted from the canonical files in macros/."
echo "::error::Run ./scripts/sync-macros.sh locally and commit the result."
exit 1
fi

View file

@ -6,6 +6,28 @@ on Apple Silicon and Linux machines without change.
The work is ongoing and subject to change. The work is ongoing and subject to change.
## Source of truth
The files in this directory (`macros/*.S`) are the **canonical**
versions of the macros. Every chapter directory that demonstrates
assembly code keeps a copy of `apple-linux-convergence.S` alongside
its sources, so that a reader browsing or downloading a single
chapter on GitHub has the macros sitting right next to the `.S`
files that use them.
Those chapter-level copies are **derived artifacts**. Do not edit
them. Edit the file here in `macros/`, then run:
```
./scripts/sync-macros.sh
```
from the repository root to propagate the change to every chapter
copy. A GitHub Actions job (`.github/workflows/check-macros.yml`)
re-runs the sync script on every push and pull request and fails
the build if any copy has drifted from canonical, so this invariant
cannot silently break.
There are limits to what these macros can do. Variadic functions such as There are limits to what these macros can do. Variadic functions such as
`printf()` must be handled via parallel code paths (i.e. use of `#if`). `printf()` must be handled via parallel code paths (i.e. use of `#if`).

69
scripts/sync-macros.sh Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env bash
#
# sync-macros.sh
#
# Propagate canonical macro files from macros/ into the per-chapter
# copies scattered throughout the repository.
#
# Source of truth:
# Any file in macros/*.S at the repository root.
#
# Targets:
# Every file elsewhere in the repository whose basename matches a
# file in macros/. The chapter copies exist so that each chapter
# directory remains self-contained (a reader can download a single
# chapter from GitHub and have the macros it needs sitting right
# next to the source files). That self-containment is valuable;
# manual synchronization of ~12 copies is not. This script lets us
# have both.
#
# Operation:
# For each canonical file, find every copy with the same basename
# outside macros/ and overwrite it if (and only if) it differs.
# Prints one line per file actually modified; prints nothing beyond
# a final summary if everything was already in sync.
#
# Intended use:
# Run after editing any file in macros/, then commit the changes
# (the canonical file plus all updated copies). CI verifies this
# was done by re-running the script and requiring `git diff
# --exit-code` to be clean.
#
# Perry Kivolowitz
# A Gentle Introduction to Assembly Language
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
if [ ! -d macros ]; then
echo "error: macros/ directory not found at repo root" >&2
exit 1
fi
changed=0
checked=0
for canonical in macros/*.S; do
[ -f "$canonical" ] || continue
name="$(basename "$canonical")"
# Find every file in the repo with this basename, excluding the
# canonical location itself and anything under .git/. The -type f
# filter skips any stray symlinks a prior experiment may have left.
while IFS= read -r copy; do
checked=$((checked + 1))
if ! cmp -s "$canonical" "$copy"; then
cp "$canonical" "$copy"
echo "synced: $copy <- $canonical"
changed=$((changed + 1))
fi
done < <(find . -name "$name" -not -path './macros/*' -not -path './.git/*' -type f)
done
if [ "$changed" -eq 0 ]; then
echo "macros already in sync ($checked chapter copies checked)"
else
echo "synced $changed of $checked chapter copies"
fi