Files
moonwell-client/validate_package.py

136 lines
3.9 KiB
Python

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