aboutsummaryrefslogtreecommitdiffstats
path: root/llamachat_venv.py
diff options
context:
space:
mode:
Diffstat (limited to 'llamachat_venv.py')
-rw-r--r--llamachat_venv.py81
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:])