ab7cc543b8
KWindowSystem ships fixx11h.h (X11 macro cleanup), netwm.h and netwm_def.h (EWMH) only with its X11 component, so a Wayland-only install lacks them. They carry no X11/ or xcb/ path prefix, so the prefix-only rule missed them: logout-greeter/shutdowndlg.cpp:51: fatal error: fixx11h.h: No such file The single remaining unguarded case is libtaskmanager/xwindowtasksmodel.cpp, which upstream excludes from the build via if(HAVE_X11) in libtaskmanager/CMakeLists.txt -- it is never compiled, so it is left alone rather than edited for appearance.
558 lines
20 KiB
Python
Executable File
558 lines
20 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)
|
|
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 <config-X11.h>")
|
|
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 <X11/...>` / `<xcb/...>` 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 "<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())
|