tools: add canonical sweep.py with explicit coa-elvui exclusion

Two failures that compounded in the last round of ElvUI breakage:

  1. The sweep script only lived in /tmp during a session — re-derived
     from scratch each time, so the EXCLUDE_FORKS knowledge wasn't
     anywhere reviewable.
  2. The old filename-only filter ('if -ElvUI in name skip') missed
     ElvUI's customizations inside otherwise-stock-named files AND
     rsync --delete killed -ElvUI suffixed widgets that exist only in
     ElvUI's bundle (e.g. AceGUIWidget-Button-ElvUI.lua).

This tool fixes both:

  - Lives in the repo (tools/sweep.py), so the exclusion list is
    visible in version control and reviewable.
  - EXCLUDED_FORKS = {'coa-elvui'} hardcoded with an in-source comment
    explaining why.
  - --exclude='*-ElvUI*' passed to every rsync as belt-and-braces, so
    even if a future fork accidentally carries an ElvUI-namespaced
    file we never wanted to overwrite, the sweep won't touch it.
  - Refuses to add new lib dirs — only updates ones already present
    in the fork.
  - --dry-run flag for safe verification.

README updated with a 'Forks excluded from sweep' section documenting
the same.
This commit is contained in:
2026-05-25 10:06:41 +02:00
parent 9583952806
commit a96308ff2c
2 changed files with 180 additions and 0 deletions
+17
View File
@@ -69,3 +69,20 @@ libs. Note the new upstream commit in the README's commit-pin line above, and re
the CoA-compat patches listed above against the new revision (the FDID one is mechanical —
see `/tmp/fix_fdid.py` history). Keep patches **minimal and documented in this README**;
prefer fixing them upstream where reasonable.
Run the sweep via `tools/sweep.py` from this repo — it walks every sibling `coa-*` fork
under `/home/sub/repos/coa`, finds each fork's bundled `LibStub` / `CallbackHandler-1.0` /
`Ace*-3.0` dirs, and rsyncs them from this bundle. Use `--dry-run` first.
### Forks excluded from sweep
`coa-elvui` is excluded from the sweep — see `EXCLUDED_FORKS` in `tools/sweep.py`. ElvUI
ships its own bundled Ace3 stack with ElvUI-specific patches inside otherwise-stock-named
files (`AceLocale-3.0.lua`, `AceConfigDialog-3.0.lua`, every `AceGUI-3.0/widgets/*.lua`)
**plus** `-ElvUI`-suffixed widgets that don't exist in canonical at all (e.g.
`AceGUIWidget-Button-ElvUI.lua`). `rsync --delete` obliterates the latter; an in-place
sync overwrites the former. Either failure breaks `/ec` and floods locale errors. ElvUI
maintains its own bundle on its own cadence and must never be touched by this tool.
The sweep also passes `--exclude='*-ElvUI*'` to every rsync as a belt-and-braces guard
against future forks that happen to carry an ElvUI-namespaced file we didn't anticipate.
Executable
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Sweep this coa-ace3 bundle into every Exiles fork that embeds Ace3 libs.
Run from anywhere; the script locates itself and walks sibling repos at
$REPOS_ROOT (default /home/sub/repos/coa). For each fork that bundles
any AceXxx-3.0 library, the matching directory is rsynced from this
bundle with --delete so the fork stays byte-identical to canonical.
USAGE
tools/sweep.py # apply sweep
tools/sweep.py --dry-run # show what would change without writing
WHY THIS SCRIPT EXISTS
Previous sweeps were ad-hoc scripts living in /tmp during a session
and re-derived by hand each time. That made it easy to forget the
ElvUI exclusion (see EXCLUDED_FORKS below) and re-clobber ElvUI's
customized lib stack on every sync. Committing the canonical script
here keeps the exclusion list visible and reviewable.
EXCLUDED_FORKS
coa-elvui ships its OWN bundled Ace3 with ElvUI-specific patches
inside otherwise-stock-named files (AceLocale, AceConfigDialog, the
AceGUI widgets) PLUS files that don't exist in canonical at all
(AceGUIWidget-Button-ElvUI.lua). rsync --delete obliterates the
latter; an in-place sync overwrites the former. Either failure
breaks /ec and floods locale errors. ElvUI maintains its own bundle
on its own cadence and must never be swept by this tool.
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
# --- configuration -----------------------------------------------------------
REPOS_ROOT = Path(os.environ.get("REPOS_ROOT", "/home/sub/repos/coa"))
BUNDLE = Path(__file__).resolve().parent.parent # the coa-ace3 repo root
# Forks that this sweep MUST NOT touch. Document the reason inline.
EXCLUDED_FORKS = {
"coa-elvui", # ships its own ElvUI-patched Ace3 stack; sweep would clobber it
}
# Lib names whose directories we sync from the bundle.
LIB_NAMES = re.compile(r"^(LibStub|CallbackHandler-1\.0|Ace\w*-3\.0)$")
# rsync excludes — repo metadata never deployed, plus a belt-and-braces
# guard against deleting any -ElvUI file that might exist in destinations we
# didn't intend to touch (e.g. a future fork that pulls in some ElvUI widget).
RSYNC_EXCLUDES = [
".git", ".gitattributes", ".gitignore", ".github", ".idea",
".editorconfig", ".luacheckrc", ".pkgmeta",
"*-ElvUI*", # never delete or overwrite ElvUI-namespaced files
]
# --- discovery ---------------------------------------------------------------
def bundle_lib_sources():
"""Return {libname: absolute path inside bundle} for every shippable lib.
Handles the nested AceConfig children (AceConfigCmd/Dialog/Registry).
"""
out = {}
for path in BUNDLE.rglob("*"):
if not path.is_dir():
continue
if path.name in {".git", "tools"}:
continue
if LIB_NAMES.match(path.name):
out.setdefault(path.name, path)
return out
def fork_lib_targets(fork: Path, src_names):
"""Return {libname: absolute path inside fork} for each lib the fork bundles.
A lib is "bundled" when there's a top-level directory containing
<libname>/<libname>.lua. We don't add new libs to forks that didn't
already ship them.
"""
targets = {}
for path in fork.rglob("*.lua"):
if "/.git/" in str(path):
continue
if "-ElvUI" in path.name:
continue
name = path.stem
if name not in src_names:
continue
parent = path.parent
if parent.name != name:
continue
targets.setdefault(name, parent)
return targets
# --- sweep -------------------------------------------------------------------
def sweep(dry_run: bool) -> int:
src_map = bundle_lib_sources()
if not src_map:
print(f"no libs found inside {BUNDLE}; nothing to sweep", file=sys.stderr)
return 2
forks = sorted(p for p in REPOS_ROOT.iterdir()
if p.is_dir() and p.name.startswith("coa-")
and p.name != "coa-ace3"
and (p / ".git").exists())
print(f"bundle: {BUNDLE}")
print(f"forks scan: {REPOS_ROOT}")
print(f"excluded: {sorted(EXCLUDED_FORKS)}")
print(f"libs: {sorted(src_map)}")
print()
total_synced = total_skipped = 0
for fork in forks:
if fork.name in EXCLUDED_FORKS:
print(f" skip {fork.name} (excluded — ships its own customized Ace3)")
total_skipped += 1
continue
targets = fork_lib_targets(fork, src_map.keys())
if not targets:
print(f" no-libs {fork.name}")
continue
for libname in sorted(targets):
src = src_map[libname]
dst = targets[libname]
cmd = ["rsync", "-a", "--delete"]
if dry_run:
cmd += ["--dry-run", "--itemize-changes"]
for exc in RSYNC_EXCLUDES:
cmd += ["--exclude", exc]
cmd += [f"{src}/", f"{dst}/"]
res = subprocess.run(cmd, capture_output=True, text=True)
if res.returncode != 0:
print(f" ! rsync failed for {fork.name}/{libname}: {res.stderr.strip()}")
continue
tag = "would-sync" if dry_run else "synced"
print(f" {tag:<9} {fork.name}/{dst.relative_to(fork)}")
total_synced += 1
print()
print(f"summary: {total_synced} lib dirs {'would be synced' if dry_run else 'synced'}, "
f"{total_skipped} forks excluded")
return 0
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--dry-run", action="store_true",
help="show what would change without writing")
args = ap.parse_args()
sys.exit(sweep(args.dry_run))
if __name__ == "__main__":
main()