From 1aec7688a680954dc095ff9d5125c32e12a03d54 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 22 Oct 2026 23:39:21 +0800 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20=E5=8A=A0=E5=9B=BA=20C=20=E9=97=A8?= =?UTF-8?q?=E7=A6=81=20=E2=80=94=E2=80=94=20=E5=8D=A1=E6=AD=BB=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=8B=96=E6=BB=A1=E8=B6=85=E6=97=B6=EF=BC=8C=E4=B8=94?= =?UTF-8?q?=E7=BC=BA=E5=B7=A5=E5=85=B7=E4=B8=8D=E5=86=8D=E5=81=87=E8=A3=85?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 两个独立问题 ### ① 卡死:20 分钟全花在 apt-get,不是门禁 实测证据:本机 `make check-csrc` 五个子门禁**各自 0~1 秒**: csrc-lint 1s / csrc-abi 0s / csrc-headers 0s / csrc-sanitize 0s / csrc-cross 0s 而 CI 里同一个 job 两次跑到 20 分钟 job 级超时(其中一次直接 cancelled)。 卡死点只可能在 `apt-get update` —— ubuntu-latest runner 上偶发挂住不返回, 既不失败也没进展,只能等 job timeout,而报错信息毫无指向性。 ### ② 假通过:缺工具时门禁 `[SKIP] ... exit 0` 这是更危险的一个。Makefile 里每道门禁在缺依赖时: if ! command -v $CC_ARM64 >/dev/null 2>&1; then echo " [SKIP] $CC_ARM64 不存在(未装交叉工具链)"; exit 0 fi 本机这样合理(开发者未必装交叉工具链),但在 CI 里意味着:apt-get 悄悄失败 ⇒ 五道门禁全部跳过 ⇒ **job 绿** ⇒ 让人以为 C 侧有门禁,实际一个都没跑。 **静默降级比红更危险:红会有人修,绿没人看。** 实测确认(`CC_ARM64=nonexistent-gcc-xyz make csrc-cross`): == C 侧交叉编译(linux/arm64)== [SKIP] nonexistent-gcc-xyz 不存在(未装交叉工具链) --- 退出码: 0 --- ← 这就是问题 ## 加固内容 | 措施 | 作用 | |---|---| | 逐包检查是否已安装 | runner 镜像通常自带 cmake/clang ⇒ **多数情况完全不碰网络** | | apt-get 自带 `Acquire::*Timeout=60` + `Retries=3` | 卡住时自身失败,而不是拖到 job 级超时(后者报错无指向性) | | 装包 step `timeout-minutes: 6` | 双保险 | | 新增「确认工具链齐备」步骤 | 缺任一工具 ⇒ 红,并 `::error::` 说明"缺工具时门禁会 SKIP 等于假装通过" | | 跑门禁 step `timeout-minutes: 5` | 门禁实测 0~1 秒,超出即异常(不是慢) | | `grep '\[SKIP\]'` 二次拦截 | 覆盖将来新增门禁时漏配依赖的情况 | | job `timeout-minutes` 20 → 15 | 已经是纯 apt-get 余量 | ## 为什么要两道 SKIP 拦截 「确认工具链齐备」+「grep [SKIP]」看似重复,其实针对不同风险: - 前者:**当前**三道门禁的依赖齐不齐 - 后者:**将来**新增门禁时,新依赖没写进安装步骤 —— 那时前者不知道要检查它, 只有日志里的 `[SKIP]` 能抓到 ## 本地实测(逐条验证,不是只改完就推) | 验证 | 结果 | |---|---| | YAML 可解析 | ✅ | | 三件套本机齐备 | ✅ cmake / clang / aarch64-linux-gnu-gcc | | `make check-csrc` 无 SKIP | ✅ 五门禁全实际执行 | | 已装则跳过 apt-get 分支 | ✅ 「三件套已存在,跳过 apt-get」 | | 部分缺失时拼装需要安装列表 | ✅ 正确识别出 `clang` | | **变异:交叉工具链缺失** | ✅ grep 拦截生效(若不加这层,CI 会绿) | --- .github/workflows/ci.yml | 93 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb5bc3ba..a7f290a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,19 +167,98 @@ jobs: csrc: name: C infrastructure gates runs-on: ubuntu-latest - timeout-minutes: 20 + # 门禁本身实测 0~1 秒(csrc-lint/abi/headers/sanitize/cross 各自 <1s)。 + # 这 15 分钟全部是给 apt-get 的余量 —— 卡死历来只发生在装包阶段。 + timeout-minutes: 15 steps: - uses: actions/checkout@v7 - # clang 供双编译器告警对照;gcc-aarch64 供跨架构编译门禁。 - # 门禁在缺工具时是显式 SKIP 而不是假通过,这里装齐以免静默降级。 - - name: 安装 C 工具链 + # ── 安装 C 工具链 ───────────────────────────────────────────── + # + # ★ 加固点(2026-10-22):实测两次该 job 卡死 20 分钟并超时, + # 而**本地同一门禁 0~1 秒全通过**(make check-csrc)。卡死点全在 + # apt-get:ubuntu-latest runner 上 `apt-get update` 偶发挂住不返回, + # 既没有失败也没有进展,只能等 job 级 timeout。 + # + # 三重加固: + # 1. 逐包检查是否已安装 —— runner 镜像通常自带 cmake/clang, + # 已装就跳过 apt-get(多数情况直接走这条,完全不碰网络) + # 2. 需要装时给 apt-get 加自身的超时与重试,让它**自己失败**, + # 而不是把整个 job 拖到 job 级 timeout(后者报错信息毫无指向性) + # 3. 装完立即 `command -v` 复核,缺工具就红 —— 见下方"缺工具即失败" + - name: 安装 C 工具链(已装则跳过) + timeout-minutes: 6 run: | - sudo apt-get update -qq - sudo apt-get install -y -qq cmake clang gcc-aarch64-linux-gnu + set -uo pipefail + need="" + command -v cmake >/dev/null || need="$need cmake" + command -v clang >/dev/null || need="$need clang" + command -v aarch64-linux-gnu-gcc >/dev/null || need="$need gcc-aarch64-linux-gnu" + if [ -z "$need" ]; then + echo "✅ 三件套已存在,跳过 apt-get:$(command -v cmake clang aarch64-linux-gnu-gcc | tr '\n' ' ')" + exit 0 + fi + + echo "需要安装:$need" + # apt-get 自己的超时:卡住时 4 分钟内失败,而不是拖到 6 分钟 step 超时。 + # 重试一次 —— runner 网络偶发抖动不该让门禁变红。 + sudo apt-get -o Acquire::http::Timeout=60 \ + -o Acquire::https::Timeout=60 \ + -o Acquire::Retries=3 \ + update -qq + # shellcheck disable=SC2086 + sudo apt-get -o Acquire::http::Timeout=60 \ + -o Acquire::https::Timeout=60 \ + -o Acquire::Retries=3 \ + install -y -qq --no-install-recommends $need + + # ── 缺工具即失败(★ 这条是本次加固的核心)──────────────────── + # + # 此前门禁在缺工具时打 `[SKIP] ... exit 0` —— **CI 照样变绿**。 + # 那意味着:apt-get 悄悄失败(或将来有人删掉安装步骤)⇒ 五道门禁 + # 全部跳过 ⇒ job 绿 ⇒ 让人以为 C 侧有门禁,实际一个都没跑。 + # + # 静默降级比红更危险:红会有人修,绿没人看。 + # 所以 CI 里显式要求工具必须在场,SKIP 视为失败。 + - name: 确认 C 工具链齐备(缺则失败,不接受 SKIP) + run: | + set -euo pipefail + miss="" + for t in cmake clang aarch64-linux-gnu-gcc; do + if command -v "$t" >/dev/null 2>&1; then + echo " ✅ $t: $(command -v "$t")" + else + echo " ❌ $t 缺失" >&2 + miss="$miss $t" + fi + done + [ -z "$miss" ] || { + echo "::error::C 工具链缺失:$miss —— 缺工具时门禁会 SKIP 并 exit 0,等于假装通过" + exit 1 + } + + # ── 跑门禁 ────────────────────────────────────────────────── + # 门禁自身 0~1 秒,给它 5 分钟绰绰有余;超出说明是异常(不是慢)。 - name: make check-csrc - run: make check-csrc + timeout-minutes: 5 + run: | + set -o pipefail + make check-csrc 2>&1 | tee /tmp/csrc.log + + # ★ 二次确认:任何 [SKIP] 都视为失败。 + # + # Makefile 里每道门禁在缺依赖时 `echo "[SKIP] ..."; exit 0`, + # 这在本机是合理的(开发者未必装了交叉工具链),但在 CI 里 + # 就等于把"没验证"伪装成"通过"。上面那步已保证工具齐备, + # 这里再拦一次是为了覆盖将来新增门禁时漏配依赖的情况。 + if grep -q '\[SKIP\]' /tmp/csrc.log; then + echo "::error::门禁出现 [SKIP],说明有检查没真正执行:" + grep '\[SKIP\]' /tmp/csrc.log >&2 + echo "CI 里不接受 SKIP —— 要么装上依赖,要么明确从 check-csrc 摘掉这条。" >&2 + exit 1 + fi + echo "✅ 全部门禁实际执行,无 SKIP" # ── 文档站构建(mkdocs,纯 Python,无外部依赖)── docs: From 16bb794a43d69486c1c3766f8e888763c7bb7936 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 22 Oct 2026 23:47:15 +0800 Subject: [PATCH 2/5] =?UTF-8?q?ci:=20=E4=BA=8C=E6=AC=A1=E5=8A=A0=E5=9B=BA?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20=E4=B8=8A=E4=B8=80=E7=89=88=E5=AF=B9=20?= =?UTF-8?q?apt=20=E5=8D=A1=E6=AD=BB=E6=97=A0=E6=95=88=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20timeout=20=E7=A1=AC=E8=B6=85=E6=97=B6=E5=B9=B6?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=20-qq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 上一版为什么没解决 第一版加了 `Acquire::http::Timeout=60` + `Retries=3` + step 6 分钟超时。 CI 实测结果: 15:39:48 需要安装: gcc-aarch64-linux-gnu 15:46:01 ##[error]The action '安装 C 工具链(已装则跳过)' has timed out after 6 minutes. **依然卡满 6 分钟**,而且日志里**一个 apt 输出都没有** —— 因为 `-qq` 把 所有输出静默了。两个结论: 1. `Acquire::*Timeout` 只管**单个连接**,不管 apt 整体 —— 源重试、DPKG 锁 等待、依赖计算都不受它约束,所以它拦不住这类卡死 2. `-qq` 让卡死**完全不可观测**:既看不到卡在哪个源,也看不到在下载什么。 不可观测正是它能悄悄拖满超时的原因 (顺带证明"已装则跳过"这条是有效的:cmake/clang 都在镜像里,只列出了 `gcc-aarch64-linux-gnu` 一项要装。) ## 这一版改法 | 改动 | 理由 | |---|---| | 去掉 `-qq`,保留可读进度 | 下次卡死必须能看出卡在哪,否则无从诊断 | | `timeout 120 apt-get update` | **外层硬超时**,强制结束,不依赖 apt 自身行为 | | `timeout 180 apt-get install` | 同上;120+180=300s 配 step 5 分钟正好 | | 失败即 `exit 1` + `::error::`,不重试整段 | 宁可红一次让人看见,不要用重试掩盖真实问题 | | `--no-install-recommends` 保留 | 减少下载量与依赖面 | ## 本地实测 | 验证 | 结果 | |---|---| | YAML 可解析 | ✅ | | `timeout 3 sleep 60` 行为 | ✅ 3 秒强制结束,退出码 124,可被 `if !` 捕获 | | "已装则跳过"分支 | ✅ 「✅ 跳过 apt-get」 | | 上一版缺工具 `[SKIP] ... exit 0` 风险 | ✅ 已复现并拦截(见前一提交) | ## 仍待 CI 验证 这一版的真实效果只有在 runner 上才知。若这次仍然卡满,说明 `timeout` 对 sudo+apt 组合失效(可能性低),届时改用「换源」或「预装镜像」方案。 --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7f290a9..a06f027b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,7 +187,9 @@ jobs: # 而不是把整个 job 拖到 job 级 timeout(后者报错信息毫无指向性) # 3. 装完立即 `command -v` 复核,缺工具就红 —— 见下方"缺工具即失败" - name: 安装 C 工具链(已装则跳过) - timeout-minutes: 6 + # 120s(update) + 180s(install) = 300s,给 5 分钟正好; + # 超出说明 timeout 命令本身没生效,此时应当红而不是继续等。 + timeout-minutes: 5 run: | set -uo pipefail need="" @@ -201,17 +203,29 @@ jobs: fi echo "需要安装:$need" - # apt-get 自己的超时:卡住时 4 分钟内失败,而不是拖到 6 分钟 step 超时。 - # 重试一次 —— runner 网络偶发抖动不该让门禁变红。 - sudo apt-get -o Acquire::http::Timeout=60 \ - -o Acquire::https::Timeout=60 \ - -o Acquire::Retries=3 \ - update -qq + + # ★ 2026-10-22 实测:加 Acquire::*Timeout=60 + Retries=3 之后仍然卡满 + # 6 分钟 step 超时,且**日志里一个 apt 输出都没有** —— 因为 `-qq` + # 把一切静默了。卡死不可观测正是它拖满超时的原因:既看不到卡在哪个 + # 源,也看不到到底在下载什么。 + # + # 所以: + # · 去掉 `-qq`,保留可读进度(否则下次卡死仍然无从诊断) + # · 用 `timeout` 命令做外层硬超时 —— Acquire::Timeout 只管单个连接, + # 不管 apt 整体(源重试、DPkg 锁等待都不受它约束) + # · 失败后**不重试整段**:宁可红一次让人看见,也不要为偶发网络 + # 抖动掩盖真实问题 + APT_OPTS="-o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Retries=2" # shellcheck disable=SC2086 - sudo apt-get -o Acquire::http::Timeout=60 \ - -o Acquire::https::Timeout=60 \ - -o Acquire::Retries=3 \ - install -y -qq --no-install-recommends $need + if ! timeout 120 sudo apt-get $APT_OPTS update; then + echo "::error::apt-get update 超时/失败(120s)—— runner 网络或源不可用" + exit 1 + fi + # shellcheck disable=SC2086 + if ! timeout 180 sudo apt-get $APT_OPTS install -y --no-install-recommends $need; then + echo "::error::apt-get install 超时/失败(180s)需要安装:$need" + exit 1 + fi # ── 缺工具即失败(★ 这条是本次加固的核心)──────────────────── # From 7fdec0670fdf78e3c9b78f2e78093b1161f2844c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 22 Oct 2026 23:55:54 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(csrc):=20=E4=BA=A4=E5=8F=89=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E6=94=B9=E7=94=A8=20clang=20--target=EF=BC=8C?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E4=BC=9A=E5=8D=A1=E6=AD=BB=E7=9A=84=20apt=20?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=20+=20=E4=BF=AE=E4=B8=A4=E5=A4=84=E5=81=87?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 卡死真因(这次可观测了) 上一版加了 `timeout` 硬超时后,日志终于能看出卡在哪: 15:47:33 Fetched 12.5 MB in 1s (9972 kB/s) ← update 很快 15:47:38 Get:2 gcc-13-aarch64-linux-gnu-base 51.9 kB 15:47:39 Get:3 cpp-13-aarch64-linux-gnu 10.8 MB ← 下到这儿卡住 15:50:37 ##[error]apt-get install 超时/失败(180s) **根因:为了跑一道 `-fsyntax-only` 的语法门禁,去下载 10MB+ 的交叉 GCC 工具链, 在 runner 上拉不动。** ## 改法:用 clang 自带的交叉前端 这道门禁只做 `-fsyntax-only`(不链接、不运行)。clang 支持 `--target=aarch64-linux-gnu`,会**真的切到 aarch64 目标**: $ clang -fsyntax-only arch2.c → error: "not aarch64 target" $ clang --target=aarch64-linux-gnu ... → 通过(__aarch64__ 生效) 指针宽度、endianness、C 扩展支持都按目标架构算 —— 足以暴露本门禁要抓的 那类问题。而 clang 在 ubuntu-latest 镜像里**已预装**(实测确认,只列出 `gcc-aarch64-linux-gnu` 一项需要安装)。 优先级:`CC_ARM64`(显式)> `clang --target` > `aarch64-linux-gnu-gcc` > SKIP ## 顺带修掉两处**本来就存在**的假通过 这两处不是本轮引入的,是新写的存在性校验暴露出来的: ### ① `cmd | head` 吞掉编译器退出码 for src in ...; do if ! $CC ... -fsyntax-only $src 2>&1 | head -20; then ok=0; fi done 管道退出码是 `head` 的(恒 0)。实测:`CC_ARM64=nonexistent-xyz make csrc-cross` 竟然输出「0 告警、编译通过 ✓」—— 编译器根本没执行。 修法:改用 `${PIPESTATUS[0]}`,取编译器的真实退出码。 ### ② `PIPESTATUS` 需要 bash,Make 默认是 dash `/bin/sh` 在 Debian/Ubuntu 上是 dash,不支持 `PIPESTATUS`(报 Bad substitution)。 必须显式 `SHELL := /bin/bash`,否则上面的修法静默失效。 ### 风险自证(三条路径实测) | 场景 | 结果 | |---|---| | 正常(clang-target) | ✅ 0 告警、编译通过 ✓ | | `CC_ARM64` 指向不存在的命令 | ✅ `[FAIL] CC_ARM64=nonexistent-xyz 不存在` → Error 1 | | 真的写坏 C 源码 | ✅ 编译器报错 + `[FAIL] arm64 交叉编译失败` → Error 1 | ## CI 侧 - 安装步骤不再要求 `gcc-aarch64-linux-gnu` ⇒ **依赖已预装,直接跳过 apt-get,完全不碰网络** - 工具链确认步骤只校验 `cmake` + `clang` - `[SKIP]` 拦截、`timeout` 硬超时、去 `-qq` 全部保留(对付将来别的原因导致的卡死) --- .github/workflows/ci.yml | 18 +++++++++---- Makefile | 57 +++++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a06f027b..9ed4a233 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,13 +192,21 @@ jobs: timeout-minutes: 5 run: | set -uo pipefail + # ★ 不再需要 gcc-aarch64-linux-gnu(2026-10-22)。 + # + # 实测:装它要从源拉 10MB+ 的 cpp-13 等依赖,在 runner 上卡住超时 + # (15:47:39 开始下 cpp-13-aarch64,180s 后仍未完成)。 + # 而 csrc-cross 只需要 `-fsyntax-only`,clang 自带交叉前端即可: + # `clang --target=aarch64-linux-gnu` 会真的切到 aarch64 目标 + # (__aarch64__ 宏生效、指针宽度按该架构算),足以暴露本门禁要抓的 + # 架构问题。Makefile 的 csrc-cross 已按 + # CC_ARM64 > clang --target > aarch64-gcc > SKIP 的优先级挑选。 need="" - command -v cmake >/dev/null || need="$need cmake" - command -v clang >/dev/null || need="$need clang" - command -v aarch64-linux-gnu-gcc >/dev/null || need="$need gcc-aarch64-linux-gnu" + command -v cmake >/dev/null || need="$need cmake" + command -v clang >/dev/null || need="$need clang" if [ -z "$need" ]; then - echo "✅ 三件套已存在,跳过 apt-get:$(command -v cmake clang aarch64-linux-gnu-gcc | tr '\n' ' ')" + echo "✅ 依赖已存在,跳过 apt-get:$(command -v cmake clang | tr '\n' ' ')" exit 0 fi @@ -239,7 +247,7 @@ jobs: run: | set -euo pipefail miss="" - for t in cmake clang aarch64-linux-gnu-gcc; do + for t in cmake clang; do if command -v "$t" >/dev/null 2>&1; then echo " ✅ $t: $(command -v "$t")" else diff --git a/Makefile b/Makefile index 4d77fe52..6b00f6b3 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +# 门禁脚本需要 bash 的 PIPESTATUS(/bin/sh=dash 不支持)。 +# 没有它就无法区分"编译器失败"与"head 成功" —— 实测会让门禁假通过。 +SHELL := /bin/bash + .PHONY: test-gui all build build-plain build-cli build-gui clean install test run build-static build-linux-arm64 lint fmt sync-client-versions check-client-versions check-starmap-shared check-client-protocol csrc csrc-test csrc-lint csrc-abi csrc-headers csrc-sanitize csrc-cross csrc-fuzz check-csrc check-csrc-full # HOMED_TAGS 默认带 onnxruntime:发行版**默认启用**本地向量空间(与 # deploy/packaging/build.sh 保持一致)。 @@ -255,21 +259,56 @@ csrc-fuzz: # C 侧的架构相关问题(endianness 假设、指针宽度、size_t vs int 宽度、 # -fsanitize 不可用)只有真的用目标编译器编一遍才会暴露。 # 与 deploy/packaging/build.sh 的 arm64 目标共用同一套 CC 变量。 +# +# ## 为什么首选 clang --target,而不是 aarch64-linux-gnu-gcc(2026-10-22) +# +# 这道门禁只做 `-fsyntax-only`(不链接、不运行),而 clang 自带交叉前端: +# `clang --target=aarch64-linux-gnu` 会真的切到 aarch64 目标(`__aarch64__` +# 宏生效、指针宽度按该架构计算),足以暴露本门禁要抓的那类问题 +# (endianness 假设、指针/size_t 宽度、不支持的 C 扩展)。 +# +# 而装 `gcc-aarch64-linux-gnu` 需要从源拉 10MB+ 的 cpp-13 等依赖 —— +# 实测在 CI runner 上会卡住超时(见 .github/workflows/ci.yml 的加固说明)。 +# 既然只需要语法层验证,就不该为它引入一个会卡死的下载依赖。 +# +# 优先级:CC_ARM64(显式指定) > clang --target > aarch64-linux-gnu-gcc > SKIP +# +# ★ 两个假通过陷阱(实测踩到,勿删): +# 1. 显式 CC_ARM64 指向不存在的命令时,`cmd | head` 的退出码是 head 的(恒 0), +# command-not-found(127) 被吞 ⇒ 门禁显示"通过"。所以循环里改用 PIPESTATUS[0], +# 且对显式指定的编译器先做 command -v 校验。 +# 2. 上述 PIPESTATUS 需要 bash;Make 默认 /bin/sh=dash 不支持(见文件顶 SHELL)。 .PHONY: csrc-cross csrc-cross: @echo "== C 侧交叉编译(linux/arm64)==" - @CC_ARM64=$${CC_ARM64:-aarch64-linux-gnu-gcc}; \ - if ! command -v $$CC_ARM64 >/dev/null 2>&1; then \ - echo " [SKIP] $$CC_ARM64 不存在(未装交叉工具链)"; exit 0; \ - fi; \ - ok=1; \ - for src in $(CSRC_SRCS); do \ - if ! $$CC_ARM64 $(CSRC_CFLAGS) -Werror -fsyntax-only $$src 2>&1 | head -20; then \ - ok=0; \ + @ok=1; \ + if [ -n "$${CC_ARM64:-}" ]; then \ + if ! command -v "$$CC_ARM64" >/dev/null 2>&1; then \ + echo " [FAIL] CC_ARM64=$$CC_ARM64 不存在"; exit 1; \ fi; \ + CC="$$CC_ARM64"; MODE="explicit"; \ + elif command -v clang >/dev/null 2>&1; then \ + CC="clang --target=aarch64-linux-gnu"; MODE="clang-target"; \ + elif command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then \ + CC="aarch64-linux-gnu-gcc"; MODE="cross-gcc"; \ + else \ + echo " [SKIP] 无 clang、无 aarch64-linux-gnu-gcc(未装任何交叉前端)"; exit 0; \ + fi; \ + \ + if [ "$$MODE" = "clang-target" ]; then \ + printf 'int main(void){return 0;}\n' > /tmp/_archchk.c; \ + if ! clang --target=aarch64-linux-gnu -fsyntax-only /tmp/_archchk.c 2>/dev/null; then \ + echo " [SKIP] clang 不支持 --target=aarch64-linux-gnu(版本过旧)"; exit 0; \ + fi; \ + fi; \ + \ + for src in $(CSRC_SRCS); do \ + $$CC $(CSRC_CFLAGS) -Werror -fsyntax-only $$src 2>&1 | head -20; \ + rc=$${PIPESTATUS[0]}; \ + if [ "$$rc" != "0" ]; then ok=0; fi; \ done; \ if [ "$$ok" = "1" ]; then \ - echo " $$CC_ARM64: 0 告警、编译通过 ✓($(words $(CSRC_SRCS)) 个源文件)"; \ + echo " $$CC [$$MODE]: 0 告警、编译通过 ✓($(words $(CSRC_SRCS)) 个源文件)"; \ else \ echo " [FAIL] arm64 交叉编译失败"; exit 1; \ fi From 0e5077497e37220f7c75c68feaface093ef32082 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 22 Oct 2026 23:59:37 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(csrc):=20clang=20=E4=BA=A4=E5=8F=89?= =?UTF-8?q?=E9=9C=80=E8=A6=81=20aarch64=20libc=20=E5=A4=B4=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E5=8F=AA=E8=A3=85=201.7MB=20=E5=A4=B4=E5=8C=85?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E8=A3=85=2010MB=20=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## CI 实测暴露的下一层问题 上一版让 clang 做交叉编译后,安装步骤成功且**完全跳过 apt**(cmake/clang 都在 镜像里),但 `make check-csrc` 红在: In file included from /usr/lib/llvm-18/lib/clang/18/include/stdint.h:52: /usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found 真因:`clang --target=aarch64-linux-gnu` 切换的是**目标架构**,但系统头 仍按**宿主**(amd64)找 —— `bits/libc-header-start.h` 是 amd64 的布局, aarch64 目标下不该用。本地能过是本机装了完整交叉 GCC,带来了 `/usr/aarch64-linux-gnu/include`。 ## 试过但走不通的两条路(记录以免重复踩) | 尝试 | 结果 | |---|---| | `-nostdinc` + clang 内建头 | ❌ clang 内建**不含 `string.h`**(那是 libc 的) | | `-ffreestanding -nostdlibinc` | ❌ 同上,代码实际用了 `strlen`/`memcpy` | ## 采用:只装 libc 头,不装编译器 libc6-dev-arm64-cross 1.7 MB(+ libc6-arm64-cross、linux-libc-dev-arm64-cross) gcc-aarch64-linux-gnu 含 cpp-13(10.8MB)+ gcc-16 等,实测下载卡死 既然只需要**头文件**(不做链接、不跑代码),就不该拉整个交叉 GCC。 装上头后 clang 就能完成 `-fsyntax-only`。 ### Makefile 配合 `csrc-cross` 的 clang-target 模式现在自动探测并注入 aarch64 头路径: for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do [ -d "$d" ] && ARM64_SYSINC="$ARM64_SYSINC -isystem $d" done 探测改用**真实头文件**(`#include `)而非空 main,这样 "clang 版本支持 --target" 与 "aarch64 头是否齐备" 两个条件一起验; 缺头时明确 SKIP 并说清装哪个包(`libc6-dev-arm64-cross`)。 ## 本地实测 | 验证 | 结果 | |---|---| | 有 aarch64 头时 | ✅ clang-target 通过(0 告警) | | `make check-csrc` 全门禁 | ✅ 全部通过,0 SKIP | | YAML 可解析 | ✅ | | 上一版三条失败路径(编译器不存在/真写坏/正常) | ✅ 仍然正确(PIPESTATUS 修复未回退) | --- .github/workflows/ci.yml | 4 ++++ Makefile | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ed4a233..cd6789b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,6 +204,10 @@ jobs: need="" command -v cmake >/dev/null || need="$need cmake" command -v clang >/dev/null || need="$need clang" + # aarch64 的 libc 头(1.7MB,仅头文件,**不含编译器**)。 + # 用它 + 已预装的 clang 交叉前端,替代 10MB+ 的完整交叉 GCC + # (实测那个会卡在下载 cpp-13-aarch64,180s 超时)。 + [ -d /usr/aarch64-linux-gnu/include ] || need="$need libc6-dev-arm64-cross" if [ -z "$need" ]; then echo "✅ 依赖已存在,跳过 apt-get:$(command -v cmake clang | tr '\n' ' ')" diff --git a/Makefile b/Makefile index 6b00f6b3..63970525 100644 --- a/Makefile +++ b/Makefile @@ -295,10 +295,15 @@ csrc-cross: echo " [SKIP] 无 clang、无 aarch64-linux-gnu-gcc(未装任何交叉前端)"; exit 0; \ fi; \ \ + ARM64_SYSINC=""; \ + for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do \ + [ -d "$$d" ] && ARM64_SYSINC="$$ARM64_SYSINC -isystem $$d"; \ + done; \ + \ if [ "$$MODE" = "clang-target" ]; then \ - printf 'int main(void){return 0;}\n' > /tmp/_archchk.c; \ - if ! clang --target=aarch64-linux-gnu -fsyntax-only /tmp/_archchk.c 2>/dev/null; then \ - echo " [SKIP] clang 不支持 --target=aarch64-linux-gnu(版本过旧)"; exit 0; \ + printf '#include \nint main(void){return 0;}\n' > /tmp/_archchk.c; \ + if ! clang --target=aarch64-linux-gnu $$ARM64_SYSINC -fsyntax-only /tmp/_archchk.c >/dev/null 2>&1; then \ + echo " [SKIP] clang 交叉前端不可用(缺 aarch64 libc 头,装 libc6-dev-arm64-cross 即可)"; exit 0; \ fi; \ fi; \ \ From 369a202d6f83e5520b0352e935a6ad48b6aa2f8e Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 23 Oct 2026 00:04:19 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(csrc):=20=E7=94=A8=20-nostdinc=20?= =?UTF-8?q?=E5=89=83=E6=8E=89=E5=AE=BF=E4=B8=BB=E5=A4=B4=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=20clang=20=E4=BA=A4=E5=8F=89=E7=9A=84=20stdint.h=20=E5=86=B2?= =?UTF-8?q?=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 上一版为什么还红 装了 `libc6-dev-arm64-cross`(1.7MB 头包)后,CI 报的错变成了: In file included from csrc/src/ha_codec.c:23: In file included from /usr/lib/llvm-18/lib/clang/18/include/stdint.h:52: /usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not found 注意路径:它找的是 **`/usr/include/stdint.h`(宿主 amd64 的)**,不是 aarch64 的那份。宿主 stdint.h 会 `#include `, 那是 amd64 的布局,aarch64 目标下必然找不到。 **只加 `-isystem` 指向 aarch64 头不够** —— 默认搜索路径里的 `/usr/include` 仍然优先。 ## 修法:`-nostdinc` 剃掉全部默认路径,再按序加回 ARM64_INC="-nostdinc -isystem $(clang -print-resource-dir)/include" for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do [ -d "$d" ] && ARM64_INC="$ARM64_INC -isystem $d" done 顺序:**clang 内建头 → aarch64 目标头**。`-nostdinc` 保证宿主 `/usr/include` 不再参与,宿主头泄漏这条路径被彻底断掉。 只对 clang-target 模式生效;explicit / cross-gcc 模式用编译器自带搜索路径, 不传这些(它们本来就知道自己的 sysroot 在哪)。 ## 本地五条路径实测 | 路径 | 结果 | |---|---| | ① clang-target(默认) | ✅ 0 告警通过 | | ② 显式 cross-gcc | ✅ 0 告警通过 | | ③ 显式指向不存在的编译器 | ✅ FAIL(Error 1) | | ④ **真的写坏 C 源码** | ✅ FAIL(Error 1) | | ⑤ 恢复后 | ✅ 通过 | | `make check-csrc` 全门禁 | ✅ 全部通过,0 SKIP | ③④ 是防止"假通过"的回归判据:③ 验 `command -v` 校验 + PIPESTATUS, ④ 验编译器真实退出码没被 `| head` 吞掉。 --- Makefile | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 63970525..f0fd39ef 100644 --- a/Makefile +++ b/Makefile @@ -273,6 +273,14 @@ csrc-fuzz: # # 优先级:CC_ARM64(显式指定) > clang --target > aarch64-linux-gnu-gcc > SKIP # +# ★ 宿主头泄漏陷阱(CI 实测):只加 -isystem 指向 aarch64 头还不够 —— +# 宿主 /usr/include/stdint.h 会先被找到,它再 include +# (amd64 布局),aarch64 目标下必然找不到: +# /usr/include/stdint.h:26: fatal error: 'bits/libc-header-start.h' file not found +# 所以 clang-target 模式必须 -nostdinc 剃掉全部默认路径,再按 +# 「clang 内建头 → aarch64 目标头」顺序加回(见下方 ARM64_INC)。 +# explicit / cross-gcc 模式用编译器自带搜索路径,不传这些。 +# # ★ 两个假通过陷阱(实测踩到,勿删): # 1. 显式 CC_ARM64 指向不存在的命令时,`cmd | head` 的退出码是 head 的(恒 0), # command-not-found(127) 被吞 ⇒ 门禁显示"通过"。所以循环里改用 PIPESTATUS[0], @@ -295,20 +303,22 @@ csrc-cross: echo " [SKIP] 无 clang、无 aarch64-linux-gnu-gcc(未装任何交叉前端)"; exit 0; \ fi; \ \ - ARM64_SYSINC=""; \ + ARM64_INC="-nostdinc -isystem $$(clang -print-resource-dir)/include"; \ for d in /usr/aarch64-linux-gnu/include /usr/include/aarch64-linux-gnu; do \ - [ -d "$$d" ] && ARM64_SYSINC="$$ARM64_SYSINC -isystem $$d"; \ + [ -d "$$d" ] && ARM64_INC="$$ARM64_INC -isystem $$d"; \ done; \ \ if [ "$$MODE" = "clang-target" ]; then \ printf '#include \nint main(void){return 0;}\n' > /tmp/_archchk.c; \ - if ! clang --target=aarch64-linux-gnu $$ARM64_SYSINC -fsyntax-only /tmp/_archchk.c >/dev/null 2>&1; then \ + if ! clang --target=aarch64-linux-gnu $$ARM64_INC -fsyntax-only /tmp/_archchk.c >/dev/null 2>&1; then \ echo " [SKIP] clang 交叉前端不可用(缺 aarch64 libc 头,装 libc6-dev-arm64-cross 即可)"; exit 0; \ fi; \ fi; \ \ + EXTRA_INC=""; \ + [ "$$MODE" = "clang-target" ] && EXTRA_INC="$$ARM64_INC"; \ for src in $(CSRC_SRCS); do \ - $$CC $(CSRC_CFLAGS) -Werror -fsyntax-only $$src 2>&1 | head -20; \ + $$CC $$EXTRA_INC $(CSRC_CFLAGS) -Werror -fsyntax-only $$src 2>&1 | head -20; \ rc=$${PIPESTATUS[0]}; \ if [ "$$rc" != "0" ]; then ok=0; fi; \ done; \