chore: import vocalinux project
Import the vocalinux source tree (tray app, Next.js site in web/, packaging, installers, docs and tests) on top of the Gitea template commit. The project README replaces the template README, and the template CI files (.gitea/workflows/ci.yml, .jscpd.json, CI.md) are kept.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""Guards for the environment Vocalinux hands to host binaries.
|
||||
|
||||
An AppImage exports its own `LD_LIBRARY_PATH`, `GI_TYPELIB_PATH` and
|
||||
`PYTHONHOME`, and every child process inherits them. A host `ibus` linked
|
||||
against a newer GLib than the bundle carries then exits with `undefined symbol:
|
||||
g_free_sized` before doing anything, which is how dictated text stopped reaching
|
||||
the focused window.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
from vocalinux.utils.host_process import host_env
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = REPO_ROOT / "src" / "vocalinux"
|
||||
SUBPROCESS_FUNCS = {"run", "Popen", "check_output", "check_call", "call"}
|
||||
|
||||
|
||||
def test_outside_a_bundle_the_environment_is_handed_over_unchanged():
|
||||
env = {"LD_LIBRARY_PATH": "/usr/lib", "PATH": "/usr/bin"}
|
||||
assert host_env(env) == env
|
||||
|
||||
|
||||
def test_bundle_paths_are_stripped_and_host_ones_survive():
|
||||
env = {
|
||||
"APPDIR": "/tmp/.mount_Vocali1234",
|
||||
"LD_LIBRARY_PATH": "/tmp/.mount_Vocali1234/usr/lib:/usr/lib",
|
||||
"GI_TYPELIB_PATH": "/tmp/.mount_Vocali1234/usr/lib/girepository-1.0",
|
||||
"PYTHONHOME": "/tmp/.mount_Vocali1234/usr",
|
||||
"PATH": "/usr/bin:/bin",
|
||||
"GTK_THEME": "Adwaita",
|
||||
}
|
||||
cleaned = host_env(env)
|
||||
|
||||
assert cleaned["LD_LIBRARY_PATH"] == "/usr/lib", "the host half must survive"
|
||||
assert "GI_TYPELIB_PATH" not in cleaned, "nothing but bundle paths were in it"
|
||||
assert "PYTHONHOME" not in cleaned
|
||||
assert cleaned["PATH"] == "/usr/bin:/bin"
|
||||
assert cleaned["GTK_THEME"] == "Adwaita", "non-path values are not paths into the bundle"
|
||||
assert cleaned["APPDIR"] == env["APPDIR"], "a child may still need to know it came from one"
|
||||
|
||||
|
||||
def _is_subprocess_call(node) -> bool:
|
||||
func = getattr(node, "func", None)
|
||||
return (
|
||||
isinstance(node, ast.Call)
|
||||
and isinstance(func, ast.Attribute)
|
||||
and func.attr in SUBPROCESS_FUNCS
|
||||
and isinstance(func.value, ast.Name)
|
||||
and func.value.id == "subprocess"
|
||||
)
|
||||
|
||||
|
||||
def _spawns_our_interpreter(node) -> bool:
|
||||
"""True when the command starts with `sys.executable` — our own Python."""
|
||||
if not node.args or not isinstance(node.args[0], ast.List) or not node.args[0].elts:
|
||||
return False
|
||||
first = node.args[0].elts[0]
|
||||
return (
|
||||
isinstance(first, ast.Attribute)
|
||||
and first.attr == "executable"
|
||||
and isinstance(first.value, ast.Name)
|
||||
and first.value.id == "sys"
|
||||
)
|
||||
|
||||
|
||||
def _env_argument(node):
|
||||
return next((kw.value for kw in node.keywords if kw.arg == "env"), None)
|
||||
|
||||
|
||||
def _is_host_env_call(value) -> bool:
|
||||
return isinstance(value, ast.Call) and getattr(value.func, "id", None) == "host_env"
|
||||
|
||||
|
||||
def test_host_binaries_get_a_stripped_environment_and_our_own_python_does_not():
|
||||
"""Two mistakes, opposite directions: a host binary that inherits the bundle
|
||||
dies on the wrong GLib, and our own interpreter stripped of it cannot find
|
||||
its stdlib or GI stack."""
|
||||
inherit, stripped = [], []
|
||||
for path in sorted(SOURCE.rglob("*.py")):
|
||||
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
|
||||
if not _is_subprocess_call(node):
|
||||
continue
|
||||
where = f"{path.relative_to(REPO_ROOT)}:{node.lineno}"
|
||||
env = _env_argument(node)
|
||||
if _spawns_our_interpreter(node):
|
||||
if _is_host_env_call(env):
|
||||
stripped.append(where)
|
||||
elif not _is_host_env_call(env):
|
||||
inherit.append(where)
|
||||
|
||||
assert (
|
||||
not inherit
|
||||
), "these hand the bundle to a host binary; pass env=host_env():\n" + "\n".join(inherit)
|
||||
assert not stripped, "these strip the bundle from our own interpreter:\n" + "\n".join(stripped)
|
||||
Reference in New Issue
Block a user