diff --git a/misc/meson.build b/misc/meson.build index a8f09722c..bf3c157f7 100644 --- a/misc/meson.build +++ b/misc/meson.build @@ -4,3 +4,9 @@ subdir('zsh') subdir('systemd') subdir('flake-registry') + +runinpty = configure_file( + copy : true, + input : meson.current_source_dir() / 'runinpty.py', + output : 'runinpty.py', +) diff --git a/misc/runinpty.py b/misc/runinpty.py new file mode 100755 index 000000000..a7649b735 --- /dev/null +++ b/misc/runinpty.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved +# SPDX-FileCopyrightText: 2024 Jade Lovelace +# SPDX-License-Identifier: LGPL-2.1-or-later +""" +This script exists to lose Lix a dependency on expect(1) for the ability to run +something in a pty. + +Yes, it could be replaced by script(1) but macOS and Linux script(1) have +diverged sufficiently badly that even specifying a subcommand to run is not the +same. +""" +import pty +import sys +import os +from termios import ONLCR, ONLRET, ONOCR, OPOST, TCSAFLUSH, tcgetattr, tcsetattr +from tty import setraw +import termios + +def setup_terminal(): + # does not matter which fd we use because we are in a fresh pty + modi = tcgetattr(pty.STDOUT_FILENO) + [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] = modi + + # Turning \n into \r\n is not cool, Linux! + oflag &= ~ONLCR + # I don't know what "implementation dependent postprocessing means" but it + # sounds bad + oflag &= ~OPOST + # Assume that NL performs the role of CR; do not insert CRs at column 0 + oflag |= ONLRET | ONOCR + + modi = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + + tcsetattr(pty.STDOUT_FILENO, TCSAFLUSH, modi) + + +def spawn(argv: list[str]): + """ + As opposed to pty.spawn, this one more seriously controls the pty settings. + Necessary to turn off such fun functionality as onlcr (LF to CRLF). + + This is essentially copy pasted from pty.spawn, since there is no way to + hook the child pre-execve + """ + pid, master_fd = pty.fork() + if pid == pty.CHILD: + setup_terminal() + os.execlp(argv[0], *argv) + + try: + mode = tcgetattr(pty.STDIN_FILENO) + setraw(pty.STDIN_FILENO) + restore = True + except termios.error: + restore = False + + try: + pty._copy(master_fd, pty._read, pty._read) # type: ignore + finally: + if restore: + tcsetattr(pty.STDIN_FILENO, TCSAFLUSH, mode) # type: ignore + + os.close(master_fd) + return os.waitpid(pid, 0)[1] + + +def main(): + if len(sys.argv) == 1: + print(f'Usage: {sys.argv[0]} [command args]', file=sys.stderr) + sys.exit(1) + + sys.exit(os.waitstatus_to_exitcode(spawn(sys.argv[1:]))) + + +if __name__ == '__main__': + main() diff --git a/package.nix b/package.nix index 18c8475bb..295e9139f 100644 --- a/package.nix +++ b/package.nix @@ -20,7 +20,6 @@ doxygen, editline-lix ? __forDefaults.editline-lix, editline, - expect, git, gtest, jq, @@ -275,8 +274,6 @@ stdenv.mkDerivation (finalAttrs: { # configure, but we don't actually want to *run* the checks here. ++ lib.optionals lintInsteadOfBuild finalAttrs.checkInputs; - nativeCheckInputs = [ expect ]; - checkInputs = [ gtest rapidcheck diff --git a/tests/functional/common/vars-and-functions.sh.in b/tests/functional/common/vars-and-functions.sh.in index eda15308d..451cf5383 100644 --- a/tests/functional/common/vars-and-functions.sh.in +++ b/tests/functional/common/vars-and-functions.sh.in @@ -234,6 +234,10 @@ enableFeatures() { sed -i 's/experimental-features .*/& '"$features"'/' "$NIX_CONF_DIR"/nix.conf } +runinpty() { + @python@ @runinpty@ "$@" +} + set -x onError() { diff --git a/tests/functional/flakes/show.sh b/tests/functional/flakes/show.sh index 25f481575..857c77ae1 100644 --- a/tests/functional/flakes/show.sh +++ b/tests/functional/flakes/show.sh @@ -104,7 +104,8 @@ cat >flake.nix< show-output.txt ' diff --git a/tests/functional/meson.build b/tests/functional/meson.build index 2b5dfe422..fb8d77a57 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -7,6 +7,8 @@ test_confdata = { 'sandbox_shell': busybox.found() ? busybox.full_path() : '', 'PACKAGE_VERSION': meson.project_version(), 'system': host_system, + 'python': python.full_path(), + 'runinpty': runinpty.full_path(), } # Just configures `common/vars-and-functions.sh.in`.