Skip to content

Commit ff226c0

Browse files
FarnaHerryspeak-agent
authored andcommitted
feat: add compat.eui-neo 0.5.3 (header-compat shape)
EUI-NEO as a Form B compat package: 20 core TUs compiled into one lib, public headers exposed via include_dirs, so consumers write `#include <eui_neo.h>`. The C++23 module surface is out of scope — upstream ships no module interface units. Sourced from upstream's v0.5.3 release tag with a real sha256, mirrored to gitcode mcpp-res/eui-neo (byte-identical asset, CN reachable). Upstream vendors freetype/glfw/libpng/zlib/glad/tray/yyjson/md4c under 3rd/; none are built here — each is already in this index at the same version, which is what the six packages from #131 were for. Two features: `app-main` gates upstream's glfw entry point (the compat.gtest `main` pattern), `markdown` pulls compat.md4c and publishes EUI_HAS_MD4C as an interface define so the header-only component compiles in on the consumer side. core/platform/platform.cpp goes through a uniquely named generated TU. It collides with compat.glfw's src/platform.c in mcpp's flat per-link obj/ dir (mcpp#233/#240), and the collision was dropping BOTH objects: a cold 646-object link had neither core::platform::* nor _glfwSelectPlatform in it, and still went green because nothing referenced them. With the stub the link carries 648 objects and both are back. Windows carries -fno-char8_t. parseWindowsSelection() pushes path::u8string() into a vector<string>, which C++20 broke by changing the return type to u8string; upstream builds at CMAKE_CXX_STANDARD 17 and never sees it, and the code is inside `#if defined(_WIN32)` so neither do Linux and macOS. Disabling char8_t is what every STL's <filesystem> branches on, so the package stays at c++23 everywhere. Worth fixing upstream — wideToUtf8() already sits eight lines above — and it only became visible once the collision above stopped eating the TU. Verified cold on linux-x86_64 with mcpp 0.0.109 (the validate.yml pin): `mcpp test -p eui-neo` and `mcpp test -p eui-neo-markdown` both pass, the latter asserting the md4c parser is really active while the former asserts it is really gated off. Design: .agents/docs/2026-07-29-add-eui-neo-plan.md Co-authored-by: SPeak Agent <248744407+speak-agent@users.noreply.github.com>
1 parent ea74cf1 commit ff226c0

8 files changed

Lines changed: 700 additions & 0 deletions

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上
3535
| 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) |
3636
| C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) |
3737
| header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) |
38+
| C++ 应用框架 compat(依赖复用索引内既有包) | [`compat.eui-neo`](pkgs/e/compat.eui-neo.lua)(上游 `3rd/` 自带 8 个 vendored 依赖,此处一个不编,全部改指索引内同版本 `compat.*`;`app-main` 门控上游 `main()`,`markdown` 经 interface define 打开消费侧 header) |
3839
| 单包多 major(形态随版本切换) | [`compat.catch2`](pkgs/c/compat.catch2.lua)(3.x 编 `src/catch2/` 出静态库;2.x 走 `single_include/` header-only) |
3940
| 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) |
4041
| 全源码直编(config 快照 + 源列表,零外部构建系统) | [`compat.ffmpeg`](pkgs/c/compat.ffmpeg.lua)(2281 TU 含 NASM 汇编,28 个目录 glob 声明) |

mcpp.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ members = [
1818
"tests/examples/cjson",
1919
"tests/examples/core",
2020
"tests/examples/eigen",
21+
"tests/examples/eui-neo",
22+
"tests/examples/eui-neo-markdown",
2123
"tests/examples/ffmpeg",
2224
"tests/examples/ffmpeg-module",
2325
"tests/examples/fmtlib.fmt",

0 commit comments

Comments
 (0)