Files
ligent/Dockerfile
T
2026-10-03 14:43:43 +08:00

136 lines
6.6 KiB
Docker
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.
# 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"]