gRPC 定制 rake-compiler-dock Docker 镜像构建全流程指南
【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc
导读
本文基于 gRPC 仓库的 third_party/rake-compiler-dock/README.md,完整还原 gRPC 团队构建内部专用rake-compiler-dockDocker 镜像的方法:从克隆上游仓库、打定制补丁、本地构建、重新打标、推送到 Google Artifact Registry,到最终把新镜像 digest 写回 CI Dockerfile 并发布。读完本文,你将掌握 gRPC Ruby 原生扩展(C 扩展)跨平台交叉编译镜像的整套维护流程,并理解仓库中update_cross_compilers.patch、各平台Dockerfile、.current_version文件与push_testing_images.sh脚本之间的协作关系。
⚠️INTERNAL ONLY:该文档明确标注为 gRPC 内部流程,目标是 gRPC 测试基础设施镜像,不面向最终用户。普通用户构建/安装 grpc Ruby gem 时不需要执行本文任何步骤。
一、背景:gRPC 为何需要定制的 rake-compiler-dock
gRPC 的 Ruby 实现包含用 C 写的原生扩展(grpc_native扩展),需要针对 Windows、macOS、多种 Linux 发行版与 CPU 架构分别编译并发布预编译 gem。rake-compiler-dock是 Ruby 社区用于交叉编译 C 扩展的标准工具链容器,它把交叉编译器、Ruby 头文件与构建脚本打包进 Docker 镜像,使开发者可以在单台 x86_64 Linux 主机上为x86_64-linux-gnu、aarch64-linux-gnu、arm64-darwin、x64-mingw-ucrt等十余种平台产出原生扩展。
gRPC 仓库对上游镜像做了三处关键定制(详见下文补丁分析):升级到 clang 14 / GCC 10 以获得更好的 C++17 支持、固定 musl 工具链版本、升级 osxcross 的 LLVM 与 macOS SDK——这是因为 gRPC 核心本身是大型 C++ 代码库,对编译器的语言标准支持要求远高于普通 Ruby gem。
从源码侧也能看到镜像与构建流程的衔接:在 src/ruby/ext/grpc/extconf.rb 中,gRPC 通过读取环境变量RCD_HOST_RUBY_VERSION(该变量由 rake-compiler-dock 在构建容器内设置)来判定是否处于交叉编译模式:
cross_compiling = ENV['RCD_HOST_RUBY_VERSION'] # set by rake-compiler-dock in build containers同文件 src/ruby/ext/grpc/extconf.rb 还处理了 rake-compiler-dock 交叉编译时在 LDFLAG 中注入-s(链接时 strip)的问题:当需要保留调试符号时,会先从标志列表中剔除-s,保证"先链接共享库、再保存调试符号、最后才 strip"的顺序。
二、前置条件与仓库布局
1. 环境变量
执行构建前需要设置两个环境变量:
export GEM_ROOT=<path_to_clone_rake_compiler_dock_repo> export GIT_ROOT=<path_to_grpc_git_repo>| 变量 | 含义 |
|---|---|
GEM_ROOT | 本地克隆的上游rake-compiler-dock仓库路径(用于打补丁与构建) |
GIT_ROOT | 当前 gRPC 仓库根目录路径(用于引用补丁、Dockerfile 与发布脚本) |
2. gRPC 仓库内的镜像定义目录
third_party/rake-compiler-dock/目录下按平台存放 Dockerfile 与版本信息,共 10 个平台:
| 平台目录 | 目标平台 |
|---|---|
rake_aarch64-linux-gnu | ARM64 Linux(glibc) |
rake_aarch64-linux-musl | ARM64 Linux(musl) |
rake_arm64-darwin | Apple Silicon macOS |
rake_x64-mingw-ucrt | 64 位 Windows(UCRT 运行时) |
rake_x86-linux-gnu | 32 位 Linux(glibc) |
rake_x86-linux-musl | 32 位 Linux(musl) |
rake_x86-mingw32 | 32 位 Windows(MSVCRT) |
rake_x86_64-darwin | Intel macOS |
rake_x86_64-linux-gnu | 64 位 Linux(glibc) |
rake_x86_64-linux-musl | 64 位 Linux(musl) |
每个平台目录包含Dockerfile,并配有一个同名.current_version文件记录当前镜像的 tag 与 sha256 digest(机制见第六节)。另有根级文件update_cross_compilers.patch(定制补丁)与README.md(本文档)。
三、逐步构建流程(上游五步法)
步骤 1:克隆上游仓库
(umask 0022; git clone https://github.com/rake-compiler/rake-compiler-dock -b v1.12.0 "$GEM_ROOT")为什么用umask 0022:构建过程中,build/目录下的脚本会在 Docker 内部以另一个 Linux 用户执行。默认umask生成的文件权限可能只对属主可写,导致容器内用户无法读取/写入克隆出来的文件。使用umask 0022可以保证其他用户获得必要的读/写权限(文件 644、目录 755)。注意umask只在子 shell 内生效,不影响当前 shell 环境。
版本固定为上游v1.12.0,与镜像 tag 前缀1.12.0-mri-*一一对应。
步骤 2:打补丁并本地构建镜像
cd "$GEM_ROOT" && git apply "${GIT_ROOT}/third_party/rake-compiler-dock/update_cross_compilers.patch" bundle config set --local path '.bundle/gems' bundle install bundle exec rake build:images执行逻辑:
git apply把 gRPC 的定制补丁 update_cross_compilers.patch 应用到上游仓库(补丁详细内容见第四节);bundle config set --local path '.bundle/gems'把 gem 安装到仓库内局部路径,避免污染系统环境;bundle install安装 rake-compiler-dock 的构建依赖;bundle exec rake build:images使用该 gem 的 Rake 任务构建全部平台镜像。
步骤 3:重新打标(re-tag)到 gRPC 测试仓库
docker image ls --filter "reference=ghcr.io/rake-compiler/rake-compiler-dock-image" --format "{{.Repository}}:{{.Tag}}" | grep '1\.12\.0' | sed -E 's@^[^:]+:@@' | xargs -r -n1 -I{} docker tag ghcr.io/rake-compiler/rake-compiler-dock-image:{} us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image:{}该命令做四件事:
- 列出本地镜像中引用前缀为
ghcr.io/rake-compiler/rake-compiler-dock-image的所有镜像; - 用
grep '1\.12\.0'筛选出 1.12.0 系列; - 用
sed剥掉仓库前缀,只保留 tag(如mri-x86_64-linux-gnu); - 用
xargs逐一对每个 tag 执行docker tag,把镜像从上游参考前缀(ghcr.io/rake-compiler/rake-compiler-dock-image)打标为 gRPC 公共测试镜像前缀(us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image)。
步骤 4:上传到 Google Artifact Registry
docker image ls --filter "reference=ghcr.io/rake-compiler/rake-compiler-dock-image" --format "{{.Repository}}:{{.Tag}}" | grep '1\.12\.0' | sed -E 's@^[^:]+:@@' | xargs -r -n1 -I{} docker push us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image:{}与步骤 3 相同的筛选逻辑,把重新打标后的镜像逐张docker push到远程 Artifact Registry。执行前需要先完成 GCP 认证:
gcloud auth configure-docker us-docker.pkg.dev gcloud auth login(这两条认证命令来自发布脚本 tools/dockerfile/push_testing_images.sh 的注释。)
步骤 5:重建 CI Dockerfile 并发布
镜像推送到远端后,Docker 会为每张镜像生成 sha256 digest。下一步是把 digest 写回 gRPC 仓库内各平台的Dockerfile,使这些测试镜像固定引用新构建的基础镜像:
docker image ls --format "{{.Tag}} {{.Repository}}:{{.Tag}}@{{.Digest}}" \ us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image \ | rg '^1.12.0-mri' \ | while read -r tag image; do dockerfile="${GIT_ROOT}/third_party/rake-compiler-dock/rake_${tag#1.12.0-mri-}/Dockerfile" if [[ -f "$dockerfile" ]]; then sed -E -i "s|^FROM [^ ]+\$|FROM ${image}|" "$dockerfile" fi done脚本逻辑拆解:
docker image ls --format "{{.Tag}} {{.Repository}}:{{.Tag}}@{{.Digest}}"输出每张镜像的 tag、完整引用(含 digest)两列;rg '^1.12.0-mri'只保留1.12.0-mri-*系列;${tag#1.12.0-mri-}是 bash 参数展开,剥离1.12.0-mri-前缀得到平台名(如x86_64-linux-gnu),拼出对应 Dockerfile 路径third_party/rake-compiler-dock/rake_<platform>/Dockerfile;- 若文件存在,
sed -E -i "s|^FROM [^ ]+\$|FROM ${image}|"把 Dockerfile 中的FROM行替换为仓库:tag@sha256:digest的完整引用,保证后续构建可复现。
最后运行发布脚本推送最终测试镜像:
tools/dockerfile/push_testing_images.sh该脚本会遍历third_party/rake-compiler-dock/*等目录(见第六节),把更新后的 Dockerfile 构建成镜像并上传,同时同步.current_version文件。
四、定制补丁详解:update_cross_compilers.patch
补丁 update_cross_compilers.patch 改编自上游 rake-compiler-dock 的 PR #201,核心动机只有一句:安装 clang 14 与 GCC 10,以获得更好的 C++17 支持(gRPC 核心的 C++ 代码依赖较新的语言特性)。补丁共修改上游仓库三个文件:
1.Dockerfile.mri.erb(镜像模板,改动最大)
针对不同平台条件替换编译器:
| 平台 | 变更 |
|---|---|
| darwin(macOS) | 新增llvm-toolchain-focal-14软件源,安装clang-14 llvm-14-dev libc++-14-dev libc++abi-14-dev python3 lzma-dev libxml2-dev libssl-dev,并通过update-alternatives把/usr/bin/clang、/usr/bin/clang++指向 14 版 |
| arm-linux-gnu(32 位 ARM) | 交叉编译包升级为gcc-10-arm-linux-gnueabihf g++-10-arm-linux-gnueabihf,再用update-alternatives接管arm-linux-gnueabihf-gcc/g++ |
| x86-linux-gnu(32 位) | 升级为gcc-10-i686-linux-gnu g++-10-i686-linux-gnu,接管i686-linux-gnu-gcc/g++ |
| aarch64-linux-gnu(ARM64) | 按TARGETPLATFORM分支:在linux/arm64上直接装gcc-10 g++-10并以update-alternatives注册为aarch64-linux-gnu-gcc/g++;否则装交叉编译包gcc-10-aarch64-linux-gnu g++-10-aarch64-linux-gnu |
| x86_64-linux-gnu(x86_64) | 按TARGETPLATFORM分支:在linux/amd64上装gcc-10 g++-10并同时接管gcc/g++与x86_64-linux-gnu-gcc/g++;否则装gcc-x86-64-linux-gnu g++-x86-64-linux-gnu |
关键设计:TARGETPLATFORM是 Docker BuildKit 提供的构建参数,这里用它区分"本机原生编译"与"真正的交叉编译"两种情形——原生平台直接复用宿主架构的 GCC 10,其他平台才安装对应的交叉编译器,从而同时覆盖同架构与跨架构两种构建路径。
2.build/mk_musl_cross.sh(musl 工具链)
新增固定版本参数:
# Use GCC 10.3.0. Binutils 2.33.1 is the newest version that still has a # verified hash in musl-cross-make and works with GCC 10 - newer versions # (like 2.44, the current default) break the GCC 10 build. GCC_VER = 10.3.0 BINUTILS_VER = 2.33.1即把 musl 交叉工具链固定为GCC 10.3.0 + Binutils 2.33.1。注释说明了原因:musl-cross-make 中带校验哈希、且与 GCC 10 兼容的最新 Binutils 是 2.33.1,而更新的版本(如当时的默认 2.44)会破坏 GCC 10 的构建。
3.build/mk_osxcross.sh(macOS 交叉编译)
- 下载的 SDK 固定为MacOSX11.1,并设置
OSX_VERSION_MIN=10.13; - 将 SDK 内嵌入的 C++ 头文件来源从
llvm-10升级为llvm-14(/usr/lib/llvm-14/include/c++),bits 头文件匹配也相应放宽为*通配版本目录; llvm-config软链接从llvm-config-10改为llvm-config-14;x86_64/aarch64-apple-darwin-objdump软链接从llvm-10/bin/llvm-objdump改为llvm-14/bin/llvm-objdump(osxcross 没有自带 objdump,借用 LLVM 的);- 同时签入 sigtool 以解决链接行库顺序问题。
补丁改动与第一节的源码衔接一致:容器内RCD_HOST_RUBY_VERSION环境变量即由这套定制的构建环境注入。
五、各平台 Dockerfile 剖析
以 rake_x86_64-linux-gnu/Dockerfile 为例,镜像分两层:
FROM us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image:1.12.0-mri-x86_64-linux-gnu@sha256:2ce8d74b6072e567a5592b330535d473ffc515857d914158fc66ebea7d12f55b #================= # Install ccache # Install ccache from source since ccache 3.x packaged with most linux distributions # does not support Redis backend for caching. RUN curl -sSL -o ccache.tar.gz https://github.com/ccache/ccache/releases/download/v4.5.1/ccache-4.5.1.tar.gz \ && tar -zxf ccache.tar.gz \ && cd ccache-4.5.1 \ && mkdir build && cd build \ && cmake -DCMAKE_BUILD_TYPE=Release -DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON .. \ && make -j4 && make install \ && cd ../.. \ && rm -rf ccache-4.5.1 ccache.tar.gz- 基础镜像:直接引用第三节步骤 3/4 上传的定制镜像,
1.12.0-mri-x86_64-linux-gnutag 后跟@sha256:...digest,实现内容寻址、不可变引用; - 额外层:从源码编译安装ccache 4.5.1。Dockerfile 注释给出原因——大多数 Linux 发行版自带的 ccache 3.x 不支持 Redis 后端缓存(gRPC 的构建缓存使用 Redis 后端),因此必须从源码安装新版本。编译时通过
-DZSTD_FROM_INTERNET=ON -DHIREDIS_FROM_INTERNET=ON从网络拉取 zstd 与 hiredis 依赖。
其他平台结构相同,仅基础镜像的 tag/digest 不同。其中 rake_arm64-darwin/Dockerfile 与 rake_x86_64-darwin/Dockerfile 是纯基础镜像(只含FROM一行,无 ccache 层),而 Linux 平台镜像(glibc/musl 各架构)普遍带有 ccache 层。
六、发布脚本 push_testing_images.sh 与 .current_version 机制
第三节步骤 5 提到的 tools/dockerfile/push_testing_images.sh 是整个 CI 镜像基础设施的总入口,它把third_party/rake-compiler-dock/*与tools/dockerfile/test/*、grpc_artifact_*、interoptest/*、distribtest/*一起纳入统一管理(脚本第 65-71 行)。
1. 支持的运行模式(环境变量)
| 环境变量 | 作用 |
|---|---|
LOCAL_ONLY_MODE | 仅本地操作,不查询 Artifact Registry、不执行上传 |
CHECK_MODE | 仅校验所有.current_version文件是否最新(供 CI sanity 测试使用) |
SKIP_UPLOAD | 构建后不推送镜像到 Artifact Registry |
HOST_ARCH_ONLY | 只构建与运行主机同架构的镜像 |
ALWAYS_BUILD | 无论 Dockerfile 是否变化都强制构建(构建时追加--no-cache --pull) |
KEEP_GOING | 单个镜像构建失败不中止,继续其余构建 |
MAX_CONCURRENCY | 并发构建数上限(默认 8) |
2. 环境自检
非 CHECK_MODE 下,脚本会先做两项环境检查:
- 验证免 sudo 的 Docker 可用:
docker run --rm debian:11 ...; - 验证 x64 主机能运行 arm64 镜像(依赖
qemu-user-static的 binfmt-misc 钩子):docker run --rm --platform=linux/arm64 arm64v8/debian:11 ...,失败时会提示先安装qemu-user-static——这是 ARM_DOCKERFILE_DIRS 列表 中众多 arm64 镜像能在 x64 CI 上构建的前提。
3. 镜像 tag 与 .current_version 的对应关系
脚本的版本管理核心是"Dockerfile 内容哈希即镜像 tag":
local DOCKER_IMAGE_TAG=$(sha1sum $DOCKERFILE_DIR/Dockerfile | cut -f1 -d\ )Dockerfile 的任何改动都会产生新 tag,从而触发重建。镜像构建/发布状态记录在同目录的<平台>.current_version文件中,其内容为:
<镜像仓库>/<镜像名>:<sha1-tag>@sha256:<repo-digest>例如 rake_x86_64-linux-gnu.current_version:
us-docker.pkg.dev/grpc-testing/testing-images-public/rake_x86_64-linux-gnu:caf2c2ae2cabdd2a2f9cd793bea744170d1fb281@sha256:80f1b44eaa660b93f696fc338f6c4c9bb05931d36a55d906d45fe1ea9f6bf921关键语义(对应 脚本 143-258 行的状态机):
- 只有
tag@sha256:...完整形式才表示"镜像已推送、可被测试使用"; - 仅含
tag(无 digest)表示"本地构建过但尚未推送",docker push成功后才把 RepoDigest 补写回.current_version; - 若远端已存在相同 digest 且
.current_version已同步,则跳过构建; CHECK_MODE下任何失同步(stale 版本文件、已变更未推送、Dockerfile 已变但版本文件未更新)都会被判定为 CHECK FAILED。
脚本末尾还会调用tools/bazelify_tests/generate_dockerimage_current_versions_bzl.sh重新生成 bazel 侧的镜像版本映射(CHECK_MODE 下则校验其是否为最新),保证 Bazel 测试与 Docker 测试引用同一套镜像。
七、维护工作流小结
一次完整的"升级交叉编译器并发布"维护周期可归纳为:
- 准备:设置
GEM_ROOT、GIT_ROOT;umask 0022克隆上游v1.12.0; - 定制:应用 update_cross_compilers.patch(clang 14 / GCC 10 / musl 固定版本 / osxcross 升级);
- 构建:
bundle exec rake build:images本地产出全部平台镜像; - 重打标并推送:从
ghcr.io/rake-compiler/rake-compiler-dock-image改挂到us-docker.pkg.dev/grpc-testing/testing-images-public/rake-compiler-dock-image并docker push; - 固化引用:用 digest 回写各平台
Dockerfile的FROM行; - 发布:运行 tools/dockerfile/push_testing_images.sh,构建
rake_*系列测试镜像、推送并同步.current_version文件与 Bazel 版本映射。
整个体系通过"内容哈希 tag + sha256 digest +.current_version状态文件"三重机制,保证了 gRPC Ruby 原生扩展跨平台构建环境(Windows UCRT/MSVCRT、macOS Intel/Apple Silicon、Linux glibc/musl 的 x86/x86_64/aarch64)的可复现性与可审计性,也让 CI 能够自动判断"何时需要重建、何时可以直接复用远端镜像"。
【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考