Files
voculinux/tests/test_host_process.py
T
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

97 lines
3.6 KiB
Python

"""Guards for the environment Voculinux 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)