From 3144bc6dbb100cebd24f47a4acd5470979da15b6 Mon Sep 17 00:00:00 2001 From: Perry Kivolowitz Date: Sun, 19 Apr 2026 02:20:34 -0500 Subject: [PATCH] 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: ", and restores the file to canonical. Verified under both bash and zsh, both paths. --- .github/workflows/check-macros.yml | 41 ++++++++++++++++++ macros/README.md | 22 ++++++++++ scripts/sync-macros.sh | 69 ++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 .github/workflows/check-macros.yml create mode 100755 scripts/sync-macros.sh diff --git a/.github/workflows/check-macros.yml b/.github/workflows/check-macros.yml new file mode 100644 index 0000000..3229e25 --- /dev/null +++ b/.github/workflows/check-macros.yml @@ -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 diff --git a/macros/README.md b/macros/README.md index 4483cfe..2941a4b 100644 --- a/macros/README.md +++ b/macros/README.md @@ -6,6 +6,28 @@ on Apple Silicon and Linux machines without 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 `printf()` must be handled via parallel code paths (i.e. use of `#if`). diff --git a/scripts/sync-macros.sh b/scripts/sync-macros.sh new file mode 100755 index 0000000..227d62d --- /dev/null +++ b/scripts/sync-macros.sh @@ -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