bins.py: host-only Python/Lua in sandboxed bundles
_which() probes host dirs only in Flatpak (/run/host/usr/bin) and AppImage (/usr/bin); apply_host_libs prepares env for host subprocesses (prepend host libs in Flatpak, strip $APPDIR pollution + PYTHONHOME in AppImage); user override resolved via _which() for bare names.
This commit is contained in:
@@ -17,7 +17,7 @@ Public API
|
||||
``reset()`` : clear the cache (mostly useful for tests)
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import api.testium as tm
|
||||
@@ -30,10 +30,126 @@ from runtime.tum_except import ETUMRuntimeError
|
||||
_PYTHON_CANDIDATES = ["python3", "python"]
|
||||
_LUA_CANDIDATES = ["lua", "lua5.5", "lua5.4", "lua5.3", "lua5.2", "lua5.1"]
|
||||
|
||||
# When running inside a Flatpak, --filesystem=host-os mounts the host at
|
||||
# /run/host (read-only). Binaries and libraries from the host are not on the
|
||||
# sandbox PATH/LD_LIBRARY_PATH, so we probe and inject them explicitly.
|
||||
_FLATPAK_HOST_DIRS = [
|
||||
"/run/host/usr/local/bin",
|
||||
"/run/host/usr/bin",
|
||||
"/run/host/bin",
|
||||
]
|
||||
_FLATPAK_HOST_LIB_DIRS = [
|
||||
"/run/host/usr/lib",
|
||||
"/run/host/usr/lib64",
|
||||
"/run/host/usr/local/lib",
|
||||
]
|
||||
|
||||
# Inside an AppImage, AppRun prepends $APPDIR/usr/bin to PATH and exports a
|
||||
# bundle-local PYTHONHOME / PYTHONPATH / LD_LIBRARY_PATH. We want py_func and
|
||||
# lua_func to run under the *host* interpreter (not the bundled one), so we
|
||||
# probe standard host bin dirs directly and scrub APPDIR-prefixed entries from
|
||||
# the env passed to host subprocesses.
|
||||
_APPIMAGE_HOST_DIRS = [
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
]
|
||||
|
||||
|
||||
def _in_flatpak():
|
||||
return os.path.isfile("/.flatpak-info")
|
||||
|
||||
|
||||
def _in_appimage():
|
||||
return "APPIMAGE" in os.environ
|
||||
|
||||
|
||||
def apply_host_lua_paths(env):
|
||||
"""Prepend host Lua module dirs to LUA_PATH / LUA_CPATH (Flatpak only).
|
||||
|
||||
Must be called after user-defined lua_env overrides are applied, so host
|
||||
paths are always first regardless of user config. User-defined paths remain
|
||||
in the variable but after the host ones.
|
||||
"""
|
||||
if not _in_flatpak():
|
||||
return
|
||||
_LUA_VERSIONS = ["5.5", "5.4", "5.3", "5.2", "5.1"]
|
||||
_HOST = "/run/host/usr"
|
||||
cpath_dirs, lpath_dirs = [], []
|
||||
for v in _LUA_VERSIONS:
|
||||
for base in [f"{_HOST}/lib/lua/{v}",
|
||||
f"{_HOST}/lib64/lua/{v}",
|
||||
f"{_HOST}/lib/x86_64-linux-gnu/lua/{v}"]:
|
||||
cpath_dirs.append(f"{base}/?.so")
|
||||
lpath_dirs.append(f"{_HOST}/share/lua/{v}/?.lua")
|
||||
lpath_dirs.append(f"{_HOST}/share/lua/{v}/?/init.lua")
|
||||
sep = ";"
|
||||
host_cpath = sep.join(cpath_dirs)
|
||||
host_lpath = sep.join(lpath_dirs)
|
||||
# ;; keeps Lua's compiled-in defaults at the end as last resort
|
||||
env["LUA_CPATH"] = host_cpath + sep + env.get("LUA_CPATH", ";;")
|
||||
env["LUA_PATH"] = host_lpath + sep + env.get("LUA_PATH", ";;")
|
||||
|
||||
|
||||
def apply_host_libs(env):
|
||||
"""Prepare *env* for launching a host binary from inside our bundle.
|
||||
|
||||
- Flatpak: prepend host library dirs to LD_LIBRARY_PATH so the dynamic
|
||||
linker can find host .so files mounted under /run/host.
|
||||
- AppImage: strip $APPDIR-prefixed entries from LD_LIBRARY_PATH and
|
||||
PYTHONPATH and drop PYTHONHOME, so the host interpreter doesn't try
|
||||
to load the bundled (incompatible) Python lib/site-packages.
|
||||
- Otherwise: no-op.
|
||||
"""
|
||||
if _in_flatpak():
|
||||
dirs = ":".join(d for d in _FLATPAK_HOST_LIB_DIRS if os.path.isdir(d))
|
||||
if dirs:
|
||||
existing = env.get("LD_LIBRARY_PATH", "")
|
||||
env["LD_LIBRARY_PATH"] = dirs + (":" + existing if existing else "")
|
||||
return
|
||||
if _in_appimage():
|
||||
appdir = os.environ.get("APPDIR", "")
|
||||
if appdir:
|
||||
for var, sep in (("LD_LIBRARY_PATH", ":"),
|
||||
("PYTHONPATH", os.pathsep),
|
||||
("PATH", os.pathsep)):
|
||||
cur = env.get(var, "")
|
||||
if not cur:
|
||||
continue
|
||||
cleaned = sep.join(
|
||||
p for p in cur.split(sep)
|
||||
if p and not p.startswith(appdir)
|
||||
)
|
||||
if cleaned:
|
||||
env[var] = cleaned
|
||||
else:
|
||||
env.pop(var, None)
|
||||
env.pop("PYTHONHOME", None)
|
||||
|
||||
|
||||
def _which(name):
|
||||
func = sys_app_path_win if tm.OS() == "Windows" else sys_app_path_lin
|
||||
return func(name)
|
||||
if tm.OS() == "Windows":
|
||||
return sys_app_path_win(name)
|
||||
if _in_flatpak():
|
||||
for d in _FLATPAK_HOST_DIRS:
|
||||
p = os.path.join(d, name)
|
||||
if os.path.isfile(p) and os.access(p, os.X_OK):
|
||||
return p
|
||||
return ""
|
||||
if _in_appimage():
|
||||
for d in _APPIMAGE_HOST_DIRS:
|
||||
p = os.path.join(d, name)
|
||||
if os.path.isfile(p) and os.access(p, os.X_OK):
|
||||
return p
|
||||
return ""
|
||||
return sys_app_path_lin(name)
|
||||
|
||||
|
||||
def _probe_env():
|
||||
"""Subprocess env for probing host binaries (adds host libs in Flatpak)."""
|
||||
env = os.environ.copy()
|
||||
apply_host_libs(env)
|
||||
return env
|
||||
|
||||
|
||||
def _python_version(path):
|
||||
@@ -41,7 +157,7 @@ def _python_version(path):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
cmd, capture_output=True, text=True,
|
||||
encoding=tm.sys_encoding(), timeout=10,
|
||||
encoding=tm.sys_encoding(), timeout=10, env=_probe_env(),
|
||||
)
|
||||
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
@@ -60,6 +176,7 @@ def _lua_version(path):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[path, "-v"], capture_output=True, text=True, timeout=10,
|
||||
env=_probe_env(),
|
||||
)
|
||||
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
@@ -97,8 +214,16 @@ def _resolve(name):
|
||||
|
||||
path = ""
|
||||
if override:
|
||||
if shutil.which(override) and validator(override):
|
||||
path = override
|
||||
# Absolute path: accept as-is (user knows exactly what they want).
|
||||
# Bare name: resolve via _which() so the override stays host-only in
|
||||
# Flatpak/AppImage instead of silently picking the bundled interpreter.
|
||||
if os.path.isabs(override):
|
||||
resolved = override if (os.path.isfile(override)
|
||||
and os.access(override, os.X_OK)) else ""
|
||||
else:
|
||||
resolved = _which(override)
|
||||
if resolved and validator(resolved):
|
||||
path = resolved
|
||||
else:
|
||||
tm.print_warn(
|
||||
f"Configured {display} interpreter '{override}' is not usable; "
|
||||
|
||||
@@ -60,6 +60,7 @@ class LuaProcessBase:
|
||||
|
||||
lua_env = tm.gd("lua_env", {})
|
||||
env = os.environ.copy()
|
||||
bins.apply_host_libs(env)
|
||||
if not isinstance(lua_env, dict):
|
||||
raise ETUMRuntimeError(f"The 'lua_env' global value should be a dictionary. But it is '{lua_env}'.")
|
||||
|
||||
@@ -70,6 +71,7 @@ class LuaProcessBase:
|
||||
env[k] = e
|
||||
else:
|
||||
env[k] = e + ";" + env.get(k, "")
|
||||
bins.apply_host_lua_paths(env)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(("localhost", 0))
|
||||
|
||||
@@ -42,6 +42,10 @@ class PyProcessBase:
|
||||
raise ETUMRuntimeError(f"The 'py_env' global value should be a dictionary. But it is '{py_env}'.")
|
||||
|
||||
env = os.environ.copy()
|
||||
bins.apply_host_libs(env)
|
||||
# PYTHONUSERBASE is set by the Flatpak runtime to isolate sandbox
|
||||
# user packages; remove it so the host Python finds ~/.local packages.
|
||||
env.pop("PYTHONUSERBASE", None)
|
||||
for k, v in self.CUST_ENV.items():
|
||||
e = py_env.get(k, "")
|
||||
if e != "":
|
||||
|
||||
Reference in New Issue
Block a user