Initial LinuxCNC WebAssembly workspace snapshot
This commit is contained in:
145
scripts/generate_source_inventory.py
Executable file
145
scripts/generate_source_inventory.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the reproducible LinuxCNC source and coverage seed indexes.
|
||||
|
||||
The source index is deliberately mechanical: every file in a declared input
|
||||
root is recorded with its size and digest. The coverage seed is not an
|
||||
implementation claim; entries remain ``planned`` until an owner assigns a
|
||||
feature, adapter, test and oracle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
ROOTS = (
|
||||
("doc", Path("linuxcnc/docs")),
|
||||
("source", Path("linuxcnc/src")),
|
||||
("config", Path("linuxcnc/configs/sim")),
|
||||
("test", Path("linuxcnc/tests")),
|
||||
("unit_test", Path("linuxcnc/unit_tests")),
|
||||
("implementation", Path("wasm-core")),
|
||||
("implementation", Path("web")),
|
||||
)
|
||||
BUILD_FILES = (
|
||||
Path("linuxcnc/VERSION"),
|
||||
Path("linuxcnc/meson.build"),
|
||||
)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def source_kind(root_kind: str, path: Path) -> str:
|
||||
if root_kind == "implementation":
|
||||
if path.suffix in {".h", ".hpp"}:
|
||||
return "web_wasm_interface"
|
||||
if path.suffix in {".c", ".cc", ".cpp", ".mjs", ".js"}:
|
||||
return "web_wasm_implementation"
|
||||
return "web_wasm_build_or_test"
|
||||
if root_kind == "doc":
|
||||
return "documentation"
|
||||
if root_kind in {"test", "unit_test"}:
|
||||
return "test"
|
||||
if root_kind == "config":
|
||||
return "simulation_config"
|
||||
if path.name.endswith(".comp"):
|
||||
return "hal_component_source"
|
||||
return "source"
|
||||
|
||||
|
||||
def iter_files(repo_root: Path) -> Iterable[tuple[str, Path]]:
|
||||
for root_kind, relative_root in ROOTS:
|
||||
root = repo_root / relative_root
|
||||
if not root.is_dir():
|
||||
raise FileNotFoundError(f"required inventory root is missing: {relative_root}")
|
||||
for path in sorted(path for path in root.rglob("*") if path.is_file()):
|
||||
yield root_kind, path
|
||||
for relative_path in BUILD_FILES:
|
||||
path = repo_root / relative_path
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"required build source is missing: {relative_path}")
|
||||
yield "build", path
|
||||
|
||||
|
||||
def make_entry(repo_root: Path, root_kind: str, path: Path) -> dict[str, object]:
|
||||
relative = path.relative_to(repo_root).as_posix()
|
||||
source_id = f"{root_kind}:{relative}"
|
||||
return {
|
||||
"source_id": source_id,
|
||||
"path": relative,
|
||||
"kind": source_kind(root_kind, path),
|
||||
"bytes": path.stat().st_size,
|
||||
"sha256": sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def make_manifest_entry(source: dict[str, object]) -> dict[str, object]:
|
||||
source_id = str(source["source_id"])
|
||||
return {
|
||||
"id": f"seed:{source_id}",
|
||||
"feature_id": None,
|
||||
"source": source_id,
|
||||
"source_sha": source["sha256"],
|
||||
"domain": "unassigned",
|
||||
"kind": source["kind"],
|
||||
"adaptation": None,
|
||||
"dependencies": [],
|
||||
"inputs": [source["path"]],
|
||||
"outputs": [],
|
||||
"errors": [],
|
||||
"sim_status": "planned",
|
||||
"web_status": "planned",
|
||||
"test_id": None,
|
||||
"oracle": None,
|
||||
"golden": None,
|
||||
"difference": None,
|
||||
"coverage_dimensions": [],
|
||||
"context_id": None,
|
||||
"exclusion_reason": None,
|
||||
"mapping_status": "needs_owner",
|
||||
}
|
||||
|
||||
|
||||
def write_jsonl(path: Path, entries: Iterable[dict[str, object]]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8", newline="\n") as handle:
|
||||
for entry in entries:
|
||||
handle.write(json.dumps(entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")))
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def generate(repo_root: Path, source_index: Path, manifest: Path) -> tuple[int, int]:
|
||||
sources = [make_entry(repo_root, kind, path) for kind, path in iter_files(repo_root)]
|
||||
sources.sort(key=lambda item: str(item["source_id"]))
|
||||
write_jsonl(source_index, sources)
|
||||
write_jsonl(manifest, (make_manifest_entry(source) for source in sources))
|
||||
return len(sources), len(sources)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--source-index", type=Path, default=Path("coverage/source-index.jsonl"))
|
||||
parser.add_argument("--manifest", type=Path, default=Path("coverage/manifest.jsonl"))
|
||||
args = parser.parse_args()
|
||||
source_count, manifest_count = generate(
|
||||
args.repo_root.resolve(),
|
||||
args.repo_root.resolve() / args.source_index,
|
||||
args.repo_root.resolve() / args.manifest,
|
||||
)
|
||||
print(f"source_index={source_count} manifest_seed={manifest_count}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user