跳到主要内容

ISSUE-043: G5 CI cargo test 链接失败 (extension-module 跳过 libpython 链接)

发现日期:2026-06-09 状态已修复 类型实现 bug 影响范围:仅影响 G5 Rust 引擎的 CI (cargo test 构建测试二进制);不影响 maturin 打包出的 .so、不影响运行时 Python import、不影响 Math/G5 评估逻辑。


问题现象

G5 Nightly (Long Tests) workflow 在 master (0d8b287) 上定时运行失败 (run 27187041258)。失败发生在编译链接阶段,测试逻辑未执行:

rust-lld: error: undefined symbol: PyFloat_FromDouble
rust-lld: error: undefined symbol: PyThreadState_Get
rust-lld: error: undefined symbol: Py_TYPE
rust-lld: error: undefined symbol: PyGILState_Release
rust-lld: error: undefined symbol: PyImport_Import
... (PyCallable_Check / _Py_CheckFunctionResult / _PyObject_MakeTpCall / _Py_Dealloc / PyObject_GetAttr 等)
error: linking with `cc` failed: exit status: 1
collect2: error: ld returned 1 exit status

缺失符号引用自 g5_rs::simulate_multi_chip 闭包与 pyo3 内部 (PyTraceback::format / PyErr::take),全部是 Python C-API 符号。

gh run list 显示 g5-quick / g5-weekly 从无运行记录g5-nightly 仅此一次且失败 —— 即整套 G5 CI 从未真正编译通过。这次定时触发只是第一次撞上,与 commit 0d8b287 (纯 docs 改动) 无因果关系。


调查过程

  • [查代码] gh run view 27187041258 --log-failed -> 失败步骤是 cargo test --release ... -- --include-ignored,报错为链接期 undefined Python 符号,非测试断言失败。
  • [查代码] Cargo.toml:11 -> pyo3 = { version = "0.28", features = ["extension-module"] },extension-module 无条件开启
  • [假设 1] LD_LIBRARY_PATH 那步 (workflow Expose libpython to test binary) 失效?-> 确认是机制不匹配:该步只设运行时 .so 搜索路径,管不到链接期缺 -lpython。不是 bug,是用错层。
  • [假设 2 / 根因] extension-module 让 pyo3 build script 跳过发出链接 libpython 的指令 (扩展模块设计为由宿主解释器在运行时提供符号)。但 cargo test 构建的是独立可执行测试二进制,没有宿主解释器,自己又没链接 libpython -> 符号全缺。确认为根因。
  • [查代码] grep audit_state_access src tests -> 该 feature 为 #[cfg(feature)]/#[cfg(not)] 双分支,不带它仍可编译 (走 not 分支),不阻塞本次链接问题。归为遗留观察,不在本次范围。
  • [查配置] 项目无 pyproject.toml / build.rs / .cargo/config -> maturin 此前直接读 Cargo.toml 构建,extension-module 写死也能产出可用 .so (Windows 本地 import 正常)。

根因分析

perfmodel/evaluation/g5/Cargo.toml:11extension-module 写死在 pyo3 依赖 features 上。该 feature 的语义是"本 crate 是 Python 扩展,链接期不要链接 libpython,符号由加载它的解释器提供"。这对 maturin 产出的 .so 是正确的,但对 cargo test 致命:

cargo test 构建独立测试二进制 g5_rs-<hash> (可执行文件)
-> extension-module 让 build script 不发 cargo:rustc-link-lib=python
-> 测试二进制无宿主解释器、又未链接 libpython
-> rust-lld 找不到 PyFloat_FromDouble 等 -> 链接失败

这是 pyo3 官方 FAQ "I can't run cargo test: I'm having linker issues" 描述的标准坑。


Spec / 文档依据

外部依据 (pyo3 官方文档)

"The extension-module feature ... will cause the python3 shared library to not be linked ... when running cargo test, the test harness produces an executable that needs to link against libpython. The recommended fix is to make extension-module an optional feature." -- pyo3 User Guide, "Building and Distribution / FAQ — linker issues"

项目约定

CLAUDE.md:改 perfmodel/evaluation/g5/src/ 必须 maturin develop --release 重建。本次修复保持该命令不变 (feature 经 pyproject 注入)。

与用户确认

  • [2026-06-09] 确认目标:修好让 CI 真正跑通,而非禁用 nightly。
  • [2026-06-09] 确认方案:extension-module 改 optional + 新增 g5/pyproject.toml 经 [tool.maturin] features 注入 (开发命令不变)。
  • [2026-06-09] 确认验证:改完推 github 触发 workflow_dispatch 在 Linux CI 上验证链接 (本地 Windows 测不出 Linux 链接)。

解决方案

备选方案

方案描述优点缺点
A (采用)extension-module 改 optional feature + 新增 pyproject.toml 用 [tool.maturin] features 注入maturin 命令不变,feature 自动注入,开发者无需记 flag引入新文件,改变 maturin 解析路径 (需本地重验一次 maturin develop)
Bextension-module 改 optional + 所有 maturin 命令手动加 --features extension-module不引入新文件开发者每次须记得带 flag,漏带则 .so 链接行为变化,易回归
Cworkflow 里给 cargo test 注入链接参数手动链 libpython不动 Cargo.tomlextension-module 一旦开,build script 主动 skip 链接,环境变量改不动;非标准做法

采用方案

方案 A。cargo test 默认不启用 extension-module -> pyo3 自动链接 libpython,测试二进制可 link;打包路径经 pyproject 注入 feature -> wheel/.so 仍是标准扩展。

涉及文件

文件改动说明
perfmodel/evaluation/g5/Cargo.tomlpyo3 依赖去掉 features=["extension-module"];[features] 段新增 extension-module = ["pyo3/extension-module"]
perfmodel/evaluation/g5/pyproject.toml新增。[tool.maturin] features = ["pyo3/extension-module"] 把 feature 注入打包路径

Commit / PR 链接

  • Commit: <待填> (fix(g5): extension-module 改 optional 修复 cargo test 链接失败)

验证

  • Linux CI (权威):推 github 后手动触发 g5-nightlyworkflow_dispatch,确认 cargo test 步骤链接通过、测试执行。Acceptance: workflow 绿,不再出现 undefined symbol: Py*
  • 本地 Windows (回归)maturin develop --release 重建后,python -c "import g5_rs" 成功,跑一次 Math/G5 评估确认 .so 仍可用。Acceptance: import 无 STATUS_DLL_NOT_FOUND / 符号错误。

Lessons Learned / Prevention

为什么会发生

  • G5 CI workflow 加入后从未真正触发成功过 (quick 靠 PR、weekly/nightly 靠定时),链接问题被推迟到首次定时运行才暴露。
  • pyo3 extension-module 与 cargo test 的冲突是已知坑,但 Cargo.toml 初始写法直接照搬了"扩展模块"配置,未区分打包 vs 测试两条构建路径。

下次怎么避免

  • extension-module 改 optional,从根上分离测试与打包路径。
  • 让 G5 CI 在引入时就跑一次 workflow_dispatch 冒烟,不要依赖定时首触发 (Action:新 workflow 合入后手动触发一次确认绿)。

遗留问题

  • 三个 G5 workflow 的 cargo test 均未带 --features audit_state_access,与 Cargo.toml 注释"CI/单测必须显式加"不符。不带时代码走 #[cfg(not)] 分支可编译,故不阻塞 CI,但 CI 未启用未声明字段访问审计。是否让 CI 开审计 (性能/时间预算权衡) 属独立决策,留待后续。