e3b1edacef
Distinct from the KWindowSystem-API gating: these are the Xlib/XCB headers themselves, which a Wayland-only sysroot does not ship, so each is a hard compile failure rather than a lost feature: appmenu/appmenu.h:13: fatal error: xcb/xcb.h: No such file or directory Two of the affected directories are genuinely compiled and would have failed in turn: kcms/kfontinst (gated only on FONTCONFIG_FOUND) and logout-greeter (CMakeLists.txt:428, outside any X11 gate). kcms/cursortheme is already gated upstream by `if(WITH_X11 AND X11_Xcursor_FOUND)` and ksmserver by if(WITH_X11); guarding them too is harmless and keeps the rule uniform rather than maintaining a list of exceptions. Only touches includes at preprocessor depth 0 with respect to HAVE_X11, so anything already guarded is left alone. Verified: preprocessor balance OK across all 59 files carrying guards, and a second run is a no-op (0 files, 0 includes) -- the recipe re-runs this on every build, so idempotency is a correctness requirement, not a nicety.
475 lines
17 KiB
Python
Executable File
475 lines
17 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 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)
|
|
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)
|
|
n_blk += n_els + n_st
|
|
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())
|