|
| 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 | +| License | Apache-2.0 | |
| 30 | + |
| 31 | +## Shape decision: C++ source compat (Form B), deps reused from the index |
| 32 | + |
| 33 | +Upstream vendors its whole dependency set under `3rd/` — freetype, glfw, libpng, zlib, |
| 34 | +glad, tray, yyjson, md4c, all fully checked in (no submodules). **None of them are built |
| 35 | +here.** Each already exists in this index at the same version upstream pins, and building |
| 36 | +them once for the ecosystem is the entire point of having them: |
| 37 | + |
| 38 | +| upstream `3rd/` | index package | version match | |
| 39 | +|---|---|---| |
| 40 | +| `3rd/freetype` | `compat.freetype` | 2.13.3 | |
| 41 | +| `3rd/libpng-1.6.43` | `compat.libpng` | 1.6.43 | |
| 42 | +| `3rd/zlib-1.3.1` | `compat.zlib` | 1.3.2 | |
| 43 | +| `3rd/glfw` | `compat.glfw` | 3.4 | |
| 44 | +| `3rd/glad` | `compat.glad` | 0.0.0-651a425 — the exact commit `3rd/dependencies.cmake` fetches | |
| 45 | +| `3rd/tray` | `compat.tray` | 0.0.0-8dd1358 | |
| 46 | +| `3rd/yyjson-0.12.0` | `compat.yyjson` | 0.12.0 | |
| 47 | +| `3rd/md4c` | `compat.md4c` | 0.5.3 (feature-gated) | |
| 48 | + |
| 49 | +`3rd/` still sits on the include path: `stb_image.h`, `nanosvg.h` and `nanosvgrast.h` are |
| 50 | +genuinely vendored single-file headers at its root, and the sources include them as |
| 51 | +`"3rd/stb_image.h"`. |
| 52 | + |
| 53 | +The build recipe tracks upstream `CMakeLists.txt` v0.5.3: `CORE_SOURCES` + the OpenGL |
| 54 | +backend + glfw's `ime_bridge.c` = 20 translation units. |
| 55 | + |
| 56 | +### Backend selection is a build-time constant, not a feature |
| 57 | + |
| 58 | +Upstream picks the render and window backend at configure time and compiles exactly one |
| 59 | +in. Only `opengl` + `glfw` are modelled. `vulkan`, `sdl2` and `network` would need |
| 60 | +`compat.vulkan`, `compat.sdl2` and `compat.curl`, none of which exist in this index — |
| 61 | +declaring features whose deps cannot resolve only moves the failure downstream, so they |
| 62 | +are omitted rather than stubbed. |
| 63 | + |
| 64 | +### One TU goes through a generated stub (mcpp#233/#240) |
| 65 | + |
| 66 | +`core/platform/platform.cpp` is **not** declared directly. mcpp emits every package's |
| 67 | +objects into a single flat per-link `obj/` directory keyed by source basename, so upstream's |
| 68 | +`core/platform/platform.cpp` and `compat.glfw`'s `src/platform.c` both want `platform.o`. |
| 69 | + |
| 70 | +This is not theoretical. On a cold 646-object link with the naive declaration, `platform.o` |
| 71 | +was absent entirely and **both** packages lost their TU — neither `core::platform::*` nor |
| 72 | +`_glfwSelectPlatform` reached the binary. It still linked green, because the minimal test |
| 73 | +happened to reference neither. A real EUI application would not be so lucky, and neither |
| 74 | +would any consumer of `compat.glfw` that links a sibling package with a `platform.*`. |
| 75 | + |
| 76 | +The fix is the technique `compat.opencv5` established for its `modules/*/src` collisions: a |
| 77 | +uniquely named `generated_files` stub that `#include`s the real source. |
| 78 | + |
| 79 | +```lua |
| 80 | +generated_files = { |
| 81 | + ["mcpp_generated/eui_neo_platform_tu.cpp"] = "#include \"core/platform/platform.cpp\"\n", |
| 82 | +}, |
| 83 | +sources = { …, "mcpp_generated/eui_neo_platform_tu.cpp" }, |
| 84 | +``` |
| 85 | + |
| 86 | +Renaming one side is enough: with `platform.o` no longer contested, glfw's object survives |
| 87 | +too. `tests/examples/eui-neo` now calls `core::platform::consumeFrameRequest()` so a |
| 88 | +regression becomes an undefined reference rather than a silent pass. |
| 89 | + |
| 90 | +A scan of eui-neo's 20 sources against every transitively linked package |
| 91 | +(freetype/libpng/zlib/glfw/glad/tray/yyjson/opengl + the X11 stack) found `platform` to be |
| 92 | +the **only** collision. |
| 93 | + |
| 94 | +### Linux tray is deliberately a no-op |
| 95 | + |
| 96 | +Upstream sets `EUI_TRAY_APPINDICATOR=1` only when pkg-config finds **both** GTK3 and |
| 97 | +libappindicator. This index carries neither, so the Linux profile sets no tray define at |
| 98 | +all and `tray_bridge.c` compiles its `EUI_TRAY_HAS_BACKEND 0` stub — which is exactly what |
| 99 | +upstream produces on a machine without those dev packages. Windows (`EUI_TRAY_WINAPI`) and |
| 100 | +macOS (`EUI_TRAY_APPKIT`, Cocoa-native) get real tray backends. |
| 101 | + |
| 102 | +## Features |
| 103 | + |
| 104 | +| feature | gates | default | |
| 105 | +|---|---|---| |
| 106 | +| `app-main` | `core/app/glfw_app_main.cpp` — upstream's `int main()` and render loop | off | |
| 107 | +| `markdown` | `compat.md4c` dep + `EUI_HAS_MD4C=1` interface define | off | |
| 108 | + |
| 109 | +**`app-main`** is a sources-only gate, the direct analogue of `compat.gtest`'s `main` |
| 110 | +(gtest_main.cc). CMake adds this file per-application (`EUI_APP_MAIN_SOURCE`), never to |
| 111 | +the library, for the same reason it is opt-in here: a consumer with its own `main()` must |
| 112 | +not be handed a second one. A real EUI application enables it and supplies only |
| 113 | +`app::dslAppConfig()` and `app::compose()`. |
| 114 | + |
| 115 | +**`markdown`** is the more interesting one. `components/markdown.h` is header-only and |
| 116 | +compiles one of *two* definitions of `detail::parseMarkdownBlocks` depending on |
| 117 | +`EUI_HAS_MD4C` — the md4c parser, or a fallback that wraps the entire source in one |
| 118 | +Paragraph. The library itself gains no translation unit either way, so the whole feature |
| 119 | +lives on the consumer side. That is why the define goes in `defines` (an INTERFACE define, |
| 120 | +propagated to the consumer's TUs) rather than `cflags` (package-private): with `cflags`, |
| 121 | +md4c would link and the component would still silently compile out. |
| 122 | + |
| 123 | +Note the skill doc's "features 仅能门控 sources" reflects mcpp 0.0.68. On the pinned |
| 124 | +0.0.109, `defines` / `deps` / `implies` / `requires` / `provides` are all accepted — see |
| 125 | +`compat.eigen`, `chriskohlhoff.asio`, `compat.spdlog`. |
| 126 | + |
| 127 | +## Consumer contract (worth knowing before using this package) |
| 128 | + |
| 129 | +`eui_neo.h` pulls in `eui/detail/dsl_app_impl.h`, which emits `app::update()` / |
| 130 | +`app::render()` into the *consumer's* translation unit and leaves two symbols for the |
| 131 | +application to define: |
| 132 | + |
| 133 | +```cpp |
| 134 | +namespace app { |
| 135 | +const DslAppConfig& dslAppConfig(); |
| 136 | +void compose(eui::Ui& ui, const eui::Screen& screen); |
| 137 | +} |
| 138 | +``` |
| 139 | +
|
| 140 | +Omitting them is a link error, not a compile error. This mirrors upstream's |
| 141 | +`examples/*.cpp`, all of which define exactly these two. Both test members do the same. |
| 142 | +
|
| 143 | +## Verification |
| 144 | +
|
| 145 | +Local, mcpp **0.0.109** (matching `validate.yml` `env.MCPP_VERSION`), linux-x86_64, gcc 16.1.0. |
| 146 | +
|
| 147 | +Both workspace members pass: |
| 148 | +
|
| 149 | +``` |
| 150 | +$ mcpp test -p eui-neo |
| 151 | +compat.eui-neo smoke test: ok (parsed eui-neo v3, markdown gated off) |
| 152 | +header ... ok |
| 153 | + test result ok. 1 passed; 0 failed |
| 154 | + |
| 155 | +$ mcpp test -p eui-neo-markdown |
| 156 | +compat.eui-neo[markdown]: ok (2 blocks, h1 = 'Heading') |
| 157 | +markdown ... ok |
| 158 | + test result ok. 1 passed; 0 failed |
| 159 | +``` |
| 160 | +
|
| 161 | +### The library really is built |
| 162 | +
|
| 163 | +PR #133's descriptor passed CI while compiling **zero** translation units (see below), so |
| 164 | +this is checked against the objects rather than inferred from a green test. Per-package |
| 165 | +counts from the build cache: |
| 166 | +
|
| 167 | +``` |
| 168 | +compat.eui-neo@0.5.3 20 objs <- exactly the 20 declared sources |
| 169 | +compat.freetype@2.13.3 29 objs |
| 170 | +compat.glfw@3.4 23 objs |
| 171 | +compat.libpng@1.6.43 15 objs |
| 172 | +compat.zlib@1.3.2 15 objs |
| 173 | +compat.x11@1.8.13 406 objs |
| 174 | +compat.yyjson@0.12.0 1 obj |
| 175 | +compat.glad@… 1 obj |
| 176 | +compat.tray@… 1 obj |
| 177 | +``` |
| 178 | +
|
| 179 | +The default member's assertions run on `eui::json::Document` (`core/platform/json.cpp`) |
| 180 | +and `core::platform::consumeFrameRequest()` (`core/platform/platform.cpp`) — an empty or |
| 181 | +partial library fails at **link** time instead of silently passing. |
| 182 | +
|
| 183 | +### The mcpp#233 collision fix, measured |
| 184 | +
|
| 185 | +Cold link of `tests/examples/eui-neo`, before vs after routing platform.cpp through the |
| 186 | +generated stub: |
| 187 | +
|
| 188 | +| | objects in the link | `core::platform::*` | `_glfwSelectPlatform` | |
| 189 | +|---|---|---|---| |
| 190 | +| before | 646 | absent | absent | |
| 191 | +| after | 648 | `eui_neo_platform_tu.o` | `platform.o` | |
| 192 | +
|
| 193 | +Two objects recovered: eui-neo's TU, and `compat.glfw`'s `platform.o` that the contested |
| 194 | +name had been taking down with it. |
| 195 | +
|
| 196 | +### Feature verification (both directions) |
| 197 | +
|
| 198 | +- **negative** — `tests/examples/eui-neo` does not request `markdown`, and asserts |
| 199 | + `parseMarkdownBlocks("# Heading\n\nBody text.\n")` returns the degenerate single |
| 200 | + Paragraph. Feature on by accident ⇒ this member fails. |
| 201 | +- **positive** — `tests/examples/eui-neo-markdown` requests it long-form and asserts an h1 |
| 202 | + block with text `Heading` plus ≥2 blocks. Interface define failing to propagate ⇒ this |
| 203 | + member fails (it `#if !defined(EUI_HAS_MD4C)`s to an explicit failure first). |
| 204 | +
|
| 205 | +## CN mirror |
| 206 | +
|
| 207 | +No `mcpp-res` write access, so per `docs/cn-mirror.md` the fallback applies: plain-string |
| 208 | +upstream `url`, no `{ GLOBAL, CN }` table. CN users fall back to the GitHub source. |
| 209 | +Maintainers can add the mirror later without touching `sha256`. |
| 210 | +
|
| 211 | +## What PR #133 got wrong |
| 212 | +
|
| 213 | +Recorded because the failure mode is subtle and CI did not catch it. |
| 214 | +
|
| 215 | +1. **The package compiled nothing, and CI was green.** Its `install()` hook guessed the |
| 216 | + tarball's wrap directory as `main` / `EUI-NEO-<version>` / `EUI-NEO-main`; the actual |
| 217 | + name was `EUI-NEO-M-main` (it pointed at a personal fork's branch archive). All three |
| 218 | + guesses missed, `os.tryrm(install_dir())` then removed the install dir, `os.mv` failed, |
| 219 | + and the hook `return true`d anyway — so mcpp recorded a successful install over a |
| 220 | + directory that did not exist. Every source glob then matched zero files. The smoke test |
| 221 | + was `import std; println(...)` referencing no EUI symbol, so even the link succeeded. |
| 222 | + Measured: `0 objs` for `compat.eui-neo`, against 29/15/23 for freetype/libpng/glfw. |
| 223 | +2. **The `install()` hook should not exist.** House style (`docs/package-types.md`, |
| 224 | + `compat.md4c`, `compat.libpng`) absorbs the wrap layer with a `*/` glob prefix. PR #133 |
| 225 | + removed those prefixes to compensate for its own hook. |
| 226 | +3. **"Form B → Form A include propagation not supported" was a misdiagnosis.** |
| 227 | + `tests/examples/freetype` does `#include <ft2build.h>` against a Form B package and |
| 228 | + passes on main. Headers were unreachable because the verdir did not exist. |
| 229 | +4. **`sha256 = ""`** disabled integrity verification on all three platforms, against a |
| 230 | + **moving branch head** (`refs/heads/main`) of a personal fork, while `repo` pointed at |
| 231 | + upstream. Upstream tags `v0.5.3`; this descriptor pins it with a real digest. |
| 232 | +5. **Dead paths.** `3rd/yyjson-0.12.0/src/yyjson.c` and `3rd/tray` do not exist in that |
| 233 | + fork's archive, and `compat.yyjson` / `compat.tray` / `compat.glad` — three of the six |
| 234 | + packages #131 added *for this framework* — were not declared as deps at all. |
| 235 | +6. **Unconditional `-DEUI_TRAY_APPINDICATOR=1` on Linux**, which upstream only sets when |
| 236 | + GTK3 + libappindicator are present. It would have required GTK3 headers this index does |
| 237 | + not carry. |
| 238 | +7. **Features with no resolvable deps** (`vulkan`, `sdl2`, `network`) — defines only, no |
| 239 | + packages behind them. |
| 240 | +
|
| 241 | +## Follow-up |
| 242 | +
|
| 243 | +- CN mirror once `mcpp-res` write access is available. |
| 244 | +- `compat.vulkan` / `compat.sdl2` / `compat.curl` would unlock the corresponding backends. |
| 245 | +- The C++23 module layer (`import eui;`) remains open, and now has a working header-compat |
| 246 | + base to build on. |
0 commit comments