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)
|
``reset()`` : clear the cache (mostly useful for tests)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import api.testium as tm
|
import api.testium as tm
|
||||||
@@ -30,10 +30,126 @@ from runtime.tum_except import ETUMRuntimeError
|
|||||||
_PYTHON_CANDIDATES = ["python3", "python"]
|
_PYTHON_CANDIDATES = ["python3", "python"]
|
||||||
_LUA_CANDIDATES = ["lua", "lua5.5", "lua5.4", "lua5.3", "lua5.2", "lua5.1"]
|
_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):
|
def _which(name):
|
||||||
func = sys_app_path_win if tm.OS() == "Windows" else sys_app_path_lin
|
if tm.OS() == "Windows":
|
||||||
return func(name)
|
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):
|
def _python_version(path):
|
||||||
@@ -41,7 +157,7 @@ def _python_version(path):
|
|||||||
try:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
cmd, capture_output=True, text=True,
|
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):
|
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
||||||
return None
|
return None
|
||||||
@@ -60,6 +176,7 @@ def _lua_version(path):
|
|||||||
try:
|
try:
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
[path, "-v"], capture_output=True, text=True, timeout=10,
|
[path, "-v"], capture_output=True, text=True, timeout=10,
|
||||||
|
env=_probe_env(),
|
||||||
)
|
)
|
||||||
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
except (FileNotFoundError, PermissionError, subprocess.TimeoutExpired):
|
||||||
return None
|
return None
|
||||||
@@ -97,8 +214,16 @@ def _resolve(name):
|
|||||||
|
|
||||||
path = ""
|
path = ""
|
||||||
if override:
|
if override:
|
||||||
if shutil.which(override) and validator(override):
|
# Absolute path: accept as-is (user knows exactly what they want).
|
||||||
path = override
|
# 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:
|
else:
|
||||||
tm.print_warn(
|
tm.print_warn(
|
||||||
f"Configured {display} interpreter '{override}' is not usable; "
|
f"Configured {display} interpreter '{override}' is not usable; "
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class LuaProcessBase:
|
|||||||
|
|
||||||
lua_env = tm.gd("lua_env", {})
|
lua_env = tm.gd("lua_env", {})
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
bins.apply_host_libs(env)
|
||||||
if not isinstance(lua_env, dict):
|
if not isinstance(lua_env, dict):
|
||||||
raise ETUMRuntimeError(f"The 'lua_env' global value should be a dictionary. But it is '{lua_env}'.")
|
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
|
env[k] = e
|
||||||
else:
|
else:
|
||||||
env[k] = e + ";" + env.get(k, "")
|
env[k] = e + ";" + env.get(k, "")
|
||||||
|
bins.apply_host_lua_paths(env)
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.bind(("localhost", 0))
|
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}'.")
|
raise ETUMRuntimeError(f"The 'py_env' global value should be a dictionary. But it is '{py_env}'.")
|
||||||
|
|
||||||
env = os.environ.copy()
|
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():
|
for k, v in self.CUST_ENV.items():
|
||||||
e = py_env.get(k, "")
|
e = py_env.get(k, "")
|
||||||
if e != "":
|
if e != "":
|
||||||
|
|||||||
Reference in New Issue
Block a user