from __future__ import annotations
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
HTOP_SRC = Path(os.environ.get("HTOP_C_SOURCE", str(Path.home() / "forkedRepos" / "htop")))
PORTED = ROOT / "src" / "ported"
SNAPSHOT = ROOT / "tests" / "data" / "htop_c_fn_names.txt"
C_KEYWORDS = {
"if", "for", "while", "switch", "return", "else", "do", "sizeof",
"static", "extern", "struct", "union", "enum", "typedef", "const",
"volatile", "inline", "register", "auto", "goto", "break", "continue",
"case", "default",
}
RUST_KEYWORDS = {
"as", "break", "const", "continue", "crate", "dyn", "else", "enum",
"extern", "false", "fn", "for", "if", "impl", "in", "let", "loop",
"match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
"static", "struct", "super", "trait", "true", "type", "unsafe", "use",
"where", "while", "async", "await", "abstract", "become", "box", "do",
"final", "macro", "override", "priv", "typeof", "unsized", "virtual",
"yield", "try", "gen",
}
RE_VALID_IDENT = re.compile(r"^[a-z_][a-z0-9_]*$")
RE_C_DEF = re.compile(r"^[A-Za-z_][\w\s\*]*?\b([A-Za-z_]\w*)\s*\(")
def load_snapshot_names() -> set[str]:
names: set[str] = set()
for line in SNAPSHOT.read_text(errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
_, _, fn = line.partition(":")
if fn:
names.add(fn)
return names
def c_defs_by_stem(srcdir: Path) -> dict[str, list[tuple[str, int, str]]]:
out: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
for c in sorted(srcdir.glob("*.c")):
stem = c.stem.lower()
if not RE_VALID_IDENT.match(stem):
continue lines = c.read_text(errors="replace").splitlines()
seen: set[str] = set()
for i, line in enumerate(lines, 1):
if not line or line[0].isspace() or line[0] in "#/*}":
continue
m = RE_C_DEF.match(line)
if not m:
continue
name = m.group(1)
if name in C_KEYWORDS or name in RUST_KEYWORDS or name in seen:
continue
tail = " ".join(lines[i - 1:i + 5])
brace = tail.find("{")
semi = tail.find(";")
if brace == -1 or (semi != -1 and semi < brace):
continue
sig = line.split("{")[0].strip().rstrip(")").strip()
if len(sig) > 160:
sig = sig[:157] + "..."
out[stem].append((name, i, sig))
seen.add(name)
return out
MODULE_HEADER = """\
//! Stub scaffold for `{cfile}` — NOT yet ported.
//!
//! Every `pub fn` below is a placeholder (`todo!()`) named after a real
//! htop C function so the port-purity gate accepts the module and the
//! port surface is laid out. Replace each stub with a faithful port of
//! the C body, updating the signature and the doc comment to `Port of
//! `{cfile}`:<line>.` as you go. `gen_port_report.py` counts these
//! `todo!()` bodies as *stubbed*, not *ported*, so scaffolding does not
//! inflate coverage.
#![allow(non_snake_case)]
#![allow(dead_code)]
"""
STUB_FN = """\
/// TODO: port of `{sig}` from `{cfile}:{line}`.
pub fn {name}() {{
todo!("port of {cfile}:{line}")
}}
"""
def _subdir_arg() -> str | None:
for i, a in enumerate(sys.argv):
if a == "--subdir" and i + 1 < len(sys.argv):
return sys.argv[i + 1]
return None
def main() -> int:
dry = "--dry-run" in sys.argv
if not HTOP_SRC.is_dir():
print(f"ERROR: htop source not found at {HTOP_SRC}", file=sys.stderr)
return 1
if not SNAPSHOT.is_file():
print(f"ERROR: snapshot not found at {SNAPSHOT}", file=sys.stderr)
return 1
subdir = _subdir_arg()
if subdir:
if not RE_VALID_IDENT.match(subdir):
print(f"ERROR: --subdir {subdir!r} is not a valid Rust module name", file=sys.stderr)
return 1
srcdir = HTOP_SRC / subdir
outdir = PORTED / subdir
modrs = outdir / "mod.rs"
if not srcdir.is_dir():
print(f"ERROR: {srcdir} not found", file=sys.stderr)
return 1
else:
srcdir = HTOP_SRC
outdir = PORTED
modrs = PORTED / "mod.rs"
snapshot = load_snapshot_names()
defs = c_defs_by_stem(srcdir)
existing = {p.stem for p in outdir.glob("*.rs")} if outdir.is_dir() else set()
existing.discard("mod")
created: list[tuple[str, int]] = []
for stem, fns in sorted(defs.items()):
if stem in existing:
continue cfile = next(f.name for f in srcdir.glob("*.c") if f.stem.lower() == stem)
gated = [(n, ln, sig) for (n, ln, sig) in fns if n in snapshot]
if not gated:
continue
body = [MODULE_HEADER.format(cfile=cfile), ""]
for (name, line, sig) in gated:
body.append(STUB_FN.format(name=name, line=line, sig=sig.replace("`", "'"), cfile=cfile))
text = "\n".join(body).rstrip() + "\n"
created.append((stem, len(gated)))
if not dry:
outdir.mkdir(parents=True, exist_ok=True)
(outdir / f"{stem}.rs").write_text(text)
header = "//! Ported htop platform modules.\n\n" if subdir else None
src = modrs.read_text() if modrs.exists() else (header or "")
have = set(re.findall(r"^pub mod (\w+);", src, re.M))
want = sorted(have | {stem for stem, _ in created})
if want != sorted(have):
prefix = src.split("pub mod ")[0].rstrip()
prefix = (prefix + "\n\n") if prefix else (header or "")
new_src = prefix + "".join(f"pub mod {m};\n" for m in want)
if not dry:
outdir.mkdir(parents=True, exist_ok=True)
modrs.write_text(new_src)
if subdir:
parent = PORTED / "mod.rs"
psrc = parent.read_text()
if not re.search(rf"^pub mod {subdir};", psrc, re.M):
if not dry:
parent.write_text(psrc.rstrip() + f"\npub mod {subdir};\n")
total_fns = sum(n for _, n in created)
verb = "would create" if dry else "created"
where = f"{subdir}/" if subdir else ""
print(f"{verb} {len(created)} stub module(s), {total_fns} stub fn(s) under {where or 'src/ported/'}")
for stem, n in created:
print(f" {where}{stem}.rs ({n} stubs)")
return 0
if __name__ == "__main__":
sys.exit(main())