init
This commit is contained in:
commit
e54390a9f8
85 files changed
+29923
No files matched your search
Executable
+363
@@ -0,0 +1,363 @@
|
||||
#!/bin/sh
|
||||
# build-static-deps.sh - 造一份"musl 工具链编出来的静态 libcurl",给 make static 用。
|
||||
#
|
||||
# 为什么需要它:发行版的 libcurl.a 是针对 glibc 编的,跟 musl 混link会得到一堆
|
||||
# "undefined reference to __libc_start_main"。所以必须自己用 musl 工具链把
|
||||
# libcurl 和它的依赖(zlib、OpenSSL)编一遍。
|
||||
#
|
||||
# 产物在 build/static-prefix/,之后 `make static` 就能链了。
|
||||
#
|
||||
# 用法:
|
||||
# ./tools/build-static-deps.sh # 用默认版本
|
||||
# ZLIB=1.3.1 OPENSSL=3.0.15 CURL=8.11.1 ./tools/build-static-deps.sh
|
||||
# CC=x86_64-linux-musl-gcc ./tools/build-static-deps.sh
|
||||
# PREFIX=/opt/musl-static ./tools/build-static-deps.sh
|
||||
#
|
||||
# 需要: musl 工具链、make、perl(OpenSSL 的 Configure 是 perl 写的)、以及
|
||||
# **能联网**(要下载三个源码包)。构建大约几分钟。
|
||||
set -eu
|
||||
|
||||
ZLIB_VER=${ZLIB:-1.3.1}
|
||||
OPENSSL_VER=${OPENSSL:-3.0.15}
|
||||
CURL_VER=${CURL:-8.11.1}
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
PREFIX=${PREFIX:-$ROOT/build/static-prefix}
|
||||
SRC=${SRC:-$ROOT/build/static-src}
|
||||
JOBS=${JOBS:-$( (nproc 2>/dev/null) || echo 2 )}
|
||||
|
||||
# musl-gcc 是包装脚本;交叉工具链要自己指定
|
||||
if [ -z "${CC:-}" ]; then
|
||||
if command -v musl-gcc >/dev/null 2>&1; then
|
||||
CC=musl-gcc
|
||||
elif command -v x86_64-linux-musl-gcc >/dev/null 2>&1; then
|
||||
CC=x86_64-linux-musl-gcc
|
||||
else
|
||||
echo "找不到 musl 工具链。" >&2
|
||||
echo " Artix/Arch: sudo pacman -S musl # 提供 musl-gcc" >&2
|
||||
echo " 或指定: CC=x86_64-linux-musl-gcc $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
need() {
|
||||
command -v "$1" >/dev/null 2>&1 || { echo "缺少 $1,装一下" >&2; exit 1; }
|
||||
}
|
||||
need make
|
||||
need "$CC"
|
||||
need perl # OpenSSL 的 Configure
|
||||
# 下载靠它。以前这里写的是 `need curl || true` —— 那是**假的容错**:need 失败时
|
||||
# 直接 exit,`|| true` 根本执行不到(那句注释"下面会再确认"也不成立)。于是缺
|
||||
# curl 时脚本会一路跑到 fetch,最后报"所有候选地址都不可用 / 要么是没网,要么
|
||||
# 是版本号写错了",把人引去查网络和版本号 —— 而真正的原因是工具没装。
|
||||
need curl
|
||||
need tar
|
||||
|
||||
# ---------------------------------------------------------------- 架构
|
||||
# OpenSSL 的 Configure 要一个明确的 target 名,它**不做**自动探测。以前这里
|
||||
# 写死 linux-x86_64,于是在 aarch64/riscv64 上会去编 x86 的汇编,直接失败 ——
|
||||
# 而这个脚本的产物恰恰是"拷到别的机器也能跑"的静态件。
|
||||
# 认不出来时**不猜**:猜错的后果是编出一堆看着像成功、其实不对的东西。
|
||||
OPENSSL_TARGET=${OPENSSL_TARGET:-}
|
||||
if [ -z "$OPENSSL_TARGET" ]; then
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) OPENSSL_TARGET=linux-x86_64 ;;
|
||||
i?86) OPENSSL_TARGET=linux-elf ;;
|
||||
aarch64|arm64) OPENSSL_TARGET=linux-aarch64 ;;
|
||||
armv7l|armv6l) OPENSSL_TARGET=linux-armv4 ;;
|
||||
riscv64) OPENSSL_TARGET=linux64-riscv64 ;;
|
||||
ppc64le) OPENSSL_TARGET=linux-ppc64le ;;
|
||||
s390x) OPENSSL_TARGET=linux64-s390x ;;
|
||||
loongarch64) OPENSSL_TARGET=linux64-loongarch64 ;; # 3.1+ 才有
|
||||
*)
|
||||
echo "认不出这台机器的架构($(uname -m)),而 OpenSSL 的 Configure" >&2
|
||||
echo "需要一个明确的 target 名。请显式指定,例如:" >&2
|
||||
echo " OPENSSL_TARGET=linux-aarch64 $0" >&2
|
||||
echo "(可用的 target 见 OpenSSL 源码里的 Configurations/10-main.conf)" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 刻意**不带** -fno-pie。依赖库编成默认的 PIE 兼容代码就行 —— PIC 代码放进
|
||||
# 非 PIE 可执行文件是合法的,最后那一步由 ligent 自己用 -no-pie 决定。
|
||||
# 反过来(依赖 -fno-pie、链接却不带 -no-pie)会立刻炸:这些项目在 make 里
|
||||
# 还会顺手链几个测试程序,而那些链接用的是默认的 PIE,于是
|
||||
# relocation R_X86_64_32 against ... can not be used when making a PIE object
|
||||
# 而且 zlib 的 configure 探测 vsnprintf 也会因此失败,悄悄退化成不安全的
|
||||
# vsprintf 版本(实测踩过)。
|
||||
# 体积优先。静态产物是"拷过去就能跑"的发布件,不是拿来调试的,所以这里的
|
||||
# 取舍和普通构建相反 —— 目标是越小越好。三组开关要一起上才有意义:
|
||||
# -Os 别做以体积换速度的展开/内联
|
||||
# -ffunction-sections 依赖库也按函数分段,这样最后链接时
|
||||
# -fdata-sections --gc-sections 才**丢得掉**没被引用的部分
|
||||
# -fno-*-unwind-tables C 代码不需要 C++ 异常和栈回溯,.eh_frame 全省
|
||||
# 实测(连同 Makefile 那侧的 strip 等一起):6.24 MB → 3.29 MB。
|
||||
# 其中这一半的功劳很大:libcurl+OpenSSL 的 .text 从 3.45 MB 掉到 2.41 MB ——
|
||||
# 静态归档里本来就有一大堆我们永远走不到的算法和协议实现。
|
||||
# 想回到调试友好(-O2、保留 unwind 表):CFLAGS_COMMON="-O2 $0"
|
||||
CFLAGS_COMMON=${CFLAGS_COMMON:-"-Os -ffunction-sections -fdata-sections -fno-asynchronous-unwind-tables -fno-unwind-tables"}
|
||||
|
||||
# musl-gcc 只搜 /usr/lib/musl/include,**不搜 /usr/include**,于是内核的 UAPI
|
||||
# 头(linux/mman.h、asm/*.h 这些)找不到 —— OpenSSL 就死在这一步:
|
||||
# fatal error: linux/mman.h: No such file or directory
|
||||
# 用 -idirafter:musl 自己的头优先,/usr/include 只在 musl 没有时兜底。
|
||||
# 直接写 -I 会让 glibc 的头盖过 musl 的,那是另一个坑。
|
||||
UAPI_INC="-idirafter /usr/include"
|
||||
|
||||
# ---- configure 参数(抽出来是为了进 WANT,见下面的 stamp 机制)----
|
||||
#
|
||||
# 以前这些参数直接写在各自的 configure 调用处,于是"改了参数"这件事**只有人
|
||||
# 记得把 BUILD_DEFS 加一**才能被发现;忘了就继续用旧参数编出来的库,症状是
|
||||
# "我明明加了个 --disable-x,怎么产物一点没变"。
|
||||
|
||||
# zlib 只要 --static:我们要的是 libz.a
|
||||
ZLIB_CONFIGURE_ARGS="--static"
|
||||
|
||||
# OpenSSL:no-shared 只出 .a;no-tests 少编一堆测试程序(省几分钟);
|
||||
# no-legacy 去掉已淘汰的算法(体积)。
|
||||
OPENSSL_CONFIGURE_ARGS="no-shared no-tests no-legacy"
|
||||
|
||||
# curl:ligent 只用 chat/completions 一个端点,所以只要能发 HTTPS 的那部分。
|
||||
# --disable-* FTP/邮件/字典/… 这些协议编进来纯属占地
|
||||
# --without-* 静态链接下每个"可选增强"都会再拖一串依赖进来(libpsl、idn2、
|
||||
# brotli、zstd、nghttp2/3…),而 ligent 一个都不用
|
||||
# --with-ca-* 把 CA 路径编进去 —— 静态件跑到别的机器上时得知道去哪找根证书
|
||||
CURL_CONFIGURE_ARGS="
|
||||
--disable-shared --enable-static
|
||||
--with-openssl=$PREFIX --with-zlib=$PREFIX
|
||||
--disable-ldap --disable-ldaps --disable-rtsp --disable-dict
|
||||
--disable-telnet --disable-tftp --disable-pop3 --disable-imap
|
||||
--disable-smtp --disable-gopher --disable-mqtt --disable-manual
|
||||
--without-libpsl --without-libidn2 --without-brotli --without-zstd
|
||||
--without-nghttp2 --without-ngtcp2 --without-nghttp3
|
||||
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt
|
||||
--with-ca-path=/etc/ssl/certs"
|
||||
|
||||
mkdir -p "$SRC" "$PREFIX"
|
||||
|
||||
# 编译选项变了就得把依赖整个重建。否则下面每个守卫都会看到 libz.a 已存在而
|
||||
# 直接跳过,于是你一直用着旧选项编出来的库,而且**看不出来** —— 症状是
|
||||
# "我明明加了 -Os,怎么产物一点没小"。
|
||||
# 这和 Makefile 里"对象目录按编译器/插桩分标签"是同一个坑的两处:make 和这个
|
||||
# 脚本都只看文件在不在,看不见命令行变了。
|
||||
STAMP="$PREFIX/.build-flags"
|
||||
# BUILD_DEFS:配方版本号,留给"改了脚本逻辑、但 WANT 里的文本一个字都没变"的
|
||||
# 改动(比如换了一种绕坑的处理方式)。凡是会改变产物内容的改动都要加一。
|
||||
#
|
||||
# 注意它现在**不再是唯一的触发源**:编译选项、三个版本号、OpenSSL target、
|
||||
# 每个库的 configure 参数都直接进了下面的 WANT,改它们会自动触发重建,不需要
|
||||
# 谁记得来这里加一 —— 以前 configure 参数不在 WANT 里,全靠这个手写数字兜着,
|
||||
# 而人是会忘的(忘了就是"一直用旧参数编出来的库,看不出来")。
|
||||
# 已有的历史值:1 = 加了体积优先的 CFLAGS;2 = configure 期间藏起 .pc 文件;
|
||||
# 3 = 架构与 configure 参数进指纹(本版)。
|
||||
BUILD_DEFS=3
|
||||
|
||||
# 多行参数折叠成一行:好塞进戳文件,也好肉眼比对
|
||||
flatten() {
|
||||
printf '%s' "$1" | tr -s '[:space:]' ' ' | sed 's/^ *//; s/ *$//'
|
||||
}
|
||||
|
||||
WANT="defs=$BUILD_DEFS | CC=$CC | zlib=$ZLIB_VER openssl=$OPENSSL_VER curl=$CURL_VER \
|
||||
| target=$OPENSSL_TARGET | cflags=$CFLAGS_COMMON | uapi=$UAPI_INC \
|
||||
| zlib:[$(flatten "$ZLIB_CONFIGURE_ARGS")] \
|
||||
| openssl:[$(flatten "$OPENSSL_CONFIGURE_ARGS")] \
|
||||
| curl:[$(flatten "$CURL_CONFIGURE_ARGS")]"
|
||||
if [ ! -f "$STAMP" ] && [ -f "$PREFIX/lib/libz.a" ]; then
|
||||
# 有库却没有选项记录 —— 那是**旧版脚本**编的,不知道用的什么选项。
|
||||
# 这一条不能省:没有它,下面的判断会认为"stamp 不存在 = 不用重建",
|
||||
# 然后我们就给旧库写上一个声称是新选项的 stamp,把它洗白成"新的"。
|
||||
echo "== 这个前缀没有编译选项记录(旧版脚本编的),重建以确认选项一致"
|
||||
rm -rf "$PREFIX"
|
||||
mkdir -p "$PREFIX"
|
||||
elif [ -f "$STAMP" ] && [ "$(cat "$STAMP" 2>/dev/null)" != "$WANT" ]; then
|
||||
echo "== 编译选项变了,重建依赖"
|
||||
echo " 原: $(cat "$STAMP")"
|
||||
echo " 新: $WANT"
|
||||
rm -rf "$PREFIX"
|
||||
mkdir -p "$PREFIX"
|
||||
fi
|
||||
printf '%s' "$WANT" > "$STAMP"
|
||||
|
||||
# ---- 被打断时把藏起来的 .pc 放回去 ----
|
||||
# curl 那一段会在 configure 期间把 pkgconfig 目录藏起来(原因见下面那段长注释)。
|
||||
# 那一步要几分钟,中途 Ctrl-C 很常见 —— 不兜住的话,用户的 openssl.pc 就会在这
|
||||
# 段时间里"消失"(他编别的东西时会找不到)。下一轮跑本脚本虽然会自愈,但空窗期
|
||||
# 是真实存在的,而且没人知道自己在等什么。
|
||||
PCDIR="$PREFIX/lib/pkgconfig"
|
||||
PCHIDE="$PREFIX/lib/pkgconfig.hidden"
|
||||
restore_pc() {
|
||||
if [ -d "$PCHIDE" ] && [ ! -d "$PCDIR" ]; then
|
||||
echo "== 把 pkgconfig 目录放回原位(上一次跑本脚本被打断过)" >&2
|
||||
mv "$PCHIDE" "$PCDIR"
|
||||
fi
|
||||
}
|
||||
trap 'restore_pc' EXIT INT TERM HUP
|
||||
|
||||
# fetch <输出文件> <url...> —— 多个地址依次试。
|
||||
# 各家下载站的目录结构会变(zlib 就把旧版本挪到了 /fossils/),写死一个地址
|
||||
# 迟早会 404,而那时的报错是 curl 的一句 "error: 404",看不出该改哪。
|
||||
fetch() {
|
||||
out=$1; shift
|
||||
[ -f "$out" ] && { echo " 已有 $(basename "$out"),跳过下载"; return; }
|
||||
for url in "$@"; do
|
||||
echo " 下载 $url"
|
||||
if curl -fL --retry 3 --connect-timeout 20 -o "$out.part" "$url"; then
|
||||
mv "$out.part" "$out"
|
||||
return 0
|
||||
fi
|
||||
echo " 这个地址不行,换下一个"
|
||||
rm -f "$out.part"
|
||||
done
|
||||
echo " 下载 $(basename "$out") 失败:所有候选地址都不可用。" >&2
|
||||
echo " 要么是没网,要么是版本号写错了。可以指定版本重来,例如:" >&2
|
||||
echo " ZLIB=1.3.1 OPENSSL=3.5.0 CURL=8.11.1 $0" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "== 目标前缀: $PREFIX"
|
||||
echo "== 工具链 : $CC ($($CC --version 2>&1 | head -1))"
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------------- zlib
|
||||
if [ ! -f "$PREFIX/lib/libz.a" ]; then
|
||||
echo "== 编 zlib $ZLIB_VER"
|
||||
fetch "$SRC/zlib-$ZLIB_VER.tar.gz" \
|
||||
"https://zlib.net/zlib-$ZLIB_VER.tar.gz" \
|
||||
"https://zlib.net/fossils/zlib-$ZLIB_VER.tar.gz" \
|
||||
"https://github.com/madler/zlib/releases/download/v$ZLIB_VER/zlib-$ZLIB_VER.tar.gz"
|
||||
rm -rf "$SRC/zlib-$ZLIB_VER"
|
||||
tar -xzf "$SRC/zlib-$ZLIB_VER.tar.gz" -C "$SRC"
|
||||
( cd "$SRC/zlib-$ZLIB_VER" &&
|
||||
CC="$CC" CFLAGS="$CFLAGS_COMMON" \
|
||||
./configure $ZLIB_CONFIGURE_ARGS --prefix="$PREFIX" &&
|
||||
make -j"$JOBS" libz.a && make install )
|
||||
else
|
||||
echo "== zlib 已就绪"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------- OpenSSL
|
||||
# curl 要 HTTPS 就得有个 TLS 后端。选 OpenSSL 是因为它最普遍;想更小的话
|
||||
# 可以换 mbedTLS(configure 里把 --with-openssl 换成 --with-mbedtls)。
|
||||
# 守卫必须看 OpenSSL **实际**装到哪。x86_64 上 OpenSSL 3 默认装 lib64,
|
||||
# 而下面 --libdir=lib 把它拉回 lib —— 只查 lib 就会每次重编 OpenSSL(几分钟)。
|
||||
if [ ! -f "$PREFIX/lib/libssl.a" ]; then
|
||||
echo
|
||||
echo "== 编 OpenSSL $OPENSSL_VER(这一步最慢)"
|
||||
fetch "$SRC/openssl-$OPENSSL_VER.tar.gz" \
|
||||
"https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz" \
|
||||
"https://github.com/openssl/openssl/releases/download/openssl-$OPENSSL_VER/openssl-$OPENSSL_VER.tar.gz"
|
||||
rm -rf "$SRC/openssl-$OPENSSL_VER"
|
||||
tar -xzf "$SRC/openssl-$OPENSSL_VER.tar.gz" -C "$SRC"
|
||||
( cd "$SRC/openssl-$OPENSSL_VER" &&
|
||||
CC="$CC" ./Configure "$OPENSSL_TARGET" $OPENSSL_CONFIGURE_ARGS \
|
||||
--prefix="$PREFIX" --openssldir="$PREFIX/ssl" --libdir=lib \
|
||||
$CFLAGS_COMMON "$UAPI_INC" &&
|
||||
make -j"$JOBS" && make install_sw )
|
||||
else
|
||||
echo "== OpenSSL 已就绪"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- curl
|
||||
if [ ! -x "$PREFIX/bin/curl-config" ]; then
|
||||
echo
|
||||
echo "== 编 curl $CURL_VER"
|
||||
fetch "$SRC/curl-$CURL_VER.tar.gz" \
|
||||
"https://curl.se/download/curl-$CURL_VER.tar.gz" \
|
||||
"https://github.com/curl/curl/releases/download/curl-$CURL_VER/curl-$CURL_VER.tar.gz"
|
||||
rm -rf "$SRC/curl-$CURL_VER"
|
||||
tar -xzf "$SRC/curl-$CURL_VER.tar.gz" -C "$SRC"
|
||||
|
||||
# ---- configure 期间把 .pc 文件藏起来,别让它走 pkg-config ----
|
||||
#
|
||||
# curl 的 configure 是这么挑 OpenSSL 的(--with-openssl 给的是自定义前缀):
|
||||
# OPENSSL_PCDIR="$OPT_OPENSSL/lib/pkgconfig"
|
||||
# if test -f "$OPENSSL_PCDIR/openssl.pc"; then PKGTEST="yes" # 用 pkg-config
|
||||
# ...
|
||||
# if test "$PKGTEST" != "yes"; then
|
||||
# SSL_LDFLAGS="-L$PREFIX_OPENSSL/lib" # 直接给目录,路径原样
|
||||
#
|
||||
# 走 pkg-config 那条路会出事:configure 开头就 `LC_ALL=C; export LC_ALL`
|
||||
# (autoconf 的标准开头,脚本外面设 LC_ALL 压不住它),而 pkg-config 在
|
||||
# C locale 下用 isprint() 判断字节要不要转义 —— 每个 >= 0x80 的字节都被判成
|
||||
# "不可打印",于是它给路径里每个非 ASCII 字节前面加个反斜杠:
|
||||
# -L/…/开发/… -> -L/…/\开\发/…
|
||||
# \ 后面跟 E5 不是合法续字节,所以那串是**非法 UTF-8**。curl 拿到就写进
|
||||
# curl-config、libcurl.pc 和一堆 Makefile(实测 26 个文件)。
|
||||
#
|
||||
# 后果不只是难看:之后 `make static` 里 `curl-config --static-libs` 一打印,
|
||||
# 坏字节就顺着工具输出进了对话历史,服务端会以
|
||||
# invalid unicode code point
|
||||
# 拒掉**整个**请求,而且坏数据已在历史里,之后每次请求都失败。
|
||||
#
|
||||
# 本仓库路径带中文,所以必中;纯 ASCII 路径的人永远碰不到。
|
||||
# 让 .pc 暂时消失,curl 就退回上面那条"直接给目录"的路,路径干干净净。
|
||||
# 藏而不是删:这些 .pc 是 OpenSSL/zlib 正常安装的一部分,别人可能要用。
|
||||
# PCDIR/PCHIDE 和恢复用的 trap 在脚本开头(restore_pc):这几步要几分钟,
|
||||
# 中途 Ctrl-C 很常见,不能让 .pc 在没人知道的情况下"消失"一段。
|
||||
restore_pc # 上一次被打断留下的隐藏目录,先放回去
|
||||
if [ -d "$PCDIR" ]; then mv "$PCDIR" "$PCHIDE"; fi
|
||||
|
||||
rc=0
|
||||
# shellcheck disable=SC2086 # configure 参数就该按空白摊开成多个词
|
||||
( cd "$SRC/curl-$CURL_VER" &&
|
||||
CC="$CC" CFLAGS="$CFLAGS_COMMON" \
|
||||
CPPFLAGS="-I$PREFIX/include $UAPI_INC" LDFLAGS="-L$PREFIX/lib" \
|
||||
./configure --prefix="$PREFIX" $CURL_CONFIGURE_ARGS \
|
||||
&& make -j"$JOBS" && make install ) || rc=$?
|
||||
|
||||
restore_pc
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "curl 构建失败(退出码 $rc)" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
else
|
||||
echo "== curl 已就绪"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------ 自检
|
||||
# 光看文件在不在不够 —— 要真的**链一个用 curl 的静态程序**,才能证明这份
|
||||
# 前缀是可用的(静态链接最常栽在依赖顺序和缺库上,而那只在链接时暴露)。
|
||||
echo
|
||||
echo "== 自检:编一个最小程序并静态链接"
|
||||
CHECK="$SRC/check.c"
|
||||
cat > "$CHECK" <<'EOF'
|
||||
#include <curl/curl.h>
|
||||
int main(void) {
|
||||
CURL *c = curl_easy_init();
|
||||
if (!c) return 1;
|
||||
curl_easy_setopt(c, CURLOPT_URL, "https://example.com/");
|
||||
curl_easy_cleanup(c);
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
LIBS=$("$PREFIX/bin/curl-config" --static-libs)
|
||||
# shellcheck disable=SC2086
|
||||
"$CC" -static -o "$SRC/check" "$CHECK" -I"$PREFIX/include" $LIBS -pthread
|
||||
if ldd "$SRC/check" 2>&1 | grep -q "not a dynamic executable\|statically linked\|不是动态可执行文件"; then
|
||||
echo " 静态链接成功,且确实不依赖动态库"
|
||||
else
|
||||
echo " 注意:ldd 没说它是静态的,检查一下下面这行输出:" >&2
|
||||
ldd "$SRC/check" 2>&1 | head -3 >&2
|
||||
fi
|
||||
|
||||
# curl-config 的输出必须是**合法 UTF-8**。
|
||||
# 它在 `make static` 里会被原样打印出来(recipe 里那句 `echo " 依赖: $LIBS"`),
|
||||
# 于是会进工具输出、进对话历史、最后进请求体。一旦含非法字节,服务端会以
|
||||
# "invalid unicode code point" 拒掉整个请求 —— 而且坏数据已经留在历史里,
|
||||
# 之后每次请求都会失败。上面 curl 那段的长注释解释了坏字节是怎么来的。
|
||||
# 用 iconv 做往返校验:截断的/被转义的序列都会被它拒掉。没有 iconv 就跳过。
|
||||
if command -v iconv >/dev/null 2>&1; then
|
||||
if "$PREFIX/bin/curl-config" --static-libs 2>/dev/null \
|
||||
| iconv -f UTF-8 -t UTF-8 > /dev/null 2>&1; then
|
||||
echo " curl-config --static-libs 输出是合法 UTF-8"
|
||||
else
|
||||
echo " 错误:curl-config --static-libs 的输出含非法 UTF-8。" >&2
|
||||
echo " 这通常意味着 configure 走了 pkg-config 那条路(见上面 curl 那段)。" >&2
|
||||
echo " 删掉这个前缀重跑本脚本:rm -rf \"$PREFIX\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "依赖就绪:$PREFIX"
|
||||
echo "接下来: make static"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# check-colors.sh - 审计所有 ANSI 颜色码,确保都在 16 色内
|
||||
#
|
||||
# 为什么要单独一个脚本:颜色越界这件事**看代码看不出来** —— \033[94m 在人眼里
|
||||
# 也是"一个蓝色",只有拿尺子量才知道它属于 aixterm 的亮色段,不在最窄的
|
||||
# 16 色 ANSI 里。而这个项目是硬编码 SGR、不查 terminfo 的,既然不探测终端
|
||||
# 能力,就该待在所有终端都认的那一档。
|
||||
#
|
||||
# 允许的 SGR 参数:
|
||||
# 0 重置
|
||||
# 1/2/3/4 加粗/暗淡/斜体/下划线(属性,不是颜色)
|
||||
# 30-37 标准前景 8 色
|
||||
# 39 默认前景色
|
||||
# 不允许:90-97(亮色)、38;5/48;5(256 色)、38;2/48;2(真彩)
|
||||
set -eu
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
cd "$ROOT"
|
||||
|
||||
# 只扫源码,不扫文档 —— README 里要举反例讲这件事,出现在那里是对的
|
||||
bad=$(grep -rhoE '\\033\[[0-9;]+m' src/*.c src/*.h 2>/dev/null \
|
||||
| grep -oE '[0-9;]+' \
|
||||
| tr ';' '\n' \
|
||||
| sort -n -u \
|
||||
| awk '$1 != 0 && $1 != 1 && $1 != 2 && $1 != 3 && $1 != 4 \
|
||||
&& $1 != 39 && !($1 >= 30 && $1 <= 37) { print }' || true)
|
||||
|
||||
if [ -n "$bad" ]; then
|
||||
echo "发现 16 色范围外的 SGR 参数:" >&2
|
||||
printf ' %s\n' $bad >&2
|
||||
echo "" >&2
|
||||
echo "允许的:0 1 2 3 4 30-37 39" >&2
|
||||
echo "越界的:90-97(aixterm 亮色)、38;5/48;5(256 色)、38;2/48;2(真彩)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
n=$(grep -rhoE '\\033\[[0-9;]+m' src/*.c src/*.h 2>/dev/null | sort -u | wc -l)
|
||||
echo "颜色审计通过:$n 个不同的 SGR 序列,全部在 16 色内。"
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/bin/sh
|
||||
# get-version.sh - 算出版本号,供 Makefile 用 -D 注入
|
||||
#
|
||||
# 输出六行(顺序固定,Makefile 按行取):
|
||||
# 1. CalVer 主版本号 YYYY.MM.DD
|
||||
# 2. git 短哈希 abc1234 / 1a2b3c4.dirty / unknown
|
||||
# 3. 分支名 master / unknown(detached HEAD 或非 git)
|
||||
# 4. 构建日期 YYYY-MM-DD(UTC)
|
||||
# 5. 构建时间 HH:MM:SS(UTC)
|
||||
# 6. 本脚本算出的版本串(仅供人看/调试)
|
||||
#
|
||||
# 版本方案:**CalVer(发布日的 YYYY.MM.DD)+ git 短哈希**。
|
||||
#
|
||||
# 2026.02.14 打在 tag 上的发布版
|
||||
# 2026.02.14-1a2b3c4 tag 之后又提交了
|
||||
# 2026.02.14-1a2b3c4.dirty 工作区还有没提交的改动
|
||||
#
|
||||
# 为什么 CalVer 而不是 SemVer:ligent 是个终端工具,不是被别人链接的库,
|
||||
# "第几个大版本"这种信息对它没什么用;"这是哪天的东西"才有用 —— 用户看到
|
||||
# 2026.02.14 立刻知道新旧。真需要表达破坏性变更时,git 历史和 README 比版本
|
||||
# 号数字说得清楚。
|
||||
#
|
||||
# 为什么还要带哈希:CalVer 的粒度是"天",同一天可能发好几次;而且用户手上
|
||||
# 的二进制未必来自 tag(可能自己改了源码)。带上哈希,报 bug 时能精确定位。
|
||||
#
|
||||
# 分隔符用 "-" 而不是 git describe 传统的 "+g":我们只用 git,不存在"区分是
|
||||
# 哪种 SCM"的需求(那正是 g 前缀存在的理由),而 2026.09.22-1a2b3c4 少两个
|
||||
# 符号、念起来也顺。
|
||||
#
|
||||
# 取版本号的优先级(从高到低):
|
||||
# 1. 环境变量 LIGENT_VERSION_OVERRIDE —— 给 CI 用,tag 打得不对时能手动指定
|
||||
# 2. git tag 里最新的 CalVer 形态的 tag(v2026.02.14 或 2026.02.14)
|
||||
# 3. 源码目录的 mtime(从 tarball 解出来时没有 .git,用文件的日期当版本)
|
||||
#
|
||||
# 任何一步失败都不算致命:退回一个明确的默认值,绝不让构建挂掉 ——
|
||||
# 版本号不对只是难看,构建挂掉是真的不能用。
|
||||
set -eu
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
|
||||
# ------------------------------------------------------- 构建日期与时间
|
||||
# SOURCE_DATE_EPOCH 是"可复现构建"的标准约定:设了它就用它当构建时间,
|
||||
# 这样同一个源码在不同时间编出来的二进制是**逐字节相同**的。
|
||||
# 不认这个约定的话,CI 上每次构建的产物哈希都不一样,没法校验。
|
||||
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
|
||||
BUILD_DATE=$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%d 2>/dev/null \
|
||||
|| date -u -r "$SOURCE_DATE_EPOCH" +%Y-%m-%d 2>/dev/null \
|
||||
|| echo unknown)
|
||||
BUILD_TIME=$(date -u -d "@$SOURCE_DATE_EPOCH" +%H:%M:%S 2>/dev/null \
|
||||
|| date -u -r "$SOURCE_DATE_EPOCH" +%H:%M:%S 2>/dev/null \
|
||||
|| echo unknown)
|
||||
else
|
||||
BUILD_DATE=$(date -u +%Y-%m-%d 2>/dev/null || echo unknown)
|
||||
BUILD_TIME=$(date -u +%H:%M:%S 2>/dev/null || echo unknown)
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 主版本号
|
||||
if [ -n "${LIGENT_VERSION_OVERRIDE:-}" ]; then
|
||||
CALVER=$LIGENT_VERSION_OVERRIDE
|
||||
else
|
||||
CALVER=
|
||||
# 只在真的在 git 工作区里、且 git 可用时才问 git
|
||||
if command -v git >/dev/null 2>&1 && [ -d "$ROOT/.git" ]; then
|
||||
# 找最近的、长得像 CalVer 的 tag。用 grep 过滤而不是 --match:不同
|
||||
# git 版本对 --match 的 glob 支持略有差异,而这里只要"第一个符合
|
||||
# YYYY.MM.DD 形态的"就够了,过滤更直观也更可移植。
|
||||
CALVER=$(git -C "$ROOT" tag --list 2>/dev/null \
|
||||
| grep -E '^v?[0-9]{4}\.[0-9]{2}\.[0-9]{2}$' \
|
||||
| sed 's/^v//' \
|
||||
| sort \
|
||||
| tail -1 || true)
|
||||
fi
|
||||
# 没有 tag:用源码目录的日期当版本。tarball 场景下这是"这份源码什么时候
|
||||
# 打的包",虽然不如 tag 精确,但比写死一个 0.0.0 有用。
|
||||
if [ -z "$CALVER" ]; then
|
||||
CALVER=$(date -u -r "$ROOT/src/main.c" +%Y.%m.%d 2>/dev/null || true)
|
||||
fi
|
||||
[ -n "$CALVER" ] || CALVER=0.0.0-unknown
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- git 哈希
|
||||
# LIGENT_HASH_OVERRIDE:给容器/CI 用。容器里没有 .git(.dockerignore 排除了),
|
||||
# 那里问不出哈希来,只能由外面算好传进来。
|
||||
HASH=${LIGENT_HASH_OVERRIDE:-}
|
||||
if [ -z "$HASH" ] && command -v git >/dev/null 2>&1 && [ -d "$ROOT/.git" ]; then
|
||||
if h=$(git -C "$ROOT" rev-parse --short=7 HEAD 2>/dev/null); then
|
||||
HASH=$h
|
||||
# 工作区脏了就标出来。**这一条很重要**:不带这个标记的话,一个改过
|
||||
# 源码的构建和一个干净的构建会报出同样的版本号,排查时会被误导。
|
||||
if ! git -C "$ROOT" diff --quiet HEAD -- 2>/dev/null; then
|
||||
HASH="$HASH.dirty"
|
||||
elif [ -n "$(git -C "$ROOT" ls-files --others --exclude-standard 2>/dev/null)" ]; then
|
||||
# 未跟踪的新文件也算脏 —— 它可能正是被编译进去的那个源文件
|
||||
HASH="$HASH.dirty"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
[ -n "$HASH" ] || HASH=unknown
|
||||
|
||||
# ---------------------------------------------------------------- 分支名
|
||||
# LIGENT_BRANCH_OVERRIDE 同理给容器/CI 用。
|
||||
BRANCH=${LIGENT_BRANCH_OVERRIDE:-}
|
||||
if [ -z "$BRANCH" ] && command -v git >/dev/null 2>&1 && [ -d "$ROOT/.git" ]; then
|
||||
b=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
||||
# detached HEAD(CI 上 checkout 某个 tag/commit 时就是这种状态)会返回
|
||||
# 字面量 "HEAD" —— 那不是分支名,照实说成 unknown,别让读的人以为
|
||||
# 有个叫 HEAD 的分支。
|
||||
if [ -n "$b" ] && [ "$b" != "HEAD" ]; then
|
||||
BRANCH=$b
|
||||
fi
|
||||
fi
|
||||
[ -n "$BRANCH" ] || BRANCH=unknown
|
||||
|
||||
printf '%s\n' "$CALVER"
|
||||
printf '%s\n' "$HASH"
|
||||
printf '%s\n' "$BRANCH"
|
||||
printf '%s\n' "$BUILD_DATE"
|
||||
printf '%s\n' "$BUILD_TIME"
|
||||
# 第 6 行只是给人看的完整串。哈希未知时**不要**拼出 "…-unknown" ——
|
||||
# 那既丑又会被误读成一个真实的哈希。src/version.c 的拼接遵循同一规则,
|
||||
# 两处必须一致,否则这个"调试输出"会误导人。
|
||||
if [ "$HASH" = unknown ]; then
|
||||
printf '%s\n' "$CALVER"
|
||||
else
|
||||
printf '%s-%s\n' "$CALVER" "$HASH"
|
||||
fi
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
/* md_demo.c - Markdown 渲染效果演示
|
||||
*
|
||||
* 一个独立的小程序:把内置示例(或你指定的文件 / stdin)喂进 ligent 的
|
||||
* 流式 Markdown 渲染器,直观看到每种语法的渲染结果。
|
||||
*
|
||||
* 编译: make md_demo
|
||||
* 运行: ./build/md_demo
|
||||
*/
|
||||
#include "markdown.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 内置示例:把支持的语法和边界情况都覆盖一遍 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static const char *SAMPLE =
|
||||
"# ligent 渲染演示\n"
|
||||
"\n"
|
||||
"## 行内样式\n"
|
||||
"\n"
|
||||
"这是 **粗体**、*斜体*、_下划线斜体_ 和 `行内代码` 的混排效果。\n"
|
||||
"标识符不会被误伤:foo_bar_baz、__init__、arr[i]、a_b_c 都保持原样。\n"
|
||||
"链接只高亮方括号:[官方文档](https://example.com/doc) 后面还有普通文字。\n"
|
||||
"图片保留原文:\n"
|
||||
"\n"
|
||||
"## 列表与引用\n"
|
||||
"\n"
|
||||
"- 减号列表项\n"
|
||||
"- 第二项\n"
|
||||
"* 星号也能当列表\n"
|
||||
"+ 加号也可以\n"
|
||||
"\n"
|
||||
"> 这是一段引用,前缀是暗色的竖线。\n"
|
||||
"> 引用可以有多行。\n"
|
||||
"\n"
|
||||
"---\n"
|
||||
"\n"
|
||||
"## 代码块(整块不渲染)\n"
|
||||
"\n"
|
||||
"```c\n"
|
||||
"#include <stdio.h>\n"
|
||||
"int main(void) {\n"
|
||||
" // **这里不会加粗**,[方括号] 也不会高亮\n"
|
||||
" printf(\"hello %s\\n\", \"world\");\n"
|
||||
" return 0;\n"
|
||||
"}\n"
|
||||
"```\n"
|
||||
"\n"
|
||||
"行内代码同理:`a ** b` 里的星号不会被当成粗体标记。\n"
|
||||
"\n"
|
||||
"## 表格\n"
|
||||
"\n"
|
||||
"| 语言 | 类型系统 | 典型用途 | 份额 |\n"
|
||||
"|:-----|:--------:|----------|-----:|\n"
|
||||
"| C | 静态弱类型 | 系统编程 | 11.4% |\n"
|
||||
"| Rust | 静态强类型 | 系统编程 | 3.2% |\n"
|
||||
"| Python | 动态强类型 | 脚本 / AI | 23.1% |\n"
|
||||
"\n"
|
||||
"| 单列表格 |\n"
|
||||
"|---|\n"
|
||||
"| 也能渲染 |\n"
|
||||
"\n"
|
||||
"## 边界情况\n"
|
||||
"\n"
|
||||
"乘法星号原样保留:2 * 3 * 4 = 24\n"
|
||||
"管道命令不会被误判成表格:ls -la | grep foo\n"
|
||||
"未闭合的 **粗体标记 会在行尾被复位\n"
|
||||
"#井号后没有空格就不是标题\n"
|
||||
"####### 七个井号也不是标题\n"
|
||||
"\n";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 渲染到内存(用于 --compare 自检) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static char *render_to_mem(const char *src, int enabled, size_t chunk)
|
||||
{
|
||||
char *buf = NULL;
|
||||
size_t size = 0;
|
||||
FILE *f = open_memstream(&buf, &size);
|
||||
if (!f) return NULL;
|
||||
|
||||
MdRenderer *r = md_new(enabled);
|
||||
md_set_out(r, f);
|
||||
|
||||
if (chunk == 0) {
|
||||
md_feed(r, src);
|
||||
} else {
|
||||
size_t len = strlen(src);
|
||||
for (size_t i = 0; i < len; i += chunk) {
|
||||
size_t n = len - i < chunk ? len - i : chunk;
|
||||
char *tmp = xstrndup(src + i, n);
|
||||
md_feed(r, tmp);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
md_finish(r);
|
||||
md_free(r);
|
||||
fclose(f);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 渲染到终端(可带分片与延时,模拟 SSE 流式) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void render_to_tty(const char *src, int enabled,
|
||||
size_t chunk, long delay_ms)
|
||||
{
|
||||
MdRenderer *r = md_new(enabled);
|
||||
md_set_out(r, stdout);
|
||||
|
||||
size_t len = strlen(src);
|
||||
if (chunk == 0) chunk = len ? len : 1;
|
||||
|
||||
struct timespec ts;
|
||||
ts.tv_sec = delay_ms / 1000;
|
||||
ts.tv_nsec = (delay_ms % 1000) * 1000000L;
|
||||
|
||||
for (size_t i = 0; i < len; i += chunk) {
|
||||
size_t n = len - i < chunk ? len - i : chunk;
|
||||
char *tmp = xstrndup(src + i, n);
|
||||
md_feed(r, tmp);
|
||||
free(tmp);
|
||||
fflush(stdout);
|
||||
if (delay_ms > 0) nanosleep(&ts, NULL);
|
||||
}
|
||||
md_finish(r);
|
||||
md_free(r);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 参数 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void usage(FILE *out)
|
||||
{
|
||||
fputs(
|
||||
"md_demo - 演示 ligent 的流式 Markdown 渲染效果\n"
|
||||
"\n"
|
||||
"用法: md_demo [选项] [文件...]\n"
|
||||
"\n"
|
||||
"不指定文件时使用内置示例;文件名为 - 或 stdin 不是终端时从标准输入读取。\n"
|
||||
"\n"
|
||||
"选项:\n"
|
||||
" -p, --plain 关闭渲染,输出原始 Markdown(对照用)\n"
|
||||
" --color 强制开启渲染(默认仅在输出到终端时开启)\n"
|
||||
" -c, --chunk N 按每 N 字节一片喂入(默认 0 = 一次性喂完)\n"
|
||||
" -d, --delay MS 每片之间延时毫秒(默认 0)\n"
|
||||
" -s, --stream 流式预设:等价于 -c 5 -d 12\n"
|
||||
" --compare 自检:把同一段文本按多种分片方式渲染,比对结果是否一致\n"
|
||||
" -h, --help 显示本帮助\n"
|
||||
"\n"
|
||||
"例子:\n"
|
||||
" md_demo 看渲染效果\n"
|
||||
" md_demo --plain 看原始 Markdown,对照差异\n"
|
||||
" md_demo -c 5 -d 12 模拟 SSE 流式(标记会被随机切断)\n"
|
||||
" md_demo --compare 验证分片不变性\n"
|
||||
" md_demo README.md 渲染任意 Markdown 文件\n"
|
||||
" echo '# hi **there**' | md_demo\n",
|
||||
out);
|
||||
}
|
||||
|
||||
static char *read_stream(FILE *f)
|
||||
{
|
||||
StrBuf sb;
|
||||
sb_init(&sb);
|
||||
char buf[8192];
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof buf, f)) > 0)
|
||||
sb_append(&sb, buf, n);
|
||||
return sb_detach(&sb);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int plain = 0, force_color = 0, compare = 0;
|
||||
long delay_ms = 0;
|
||||
size_t chunk = 0;
|
||||
const char *file = NULL;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
const char *a = argv[i];
|
||||
if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) {
|
||||
usage(stdout);
|
||||
return 0;
|
||||
} else if (strcmp(a, "-p") == 0 || strcmp(a, "--plain") == 0) {
|
||||
plain = 1;
|
||||
} else if (strcmp(a, "--color") == 0) {
|
||||
force_color = 1;
|
||||
} else if (strcmp(a, "-c") == 0 || strcmp(a, "--chunk") == 0) {
|
||||
if (++i >= argc) { fputs("缺少 -c 的参数\n", stderr); return 2; }
|
||||
chunk = (size_t)strtoul(argv[i], NULL, 10);
|
||||
} else if (strcmp(a, "-d") == 0 || strcmp(a, "--delay") == 0) {
|
||||
if (++i >= argc) { fputs("缺少 -d 的参数\n", stderr); return 2; }
|
||||
delay_ms = strtol(argv[i], NULL, 10);
|
||||
} else if (strcmp(a, "-s") == 0 || strcmp(a, "--stream") == 0) {
|
||||
chunk = 5;
|
||||
delay_ms = 12;
|
||||
} else if (strcmp(a, "--compare") == 0) {
|
||||
compare = 1;
|
||||
} else if (a[0] == '-' && a[1] != '\0' && strcmp(a, "-") != 0) {
|
||||
fprintf(stderr, "未知选项 %s(--help 查看用法)\n", a);
|
||||
return 2;
|
||||
} else {
|
||||
file = a;
|
||||
}
|
||||
}
|
||||
|
||||
/* 取输入 */
|
||||
char *src = NULL;
|
||||
const char *source_name = "内置示例";
|
||||
if (file && strcmp(file, "-") != 0) {
|
||||
size_t len = 0;
|
||||
src = read_entire_file(file, &len);
|
||||
if (!src) {
|
||||
fprintf(stderr, "无法读取文件 %s\n", file);
|
||||
return 1;
|
||||
}
|
||||
source_name = file;
|
||||
} else if (file || !isatty(fileno(stdin))) {
|
||||
src = read_stream(stdin);
|
||||
source_name = "标准输入";
|
||||
if (src[0] == '\0') {
|
||||
/* stdin 是空管道(例如被 CI / make 调用)时退回内置示例 */
|
||||
free(src);
|
||||
src = xstrdup(SAMPLE);
|
||||
source_name = "内置示例(stdin 为空)";
|
||||
}
|
||||
} else {
|
||||
src = xstrdup(SAMPLE);
|
||||
}
|
||||
|
||||
int enabled = plain ? 0 : (force_color ? 1 : (isatty(fileno(stdout)) ? 1 : 0));
|
||||
|
||||
if (compare) {
|
||||
/* 分片不变性自检:不同分片大小必须得到完全相同的结果 */
|
||||
puts("分片不变性自检(同一段文本,不同分片方式,输出必须逐字节一致)");
|
||||
printf("输入:%s(%zu 字节)\n\n", source_name, strlen(src));
|
||||
|
||||
char *whole = render_to_mem(src, 1, 0);
|
||||
size_t sizes[] = { 1, 2, 3, 5, 7, 13, 64 };
|
||||
int all_ok = 1;
|
||||
for (size_t k = 0; k < sizeof sizes / sizeof sizes[0]; k++) {
|
||||
char *part = render_to_mem(src, 1, sizes[k]);
|
||||
int ok = part && whole && strcmp(whole, part) == 0;
|
||||
printf(" %-5zu 字节/片 %s\n", sizes[k], ok ? "一致 ✓" : "不一致 ✗");
|
||||
if (!ok) all_ok = 0;
|
||||
free(part);
|
||||
}
|
||||
/* 随机分片(模拟真实 SSE 的抖动) */
|
||||
srand(12345);
|
||||
char *buf = NULL;
|
||||
size_t bsize = 0;
|
||||
FILE *f = open_memstream(&buf, &bsize);
|
||||
MdRenderer *r = md_new(1);
|
||||
md_set_out(r, f);
|
||||
size_t len = strlen(src), i = 0;
|
||||
while (i < len) {
|
||||
size_t n = (size_t)(rand() % 11) + 1;
|
||||
if (i + n > len) n = len - i;
|
||||
char *tmp = xstrndup(src + i, n);
|
||||
md_feed(r, tmp);
|
||||
free(tmp);
|
||||
i += n;
|
||||
}
|
||||
md_finish(r);
|
||||
md_free(r);
|
||||
fclose(f);
|
||||
int ok = buf && whole && strcmp(whole, buf) == 0;
|
||||
printf(" %-5s %s\n", "随机", ok ? "一致 ✓" : "不一致 ✗");
|
||||
if (!ok) all_ok = 0;
|
||||
free(buf);
|
||||
free(whole);
|
||||
|
||||
printf("\n结论:%s\n", all_ok ? "全部一致,标记跨分片处理正确。"
|
||||
: "存在不一致,渲染器有 bug。");
|
||||
free(src);
|
||||
return all_ok ? 0 : 1;
|
||||
}
|
||||
|
||||
if (isatty(fileno(stdout))) {
|
||||
fprintf(stderr, "── 输入:%s 渲染:%s%s ──\n\n",
|
||||
source_name, enabled ? "开" : "关",
|
||||
chunk ? "(分片模式)" : "");
|
||||
}
|
||||
|
||||
render_to_tty(src, enabled, chunk, delay_ms);
|
||||
|
||||
if (isatty(fileno(stdout))) {
|
||||
fprintf(stderr, "\n── 演示结束 ──\n");
|
||||
if (enabled && chunk == 0)
|
||||
fprintf(stderr, "试试 `md_demo -s` 看流式分片下标记被切断时的表现,"
|
||||
"或 `md_demo -p` 对照原始 Markdown。\n");
|
||||
}
|
||||
|
||||
free(src);
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user