Rust 编译器 CI Docker 测试指南:用 citool 与 run.sh 在本地复现 rustc 构建任务
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
rustc 的持续集成(CI)依托于一组精心构造的 Docker 镜像来完成 Linux 平台的构建与测试,本指南以 rustc-dev-guide 的「Testing with Docker」章节为主线,系统讲解如何在本地开发机上复现这些 CI 任务:先介绍一键式工具citool,再深入剖析底层脚本src/ci/docker/run.sh的镜像构建、目录挂载与容器运行机制,最后介绍交互模式,帮助读者在不同于本机的环境里调试、实验 rustc 的完整构建流程。
Docker 在 rustc CI 中的角色
rustc 的 CI 需要在大量 Linux 目标平台(x86_64、i686、ARM、MIPS、s390x 等)上验证编译器的可移植性,而这些目标环境在普通开发机上通常并不存在。为此,仓库在src/ci/docker目录中为 GitHub Actions 上执行的Linux 作业(job)准备了完整的 Docker 镜像定义;非 Linux 作业(如 macOS、Windows)则在 Docker 之外直接运行。
src/ci/docker目录结构如下:
host-x86_64/:面向 x86_64 主机的镜像定义,包含dist-*(发行版构建,如dist-x86_64-linux)与test-*(测试,如test-x86_64-gnu)两大类,每类对应一个子目录;host-aarch64/:面向 aarch64 主机的镜像定义;run.sh:构建并运行指定镜像的核心脚本;scripts/:镜像构建过程中被Dockerfile调用的辅助脚本(如sccache.sh、cross-apt-packages.sh等);static/:构建时复制的静态文件。
这意味着你可以把这些作业搬到本地开发机上运行,用以验证那些与本地系统环境不同的目标平台,例如在 x86_64 机器上构建面向 32 位或其它架构的发行产物。你需要在一台 Linux、Windows 或 macOS 机器上安装 Docker——通常情况下 Linux 会快得多,因为 Windows 和 macOS 需要借助虚拟机来模拟 Linux 环境。
快速上手:用 citool 在本地复现 CI 作业
CI 中的作业通过一组 bash 脚本配置,直接在本地完整复刻它们的行为并不容易。src/ci/citool是一个专门的 Rust 工具(即 rustc 团队的 "CI tooling",详见 src/ci/citool/README.md),它负责在 CI 上根据触发场景(PR、try 作业、合并尝试)计算应执行的作业矩阵,同时提供在本地执行部分 CI 作业的能力。
若希望以最简方式在本地运行某个 CI 作业,可使用其run-local子命令:
cargo run --manifest-path src/ci/citool/Cargo.toml run-local <job-name> # 示例:运行 dist-x86_64-linux-alt 作业 cargo run --manifest-path src/ci/citool/Cargo.toml run-local dist-x86_64-linux-alt从 citool 入口 可以看到,run-local接受一个必填的作业名参数,以及可选的--type(默认auto,可指定为pr,对应合并尝试作业与 PR 作业)。其内部执行流程为(见 run_workflow_locally 实现):
- 从
src/ci/github-actions/jobs.yml加载作业数据库,并按作业名在 Linux 作业中查找匹配项; - 按作业名推断环境变量:作业名以
dist-开头且以-alt结尾时设置DEPLOY_ALT=1,仅以dist-开头则设置DEPLOY=1,随后合并作业自身定义的环境变量,用于尽量复刻src/ci/scripts/setup-environment.sh的效果; - 必要时初始化
src/llvm-project/子模块; - 最终以作业的镜像名调用
src/ci/docker/run.sh完成构建与测试。
如果上述方式在你的环境里不可用,或者你希望对 Docker 镜像的执行有更多控制、想弄清 Docker 作业执行期间究竟发生了什么,可以继续阅读下文,直接使用底层脚本run.sh。
核心脚本 run.sh:镜像构建、挂载与运行
src/ci/docker/run.sh负责完成四件事:构建指定的 Docker 镜像、运行容器、在镜像内构建 Rust,随后运行测试或准备面向分发的归档文件。
目录挂载与产物位置
脚本以只读方式挂载你的本地 Rust 源码树,以读写方式挂载一个obj目录;所有编译器构建产物都会写入obj目录。容器内的 shell 起始工作目录是obj,随后执行../src/ci/run.sh,由该脚本按镜像定义启动实际的构建。
从 run.sh 实现 可以看到非 CI 场景下的挂载细节:
- 源码树挂载为
/checkout(默认追加:ro只读标记); obj目录挂载为/checkout/obj;- 本机
~/.cargo挂载为/cargo; /tmp/toolstate挂载为容器内同名目录,用于 toolstate 数据交换;- 容器工作目录设置为
/checkout/obj,并注入SRC=/checkout、CARGO_HOME=/cargo等环境变量。
关于只读挂载,脚本支持通过环境变量READ_ONLY_SRC关闭默认的只读行为:当READ_ONLY_SRC=0时,源码树将以读写方式挂载,从而允许在容器内进行实验性工作或调试(例如拉取并构建子模块),详见 run.sh 中的挂载选项。
obj 目录的本地布局
镜像构建完成后,obj目录在 CI 与非 CI 场景下路径不同(见 run.sh 第 41-46 行):
- 在 CI 上:
objdir=$root_dir/obj; - 在本地:
objdir=$root_dir/obj/$image,即以镜像名隔离,避免不同作业的产物互相干扰。
此外obj目录下还会创建tmp、cores子目录,并生成github-summary.md供 CI 汇总步骤摘要使用。
直接运行 run.sh
可以这样直接运行:
src/ci/docker/run.sh <image-name>关于 run.sh 的几个重要注意事项
原文档给出了以下关键提示,这里逐条结合源码展开:
1. 子模块必须齐全。在 CI 上执行时,脚本假定所有子模块都已检出。如果作业访问的某个子模块缺失,构建会直接报错。因此在本地执行前应确保所需子模块均已检出:既可以通过 git 手动检出,也可以在bootstrap.toml中设置build.submodules = true,然后运行类似x build的命令让 bootstrap 下载最重要的子模块——但请注意,这未必覆盖某个具体 CI 作业所需的全部子模块。
2.<image-name>与作业名的对应关系。镜像名对应src/ci/docker/host-*目录下的某一个子目录。注意镜像名不一定等于作业名:某些作业复用同一个镜像,但通过不同的环境变量或 Docker 构建参数区分。这正是本地复现 CI 作业困难的一部分原因——citool的价值就在于替你把这一层映射关系自动化掉。镜像构建基于 Dockerfile 的校验和:脚本会将镜像名、Dockerfile 内容、COPY涉及的源文件内容、主机架构以及缓存版本号拼接后计算 SHA-512,作为镜像标签的一部分(详见 run.sh 的校验和逻辑)。
3. dist 作业需要DEPLOY=1。如果执行的是dist-开头的作业,应设置环境变量DEPLOY=1。
4. alternative dist 作业需要DEPLOY_ALT=1。如果执行的是以dist-开头且以-alt结尾的作业,应设置DEPLOY_ALT=1。
这两个变量在src/ci/run.sh中决定了一系列配置差异:例如DEPLOY/DEPLOY_ALT生效时启用--enable-llvm-static-stdcpp、设置--debuginfo-level、按CODEGEN_BACKENDS(默认llvm)配置 codegen 后端;-alt作业还会额外启用 debug assertions 与 LLVM assertions(见 src/ci/run.sh 第 111-137 行)。反之,非 dist 的测试作业默认同时测试llvm,cranelift两个 codegen 后端,并启用 debug assertions、overflow checks 等(见 src/ci/run.sh 第 138-176 行)。同时citool的run-local正是依据作业名前缀/后缀自动注入这两个变量的。
5. 部分 std 测试需要 IPv6。某些标准库测试依赖 IPv6 支持,而 Linux 上的 Docker 默认禁用了它。需要在创建容器前执行src/ci/scripts/enable-docker-ipv6.sh开启 IPv6,且只需执行一次。该脚本会向/etc/docker/daemon.json写入{"ipv6":true,"fixed-cidr-v6":"fd9a:8454:6789:13f7::/64"}并重启 Docker 服务(见 enable-docker-ipv6.sh)。
镜像构建与缓存策略
run.sh在镜像获取上做了分层处理(见 run.sh 第 121-192 行):
- 本地非 CI 执行:优先尝试从
ghcr.io的rust-ci镜像仓库docker pull预构建镜像(标签为镜像输入校验和),拉取成功则直接打上rust-ci标签使用;拉取失败才回退到本地docker build。 - PR CI 作业:使用 Buildx 的 registry 缓存后端(
--cache-from)复用缓存,但不向仓库写入缓存。 - auto/try 合并作业:先登录镜像仓库,若相同校验和的镜像已存在则直接下载;否则用
--cache-from/--cache-to构建并推送,避免镜像内容未变化时重复构建导致缓存抖动。
容器运行使用--init(正确处理僵尸进程回收)与--rm(退出即删除),并默认以--privileged特权模式运行——原因是 LLVM 5.0 升级后 Leak Sanitizer 需要 ptrace 等系统调用,于是对所有构建器统一开启(见 run.sh 第 264-269 行)。
构建加速与容器内用户身份
run.sh会为容器注入 sccache 相关配置:若设置了SCCACHE_BUCKET等变量则透传 AWS/S3 相关环境(PR 构建禁用 S3 认证),否则挂载本机~/.cache/sccache到/sccache以复用本地缓存(见 run.sh 第 238-254 行)。同时通过LOCAL_USER_ID环境变量把宿主机用户 ID 传入容器,由src/ci/run.sh在容器内创建同名用户并以该用户身份执行构建,从而保证obj目录中产物的属主与本机用户一致;在使用 rootless Podman 时会改用--userns=keep-id并设置NO_CHANGE_USER=1(见 run.sh 第 303-311 行)。
Docker-in-Docker 场景
如果run.sh本身已经在容器内运行(通过/.dockerenv文件判断),则无法直接使用--volume挂载宿主机目录。此时脚本改用「临时容器 +docker cp」的方式:创建带/checkout卷的临时容器、把源码目录复制进去、用--volumes-from复用该卷,构建结束后再把obj目录从容器拷回宿主机(见 run.sh 第 293-312 行)。
交互模式:在容器里自由实验
有时我们希望先构建出某个特定的 Docker 镜像,再在容器内执行自定义命令,以便观察目标系统环境的具体行为。这时可以使用交互模式:
src/ci/docker/run.sh --dev <image-name>该模式会在容器内启动一个 bash shell。进入容器后,你可以执行单个命令来完成特定任务,例如只跑 UI 测试:
../x test tests/ui从 run.sh 实现 看,--dev会追加-it(交互 + TTY)参数,且容器命令不再指向src/ci/run.sh,而是直接进入 bash;若源码树来自 git 检出,还会预先执行git config --global --add safe.directory /checkout,避免容器内出现 git "dubious ownership" 保护报错。
使用交互模式时有几点补充说明:
- 容器退出即删除,但产物保留:退出 shell 时容器会被自动删除(
--rm),不过构建产物仍保留在obj目录中。如果在不同 Docker 镜像间切换,前一个环境留在obj目录里的产物可能干扰构建系统,有时需要在容器内构建前删除obj目录的部分或全部内容。 - 容器是精简环境:容器只安装了最小化软件包集合,缺少编辑器等常用工具时,可以自行安装,例如
apt install less vim。 - 可开多个 shell:在容器内可打开多个 shell。首先获取容器名(shell 提示符中会显示一个短哈希,或在容器外运行
docker container ls列出可用容器),然后执行docker exec -it <CONTAINER> /bin/bash,其中<CONTAINER>是类似4ba195e95cef的容器名。
实例剖析:dist-x86_64-linux 作业
以文档示例中的dist-x86_64-linux作业为例,其镜像定义位于 src/ci/docker/host-x86_64/dist-x86_64-linux:
- Dockerfile 以 CentOS 7(glibc 2.17、kernel 3.2 的最低平台支持基线)为基础,依次编译 GCC 9.5、cmake、LLVM+Clang(目标架构 X86/AMDGPU/NVPTX)、zstd 与 sccache,并通过
RUST_CONFIGURE_ARGS注入--enable-sanitizers、--enable-profiler、--set llvm.thin-lto=true、--set rust.lto=thin等构建配置; - dist.sh 定义了容器内实际执行的命令:先构建
opt-dist(PGO 优化工具),再以x.py dist生成分发产物;非 try 构建还会额外使用 GCC 构建gcc-dev/gcc组件并验证retain属性支持。
这说明镜像名与作业名之间的映射、构建参数的注入,最终都汇聚在run.sh+src/ci/run.sh这一条链路上:前者负责「环境」,后者负责「构建决策」。
小结
在本地复现 rustc CI 的 Docker 作业,路径可以非常平滑:优先使用cargo run --manifest-path src/ci/citool/Cargo.toml run-local <job-name>一键执行;需要精细控制或排查问题时,再直接使用src/ci/docker/run.sh <image-name>或--dev交互模式。理解run.sh的镜像缓存、只读挂载、obj目录布局、DEPLOY/DEPLOY_ALT语义以及 IPv6 与子模块前提,是在本地稳定复现 CI 结果的关键;而citool与jobs.yml则把作业名到镜像名、环境变量的复杂映射自动化,大大降低了本地复刻 CI 的门槛。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考