Files
Modus/src/window/globalmenu/environment.py
T
sankalp ef685ab34a fix(globalmenu): remove shell=True from gcc compilation command
Use list form with shlex.split() for pkg-config flags instead of
shell=True string interpolation. Eliminates command injection vector.
2026-07-18 14:13:03 +05:45

204 lines
7.2 KiB
Python

"""Global menu environment setup — GTK_MODULES, UBUNTU_MENUPROXY, settings.ini, Flatpak."""
import os
import subprocess
from pathlib import Path
from fabric.utils import exec_shell_command, logger
from utils.functions import shell_split
SHIM_SRC = Path(__file__).resolve().parent / "libmenu_button_shim.c"
SHIM_SO = Path(__file__).resolve().parent / "libmenu_button_shim.so"
ENV_VARS = {
"GTK_MODULES": "appmenu-gtk-module",
"UBUNTU_MENUPROXY": "1",
}
# Marker lines previously written around the global LD_PRELOAD export in shell
# rc files. Kept so we can strip the stale block from already-configured systems.
_SHIM_BEGIN = "# >>> modus global-menu shim >>>"
_SHIM_END = "# <<< modus global-menu shim <<<"
def _remove_global_ld_preload():
"""Strip the legacy global LD_PRELOAD export from previously configured systems.
The shim is now injected per-launch into GTK3 apps only (see
globalmenu.launch). Any globally exported LD_PRELOAD must be removed so it
no longer breaks GTK4 processes.
"""
home = Path.home()
shell = os.environ.get("SHELL", "")
rc_files = [
home / ".zshrc",
home / ".bashrc",
home / ".profile",
home / ".config" / "fish" / "config.fish",
]
rc_path = {
"zsh": home / ".zshrc",
"bash": home / ".bashrc",
"fish": home / ".config" / "fish" / "config.fish",
}.get(Path(shell).name)
if rc_path:
rc_files.append(rc_path)
rc_files = list(dict.fromkeys(rc_files)) # de-dupe, keep order
for rc in rc_files:
try:
if not rc.is_file():
continue
lines = rc.read_text().splitlines(keepends=True)
if not any(_SHIM_BEGIN in ln for ln in lines):
continue
out = []
drop = False
for ln in lines:
if _SHIM_BEGIN in ln:
drop = True
continue
if _SHIM_END in ln:
drop = False
continue
if drop:
continue
out.append(ln)
rc.write_text("".join(out))
logger.info(f"[GlobalMenu] Removed stale LD_PRELOAD block from {rc}")
except OSError as e:
logger.warning(f"[environment] if not rc.is_file(): continue failed: {e}")
# Strip the LD_PRELOAD line from environment.d/appmenu.conf.
env_path = home / ".config" / "environment.d" / "appmenu.conf"
try:
if env_path.is_file():
kept = [
ln
for ln in env_path.read_text().splitlines(keepends=True)
if not ln.strip().startswith("LD_PRELOAD=")
]
env_path.write_text("".join(kept))
except OSError as e:
logger.warning(
f"[environment] if env_path.is_file(): kept = [ ln for ln in env_path.rea... failed: {e}"
)
# Clear it from the activation environment for future launches.
exec_shell_command("dbus-update-activation-environment --systemd LD_PRELOAD=")
def get_compiled_shim() -> Path | None:
"""Compile the GTK3 menu-button shim if needed. Returns path to .so or None."""
try:
if SHIM_SO.exists() and SHIM_SO.stat().st_mtime >= SHIM_SRC.stat().st_mtime:
return SHIM_SO
if not SHIM_SRC.exists():
logger.debug("[GlobalMenu] Shim source not found, skipping")
return None
gtk_cflags = subprocess.check_output(
["pkg-config", "--cflags", "--libs", "gtk+-3.0"],
stderr=subprocess.DEVNULL,
text=True,
).strip()
cmd = [
"gcc",
"-shared",
"-fPIC",
"-O2",
"-o",
str(SHIM_SO),
str(SHIM_SRC),
*shell_split(gtk_cflags),
"-ldl",
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if result.returncode != 0:
logger.warning(f"[GlobalMenu] Shim compilation failed:\n{result.stderr}")
return None
logger.info(f"[GlobalMenu] Compiled menu shim -> {SHIM_SO}")
return SHIM_SO
except Exception as e:
logger.warning(f"[GlobalMenu] Shim compilation error: {e}")
return None
def setup_global_menu_environment():
"""Write env config files so GTK apps register with AppMenu at next login.
Does NOT set env via hyprctl keyword env — that would cause Fabric's own
GTK windows to load appmenu-gtk-module and trigger Gdk-CRITICAL assertions.
Uses session-level config files that take effect at login.
"""
try:
shim_path = get_compiled_shim()
# Remove any legacy global LD_PRELOAD export from older installs.
_remove_global_ld_preload()
# NOTE: The menu-button shim is compiled against GTK3 and must NOT be
# exported globally. Injecting it into a GTK4 process aborts the app
# ("GTK 2/3 symbols detected"). It is injected per-launch into GTK3
# apps only — see globalmenu.launch.
if shim_path:
logger.debug(f"[GlobalMenu] GTK3 shim ready at {shim_path}")
exec_shell_command(
"dbus-update-activation-environment --systemd "
"GTK_MODULES=appmenu-gtk-module UBUNTU_MENUPROXY=1"
)
exec_shell_command(
"flatpak override --user --talk-name=com.canonical.AppMenu.Registrar"
)
env_dir = Path.home() / ".config" / "environment.d"
try:
env_dir.mkdir(parents=True, exist_ok=True)
env_path = env_dir / "appmenu.conf"
with open(env_path, "w") as f:
for k, v in ENV_VARS.items():
f.write(f"{k}={v}\n")
except OSError as e:
logger.warning(
f"[environment] env_dir.mkdir(parents=True, exist_ok=True) failed: {e}"
)
settings_path = Path.home() / ".config" / "gtk-3.0" / "settings.ini"
try:
settings_path.parent.mkdir(parents=True, exist_ok=True)
content = ""
if settings_path.exists():
content = settings_path.read_text()
if "gtk-shell-shows-menubar" not in content:
with open(settings_path, "a") as f:
if content and not content.endswith("\n"):
f.write("\n")
f.write("gtk-shell-shows-menubar=1\n")
except OSError as e:
logger.warning(
f"[environment] settings_path.parent.mkdir(parents=True, exist_ok=True) failed: {e}"
)
pam_path = Path.home() / ".pam_environment"
try:
existing = ""
if pam_path.exists():
existing = pam_path.read_text()
lines = existing.splitlines(True)
for k, v in ENV_VARS.items():
line = f"{k} DEFAULT={v}\n"
if not any(ln.startswith(f"{k} ") for ln in lines):
with open(pam_path, "a") as f:
f.write(line)
except OSError as e:
logger.warning(f"[environment] existing = '' failed: {e}")
logger.info("[GlobalMenu] GTK environment injected")
except Exception as e:
logger.warning(f"[GlobalMenu] Environment setup failed: {e}")