Use list form with shlex.split() for pkg-config flags instead of shell=True string interpolation. Eliminates command injection vector.
204 lines
7.2 KiB
Python
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}")
|