diff --git a/mcpp.toml b/mcpp.toml index 6f3644c..19fd018 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -35,6 +35,7 @@ members = [ "tests/examples/eui-neo-app-main", "tests/examples/eui-neo-markdown", "tests/examples/eui-neo-sdl2", + "tests/examples/eui-neo-tray", "tests/examples/eui-neo-vulkan", "tests/examples/eui-neo-window", "tests/examples/ffmpeg", diff --git a/pkgs/e/compat.eui-neo.lua b/pkgs/e/compat.eui-neo.lua index d9952d8..240e5b6 100644 --- a/pkgs/e/compat.eui-neo.lua +++ b/pkgs/e/compat.eui-neo.lua @@ -606,17 +606,18 @@ end -- work by an accident of the archive's shape rather than by anything this -- descriptor controls. Adding a hook means owning that. -- --- ⚠️ DONE IN THE SHELL, and not for brevity: this xim's Lua sandbox has --- neither `os.files` nor `path.basename`, and both absences surface as --- `attempt to call a nil value` AFTER the entire dependency set has --- downloaded. `os.exec` / `os.isfile` / `os.isdir` / `os.mkdir` are what the --- rest of this index uses, so they are what this uses. +-- ⚠️ WHAT THIS RUNTIME ACTUALLY OFFERS, measured here rather than read out of +-- xmake's sources -- descriptors run on libxpkg, which is a different and much +-- narrower environment. Three things a first draft assumed, and none hold: +-- `os.files` and `path.basename` are absent, and `os.cp` takes a LITERAL path, +-- so a glob argument copies nothing without raising. Every one of those +-- surfaces only AFTER the whole dependency set has downloaded, which is why +-- the assertions further down are worth their lines. local function normalise_layout(layer) - -- ⚠️ NO SHELL. `sh -c` does not exist on a Windows runner, and this hook - -- runs on every platform -- the flag that says otherwise is further down, - -- and it only guards the glib staging. Listing a directory is also out - -- (`os.files` is not in this sandbox), so the archive's shape is ASKED - -- ABOUT by name rather than discovered. + -- ⚠️ NO SHELL HERE. `sh -c` does not exist on a Windows runner and this + -- hook runs on every platform; the host guard is further down and it only + -- covers the glib staging. With no directory listing available, the + -- archive's shape is ASKED ABOUT by name rather than discovered. local v = pkginfo.version() for _, name in ipairs({ "EUI-NEO-" .. v, "eui-neo-" .. v, "EUI-NEO-v" .. v, "eui-neo-v" .. v }) do @@ -625,21 +626,17 @@ local function normalise_layout(layer) return os.isfile(path.join(layer, "CMakeLists.txt")) end end - -- A mirror that flattened its tarball: everything at this level IS the - -- tree, so give it the one layer the globs expect. + -- ⚠️ NO FALLBACK FOR A FLATTENED ARCHIVE, and that is a measured decision + -- rather than an omission. A draft of this file carried one built on + -- `os.cp("*", layer)`; libxpkg's `os.cp` copies a LITERAL path and silently + -- copies nothing when handed a glob, so the branch could only ever have + -- returned false one line later -- a fallback in shape only. Reaching the + -- error below is strictly more honest than appearing to recover. -- - -- ⚠️ DEFENSIVE, NOT OBSERVED. An earlier draft of this change said the CN - -- mirror does not preserve the wrap layer. Checked rather than repeated: - -- `eui-neo-0.5.6.tar.gz` from gitcode unpacks to `EUI-NEO-0.5.6/`, byte - -- for byte the same archive as GitHub's (same sha256, which is what one - -- `sha256` field for two URLs already required). Every mirrored version - -- was re-downloaded and checked the same way. This branch exists so the - -- globs cannot depend on that continuing to hold. - if os.isfile("CMakeLists.txt") then - os.mkdir(layer) - os.cp("*", layer) - return os.isfile(path.join(layer, "CMakeLists.txt")) - end + -- Nothing published needs it either: `eui-neo-0.5.6.tar.gz` from gitcode + -- unpacks to `EUI-NEO-0.5.6/`, byte for byte the same archive as GitHub's + -- (one `sha256` field for two URLs already required that), and every + -- mirrored version was re-downloaded and checked the same way. return false end @@ -674,6 +671,20 @@ local function stage_glib(outdir) -- host guard in install()), and preserving the SYMLINKS matters. glib ships -- `libgio-2.0.so -> .so.0 -> .so.0.8000.0`; dereferencing them would stage -- three full copies of each library and lose the name the linker resolves. + -- ⚠️ THE SHELL IS DOING TWO THINGS `os.cp` CANNOT, both measured against + -- this runtime rather than assumed: + -- + -- * GLOBBING. libxpkg's `os.cp` copies a literal path; handed + -- `libgio-2.0.so*` it silently copies NOTHING, and the assertion below + -- is what catches it. The versioned real file (`libgio-2.0.so.0.8000.0`) + -- cannot be named literally without discovering it, and this sandbox + -- has no directory listing (`os.files` is absent). + -- * SYMLINKS. glib ships `libgio-2.0.so -> .so.0 -> .so.0.8000.0`; + -- dereferencing that chain would stage three full copies of each + -- library and lose the name `-lgio-2.0` actually resolves to. + -- + -- Safe despite running inside a hook that fires on every platform, because + -- install() returns before this on any non-Linux host. for _, pattern in ipairs(glib_libs) do os.exec("for lib in " .. sh_quote(path.join(glib.path, "lib")) .. "/" .. pattern .. "; do [ -e \"$lib\" ] || continue; " .. diff --git a/tests/examples/eui-neo-tray/mcpp.toml b/tests/examples/eui-neo-tray/mcpp.toml new file mode 100644 index 0000000..7a2a440 --- /dev/null +++ b/tests/examples/eui-neo-tray/mcpp.toml @@ -0,0 +1,28 @@ +# compat.eui-neo's system tray, over the public `core::platform` surface. +# +# The other six eui-neo members cover the headless surface, the two entry-point +# shapes, and the SDL2 / Vulkan / markdown legs. None of them touches the tray, +# and none of them CAN detect it: tray_bridge.c's `#else` arm defines all six +# tray symbols and returns 0 from each, so a descriptor that lost its backend +# define keeps every existing member green while the tray quietly stops +# existing. This member exists to make that difference observable. +# +# ALL THREE PLATFORMS, deliberately not cfg-gated. The descriptor gives windows +# EUI_TRAY_WINAPI and macOS EUI_TRAY_APPKIT unconditionally, and linux +# EUI_TRAY_SNI since 0.5.7; `core/platform/platform.h` is platform-clean, so the +# consumer code below is identical everywhere. Only the ASSERTION is +# linux-specific, for the reason given in tests/tray.cpp. +# +# compat.eui-neo is the only declared dependency. On linux the tray pulls glib +# through the package's own staged copy and its `runtime` deps, all of which +# mcpp propagates to the consumer -- the same convention as eui-neo-sdl2 and +# eui-neo-vulkan, which include / without re-declaring +# either. +# +# The `compat` index redirect is inherited from the workspace root. +[package] +name = "eui-neo-tray-tests" +version = "0.1.0" + +[dependencies.compat] +eui-neo = "0.5.7" diff --git a/tests/examples/eui-neo-tray/tests/tray.cpp b/tests/examples/eui-neo-tray/tests/tray.cpp new file mode 100644 index 0000000..3da357b --- /dev/null +++ b/tests/examples/eui-neo-tray/tests/tray.cpp @@ -0,0 +1,152 @@ +// compat.eui-neo's SYSTEM TRAY, through the public C++ surface. +// +// This is the shape a consumer should copy. `core::platform` exposes the whole +// tray as six free functions and a two-field options struct, and none of it +// needs a window, a GL context, or the DSL app loop -- a tray-only utility is +// the ten lines in runTray() below. The raw C bridge in +// core/platform/tray_bridge.h is an implementation detail and is not used here. +// +// WHY THIS MEMBER EXISTS, and why the assertion is the shape it is. +// +// tray_bridge.c selects one of four backends at preprocessor level -- +// EUI_TRAY_WINAPI, EUI_TRAY_APPKIT, EUI_TRAY_SNI, EUI_TRAY_APPINDICATOR -- and +// falls through to an `#else` stub when none is defined. THE STUB DEFINES ALL +// SIX SYMBOLS and returns 0 from every one of them. A descriptor that lost its +// backend define would still compile, still link, and still pass all six other +// eui-neo members, while the tray silently stopped existing: "no tray" and "not +// tested" would read identically. That is the failure this member is built +// against. +// +// Two criteria were tried and only the second one works: +// +// * `dlsym(RTLD_DEFAULT, "g_bus_get_sync") != nullptr` -- BLUNT, verified by +// rebuilding with the define removed and watching it pass anyway. The +// descriptor's `ldflags` carry `-lgio-2.0` unconditionally, so gio is in +// DT_NEEDED and its symbols resolve whether or not the SNI arm was +// compiled. It answered "is gio linked", not "is the backend real". +// +// * initializeTray() under a session bus -- SHARP. Measured both ways on a +// bare `dbus-run-session` with no panel and no display: the real backend +// returns true (it exports its StatusNotifierItem object and waits for a +// watcher), the stub returns false unconditionally. +// +// So on linux this test supplies its own private bus and asserts the tray comes +// up. It needs no display, no panel, and no desktop session, which is what lets +// it run on an ordinary CI runner. +// +// Windows and macOS get compile-and-link coverage only. Their backends are +// unconditional in the descriptor, and neither has an equivalent cheap probe +// that a headless runner can perform. + +#include "core/platform/platform.h" + +#if defined(__linux__) +#include +#include +#endif + +import std; + +namespace { + +// What a consumer actually writes. Everything else in this file is about +// proving the backend is real; this is the feature. +int runTray() { + core::platform::TrayOptions options; + options.tooltip = "EUI NEO tray example"; + options.iconPath = ""; // empty: the panel substitutes a placeholder icon + + if (!core::platform::initializeTray(options)) { + std::println("tray unavailable (no session bus)"); + return 1; + } + + std::println("tray is live -- right click it for Show / Exit"); + while (!core::platform::consumeTrayExitRequested()) { + core::platform::pollTray(false); + if (core::platform::consumeTrayShowRequested()) std::println(" -> Show"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + core::platform::shutdownTray(); + std::println("tray shut down"); + return 0; +} + +#if defined(__linux__) +// Re-run this same executable with a private session bus around it. Returns +// only on failure; on success `dbus-run-session` replaces this process and the +// second entry takes the branch below it. +void reexec_under_private_bus(const char* marker) { + ::setenv(marker, "1", 1); + + char self[PATH_MAX]; + const ssize_t n = ::readlink("/proc/self/exe", self, sizeof(self) - 1); + if (n <= 0) return; + self[n] = '\0'; + + char* const argv[] = { const_cast("dbus-run-session"), + const_cast("--"), self, nullptr }; + ::execvp("dbus-run-session", argv); +} +#endif + +} // namespace + +int main() { + // Safe on every platform and before any backend has been touched: the state + // is a plain bool and the stub's eui_tray_is_initialized() also returns 0. + // This pins the query, not the backend. + if (core::platform::isTrayInitialized()) { + std::println(stderr, "isTrayInitialized() is true before initializeTray()"); + return 1; + } + + if (std::getenv("MCPP_RUN_TRAY") != nullptr) return runTray(); + +#if defined(__linux__) + constexpr const char* kMarker = "EUI_TRAY_TEST_HAS_BUS"; + + if (std::getenv("DBUS_SESSION_BUS_ADDRESS") == nullptr) { + if (std::getenv(kMarker) != nullptr) { + // dbus-run-session ran but produced no bus address. Do not report + // this as a pass: an unverifiable backend is exactly what this + // member exists to prevent. + std::println(stderr, + "dbus-run-session left no DBUS_SESSION_BUS_ADDRESS, so " + "the linux tray backend could not be verified"); + return 1; + } + reexec_under_private_bus(kMarker); + std::println(stderr, + "could not exec dbus-run-session, so the linux tray backend " + "could not be verified. It ships with dbus; install that " + "package on this runner."); + return 1; + } + + // THE ASSERTION. True only when tray_bridge.c compiled a real backend -- + // the EUI_TRAY_HAS_BACKEND=0 stub returns 0 here no matter what the bus is. + core::platform::TrayOptions options; + options.tooltip = "compat.eui-neo tray smoke test"; + if (!core::platform::initializeTray(options)) { + std::println(stderr, + "compat.eui-neo was built WITHOUT its linux tray backend: a " + "session bus is present, yet initializeTray() failed, which " + "is what tray_bridge.c's EUI_TRAY_HAS_BACKEND=0 stub does " + "unconditionally. Expected -DEUI_TRAY_SNI=1 and glib on the " + "link line (see pkgs/e/compat.eui-neo.lua)."); + return 1; + } + + if (!core::platform::isTrayInitialized()) { + std::println(stderr, "initializeTray() succeeded but isTrayInitialized() is false"); + return 1; + } + core::platform::shutdownTray(); + std::println("linux tray backend verified over a private session bus"); +#else + std::println("tray path compiled and linked; run with MCPP_RUN_TRAY=1 to show it"); +#endif + return 0; +}