asm_book/scripts/sync-macros.sh
Perry Kivolowitz 3144bc6dbb 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.
2026-04-19 02:20:34 -05:00

69 lines
2.2 KiB
Bash
Executable file

#!/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