Skip to content
Merged
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
154 changes: 154 additions & 0 deletions .agents/docs/2026-07-15-add-spdlog-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# 收录 spdlog 1.17.0 —— 形态判定、双模态与验证结论

日期:2026-07-15。对应描述符 `pkgs/c/compat.spdlog.lua`、示例 `tests/examples/spdlog/`。
CI 版本 mcpp 0.0.91,index floor `min_mcpp = 0.0.87`。

## 1. 来源与形态

- 来源:(a) 第三方上游库 —— spdlog(https://github.com/gabime/spdlog)上游不提供
mcpp 支持,由本仓以 `compat` 形态适配。
- 最新 tag:`v1.17.0`(`git ls-remote --tags` 排序确认)。裸版本 `1.17.0`,
下载 URL 保留上游 `.../v1.17.0.tar.gz` 拼写。
- License:MIT。
- sha256:`d8862955c6d74e5846b3f580b1605d2428b11d97a410d86e2fb13e857cd3a744`
(下载后重复计算两次,稳定)。
- 源码布局:`include/spdlog/**`(全部头文件,含 `-inl.h` 实现单元与
`fmt/bundled/` 内联版 {fmt})、`src/*.cpp`(7 个预编译库 TU)。
- 形态判定:**header-only + source-gated feature**,与 `compat.eigen` 同类。
spdlog 是 DUAL-MODAL:
- 默认(不定义宏)走 header-only:`common.h` 在未定义 `SPDLOG_COMPILED_LIB`
时打开 `SPDLOG_HEADER_ONLY`,头文件末尾 `#include "*-inl.h"` 把实现内联。
bundled fmt 以 `FMT_HEADER_ONLY` 使用,**整包自包含,无需外部 fmt 依赖**。
- 预编译模态(定义 `SPDLOG_COMPILED_LIB`)改为编译 `src/*.cpp`;每个 src TU
在缺少该宏时 `#error`。该宏是 **interface define**:必须同时到达库源与
消费者头(否则消费者头走 inline,与 .a 符号重复/冲突)。

## 2. 描述符设计

header-only 骨架(`include_dirs = {"*/include"}` + anchor TU 提供可构建 lib
target),外加一个声明式 `compiled` feature:

```lua
features = {
["compiled"] = {
sources = { "*/src/*.cpp" },
defines = { "SPDLOG_COMPILED_LIB" },
},
}
```

## 3. 关键实证:feature sources 在 `mcpp test` 下不编译(mcpp ≤0.0.93 的 bug,0.0.94 已修)

> **修订(2026-07-15)**:本节初版结论「mcpp 0.0.91 引擎不编译 feature 门控的
> sources」**是误判**,已由 mcpp#218 查清并修复。误判的成因值得记下来。

初版三重佐证如下:

| 实验 | feature | 现象 |
|---|---|---|
| spdlog | `compiled` | `src/*.cpp` 未编译 → 大量 `undefined reference` |
| compat.cjson | `utils` | `cJSON_Utils.c` 未编译 → `undefined reference to cJSONUtils_*` |
| compat.eigen | `eigen_blas` | `blas/*.cpp` 未编译 → `undefined reference to dgemm_` |

三例现象属实,但**三次都只走了 `mcpp test` 这一条路径**(本仓 CI 是 workspace/test
模型),于是把一个**只影响 test 模式**的 bug 误读成了「引擎不支持 feature sources」。

**真因**(`mcpp/src/build/prepare.cppm`):feature 源集解析(drop + add)**整段**被门在
`!includeDevDeps`,而 `mcpp test` 走 `includeDevDeps = true` → 激活 feature 的 sources
从不被加回构建图。叠加 xpkg 侧 `features.X.sources` 只落进 `featureSources`、**从不进
base `sources`** —— 于是「只在 features 下声明」的包:

| 路径 | 结果 |
|---|---|
| `mcpp build` | ✅ **一直是好的**,7 个 src TU 全进 build.ninja,二进制真跑 |
| `mcpp test` | ❌ `undefined reference` |

那段门的注释假设「descriptor 会把 `gtest_main.cc` 同时留在 base sources 里,故 test
模式不受影响」——**只对 gtest 成立**,是未被察觉的隐式耦合。`compat.eigen` 里
"linking feature-built dependency objects into test binaries is a follow-up" 的定性
同样是错的:**不是链接问题,是源集解析问题**。

**修复**:mcpp 0.0.94(#218)—— drop 仍只在 build 模式做(test 模式需保留完整源面供
dev-dep 轨 per-test main 检测剪枝),add 改为两模式都做 + 去重。

**采用方案**:描述符的 `compiled` feature 声明**本来就是对的,一行未改**;只把注释从
「引擎不支持」改为如实的「`mcpp test` 下需 ≥0.0.94」。CI pin 提到 0.0.94 后,
**两个模态都进 CI 断言**(`tests/examples/spdlog/` header-only +
`tests/examples/spdlog-compiled/` compiled)。

**教训**:诊断 feature 相关问题**必须 `build` / `test` 两条路径对比**,只测一条会把
路径 bug 误判成引擎能力缺失。

## 4. feature 评估:为何不为 tweakme 开关新增 feature

spdlog 的 `tweakme.h` 有大量编译期开关(`SPDLOG_USE_STD_FORMAT`、
`SPDLOG_NO_SOURCE_LOC`、`SPDLOG_CLOCK_COARSE`、`SPDLOG_ACTIVE_LEVEL=...` 等)。
经确认(mcpp `05-mcpp-toml.md`),消费者可在**自己的** `mcpp.toml` 直接注入,
无需包侧声明 feature:

```toml
[targets.my-app]
defines = ["SPDLOG_USE_STD_FORMAT"] # per-target,-D 随本 TU 编译
# 或 [build] cxxflags = ["-DSPDLOG_NO_SOURCE_LOC"] # 整工程
```

作用域陷阱(官方明示):`defines`/`cxxflags` **只作用于该 target 自己的 entry,
不传播到共享/依赖代码**。对 spdlog:

- header-only 模态下 spdlog 全在头文件里,随消费者 TU 编译,消费者 `-D` 天然覆盖
→ 所有 tweakme 开关消费者自助即可,做成 feature 是冗余(且不如消费侧灵活,
消费侧还能带值,如 `SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO`)。
- 唯一"消费者 `-D` 够不着、必须包侧统一注入"的是 `SPDLOG_COMPILED_LIB`
(要同时影响库源与消费者)——已由 `compiled` feature 承载(受 §3 限制)。

因此**不新增 tweakme feature**,避免过度设计。

## 5. CN 镜像:纯字符串回退

本环境无 gitcode token(`~/.config/gitcode-tool/config.json` 不存在,
`GITCODE_TOKEN` 未设),无法在 `mcpp-res` 上传资产。探查发现 `mcpp-res/spdlog`
仓库页面已存在(200)但为空壳(release 资产 403)。

按 `docs/cn-mirror.md` 回退方案:三平台 `url` 采用**纯字符串**(仅上游 GitHub
release),lint(`check_mirror_urls.lua`)对纯字符串 url 不施加镜像约束,
`mirror-cn-reachable` 也不会抽取到需 curl 的 CN url。CN 用户回退至上游源。
先例:`pkgs/t/tensorvia-cpu.lua`。后续获得权限或维护者补充镜像后,可将各
`url` 改写为 `{ GLOBAL, CN }` 表(sha256 不变)。

lint 只允许 `gitcode.com/mcpp-res/` 下的 CN url(信任边界 + 字节一致),任何
第三方域名在表形式下都过不了 lint,故无"其他可用 CN 镜像"。

## 6. 验证结论(mcpp 0.0.94,GLOBAL)

**两个模态都已 CI 断言**:

- `mcpp test -p spdlog` → header-only 默认模态,`test result ok. 1 passed`。
- `mcpp test -p spdlog-compiled` → compiled 模态,`test result ok. 1 passed`。
`tests/examples/spdlog-compiled/tests/compiled_test.cpp` 静态断言
`SPDLOG_COMPILED_LIB` 已定义且 `SPDLOG_HEADER_ONLY` 未定义(退化回 inline 路径
会是**编译错误**而非静默通过),再跑与 header-only 同款的行为断言 —— 这些调用
解析到 `default_logger_raw` / `log_msg` ctor / `logger::log_it_` / bundled fmt
`vformat` 等**非 inline 符号**,正是 feature sources 没编译时链不上的那批。

以下为初版(0.0.91)记录,保留作历史:

- `mcpp xpkg parse pkgs/c/compat.spdlog.lua` → `parse OK`(strict floor/grammar)。
- workspace 成员 `mcpp test -p spdlog` → `test result ok. 1 passed`。
示例 `tests/examples/spdlog/tests/log_test.cpp` 用 ostream sink 捕获日志,
断言 `logger.info("hello {}={}", "answer", 42)` 与 `{:#x}` 的格式化输出
(走 bundled fmt 头内联),`return ok ? 0 : 1`。
- 负向语义:header-only 默认构建 `build.ninja` 仅 `spdlog_anchor.o`
(证明 src 未被默认编入);compiled 请求时消费者带 `-DSPDLOG_COMPILED_LIB`
(证明 define 门控生效)。
- 本地 lint(等价 CI lint job)全量 `ALL LINT PASS`;spdlog 镜像 lint 单独 OK。

## 7. 落点与注意事项

- 描述符落点 `pkgs/c/compat.spdlog.lua`——目录取**完整包名首字母**
(`compat.spdlog` → `c`,非短名 `s`)。初次误置 `pkgs/s/` 导致
`not found in local index`,移至 `pkgs/c/` 后解决。
- 示例已登记进根 `mcpp.toml` 的 `[workspace].members`(否则
`mcpp test --workspace` 不会跑到)。
- CI 已由早期 `detect`+`run_example.sh` 重构为 workspace 模式
(`mcpp test --workspace`),示例采用 `tests/*.cpp` + 断言布局,而非
`src/main.cpp`。
11 changes: 6 additions & 5 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ on:
workflow_dispatch:

env:
# Bumped to 0.0.91: carries standard = "c++fly" (experimental playground
# mode) in the resolver grammar — required so descriptors declaring it get
# the lint WARN below instead of a hard grammar-parse rejection from an
# older pinned mcpp (admission policy: warn, don't reject).
MCPP_VERSION: "0.0.91"
# Bumped to 0.0.94: fixes feature-gated `sources` never being compiled under
# `mcpp test` (mcpp#218) — required by the tests/examples/spdlog-compiled
# member, which links against spdlog's feature-compiled src/*.cpp. Also
# carries standard = "c++fly" in the resolver grammar (0.0.91), so c++fly
# descriptors get the lint WARN below, not a hard grammar-parse rejection.
MCPP_VERSION: "0.0.94"

jobs:
lint:
Expand Down
2 changes: 2 additions & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ members = [
"tests/examples/nlohmann.json",
"tests/examples/openblas",
"tests/examples/opencv",
"tests/examples/spdlog",
"tests/examples/spdlog-compiled",
]
103 changes: 103 additions & 0 deletions pkgs/c/compat.spdlog.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
-- Form B inline descriptor for spdlog — a very fast, header-only / compiled C++
-- logging library. spdlog is DUAL-MODAL and this recipe exposes both modes off
-- one descriptor:
--
-- * DEFAULT (header-only). No feature requested. spdlog's headers are
-- self-contained: including <spdlog/spdlog.h> pulls the -inl.h
-- implementation inline (common.h flips on SPDLOG_HEADER_ONLY when
-- SPDLOG_COMPILED_LIB is NOT defined). Nothing under src/ compiles; a tiny
-- anchor TU gives mcpp a buildable `lib` target (same shape as
-- compat.eigen / compat.opengl). The bundled {fmt} (include/spdlog/fmt/
-- bundled/) is used in FMT_HEADER_ONLY mode, so this package is
-- self-contained and needs NO external fmt dependency.
--
-- * COMPILED (`features = ["compiled"]`). Turns spdlog into a precompiled
-- library: (1) compiles the seven src/*.cpp translation units into the lib,
-- and (2) contributes the SPDLOG_COMPILED_LIB define. That define is an
-- INTERFACE define — it must reach BOTH spdlog's own sources (each src/*.cpp
-- #errors out without it) AND every consumer TU that includes a spdlog
-- header (so the consumer's headers switch to the extern-template /
-- non-inline path and link against the compiled objects instead of
-- re-emitting the implementation inline). bundled_fmtlib_format.cpp compiles
-- the bundled fmt implementation into the lib, so compiled mode is
-- self-contained too — still NO external fmt dependency.
--
-- REQUIRES mcpp >= 0.0.94 under `mcpp test`. mcpp 0.0.93 and older gated the
-- whole feature-source resolution on build mode, so an active feature's
-- `sources` were never compiled under `mcpp test` → link-time `undefined
-- reference` (`mcpp build` was always fine). Fixed in mcpp 0.0.94; the same
-- bug is what compat.cjson's `utils` and compat.eigen's `eigen_blas`
-- (`dgemm_`) hit. Header-only mode is unaffected and works on any version,
-- which is why the index floor (`index.toml` min_mcpp) does not move: that
-- floor tracks descriptor GRAMMAR, and this descriptor parses everywhere.
--
-- All `mcpp` paths are GLOBS relative to the verdir; the leading `*` absorbs the
-- GitHub archive's `spdlog-<tag>/` wrap layer. include_dirs points at
-- `*/include` so consumers write `#include <spdlog/spdlog.h>`.
--
-- No CN mirror yet: `url` is a plain string (upstream GitHub release only), the
-- documented fallback when there is no mcpp-res write access (docs/cn-mirror.md;
-- precedent: pkgs/t/tensorvia-cpu.lua). CN users fall back to the upstream
-- source. A maintainer can later rewrite each `url` to a { GLOBAL, CN } table
-- (sha256 unchanged) once the gitcode mcpp-res/spdlog mirror exists.
package = {
spec = "1",
namespace = "compat",
name = "compat.spdlog",
description = "Fast C++ logging library (header-only by default, compiled via the `compiled` feature)",
licenses = {"MIT"},
repo = "https://github.com/gabime/spdlog",
type = "package",

xpm = {
linux = {
["1.17.0"] = {
url = "https://github.com/gabime/spdlog/archive/refs/tags/v1.17.0.tar.gz",
sha256 = "d8862955c6d74e5846b3f580b1605d2428b11d97a410d86e2fb13e857cd3a744",
},
},
macosx = {
["1.17.0"] = {
url = "https://github.com/gabime/spdlog/archive/refs/tags/v1.17.0.tar.gz",
sha256 = "d8862955c6d74e5846b3f580b1605d2428b11d97a410d86e2fb13e857cd3a744",
},
},
windows = {
["1.17.0"] = {
url = "https://github.com/gabime/spdlog/archive/refs/tags/v1.17.0.tar.gz",
sha256 = "d8862955c6d74e5846b3f580b1605d2428b11d97a410d86e2fb13e857cd3a744",
},
},
},

mcpp = {
language = "c++23",
import_std = false,
c_standard = "c11",
-- Exposes include/spdlog/** so consumers write `#include <spdlog/...>`.
-- The bundled fmt under include/spdlog/fmt/bundled/ rides along, so no
-- external fmt dependency is needed in either mode.
include_dirs = { "*/include" },
-- Header-only default: a trivial anchor TU gives mcpp a buildable lib
-- target when no source is compiled.
generated_files = {
["mcpp_generated/spdlog_anchor.c"] = [==[
int mcpp_compat_spdlog_headers_anchor(void) { return 0; }
]==],
},
sources = { "mcpp_generated/spdlog_anchor.c" },
targets = { ["spdlog"] = { kind = "lib" } },
features = {
-- Precompiled mode: compiles spdlog's src/*.cpp into the lib AND
-- publishes SPDLOG_COMPILED_LIB as an interface define so consumer
-- headers take the non-inline / extern-template path. src/spdlog.cpp
-- (and the other six) #error without this define. Needs mcpp >=
-- 0.0.94 under `mcpp test` — see the header note.
["compiled"] = {
sources = { "*/src/*.cpp" },
defines = { "SPDLOG_COMPILED_LIB" },
},
},
deps = { },
},
}
19 changes: 19 additions & 0 deletions tests/examples/spdlog-compiled/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# spdlog test project: consumes compat.spdlog in its COMPILED mode
# (`features = ["compiled"]`) — spdlog's src/*.cpp are compiled into the lib and
# SPDLOG_COMPILED_LIB reaches this consumer's TUs as an interface define, so the
# headers take the non-inline path and link against those objects.
#
# Sibling of tests/examples/spdlog/ (header-only default mode). Both modes ship
# from one descriptor; this member is what keeps the compiled path honest.
#
# Needs mcpp >= 0.0.94: older versions never compiled a dependency's active
# feature `sources` under `mcpp test`, so this member would fail to link.
[package]
name = "spdlog-compiled-tests"
version = "0.1.0"

[indices]
compat = { path = "../../.." }

[dependencies.compat]
spdlog = { version = "1.17.0", features = ["compiled"] }
42 changes: 42 additions & 0 deletions tests/examples/spdlog-compiled/tests/compiled_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Behavioral test: consume compat.spdlog in COMPILED mode. Asserts both halves
// of what the `compiled` feature promises:
//
// 1. the interface define reached this TU (SPDLOG_COMPILED_LIB defined, and
// spdlog's headers therefore did NOT flip on SPDLOG_HEADER_ONLY) — a
// static check, so a regression is a compile error, not a silent fallback
// to the header-only path that would still pass the runtime assertions;
// 2. spdlog's src/*.cpp really were compiled and linked — the calls below
// resolve to out-of-line symbols (default_logger_raw, log_msg's ctor,
// logger::log_it_, and bundled fmt's vformat), which is exactly what fails
// to link when the engine skips a feature's sources.
#include <spdlog/spdlog.h>
#include <spdlog/sinks/ostream_sink.h>
#include <sstream>
#include <string>

#ifndef SPDLOG_COMPILED_LIB
#error "compiled feature did not propagate SPDLOG_COMPILED_LIB to the consumer"
#endif
#ifdef SPDLOG_HEADER_ONLY
#error "SPDLOG_HEADER_ONLY is on in compiled mode — headers took the inline path"
#endif

int main() {
std::ostringstream captured;
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(captured);
spdlog::logger logger("test", sink);
logger.set_pattern("%v"); // message only, no timestamp/level decoration

logger.info("hello {}={}", "answer", 42);
logger.warn("{:#x}", 255);
logger.flush();

// Exercises the global registry too — default_logger_raw() is one of the
// out-of-line symbols that only exists once src/spdlog.cpp is compiled.
spdlog::set_level(spdlog::level::info);

const std::string out = captured.str();
const bool ok = out.find("hello answer=42") != std::string::npos
&& out.find("0xff") != std::string::npos;
return ok ? 0 : 1;
}
15 changes: 15 additions & 0 deletions tests/examples/spdlog/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# spdlog test project: consumes compat.spdlog in its DEFAULT header-only mode
# (bundled fmt, no external dependency) and asserts behavior under `mcpp test`.
# Part of the mcpp-index self-referential workspace: `[indices] compat` points
# at this repo, so the dependency resolves to the checked-in recipe
# (pkgs/c/compat.spdlog.lua). Compiled mode is covered by the sibling member
# tests/examples/spdlog-compiled/.
[package]
name = "spdlog-tests"
version = "0.1.0"

[indices]
compat = { path = "../../.." }

[dependencies.compat]
spdlog = "1.17.0"
25 changes: 25 additions & 0 deletions tests/examples/spdlog/tests/log_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Behavioral test: consume compat.spdlog in its DEFAULT header-only mode. Logs
// through a custom logger wired to an in-memory ostream sink, then asserts the
// formatted output — this exercises the logger, the pattern formatter, and the
// bundled {fmt} formatting all via the header inlines. Returns non-zero on any
// mismatch.
#include <spdlog/spdlog.h>
#include <spdlog/sinks/ostream_sink.h>
#include <sstream>
#include <string>

int main() {
std::ostringstream captured;
auto sink = std::make_shared<spdlog::sinks::ostream_sink_mt>(captured);
spdlog::logger logger("test", sink);
logger.set_pattern("%v"); // message only, no timestamp/level decoration

logger.info("hello {}={}", "answer", 42);
logger.warn("{:#x}", 255);
logger.flush();

const std::string out = captured.str();
const bool ok = out.find("hello answer=42") != std::string::npos
&& out.find("0xff") != std::string::npos;
return ok ? 0 : 1;
}
Loading