From 6f36e3330e902d6e51c79ac3c0d674331616dd38 Mon Sep 17 00:00:00 2001 From: mcpp-index agent Date: Sat, 3 Oct 2026 03:15:22 +0800 Subject: [PATCH] feat(pkg): add LibSerial 1.0.0 compat package Add compat.libserial, a Linux-only C++-source compat package for LibSerial 1.0.0 (SerialPort / SerialStream), built from upstream's LIBSERIAL_SOURCES three translation units and exposing the public headers under libserial/ the way upstream's install does. The headers open with and every TU reaches , so the descriptor declares only the linux platform and consumers gate it with [target.'cfg(linux)'.dependencies] (same shape as compat.libaio). Upstream's SerialPortConstants.h uses uint8_t without including ; a generated_files shim supplies it and #include_next's the real header. The test opens on the libserial header first of all, so the shim is compile-time asserted, and asserts the rest over a real openpty() pair. No CN mirror (no mcpp-res write access): plain-string upstream url per docs/cn-mirror.md. --- .agents/docs/2026-10-03-add-libserial-plan.md | 111 +++++++++++++ CHANGELOG.md | 2 + docs/descriptor-examples.md | 1 + docs/zh/descriptor-examples.md | 1 + mcpp.toml | 1 + pkgs/c/compat.libserial.lua | 101 +++++++++++ tests/examples/libserial/mcpp.toml | 19 +++ tests/examples/libserial/tests/serial.cpp | 157 ++++++++++++++++++ 8 files changed, 393 insertions(+) create mode 100644 .agents/docs/2026-10-03-add-libserial-plan.md create mode 100644 pkgs/c/compat.libserial.lua create mode 100644 tests/examples/libserial/mcpp.toml create mode 100644 tests/examples/libserial/tests/serial.cpp diff --git a/.agents/docs/2026-10-03-add-libserial-plan.md b/.agents/docs/2026-10-03-add-libserial-plan.md new file mode 100644 index 00000000..93a1ff87 --- /dev/null +++ b/.agents/docs/2026-10-03-add-libserial-plan.md @@ -0,0 +1,111 @@ +# 收录 LibSerial 1.0.0(compat.libserial) + +日期:2026-10-03 · 分支:`feat/add-libserial` · 状态:本地验证通过 + +## 1. 来源与形态判定 + +LibSerial 属于来源 (a):第三方上游库,上游不提供 mcpp 支持。 + +- 上游:。 +- 最新版本:`git ls-remote --tags` 里 v1 系列只有 **`v1.0.0`**;更早的 tag 是 + `libserial_0_5_0` / `libserial_0_6_0rc1..3` / `v0.6.0rc3` 这类 0.5/0.6 候选版, + 不收录。 +- License:BSD-3-Clause(`LICENSE.txt`,版权归 LibSerial Development Team)。 +- 源码布局:`libserial-1.0.0/` 包一层,库源码是 `src/SerialPort.cpp`、 + `src/SerialStream.cpp`、`src/SerialStreamBuf.cpp` 三个 TU,公开头在 + `src/libserial/`(`SerialPort.h`、`SerialStream.h`、`SerialStreamBuf.h`、 + `SerialPortConstants.h`)。无 configure 生成的配置头(CMake 只做选项开关), + 无 submodule、无必需生成步骤。 + +**形态 = A(C++ 源码 compat)**,与 compat.websocket / compat.yaml-cpp 同类: +把上游 CMake 的 `LIBSERIAL_SOURCES` 编成一个 lib,公开头经 `include_dirs` 暴露, +消费者写 `#include `(上游 `install(DIRECTORY libserial +DESTINATION include)` 的布局)。 + +## 2. 版本与下载源 + +`sha256 = 063142d6bfe08898316e9a6055f2ddeedef56de06f7cfc8dcdfecc6efabf4bdd`(121235 字节,连算两次一致)。 + +GLOBAL 用 GitHub 的 tag 归档 `.../archive/refs/tags/v1.0.0.tar.gz`。 + +## 3. CN 镜像 + +当前环境没有 gitcode `mcpp-res` 写权限(`~/.config/gitcode-tool/config.json` 不存在、 +`GITCODE_TOKEN` 未设置)。按 `docs/cn-mirror.md` 的回退方案,用**纯字符串 url** 指向 +上游 release,lint 对纯字符串不做镜像约束;待拿到 mcpp-res 权限后再改写成 +`{ GLOBAL, CN }` 表(sha256 不变)。先例:compat.libuv / compat.hiredis / +compat.websocket。 + +## 4. 平台判定:仅 linux + +LibSerial 面向 POSIX:公开头 `SerialPortConstants.h` 顶部就是 `#include `, +实现 TU 一律触及 ``、``、``,驱动的 ioctl +(`TIOCEXCL` / `TIOCMGET` / `FIONREAD` / `TIOCSSERIAL`)与 `struct serial_struct` +都是 Linux UAPI。上游 CMake/autotools 也只在 Linux 发行版上分发。 + +因此 `xpm` 只写 `linux` 一段,消费者用 `[target.'cfg(linux)'.dependencies]` 门控, +测试在非 Linux 上编成 no-op `main()` —— 与 compat.libaio 完全同形。单平台 `xpm` +不会触发 platform-version-parity(它只比较**都带版本**的平台)。 + +## 5. 一处消费者可见的头文件缺陷(用 generated_files 修复) + +`src/libserial/SerialPortConstants.h` 声明 `using DataBuffer = std::vector;`, +却既没 `#include ` 也没 `#include `。在 gcc 16.1.0 / libstdc++ 上它 +**只是碰巧**能编 —— 靠其它 stdlib 头的传递包含;一个先打开 libserial 头的干净 TU 会报 +`'uint8_t' was not declared in this scope`(已在 tarball 上复现)。 + +修法沿用 compat.yaml-cpp / compat.cpptrace / compat.catch2 的 shim 模式: +`generated_files` 里放一个同名路径 `mcpp_generated/libserial/SerialPortConstants.h`, +先 `#include ` 再 `#include_next `;`include_dirs` +把 `mcpp_generated` 排在 `*/src` 之前,shim 命中在真头之前。包自身的三个 TU 与消费者 +都经过它。 + +测试把 `#include ` 放在**整个文件的第一行**(上面不放任何 +stdlib 头),使 shim 成为编译期断言:去掉 shim 该 TU 就编不过。 + +## 6. feature 评估:无 + +判据是「是否存在额外的、可门控的**可编译源码**」。LibSerial 没有: + +- `test/` 是 GoogleTest 套件(自带 `main()`),mcpp 的 lib 目标对象全量入链,包里带 + `main()` 会与消费者冲突; +- `sip/` 是 Python SIP 绑定(需 sip/PythonLibs,非库的可选编译组件); +- `doxygen.conf.in` 只生成文档。 + +故 `features` 整个不声明。 + +## 7. 测试成员 `tests/examples/libserial` + +依赖按 `[target.'cfg(linux)'.dependencies.compat]` 门控,非 Linux 编成 no-op。断言跑在 +真实 **openpty()** 对上(构建机没有 `/dev/ttyUSB0`;纯链接测试即使 termios 全错也会绿): + +1. `NotOpen` 契约:关闭状态下 `Write`/`GetBaudRate` 抛 `NotOpen`; +2. `Open`/`IsOpen`,二次 `Open` 抛 `AlreadyOpen`; +3. 打开默认值:`BAUD_115200`、`CHAR_SIZE_8`、`PARITY_NONE`、`STOP_BITS_1`; +4. `Set/Get` 往返:**只断言 pty 真正承载的属性**(波特率、停止位、流控)。CS7/偶校验 + 不断言 —— pty 的行规程不保存这些位,写了也读回 CS8/NONE(实测),那是 pty 的性质、 + 不是 LibSerial 的,所以只钉可观测的部分; +5. TX:`SerialPort::Write` → 主机 `read` 收到 `"hello\n"`; +6. RX:主机 `write` `"ping\n"` → `IsDataAvailable` + `ReadByte` + `ReadLine` 拼出 `"ing\n"`; +7. `SerialStream`:用**另一对** pty(复用同一对会在二次 Open 时抛 `OpenFailed`,那是上游 + Open 路径的行为)验证 iostream 写路径 `stream << "xyz"`,主机读到 `"xyz"`。 + +**故意不测 SerialStream 的读路径**:上游 `SerialStreamBuf::showmanyc()` 置了 putback +标志却没存下它读到的那个字节,于是 `rdbuf()->in_avail()` 后接流式提取会吐出乱码首字节, +而安静 pty 上 `stream >> x` 会阻塞(实测)。这是 `SerialStreamBuf` 的上游缺陷,不在本 +描述符能修的范围内,故测试只钉能工作的那一半。 + +## 8. 验证结论(与 CI 同版本 mcpp) + +- `mcpp xpkg parse pkgs/c/compat.libserial.lua` → `parse OK`。 +- 冷跑(先删 `target/` 与 `.mcpp/`)`mcpp test -p libserial` → `test result ok`。 +- 六个本地 lint(syntax / mirror-urls / package-name / platform-parity / + duplicate-versions / cross-package-refs)全部通过。 +- 负向验证:把 TX 断言改成必失败,`mcpp test` 报 `FAIL`/非零退出,证明断言确实生效。 + +## 9. 注意事项 + +- 上游公开头对 `` 的隐式依赖是本包必须带 shim 的唯一原因;若未来上游补上该 + 包含,shim 仍是无害的(重复包含 与 include_next 都安全)。 +- `include_dirs` 用 `*/src`(上游安装根),该目录下只有 `libserial/` 一个子目录,不会 + 遮蔽任何系统头。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0cfcf9..4d22b089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ ### Added +- **`compat.libserial` 1.0.0。** LibSerial 的 `SerialPort`/`SerialStream` 以 compat 形态收录:上游 CMake 的 `LIBSERIAL_SOURCES` 三个 TU 编成一个 lib,公开头经 `include_dirs` 暴露。仅声明 `linux`(公开头以 `` 开头、实现触及 ``,消费者用 `[target.'cfg(linux)'.dependencies]` 门控)。上游 `SerialPortConstants.h` 用了 `uint8_t` 却未含 ``,仅靠传递包含碰巧能编;本包用 `generated_files` shim 补上并由测试的首行包含做编译期断言。无 CN 镜像(无 mcpp-res 写权限,按文档回退为纯上游 url)。 + - **`openkal-linux` 0.16.0、`openkal-macos` 0.13.0、`openkal-windows` 0.11.0、 `openkal-emscripten` 0.4.0、`openkal-opensbi` 0.8.2、`openkal-musl` 0.20.0。** 六个同轮仓库的行:GLOBAL 为上游 tag 归档,CN 为字节一致的 gitcode 资源 diff --git a/docs/descriptor-examples.md b/docs/descriptor-examples.md index 9cbfe21b..615a545d 100644 --- a/docs/descriptor-examples.md +++ b/docs/descriptor-examples.md @@ -18,6 +18,7 @@ in the [root README](../README.md#reference-examples). | C-source compat (with `features`) | [`compat.cjson`](../pkgs/c/compat.cjson.lua) · [`compat.zlib`](../pkgs/c/compat.zlib.lua) · [`compat.hiredis`](../pkgs/c/compat.hiredis.lua) (the classic 1.2.0 — a 7-TU C build whose flat tarball headers get `hiredis/`-prefixed wrapper headers via `generated_files`, so consumers write `#include ` exactly like upstream's install layout) · [`compat.sqlite3`](../pkgs/c/compat.sqlite3.lua) (plain C-source, no features: the single `sqlite3.c` amalgamation; 3.45.3, the final maintenance release of the most widely deployed 3.45.x line) · [`compat.libuv`](../pkgs/c/compat.libuv.lua) (libuv 1.48.0 — the per-OS source sets transcribed from upstream's CMakeLists, because a `src/unix/*.c` glob would compile every OS's backend at once; linux/macos get explicit unix subsets, windows globs `src/win/*.c`) · [`compat.xxhash`](../pkgs/c/compat.xxhash.lua) (one TU, one header, no features at all — the interesting decision is what is NOT compiled: `xxh_x86dispatch.c` selects an AVX2/AVX512 path at RUNTIME and needs per-file `-mavx2` plus `XXH_X86DISPATCH` at every call site, so the package ships the flagless SSE2 baseline instead. Nor is the header-only `XXH_INLINE_ALL` mode chosen: it re-emits the implementation in every TU that hashes anything, which is the right trade only when there is exactly one such TU — something a package cannot know) · [`compat.quickjs-ng`](../pkgs/c/compat.quickjs-ng.lua) (QuickJS-ng 0.17.0 — upstream's four engine TUs and nothing else. `quickjs-libc.c`, the optional standard library that gives scripts files, the environment, timers and child processes, is NOT compiled, so an embedder gets an engine with no host access unless it adds its own. The tarball keeps `quickjs.h` beside private headers with generic names (`list.h`, `cutils.h`), so a `generated_files` forwarder is the only include directory, as in compat.libaio) | | C-source compat whose sources are chosen by ARCHITECTURE | [`compat.wamr`](../pkgs/c/compat.wamr.lua) (WAMR 2.4.5 — the descriptor schema varies `sources` and `cflags` per OS but has no per-architecture hook, and `archs` is package metadata rather than a selector. WAMR needs both: one of `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` must be defined or `wasm_runtime_common.c` compiles its whole invoke-native section away and the link fails on `invokeNative`, and the implementation of that symbol is a hand-written assembly file per architecture. Both halves are handed to the preprocessor instead: a `generated_files` config header maps `__x86_64__`/`__aarch64__` to the matching `BUILD_TARGET_*` and reaches every TU through `-include`, and a generated `.S` — `.S`, because upstream's `arch/invokeNative_*.s` are lowercase and clang assembles those WITHOUT the preprocessor, so unlike libffi's `.S` files they cannot guard themselves — `#include`s the chosen one as text. Two further notes worth copying: `-std=gnu11` arrives through `cflags` because `c_standard = "c11"` defines `__STRICT_ANSI__`, under which the bare `asm` in `platform_internal.h` is not a keyword; and the POSIX files upstream drops when WASI is off are carried unconditionally rather than `!`-excluded and re-added by the `libc-wasi` feature, because an exclusion glob is global and wins over the feature's own entry for the same file) | | C-source compat where the library IS a kernel ABI | [`compat.libaio`](../pkgs/c/compat.libaio.lua) (libaio 0.3.113 — twelve syscall-wrapper TUs, and the only `xpm` section is `linux`, because there is no port to declare: `struct iocb` is the kernel's and every TU is `syscall(__NR_io_*, …)`. Consumers gate it with `[target.'cfg(linux)'.dependencies]`, the mirror image of compat.wil. Three things it teaches. **One public header out of a source dir**: upstream installs exactly one, `libaio.h`, but the tarball keeps it in `src/` beside the private headers — one of which is named `syscall.h` and would SHADOW glibc's for every consumer TU — so `include_dirs` names a `generated_files` forwarder and nothing else; the package's own sources reach the real header through it while their quote-form `#include "syscall.h"` still resolves next to the including `.c`, so no `-I` into `src/` is needed at all. **A `c_standard` that is a trap**: `-std=c11` sets `__STRICT_ANSI__`, which hides `syscall()` and `sigset_t`, and the public header then fails to parse at `io_pgetevents`; declaring `c_standard = "gnu11"` LOOKS like the fix but mcpp 2026.8.27.2 accepts the string and still emits `-std=c11` (visible in the emitted `compile_commands.json`), so `-D_GNU_SOURCE` in `cflags` is the spelling that takes effect. **Symbol versioning in a static package**: `io_getevents` and `io_cancel` have no ordinary definitions upstream — the functions are `io_getevents_0_4` etc. publishing short names through `.symver … @@LIBAIO_0.4` — which resolves for an executable under both ld.bfd and lld, but not when a consumer builds a `.so` straight out of these objects; that needs upstream's `src/libaio.map`, exactly as upstream's own `libaio.a` does) | +| C++-source compat, Linux-only POSIX API + a consumer-visible header fix | [`compat.libserial`](../pkgs/c/compat.libserial.lua) (LibSerial 1.0.0 — `SerialPort`/`SerialStream`, three TUs from upstream's `LIBSERIAL_SOURCES`, and the only `xpm` section is `linux`: the headers open with `` and every TU reaches ``, so it is gated like compat.libaio. **A header that only compiles by accident gets a shim**: `src/libserial/SerialPortConstants.h` uses `uint8_t` but includes neither `` nor ``, so a consumer TU that opens a libserial header first fails with an undeclared-type error; a `generated_files` shim placed ahead of `*/src` includes `` and `#include_next`s upstream's header (the compat.yaml-cpp / compat.cpptrace pattern). The test proves it by including the libserial header first of all and asserts the rest over a real openpty() pair) | | C++-source compat, one depending on the other | [`compat.abseil`](../pkgs/c/compat.abseil.lua) (151 TUs; a wildcard over `absl/**` trimmed by upstream's test/benchmark naming conventions) · [`compat.protobuf`](../pkgs/c/compat.protobuf.lua) (the libprotobuf runtime, 79 TUs transcribed from upstream's own `src/file_lists.cmake`; declares `compat.abseil` as a dependency because protobuf's public headers include `absl/…`, and its `gzip` feature defines `HAVE_ZLIB` and pulls `compat.zlib`, while `upb` adds protobuf's 64-TU C runtime out of the same tarball. It also exposes **`protoc`** as a `kind = "bin"` target, so a consumer writing `tools = ["protoc"]` gets the compiler built for its own machine out of the same package it links — making a generator/runtime version mismatch inexpressible) · [`compat.re2`](../pkgs/c/compat.re2.lua) (22 TUs, upstream's own `RE2_SOURCES`) · [`compat.redis-plus-plus`](../pkgs/c/compat.redis-plus-plus.lua) (redis++ 1.3.13 — the sync client, 17 TUs + `patterns/redlock.cpp`, depends on `compat.hiredis`; the one header CMake would generate, `hiredis_features.h`, is snapshotted via `generated_files`, and the async/TLS TUs are left out so the base build stays a two-package pair. An `async` feature adds the libuv-backed `AsyncRedis` interface (the 9 async TUs + `compat.libuv`; `event_loop.cpp` runs `uv_run` on a background thread, and `` arrives through compat.hiredis' wrapper headers). Two versions, one on each side of the source-structure watershed, share this ONE source list: 1.3.13 (modern 17-TU layout) and 1.3.3 (pre-`redis_uri.cpp`/`redlock` 15-TU layout) — the union works because 1.3.3's TUs are a strict subset, so exactly two globs match nothing there (a warning, not an error; same trick as compat.catch2)) | · [`compat.sqlitecpp`](../pkgs/c/compat.sqlitecpp.lua) (the RAII C++ wrapper over SQLite. Upstream vendors sqlite3 as a GIT SUBMODULE, so a source tarball simply does not contain it and the library cannot link — the dependency edge on `compat.sqlite3` replaces the submodule, and does it better: two consumers of SQLite in one link now share ONE amalgamation instead of each embedding a private copy with its own compile-time options. Its two CMake knobs are deliberately not set — `SQLITECPP_USE_ASSERT_ON_ERRORS` changes the error model from throwing to aborting, and `SQLITE_ENABLE_COLUMN_METADATA` has to agree with how SQLite ITSELF was built; both are the consumer's call, and the headers already guard them with `#ifdef`) | Multi-component upstream flattened into one lib | [`compat.recastnavigation`](../pkgs/c/compat.recastnavigation.lua) (Recast Navigation 1.6.0 — upstream is five inter-dependent CMake libraries; the two every consumer uses are the base and the other three are `features`, all compiled into one lib. Their dependency edges have to be rebuilt by hand, which is why `debug-utils` carries `implies = { "tilecache" }`: upstream links DetourTileCache unconditionally, and without the implication a consumer asking only for debug drawing fails at link with missing `dtTileCache*` symbols. Upstream's install puts every header flat under `include/recastnavigation/` **and** keeps both that directory and its parent on the interface include path, so both `` and `` are legal against a real install — 26 generated forwarding headers restore the second spelling for a source-tree build. `RECASTNAVIGATION_DT_POLYREF64` and `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` are deliberately NOT features: they change the ABI of types crossing the library boundary, and a feature's `defines` reach only the package's own TUs) | | C transport + the header-only C++ server on top of it | [`compat.usockets`](../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../pkgs/c/compat.uwebsockets.lua) (uSockets picks ONE event loop for all three platforms — libuv, via `compat.libuv` — because the alternative makes `us_loop_t` a different struct per platform for no gain; SSL and QUIC are left out so the base package's only dependency is that loop. The pair's real lesson is that `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` are INTERFACE facts: `libusockets.h` changes the layout of `us_loop_t` under the first and gates its SSL declarations on the second, and uWS is header-only so its templates are instantiated in the CONSUMER's translation unit. An index descriptor's `cflags` reach only the package's own TUs, so every consumer must declare all three — a mismatch does not fail to build, it corrupts. The usockets test therefore writes to loop-attached extension memory from a timer callback and reads it back, which is exactly the assertion a layout disagreement breaks) | diff --git a/docs/zh/descriptor-examples.md b/docs/zh/descriptor-examples.md index 5f976ef8..7eead6f2 100644 --- a/docs/zh/descriptor-examples.md +++ b/docs/zh/descriptor-examples.md @@ -16,6 +16,7 @@ | C 源码 compat(含 `features`) | [`compat.cjson`](../../pkgs/c/compat.cjson.lua) · [`compat.zlib`](../../pkgs/c/compat.zlib.lua) · [`compat.hiredis`](../../pkgs/c/compat.hiredis.lua)(经典 1.2.0 —— 7 个 C TU;tarball 平铺头经 `generated_files` 补 `hiredis/` 前缀薄包装头,消费者可写 `#include `,与上游安装布局一致) · [`compat.sqlite3`](../../pkgs/c/compat.sqlite3.lua)(纯 C 源码、无 feature:单一 `sqlite3.c` amalgamation;3.45.3,部署最广的 3.45.x 线) · [`compat.libuv`](../../pkgs/c/compat.libuv.lua)(libuv 1.48.0 —— 逐 OS 源清单转录自上游 CMakeLists,因为 `src/unix/*.c` 通配会一次编进所有 OS 的后端;linux/macos 显式列 unix 子集,windows 用 `src/win/*.c` glob) · [`compat.xxhash`](../../pkgs/c/compat.xxhash.lua)(单 TU、单头、无 feature —— 值得说的是**没有**编译什么:`xxh_x86dispatch.c` 在运行期选择 AVX2/AVX512 路径,需要 per-file `-mavx2` 并要求每个调用点定义 `XXH_X86DISPATCH`,故本包只出无需任何 flag 的 SSE2 基线。也没有选 header-only 的 `XXH_INLINE_ALL` 模式:它会在每个做哈希的 TU 里重新展开整份实现,那只有在「恰好只有一个这样的 TU」时才划算 —— 而这件事包本身无从知道) · [`compat.quickjs-ng`](../../pkgs/c/compat.quickjs-ng.lua)(QuickJS-ng 0.17.0 —— 只编上游的四个引擎 TU。可选标准库 `quickjs-libc.c`(给脚本文件、环境变量、定时器与子进程能力)不编,嵌入方拿到的引擎碰不到宿主,除非自己提供。tarball 把 `quickjs.h` 和 `list.h`、`cutils.h` 这类名字很通用的私有头放在一起,所以 include 路径上只放一个 `generated_files` 转发头,与 compat.libaio 同法) | | C 源码 compat(源码按**架构**选择) | [`compat.wamr`](../../pkgs/c/compat.wamr.lua)(WAMR 2.4.5 —— 描述符能按 OS 分 `sources`/`cflags`,但**没有按架构分**的钩子,`archs` 是包级元数据而非选择器。而 WAMR 两处都要按架构走:不定义 `BUILD_TARGET_X86_64`/`BUILD_TARGET_AARCH64` 之一,`wasm_runtime_common.c` 会把整段 invoke-native 编没,链接期报 `invokeNative` 未定义;而该符号的实现本身就是每架构一份手写汇编。两处都改交给预处理器:`generated_files` 生成的配置头把 `__x86_64__`/`__aarch64__` 映射到对应 `BUILD_TARGET_*`,经 `-include` 到达每个 TU;另一份生成的 `.S` 用 `#include` 把选中的汇编原样拉进来 —— 之所以必须是 `.S`,是因为上游的 `arch/invokeNative_*.s` 是小写后缀,clang 汇编它们**不过预处理器**,因此不像 libffi 的 `.S` 那样能自己加架构守卫。另有两点值得抄:`-std=gnu11` 经 `cflags` 追加,因为 `c_standard = "c11"` 会定义 `__STRICT_ANSI__`,此时 `platform_internal.h` 里裸写的 `asm` 不是关键字;以及上游在关掉 WASI 时会剔除的那几个 POSIX 文件,这里**无条件常含**,而不是 base 用 `!` 排除、再由 `libc-wasi` feature 加回来 —— 排除 glob 是全局的,会盖过 feature 里同名文件的条目) | | C 源码 compat(库本身就是内核 ABI) | [`compat.libaio`](../../pkgs/c/compat.libaio.lua)(libaio 0.3.113 —— 12 个系统调用封装 TU,`xpm` 只有 `linux` 一段,因为根本不存在「移植」可声明:`struct iocb` 就是内核的结构体,每个 TU 都是 `syscall(__NR_io_*, …)`。消费者用 `[target.'cfg(linux)'.dependencies]` 门控,与 compat.wil 互为镜像。它给出三条经验。**把唯一的公开头从源码目录里择出来**:上游只安装 `libaio.h` 一个头,但 tarball 把它放在 `src/` 里、与私有头并列 —— 其中一个恰好叫 `syscall.h`,一旦上了 include 路径就会**遮蔽** glibc 的同名头。故 `include_dirs` 只指向一个 `generated_files` 转发头;包自身的源码经它拿到真头文件,而它们引号形式的 `#include "syscall.h"` 仍按「包含者所在目录优先」解析,于是整包**不需要任何指向 `src/` 的 `-I`**。**一个会骗人的 `c_standard`**:`-std=c11` 会定义 `__STRICT_ANSI__`,从而藏掉 `syscall()` 与 `sigset_t`,连公开头都会在 `io_pgetevents` 处解析失败;写 `c_standard = "gnu11"` **看起来**是解法,但 mcpp 2026.8.27.2 接受这个字符串却依然发 `-std=c11`(在产出的 `compile_commands.json` 里可见),真正生效的写法是 `cflags` 里的 `-D_GNU_SOURCE`。**静态包里的符号版本**:`io_getevents` / `io_cancel` 在上游并没有普通定义 —— 函数名是 `io_getevents_0_4` 之类,短名经 `.symver … @@LIBAIO_0.4` 发布 —— 链接**可执行文件**时 ld.bfd 与 lld 都能解析,但消费者若直接拿这些对象去构建 `.so` 就不行,那需要上游的 `src/libaio.map`,与上游自己的 `libaio.a` 完全同理) | +| C++ 源码 compat,仅 Linux 的 POSIX 接口 + 一处消费者可见的头文件修复 | [`compat.libserial`](../../pkgs/c/compat.libserial.lua)(LibSerial 1.0.0 —— `SerialPort`/`SerialStream`,上游 `LIBSERIAL_SOURCES` 的三个 TU,`xpm` 只有 `linux` 一段:各头文件以 `` 开头,每个 TU 都触及 ``,故与 compat.libaio 同样门控。**一个「只是碰巧能编」的头被 shim 修复**:`src/libserial/SerialPortConstants.h` 用了 `uint8_t`,却既没含 `` 也没含 ``,消费者 TU 若先打开 libserial 头就会报未声明类型;`generated_files` 的 shim 排在 `*/src` 之前,先含 `` 再 `#include_next` 上游真头(compat.yaml-cpp / compat.cpptrace 的做法)。测试把 libserial 头放在**第一行**来证明 shim 生效,其余断言跑在真实的 openpty() 对上) | | C++ 源码 compat(彼此依赖) | [`compat.abseil`](../../pkgs/c/compat.abseil.lua)(151 TU;对 `absl/**` 取通配后,按上游自身的 test/benchmark 命名约定裁剪) · [`compat.protobuf`](../../pkgs/c/compat.protobuf.lua)(libprotobuf 运行时,79 TU 逐条转录自上游 `src/file_lists.cmake`;因 protobuf 公开头文件 include 了 `absl/…`,故显式依赖 `compat.abseil`;`gzip` feature 定义 `HAVE_ZLIB` 并拉入 `compat.zlib`,`upb` feature 则从同一个 tarball 里再编出 protobuf 的 64 TU C 运行时;还以 `kind = "bin"` target 暴露 **`protoc`**,消费者写 `tools = ["protoc"]` 即可从「自己链接的那个包」拿到为本机构建的编译器,使生成器与运行时的版本错配无法表达) · [`compat.re2`](../../pkgs/c/compat.re2.lua)(22 TU,取自上游自身的 `RE2_SOURCES`) · [`compat.redis-plus-plus`](../../pkgs/c/compat.redis-plus-plus.lua)(redis++ 1.3.13 —— 同步客户端,17 TU + `patterns/redlock.cpp`,依赖 `compat.hiredis`;CMake 唯一会生成的头 `hiredis_features.h` 用 `generated_files` 快照,async/TLS TU 不收,基座保持两包成对。`async` feature 补齐 libuv 版 `AsyncRedis` 接口(9 个 async TU + `compat.libuv`;`event_loop.cpp` 在后台线程跑 `uv_run`,`` 经 compat.hiredis 的包装头到达)。两个版本分处源码结构分水岭两侧,共享同一份源列表:1.3.13(现代 17-TU 布局)与 1.3.3(缺 `redis_uri.cpp`/`redlock` 的 15-TU 旧布局)—— 并集之所以成立,是因为 1.3.3 的 TU 是 1.3.13 的严格子集,恰好两个 glob 在 1.3.3 上零命中(仅警告,非错误;与 compat.catch2 同款手法)) · [`compat.sqlitecpp`](../../pkgs/c/compat.sqlitecpp.lua)(SQLite 的 RAII C++ 封装。上游用 **git submodule** 引 sqlite3,源码 tarball 里根本没有它,库因此无法链接 —— 依赖边指向 `compat.sqlite3` 替代了那个 submodule,而且更好:同一次链接里的两个 SQLite 消费者从此共享**一份** amalgamation,而不是各自内嵌一份带各自编译选项的副本。它的两个 CMake 开关有意不设 —— `SQLITECPP_USE_ASSERT_ON_ERRORS` 把错误模型从抛异常改成中止进程,`SQLITE_ENABLE_COLUMN_METADATA` 必须与 SQLite **自身**的构建一致;两者都该由消费者决定,而头文件本来就用 `#ifdef` 守着) | | 多组件上游拍平进单个库 | [`compat.recastnavigation`](../../pkgs/c/compat.recastnavigation.lua)(Recast Navigation 1.6.0 —— 上游是五个互相依赖的 CMake target;这里把「人人都要用」的两个作为核心,其余三个做 `features`,全部编进同一个 lib。依赖边必须手工重建,这正是 `debug-utils` 带 `implies = { "tilecache" }` 的原因:上游无条件链接 DetourTileCache,没有这条 implication,只开调试绘制的消费者会在**链接期**撞上缺失的 `dtTileCache*` 符号。上游安装把头文件拍平进 `include/recastnavigation/`,同时把该目录**及其父目录**都放进 interface include path,于是对着真实安装 `#include ` 与 `#include ` 都合法 —— 26 个生成的转发头把第二种拼写还给源码树构建。`RECASTNAVIGATION_DT_POLYREF64` 与 `RECASTNAVIGATION_DT_VIRTUAL_QUERYFILTER` 刻意**不**做成 feature:它们改变跨库边界类型的 ABI,而 feature 的 `defines` 只作用于本包自己的 TU) | | C 传输层 + 其上的 header-only C++ 服务端 | [`compat.usockets`](../../pkgs/c/compat.usockets.lua) · [`compat.uwebsockets`](../../pkgs/c/compat.uwebsockets.lua)(uSockets 三平台统一选 libuv 一个事件循环(经 `compat.libuv`),因为按平台各选后端只会让 `us_loop_t` 每个平台一个形状而毫无收益;SSL 与 QUIC 不收,于是基础包的唯一依赖就是那个循环。这一对真正的教训是 `LIBUS_USE_LIBUV` / `LIBUS_NO_SSL` / `UWS_NO_ZLIB` 是**接口级**事实:`libusockets.h` 会因前者改变 `us_loop_t` 的布局、因后者门控 SSL 声明,而 uWS 是 header-only —— 它的模板是在**消费者**的 TU 里实例化的。描述符的 `cflags` 只作用于包自身的 TU,所以每个消费者都必须自己声明这三个;不一致不会构建失败,而是内存损坏。usockets 的测试因此从定时器回调里写loop 附属的扩展内存再读回来 —— 布局一旦不一致,正是这条断言会断) | diff --git a/mcpp.toml b/mcpp.toml index 9c63f694..d6873549 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -104,6 +104,7 @@ members = [ "tests/examples/glad", "tests/examples/glm", "tests/examples/libaio", + "tests/examples/libserial", "tests/examples/libassert", "tests/examples/libcoro", "tests/examples/libdrm", diff --git a/pkgs/c/compat.libserial.lua b/pkgs/c/compat.libserial.lua new file mode 100644 index 00000000..48da89f5 --- /dev/null +++ b/pkgs/c/compat.libserial.lua @@ -0,0 +1,101 @@ +-- compat.libserial — LibSerial 1.0.0, an object-oriented C++ wrapper over the +-- POSIX serial-port interface: `SerialPort` (a descriptor-style class) and +-- `SerialStream` (an iostream on top of a `SerialStreamBuf`). +-- +-- Shape A (C++-source compat), same as compat.websocket: upstream's three +-- translation units from src/CMakeLists.txt (`LIBSERIAL_SOURCES`) compiled into +-- one lib target, and the public headers exposed through `include_dirs` so a +-- consumer writes `#include ` — upstream's own install +-- layout (`install(DIRECTORY libserial DESTINATION include)`). +-- +-- LINUX ONLY, and there is no macOS/Windows section to write. Upstream targets +-- POSIX: SerialPortConstants.h includes from the top, and every +-- implementation TU reaches , and . +-- The serial-port ioctls it drives (TIOCEXCL, TIOCMGET, FIONREAD, TIOCSSERIAL) +-- and `struct serial_struct` are Linux UAPI. This is why the test member gates +-- the dependency with `[target.'cfg(linux)'.dependencies]` and compiles to a +-- no-op main() elsewhere — the mirror image of compat.wil and the same shape as +-- compat.libaio. (Single-platform `xpm` is why platform-version-parity stays +-- quiet: it only compares platforms that both carry versions.) +-- +-- ONE CONSUMER-VISIBLE DEFECT IS FIXED HERE. src/libserial/SerialPortConstants.h +-- declares `using DataBuffer = std::vector;` but includes neither +-- nor . On gcc 16.1.0 / libstdc++ this compiles only by +-- accident of transitive includes — a bare consumer that opens the header first +-- gets "'uint8_t' was not declared in this scope" (reproduced against the +-- tarball). A `generated_files` shim named after the same path, found first +-- because mcpp_generated precedes `*/src` in include_dirs, includes +-- and then `#include_next`s upstream's real header (the compat.yaml-cpp / +-- compat.cpptrace / compat.catch2 pattern). The test's first line is that +-- include with nothing before it, so the shim is compile-time asserted. +-- +-- NO FEATURE. The three TUs are the whole library; upstream's optional parts +-- are the GoogleTest suite (test/, its own main()), the SIP Python bindings +-- (sip/) and the Doxygen docs (doxygen.conf.in) — none is an optional +-- compilable component of the lib. So `features` is not declared at all. +-- +-- VERSION. 1.0.0 is upstream's latest release (github tag `v1.0.0`, the only +-- v1 tag; earlier tags are 0.5/0.6 release candidates and are not built here). +-- No CN mirror: there is no mcpp-res write access in this environment, so this +-- uses the documented plain-string fallback (docs/cn-mirror.md; precedent +-- compat.libuv / compat.hiredis / compat.websocket). Flip each `url` to a +-- { GLOBAL, CN } table once a gitcode release exists — the sha256 is unchanged. +package = { + spec = "1", + namespace = "compat", + name = "libserial", + description = "LibSerial — object-oriented C++ serial-port library (SerialPort / SerialStream)", + licenses = {"BSD-3-Clause"}, + repo = "https://github.com/crayzeewulf/libserial", + type = "package", + + xpm = { + linux = { + ["1.0.0"] = { + url = "https://github.com/crayzeewulf/libserial/archive/refs/tags/v1.0.0.tar.gz", + sha256 = "063142d6bfe08898316e9a6055f2ddeedef56de06f7cfc8dcdfecc6efabf4bdd", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + + -- ORDER MATTERS: mcpp_generated first, so the shim for + -- libserial/SerialPortConstants.h is found before upstream's copy, + -- which it then reaches with #include_next. `*/src` is upstream's + -- install root and carries only the libserial/ directory, so there is + -- nothing generic here that could shadow a system header. + include_dirs = { "mcpp_generated", "*/src" }, + + generated_files = { + -- See the header note. Without this, a consumer TU that opens a + -- libserial header before any libstdc++ header that transitively + -- pulls fails with "'uint8_t' was not declared". + ["mcpp_generated/libserial/SerialPortConstants.h"] = [==[ +// compat.libserial shim: upstream's SerialPortConstants.h uses uint8_t but +// includes neither nor ; it compiles only through a +// transitive include of whatever stdlib header happens to come first. This +// shim guarantees and then resumes the search at upstream's header. +#ifndef MCPP_COMPAT_LIBSERIAL_CSTDINT_SHIM +#define MCPP_COMPAT_LIBSERIAL_CSTDINT_SHIM +#include +#include_next +#endif +]==], + }, + + -- Upstream src/CMakeLists.txt `LIBSERIAL_SOURCES`, verbatim. + sources = { + "*/src/SerialPort.cpp", + "*/src/SerialStream.cpp", + "*/src/SerialStreamBuf.cpp", + }, + + -- `libserial.a` / `-lserial`, the spelling upstream's libserial.pc + -- publishes. + targets = { ["serial"] = { kind = "lib" } }, + deps = { }, + }, +} diff --git a/tests/examples/libserial/mcpp.toml b/tests/examples/libserial/mcpp.toml new file mode 100644 index 00000000..5f40c911 --- /dev/null +++ b/tests/examples/libserial/mcpp.toml @@ -0,0 +1,19 @@ +# libserial test project: drive a real PTY through the SerialPort API. +# +# LibSerial is not a portable library with a Linux backend -- it IS a POSIX +# serial-port wrapper whose headers begin with and whose TUs +# include -- so the descriptor has a `linux` section and +# nothing else, the dependency is gated, and the test compiles to a no-op +# main() elsewhere. Same shape as tests/examples/libaio. +# +# Nothing is mocked: a pseudo-terminal pair (openpty) stands in for the UART, +# because on a build machine /dev/ttyUSB0 does not exist, and a link-only test +# would pass even if SerialPort mis-set termios flags. Writing to the master +# exercises SerialPort's read path; writing through SerialPort exercises the +# master's read path. +[package] +name = "libserial-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +libserial = "1.0.0" diff --git a/tests/examples/libserial/tests/serial.cpp b/tests/examples/libserial/tests/serial.cpp new file mode 100644 index 00000000..008bc383 --- /dev/null +++ b/tests/examples/libserial/tests/serial.cpp @@ -0,0 +1,157 @@ +// Behavioral test: a real pseudo-terminal pair driven through SerialPort. +// +// Why a PTY and not /dev/ttyUSB0: a build machine has no serial hardware, and +// a link-only test would pass even if SerialPort got termios wrong. openpty() +// gives a master/slave pair the same way `socat` would; SerialPort opens the +// slave, and writing on the master exercises its read path while writing +// through SerialPort exercises the master's. +// +// The first include is the one that matters for the descriptor: upstream's +// SerialPortConstants.h uses uint8_t but does not include , so a bare +// `#include ` with nothing before it fails unless the +// package's generated shim supplies it. Deliberately open on the libserial +// header first, with no stdlib header above it, so that shim is compile-time +// asserted. +#ifdef __linux__ + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +using namespace LibSerial; + +namespace { + +// master>=0 on success; the slave fd is closed again because SerialPort opens +// the path itself. +int open_pty(char (&slave_path)[128]) { + int master = -1, slave = -1; + if (::openpty(&master, &slave, nullptr, nullptr, nullptr) != 0) return -1; + if (::ptsname_r(master, slave_path, sizeof(slave_path)) != 0) { + ::close(master); + ::close(slave); + return -1; + } + ::close(slave); // SerialPort::Open() reopens the slave by path. + return master; +} + +} // namespace + +int main() { + char slave_path[128] = {}; + int master = open_pty(slave_path); + assert(master >= 0 && "openpty/ptsname_r failed"); + + SerialPort port; + + // -- NotOpen contract: a closed port refuses I/O instead of touching fd -1. + bool threw_not_open = false; + try { port.Write(std::string("x")); } + catch (const NotOpen&) { threw_not_open = true; } + assert(threw_not_open && "Write on a closed port did not throw NotOpen"); + assert(!port.IsOpen()); + + // -- Open / IsOpen, and AlreadyOpen on a second Open. + port.Open(slave_path); + assert(port.IsOpen() && "port did not open"); + + bool threw_already_open = false; + try { port.Open(slave_path); } + catch (const AlreadyOpen&) { threw_already_open = true; } + assert(threw_already_open && "second Open did not throw AlreadyOpen"); + + // -- Open defaults: BAUD_115200, 8 data bits, no parity, 1 stop bit. + assert(port.GetBaudRate() == BaudRate::BAUD_115200); + assert(port.GetCharacterSize() == CharacterSize::CHAR_SIZE_8); + assert(port.GetParity() == Parity::PARITY_NONE); + assert(port.GetStopBits() == StopBits::STOP_BITS_1); + + // -- Set/Get round trips for the attributes a PTY can actually carry. + // CS7/parity-even are NOT asserted: a pty's line discipline does not store + // those bits, so GetCharacterSize() stays CS8 and GetParity() stays NONE + // no matter what is written (measured against the tarball). That is a + // property of the PTY, not of LibSerial, so the test pins only what is + // observable here. + port.SetBaudRate(BaudRate::BAUD_9600); + assert(port.GetBaudRate() == BaudRate::BAUD_9600); + port.SetStopBits(StopBits::STOP_BITS_2); + assert(port.GetStopBits() == StopBits::STOP_BITS_2); + port.SetFlowControl(FlowControl::FLOW_CONTROL_NONE); + assert(port.GetFlowControl() == FlowControl::FLOW_CONTROL_NONE); + + // -- TX: SerialPort -> master. SerialPort opens the slave O_NONBLOCK; a pty + // accepts the write immediately, so this returns without a reader race. + port.Write(std::string("hello\n")); + char rx[16] = {}; + ssize_t got = ::read(master, rx, sizeof(rx)); + assert(got == 6 && std::strncmp(rx, "hello\n", 6) == 0 && "TX bytes mismatched"); + + // -- RX: master -> SerialPort. Write on the master, then let the slave's + // input queue fill before asking (a 200 ms settle removes the race + // between write() returning and the byte landing in the slave buffer). + assert(::write(master, "ping\n", 5) == 5); + ::usleep(200 * 1000); + assert(port.IsDataAvailable() && "IsDataAvailable saw no queued byte"); + + char first = 0; + port.ReadByte(first, 2000); + assert(first == 'p' && "ReadByte returned the wrong byte"); + + std::string line; + port.ReadLine(line, '\n', 2000); + assert(line == "ing\n" && "ReadLine did not assemble the rest of the line"); + + // -- SerialStream: the iostream interface, on its OWN pty pair. Reusing + // the pair above fails: after SerialPort::Close() restored the slave's + // saved termios, a second Open() of the same pty throws OpenFailed + // ("Bad file descriptor") -- upstream's Open path, not this descriptor's. + port.Close(); + assert(!port.IsOpen() && "port still open after Close"); + + char stream_path[128] = {}; + int stream_master = open_pty(stream_path); + assert(stream_master >= 0 && "second openpty/ptsname_r failed"); + + SerialStream stream; + stream.Open(stream_path); + assert(stream.IsOpen() && "SerialStream did not open"); + stream.SetBaudRate(BaudRate::BAUD_57600); + assert(stream.GetBaudRate() == BaudRate::BAUD_57600); + + // SerialStream -> master, i.e. the iostream write path (xsputn/overflow). + // The READ path is deliberately not exercised: upstream's + // SerialStreamBuf::showmanyc() sets the putback flag without storing the + // character it read, so rdbuf()->in_avail() followed by a stream + // extraction yields a garbage first byte, and `stream >> x` on a quiet + // pty blocks (measured against the tarball). That is an upstream defect in + // SerialStreamBuf, not something this descriptor can fix without patching + // source, so the test pins the half that works. + stream << "xyz" << std::flush; + char tx[16] = {}; + ssize_t got2 = ::read(stream_master, tx, sizeof(tx)); + assert(got2 == 3 && std::strncmp(tx, "xyz", 3) == 0 && "SerialStream TX mismatched"); + + stream.Close(); + assert(!stream.IsOpen() && "stream still open after Close"); + ::close(stream_master); + + ::close(master); + return 0; +} + +#else + +// LibSerial is POSIX/Linux: and . Nothing to +// assert on a platform the descriptor does not declare. +int main() { return 0; } + +#endif