Files
RedBear-OS/local/scripts/gate-kx11extras.py
T
vasilito 221a91cba4 build: port libcanberra, gate X11-only KWindowSystem APIs, build out of tree
libcanberra: plasma-workspace calls find_package(Canberra) TYPE REQUIRED and
includes <canberra.h> from four translation units with no HAVE_CANBERRA guard,
one of them libnotificationmanager (the core notification library). It cannot
be made optional, so it is ported for real. Built shared: it dlopens its
backend through libltdl, the libtool recipe stages libltdl.so only (no .a), so
a static build left every consumer with an undefined lt_dlopenext.

Audio is SILENT. Upstream 0.30 offers alsa/oss/pulse/gstreamer/null and none
speak Redox; the pulse driver needs PulseAudio's client library, which PipeWire
does not provide. The null driver is real upstream code, so the API/ABI is
genuine and all consumers work -- but nothing reaches the speakers until a
native /scheme/audio driver exists.

KX11Extras et al: gate the five X11-only KWindowSystem headers behind the
HAVE_X11 that plasma-workspace already defines. X11 is compiled OUT, not added.
gate-kx11extras.py brace-matches each block and keeps any trailing else outside
the guard, since wrapping a whole if/else-if chain yields invalid C++.

check-recipe-escapes.py: a single backslash in a TOML multi-line string is a
TOML escape, so a sed written as \bFoo\b parses to <BS>Foo<BS> and silently
matches nothing -- no TOML error, no sed error. This class bit the tree three
times. Now gated in preflight; it also found openssh, which used an invalid \$
and could not be parsed by any compliant parser.

cook_build.rs: build git-tracked vendored sources OUT OF TREE. Recipe scripts
rewrite their source, so builds were mutating version-controlled files: a
no-op sed became indistinguishable from a working one, and the integrity gate
fired so often that clearing it stopped being protective. Uses
cp -a --reflink=auto, not cp -al: sed -i is link-safe but `cp -f` and `>`
truncate in place and would write through a hard link into the tracked
original. Content hashing still runs against the pristine tree, so hashes now
describe committed state. Escape hatch: REDBEAR_IN_TREE_BUILD=1.
2026-08-04 16:41:39 +03:00

374 lines
13 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 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
if not any(sym in original for sym in X11_HEADERS):
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)
n_blk += n_els + n_st
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())