# Dockerfile - 构建完全静态的 ligent # # 用法 (podman 和 docker 都行, 命令一样): # # podman build -t ligent-static . # podman run --rm ligent-static --version # podman create --name tmp ligent-static && podman cp tmp:/ligent-static . && podman rm tmp # # 或者直接跑 make 目标: # make docker-build # 构建镜像 # make docker-extract # 把产物拷到当前目录 (./ligent-static) # # 只在构建期需要网络 (下载 zlib/OpenSSL/curl 源码); 运行期不需要任何东西. # # # 为什么用 Alpine 做构建环境 # -------------------------- # ligent 的静态目标要一份**用 musl 编出来的** libcurl (发行版那份是给 glibc 的, # 混着链会得到一堆 undefined reference to __libc_start_main). # Alpine 本身就是 musl 的发行版, `apk add build-base` 得到的 gcc 直接就是 # musl 目标, 省掉了"装 musl 工具链 + 配 spec 文件"这一整段麻烦, # 而且 musl-gcc 这个包装脚本在 Alpine 上根本不存在也不需要. # # 具体地: Alpine 的 gcc 默认 `--target=x86_64-alpine-linux-musl`, # 所以 tools/build-static-deps.sh 里的 CC 检测会落到 x86_64-linux-musl-gcc # 分支 (我们显式给它 CC=cc), 产出的 .a 天然就是 musl 的. # # # 为什么分多阶段 # -------------- # 最终镜像里只有那个静态二进制. 构建工具链 (gcc/perl/make)加上 zlib/OpenSSL/curl # 的源码和中间产物有好几百 MB, 全都不该进最终镜像 —— 而 ligent 是静态链接的, # 最终镜像连 libc 都不需要, scratch 就够. # # # 关于构建缓存 # ------------ # 依赖的编译 (OpenSSL 那步最慢, 几分钟)单独占一层, 且只依赖脚本本身. # 源码 COPY 放在它后面: 改一行 C 代码不会让"重编 OpenSSL"这件事再发生一遍. # 这是这个 Dockerfile 里唯一一处为了速度而做的结构调整, 值得. # ------------------------------------------------------------------ 构建阶段 FROM docker.io/library/alpine:3.20 AS build # build-base: gcc/make/binutils (Alpine 的 gcc 就是 musl 目标的) # perl: OpenSSL 的 Configure 是 perl 写的 # curl: tools/build-static-deps.sh 用来下载源码包 # linux-headers: 依赖要内核 UAPI 头 (linux/mman.h 这些). # 注意在本机上这一项是用 -idirafter /usr/include 解决的, # 容器里显式装出来更干净. # tar/xz: 解压源码包 RUN apk add --no-cache \ build-base \ perl \ curl \ tar \ xz \ linux-headers WORKDIR /src # 先只拷贝依赖脚本, 让"编依赖"这一步能独立于源码变化被缓存. # 版本号通过 build-arg 传进来 (CI 里从 git tag 取), 这样镜像内构建不依赖 .git —— # .dockerignore 把 .git 排除了, 容器里本来也看不到它. COPY tools/build-static-deps.sh /src/tools/build-static-deps.sh # CC=cc: Alpine 的 cc 就是 musl 目标的 gcc, 不需要 musl-gcc 包装脚本. # 这一步会联网下载并编译 zlib + OpenSSL + curl, 约几分钟. RUN CC=cc sh /src/tools/build-static-deps.sh # 再拷源码. 放在依赖之后, 改 C 代码不会触发依赖重编. COPY src/ /src/src/ COPY Makefile /src/Makefile COPY tools/get-version.sh /src/tools/get-version.sh # 版本号由外面注入. 容器里没有 .git, get-version.sh 会回退到源码文件的 mtime # —— 那是"这份源码的日期", 不是"tag 的日期", 所以 CI 应当显式传进来: # podman build --build-arg VERSION=$(git describe --tags --abbrev=0) . # podman build --build-arg VERSION=$(date -u +%Y.%m.%d) . ARG VERSION= ARG GIT_HASH=unknown ARG GIT_BRANCH=unknown # 完整的 Unix 时间戳 (秒). 不要传 "2026-09-22" 这种只有日期的串 —— # date(1) 会把它解析成那天 00:00:00 UTC, 产物的构建时间就永远是午夜. ARG BUILD_EPOCH= RUN set -eux; \ # 传了就用传进来的, 没传就让脚本自己算 (会回退到源码日期) if [ -n "$VERSION" ]; then export LIGENT_VERSION_OVERRIDE="$VERSION"; fi; \ if [ -n "$GIT_HASH" ]; then export LIGENT_HASH_OVERRIDE="$GIT_HASH"; fi; \ if [ -n "$GIT_BRANCH" ]; then export LIGENT_BRANCH_OVERRIDE="$GIT_BRANCH"; fi; \ if [ -n "$BUILD_EPOCH" ]; then export SOURCE_DATE_EPOCH="$BUILD_EPOCH"; fi; \ \ # 静态目标. STATIC_CC=cc: 同上, Alpine 的 cc 已经是 musl 目标. \ make static STATIC_CC=cc; \ \ # 自检: 产物必须真的存在、真的是静态的、真的能跑. \ # 不检查的话, "构建成功"可能只是"链接器没报错", 而产物是个空壳 \ # (比如依赖没链全, 或者 -s 把符号表剥没了导致入口找不到). \ test -x /src/ligent-static; \ if ldd /src/ligent-static 2>&1 | grep -qv "not a dynamic executable\|statically linked\|Not a valid dynamic program"; then \ echo "产物不是静态链接的: " >&2; ldd /src/ligent-static >&2; exit 1; \ fi; \ V=$(/src/ligent-static --version); \ echo "容器内产物版本: $V"; \ # 版本号还是回退默认值 => -D 没生效, 构建是"看起来成功". # 这里只在**没传 VERSION** 时警告: 那种情况下回退是预期行为 (比如有人 # 直接 podman build 不带参数), 传了却没用上才是真问题. if [ -n "$VERSION" ] && echo "$V" | grep -q '0.0.0-unknown'; then \ echo "错误: 传了 VERSION=$VERSION 但产物仍报回退版本号" >&2; exit 1; \ fi # ------------------------------------------------------------------ 运行阶段 # scratch 就够了: ligent-static 是完全静态的, 连 libc 都不需要. # 没有任何攻击面可言 —— 镜像里只有一个二进制文件, 没有 shell、没有包管理器. FROM scratch # CA 证书是**唯一**需要的外部文件: TLS 握手要它. # 注意 ligent 用的是编进 libcurl 的默认路径 /etc/ssl/certs/ca-certificates.crt # (libcurl 库不读 CURL_CA_BUNDLE, 那是 curl 命令行工具的行为), # 所以证书必须放在那个位置才行, 放别处不会被读到. COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=build /src/ligent-static /ligent # 说明一下这个镜像是干什么的、怎么用. # 用 LABEL 而不是放个 README 文件: scratch 里没有 cat, 放文件也读不了. LABEL org.opencontainers.image.title="ligent" \ org.opencontainers.image.description="纯 C 实现的 AI Agent (静态构建)" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.source="https://github.com/pluvium27/ligent" # 默认进 REPL 需要 tty; 不带参数跑会因为没有 API key 而给出提示, # 所以这里不做 ENTRYPOINT 的花活, 直接就是那个二进制. ENTRYPOINT ["/ligent"]