#!/usr/bin/env python3 import argparse import os import re import sys import tempfile from pathlib import Path try: from tools.nid_names import CHARSET, SUFFIX, compute_nid except ImportError: from nid_names import CHARSET, SUFFIX, compute_nid def strip_nid_token(raw): s = raw.lstrip("\ufeff").strip() if not s: return "" if s.startswith("#"): return "" if "#" in s: s = s.split("#", 1)[0].strip() if not s: return "" return s.split()[0] def parse_nids(path): nids = [] seen = set() with open(path, "r", encoding="utf-8-sig", errors="replace") as f: for number, line in enumerate(f, 1): nid = strip_nid_token(line) if not nid: continue if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: raise ValueError(f"{path}:{number}: invalid NID {nid!r}") if nid in seen: continue seen.add(nid) nids.append(nid) return nids def parse_db(path): mapping = {} with open(path, "r", encoding="utf-8-sig", errors="replace") as f: for line in f: s = line.lstrip("\ufeff").strip() if not s or s.startswith("#"): continue parts = s.split() if len(parts) < 2: continue nid = parts[0].split("#", 1)[0].strip().lstrip("\ufeff") name = parts[1].split("#", 1)[0].strip().lstrip("\ufeff") if not nid or not name: continue if nid not in mapping: mapping[nid] = name return mapping def sanitize_identifier(name): s = re.sub(r"[^0-9A-Za-z_]", "_", name) if not s: return "_" if s[0].isdigit(): s = "_" + s return s def module_prefix(module): base = module if base.startswith("lib") and len(base) > 3 and base[3].isupper(): base = base[3:] base = base[0].lower() + base[1:] if base else module return sanitize_identifier(base) def is_runtime_name(name): if name.startswith("_"): return True if name.startswith("sce") or name.startswith("Sce"): return False return True def is_valid_ident(name): return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name) is not None def _default_td_path(module): candidates = [ "core/libs/prx/" + module + "/Export.cpp", "core/libs/prx/" + module + "/Unimplemented.cpp", "core/libs/prx/" + module + "/src/Unimplemented.cpp", ] root = Path(__file__).resolve().parent.parent for cand in candidates: try: if (root / cand).is_file(): return cand except OSError: continue return candidates[0] def td_target(module, td_path, td_lib): if td_path is None: td_path = "../../" + _default_td_path(module) if td_lib is None: td_lib = module return (td_path, td_lib) def generate(module, nids, db, td_path=None, td_lib=None): prefix = module_prefix(module) path, lib = td_target(module, td_path, td_lib) used = set() unknown_counter = 0 chunks = [] td = [] for nid in nids: real = db.get(nid) func = None display = None if real is not None: if is_valid_ident(real): if compute_nid(real) == nid: if is_runtime_name(real): candidate = real + "_nid_postfix" else: candidate = real if candidate not in used: func = candidate display = real if func is None: while True: func = "%sUnknown%02d" % (prefix, unknown_counter) unknown_counter += 1 if func not in used: break display = None used.add(func) if display is not None: chunks.append("") chunks.append("int APS5_VABI %s(void) {" % func) chunks.append(' NotImplemented_nid_no_patch("%s");' % nid) chunks.append(" return 0;") chunks.append("}") td.append("- [%s](%s) (%s) - unknown signature" % (display, path, lib)) else: chunks.append("") chunks.append('APS5_EXPORT("%s", %s);' % (nid, func)) chunks.append("int APS5_VABI %s(void) {" % func) chunks.append(' NotImplemented_nid_no_patch("%s");' % nid) chunks.append(" return 0;") chunks.append("}") td.append("- [%s](%s) (%s) - unknown name, signature" % (nid, path, lib)) header = [] header.append("#include ") header.append("#include ") header.append('#include "SceTypes.hpp"') header.append('#include "prx/libc/include/General.hpp"') header.append("") header.append('extern "C" {') footer = ["", "}"] code = "\n".join(header + chunks + footer) + "\n" td_text = "\n".join(td) + ("\n" if td else "") return (code, td_text) def same_file(left, right): left, right = Path(left), Path(right) return left.resolve() == right.resolve() or (left.exists() and right.exists() and left.samefile(right)) def write_outputs(outputs): staged = [] umask = os.umask(0) os.umask(umask) try: for filename, content in outputs: path = Path(filename).resolve() if path.is_dir(): raise IsADirectoryError(str(path)) with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as handle: temporary = Path(handle.name) staged.append((temporary, path)) handle.write(content) if path.exists(): temporary.chmod(path.stat().st_mode & 0o7777) else: temporary.chmod(0o666 & ~umask) for temporary, path in staged: temporary.replace(path) finally: for temporary, path in staged: temporary.unlink(missing_ok=True) def main(argv=None): ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") ap.add_argument("--nids", required=True, help="path to missing-NIDs file, one 11-char NID per line") ap.add_argument("--db", required=False, default=None, help="path to NID name CSV, NID name per line like aerolib.csv") ap.add_argument("--out", required=False, default=None, help="output file for code, default stdout") ap.add_argument("--td-path", required=False, default=None, help="override file path used inside TechnicalDebt lines, default derived from module, relative to docs/dev") ap.add_argument("--td-lib", required=False, default=None, help="override lib label used inside TechnicalDebt lines, default derived from module") ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) try: if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): ap.error("--out and --td-out must name different files") destinations = [args.td_out] if args.td_only else [args.out, args.td_out] for destination in filter(None, destinations): for source in filter(None, [args.nids, args.db]): if same_file(destination, source): ap.error(f"output {destination!r} would overwrite input {source!r}") nids = parse_nids(args.nids) db = parse_db(args.db) if args.db else {} code, td = generate(args.module, nids, db, args.td_path, args.td_lib) outputs = [] if not args.td_only and args.out: outputs.append((args.out, code)) if args.td_out: outputs.append((args.td_out, td)) write_outputs(outputs) except (OSError, ValueError) as error: ap.error(str(error)) if args.td_only: if not args.td_out: sys.stdout.write(td) return 0 if not args.out: sys.stdout.write(code) if not args.td_out: (sys.stdout if args.out else sys.stderr).write(td) return 0 if __name__ == "__main__": raise SystemExit(main())