chore: 规范标点

This commit is contained in:
pluv committed 2026-10-03 14:43:43 +08:00
1 parent e54390a9f8
commit 15e79887fa
84 files changed
+5439 -5398

No files matched your search

+173 -173
View File
@@ -1,34 +1,34 @@
CC ?= cc
# 警告就是纪律:这一串是"改完就回不去"的那类问题(影子变量、丢 const、
# 没有原型的函数)。加进来之后新代码写错会当场报出来,而不是等哪天静态分析
# 才捞到。项目在这套 flags 下是零警告的 —— 注意这句话是**连 make test 一起算的**:
# 测试程序用的是同一套 CFLAGS,以前它们里面藏着 5 条 const 丢弃警告,只有跑
# test-unit 才会看到,光看 make 的输出是看不见的。
# 警告就是纪律:这一串是"改完就回不去"的那类问题 (影子变量、丢 const、
# 没有原型的函数). 加进来之后新代码写错会当场报出来, 而不是等哪天静态分析
# 才捞到. 项目在这套 flags 下是零警告的 —— 注意这句话是**连 make test 一起算的**:
# 测试程序用的是同一套 CFLAGS, 以前它们里面藏着 5 条 const 丢弃警告, 只有跑
# test-unit 才会看到, 光看 make 的输出是看不见的.
CFLAGS ?= -std=c99 -D_GNU_SOURCE -O2 -g -Wall -Wextra -Wno-unused-parameter \
-Wshadow -Wwrite-strings -Wstrict-prototypes -Wmissing-prototypes \
-Wold-style-definition -Wpointer-arith \
-Wformat=2 -Wno-format-nonliteral
# -Wformat=2 里值钱的是 -Wformat-security:抓"把变量当格式串"。已经抓到一个 ——
# doctor 里把 HTTPS_PROXY 的值直接喂给 field(),而代理 URL 里带 % 是常事。
# -Wformat=2 里值钱的是 -Wformat-security:抓"把变量当格式串". 已经抓到一个 ——
# doctor 里把 HTTPS_PROXY 的值直接喂给 field(), 而代理 URL 里带 % 是常事.
# -Wno-format-nonliteral 关掉的是它的噪声面:我们自己那套包装内部转发 fmt 的
# 地方(xvasprintf / err_result / …)天然就是非字面量。可查的部分已经由
# LIGENT_PRINTF 属性在**调用点**查完了。
# 地方 (xvasprintf / err_result / …)天然就是非字面量. 可查的部分已经由
# LIGENT_PRINTF 属性在**调用点**查完了.
CPPFLAGS += -Isrc -pthread
# 版本号的 -D 也挂在 CPPFLAGS 上:这样主程序、静态目标、以及二十来条测试规则
# 全都自动带上,不用逐个改。测试程序确实需要它 —— 它们会链 version.c。
# 全都自动带上, 不用逐个改. 测试程序确实需要它 —— 它们会链 version.c.
CPPFLAGS += $(VERSION_DEFS)
# -lm 是必须的:代码里调了 fabs,而 gcc 会把它折成内建函数,于是"不写 -lm
# 也能链接"是个假象 —— 换个编译器(tcc)或加 -fno-builtin 就会露出来。
# 显式写上,不要靠编译器的好意。
# -lm 是必须的:代码里调了 fabs, 而 gcc 会把它折成内建函数, 于是"不写 -lm
# 也能链接"是个假象 —— 换个编译器 (tcc)或加 -fno-builtin 就会露出来.
# 显式写上, 不要靠编译器的好意.
LDLIBS += -lcurl -lm -pthread
# make SAN=1 —— 打开 AddressSanitizer / UndefinedBehaviorSanitizer。
# 链接行也要带,否则只有目标文件被插桩、链接期运行时库没进来。
# make SAN=1 —— 打开 AddressSanitizer / UndefinedBehaviorSanitizer.
# 链接行也要带, 否则只有目标文件被插桩、链接期运行时库没进来.
ifeq ($(SAN),1)
# 注意 override:命令行上传的变量会屏蔽 makefile 里对它的一切赋值(连 += 也算),
# 注意 override:命令行上传的变量会屏蔽 makefile 里对它的一切赋值 (连 += 也算),
# 所以 `make SAN=1 CFLAGS="-O1"` 不加 override 的话会**静默地不带 sanitizer** ——
# 结果是一份没插桩却看起来「干净」的构建。这个坑踩过一次,别再踩。
# -fno-sanitize-recover=all 让报错直接中止,而不是报告完继续跑。
# 结果是一份没插桩却看起来「干净」的构建. 这个坑踩过一次, 别再踩.
# -fno-sanitize-recover=all 让报错直接中止, 而不是报告完继续跑.
override CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all
override LDLIBS += -fsanitize=address,undefined
endif
@@ -37,32 +37,32 @@ BIN := ligent
SRCDIR := src
BUILDDIR := build
# 对象目录带"编译器 + 是否插桩"的标签,见文件末尾"为什么对象目录要带标签"。
# 对象目录带"编译器 + 是否插桩"的标签, 见文件末尾"为什么对象目录要带标签".
BUILD_TAG := $(basename $(notdir $(CC)))$(if $(filter 1,$(SAN)),-san,)
OBJDIR := $(BUILDDIR)/$(BUILD_TAG)
# 头文件也是依赖。以前没有这一条,改了 .h 之后 make 会说"无需做任何事",
# 目标文件仍然是旧布局 —— 这正是"子代理读到的字段全是垃圾值"那次事故的成因。
# 全量重编只要几秒,不值得为它引入 .d 文件那一套。
# 头文件也是依赖. 以前没有这一条, 改了 .h 之后 make 会说"无需做任何事",
# 目标文件仍然是旧布局 —— 这正是"子代理读到的字段全是垃圾值"那次事故的成因.
# 全量重编只要几秒, 不值得为它引入 .d 文件那一套.
HEADERS := $(wildcard $(SRCDIR)/*.h)
# ------------------------------------------------------------------ */
# 版本号:CalVer + git 哈希,构建期算出来注入 */
# 版本号:CalVer + git 哈希, 构建期算出来注入 */
# ------------------------------------------------------------------ */
#
# 源码里**不写死**版本号,只留一个回退默认值(见 src/version.h 的说明)。
# 这里取真值,用 -D 注入。算法和理由都在 tools/get-version.sh 里。
# 源码里**不写死**版本号, 只留一个回退默认值 (见 src/version.h 的说明).
# 这里取真值, 用 -D 注入. 算法和理由都在 tools/get-version.sh 里.
#
# 用 := 而不是 = :这段要跑三次 git,展开式变量会在每次引用时重跑一遍。
# 用 := 而不是 = :这段要跑三次 git, 展开式变量会在每次引用时重跑一遍.
VERSION_SCRIPT := $(SRCDIR)/../tools/get-version.sh
LIGENT_VERSION := $(shell sh $(VERSION_SCRIPT) 2>/dev/null | sed -n 1p)
LIGENT_GIT_HASH := $(shell sh $(VERSION_SCRIPT) 2>/dev/null | sed -n 2p)
LIGENT_GIT_BRANCH := $(shell sh $(VERSION_SCRIPT) 2>/dev/null | sed -n 3p)
LIGENT_BUILD_DATE := $(shell sh $(VERSION_SCRIPT) 2>/dev/null | sed -n 4p)
LIGENT_BUILD_TIME := $(shell sh $(VERSION_SCRIPT) 2>/dev/null | sed -n 5p)
# 脚本跑不起来(比如没有 sh)时给个明确的默认值,别让 -D 后面空着 ——
# 空的 LIGENT_VERSION 会让预处理器把它当成"没定义",于是回退到 version.h
# 里的值。那样其实也对,但显式写出来更不容易被误读。
# 脚本跑不起来 (比如没有 sh)时给个明确的默认值, 别让 -D 后面空着 ——
# 空的 LIGENT_VERSION 会让预处理器把它当成"没定义", 于是回退到 version.h
# 里的值. 那样其实也对, 但显式写出来更不容易被误读.
LIGENT_VERSION := $(if $(LIGENT_VERSION),$(LIGENT_VERSION),0.0.0-unknown)
LIGENT_GIT_HASH := $(if $(LIGENT_GIT_HASH),$(LIGENT_GIT_HASH),unknown)
LIGENT_GIT_BRANCH := $(if $(LIGENT_GIT_BRANCH),$(LIGENT_GIT_BRANCH),unknown)
@@ -76,42 +76,42 @@ VERSION_DEFS = -DLIGENT_VERSION='"$(LIGENT_VERSION)"' \
-DLIGENT_BUILD_TIME='"$(LIGENT_BUILD_TIME)"'
# ------------------------------------------------------------------ */
# 版本号变了就自动清掉对象,免得 make 拿旧版本号蒙混过关 */
# 版本号变了就自动清掉对象, 免得 make 拿旧版本号蒙混过关 */
# ------------------------------------------------------------------ */
#
# 这是"make 看不见命令行变量变了"这个坑的第三个入口(前两个:混用编译器
# 导致链接错误、make SAN=1 不重新插桩 —— 那两个是用 build/<tag>/ 分目录解决的)。
# 这是"make 看不见命令行变量变了"这个坑的第三个入口 (前两个:混用编译器
# 导致链接错误、make SAN=1 不重新插桩 —— 那两个是用 build/<tag>/ 分目录解决的).
#
# 版本号只走 -D,不进任何依赖关系,于是:
# 版本号只走 -D, 不进任何依赖关系, 于是:
# $ make && ./ligent --version # 2027.01.01+...
# $ git tag -d v2027.01.01 # 把 tag 删了
# $ make # "无需做任何事"
# $ ./ligent --version # 还是 2027.01.01 —— **在说谎**
# 二进制看着完全正常,只是版本号是旧的。而这恰恰是 .dirty 标记想避免的那类误导。
# 二进制看着完全正常, 只是版本号是旧的. 而这恰恰是 .dirty 标记想避免的那类误导.
#
# 用一个戳文件记下上次构建用的版本串,对不上就把**我们自己的**对象目录清掉。
# 用一个戳文件记下上次构建用的版本串, 对不上就把**我们自己的**对象目录清掉.
#
# 为什么不用 `make clean`:那个会连 build/static-prefix 和 build/static-src 一起删
# (用户明确要的语义),而重建静态依赖要联网下载 + 编译好几分钟。为了一个版本号
# 字符串付那个代价不合理。所以这里只删自己的产物,精确地清。
# (用户明确要的语义), 而重建静态依赖要联网下载 + 编译好几分钟. 为了一个版本号
# 字符串付那个代价不合理. 所以这里只删自己的产物, 精确地清.
VERSION_STAMP := $(BUILDDIR)/.version-stamp
# 戳文件的内容:版本号 + 哈希 + 构建日期。任意一项变了都要重编 ——
# 比如只切了 tag(版本号变)或只改了工作区(.dirty 出现)。
# 戳文件的内容:把**每一个**注入进二进制的字符串都算进去。任意一项变了都要
# 重编 —— 比如只切了 tag(版本号变)、只改了工作区(.dirty 出现)、只换了
# 分支(branch 变)。少算一项,那一项就会在二进制里过期而 make 毫无察觉,
# 正是这个机制要防的事。
# 戳文件的内容:版本号 + 哈希 + 构建日期. 任意一项变了都要重编 ——
# 比如只切了 tag (版本号变)或只改了工作区 (.dirty 出现).
# 戳文件的内容:把**每一个**注入进二进制的字符串都算进去. 任意一项变了都要
# 重编 —— 比如只切了 tag (版本号变)、只改了工作区 (.dirty 出现)、只换了
# 分支 (branch 变). 少算一项, 那一项就会在二进制里过期而 make 毫无察觉,
# 正是这个机制要防的事.
#
# 注意 BUILD_TIME 不在这里:它每秒都在变,算进去等于每次 make 都全量重编。
# 代价是同一分钟内改了源码、二进制里的时间戳可能停在上一分钟 —— 那无所谓,
# 时间戳是"什么时候编的",不是"源码什么时候改的",而日期(BUILD_DATE)在,
# 粒度够用了。
# 注意 BUILD_TIME 不在这里:它每秒都在变, 算进去等于每次 make 都全量重编.
# 代价是同一分钟内改了源码、二进制里的时间戳可能停在上一分钟 —— 那无所谓,
# 时间戳是"什么时候编的", 不是"源码什么时候改的", 而日期 (BUILD_DATE)在,
# 粒度够用了.
VERSION_FINGERPRINT := $(LIGENT_VERSION)|$(LIGENT_GIT_HASH)|$(LIGENT_GIT_BRANCH)|$(LIGENT_BUILD_DATE)
# 用 $(shell) 在每个 make 里比对一次。
# 用 $(shell) 在每个 make 里比对一次.
# 注意 $(if $(filter-out X,Y),...) 在 Y 为空时**恒为假** —— filter-out 一个
# 空列表得到空列表,什么都过滤不掉。所以这里必须显式处理"戳文件不存在"这一
# 情况(第一次构建就是它),否则戳文件永远写不出来,机制整个失效。
# 空列表得到空列表, 什么都过滤不掉. 所以这里必须显式处理"戳文件不存在"这一
# 情况 (第一次构建就是它), 否则戳文件永远写不出来, 机制整个失效.
CURRENT_STAMP := $(shell cat $(VERSION_STAMP) 2>/dev/null)
ifeq ($(CURRENT_STAMP),)
NEED_VERSION_CLEAN := 1
@@ -122,13 +122,13 @@ NEED_VERSION_CLEAN :=
endif
ifeq ($(NEED_VERSION_CLEAN),1)
# 清掉再让 make 正常评估依赖 —— 这一步发生在**读 Makefile 的时候**,
# 所以后面的 $(OBJS) 会看到"对象不存在",自然全量重编。
# 在 $(shell) 里写文件是安全的:它只在真的需要清的时候跑一次。
# 清掉再让 make 正常评估依赖 —— 这一步发生在**读 Makefile 的时候**,
# 所以后面的 $(OBJS) 会看到"对象不存在", 自然全量重编.
# 在 $(shell) 里写文件是安全的:它只在真的需要清的时候跑一次.
$(shell rm -rf $(OBJDIR) $(BIN) && mkdir -p $(BUILDDIR) && printf '%s' '$(VERSION_FINGERPRINT)' > $(VERSION_STAMP))
endif
# $^ 里既有 .c 也有 .h(后者是为了依赖关系才加上的),真正要编译的只有 .c
# $^ 里既有 .c 也有 .h (后者是为了依赖关系才加上的), 真正要编译的只有 .c
SRCFILES = $(filter-out %.h,$^)
SRCS := $(wildcard $(SRCDIR)/*.c)
@@ -149,13 +149,13 @@ $(OBJDIR):
# 清掉**一切**构建产物:
# build/ 各 <编译器>[-san] 对象目录、build/static、测试可执行文件、
# e2e 的临时文件,以及 tools/build-static-deps.sh 编出来的
# static-prefix(静态库)和 static-src(源码 + 压缩包缓存)
# e2e 的临时文件, 以及 tools/build-static-deps.sh 编出来的
# static-prefix (静态库)和 static-src (源码 + 压缩包缓存)
# ligent / ligent-static
# tests/__pycache__ 跑测试时 python 生成的字节码
#
# 特意**不动** ligent.history:那不是构建产物,是你的输入历史,删了找不回来。
# (e2e 自己写的是 build/e2e.history,在 build/ 里,跟着一起清。)
# 特意**不动** ligent.history:那不是构建产物, 是你的输入历史, 删了找不回来.
# (e2e 自己写的是 build/e2e.history, 在 build/ 里, 跟着一起清. )
clean:
rm -rf $(BUILDDIR) $(BIN) $(BIN)-static tests/__pycache__
@@ -164,56 +164,56 @@ clean:
# ------------------------------------------------------------------ */
#
# 目标是一个**完全静态**的可执行文件:不依赖宿主机的 libc、libcurl、
# OpenSSL。拷到任何同架构的 Linux 上就能跑。
# OpenSSL. 拷到任何同架构的 Linux 上就能跑.
#
# 为什么不能直接拿发行版的 libcurl.a 链接:那是针对 glibc 编的,和 musl 混在
# 一起会得到一堆 "undefined reference to __libc_start_main" 之类。必须有一份
# **用 musl 工具链编出来的**静态 libcurl。造它的办法见 tools/build-static-deps.sh。
# 为什么不能直接拿发行版的 libcurl.a 链接:那是针对 glibc 编的, 和 musl 混在
# 一起会得到一堆 "undefined reference to __libc_start_main" 之类. 必须有一份
# **用 musl 工具链编出来的**静态 libcurl. 造它的办法见 tools/build-static-deps.sh.
#
# 用法:
# ./tools/build-static-deps.sh # 先把依赖编到 build/static-prefix
# make static # 然后链接
#
# 变量(都可覆盖):
# 变量 (都可覆盖):
# STATIC_CC 默认依次找 musl-gcc / x86_64-linux-musl-gcc
# STATIC_PREFIX 依赖所在的前缀(默认 build/static-prefix)
# STATIC_PREFIX 依赖所在的前缀 (默认 build/static-prefix)
STATIC_CC ?= $(shell command -v musl-gcc 2>/dev/null || \
command -v x86_64-linux-musl-gcc 2>/dev/null)
STATIC_PREFIX ?= $(BUILDDIR)/static-prefix
STATIC_BIN := $(BIN)-static
# 静态目标用**独立的对象目录**:那边的 .o 是 musl 编的,和这边的 glibc 目标
# 文件混在一起会互相覆盖,症状是莫名其妙的重链接或不重链接。
# 静态目标用**独立的对象目录**:那边的 .o 是 musl 编的, 和这边的 glibc 目标
# 文件混在一起会互相覆盖, 症状是莫名其妙的重链接或不重链接.
STATIC_OBJDIR := $(BUILDDIR)/static
STATIC_OBJS := $(patsubst $(SRCDIR)/%.c,$(STATIC_OBJDIR)/%.o,$(SRCS))
static-check:
@test -n "$(STATIC_CC)" || { \
echo "找不到 musl 工具链。装一个,或者指定 STATIC_CC:"; \
echo "找不到 musl 工具链. 装一个, 或者指定 STATIC_CC:"; \
echo " Artix/Arch : sudo pacman -S musl # 提供 musl-gcc"; \
echo " 或者用交叉工具链: make static STATIC_CC=x86_64-linux-musl-gcc"; \
exit 1; }
@test -d "$(STATIC_PREFIX)" || { \
echo "找不到静态依赖前缀 $(STATIC_PREFIX)。先跑:"; \
echo "找不到静态依赖前缀 $(STATIC_PREFIX). 先跑:"; \
echo " ./tools/build-static-deps.sh"; \
exit 1; }
@test -x "$(STATIC_PREFIX)/bin/curl-config" || { \
echo "$(STATIC_PREFIX) 里没有 curl-config —— 那份 libcurl 不是在这个"; \
echo "前缀下编的,或者没编完。重跑 ./tools/build-static-deps.sh"; \
echo "前缀下编的, 或者没编完. 重跑 ./tools/build-static-deps.sh"; \
exit 1; }
$(STATIC_OBJDIR):
mkdir -p $(STATIC_OBJDIR)
# -I$(STATIC_PREFIX)/include:musl-gcc **不搜 /usr/include**,系统那份 curl 头
# 也不是给 musl 用的 —— 必须用我们这份 prefix 里的头。
# -I$(STATIC_PREFIX)/include:musl-gcc **不搜 /usr/include**, 系统那份 curl 头
# 也不是给 musl 用的 —— 必须用我们这份 prefix 里的头.
STATIC_CPPFLAGS = -I$(STATIC_PREFIX)/include $(CPPFLAGS)
$(STATIC_OBJDIR)/%.o: $(SRCDIR)/%.c $(HEADERS) | $(STATIC_OBJDIR)
$(STATIC_CC) $(STATIC_CFLAGS) $(STATIC_CPPFLAGS) -c -o $@ $<
# 依赖列表交给 curl 自己报:它最清楚自己链了哪些库、什么顺序。
# 手写一串 -lssl -lcrypto -lz 是会过期的,而且顺序错了链接就失败。
# 依赖列表交给 curl 自己报:它最清楚自己链了哪些库、什么顺序.
# 手写一串 -lssl -lcrypto -lz 是会过期的, 而且顺序错了链接就失败.
static: static-check $(STATIC_OBJS)
@CURL_CONFIG="$(STATIC_PREFIX)/bin/curl-config"; \
LIBS="$$($$CURL_CONFIG --static-libs)" || exit 1; \
@@ -224,37 +224,37 @@ static: static-check $(STATIC_OBJS)
file $(STATIC_BIN) 2>/dev/null || true; \
ldd $(STATIC_BIN) 2>&1 | head -2 || true
# -fno-pie / -no-pie:现代 gcc 默认编 PIE,而静态链接一份非 PIC 的 .a
# (我们那份 libcurl.a 就是 -fno-pie 编的)会报
# "relocation R_X86_64_32 against ... can not be used when making a PIE object"。
# 两头都关掉,省得踩。
# -fno-pie / -no-pie:现代 gcc 默认编 PIE, 而静态链接一份非 PIC 的 .a
# (我们那份 libcurl.a 就是 -fno-pie 编的)会报
# "relocation R_X86_64_32 against ... can not be used when making a PIE object".
# 两头都关掉, 省得踩.
STATIC_CFLAGS ?= $(CFLAGS) -fno-pie
STATIC_LDFLAGS ?= -static -no-pie
# ------------------------------------------------------------------ */
# 静态产物优先体积(make static STATIC_RELEASE=0 关掉) */
# 静态产物优先体积 (make static STATIC_RELEASE=0 关掉) */
# ------------------------------------------------------------------ */
#
# 普通构建的取舍是"能调试",静态产物的取舍是"拷过去就能跑、越小越好",
# 所以这里单独一套开关。实测 6.23 MB → 4.85 MB(-22%),逐项收益:
# 普通构建的取舍是"能调试", 静态产物的取舍是"拷过去就能跑、越小越好",
# 所以这里单独一套开关. 实测 6.23 MB → 4.85 MB (-22%), 逐项收益:
#
# -s 4.92 MB 链接期 strip。**单项最大**:-g 带进来的调试信息
# (551 KB) 加上符号表,一共 1.31 MB —— 占了原产物的
# 五分之一,而静态发布件根本用不上。
# -ffunction-sections 4.91 MB 只对我们自己的 .o 生效,所以几乎没用
# -fdata-sections (收益来自后面 --gc-sections 能按函数/数据丢段;
# 依赖库的 .a 不是这么编的,见下面的"还能再小")
# -fno-*-unwind-tables 4.90 MB .eh_frame 本来 514 KB。C 代码不需要
# C++ 异常和栈回溯。
# -s 4.92 MB 链接期 strip. **单项最大**:-g 带进来的调试信息
# (551 KB) 加上符号表, 一共 1.31 MB —— 占了原产物的
# 五分之一, 而静态发布件根本用不上.
# -ffunction-sections 4.91 MB 只对我们自己的 .o 生效, 所以几乎没用
# -fdata-sections (收益来自后面 --gc-sections 能按函数/数据丢段;
# 依赖库的 .a 不是这么编的, 见下面的"还能再小")
# -fno-*-unwind-tables 4.90 MB .eh_frame 本来 514 KB. C 代码不需要
# C++ 异常和栈回溯.
# -Os 4.86 MB 我们自己的代码优先体积
# -z noseparate-code 4.85 MB 不再为代码/数据段留页对齐空洞
# --build-id=none 少一个 note 段
# -g0 不生成调试信息(否则每个 .o 都白白大一截,
# 编译也慢,反正最后要 strip)
# -g0 不生成调试信息 (否则每个 .o 都白白大一截,
# 编译也慢, 反正最后要 strip)
#
# 还能再小的地方(不在本文件范围):3.49 MB 的 .text 几乎全是 libcurl +
# OpenSSL。要动它得在 tools/build-static-deps.sh 里给依赖也加 -Os 和
# -ffunction-sections,让 --gc-sections 能真正丢进没被引用的 OpenSSL 代码。
# 还能再小的地方 (不在本文件范围):3.49 MB 的 .text 几乎全是 libcurl +
# OpenSSL. 要动它得在 tools/build-static-deps.sh 里给依赖也加 -Os 和
# -ffunction-sections, 让 --gc-sections 能真正丢进没被引用的 OpenSSL 代码.
STATIC_RELEASE ?= 1
ifeq ($(STATIC_RELEASE),1)
STATIC_CFLAGS += -Os -g0 -ffunction-sections -fdata-sections \
@@ -274,25 +274,25 @@ $(OBJDIR)/md_demo: tools/md_demo.c $(SRCDIR)/markdown.c $(SRCDIR)/util.c $(SRCDI
demo: $(OBJDIR)/md_demo
@./$(OBJDIR)/md_demo
# 只编译,不运行
# 只编译, 不运行
md_demo: $(OBJDIR)/md_demo
# ------------------------------------------------------------------
# 测试
# ------------------------------------------------------------------
#
# 测试与演示程序**复用主构建编出来的 .o**(只链它真正需要的那几个模块),而不是
# 每个测试各自把源文件重新编译一遍。
# 测试与演示程序**复用主构建编出来的 .o** (只链它真正需要的那几个模块), 而不是
# 每个测试各自把源文件重新编译一遍.
#
# 以前每条规则的依赖写的是源文件($(SRCDIR)/json.c 之类),recipe 是
# `$(CC) … -o $@ $(SRCFILES)` —— 于是同一个源文件会被编很多遍:只改 util.c 一行,
# 它会按依赖被重编 16 次,实测 `make test-unit` 要 25 秒,而真正的工作量只有
# "编一个 util.o + 重链 17 个二进制"。现在依赖写成 .o:编译一次、链接多次,
# 同样的场景降到几秒。而这些 .o 主构建本来就要用,等于没有额外成本。
# 以前每条规则的依赖写的是源文件 ($(SRCDIR)/json.c 之类), recipe 是
# `$(CC) … -o $@ $(SRCFILES)` —— 于是同一个源文件会被编很多遍:只改 util.c 一行,
# 它会按依赖被重编 16 次, 实测 `make test-unit` 要 25 秒, 而真正的工作量只有
# "编一个 util.o + 重链 17 个二进制". 现在依赖写成 .o:编译一次、链接多次,
# 同样的场景降到几秒. 而这些 .o 主构建本来就要用, 等于没有额外成本.
#
# 代价:测试的依赖里多了一层(.o 是否存在/是否过期由主构建的规则负责),排查
# "为什么这个测试用的还是旧代码"要看 .o 的时间戳而不是源文件。$(SRCFILES) 仍然
# 有意义 —— $^ 里混着 $(HEADERS),而头文件不能上链接行。
# 代价:测试的依赖里多了一层 (.o 是否存在/是否过期由主构建的规则负责), 排查
# "为什么这个测试用的还是旧代码"要看 .o 的时间戳而不是源文件. $(SRCFILES) 仍然
# 有意义 —— $^ 里混着 $(HEADERS), 而头文件不能上链接行.
$(OBJDIR)/test_json: tests/test_json.c $(OBJDIR)/json.o $(OBJDIR)/util.o $(OBJDIR)/i18n.o $(HEADERS) | $(OBJDIR)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(SRCFILES) $(LDLIBS)
@@ -404,17 +404,17 @@ test-unit: $(addprefix $(OBJDIR)/,$(UNIT_TESTS)) $(OBJDIR)/md_demo $(OBJDIR)/fuz
test_i18n) label="界面文案" ;; \
*) label="$$t" ;; \
esac; \
echo "==== $$label 测试($$t)===="; \
echo "==== $$label 测试 ($$t)===="; \
./$(OBJDIR)/$$t || exit 1; \
echo; \
done
@echo "==== 演示程序分片自检 ===="
@./$(OBJDIR)/md_demo --compare < /dev/null
@echo
@echo "==== 颜色审计(只用 16 色内的 SGR) ===="
@echo "==== 颜色审计 (只用 16 色内的 SGR) ===="
@./tools/check-colors.sh
@echo
@echo "==== 模糊测试(每轮固定种子,可复现) ===="
@echo "==== 模糊测试 (每轮固定种子, 可复现) ===="
@./$(OBJDIR)/fuzz 4000
test-e2e: $(BIN)
@@ -424,12 +424,12 @@ test-e2e: $(BIN)
# make check-docs —— README 里写的测试数量必须和现实一致 */
# ------------------------------------------------------------------ */
#
# README 里引用了"831 单元 + 314 e2e"这样的数字。数字会随着加测试而过期,
# 而过期的数字比不写更糟 —— 它是**错的信息**,读的人没有理由怀疑它。
# (这件事就发生过:加完 JSON 的 UTF-8 测试后,README 还写着 797。)
# README 里引用了"831 单元 + 314 e2e"这样的数字. 数字会随着加测试而过期,
# 而过期的数字比不写更糟 —— 它是**错的信息**, 读的人没有理由怀疑它.
# (这件事就发生过:加完 JSON 的 UTF-8 测试后, README 还写着 797. )
#
# 所以把"文档说的"和"实际跑出来的"对一遍。不做成 make test 的一部分:
# 它要跑一遍单元测试来数数,比 test-unit 本身还慢。
# 所以把"文档说的"和"实际跑出来的"对一遍. 不做成 make test 的一部分:
# 它要跑一遍单元测试来数数, 比 test-unit 本身还慢.
check-docs: $(addprefix $(OBJDIR)/,$(UNIT_TESTS)) $(BIN)
@unit=0; \
for t in $(UNIT_TESTS); do \
@@ -437,50 +437,50 @@ check-docs: $(addprefix $(OBJDIR)/,$(UNIT_TESTS)) $(BIN)
unit=$$((unit + $${n:-0})); \
done; \
e2e=$$(bash tests/run_e2e.sh 2>/dev/null | grep -oE '通过 [0-9]+ 项' | grep -oE '[0-9]+' | tail -1); \
unit_doc=$$(grep -oE '([0-9]+ 单元' README.md | grep -oE '[0-9]+' | head -1); \
unit_doc=$$(grep -oE ' ([0-9]+ 单元' README.md | grep -oE '[0-9]+' | head -1); \
unit_docs=$$(grep -oE '[0-9]+ 个可执行、[0-9]+ 项' README.md | grep -oE '[0-9]+ 项' | grep -oE '[0-9]+' | head -1); \
e2e_doc=$$(grep -oE '[0-9]+ e2e' README.md | grep -oE '[0-9]+' | head -1); \
bad=0; \
if [ "$$unit" != "$$unit_doc" ]; then \
echo " ✗ 单元测试:README 说 $$unit_doc,实际 $$unit"; bad=1; \
else echo " ✓ 单元测试 $$unit 项,与 README 一致"; fi; \
echo " ✗ 单元测试:README 说 $$unit_doc, 实际 $$unit"; bad=1; \
else echo " ✓ 单元测试 $$unit 项, 与 README 一致"; fi; \
if [ "$$unit" != "$$unit_docs" ]; then \
echo " ✗ SAN 那节:README 说 $$unit_docs,实际 $$unit"; bad=1; \
echo " ✗ SAN 那节:README 说 $$unit_docs, 实际 $$unit"; bad=1; \
else echo " ✓ SAN 那节的数量一致"; fi; \
if [ "$$e2e" != "$$e2e_doc" ]; then \
echo " ✗ e2e:README 说 $$e2e_doc,实际 $$e2e"; bad=1; \
else echo " ✓ e2e $$e2e 项,与 README 一致"; fi; \
if [ $$bad -eq 0 ]; then echo "文档里的数量与现实一致。"; \
else echo "README 里的数字已过期,请更新(数字对不上就是错的信息)。"; exit 1; fi
echo " ✗ e2e:README 说 $$e2e_doc, 实际 $$e2e"; bad=1; \
else echo " ✓ e2e $$e2e 项, 与 README 一致"; fi; \
if [ $$bad -eq 0 ]; then echo "文档里的数量与现实一致. "; \
else echo "README 里的数字已过期, 请更新 (数字对不上就是错的信息). "; exit 1; fi
# 拿同一套 e2e 打静态产物。以前"静态构建可用"实际上只等于"它能启动" ——
# 依赖脚本的自检只做 curl_easy_init + setopt,连一个请求都不发;现在这 300
# 多项跑一遍,才真的说明它是可用的。
# 单独一个目标、不进 make test:要先编依赖,慢。
# 拿同一套 e2e 打静态产物. 以前"静态构建可用"实际上只等于"它能启动" ——
# 依赖脚本的自检只做 curl_easy_init + setopt, 连一个请求都不发; 现在这 300
# 多项跑一遍, 才真的说明它是可用的.
# 单独一个目标、不进 make test:要先编依赖, 慢.
static-test: static
@BIN="$(abspath $(STATIC_BIN))" bash tests/run_e2e.sh
# ------------------------------------------------------------------ */
# 容器里构建静态产物(CI/CD 用) */
# 容器里构建静态产物 (CI/CD 用) */
# ------------------------------------------------------------------ */
#
# podman 和 docker 命令行兼容,默认用 podman(这个项目在 rootless 环境下开发)。
# 用 CONTAINER=... 换。
# podman 和 docker 命令行兼容, 默认用 podman (这个项目在 rootless 环境下开发).
# 用 CONTAINER=... 换.
CONTAINER ?= podman
CONTAINER_IMAGE ?= ligent-static
# 版本号显式传给容器:容器里没有 .git,get-version.sh 只能回退到源码 mtime,
# 那得到的是"文件日期"而不是"tag 日期"。这里在本机算好传进去。
# GIT_HASH / GIT_BRANCH 同理 —— 二进制里的分支名应当反映**源码来自哪个分支**,
# 而不是"在容器里看不到分支所以是 unknown"。$(VERSION_SCRIPT) 见上面。
# 版本号显式传给容器:容器里没有 .git, get-version.sh 只能回退到源码 mtime,
# 那得到的是"文件日期"而不是"tag 日期". 这里在本机算好传进去.
# GIT_HASH / GIT_BRANCH 同理 —— 二进制里的分支名应当反映**源码来自哪个分支**,
# 而不是"在容器里看不到分支所以是 unknown". $(VERSION_SCRIPT) 见上面.
CONTAINER_VERSION := $(LIGENT_VERSION)
CONTAINER_GIT_HASH := $(LIGENT_GIT_HASH)
CONTAINER_GIT_BRANCH := $(LIGENT_GIT_BRANCH)
# 传**完整时间戳**而不是只有日期:Dockerfile 里拿它当 SOURCE_DATE_EPOCH,
# 传**完整时间戳**而不是只有日期:Dockerfile 里拿它当 SOURCE_DATE_EPOCH,
# 而 "2026-09-22" 被 date(1) 解析出来是那天的 00:00:00 UTC —— 于是容器产物
# 的 --version 里时间永远显示午夜。传秒数就没有这个歧义,而且仍然可复现。
# 设了 SOURCE_DATE_EPOCH 的话就沿用它(那正是"可复现构建"要求的:同一份
# 源码无论何时构建都得到同样的时间戳)。
# 的 --version 里时间永远显示午夜. 传秒数就没有这个歧义, 而且仍然可复现.
# 设了 SOURCE_DATE_EPOCH 的话就沿用它 (那正是"可复现构建"要求的:同一份
# 源码无论何时构建都得到同样的时间戳).
CONTAINER_BUILD_EPOCH := $(if $(SOURCE_DATE_EPOCH),$(SOURCE_DATE_EPOCH),$(shell date -u +%s))
docker-build:
@@ -491,9 +491,9 @@ docker-build:
--build-arg BUILD_EPOCH='$(CONTAINER_BUILD_EPOCH)' \
-t $(CONTAINER_IMAGE) .
# 把产物从镜像里拷出来。用 create+cp+rm 而不是 build 到本地:
# 镜像是分发单位,本地文件是给人直接用的,两者都要,但不必让 Dockerfile
# 同时干这两件事(那需要挂载点或者 --output,podman/docker 的支持还不齐)。
# 把产物从镜像里拷出来. 用 create+cp+rm 而不是 build 到本地:
# 镜像是分发单位, 本地文件是给人直接用的, 两者都要, 但不必让 Dockerfile
# 同时干这两件事 (那需要挂载点或者 --output, podman/docker 的支持还不齐).
docker-extract: docker-build
@$(CONTAINER) rm -f $(CONTAINER_IMAGE)-tmp >/dev/null 2>&1 || true
@$(CONTAINER) create --name $(CONTAINER_IMAGE)-tmp $(CONTAINER_IMAGE) >/dev/null
@@ -502,82 +502,82 @@ docker-extract: docker-build
@echo " 产物: $(STATIC_BIN) ($$(stat -c%s $(STATIC_BIN) 2>/dev/null || echo '?') 字节)"
@file $(STATIC_BIN) 2>/dev/null || true
# 容器产物也要过一遍 e2e —— 和 static-test 一个道理:能跑起来不等于能用。
# 容器产物也要过一遍 e2e —— 和 static-test 一个道理:能跑起来不等于能用.
docker-test: docker-extract
@BIN="$(abspath $(STATIC_BIN))" bash tests/run_e2e.sh
docker-clean:
-@$(CONTAINER) rmi -f $(CONTAINER_IMAGE) >/dev/null 2>&1 || true
# 对抗性模糊测试:单独一个目标,跑得久一些
# 对抗性模糊测试:单独一个目标,轮数比 test-unit 里那个底线大得多
# 对抗性模糊测试:单独一个目标, 跑得久一些
# 对抗性模糊测试:单独一个目标, 轮数比 test-unit 里那个底线大得多
test-fuzz: $(OBJDIR)/fuzz
@echo "==== 模糊测试(20 万轮)===="
@echo "==== 模糊测试 (20 万轮)===="
@./$(OBJDIR)/fuzz 200000
# `make -j test` 会让单元测试与 e2e 并行,输出互相穿插、也互相抢资源 ——
# 这两个目标内部本来就是逐条跑的,没有并行的必要。
# `make -j test` 会让单元测试与 e2e 并行, 输出互相穿插、也互相抢资源 ——
# 这两个目标内部本来就是逐条跑的, 没有并行的必要.
.NOTPARALLEL: test test-unit test-e2e
test: test-unit test-e2e
@echo
@echo "全部测试通过。"
@echo "全部测试通过. "
# 依赖统一由 $(HEADERS) 管:每个 .o 和每个测试都以全部头文件为依赖。
# 不精确,但一共只有十几个文件、全量重编几秒钟,换来的是「永远不会漏」。
# 原先这里还有一段手写依赖表,既冗余又不完整(上次 agent.h 漏更新就是它
# 没跟上),已删。
# 依赖统一由 $(HEADERS) 管:每个 .o 和每个测试都以全部头文件为依赖.
# 不精确, 但一共只有十几个文件、全量重编几秒钟, 换来的是「永远不会漏」.
# 原先这里还有一段手写依赖表, 既冗余又不完整 (上次 agent.h 漏更新就是它
# 没跟上), 已删.
$(OBJDIR)/reduce: tests/reduce.c $(OBJDIR)/markdown.o $(OBJDIR)/util.o \
$(OBJDIR)/i18n.o | $(OBJDIR)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(SRCFILES) $(LDLIBS)
# ------------------------------------------------------------------ */
# make analyze —— gcc 静态分析器(-fanalyzer) */
# make analyze —— gcc 静态分析器 (-fanalyzer) */
# ------------------------------------------------------------------ */
#
# 它抓的是单靠警告看不出来的那类问题:读未初始化的值、空指针、资源泄漏。
# 第一次跑就捞出两个真 bug(attempt() 里 curl_easy_init 失败那条提前返回
# 会把未初始化的 cerr 当字符串读;POST 分支对 body==NULL 不设防)。
# 它抓的是单靠警告看不出来的那类问题:读未初始化的值、空指针、资源泄漏.
# 第一次跑就捞出两个真 bug (attempt() 里 curl_easy_init 失败那条提前返回
# 会把未初始化的 cerr 当字符串读; POST 分支对 body==NULL 不设防).
#
# 用 -O0 而不是平时的 -O2:开了优化之后 gcc 会内联,而 -fanalyzer 的跨函数
# 用 -O0 而不是平时的 -O2:开了优化之后 gcc 会内联, 而 -fanalyzer 的跨函数
# 状态在遇到内联时并不可靠 —— image.c 里的 rand_suffix 在 -O1/-O2/-Os 下会
# 报一条 "leak of FILE 'f'",而 -O0 下干净,fclose 也确实就写在 fread 下一行。
# 分析器自己的分析是跨函数的,不依赖 -O 级别,所以 -O0 不会少查什么。
# gcc 文档同样建议 -fanalyzer 配 -O0。
# 报一条 "leak of FILE 'f'", 而 -O0 下干净, fclose 也确实就写在 fread 下一行.
# 分析器自己的分析是跨函数的, 不依赖 -O 级别, 所以 -O0 不会少查什么.
# gcc 文档同样建议 -fanalyzer 配 -O0.
#
# 单独一个目标、不进 make test:它比整个测试套件加起来还慢。
# 单独一个目标、不进 make test:它比整个测试套件加起来还慢.
analyze:
@fail=0; \
for f in $(SRCS); do \
$(CC) $(CFLAGS) -O0 -fanalyzer -Werror $(CPPFLAGS) -c "$$f" -o /dev/null || fail=1; \
done; \
if [ $$fail -eq 0 ]; then \
echo "静态分析通过:无告警。"; \
echo "静态分析通过:无告警. "; \
else \
echo "静态分析发现问题(上面的 warning: 就是原因)。"; exit 1; \
echo "静态分析发现问题 (上面的 warning: 就是原因). "; exit 1; \
fi
# ------------------------------------------------------------------ */
# 为什么对象目录要带标签(build/gcc、build/clang、build/gcc-san …) */
# 为什么对象目录要带标签 (build/gcc、build/clang、build/gcc-san …) */
# ------------------------------------------------------------------ */
#
# make 只按"源文件比目标文件新"决定要不要重编,它**看不见命令行变量变了**。
# make 只按"源文件比目标文件新"决定要不要重编, 它**看不见命令行变量变了**.
# 于是有两类事故:
#
# 1) make CC=tcc 之后再 make CC=clang —— 只有被 touch 过的那几个 .o 是 clang
# 编的,其余还是 tcc 编的。tcc 的目标文件要 tcc 自己的运行时辅助例程
# (__floatundidf 之类),拿 clang 去链接就是一堆 undefined reference。
# 我自己的构建就是这么炸的,而且第一次还因为只用 ls 看文件大小而误判成
# "clang 编译通过"。
# 编的, 其余还是 tcc 编的. tcc 的目标文件要 tcc 自己的运行时辅助例程
# (__floatundidf 之类), 拿 clang 去链接就是一堆 undefined reference.
# 我自己的构建就是这么炸的, 而且第一次还因为只用 ls 看文件大小而误判成
# "clang 编译通过".
#
# 2) 更要命的一类:make 之后再 make SAN=1 —— 一个 .o 都不会重编。
# 链接行带着 -fsanitize=...,ASan 运行时也进了二进制,于是它**看起来**
# 是插桩过的;实际 nm 里一个 __asan/__ubsan 符号都没有,什么都不会检测。
# 这意味着"没先 make clean 的 ASan 结论全是假绿灯"。
# 2) 更要命的一类:make 之后再 make SAN=1 —— 一个 .o 都不会重编.
# 链接行带着 -fsanitize=..., ASan 运行时也进了二进制, 于是它**看起来**
# 是插桩过的; 实际 nm 里一个 __asan/__ubsan 符号都没有, 什么都不会检测.
# 这意味着"没先 make clean 的 ASan 结论全是假绿灯".
#
# 把标签写进路径之后这两种混用都不可能发生了:配置一变,对应的目录就是空的,
# 该编的自然会全编。全量重编只要几秒,值得。
# 把标签写进路径之后这两种混用都不可能发生了:配置一变, 对应的目录就是空的,
# 该编的自然会全编. 全量重编只要几秒, 值得.
#
# 注意 STATIC_PREFIX / STATIC_OBJDIR 挂在 $(BUILDDIR) 上而不是 $(OBJDIR) 上:
# build/static-prefix 是 tools/build-static-deps.sh 写死的位置,不能被标签带跑。
# build/static-prefix 是 tools/build-static-deps.sh 写死的位置, 不能被标签带跑.