diff options
Diffstat (limited to 'llamachat_venv.py')
| -rw-r--r-- | llamachat_venv.py | 81 |
1 files changed, 81 insertions, 0 deletions
diff --git a/llamachat_venv.py b/llamachat_venv.py new file mode 100644 index 0000000..68fd981 --- /dev/null +++ b/llamachat_venv.py @@ -0,0 +1,81 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# llamachat - a small native chat client for a local llama.cpp router +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +"""Finding the interpreter that has PySide6. + +PySide6 is usually installed in a virtualenv rather than the system Python, +so a script started with `#!/usr/bin/env python3` has to hand over to that +interpreter. Keeping the search here means the launcher and the test script +agree on where to look, and neither hardcodes a path. + +`LLAMACHAT_PYTHON` names an interpreter explicitly. `LLAMACHAT_NO_REEXEC` +disables the handover, which is also how the re-executed process avoids +looping. +""" + +import os +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent + +# Checked in order; the first interpreter that exists wins. +CANDIDATES = ( + HERE / ".venv" / "bin" / "python3", + HERE / "venv" / "bin" / "python3", + Path.home() / ".local" / "share" / "llamachat-venv" / "bin" / "python3", +) + + +def have_pyside() -> bool: + try: + import PySide6 # noqa: F401 + except ImportError: + return False + return True + + +def find_interpreter() -> Path | None: + """The first candidate interpreter that exists and is not this one. + + Paths are compared unresolved on purpose. A venv created with + --system-site-packages has a bin/python3 that symlinks to the system + interpreter, so resolving both sides would make the venv look identical + to the interpreter we are trying to escape. What differs is sys.prefix, + which follows from the path used to start it, not from the real binary. + """ + override = os.environ.get("LLAMACHAT_PYTHON") + candidates = [Path(override)] if override else list(CANDIDATES) + current = Path(sys.executable) + for candidate in candidates: + if candidate.is_file() and candidate != current: + return candidate + return None + + +def reexec(script: str) -> None: + """Restart `script` under an interpreter that has PySide6. + + Returns normally when the current interpreter is already usable, when + the handover is disabled, or when no other interpreter was found; in + that last case the caller fails later with a clear ImportError. + """ + if os.environ.get("LLAMACHAT_NO_REEXEC") or have_pyside(): + return + interpreter = find_interpreter() + if interpreter is None: + return + # Set before exec so the new process cannot bounce again. + os.environ["LLAMACHAT_NO_REEXEC"] = "1" + path = str(Path(script).resolve()) + os.execv(str(interpreter), [str(interpreter), path] + sys.argv[1:]) |
