#!/usr/bin/env python3 """Copy 40250 sources into the port mirror (extension/src/port//) with mechanical edits only. port_copy.py copy [...] [--force] write the mechanical copy (refuses to overwrite without --force) port_copy.py diff [...] show the manual edits: port file vs its mechanical copy (no args: every mirrored 40250 file that differs) `` is a reference path such as `GameLib/ActorInstance.h`, matched case-insensitively. Mechanical edits, and nothing else: - CP949 is decoded and written as UTF-8 (comments are Korean). A non-ASCII string or character literal would change bytes at runtime, so its non-ASCII characters are written as their CP949 bytes in 3-digit octal escapes (`"\301\326..."`): the compiled bytes stay those of 40250. - CRLF becomes LF. - `#include "..."` paths get the on-disk case of the file they name (40250 builds on a case-insensitive file system and writes `../eterLib/StdAfx.h`, `Parser.h` for `parser.h`, ...), keeping the relative form, so the original include resolves the same way on every platform. Everything else in a port file that differs from this copy is a manual edit, marked `// PORT:`. """ from __future__ import annotations import argparse import difflib import re import sys from pathlib import Path import refroot # script directory is on sys.path when run directly REPO = refroot.repo_root() PORT = REPO / "extension" / "src" / "port" INCLUDE = re.compile(r'^(\s*#\s*include\s*")([^"]+)(")', re.M) LITERAL = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'') COMMENT = re.compile(r"//[^\n]*|/\*.*?\*/", re.S) def ref_index(ref: Path) -> dict[str, Path]: return {p.relative_to(ref).as_posix().lower(): p for p in ref.rglob("*") if p.is_file()} def canonical(ref: Path, index: dict[str, Path], path: Path) -> str: return index[path.relative_to(ref).as_posix().lower()].relative_to(ref).as_posix() def resolve(ref: Path, index: dict[str, Path], rel: str) -> Path: hit = index.get(rel.replace("\\", "/").lower()) if not hit: raise SystemExit(f"{rel}: not in the 40250 reference tree") return hit def fix_include(ref: Path, index: dict[str, Path], src: Path, target: str) -> str: base = src.parent.relative_to(ref).as_posix() parts = [] if base == "." else base.split("/") for piece in target.replace("\\", "/").split("/"): if piece == "..": if not parts: return target parts.pop() elif piece != ".": parts.append(piece) hit = index.get("/".join(parts).lower()) if not hit: return target # unresolved (commented-out or absent in 40250): leave untouched real = hit.relative_to(ref).as_posix().split("/") # Keep the original shape: same number of `..` and segments, only the case changes. kept = [p for p in target.replace("\\", "/").split("/") if p in ("..", ".")] tail = real[len(real) - (len(target.replace("\\", "/").split("/")) - len(kept)):] return "/".join(kept + tail) def mechanical(ref: Path, index: dict[str, Path], src: Path) -> str: raw = src.read_bytes() try: text = raw.decode("cp949") except UnicodeDecodeError as e: raise SystemExit(f"{src.relative_to(ref)}: not CP949 ({e})") text = text.replace("\r\n", "\n").lstrip("") code = COMMENT.sub(lambda m: " " * len(m.group(0)), text) # Literals are found with comments blanked out; offsets are the same in `text`. for m in reversed(list(LITERAL.finditer(code))): if not m.group(0).isascii(): escaped = "".join(c if c.isascii() else "".join(f"\\{b:03o}" for b in c.encode("cp949")) for c in m.group(0)) text = text[:m.start()] + escaped + text[m.end():] return INCLUDE.sub(lambda m: m.group(1) + fix_include(ref, index, src, m.group(2)) + m.group(3), text) def cmd_copy(args) -> int: ref = refroot.reference_root() index = ref_index(ref) for rel in args.rel: src = resolve(ref, index, rel) dst = PORT / canonical(ref, index, src) if dst.exists() and not args.force: print(f"skip {dst.relative_to(REPO)} (exists; --force overwrites manual edits)") continue dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(mechanical(ref, index, src), encoding="utf-8") print(f"copy {dst.relative_to(REPO)}") return 0 def cmd_diff(args) -> int: ref = refroot.reference_root() index = ref_index(ref) if args.rel: pairs = [(resolve(ref, index, r), PORT / canonical(ref, index, resolve(ref, index, r))) for r in args.rel] else: pairs = [] for dst in sorted(PORT.rglob("*")): key = dst.relative_to(PORT).as_posix().lower() if dst.is_file() and key in index and not key.startswith("common/"): pairs.append((index[key], dst)) changed = 0 for src, dst in pairs: if not dst.exists(): print(f"missing {dst.relative_to(REPO)}") continue want = mechanical(ref, index, src).splitlines(keepends=True) have = dst.read_text(encoding="utf-8").splitlines(keepends=True) diff = list(difflib.unified_diff(want, have, f"40250/{src.relative_to(ref).as_posix()}", dst.relative_to(REPO).as_posix())) if diff: changed += 1 sys.stdout.writelines(diff) print(f"{changed} of {len(pairs)} file(s) with manual edits", file=sys.stderr) return 0 def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = ap.add_subparsers(dest="cmd", required=True) c = sub.add_parser("copy") c.add_argument("rel", nargs="+") c.add_argument("--force", action="store_true") d = sub.add_parser("diff") d.add_argument("rel", nargs="*") args = ap.parse_args() return {"copy": cmd_copy, "diff": cmd_diff}[args.cmd](args) if __name__ == "__main__": sys.exit(main())