827a5bdc60
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.
164 lines
5.8 KiB
Python
Executable File
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()
|