fix: keep each installer error to one sentence

Six messages on stderr ran 26 to 35 words before the option to give. Each now says the problem and the remedy in at most 25 words, and a test measures every stderr message against that budget.
This commit is contained in:
Abdessamad Derraz committed 2026-11-10 02:53:07 +01:00
1 parent 07178f8c28
commit 63088b663f
5 files changed
+53 -27

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
# and Invoke-WebRequest throws on a cut transfer before the hash is read:
# the site's build rewrites this line there (scripts/sitebootstrap.py).
$mismatchAdvice = "The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
$defaultInstallSha256 = "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738"
$defaultInstallSha256 = "e9bfce57f1aab1123937498d7ed7913223762df755bb572b9a36f2d6a69b7a55"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
$maximumInstallerBytes = 8MB
+14 -17
View File
@@ -4423,9 +4423,9 @@ def fetch_manifest(plat: str) -> dict:
# request, so the revision is what has no such list.
print(f" No file list for {plat} at {RELEASE_REF}.", file=sys.stderr)
if RELEASE_REF != DEFAULT_RELEASE_REF:
print(f" RETROBIOS_REF={RELEASE_REF} names a tag that does not exist or "
f"predates {plat}: unset RETROBIOS_REF, or pick a tag from "
"https://github.com/Abdess/retrobios/releases.", file=sys.stderr)
print(f" RETROBIOS_REF={RELEASE_REF} has no list for {plat}: unset it, or "
"pick a newer tag from https://github.com/Abdess/retrobios/releases.",
file=sys.stderr)
else:
print(" The repository may be moving its files: run the same command "
"again in a few minutes.", file=sys.stderr)
@@ -4441,9 +4441,9 @@ def fetch_manifest(plat: str) -> dict:
message=words if code != "network"
else f"the file list could not be fetched: {words}")
if code == "offline":
print(f" No file list for {plat}: {offline_words()}. Give it with "
f"--manifest FILE (a copy of install/{plat}.json) or "
f"--from-pack PACK, or run {offline_undo()}.", file=sys.stderr)
print(f" No file list for {plat}: {offline_words()}. Give --manifest FILE "
f"(a copy of install/{plat}.json) or --from-pack PACK, or run "
f"{offline_undo()}.", file=sys.stderr)
sys.exit(1)
if code != "network":
print(f" Could not fetch the file list for {plat}.", file=sys.stderr)
@@ -4502,9 +4502,9 @@ def fetch_targets(plat: str) -> "dict | None":
if LOCAL.offline:
source = (f"{LOCAL.pack.name} carries none" if LOCAL.pack is not None
else f"{offline_named()} makes no request")
print(f"Error: --target reads the cores of each {display_name(plat)} board from a "
f"list, and {source}. Put install/targets/{plat}.json beside the file list "
"named with --manifest, or run with the network.", file=sys.stderr)
print(f"Error: no {display_name(plat)} board list here ({source}). Put "
f"install/targets/{plat}.json beside the --manifest file, or go online.",
file=sys.stderr)
sys.exit(2)
url = TARGETS_URL.format(platform=plat)
try:
@@ -14011,9 +14011,8 @@ def main() -> None:
and not (named and named not in TARGET_PLATFORMS)):
# No list can be at hand: a pack carries one, and so can the folder
# of a --manifest (targets/<platform>.json).
parser.error("--target and --list-targets read a core list from the network, "
f"which {offline_named()} does not use: name a pack with --from-pack, "
"or a file list with --manifest and targets/<platform>.json beside it")
parser.error(f"--target needs a core list, which {offline_named()} does not fetch: "
"give --from-pack, or --manifest with targets/<platform>.json beside it")
install_bounded_opener()
if getattr(args, "from") and (
args.list_platforms or args.list_targets or args.list_systems
@@ -15207,8 +15206,7 @@ def card_install_dest(named: str, plat: str, interactive: bool, ask: bool = True
message=f"{system} keeps its BIOS files on its SD card, and no "
"card was named with --dest")
print(f" {system} keeps its BIOS files on its SD card, not on this machine. "
f"Mount the card and name its root with --dest, for example: {example}",
file=sys.stderr)
f"Mount it and give its root with --dest: {example}", file=sys.stderr)
sys.exit(1)
print(f" {system} keeps its BIOS files {card_where(system, setup.below)}, not on "
"this machine. "
@@ -15393,9 +15391,8 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
kind = _output_kind(real_stdout)
where = f"Output goes to a {kind}" if kind else "Output is not a terminal"
undo = "the pipe" if kind == "pipe" else "the redirection"
print(f"{where}, so nothing is asked: the run goes on as with --no-input. "
f"Add --no-input to say so, or run it without {undo} to answer.",
file=sys.stderr)
print(f"{where}, so nothing is asked. Add --no-input to say so, or run it "
f"without {undo} to answer.", file=sys.stderr)
OUTPUT.verbose = bool(getattr(args, "verbose", False))
OUTPUT.where_said = False
detail(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n")
+1 -1
View File
@@ -23,7 +23,7 @@ DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/ins
# curl or wget stop on a cut transfer before the hash is read: the site's
# build rewrites this line there (scripts/sitebootstrap.py).
MISMATCH_ADVICE="The download was most likely cut short: try again. Within minutes of an update of this project the two files can come from caches of different ages. Waiting five minutes settles that."
DEFAULT_INSTALL_SHA256="61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738"
DEFAULT_INSTALL_SHA256="e9bfce57f1aab1123937498d7ed7913223762df755bb572b9a36f2d6a69b7a55"
# The pin above refuses any changed byte: this cap only stops a body that
# cannot be the installer (a sign-in portal's page) before it is hashed.
MAX_INSTALLER_BYTES=8388608
+8 -8
View File
@@ -27,8 +27,8 @@
{
"dest": "roms/ports/retrobios/install.py",
"repo_path": "install.py",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"size": 1142976,
"sha256": "e9bfce57f1aab1123937498d7ed7913223762df755bb572b9a36f2d6a69b7a55",
"executable": false
},
{
@@ -71,8 +71,8 @@
"muos": {
"archive": {
"dest": "ARCHIVE/RetroBIOS.muxapp",
"size": 1196883,
"sha256": "2c84115b87ed4bcd4fdf4d64d56fc1221485cbd9822cfeb0770b07b9b4226e51"
"size": 1196601,
"sha256": "9305dd087110937f9c4c2d08cf4b52aa55dc0c8835109991f913e23ca30f3926"
},
"members": [
{
@@ -92,8 +92,8 @@
{
"name": "RetroBIOS/install.py",
"repo_path": "install.py",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"size": 1142976,
"sha256": "e9bfce57f1aab1123937498d7ed7913223762df755bb572b9a36f2d6a69b7a55",
"executable": false
},
{
@@ -166,8 +166,8 @@
{
"dest": ".config/modules/retrobios/install.py",
"repo_path": "install.py",
"size": 1143258,
"sha256": "61176387d38c67e596c1669fee2bb3b981333ab0b808dd2e280a5a3870957738",
"size": 1142976,
"sha256": "e9bfce57f1aab1123937498d7ed7913223762df755bb572b9a36f2d6a69b7a55",
"executable": false
},
{
+29
View File
@@ -7677,5 +7677,34 @@ class APackageHintNamesThePackageCommand(unittest.TestCase):
"uvx retrobios --platform retroarch --dest DIR"))
class AnErrorIsOneShortSentence(unittest.TestCase):
"""A message on stderr says what to do in a few words: one of 35 words
asked a beginner to read a paragraph to find the option to give."""
BUDGET = 25
def test_every_stderr_message_is_short(self):
import ast
tree = ast.parse((REPO_ROOT / "install.py").read_text(encoding="utf-8"))
long = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
name = getattr(node.func, "id", getattr(node.func, "attr", ""))
to_err = any(k.arg == "file" and "stderr" in ast.unparse(k.value)
for k in node.keywords)
if not (to_err or name == "error"):
continue
text = " ".join(ast.unparse(a) for a in node.args)
if ".command(" in text:
continue
prose = re.sub(r"\{[^}]*\}", "X", text)
words = len(re.findall(r"[A-Za-z']+", prose))
if words > self.BUDGET:
long.append((node.lineno, words))
self.assertEqual(long, [])
if __name__ == "__main__":
unittest.main()