diff --git a/pyproject.toml b/pyproject.toml index f158f2deb..bf5a325ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ description = "BIOS collection manager for retrogaming platforms" # install.py is the only entry point a user runs before cloning, and both # bootstraps accept 3.8, so the floor stays where the bootstraps put it. That # floor covers install.py alone: the build scripts under scripts/ need 3.9 -# (pipeline.py checks it), and CI runs the test suite on 3.12. +# (common.py, which every build script imports, checks it), and CI runs the +# test suite on 3.12. requires-python = ">=3.8" dependencies = [ "pyyaml", diff --git a/scripts/campaign.py b/scripts/campaign.py index fd014113a..1021aee9f 100644 --- a/scripts/campaign.py +++ b/scripts/campaign.py @@ -83,14 +83,17 @@ from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path -import yaml - sys.path.insert(0, str(Path(__file__).resolve().parent)) +# First: core_probe stops an interpreter older than 3.9 before anything else +# fails on it. +import core_probe + +import yaml + import autorun import campaign_profiles import campaign_rules as rules -import core_probe import measurement_records as mr ROOT = Path(__file__).resolve().parent.parent diff --git a/scripts/common.py b/scripts/common.py index 2bf7ee504..80eb59564 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -20,6 +20,15 @@ import sys import zipfile from pathlib import Path +# Every build script imports this module. verify.py, slots.py, validate_pr.py +# and cross_reference.py started under 3.8 and stopped halfway on a +# str.removeprefix AttributeError, which named no version. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + try: import yaml diff --git a/scripts/core_probe.py b/scripts/core_probe.py index 4d3cc7f1c..d58142c78 100644 --- a/scripts/core_probe.py +++ b/scripts/core_probe.py @@ -40,6 +40,14 @@ from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, field from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + ROOT = Path(__file__).resolve().parent.parent MIN_BENCH_REPETITIONS = 5 DEFAULT_WORK_DIR = ROOT / "tmp" / "core_probe" diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22ac647b6..67898e5ec 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -14,7 +14,6 @@ Usage: from __future__ import annotations -from siterender import SECTION_LABELS, WIKI_SRC_DIR import argparse import csv import hashlib @@ -64,6 +63,7 @@ from common import ( unique_emulator_profiles, yaml_load, ) +from siterender import SECTION_LABELS, WIKI_SRC_DIR from artifacts import ArtifactLockBusy, artifact_lock from artifacts import json_text as _json_text from cross_reference import FileConsumers, entry_target, held_copy diff --git a/scripts/region_check.py b/scripts/region_check.py index b7eff324f..6fa9a9019 100644 --- a/scripts/region_check.py +++ b/scripts/region_check.py @@ -36,6 +36,14 @@ import urllib.request from collections.abc import Callable, Iterable from pathlib import Path +# The probe tools do not import common.py, which states the 3.9 floor for the +# other scripts. Under 3.8 they stopped on a str.removesuffix AttributeError. +if sys.version_info < (3, 9): + sys.exit( + f"{Path(sys.argv[0]).name} needs Python 3.9 or newer, " + f"this is {sys.version.split()[0]}" + ) + sys.path.insert(0, str(Path(__file__).resolve().parent)) import yaml # noqa: E402 diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 11c6bcde6..6d97d07d7 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -136,6 +136,41 @@ class ReadmeRegressions(unittest.TestCase): self.assertNotIn("SyntaxError", proc.stderr) self.assertIn("generate_pack.py needs Python 3.9", proc.stderr) + def test_every_script_names_the_floor_under_an_older_interpreter(self): + """Only generate_pack.py and pipeline.py checked the version. verify.py, + slots.py, validate_pr.py and cross_reference.py started under 3.8 and + stopped halfway on a str.removeprefix AttributeError.""" + import shutil # noqa: PLC0415 + + python38 = shutil.which("python3.8") + if python38 is None: + if os.environ.get("CI"): + self.fail("CI must provide python3.8") + self.skipTest("no python3.8 on PATH") + commands = ( + ["verify.py", "--emulator", "handy"], + ["verify.py", "--platform", "batocera"], + ["validate_pr.py", "--json", "README.md"], + ["cross_reference.py", "--emulator", "handy"], + ["slots.py"], + ["identity.py"], + ["generate_site.py", "--help"], + ["campaign.py", "--help"], + ["region_check.py", "--help"], + ) + for command in commands: + with self.subTest(command=" ".join(command)): + # -E -s rather than -I: -I drops the script's folder from + # sys.path, so the imports would fail before any check. + proc = subprocess.run( + [python38, "-E", "-s", f"scripts/{command[0]}", *command[1:]], + cwd=ROOT, capture_output=True, text=True, timeout=60, + check=False, + ) + self.assertEqual(proc.returncode, 1, proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + self.assertIn(f"{command[0]} needs Python 3.9", proc.stderr) + def test_the_floor_interpreter_imports_every_script(self): """Every module under scripts/ is loaded by a real interpreter of the floor version, as the installer's 3.8 floor is proven by running it.