构建脚本和构建期资源/平台衔接
这一阶段不是程序运行时干活,而是在“出厂前装配”。Cargo 编译时会先跑这些小脚本,把不同平台需要的零件补齐。bwrap 会在 Linux 上把 bubblewrap 这套沙箱工具的 C 代码一起编进去;windows-sandbox-rs 会把 Windows 权限清单塞进辅助程序,免得权限行为出错;cli 给 macOS 补上 Objective-C 链接参数;skills 像哨兵盯着样例资源,资源一变就提醒重新构建。它们合起来保证成品在各系统上能正确编出、正确启动。
原生组件构建
这些构建脚本通过编译随附代码并在支持时嵌入 Windows 元数据,准备特定平台的原生部分。
bwrap/build.rs源码 ↗
这个文件解决的是“沙箱工具从哪里来、怎么编进去”的问题。bubblewrap 可以粗略理解成一个给程序搭临时隔离房间的工具,常用来限制程序能看到和能碰到的系统资源。这个构建脚本会先告诉 Cargo(Rust 的构建工具)哪些环境变量和源码文件变了就要重新编译。然后它判断当前目标系统是不是 Linux,以及用户有没有明确要求跳过构建;不是 Linux 或被跳过时就什么也不做。真正要构建时,它会找到 bubblewrap 的源码目录,优先用用户指定的位置,否则用项目里自带的 vendor 目录;接着用 pkg-config 查找 libcap 这个系统能力库,生成一个很小的 config.h,再用 cc 工具把几份 C 源码编成静态组件。它还把 C 程序原来的 main 改名成 bwrap_main,避免和 Rust 程序入口冲突。最后它告诉 Cargo:bubblewrap 可用了,并补上需要链接的库路径和库名。
main5–30 ↗
fn main()
作用:这是构建脚本的入口,Cargo 在编译这个 crate 时会先运行它。它决定要不要尝试编译 bubblewrap,并告诉 Cargo 哪些文件或环境变量变化后需要重新跑构建。
数据流:进去的是编译环境里的信息,比如目标操作系统、项目目录、一些控制开关环境变量。它先打印 Cargo 能理解的指令,登记需要监听的环境变量和 C 源码文件;然后检查目标系统是不是 Linux、是否设置了跳过构建。符合条件时,它调用 try_build_bwrap 去真正编译;如果编译失败,就直接让构建报错停止。
调用关系:它是整个文件的总开关。Cargo 调起它后,它先做规则登记和平台判断,再把重活交给 try_build_bwrap;try_build_bwrap 如果返回错误,main 会用 panic! 把错误变成清晰的构建失败。
调用图:调用 1 个内部函数(try_build_bwrap);外部调用 5 个(from, var, var_os, panic!, println!)。
try_build_bwrap32–77 ↗
fn try_build_bwrap() -> Result<(), String>
作用:这个函数真正负责把 bubblewrap 的 C 源码编译进项目。有人会用它,是因为 Rust 项目需要在构建阶段准备好一个可链接的内置沙箱组件。
数据流:进去的是环境变量里的项目目录和输出目录,以及系统里通过 pkg-config 能找到的 libcap 信息。它先找到 bubblewrap 源码目录,再检查 libcap 是否可用;然后在构建输出目录写入 config.h,配置 C 编译器要编译哪些 C 文件、包含哪些头文件目录、定义哪些编译开关。它把 main 改名为 bwrap_main,避免入口函数重名。编译完成后,它把库搜索路径、要链接的库名,以及 bwrap_available 这个编译标记打印给 Cargo。成功时返回 Ok,失败时返回带说明的错误字符串。
调用关系:它由 main 在确认需要构建时调用,是“真正干活”的步骤。它自己不会决定平台和跳过规则,只负责执行构建;其中源码目录的选择交给 resolve_bwrap_source_dir,系统库信息交给外部的 pkg-config 查询,C 编译动作交给 cc 构建工具。
调用图:调用 1 个内部函数(resolve_bwrap_source_dir);被 1 处调用(main);外部调用 7 个(from, new, new, var, format!, println!, write)。
resolve_bwrap_source_dir84–106 ↗
fn resolve_bwrap_source_dir(manifest_dir: &Path) -> Result<PathBuf, String>
作用:这个函数负责决定 bubblewrap 源码应该从哪里拿。它让开发者既可以用自己指定的源码 checkout,也可以默认使用项目随代码带着的 vendor 版本。
数据流:进去的是当前 crate 的目录路径。它先看环境变量 CODEX_BWRAP_SOURCE_DIR 有没有设置;如果设置了,并且这个目录真实存在,就把它作为源码目录返回;如果设置了但目录不存在,就返回错误。没有设置时,它会检查项目相对路径 ../vendor/bubblewrap 是否存在;存在就返回这个默认目录,不存在就返回一段说明该怎么修复的错误信息。
调用关系:它被 try_build_bwrap 调用,位置在正式编译之前。try_build_bwrap 需要先知道 C 源码在哪里,才能把 bubblewrap.c、bind-mount.c、network.c、utils.c 交给 C 编译器;这个函数就是专门负责把“源码位置”这件事判断清楚。
调用图:被 1 处调用(try_build_bwrap);外部调用 4 个(join, from, var, format!)。
windows-sandbox-rs/build.rs源码 ↗
这个文件只在“编译项目”的时候工作,不是程序运行时的一部分。它先告诉 Cargo(Rust 的构建工具):如果权限清单文件变了,就要重新构建。然后它检查目标系统是不是 Windows;如果不是 Windows,就直接什么也不做。到了 Windows 上,它会找到项目目录里的 codex-windows-sandbox-setup.manifest 文件,并根据当前使用的 Windows 编译工具链,给链接器传入不同格式的参数。链接器可以理解为“把零件最终拼成可执行文件的工具”。这些参数只作用在 codex-windows-sandbox-setup 这个辅助程序上,避免其他 Codex 程序也意外带上这份 Windows 资源信息。简单说,它像是在打包前把一张“权限说明卡”塞进指定的安装助手程序里。
main7–39 ↗
fn main() -> Result<(), String>
作用:这是构建脚本的入口。Cargo 在编译这个包时会运行它,用它来决定是否以及怎样把 Windows manifest(清单文件,用来告诉 Windows 程序需要的权限和兼容信息)嵌入到指定的辅助可执行文件中。
数据流:进去的是编译环境里的信息,比如目标操作系统、编译工具链类型、项目目录位置;它先打印一条给 Cargo 的提示,说明清单文件变化时要重新构建。接着如果目标不是 Windows,就直接结束;如果是 Windows,它拼出清单文件路径,再根据 MSVC 或 GNU/LLVM 工具链打印对应的链接参数。出来的结果不是普通返回值,而是一组写到标准输出的 Cargo 指令;如果缺少必要的项目目录环境变量,则返回错误。
调用关系:它由 Cargo 在构建阶段自动调用。它自己不去真正修改可执行文件,而是通过读取环境变量了解当前构建情况,再用 println! 输出 Cargo 能识别的命令,把后续嵌入 manifest 的工作交给 Cargo 和链接器完成。
平台链接器配置
此构建脚本会为 macOS 专用 CLI 构建调整链接器行为。
cli/build.rs源码 ↗
Rust 项目用 Cargo 编译时,可以有一个 build.rs 文件,叫“构建脚本”,意思是:程序还没真正编出来之前,先跑它来告诉编译器一些额外规则。这个文件做的事很少,但很关键:它查看当前要编译到哪个操作系统。如果目标系统是 macOS,就向 Cargo 打一行特殊指令,让最终链接时加上 -ObjC 参数。可以把它想成装家具前的一张小提示纸:如果是在 macOS 这间屋子里安装,就记得多拧一个特殊螺丝。没有这个参数时,某些和 Objective-C,也就是苹果平台常见语言运行时有关的代码,可能在链接阶段找不到或没有被正确带进去。
main1–5 ↗
fn main()
作用:这是构建脚本的入口,Cargo 在编译项目前会自动运行它。它判断目标系统是不是 macOS,如果是,就告诉 Rust 编译器在链接时加上一个 macOS 需要的参数。
数据流:它先读取环境变量 CARGO_CFG_TARGET_OS,这个变量由 Cargo 提供,表示当前编译目标的操作系统。然后它把读到的值和 macos 比较;如果相等,就输出一行 cargo:rustc-link-arg=-ObjC。Cargo 会把这行输出当成命令,不是普通日志,并把 -ObjC 传给后面的链接步骤;如果不是 macOS,它什么也不改。
调用关系:这个函数不被项目里的普通运行代码调用,而是在构建阶段由 Cargo 自动执行。它只依赖两个外部能力:读取环境变量 var,以及用 println! 输出给 Cargo 的构建指令;真正的链接工作随后由 Cargo 和 Rust 编译工具链接着完成。
嵌入式资源跟踪
此构建脚本将嵌入的示例资源接入 Cargo 的变更检测,以便资源树变化时触发重新构建。
skills/build.rs源码 ↗
Rust 的 Cargo(Rust 的构建工具)默认会知道源码变了要重新编译,但不一定会自动盯住某些资源目录。这个文件专门检查 src/assets/samples 这个样例资源目录是否存在;如果存在,就把这个目录以及里面每一个文件、子目录都告诉 Cargo:这些东西一变,就重新运行构建。它先从 main 开始,找到样例目录;如果目录不存在,就直接退出,不报错。目录存在时,它打印特殊格式的指令 cargo:rerun-if-changed=...,这是 Cargo 能看懂的“请监控这个路径”的信号。然后 visit_dir 会一层层走进子目录,把所有路径都登记上。这样做的好处是:新增、修改或删除样例资源时,构建系统更容易及时发现,不会漏掉变化。
main4–12 ↗
fn main()
作用:这是构建脚本的入口。它检查样例资源目录是否存在,并在存在时启动递归扫描,让 Cargo 以后能发现这些资源文件的变化。
数据流:进去的是固定路径 src/assets/samples,它先把这个字符串变成一个路径对象,再检查这个目录在磁盘上是否存在。不存在就什么也不做直接结束;存在就向 Cargo 打印一条“这个目录变了要重新构建”的提示,然后把目录交给 visit_dir 继续扫描里面的内容。
调用关系:Cargo 在构建项目时会自动运行这个 main。它自己只负责确定起点和发出第一条监控指令,真正逐层查看目录内容的工作交给 visit_dir。
visit_dir14–27 ↗
fn visit_dir(dir: &Path)
作用:这个函数负责走遍一个目录里的所有东西,并把每个文件或子目录都登记给 Cargo 监控。遇到子目录时,它会继续往里走。
数据流:进去的是一个目录路径。它尝试读取这个目录的内容;如果读取失败,比如目录权限不够或突然不存在,就安静退出。读取成功后,它逐个拿到里面的条目,打印“这个路径变了要重新构建”的提示;如果某个条目还是目录,就再次调用自己,继续扫描更深一层。结果不是返回数据,而是通过打印给 Cargo 的指令改变构建系统的监控范围。
调用关系:visit_dir 由 main 在确认样例目录存在后调用。它是整个扫描流程里的“递归小工”,每发现一个子目录就把同样的工作再做一遍,直到所有层级都登记完成。