diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index e8d03340..5ba4cc83 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -341,6 +341,9 @@ class FileConsumers: # Where each platform puts the file in its BIOS directory. self.platform_destinations: dict[str, dict[str, set[str]]] = {} self.emulators: dict[str, set[str]] = {} + # Where each platform's pack installs the file, its cores' files + # included: the platform's own list names a fraction of them. + self.installed: dict[str, dict[str, set[str]]] = {} def sha1_of(self, entry: dict, dest: str = "", owner: str = "") -> str | None: """The collected file an entry resolves to, or None. @@ -378,6 +381,22 @@ class FileConsumers: name, set() ).add(where) + def add_manifests(self, install_dir: str | Path) -> None: + """Read what each install manifest places, alternatives included.""" + for path in sorted(Path(install_dir).glob("*.json")): + try: + manifest = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + print(f"WARNING: {path}: {exc}", file=sys.stderr) + continue + platform = str(manifest.get("platform") or path.stem) + for entry in manifest.get("files") or []: + for sha1 in {entry.get("sha1"), *( + alt.get("sha1") for alt in entry.get("alternatives") or [] + )} - {None, ""}: + self.installed.setdefault(sha1, {}).setdefault( + platform, set()).add(str(entry.get("dest", ""))) + def path_of(self, sha1: str) -> str: """The collection path the database records for a file.""" return str(self._db.get("files", {}).get(sha1, {}).get("path", "")) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 2ee9a1b7..d2252d92 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2325,6 +2325,18 @@ def _platform_links(sha1: str, consumers: FileConsumers) -> list[str]: return links +def _installed_links(sha1: str, consumers: FileConsumers, declared: set[str]) -> list[str]: + """Each pack installing the file that its platform's list does not name, + with the place the pack gives it.""" + links = [] + for platform, dests in sorted(getattr(consumers, "installed", {}).get(sha1, {}).items()): + if platform in declared: + continue + where = ", ".join(_code(d) for d in sorted(dests) if d) + links.append(f"[{platform}](../platforms/{platform}.md)" + (f" {where}" if where else "")) + return links + + def _system_file_card(f: dict, consumers: FileConsumers) -> list[str]: """One file of a system page: hashes, who reads it, its provenance.""" name = f["name"] @@ -2364,6 +2376,12 @@ def _system_file_card(f: dict, consumers: FileConsumers) -> list[str]: # that platform puts the file: from the card, retrodeck sent the # reader to the top of a 2,000-file page. lines.append(f"- Platforms: {', '.join(_platform_links(sha1_full, consumers))}") + installed = _installed_links(sha1_full, consumers, set(plats)) + if installed: + # The packs that carry the file for their cores: a RetroBat user + # looking up scph5501.bin found every platform but RetroBat, whose + # pack installs it for six cores. + lines.append(f"- Also installed by: {', '.join(installed)}") if emus: lines.append(f"- Emulators: {', '.join(_emulator_link(e, '../') for e in emus)}") provenance = f.get("provenance", {}) @@ -4117,6 +4135,7 @@ def _build_file_consumers(db: dict, coverages: dict, profiles: dict) -> FileCons consumers = FileConsumers(db, build_zip_contents_index(db)) consumers.add_platforms({name: cov["config"] for name, cov in coverages.items()}) consumers.add_emulators(profiles) + consumers.add_manifests(Path(__file__).resolve().parent.parent / "install") return consumers diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index 3d014742..ab331ff1 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -678,5 +678,29 @@ class AFileHasAnAddressByContent(unittest.TestCase): self.assertIn("../../files/?sha1=" + "ab" * 20, card) +class ACardNamesThePacksThatInstallIt(unittest.TestCase): + """A file card named only the platforms whose own list declares the + file: a RetroBat user found no RetroBat on scph5501.bin, which its pack + installs for six cores.""" + + def test_a_manifest_destination_reaches_the_card(self): + import json as _json + + from cross_reference import FileConsumers + from generate_site import _installed_links + + with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as tmp: + Path(tmp, "retrobat.json").write_text(_json.dumps({ + "platform": "retrobat", + "files": [{"dest": "scph5501.bin", "sha1": "b" * 40, + "alternatives": [{"sha1": "a" * 40}]}], + })) + consumers = FileConsumers({"files": {}}) + consumers.add_manifests(tmp) + links = _installed_links("a" * 40, consumers, set()) + self.assertEqual(links, ["[retrobat](../platforms/retrobat.md) `scph5501.bin`"]) + self.assertEqual(_installed_links("a" * 40, consumers, {"retrobat"}), []) + + if __name__ == "__main__": unittest.main()