From 1b4b31fb328b6d6da8ec2f40eaeac869b9e7368f Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:09:35 +0100 Subject: [PATCH] fix: run the read-only tools from the repo root Only verify.py and generate_pack.py moved to the repository root. From any other folder slots, region_audit, provenance_report and bundled_only said database.json was missing and to rebuild it, identity and list_platforms named a missing folder, and game_rules ended on a FileNotFoundError traceback. work_from_repo_root moves to common.py and the eight tools call it: defaults are read from the root, a path the user typed from where it was typed. --- scripts/bundled_only.py | 3 +++ scripts/common.py | 26 ++++++++++++++++++++++ scripts/cross_reference.py | 3 +++ scripts/game_rules.py | 3 +++ scripts/identity.py | 3 +++ scripts/list_platforms.py | 5 ++++- scripts/provenance_report.py | 3 +++ scripts/region_audit.py | 3 +++ scripts/slots.py | 3 +++ scripts/verify.py | 24 +------------------- tests/test_tool_messages.py | 43 ++++++++++++++++++++++++++++++++++++ 11 files changed, 95 insertions(+), 24 deletions(-) diff --git a/scripts/bundled_only.py b/scripts/bundled_only.py index d80d5bac5..bf4e34471 100644 --- a/scripts/bundled_only.py +++ b/scripts/bundled_only.py @@ -116,6 +116,9 @@ def main() -> int: parser.add_argument("--json", action="store_true", help="print the list as JSON") parser.add_argument("--strict", action="store_true", help="exit 1 while a file is listed") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir", "platforms_dir", "install_dir"]) rows = bundled_only( load_database(args.db), load_emulator_profiles(args.emulators_dir), diff --git a/scripts/common.py b/scripts/common.py index 7c2cbbbc1..02c559415 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable from collections import Counter +import argparse import filecmp import hashlib import functools @@ -670,6 +671,31 @@ def suggest_names( return f"{listing_flag} lists all {total}." +def work_from_repo_root( + args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] +) -> None: + """Run from the repository root, whichever directory the command started in. + + database.json records every file from the root and the data registry + names its caches the same way, so a run from scripts/ ended on a + FileNotFoundError traceback. A path the caller typed is read from where + they typed it, a default from the root. + """ + root = Path(__file__).resolve().parent.parent + if Path.cwd().resolve() == root: + return + for flag in path_flags: + value = getattr(args, flag, None) + if value and value != parser.get_default(flag): + # A Path stays a Path for the code that joins onto it. + setattr(args, flag, type(value)(os.path.abspath(value))) + os.chdir(root) + if getattr(args, "platform", None): + # Read before the move, the registry was not there to map + # "RetroArch" to its slug. + args.platform = platform_arg(args.platform) + + def platform_arg(value: str) -> str: """--platform as typed, mapped to the registry's slug when it names one. diff --git a/scripts/cross_reference.py b/scripts/cross_reference.py index f92545b27..365efefda 100644 --- a/scripts/cross_reference.py +++ b/scripts/cross_reference.py @@ -1191,6 +1191,9 @@ def main() -> None: "--list-emulators", action="store_true", help="List available emulators" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["emulators_dir", "platforms_dir", "db"]) if args.list_emulators: ignored = [flag for flag, on in ( ("--emulator", args.emulator), ("--platform", args.platform), diff --git a/scripts/game_rules.py b/scripts/game_rules.py index 040884889..d9512ba34 100644 --- a/scripts/game_rules.py +++ b/scripts/game_rules.py @@ -247,6 +247,9 @@ def main() -> int: help="profiles the rules are checked against (default emulators)") parser.add_argument("--json", action="store_true", help="print the summary as JSON") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["regions", "extras", "headers", "emulators_dir"]) regions = load_yaml(args.regions) extras = load_yaml(args.extras) diff --git a/scripts/identity.py b/scripts/identity.py index 745029f96..188e899d1 100644 --- a/scripts/identity.py +++ b/scripts/identity.py @@ -146,6 +146,9 @@ def main() -> int: "--strict", action="store_true", help="exit non-zero when any is found" ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["bios_dir"]) from common import require_directory # noqa: PLC0415 require_directory(args.bios_dir) diff --git a/scripts/list_platforms.py b/scripts/list_platforms.py index 82f8e4c31..88fa39319 100644 --- a/scripts/list_platforms.py +++ b/scripts/list_platforms.py @@ -26,7 +26,10 @@ def list_platforms() -> list[str]: def main() -> None: parser = argparse.ArgumentParser(allow_abbrev=False, description="List available platforms") - parser.parse_args() + args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, []) platforms = list_platforms() diff --git a/scripts/provenance_report.py b/scripts/provenance_report.py index a54c1b0a6..16ec2f5f8 100644 --- a/scripts/provenance_report.py +++ b/scripts/provenance_report.py @@ -384,6 +384,9 @@ def main() -> int: "(the pipeline writes derivable.json)", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "provenance_dir", "emulators_dir", "write_derivable"]) if args.write_derivable and not args.derive: parser.error("--write-derivable records what --derive finds: pass both") diff --git a/scripts/region_audit.py b/scripts/region_audit.py index eb02e2e6f..731e0d2a8 100644 --- a/scripts/region_audit.py +++ b/scripts/region_audit.py @@ -201,6 +201,9 @@ def main() -> int: ) parser.add_argument("--json", action="store_true", help="Machine-readable output") args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "emulators_dir"]) db = load_database(args.db) profiles = load_emulator_profiles(args.emulators_dir) diff --git a/scripts/slots.py b/scripts/slots.py index b261d6c74..8f62ed3e4 100644 --- a/scripts/slots.py +++ b/scripts/slots.py @@ -1009,6 +1009,9 @@ def main() -> int: "without it the run reports them and exits 0", ) args = parser.parse_args() + from common import work_from_repo_root # noqa: PLC0415 + + work_from_repo_root(args, parser, ["db", "platforms_dir", "emulators_dir"]) import contextlib diff --git a/scripts/verify.py b/scripts/verify.py index 700626e52..ef62d1116 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -41,6 +41,7 @@ from common import ( _foreign_tree, _owner_tokens, platform_arg, + work_from_repo_root, target_listing_line, suggest_names, system_index, @@ -130,29 +131,6 @@ DEFAULT_EMULATORS_DIR = "emulators" REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -def work_from_repo_root( - args: argparse.Namespace, parser: argparse.ArgumentParser, path_flags: list[str] -) -> None: - """Run from the repository root, whichever directory the command started in. - - database.json records every file from the root and the data registry - names its caches the same way, so a run from scripts/ ended on a - FileNotFoundError traceback. A path the caller typed is read from where - they typed it, a default from the root. - """ - if os.path.realpath(os.getcwd()) == os.path.realpath(REPO_ROOT): - return - for flag in path_flags: - value = getattr(args, flag, None) - if value and value != parser.get_default(flag): - setattr(args, flag, os.path.abspath(value)) - os.chdir(REPO_ROOT) - if getattr(args, "platform", None): - # Read before the move, the registry was not there to map - # "RetroArch" to its slug. - args.platform = platform_arg(args.platform) - - # Status model: aligned with Batocera BiosStatus (batocera-systems:967-969) diff --git a/tests/test_tool_messages.py b/tests/test_tool_messages.py index 62d56d5f2..07937e45a 100644 --- a/tests/test_tool_messages.py +++ b/tests/test_tool_messages.py @@ -188,6 +188,49 @@ class TheAllHelpNamesWhatItCovers(unittest.TestCase): self.assertEqual(stale, []) +class ReadOnlyToolsRunFromAnyFolder(unittest.TestCase): + """Only verify.py and generate_pack.py moved to the repository root. + From another folder slots, region_audit, provenance_report and + bundled_only said database.json was missing and to rebuild it, + identity and list_platforms named a missing folder, and game_rules + ended on a FileNotFoundError traceback.""" + + COMMANDS = ( + ["slots.py", "--platform", "retroarch"], + ["identity.py"], + ["cross_reference.py", "--emulator", "handy"], + ["list_platforms.py"], + ["region_audit.py"], + ["provenance_report.py"], + ["bundled_only.py"], + ["game_rules.py"], + ) + + def test_each_reads_the_repository_from_a_subfolder(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + for command in self.COMMANDS: + with self.subTest(command=command[0]): + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / command[0]), + *command[1:]], + cwd=folder, capture_output=True, text=True, timeout=300, + check=False, + ) + self.assertEqual(proc.returncode, 0, proc.stderr[-500:]) + self.assertNotIn("Traceback", proc.stderr) + self.assertEqual(os.listdir(folder), []) + + def test_a_typed_path_is_read_from_where_it_was_typed(self): + with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as folder: + proc = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "slots.py"), + "--db", "nosuch.json"], + cwd=folder, capture_output=True, text=True, timeout=300, check=False, + ) + self.assertEqual(proc.returncode, 1) + self.assertIn(str(Path(folder) / "nosuch.json"), proc.stderr) + + class AMistypedTargetAcrossAllPlatforms(unittest.TestCase): """`verify --all --target swich` ended on "no platform has it" after 17 INFO lines, two of which held the suggestion."""