diff --git a/scripts/generate_event_schema.py b/scripts/generate_event_schema.py index 6c2a309dc..ba8c9ce49 100644 --- a/scripts/generate_event_schema.py +++ b/scripts/generate_event_schema.py @@ -13,6 +13,8 @@ import json import sys from pathlib import Path +from artifacts import write_text_atomic + ROOT = Path(__file__).resolve().parent.parent OUTPUT = ROOT / "schemas" / "install-events.schema.json" @@ -105,7 +107,7 @@ def main(argv: list[str] | None = None) -> int: print(f"{args.output} differs from EVENT_KEYS: run " "python scripts/generate_event_schema.py", file=sys.stderr) return 1 - args.output.write_text(text, encoding="utf-8") + write_text_atomic(str(args.output), text) print(f"Wrote {args.output}") return 0 diff --git a/scripts/generate_games_index.py b/scripts/generate_games_index.py index e61db4278..693178b59 100644 --- a/scripts/generate_games_index.py +++ b/scripts/generate_games_index.py @@ -59,6 +59,7 @@ from romfolders import ( # noqa: E402 folder_names, load_setups, ) +from artifacts import write_text_atomic # noqa: E402 from safeparse import yaml_load # noqa: E402 import install # noqa: E402 @@ -635,7 +636,7 @@ def main() -> int: path.unlink() for path in changed: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(files[path], encoding="utf-8") + write_text_atomic(str(path), files[path]) print(f"{len(changed)} written, {len(stale)} removed") return 1 if failures else 0 diff --git a/scripts/generate_installer_doc.py b/scripts/generate_installer_doc.py index 5775298e3..d3a1309a4 100644 --- a/scripts/generate_installer_doc.py +++ b/scripts/generate_installer_doc.py @@ -45,6 +45,8 @@ import zipfile from pathlib import Path, PurePosixPath from typing import Callable +from artifacts import write_text_atomic + ROOT = Path(__file__).resolve().parent.parent PAGE = ROOT / "wiki" / "installer.md" INSTALLER = ROOT / "install.py" @@ -643,7 +645,7 @@ def main(argv: list[str] | None = None) -> int: print(f"{args.page} differs from install.py and install/*.json: run " "python scripts/generate_installer_doc.py", file=sys.stderr) return 1 - args.page.write_text(text, encoding="utf-8") + write_text_atomic(str(args.page), text) print(f"Wrote {args.page}") return 0 diff --git a/scripts/generate_launcher_index.py b/scripts/generate_launcher_index.py index 7da8c7103..6592d26e4 100644 --- a/scripts/generate_launcher_index.py +++ b/scripts/generate_launcher_index.py @@ -22,6 +22,7 @@ import json import sys from pathlib import Path +from artifacts import write_text_atomic from build_port import ( BATOCERA_ZIP, MUOS_APP, @@ -121,7 +122,7 @@ def main(argv: list[str] | None = None) -> int: "python scripts/generate_launcher_index.py", file=sys.stderr) return 1 args.output.parent.mkdir(parents=True, exist_ok=True) - args.output.write_text(text, encoding="utf-8") + write_text_atomic(str(args.output), text) print(f"Wrote {args.output}") return 0 diff --git a/tests/test_atomic_artifacts.py b/tests/test_atomic_artifacts.py index f54363823..ad3868035 100644 --- a/tests/test_atomic_artifacts.py +++ b/tests/test_atomic_artifacts.py @@ -83,6 +83,27 @@ class EveryWriterGoesThroughIt(unittest.TestCase): ] self.assertEqual(offenders, []) + def test_generators_publish_whole_files(self): + """install/games/ (12 MB, read by install.py --for-games), the event + schema, the launcher index and wiki/installer.md were truncated at + their final name: an interrupted run left a committed-looking file + holding part of a JSON document. The installer doc's sandbox writes + throwaway fixtures under tmp/ and is not published.""" + import ast + + sandbox = {"write_small_list", "Sandbox"} + offenders = [] + for path in sorted((REPO_ROOT / "scripts").glob("generate_*.py")): + tree = ast.parse(path.read_text(encoding="utf-8")) + for top in tree.body: + if path.name == "generate_installer_doc.py" and getattr(top, "name", "") in sandbox: + continue + for node in ast.walk(top): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and node.func.attr in ("write_text", "write_bytes")): + offenders.append(f"{path.name}:{node.lineno}") + self.assertEqual(offenders, []) + def test_the_text_is_utf8_whatever_the_locale(self): import tempfile