Files
ligent/tools/build-static-deps.sh
T
2026-10-03 14:43:43 +08:00

364 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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"