From e003828e26186538eec7ebe2757b3eb20612dc69 Mon Sep 17 00:00:00 2001
From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com>
Date: Fri, 23 Oct 2026 22:30:47 +0200
Subject: [PATCH] fix: ask nothing when the output goes to a file
install.py > log.txt at a terminal wrote the menu into the file and
waited for an answer nobody saw. A question now needs stdin and stdout on
a terminal, otherwise the run goes on as with --no-input and says so.
---
install.py | 15 +++++++++++++++
tests/test_install_experience.py | 24 ++++++++++++++++++++++++
wiki/installer.md | 2 +-
3 files changed, 40 insertions(+), 1 deletion(-)
diff --git a/install.py b/install.py
index e857d7ac..cc258bf0 100644
--- a/install.py
+++ b/install.py
@@ -8933,6 +8933,14 @@ def _unknown_target(target: str, plat: str, targets: dict) -> None:
print(f" {plat} publishes no target list", file=sys.stderr)
+def _is_terminal(stream: TextIO) -> bool:
+ isatty = getattr(stream, "isatty", None)
+ try:
+ return bool(isatty and isatty())
+ except (OSError, ValueError):
+ return False
+
+
def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
"""One run of the installer, once the options are read."""
KEPT.reset()
@@ -8940,6 +8948,13 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None:
interactive = sys.stdin.isatty() and not args.no_input and not args.quiet
listing = (args.list_targets or args.list_systems or args.list_cores
or args.list_emulators)
+ if interactive and not _is_terminal(real_stdout):
+ # A question goes to stdout: sent to a file, it waited on an answer
+ # to a prompt nobody could see.
+ interactive = False
+ if not listing and not args.check:
+ print("Output goes to a file, so nothing is asked: the run goes on as "
+ "with --no-input.", file=sys.stderr)
print(f"{term.bold('RetroBIOS')} {term.dim(installer_id())}\n")
os_type = detect_os()
diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py
index 76dc68f4..2a74b539 100644
--- a/tests/test_install_experience.py
+++ b/tests/test_install_experience.py
@@ -566,6 +566,30 @@ class AFolderThisUserCannotEnter(unittest.TestCase):
self.assertEqual(state, "wrong_hash")
+class OutputToAFileAsksNothing(unittest.TestCase):
+ """`install.py > log.txt` at a terminal: the menu went into the file
+ and the run waited on an answer to a prompt nobody could see."""
+
+ @unittest.skipIf(os.name == "nt", "a pseudo-terminal for stdin")
+ def test_the_run_goes_on_as_with_no_input(self):
+ import pty
+
+ master, slave = pty.openpty()
+ try:
+ with FakeRepository() as repo:
+ proc = subprocess.run(
+ repo.command(), env=repo.env(), stdin=slave, capture_output=True,
+ text=True, timeout=60, check=False)
+ installed = repo.installed()
+ finally:
+ os.close(master)
+ os.close(slave)
+ self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
+ self.assertIn("nothing is asked", proc.stderr)
+ self.assertNotIn("Enter) install everything", proc.stdout)
+ self.assertEqual(len(installed), len(FakeRepository.PAYLOADS))
+
+
class _Tty(io.StringIO):
def isatty(self) -> bool:
return True
diff --git a/wiki/installer.md b/wiki/installer.md
index cb1dcedf..a3efbc96 100644
--- a/wiki/installer.md
+++ b/wiki/installer.md
@@ -701,7 +701,7 @@ what the options name, everything by default.
| `--list-cores` | Print the cores whose extra files a platform installs |
| `--list-emulators` | Print every name `--emulator` accepts, each buildbot name beside the emulator it stands for |
| `--romm-firmware TEMPLATE` | RomM: where each platform's firmware goes in the library, as `filesystem.structure.firmware` in RomM's `config.yml` says (`{platform}/bios` for a library laid out as `{platform}/roms`). Read from `config.yml` beside the library when it is there. See [RomM firmware layout](#romm-firmware-layout) |
-| `--no-input` | Never ask a question, even at a terminal |
+| `--no-input` | Never ask a question, even at a terminal. A run whose output goes to a file or a pipe asks nothing either, and says so on stderr |
| `--check` | Report and exit without writing |
| `--dry-run` | Show what would be downloaded, the totals first, then by system and with sizes, without writing anything. A plan taller than the screen names the ten largest systems, and `--verbose` lists every system and file |
| `--yes`, `-y` | Do not ask before a download of 1 GB or more |