#!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import hashlib import json import os import sys from pathlib import Path from typing import Optional IGNORED_TOP_LEVEL_DIRS = { "Cache", "Errors", "Logs", "Screenshots", "WTF", } IGNORED_DIRS_ANYWHERE = { ".git", ".moonwell_launcher", } ENV_VAR_NAME = "WOW_HOME" def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_file(path: Path, chunk_size: int = 1024 * 1024) -> str: h = hashlib.sha256() with path.open("rb") as f: while chunk := f.read(chunk_size): h.update(chunk) return h.hexdigest() def is_ignored(rel_path: Path) -> bool: if not rel_path.parts: return False if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS: return True return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1]) def load_dotenv(env_path: Path) -> dict[str, str]: values: dict[str, str] = {} if not env_path.exists(): return values for raw_line in env_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip() if key.startswith("export "): key = key.removeprefix("export ").strip() if not key: continue if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}: value = value[1:-1] values[key] = value return values def resolve_root(explicit_dir: Optional[str]) -> Optional[Path]: if explicit_dir: return Path(explicit_dir).resolve() env_value = os.environ.get(ENV_VAR_NAME) if env_value: return Path(env_value).expanduser().resolve() project_root = Path(__file__).resolve().parent dotenv_values = load_dotenv(project_root / ".env") env_value = dotenv_values.get(ENV_VAR_NAME) if env_value: os.environ.setdefault(ENV_VAR_NAME, env_value) return Path(env_value).expanduser().resolve() return None def compute_build_hash(files: list[dict]) -> str: lines = [ f'{item["path"]}:{item["size"]}:{item["sha256"]}' for item in sorted(files, key=lambda x: x["path"]) ] payload = "\n".join(lines).encode("utf-8") return sha256_bytes(payload) def main() -> int: parser = argparse.ArgumentParser(description="Collect files into manifest.json") parser.add_argument( "--dir", help=f"Directory to scan. Defaults to {ENV_VAR_NAME} from env or .env", ) parser.add_argument("--output", default="manifest.json", help="Output file") args = parser.parse_args() root = resolve_root(args.dir) output = Path(args.output).resolve() if root is None: print( f"ERROR: directory is not set. Pass --dir or define {ENV_VAR_NAME} in env/.env", file=sys.stderr, ) return 1 if not root.exists() or not root.is_dir(): print(f"ERROR: directory not found: {root}", file=sys.stderr) return 1 files = [] for file_path in sorted(root.rglob("*")): if not file_path.is_file(): continue if file_path.resolve() == output: continue rel_path_obj = file_path.relative_to(root) if is_ignored(rel_path_obj): continue rel_path = rel_path_obj.as_posix() files.append({ "path": rel_path, "size": file_path.stat().st_size, "sha256": sha256_file(file_path), }) build_hash = compute_build_hash(files) manifest = { "buildHash": build_hash, "files": files } with output.open("w", encoding="utf-8", newline="\n") as f: json.dump(manifest, f, ensure_ascii=False, indent=2) f.write("\n") print(f"OK: written {output}") print(f"Files: {len(files)}") print(f"Build hash: {build_hash}") return 0 if __name__ == "__main__": raise SystemExit(main())