Files
florian.berthold 827a5bdc60 chore: move bundle into Ace3/ subfolder + standard .gitignore
Matches the Exiles fork-layout convention (one folder per addon). README
and tools/sweep.py updated for the new path; tools/sweep.py stays at repo
root since it's dev tooling, not part of the shipped bundle.
2026-05-25 10:59:24 +02:00

164 lines
5.8 KiB
Python
Executable File

#!/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 / "Ace3" # canonical Ace3 lives under <repo>/Ace3/
# 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()