Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .agents/docs/2026-10-03-add-libserial-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 收录 LibSerial 1.0.0(compat.libserial)

日期:2026-10-03 · 分支:`feat/add-libserial` · 状态:本地验证通过

## 1. 来源与形态判定

LibSerial 属于来源 (a):第三方上游库,上游不提供 mcpp 支持。

- 上游:<https://github.com/crayzeewulf/libserial>。
- 最新版本:`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 <libserial/SerialPort.h>`(上游 `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 <termios.h>`,
实现 TU 一律触及 `<linux/serial.h>`、`<sys/ioctl.h>`、`<unistd.h>`,驱动的 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<uint8_t>;`,
却既没 `#include <cstdint>` 也没 `#include <stdint.h>`。在 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 <cstdint>` 再 `#include_next <libserial/SerialPortConstants.h>`;`include_dirs`
把 `mcpp_generated` 排在 `*/src` 之前,shim 命中在真头之前。包自身的三个 TU 与消费者
都经过它。

测试把 `#include <libserial/SerialPort.h>` 放在**整个文件的第一行**(上面不放任何
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. 注意事项

- 上游公开头对 `<cstdint>` 的隐式依赖是本包必须带 shim 的唯一原因;若未来上游补上该
包含,shim 仍是无害的(重复包含 <cstdint> 与 include_next 都安全)。
- `include_dirs` 用 `*/src`(上游安装根),该目录下只有 `libserial/` 一个子目录,不会
遮蔽任何系统头。
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

### Added

- **`compat.libserial` 1.0.0。** LibSerial 的 `SerialPort`/`SerialStream` 以 compat 形态收录:上游 CMake 的 `LIBSERIAL_SOURCES` 三个 TU 编成一个 lib,公开头经 `include_dirs` 暴露。仅声明 `linux`(公开头以 `<termios.h>` 开头、实现触及 `<linux/serial.h>`,消费者用 `[target.'cfg(linux)'.dependencies]` 门控)。上游 `SerialPortConstants.h` 用了 `uint8_t` 却未含 `<cstdint>`,仅靠传递包含碰巧能编;本包用 `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 资源
Expand Down
1 change: 1 addition & 0 deletions docs/descriptor-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hiredis/hiredis.h>` 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 `<termios.h>` and every TU reaches `<linux/serial.h>`, 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 `<cstdint>` nor `<stdint.h>`, 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 `<cstdint>` 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 `<hiredis/adapters/libuv.h>` 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 `<Recast.h>` and `<recastnavigation/Recast.h>` 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) |
Expand Down
Loading