import json
import re
import subprocess
import sys
from pathlib import Path
GENERIC_TYPES = {"creator", "generator", "explicit", "mass_generator"}
CODE_DEFAULTS = {
"out/cc": "out/processor.creator.cc",
"out/linux-module": "out/processor.creator.linux_module",
"out/gem": "out/processor.creator.gem",
"out/tags": "out/processor.generator.tags",
}
def rsconstruct_json(*args):
try:
out = subprocess.run(
["rsconstruct", "--json", *args],
check=True,
capture_output=True,
text=True,
).stdout
except (OSError, subprocess.CalledProcessError) as exc:
sys.exit(f"cannot run `rsconstruct --json {' '.join(args)}`: {exc}")
return json.loads(out)
def load_registry():
types = {}
moved = dict(CODE_DEFAULTS)
for entry in rsconstruct_json("processor", "list"):
full = entry["name"]
if not full.startswith("processor."):
sys.exit(
f"this rsconstruct still reports short names ({full!r}); "
"install one that speaks full names first"
)
_, ptype, short = full.split(".", 2)
if short != "generic":
types[short] = ptype
default_dir = rsconstruct_json("processor", "defconfig", full).get("output_dir") or ""
if default_dir == f"out/{full}":
old_name = ptype if short == "generic" else short
moved[f"out/{old_name}"] = default_dir
return types, moved
class Migrator:
def __init__(self, types, moved):
self.types = types
self.moved = moved
self.unknown = set()
alternatives = "|".join(
re.escape(old[len("out/"):]) for old in sorted(moved, key=len, reverse=True)
)
self.out_pattern = re.compile(
r"out/(" + alternatives + r")((?:\.[A-Za-z_]\w*)?)(?![\w.-])"
)
def full_name(self, dotted):
parts = dotted.split(".")
head = parts[0]
if head in GENERIC_TYPES:
second = parts[1] if len(parts) > 1 else ""
if second == "generic" or self.types.get(second) == head:
return None
return ".".join([head, "generic"] + parts[1:])
if head == "checker":
return None
if head in self.types:
return ".".join([self.types[head], head] + parts[1:])
self.unknown.add(head)
return None
def rewrite_headers(self, text):
def sub(match):
new = self.full_name(match.group(2))
if new is None:
return match.group(0)
close = "]]" if match.group(1) == "[[" else "]"
return f"{match.group(1)}processor.{new}{close}"
return re.sub(r"(\[\[?)processor\.([A-Za-z_][\w.-]*)\]\]?", sub, text)
def rewrite_out_dirs(self, text):
def sub(match):
return self.moved["out/" + match.group(1)] + match.group(2)
return self.out_pattern.sub(sub, text)
def migrate_file(self, path):
old = path.read_text(encoding="utf-8")
new = self.rewrite_out_dirs(self.rewrite_headers(old))
if new != old:
path.write_text(new, encoding="utf-8")
return True
return False
def stray_out_dirs(self, repo):
try:
files = subprocess.run(
["git", "-C", str(repo), "ls-files"],
check=True,
capture_output=True,
text=True,
).stdout.split()
except (OSError, subprocess.CalledProcessError):
return []
hits = []
for rel in files:
if rel == "rsconstruct.toml":
continue
p = repo / rel
try:
text = p.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
for lineno, line in enumerate(text.splitlines(), 1):
if self.out_pattern.search(line):
hits.append(f"{p}:{lineno}: {line.strip()}")
return hits
def main(argv):
if len(argv) < 2:
sys.exit(__doc__)
migrator = Migrator(*load_registry())
changed = 0
for arg in argv[1:]:
path = Path(arg)
if migrator.migrate_file(path):
changed += 1
print(f"rewrote {path}")
for hit in migrator.stray_out_dirs(path.parent):
print(f" review: {hit}")
print(f"{changed} file(s) rewritten")
if migrator.unknown:
names = ", ".join(sorted(migrator.unknown))
print(f"left unchanged (not a built-in processor): {names}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))