#!/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 ` 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 <- 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) result = "\n".join(out) + ("\n" if text.endswith("\n") else "") # `#ifdef` merely asks whether the macro exists; `#if` READS ITS VALUE, so # the definition must actually be in scope. Converting the form without # guaranteeing the include turned a silently-inert guard into a hard error: # shell/panelview.h:13: error: 'HAVE_X11' is not defined, evaluates to 0 # (KDE builds with -Werror=undef). Pull in config-X11.h when the file uses # HAVE_X11 and does not already include it. if changed and "config-X11.h" not in result: lines = result.splitlines() insert_at = 0 for i, l in enumerate(lines): if l.strip().startswith("#pragma once") or l.strip().startswith("#define ") and i < 5: insert_at = i + 1 elif l.strip().startswith("#include"): insert_at = i break lines.insert(insert_at, "#include ") result = "\n".join(lines) + ("\n" if text.endswith("\n") else "") return result, changed def gate_raw_x11_includes(text: str) -> tuple[str, int]: r"""Guard bare `#include ` / `` that sit outside HAVE_X11. Distinct from the KWindowSystem-API gating above: these are the Xlib/XCB headers themselves, plus the X11-only KDE headers that KWindowSystem ships only with its X11 component -- fixx11h.h (X11 macro cleanup), netwm.h and netwm_def.h (EWMH). Those carry no X11/ or xcb/ path prefix, so a prefix-only rule missed them: logout-greeter/shutdowndlg.cpp:51: fatal error: fixx11h.h: No such file 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/|fixx11h\.h|netwm\.h|netwm_def\.h)", 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 "" 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() ->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())