# SPDX-License-Identifier: GPL-2.0-only # # llamachat - a small native chat client for a local llama.cpp router # Copyright (C) 2026 Danilo M. # # 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:])