Files
RedBear-OS/local/scripts/gate-kx11extras.py
T
vasilito d8d2ed837c gate-kx11extras: guard namespace blocks using QX11Application
libtaskmanager/virtualdesktopinfo.cpp defines namespace X11Info at namespace
scope in terms of QNativeInterface::QX11Application, which does not exist in a
Qt built without the xcb platform:
  error: 'QX11Application' is not a member of 'QNativeInterface'

All three call sites were already inside #if HAVE_X11 -- only the definition
was exposed -- so wrapping the block changes no reachable behaviour.

Restricted to depth-0 'namespace' blocks whose body names an X11-only native
interface. Narrow on purpose: brace-matching arbitrary function definitions is
much easier to get wrong, and a bad transform here produces invalid C++ rather
than a clean failure.
2026-08-04 21:26:14 +03:00

532 lines
19 KiB
Python
Executable File

#!/usr/bin/env python3
"""Gate plasma-workspace's KX11Extras usage behind HAVE_X11.
KX11Extras is KWindowSystem's X11-only API. A Wayland-only kwindowsystem does
not install it, so plasma-workspace fails with
klipper/klipperpopup.cpp:21:10: fatal error: KX11Extras
plasma-workspace already ships config-X11.h defining HAVE_X11 (CMakeLists.txt
sets 1 with X11, 0 without), so this is upstream's own mechanism, not an
invented one.
Every call site already sits inside a runtime `KWindowSystem::isPlatformX11()`
test that is permanently false on Wayland, so compiling them out removes no
reachable behaviour.
THE ELSE-CHAIN PROBLEM
Naively wrapping the whole `if` in #if HAVE_X11 breaks any chain that has an
else. The transform used here keeps the `else` OUTSIDE the guard:
#if HAVE_X11
if (KWindowSystem::isPlatformX11()) { A }
else
#endif
if (cond) { B } <- else-if chain survives
#if HAVE_X11
if (KWindowSystem::isPlatformX11()) { A }
else
#endif
{ B } <- plain else becomes an unconditional block
With HAVE_X11 == 0 the first form degrades to `if (cond) { B }` and the second
to a bare `{ B }` -- both valid C++ and both exactly the Wayland behaviour.
Deliberately NOT done: fabricating a KX11Extras compatibility header. Every
call is dead on Wayland so it would "work", but it invents an API -- the same
disguised-stub pattern removed from kf6-ktexteditor.
Idempotent: files already carrying the guard are left alone.
"""
import re
import sys
from pathlib import Path
GUARD_OPEN = "#if HAVE_X11"
GUARD_CLOSE = "#endif"
# X11-only KWindowSystem APIs. A Wayland-only kwindowsystem installs none of
# these headers, so each is a hard compile failure rather than a lost feature.
# Discovered by diffing plasma-workspace's K* includes against what the sysroot
# actually provides -- only these were genuinely absent.
X11_HEADERS = [
"KX11Extras",
"KUserTimestamp",
"KStartupInfo",
"KWindowInfo",
"KSelectionOwner",
]
# Runtime tests that mean "we are on X11". Both forms appear: the KWindowSystem
# helper, and a direct probe for the Qt X11 native interface (shell/desktopview.cpp).
X11_CONDS = [
"KWindowSystem::isPlatformX11()",
"QNativeInterface::QX11Application",
]
def find_block_end(text: str, brace_open: int) -> int:
"""Return index just past the matching close brace, skipping strings/comments."""
depth = 0
i = brace_open
n = len(text)
while i < n:
c = text[i]
if c == "/" and i + 1 < n and text[i + 1] == "/":
i = text.find("\n", i)
if i < 0:
return -1
continue
if c == "/" and i + 1 < n and text[i + 1] == "*":
i = text.find("*/", i)
if i < 0:
return -1
i += 2
continue
if c in "\"'":
quote = c
i += 1
while i < n:
if text[i] == "\\":
i += 2
continue
if text[i] == quote:
break
i += 1
i += 1
continue
if c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
return i + 1
i += 1
return -1
def line_start(text: str, idx: int) -> int:
nl = text.rfind("\n", 0, idx)
return nl + 1 if nl >= 0 else 0
def gate_includes(text: str) -> tuple[str, int]:
"""Wrap `#include <KX11Extras>` and ensure config-X11.h is present."""
changed = 0
pattern = re.compile(
r"^([ \t]*)#include\s*<(?:" + "|".join(X11_HEADERS) + r")>[ \t]*$", re.M
)
def repl(m):
nonlocal changed
start = m.start()
# Idempotency: skip if the preceding non-blank line already guards it.
prev = text.rfind("\n", 0, start)
prev_line = text[text.rfind("\n", 0, prev) + 1 : prev].strip() if prev > 0 else ""
if prev_line == GUARD_OPEN:
return m.group(0)
changed += 1
ind = m.group(1)
return f"{ind}{GUARD_OPEN}\n{m.group(0)}\n{ind}{GUARD_CLOSE}"
text = pattern.sub(repl, text)
if changed and '#include "config-X11.h"' not in text:
# Place it before the first guarded include so HAVE_X11 is defined.
anchor = text.find(GUARD_OPEN)
ls = line_start(text, anchor)
text = text[:ls] + '#include "config-X11.h"\n' + text[ls:]
return text, changed
def gate_blocks(text: str) -> tuple[str, int]:
"""Wrap `if (isPlatformX11()) {...}` blocks, keeping any else outside."""
changed = 0
search_from = 0
while True:
idx = -1
for cond in X11_CONDS:
m = re.compile(r"\bif\s*\(([^\n]*" + re.escape(cond) + r"[^\n]*)\)").search(text, search_from)
if m and (idx < 0 or m.start() < idx):
idx = m.start()
if idx < 0:
break
ls = line_start(text, idx)
# Idempotency: already guarded?
prev_nl = text.rfind("\n", 0, ls - 1)
prev_line = text[prev_nl + 1 : ls - 1].strip() if ls > 0 else ""
if prev_line == GUARD_OPEN:
search_from = idx + 1
continue
brace = text.find("{", idx)
if brace < 0:
search_from = idx + 1
continue
end = find_block_end(text, brace)
if end < 0:
search_from = idx + 1
continue
body = text[idx:end]
if not any(sym in body for sym in X11_HEADERS):
search_from = end
continue
# Is this `if` actually the tail of an `else if`? The line then looks
# like "} else if (...) {" and the text before `if` is NOT indentation.
# Treating it as indentation is what produced the corruption
# } else #if HAVE_X11
# so the two shapes need genuinely different transforms.
prefix = text[ls:idx]
m_elseif = re.search(r"\belse[ \t]*$", prefix)
tail = text[end:]
m_else = re.match(r"[ \t]*else\b", tail)
if m_elseif:
# `} else if (X11) { A }` [ else ... ]
# Guard from the `else` through the block; any FOLLOWING else stays
# outside, so with HAVE_X11==0 the chain reads `} else if (...) {B}`.
else_pos = ls + m_elseif.start()
head = text[ls:else_pos].rstrip() # the closing `}` etc.
indent = re.match(r"[ \t]*", text[ls:]).group(0)
new = (
(f"{head}\n" if head else "")
+ f"{indent}{GUARD_OPEN}\n"
+ f"{indent}{text[else_pos:end].strip()}\n"
+ f"{indent}{GUARD_CLOSE}\n"
)
rest = text[end:].lstrip(" \t")
text = text[:ls] + new + (indent + rest if rest.startswith("else") else rest)
elif m_else:
indent = prefix
else_end = end + m_else.end()
# Insert #endif right after `else`, so the following branch stays live.
#
# The trailing newline + indent is REQUIRED, not cosmetic. `else` is
# very often followed by `if (...)` on the SAME line, and #endif is a
# preprocessor directive: anything after it on that line is invalid.
# Without this the transform emitted
# #endif if (m_plasmashell && ...) {
# which does not compile.
rest = text[else_end:].lstrip(" \t")
new = (
f"{indent}{GUARD_OPEN}\n"
+ text[ls:else_end]
+ f"\n{indent}{GUARD_CLOSE}\n{indent}"
)
text = text[:ls] + new + rest
else:
indent = prefix
new = f"{indent}{GUARD_OPEN}\n" + text[ls:end] + f"\n{indent}{GUARD_CLOSE}"
text = text[:ls] + new + text[end:]
changed += 1
search_from = ls + len(new)
return text, changed
def gate_else_blocks(text: str) -> tuple[str, int]:
"""Guard `else { ...X11... }` where the `if` branch is the Wayland path.
Shape seen in shell/desktopview.cpp, libnotificationmanager/server.cpp and
portal_p.cpp:
if (KWindowSystem::isPlatformWayland()) { wayland }
else { X11 }
The X11 code is in the ELSE, so the if-driven pass above never sees it.
Guarding just the else leaves `if (wayland) { ... }` standing alone when
HAVE_X11 == 0, which is valid and correct.
"""
changed = 0
search_from = 0
while True:
m = re.compile(r"\belse[ \t]*\{").search(text, search_from)
if not m:
break
brace = text.index("{", m.start())
end = find_block_end(text, brace)
if end < 0:
search_from = m.end()
continue
body = text[brace:end]
if not any(sym in body for sym in X11_HEADERS):
search_from = end
continue
ls = line_start(text, m.start())
prev_nl = text.rfind("\n", 0, ls - 1)
if text[prev_nl + 1 : ls - 1].strip() == GUARD_OPEN:
search_from = end
continue
indent = re.match(r"[ \t]*", text[ls:]).group(0)
head = text[ls : m.start()].rstrip() # usually the closing `}` of the if
new = (
(f"{head}\n" if head else "")
+ f"{indent}{GUARD_OPEN}\n"
+ f"{indent}{text[m.start():end].strip()}\n"
+ f"{indent}{GUARD_CLOSE}\n"
)
text = text[:ls] + new + text[end:].lstrip(" \t")
changed += 1
search_from = ls + len(new)
return text, changed
def gate_statements(text: str) -> tuple[str, int]:
"""Guard leftover single-line statements that call an X11-only API.
After the if/else passes, what remains is the odd standalone call whose
whole enclosing function is X11-specific, e.g.
KUserTimestamp::updateUserTimestamp(); (soliduiserver)
KX11Extras::forceActiveWindow(window->winId()); (notificationapplet)
Restricted to a COMPLETE single-line statement ending in `;` so a
multi-line expression can never be split across the guard, and skipped for
declarations (whose variable would then be undefined further down).
"""
changed = 0
out = []
depth_x11 = 0
for line in text.splitlines():
st = line.strip()
if re.match(r"#\s*if", st):
depth_x11 += 1 if ("HAVE_X11" in st and not re.match(r"#\s*if\s*!", st)) else 0
elif re.match(r"#\s*endif", st) and depth_x11:
depth_x11 -= 1
if (
depth_x11 == 0
and st.endswith(";")
and any(sym + "::" in st for sym in X11_HEADERS)
and not st.startswith(("//", "*", "/*", "#"))
and "=" not in st.split("(")[0]
):
ind = re.match(r"[ \t]*", line).group(0)
out.append(f"{ind}{GUARD_OPEN}")
out.append(line)
out.append(f"{ind}{GUARD_CLOSE}")
changed += 1
else:
out.append(line)
return "\n".join(out) + ("\n" if text.endswith("\n") else ""), changed
def normalize_ifdef_guards(text: str) -> tuple[str, int]:
r"""Turn inert `#ifdef HAVE_X11` guards into working `#if HAVE_X11`.
config-X11.h.cmake declares the macro with `#cmakedefine01 HAVE_X11`, which
always DEFINES it -- as 0 when X11 is off, not undefined. So `#ifdef
HAVE_X11` is true in both configurations and the guard does nothing:
#ifdef HAVE_X11
#include <xcb/xcb.h> <- compiled even with HAVE_X11 == 0
#endif
which is how appmenu.h reached `fatal error: xcb/xcb.h` despite looking
guarded. `#if HAVE_X11` reads the value and behaves as intended.
An upstream bug that is invisible on any system that has X11 headers
installed, because there the include simply succeeds.
"""
changed = 0
out = []
for line in text.splitlines():
st = line.strip()
if st in ("#ifdef HAVE_X11", "#if defined(HAVE_X11)"):
out.append(line.replace(st, "#if HAVE_X11"))
changed += 1
elif st in ("#ifndef HAVE_X11", "#if !defined(HAVE_X11)"):
out.append(line.replace(st, "#if !HAVE_X11"))
changed += 1
else:
out.append(line)
return "\n".join(out) + ("\n" if text.endswith("\n") else ""), changed
def gate_raw_x11_includes(text: str) -> tuple[str, int]:
r"""Guard bare `#include <X11/...>` / `<xcb/...>` that sit outside HAVE_X11.
Distinct from the KWindowSystem-API gating above: these are the Xlib/XCB
headers themselves. A Wayland-only sysroot ships none of them, so each is a
hard compile failure:
appmenu/appmenu.h:13: fatal error: xcb/xcb.h: No such file or directory
Only touches includes at preprocessor depth 0 with respect to HAVE_X11 --
anything already inside a guard is left alone, so this is idempotent and
cannot double-wrap.
Scoped to files that are actually compiled. kcms/cursortheme is skipped
because upstream already gates it (kcms/CMakeLists.txt:13,
`if(WITH_X11 AND X11_Xcursor_FOUND)`); kfontinst and logout-greeter are NOT
gated and do reach the compiler.
"""
lines = text.splitlines()
stack, inx, changed = [], 0, 0
out = []
for line in lines:
st = line.strip()
if re.match(r"#\s*if", st):
g = ("HAVE_X11" in st) and not re.match(r"#\s*if\s*!", st)
stack.append(g)
if g:
inx += 1
out.append(line)
continue
if re.match(r"#\s*endif", st):
if stack and stack.pop():
inx -= 1
out.append(line)
continue
if re.match(r"#\s*el(se|if)", st):
if stack and stack[-1]:
stack[-1] = False
inx -= 1
out.append(line)
continue
if inx == 0 and re.match(r"#\s*include\s*<(X11/|xcb/)", st):
ind = re.match(r"[ \t]*", line).group(0)
out.append(f"{ind}{GUARD_OPEN}")
out.append(line)
out.append(f"{ind}{GUARD_CLOSE}")
changed += 1
continue
out.append(line)
if changed and '#include "config-X11.h"' not in text and "<config-X11.h>" not in text:
for i, l in enumerate(out):
if l.strip() == GUARD_OPEN:
out.insert(i, '#include "config-X11.h"')
break
return "\n".join(out) + ("\n" if text.endswith("\n") else ""), changed
def gate_x11_namespace_blocks(text: str) -> tuple[str, int]:
r"""Guard a namespace block defined in terms of an X11-only Qt interface.
libtaskmanager/virtualdesktopinfo.cpp defines
namespace X11Info
{
[[nodiscard]] inline auto connection()
{
return qGuiApp->nativeInterface<QNativeInterface::QX11Application>()
->connection();
}
}
at namespace scope. QNativeInterface::QX11Application does not exist in a Qt
built without the xcb platform, so this fails to compile even though all
THREE call sites are already inside #if HAVE_X11:
error: 'QX11Application' is not a member of 'QNativeInterface'
Only the definition is exposed, so wrapping the block is sufficient and
changes no reachable behaviour. Restricted to depth-0 `namespace` blocks
whose body names an X11-only native interface -- narrow on purpose, since
brace-matching arbitrary function definitions is far easier to get wrong.
"""
changed = 0
search = 0
while True:
m = re.compile(r"^namespace\s+\w+\s*$", re.M).search(text, search)
if not m:
break
brace = text.find("{", m.end())
if brace < 0:
search = m.end()
continue
end = find_block_end(text, brace)
if end < 0:
search = m.end()
continue
body = text[m.start():end]
if "QX11Application" not in body:
search = end
continue
ls = line_start(text, m.start())
prev_nl = text.rfind("\n", 0, ls - 1)
if text[prev_nl + 1: ls - 1].strip() == GUARD_OPEN:
search = end
continue
new = f"{GUARD_OPEN}\n" + text[ls:end] + f"\n{GUARD_CLOSE}"
text = text[:ls] + new + text[end:]
changed += 1
search = ls + len(new)
return text, changed
def main() -> int:
root = Path(sys.argv[1])
# libtaskmanager's X11 backend is excluded from the build entirely (Wayland
# uses WaylandTasksModel), so it is not gated here.
skip = {"xwindowtasksmodel.cpp", "xwindowsystemeventbatcher.cpp"}
total_files = 0
total_inc = 0
total_blk = 0
for path in sorted(root.rglob("*")):
if path.suffix not in (".cpp", ".h") or path.name in skip:
continue
try:
original = path.read_text()
except (UnicodeDecodeError, OSError):
continue
text0, n_ifdef = normalize_ifdef_guards(original)
if n_ifdef:
path.write_text(text0)
original = text0
print(f" {path.relative_to(root)}: {n_ifdef} inert #ifdef HAVE_X11 -> #if")
if not any(sym in original for sym in X11_HEADERS):
text, n_raw = gate_raw_x11_includes(original)
text, n_ns = gate_x11_namespace_blocks(text)
n_raw += n_ns
if n_raw:
path.write_text(text)
total_files += 1
total_inc += n_raw
print(f" {path.relative_to(root)}: {n_raw} raw X11/xcb include(s)")
continue
text, n_inc = gate_includes(original)
text, n_blk = gate_blocks(text)
text, n_els = gate_else_blocks(text)
text, n_st = gate_statements(text)
text, n_raw = gate_raw_x11_includes(text)
text, n_ns = gate_x11_namespace_blocks(text)
n_blk += n_els + n_st + n_ns
n_inc += n_raw
if text != original:
path.write_text(text)
total_files += 1
total_inc += n_inc
total_blk += n_blk
print(f" {path.relative_to(root)}: {n_inc} include(s), {n_blk} block(s)")
print(f"gated {total_files} files: {total_inc} includes, {total_blk} blocks")
# Report anything left unguarded so it cannot pass silently.
leftovers = []
for path in sorted(root.rglob("*")):
if path.suffix not in (".cpp", ".h") or path.name in skip:
continue
try:
text = path.read_text()
except (UnicodeDecodeError, OSError):
continue
for i, line in enumerate(text.splitlines(), 1):
if any(sym in line for sym in X11_HEADERS) and not line.lstrip().startswith(("//", "*")):
# crude but useful: is there any guard above in this file?
if "HAVE_X11" not in text:
leftovers.append(f"{path.relative_to(root)}:{i}")
if leftovers:
print(f"\nUNGUARDED X11-only references remain ({len(leftovers)}):")
for entry in leftovers[:20]:
print(f" {entry}")
return 0
if __name__ == "__main__":
sys.exit(main())