|
| 1 | +# Design doc: add `compat.eui-neo` (EUI-NEO 0.5.3) |
| 2 | + |
| 3 | +Date: 2026-07-29 |
| 4 | + |
| 5 | +Follow-up to `.agents/docs/2026-07-28-add-eui-compat-deps-plan.md`, which landed the six |
| 6 | +dependency packages as PR #131 and deferred the framework itself. This is that framework. |
| 7 | + |
| 8 | +Supersedes the `compat.eui-neo` descriptor proposed in PR #133 — see "What PR #133 got |
| 9 | +wrong" below, which is also why this package is rebuilt from upstream rather than rebased. |
| 10 | + |
| 11 | +## Motivation |
| 12 | + |
| 13 | +[EUI-NEO](https://github.com/sudoevolve/EUI-NEO) is a declarative retained-mode C++17 UI |
| 14 | +framework (GLFW window backend, OpenGL render backend). This adds it in **header-compat |
| 15 | +shape only**: a consumer writes `#include <eui_neo.h>`. The C++23 module surface |
| 16 | +(`import eui;`) is explicitly out of scope — upstream ships no module interface units, so |
| 17 | +`import` would mean hand-authoring wrappers over 40+ component headers. Header compat is a |
| 18 | +prerequisite for that work, not an alternative to it. |
| 19 | + |
| 20 | +## Source and version |
| 21 | + |
| 22 | +| | | |
| 23 | +|---|---| |
| 24 | +| Upstream | `https://github.com/sudoevolve/EUI-NEO` | |
| 25 | +| Version | `0.5.3` (latest release, published 2026-07-27) | |
| 26 | +| Tarball | `archive/refs/tags/v0.5.3.tar.gz` | |
| 27 | +| sha256 | `6951ac330d0307c633bafe720b7888bf32785103eb16973adb4ee05ef06e64d1` (computed twice, stable) | |
| 28 | +| Wrap dir | `EUI-NEO-0.5.3/` — absorbed by the standard `*/` glob prefix, no `install()` hook | |
| 29 | +| CN mirror | `gitcode.com/mcpp-res/eui-neo` @ `0.5.3`, byte-identical (see below) | |
| 30 | +| License | Apache-2.0 | |
| 31 | + |
| 32 | +## Shape decision: C++ source compat (Form B), deps reused from the index |
| 33 | + |
| 34 | +Upstream vendors its whole dependency set under `3rd/` — freetype, glfw, libpng, zlib, |
| 35 | +glad, tray, yyjson, md4c, all fully checked in (no submodules). **None of them are built |
| 36 | +here.** Each already exists in this index at the same version upstream pins, and building |
| 37 | +them once for the ecosystem is the entire point of having them: |
| 38 | + |
| 39 | +| upstream `3rd/` | index package | version match | |
| 40 | +|---|---|---| |
| 41 | +| `3rd/freetype` | `compat.freetype` | 2.13.3 | |
| 42 | +| `3rd/libpng-1.6.43` | `compat.libpng` | 1.6.43 | |
| 43 | +| `3rd/zlib-1.3.1` | `compat.zlib` | 1.3.2 | |
| 44 | +| `3rd/glfw` | `compat.glfw` | 3.4 | |
| 45 | +| `3rd/glad` | `compat.glad` | 0.0.0-651a425 — the exact commit `3rd/dependencies.cmake` fetches | |
| 46 | +| `3rd/tray` | `compat.tray` | 0.0.0-8dd1358 | |
| 47 | +| `3rd/yyjson-0.12.0` | `compat.yyjson` | 0.12.0 | |
| 48 | +| `3rd/md4c` | `compat.md4c` | 0.5.3 (feature-gated) | |
| 49 | + |
| 50 | +`3rd/` still sits on the include path: `stb_image.h`, `nanosvg.h` and `nanosvgrast.h` are |
| 51 | +genuinely vendored single-file headers at its root, and the sources include them as |
| 52 | +`"3rd/stb_image.h"`. |
| 53 | + |
| 54 | +The build recipe tracks upstream `CMakeLists.txt` v0.5.3: `CORE_SOURCES` + the OpenGL |
| 55 | +backend + glfw's `ime_bridge.c` = 20 translation units. |
| 56 | + |
| 57 | +### Backend selection is a build-time constant, not a feature |
| 58 | + |
| 59 | +Upstream picks the render and window backend at configure time and compiles exactly one |
| 60 | +in. Only `opengl` + `glfw` are modelled. `vulkan`, `sdl2` and `network` would need |
| 61 | +`compat.vulkan`, `compat.sdl2` and `compat.curl`, none of which exist in this index — |
| 62 | +declaring features whose deps cannot resolve only moves the failure downstream, so they |
| 63 | +are omitted rather than stubbed. |
| 64 | + |
| 65 | +### One TU goes through a generated stub (mcpp#233/#240) |
| 66 | + |
| 67 | +`core/platform/platform.cpp` is **not** declared directly. mcpp emits every package's |
| 68 | +objects into a single flat per-link `obj/` directory keyed by source basename, so upstream's |
| 69 | +`core/platform/platform.cpp` and `compat.glfw`'s `src/platform.c` both want `platform.o`. |
| 70 | + |
| 71 | +This is not theoretical. On a cold 646-object link with the naive declaration, `platform.o` |
| 72 | +was absent entirely and **both** packages lost their TU — neither `core::platform::*` nor |
| 73 | +`_glfwSelectPlatform` reached the binary. It still linked green, because the minimal test |
| 74 | +happened to reference neither. A real EUI application would not be so lucky, and neither |
| 75 | +would any consumer of `compat.glfw` that links a sibling package with a `platform.*`. |
| 76 | + |
| 77 | +The fix is the technique `compat.opencv5` established for its `modules/*/src` collisions: a |
| 78 | +uniquely named `generated_files` stub that `#include`s the real source. |
| 79 | + |
| 80 | +```lua |
| 81 | +generated_files = { |
| 82 | + ["mcpp_generated/eui_neo_platform_tu.cpp"] = "#include \"core/platform/platform.cpp\"\n", |
| 83 | +}, |
| 84 | +sources = { …, "mcpp_generated/eui_neo_platform_tu.cpp" }, |
| 85 | +``` |
| 86 | + |
| 87 | +Renaming one side is enough: with `platform.o` no longer contested, glfw's object survives |
| 88 | +too. `tests/examples/eui-neo` now calls `core::platform::consumeFrameRequest()` so a |
| 89 | +regression becomes an undefined reference rather than a silent pass. |
| 90 | + |
| 91 | +A scan of eui-neo's 20 sources against every transitively linked package |
| 92 | +(freetype/libpng/zlib/glfw/glad/tray/yyjson/opengl + the X11 stack) found `platform` to be |
| 93 | +the **only** collision. |
| 94 | + |
| 95 | +### Linux tray is deliberately a no-op |
| 96 | + |
| 97 | +Upstream sets `EUI_TRAY_APPINDICATOR=1` only when pkg-config finds **both** GTK3 and |
| 98 | +libappindicator. This index carries neither, so the Linux profile sets no tray define at |
| 99 | +all and `tray_bridge.c` compiles its `EUI_TRAY_HAS_BACKEND 0` stub — which is exactly what |
| 100 | +upstream produces on a machine without those dev packages. Windows (`EUI_TRAY_WINAPI`) and |
| 101 | +macOS (`EUI_TRAY_APPKIT`, Cocoa-native) get real tray backends. |
| 102 | + |
| 103 | +## Features |
| 104 | + |
| 105 | +| feature | gates | default | |
| 106 | +|---|---|---| |
| 107 | +| `app-main` | `core/app/glfw_app_main.cpp` — upstream's `int main()` and render loop | off | |
| 108 | +| `markdown` | `compat.md4c` dep + `EUI_HAS_MD4C=1` interface define | off | |
| 109 | + |
| 110 | +**`app-main`** is a sources-only gate, the direct analogue of `compat.gtest`'s `main` |
| 111 | +(gtest_main.cc). CMake adds this file per-application (`EUI_APP_MAIN_SOURCE`), never to |
| 112 | +the library, for the same reason it is opt-in here: a consumer with its own `main()` must |
| 113 | +not be handed a second one. A real EUI application enables it and supplies only |
| 114 | +`app::dslAppConfig()` and `app::compose()`. |
| 115 | + |
| 116 | +**`markdown`** is the more interesting one. `components/markdown.h` is header-only and |
| 117 | +compiles one of *two* definitions of `detail::parseMarkdownBlocks` depending on |
| 118 | +`EUI_HAS_MD4C` — the md4c parser, or a fallback that wraps the entire source in one |
| 119 | +Paragraph. The library itself gains no translation unit either way, so the whole feature |
| 120 | +lives on the consumer side. That is why the define goes in `defines` (an INTERFACE define, |
| 121 | +propagated to the consumer's TUs) rather than `cflags` (package-private): with `cflags`, |
| 122 | +md4c would link and the component would still silently compile out. |
| 123 | + |
| 124 | +Note the skill doc's "features 仅能门控 sources" reflects mcpp 0.0.68. On the pinned |
| 125 | +0.0.109, `defines` / `deps` / `implies` / `requires` / `provides` are all accepted — see |
| 126 | +`compat.eigen`, `chriskohlhoff.asio`, `compat.spdlog`. |
| 127 | + |
| 128 | +## Consumer contract (worth knowing before using this package) |
| 129 | + |
| 130 | +`eui_neo.h` pulls in `eui/detail/dsl_app_impl.h`, which emits `app::update()` / |
| 131 | +`app::render()` into the *consumer's* translation unit and leaves two symbols for the |
| 132 | +application to define: |
| 133 | + |
| 134 | +```cpp |
| 135 | +namespace app { |
| 136 | +const DslAppConfig& dslAppConfig(); |
| 137 | +void compose(eui::Ui& ui, const eui::Screen& screen); |
| 138 | +} |
| 139 | +``` |
| 140 | +
|
| 141 | +Omitting them is a link error, not a compile error. This mirrors upstream's |
| 142 | +`examples/*.cpp`, all of which define exactly these two. Both test members do the same. |
| 143 | +
|
| 144 | +## Verification |
| 145 | +
|
| 146 | +Local, mcpp **0.0.109** (matching `validate.yml` `env.MCPP_VERSION`), linux-x86_64, gcc 16.1.0. |
| 147 | +
|
| 148 | +Both workspace members pass: |
| 149 | +
|
| 150 | +``` |
| 151 | +$ mcpp test -p eui-neo |
| 152 | +compat.eui-neo smoke test: ok (parsed eui-neo v3, markdown gated off) |
| 153 | +header ... ok |
| 154 | + test result ok. 1 passed; 0 failed |
| 155 | + |
| 156 | +$ mcpp test -p eui-neo-markdown |
| 157 | +compat.eui-neo[markdown]: ok (2 blocks, h1 = 'Heading') |
| 158 | +markdown ... ok |
| 159 | + test result ok. 1 passed; 0 failed |
| 160 | +``` |
| 161 | +
|
| 162 | +### The library really is built |
| 163 | +
|
| 164 | +PR #133's descriptor passed CI while compiling **zero** translation units (see below), so |
| 165 | +this is checked against the objects rather than inferred from a green test. Per-package |
| 166 | +counts from the build cache: |
| 167 | +
|
| 168 | +``` |
| 169 | +compat.eui-neo@0.5.3 20 objs <- exactly the 20 declared sources |
| 170 | +compat.freetype@2.13.3 29 objs |
| 171 | +compat.glfw@3.4 23 objs |
| 172 | +compat.libpng@1.6.43 15 objs |
| 173 | +compat.zlib@1.3.2 15 objs |
| 174 | +compat.x11@1.8.13 406 objs |
| 175 | +compat.yyjson@0.12.0 1 obj |
| 176 | +compat.glad@… 1 obj |
| 177 | +compat.tray@… 1 obj |
| 178 | +``` |
| 179 | +
|
| 180 | +The default member's assertions run on `eui::json::Document` (`core/platform/json.cpp`) |
| 181 | +and `core::platform::consumeFrameRequest()` (`core/platform/platform.cpp`) — an empty or |
| 182 | +partial library fails at **link** time instead of silently passing. |
| 183 | +
|
| 184 | +### The mcpp#233 collision fix, measured |
| 185 | +
|
| 186 | +Cold link of `tests/examples/eui-neo`, before vs after routing platform.cpp through the |
| 187 | +generated stub: |
| 188 | +
|
| 189 | +| | objects in the link | `core::platform::*` | `_glfwSelectPlatform` | |
| 190 | +|---|---|---|---| |
| 191 | +| before | 646 | absent | absent | |
| 192 | +| after | 648 | `eui_neo_platform_tu.o` | `platform.o` | |
| 193 | +
|
| 194 | +Two objects recovered: eui-neo's TU, and `compat.glfw`'s `platform.o` that the contested |
| 195 | +name had been taking down with it. |
| 196 | +
|
| 197 | +### `app-main`, and what is NOT covered |
| 198 | +
|
| 199 | +`app-main` has no workspace member, because a member that enables it cannot run on a CI |
| 200 | +runner: the feature's whole point is that `main()` comes from upstream's render loop, which |
| 201 | +calls `glfwInit()` and opens a window. Verified out-of-tree on linux-x86_64 instead, with a |
| 202 | +throwaway member whose only source defines `dslAppConfig()` + `compose()` and no `main`: |
| 203 | +
|
| 204 | +``` |
| 205 | +Compiling compat.eui-neo v0.5.3 |
| 206 | +Compiling probe (test) |
| 207 | + Running bin/probe |
| 208 | +probe ... FAIL (exit 255) <- headless; glfwInit() has no display |
| 209 | + |
| 210 | +obj/glfw_app_main.o present, 648 objects in the link |
| 211 | +nm: 0000000000001897 T main <- main comes from the feature, not the consumer |
| 212 | +``` |
| 213 | +
|
| 214 | +So the gate compiles, supplies `main()`, and links against a consumer that has none. It is |
| 215 | +**not** verified on macOS or Windows, and the render loop is never executed anywhere. |
| 216 | +
|
| 217 | +More generally, nothing in this package's test surface draws a frame. Every member is |
| 218 | +headless by construction, so what CI proves is: the library builds on three platforms, its |
| 219 | +umbrella header is consumable, and the non-graphical facades (JSON, platform frame flags, |
| 220 | +markdown parsing) behave. Window creation, GL context setup, text rasterization, image |
| 221 | +decode, input and IME are all **unexercised**, as are the Windows WinAPI and macOS AppKit |
| 222 | +tray paths — they compile, they have never run. |
| 223 | +
|
| 224 | +Not modelled at all, and therefore not usable through this package: the `vulkan` render |
| 225 | +backend, the `sdl2` window backend, and `network` / `EUI_HAS_CURL`. Linux tray is a |
| 226 | +compiled no-op. See the sections above for why. |
| 227 | +
|
| 228 | +### Feature verification (both directions) |
| 229 | +
|
| 230 | +- **negative** — `tests/examples/eui-neo` does not request `markdown`, and asserts |
| 231 | + `parseMarkdownBlocks("# Heading\n\nBody text.\n")` returns the degenerate single |
| 232 | + Paragraph. Feature on by accident ⇒ this member fails. |
| 233 | +- **positive** — `tests/examples/eui-neo-markdown` requests it long-form and asserts an h1 |
| 234 | + block with text `Heading` plus ≥2 blocks. Interface define failing to propagate ⇒ this |
| 235 | + member fails (it `#if !defined(EUI_HAS_MD4C)`s to an explicit failure first). |
| 236 | +
|
| 237 | +## CN mirror |
| 238 | +
|
| 239 | +Published to gitcode `mcpp-res` per `docs/cn-mirror.md`, so the `url` is a |
| 240 | +`{ GLOBAL, CN }` table on all three platforms: |
| 241 | +
|
| 242 | +``` |
| 243 | +repo https://gitcode.com/mcpp-res/eui-neo |
| 244 | +CN https://gitcode.com/mcpp-res/eui-neo/releases/download/0.5.3/eui-neo-0.5.3.tar.gz |
| 245 | +``` |
| 246 | +
|
| 247 | +Closed-loop verified — the asset is the byte-identical GLOBAL tarball, not a repack: |
| 248 | +
|
| 249 | +``` |
| 250 | +CN http=200 |
| 251 | +GLOBAL=6951ac330d0307c633bafe720b7888bf32785103eb16973adb4ee05ef06e64d1 |
| 252 | +CN =6951ac330d0307c633bafe720b7888bf32785103eb16973adb4ee05ef06e64d1 |
| 253 | +BYTE-IDENTICAL |
| 254 | +``` |
| 255 | +
|
| 256 | +## What PR #133 got wrong |
| 257 | +
|
| 258 | +Recorded because the failure mode is subtle and CI did not catch it. |
| 259 | +
|
| 260 | +1. **The package compiled nothing, and CI was green.** Its `install()` hook guessed the |
| 261 | + tarball's wrap directory as `main` / `EUI-NEO-<version>` / `EUI-NEO-main`; the actual |
| 262 | + name was `EUI-NEO-M-main` (it pointed at a personal fork's branch archive). All three |
| 263 | + guesses missed, `os.tryrm(install_dir())` then removed the install dir, `os.mv` failed, |
| 264 | + and the hook `return true`d anyway — so mcpp recorded a successful install over a |
| 265 | + directory that did not exist. Every source glob then matched zero files. The smoke test |
| 266 | + was `import std; println(...)` referencing no EUI symbol, so even the link succeeded. |
| 267 | + Measured: `0 objs` for `compat.eui-neo`, against 29/15/23 for freetype/libpng/glfw. |
| 268 | +2. **The `install()` hook should not exist.** House style (`docs/package-types.md`, |
| 269 | + `compat.md4c`, `compat.libpng`) absorbs the wrap layer with a `*/` glob prefix. PR #133 |
| 270 | + removed those prefixes to compensate for its own hook. |
| 271 | +3. **"Form B → Form A include propagation not supported" was a misdiagnosis.** |
| 272 | + `tests/examples/freetype` does `#include <ft2build.h>` against a Form B package and |
| 273 | + passes on main. Headers were unreachable because the verdir did not exist. |
| 274 | +4. **`sha256 = ""`** disabled integrity verification on all three platforms, against a |
| 275 | + **moving branch head** (`refs/heads/main`) of a personal fork, while `repo` pointed at |
| 276 | + upstream. Upstream tags `v0.5.3`; this descriptor pins it with a real digest. |
| 277 | +5. **Dead paths.** `3rd/yyjson-0.12.0/src/yyjson.c` and `3rd/tray` do not exist in that |
| 278 | + fork's archive, and `compat.yyjson` / `compat.tray` / `compat.glad` — three of the six |
| 279 | + packages #131 added *for this framework* — were not declared as deps at all. |
| 280 | +6. **Unconditional `-DEUI_TRAY_APPINDICATOR=1` on Linux**, which upstream only sets when |
| 281 | + GTK3 + libappindicator are present. It would have required GTK3 headers this index does |
| 282 | + not carry. |
| 283 | +7. **Features with no resolvable deps** (`vulkan`, `sdl2`, `network`) — defines only, no |
| 284 | + packages behind them. |
| 285 | +
|
| 286 | +## Windows: `-fno-char8_t` |
| 287 | +
|
| 288 | +`parseWindowsSelection()` in `core/platform/platform.cpp` pushes `path::u8string()` into a |
| 289 | +`std::vector<std::string>`. C++20 changed that return type to `std::u8string`, so the line |
| 290 | +does not compile at this index's c++23 floor — upstream builds at `CMAKE_CXX_STANDARD 17` |
| 291 | +and never sees it. It sits inside `#if defined(_WIN32)`, so Linux and macOS do not either. |
| 292 | +
|
| 293 | +`language = "c++17"` is not an option (mcpp accepts c++23 and up). The root cause is |
| 294 | +`char8_t` rather than the standard level — every STL selects the `u8string()` return type |
| 295 | +on `__cpp_char8_t` — so the Windows profile carries `cxxflags = { "-fno-char8_t" }` and the |
| 296 | +package stays at c++23 everywhere. |
| 297 | +
|
| 298 | +Worth fixing upstream: `wideToUtf8()` already sits eight lines above and does the right |
| 299 | +thing. Until then this is what keeps the descriptor on a real upstream release tag instead |
| 300 | +of a fork carrying the patch (which is what PR #133 did). |
| 301 | +
|
| 302 | +Note this only became visible once `platform.cpp` was actually being compiled — the |
| 303 | +mcpp#233 collision above had been silently dropping the TU, so no compiler ever saw the |
| 304 | +line. |
| 305 | +
|
| 306 | +## Follow-up |
| 307 | +
|
| 308 | +- Upstream PR for the `u8string()` line, after which `-fno-char8_t` can go. |
| 309 | +- `compat.vulkan` / `compat.sdl2` / `compat.curl` would unlock the corresponding backends. |
| 310 | +- The C++23 module layer (`import eui;`) remains open, and now has a working header-compat |
| 311 | + base to build on. |
0 commit comments