build: remove expect as a dependency

I was packaging Lix 2.91 for nixpkgs and was annoyed at the expect
dependency. Turns out that you can replace unbuffer with a pretty-short
Python script.

It became less short after I found out that Linux was converting \n to
\r\n in the terminal subsystem, which was not very funny, but is at
least solved by twiddling termios bits.

Change-Id: I8a2700abcbbf6a9902e01b05b40fa9340c0ab90c
This commit is contained in:
Jade Lovelace 2024-08-09 23:24:17 -07:00
parent 292567e0b0
commit 0c76195351
6 changed files with 91 additions and 4 deletions

View file

@ -4,3 +4,9 @@ subdir('zsh')
subdir('systemd') subdir('systemd')
subdir('flake-registry') subdir('flake-registry')
runinpty = configure_file(
copy : true,
input : meson.current_source_dir() / 'runinpty.py',
output : 'runinpty.py',
)

77
misc/runinpty.py Executable file
View file

@ -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()

View file

@ -20,7 +20,6 @@
doxygen, doxygen,
editline-lix ? __forDefaults.editline-lix, editline-lix ? __forDefaults.editline-lix,
editline, editline,
expect,
git, git,
gtest, gtest,
jq, jq,
@ -275,8 +274,6 @@ stdenv.mkDerivation (finalAttrs: {
# configure, but we don't actually want to *run* the checks here. # configure, but we don't actually want to *run* the checks here.
++ lib.optionals lintInsteadOfBuild finalAttrs.checkInputs; ++ lib.optionals lintInsteadOfBuild finalAttrs.checkInputs;
nativeCheckInputs = [ expect ];
checkInputs = [ checkInputs = [
gtest gtest
rapidcheck rapidcheck

View file

@ -234,6 +234,10 @@ enableFeatures() {
sed -i 's/experimental-features .*/& '"$features"'/' "$NIX_CONF_DIR"/nix.conf sed -i 's/experimental-features .*/& '"$features"'/' "$NIX_CONF_DIR"/nix.conf
} }
runinpty() {
@python@ @runinpty@ "$@"
}
set -x set -x
onError() { onError() {

View file

@ -104,7 +104,8 @@ cat >flake.nix<<EOF
}; };
} }
EOF EOF
unbuffer sh -c '
runinpty sh -c '
stty rows 20 cols 100 stty rows 20 cols 100
nix flake show > show-output.txt nix flake show > show-output.txt
' '

View file

@ -7,6 +7,8 @@ test_confdata = {
'sandbox_shell': busybox.found() ? busybox.full_path() : '', 'sandbox_shell': busybox.found() ? busybox.full_path() : '',
'PACKAGE_VERSION': meson.project_version(), 'PACKAGE_VERSION': meson.project_version(),
'system': host_system, 'system': host_system,
'python': python.full_path(),
'runinpty': runinpty.full_path(),
} }
# Just configures `common/vars-and-functions.sh.in`. # Just configures `common/vars-and-functions.sh.in`.