Files
Sylpheed/tools/generate_export_docs.py
sim 75b546cf2f
All checks were successful
CI / Native — linux (pull_request) Successful in 2h0m10s
CI / WASM — Web (pull_request) Successful in 28m47s
CI / Formatting (pull_request) Successful in 1m29s
docs(reference): adopt the last reference files from the project root
Three untracked files in the project root had no home in either repository:

- the Xbox 360 technical reference report (a research compilation) becomes
  docs/reference/xbox360-re-technical-reference.md, unchanged;
- XBOX360_ARCHITECTURE.md becomes docs/reference/xbox360-architecture.md,
  trimmed to its platform facts. Its format "status" sections (PAK unknown,
  mesh unknown, audio TODO) and the `just sniff` workflow predate every
  decoder in this repo and were wrong;
- generate_export_docs.py becomes tools/generate_export_docs.py, the path the
  committed xbox360-exports.* already name as their generator. It lived
  inside a Canary checkout, so it now takes --canary and --out and fails
  loudly on a tree that is not Canary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:19:54 +02:00

369 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate documentation of all Xbox 360 system exports in Xenia Canary.
Parses the export table .inc files and implementation .cc files to produce
a comprehensive listing of all exports with their implementation status.
Reads a Xenia Canary working tree and writes
`docs/reference/xbox360-exports.{md,json}`. The committed copies were generated
from an older Canary, so a fresh run changes some statuses.
Usage:
python3 tools/generate_export_docs.py [--canary ../xenia-canary] [--out docs/reference]
"""
import argparse
import json
import pathlib
import re
from collections import OrderedDict
SYLPHEED_ROOT = pathlib.Path(__file__).resolve().parent.parent
CANARY_DEFAULT = SYLPHEED_ROOT.parent / "xenia-canary"
# --- Configuration ---
# Set by configure() from --canary, before anything reads them.
REPO_ROOT = CANARY_DEFAULT
TABLE_FILES = {}
IMPL_DIRS = {}
def configure(canary_root):
"""Point the table and implementation paths at a Canary checkout."""
global REPO_ROOT, TABLE_FILES, IMPL_DIRS
REPO_ROOT = canary_root
TABLE_FILES = {
"xboxkrnl": REPO_ROOT / "src/xenia/kernel/xboxkrnl/xboxkrnl_table.inc",
"xam": REPO_ROOT / "src/xenia/kernel/xam/xam_table.inc",
"xbdm": REPO_ROOT / "src/xenia/kernel/xbdm/xbdm_table.inc",
}
IMPL_DIRS = {
"xboxkrnl": REPO_ROOT / "src/xenia/kernel/xboxkrnl",
"xam": REPO_ROOT / "src/xenia/kernel/xam",
"xbdm": REPO_ROOT / "src/xenia/kernel/xbdm",
}
missing = [str(p) for p in TABLE_FILES.values() if not p.is_file()]
if missing:
raise SystemExit(
f"not a Xenia Canary checkout: {canary_root} "
f"(missing {', '.join(missing)}); pass --canary"
)
MODULE_NAMES = {
"xboxkrnl": "xboxkrnl.exe",
"xam": "xam.xex",
"xbdm": "xbdm.xex",
}
MODULE_MAP = {"XBOXKRNL": "xboxkrnl", "XAM": "xam", "XBDM": "xbdm"}
# Regex patterns
XE_EXPORT_RE = re.compile(
r"XE_EXPORT\(\s*(\w+)\s*,\s*(0x[0-9A-Fa-f]+)\s*,\s*(\w+)\s*,\s*k(\w+)\s*\)"
)
DECLARE_RE = re.compile(
r"DECLARE_(XBOXKRNL|XAM|XBDM)_EXPORT\d*\(\s*(\w+)\s*,"
r"\s*k(\w+)\s*,\s*(.+?)\)\s*;",
re.DOTALL,
)
TAG_RE = re.compile(r"k(\w+)")
SHIM_RE = re.compile(r'SHIM_SET_MAPPING\(\s*"[^"]+"\s*,\s*(\w+)\s*,')
VAR_MAPPING_RE = re.compile(r"ordinals::(\w+)")
EXPORT_KVAR_RE = re.compile(r"EXPORT_KVAR\((\w+)\)")
def parse_table_files():
"""Phase A: Parse all XE_EXPORT entries from *_table.inc files."""
exports = OrderedDict()
for module, path in TABLE_FILES.items():
content = path.read_text()
for match in XE_EXPORT_RE.finditer(content):
mod, ordinal_hex, name, typ = match.groups()
exports[(mod, name)] = {
"module": mod,
"ordinal": int(ordinal_hex, 16),
"ordinal_hex": ordinal_hex.upper().replace("0X", "0x"),
"name": name,
"type": "variable" if typ == "Variable" else "function",
"status": "not_implemented",
"tags": [],
"category": "",
"source_file": "",
}
return exports
def scan_declare_macros(exports):
"""Phase B: Scan DECLARE_*_EXPORT macros in .cc files."""
for mod_key, dir_path in IMPL_DIRS.items():
for cc_file in sorted(dir_path.glob("*.cc")):
content = cc_file.read_text()
for match in DECLARE_RE.finditer(content):
mod_prefix, name, category, tags_str = match.groups()
module = MODULE_MAP[mod_prefix]
tags = TAG_RE.findall(tags_str)
key = (module, name)
if key in exports:
entry = exports[key]
entry["category"] = category
entry["tags"] = tags
entry["source_file"] = str(
cc_file.relative_to(REPO_ROOT)
)
if "Implemented" in tags:
entry["status"] = "implemented"
elif "Stub" in tags:
entry["status"] = "stub"
elif "Sketchy" in tags:
entry["status"] = "sketchy"
else:
entry["status"] = "stub"
def scan_legacy_patterns(exports):
"""Phase C: Handle SHIM_SET_MAPPING and SetVariableMapping."""
# SHIM_SET_MAPPING in xboxkrnl_strings.cc
strings_file = IMPL_DIRS["xboxkrnl"] / "xboxkrnl_strings.cc"
if strings_file.exists():
content = strings_file.read_text()
for match in SHIM_RE.finditer(content):
name = match.group(1)
key = ("xboxkrnl", name)
if key in exports:
exports[key]["status"] = "implemented"
exports[key]["source_file"] = str(
strings_file.relative_to(REPO_ROOT)
)
if not exports[key]["tags"]:
exports[key]["tags"] = ["Implemented"]
# SetVariableMapping / EXPORT_KVAR in xboxkrnl_module.cc
module_file = IMPL_DIRS["xboxkrnl"] / "xboxkrnl_module.cc"
if module_file.exists():
content = module_file.read_text()
for match in VAR_MAPPING_RE.finditer(content):
name = match.group(1)
key = ("xboxkrnl", name)
if key in exports:
exports[key]["status"] = "implemented"
exports[key]["source_file"] = str(
module_file.relative_to(REPO_ROOT)
)
if not exports[key]["tags"]:
exports[key]["tags"] = ["Implemented"]
for match in EXPORT_KVAR_RE.finditer(content):
name = match.group(1)
key = ("xboxkrnl", name)
if key in exports:
exports[key]["status"] = "implemented"
exports[key]["source_file"] = str(
module_file.relative_to(REPO_ROOT)
)
if not exports[key]["tags"]:
exports[key]["tags"] = ["Implemented"]
def compute_statistics(exports):
"""Compute per-module statistics."""
stats = {}
for module in TABLE_FILES:
module_exports = [e for e in exports.values() if e["module"] == module]
stats[module] = {
"total": len(module_exports),
"implemented": sum(
1 for e in module_exports if e["status"] == "implemented"
),
"stub": sum(1 for e in module_exports if e["status"] == "stub"),
"sketchy": sum(
1 for e in module_exports if e["status"] == "sketchy"
),
"not_implemented": sum(
1
for e in module_exports
if e["status"] == "not_implemented"
),
"functions": sum(
1 for e in module_exports if e["type"] == "function"
),
"variables": sum(
1 for e in module_exports if e["type"] == "variable"
),
}
return stats
def generate_markdown(exports, stats):
"""Generate the Markdown documentation."""
lines = []
lines.append("# Xbox 360 System Exports - Xenia Canary")
lines.append("")
lines.append(
"Auto-generated by `tools/generate_export_docs.py`. "
"Do not edit manually."
)
lines.append("")
# Overall summary
lines.append("## Summary")
lines.append("")
total_all = sum(s["total"] for s in stats.values())
impl_all = sum(s["implemented"] for s in stats.values())
stub_all = sum(s["stub"] for s in stats.values())
sketchy_all = sum(s["sketchy"] for s in stats.values())
not_impl_all = sum(s["not_implemented"] for s in stats.values())
lines.append(
f"**Total exports: {total_all}** | "
f"Implemented: {impl_all} | "
f"Stub: {stub_all} | "
f"Sketchy: {sketchy_all} | "
f"Not implemented: {not_impl_all}"
)
lines.append("")
lines.append(
"| Module | File | Total | Implemented | Stub | Sketchy | Not Implemented | Functions | Variables |"
)
lines.append(
"|--------|------|------:|------------:|-----:|--------:|----------------:|----------:|----------:|"
)
for module in TABLE_FILES:
s = stats[module]
lines.append(
f"| {module} | {MODULE_NAMES[module]} | {s['total']} | "
f"{s['implemented']} | {s['stub']} | {s['sketchy']} | "
f"{s['not_implemented']} | {s['functions']} | {s['variables']} |"
)
lines.append("")
# Per-module tables
for module in TABLE_FILES:
s = stats[module]
lines.append(f"## {module} ({MODULE_NAMES[module]})")
lines.append("")
lines.append(
f"Total: {s['total']} | "
f"Implemented: {s['implemented']} | "
f"Stub: {s['stub']} | "
f"Sketchy: {s['sketchy']} | "
f"Not implemented: {s['not_implemented']}"
)
lines.append("")
lines.append(
"| Ordinal | Name | Type | Status | Category | Source |"
)
lines.append(
"|--------:|------|------|--------|----------|--------|"
)
module_exports = sorted(
[e for e in exports.values() if e["module"] == module],
key=lambda e: e["ordinal"],
)
for e in module_exports:
ordinal = f"0x{e['ordinal']:03X}"
status_icon = {
"implemented": "implemented",
"stub": "stub",
"sketchy": "sketchy",
"not_implemented": "-",
}.get(e["status"], e["status"])
source = e["source_file"]
if source:
# Show just the filename for brevity
source = pathlib.Path(source).name
lines.append(
f"| {ordinal} | {e['name']} | {e['type']} | "
f"{status_icon} | {e['category']} | {source} |"
)
lines.append("")
return "\n".join(lines)
def generate_json(exports, stats):
"""Generate the JSON documentation."""
data = {
"description": "Xbox 360 system exports implemented in Xenia Canary",
"generator": "tools/generate_export_docs.py",
"statistics": stats,
"modules": {},
}
for module in TABLE_FILES:
module_exports = sorted(
[e for e in exports.values() if e["module"] == module],
key=lambda e: e["ordinal"],
)
data["modules"][module] = {
"file": MODULE_NAMES[module],
"exports": module_exports,
}
return json.dumps(data, indent=2)
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--canary", type=pathlib.Path, default=CANARY_DEFAULT,
help="Xenia Canary working tree (default: %(default)s)")
parser.add_argument("--out", type=pathlib.Path,
default=SYLPHEED_ROOT / "docs" / "reference",
help="output directory (default: %(default)s)")
args = parser.parse_args()
configure(args.canary.resolve())
print("Parsing export tables...")
exports = parse_table_files()
print(f" Found {len(exports)} exports across {len(TABLE_FILES)} modules")
print("Scanning DECLARE_*_EXPORT macros...")
scan_declare_macros(exports)
print("Scanning legacy registration patterns...")
scan_legacy_patterns(exports)
stats = compute_statistics(exports)
for module, s in stats.items():
print(
f" {module}: {s['total']} total, "
f"{s['implemented']} implemented, "
f"{s['stub']} stub, "
f"{s['sketchy']} sketchy, "
f"{s['not_implemented']} not implemented"
)
# Generate outputs
docs_dir = args.out
docs_dir.mkdir(parents=True, exist_ok=True)
md_path = docs_dir / "xbox360-exports.md"
md_content = generate_markdown(exports, stats)
md_path.write_text(md_content)
print(f"Written: {md_path}")
json_path = docs_dir / "xbox360-exports.json"
json_content = generate_json(exports, stats)
json_path.write_text(json_content)
print(f"Written: {json_path}")
if __name__ == "__main__":
main()