#!/usr/bin/env python3 import argparse import sys from pathlib import Path from patch_layout import PATCH_LAYOUT, normalized_path EXTENSIONS = { "MoonWell", "wxl-db2", "wxl-fdid-moonwell", "wxl-grasswind", "wxl-modern-adt", "wxl-modern-blp", "wxl-modern-m2", "wxl-modern-wmo", "wxl-moonwell-storage-fallback", "wxl-unit-outline", } EXTENSION_KEYS = {name.casefold() for name in EXTENSIONS} ALLOWED_TOP_LEVEL_FILES = { "d3d9.dll", "warcraftxl.dll", "wow.exe", } ALLOWED_TOP_LEVEL_DIRS = {"data", "extensions", "utils"} REQUIRED_RUNTIME_FILES = { "wow.exe", "d3d9.dll", "warcraftxl.dll", *PATCH_LAYOUT.repository_patches, "utils/d3d9-native.dll", *(f"extensions/{name.casefold()}/{name.casefold()}.dll" for name in EXTENSIONS), } ALLOWED_DLL_FILES = { path for path in REQUIRED_RUNTIME_FILES if path.endswith(".dll") } BUILD_ONLY_SUFFIXES = { ".bak", ".exp", ".ilk", ".lib", ".log", ".obj", ".old", ".orig", ".pdb", ".pyc", ".rej", ".temp", ".tmp", } def normalized(path: Path) -> str: return normalized_path(path.as_posix()) def validate(root: Path) -> list[str]: errors: list[str] = [] files = sorted(path for path in root.rglob("*") if path.is_file()) relative_files = {normalized(path.relative_to(root)) for path in files} for missing in sorted(REQUIRED_RUNTIME_FILES - relative_files): errors.append(f"missing required runtime file: {missing}") for entry in root.iterdir(): key = entry.name.casefold() if entry.is_file() and key not in ALLOWED_TOP_LEVEL_FILES: errors.append(f"unexpected top-level file: {entry.name}") elif entry.is_dir() and key not in ALLOWED_TOP_LEVEL_DIRS: errors.append(f"unexpected top-level directory: {entry.name}") for path in files: relative = path.relative_to(root) relative_key = normalized(relative) parts = relative.parts if PATCH_LAYOUT.is_external_graphics_patch(relative.as_posix()): errors.append( f"externally managed graphics patch in repository package: {relative.as_posix()}" ) if path.suffix.casefold() in BUILD_ONLY_SUFFIXES: errors.append(f"build-only or temporary file: {relative.as_posix()}") if path.suffix.casefold() == ".dll" and relative_key not in ALLOWED_DLL_FILES: errors.append(f"unexpected DLL: {relative.as_posix()}") if path.suffix.casefold() == ".exe" and relative_key != "wow.exe": errors.append(f"unexpected executable: {relative.as_posix()}") if parts and parts[0].casefold() == "extensions": if len(parts) != 3: errors.append(f"unexpected Extensions layout: {relative.as_posix()}") continue extension_name = parts[1] expected_file = f"{extension_name}.dll" if extension_name.casefold() not in EXTENSION_KEYS: errors.append(f"unexpected extension: {relative.as_posix()}") elif parts[2].casefold() != expected_file.casefold(): errors.append(f"unexpected file in extension package: {relative.as_posix()}") return errors def main() -> int: parser = argparse.ArgumentParser(description="Validate MoonWell package layout") parser.add_argument("--dir", required=True, help="Package staging directory") args = parser.parse_args() root = Path(args.dir).resolve() if not root.is_dir(): print(f"ERROR: package directory not found: {root}", file=sys.stderr) return 1 errors = validate(root) if errors: print("ERROR: package validation failed:", file=sys.stderr) for error in errors: print(f" - {error}", file=sys.stderr) return 1 print(f"OK: package layout validated ({len(list(root.rglob('*')))} entries)") return 0 if __name__ == "__main__": raise SystemExit(main())