为什么 Modular 代码库选择 Bazel:从“我的机器上能跑“到“任何机器都能构建“

发布时间:2026/9/12 15:48:47
为什么 Modular 代码库选择 Bazel:从“我的机器上能跑“到“任何机器都能构建“
为什么 Modular 代码库选择 Bazel从我的机器上能跑到任何机器都能构建【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo导读构建系统是每个开发者每天都要打交道的底层设施速度、易用性、正确性缺一不可。本文以 Modular 平台包含 MAX 与 Mojo开源仓库中的官方文档 max/docs/why-bazel.md 为主体系统梳理该仓库选择 Bazel 而非其他构建系统的完整论证从 hermetic 可复现构建带来的速度与正确性到多语言Mojo / C / Python代码库的统一构建策略再到 Starlark 赋予的构建基础设施即代码能力并辅以仓库内 bazelw、.bazelrc、MODULE.bazel 等真实配置作为源码级佐证。读完本文你将理解一个大型多语言仓库为何最终收敛到单一语言无关的构建系统以及如何在这个仓库里实际使用./bazelw完成构建、测试与格式化。Bazel 的核心动机速度与正确性一个都不能少Bazel 的口号就是 {fast, correct}, pick two——默认假设你无法同时拥有快速与正确而它要做的就是打破这个二选一。Bazel 的实现路径是聚焦于hermetic封闭式、可复现的构建一个人在某个机器上构建出的东西另一个人应该在另一台机器上用完全相同的流程构建出完全一致的结果除 OS、架构这类根本性差异外。这正是对为什么在我机器上跑不起来这一经典问题的彻底回应——Bazel 用机制保证这类问题几乎不会发生。严格声明输入输出是正确性的地基要做到这一点Bazel 对构建的输入与输出极其严格给定一组输入和一条构建规则输出是确定的。这种完整掌握构建全貌的能力同时带来了速度收益——因为构建系统随时知道哪些输入变了、哪些 target 需要失效重编因此可以精确地做增量失效与重建而不是靠猜。此外Bazel 还能借助sandbox沙箱将构建工具与系统其余部分隔离进一步防止隐式依赖悄悄混进构建。在 Modular 仓库中这些理念有非常具体的落地。仓库根目录的 .bazelrc 第一行就写着# Avoid PATH leaking into actions common --incompatible_strict_action_env--incompatible_strict_action_env让构建 action 不再继承宿主机器的PATH从源头杜绝某台机器恰好装了某个工具所以能编过的隐式依赖——这是 hermetic 构建在真实配置里的直接体现。远程执行与缓存突破单机资源上限Bazel 还支持远程执行remote execution与远程缓存remote caching拥有该系统访问权限的开发者可以享受命中缓存时的瞬时构建或远程执行带来的远超本地机器资源的构建速度。仓库的 .bazelrc 中可以看到远程缓存的实际配置build --remote_instance_namemodular-public build:public-cache --bes_backendgrpcs://modular-public.buildbuddy.io build:public-cache --bes_results_urlhttps://modular-public.buildbuddy.io/invocation/ build:public-cache --remote_downloadergrpcs://modular-public.buildbuddy.io build:public-cache --remote_cachegrpcs://modular-public.buildbuddy.io这说明 Modular 仓库确实接入了公共远程缓存服务modular-public并且 .bazelversion 中固定了对应的 Bazel 发行版buildbuddy-io/5.0.382保证版本与远程服务兼容。bazel clean为什么几乎不需要因为正确性足够高bazel clean在 Bazel 工作流里极少被需要——它通常只用于释放磁盘空间或者绕过构建系统自身的 bug。绝大多数开发者根本不需要碰它。注意这句论断有一个隐含前提即构建工程师必须保证所用工具是 hermetic 的因为 Bazel 的所有优化都建立在这个假设之上。封闭性的代价vendored 工具链与 sysroot这份正确性并非免费获得。C 编译器尤其贪心——它们非常喜欢读取系统配置这会让同一份代码在不同机器上编出不同结果。因此 Modular 仓库使用vendored自带/内嵌工具链确保所有编译 C 的开发者使用同一个编译器、同一套标准库、同一组编译标志。仓库中 bazel/internal/cc-toolchain/BUILD.bazel 就是这一策略的完整实现它为linux_aarch64、linux_x86_64、macos三个平台分别定义了cc_toolchain并通过cc_sysroot引入 vendored sysroot如sysroot-jammy-x86_64//sysroot:rootcc_sysroot( name linux_sysroot_x86_64, actions [ rules_cc//cc/toolchains/actions:compile_actions, rules_cc//cc/toolchains/actions:link_actions, ], allowlist_include_directories [sysroot-jammy-x86_64//sysroot:root], data [sysroot-jammy-x86_64//sysroot:directory], sysroot sysroot-jammy-x86_64//sysroot:root, )正如文档中强调的编译标志里包含-isysroot将编译器指向 vendored sysroot与-D__TIME__避免把时间戳编进二进制导致不可复现甚至 glibc 版本都被完整地封闭在 Bazel 内部与此同时开启 sandbox 作为又一道防线。从源码结构看这些编译参数被组织在 bazel/internal/cc-toolchain/args 目录下与resource_dir_args、compile_and_link_args、link_args、compile_warnings等拆分管理并通过 MODULE.bazel 中的register_toolchains统一注册register_toolchains( //bazel/internal:mojo_toolchain, mojo_toolchains//..., //bazel/internal/cc-toolchain:all, //bazel/internal:mojo_copts_toolchain, )这套封闭工具链体系就是文档所论述的速度与正确性在仓库中的工程实体。多语言代码库为什么必须用一个语言无关的系统Modular 代码库的主体是三种语言Mojo、C 和 Python。C 有自己的构建系统生态Python 也有唯独 Mojo 没有现成的构建系统。这迫使团队从以下四个选项中做出选择为 Mojo 自研一套构建系统用 C 或 Python 的构建系统来构建 Mojo其他语言各自为政用 C 或 Python 的构建系统统一构建所有语言用一个通用的、语言无关的构建系统统一构建所有语言。文档明确指出前三个选项都有严重缺陷自研构建系统意味着巨大的工程量还要长期维护配套生态让多套语言专属构建系统互相调用会很别扭。仓库的高层结构是Python 调用 C 或 MojoMojo 又调用 C如果中间隔着一串各自为政的构建系统那么没有任何一个工具能拥有对构建的完整视图full picture of the world在代码库不同区域工作的体验会截然不同让所有语言迁就单一语言构建系统同样是场硬仗——就好比让 Cargo 去构建 Go 代码或许能靠build.rs实现但你几乎丧失了对构建过程的可见性。于是剩下的只有第四条路使用单一的语言无关构建系统。Bazel 支持任何语言——只要有人为它编写对应的 ruleset——并且不预设任何特定语言的假设。这一点的价值在仓库中随处可见既有 AsyncRTC 异步运行时、Mojo stdlib、SupportC 基础设施库这样的 C 代码也有 max/python 下上千个 Python 文件以及遍布 Mojo/stdlib 与 max/kernels 的 Mojo 源码。它们的构建与测试统一由 各目录的 BUILD.bazel 描述例如 Mojo/stdlib/test/BUILD.bazel、AsyncRT/benchmarks/BUILD.bazel 都遵循同一套规则体系。更值得注意的是文档强调代码库里的语言远不止这三种——每新增一种语言语言无关方案的价值就放大一分。Mojo 官方还提供mojo_library、mojo_test、mojo_binary等 Bazel 规则见 bazel/mojo_library.bzl、bazel/mojo_test.bzl、bazel/mojo_binary.bzl让 Mojo 从没有构建系统到拥有一等公民的构建规则这正是有人写了 ruleset 就能支持的典型例证。构建基础设施即代码Starlark 带来的可见性与可编程性文档提出的关键概念是Visibility可见性任何一个构建 action 都能精确知道它拿到什么输入、产出什么输出这对速度与正确性极其重要——但 Bazel 还能做得更多。Starlark会编程的构建语言Bazel 使用一种名为Starlark有时也叫 BUILD language的规范化语言来描述构建。它大致是 Python 的一个子集这是刻意设计——让开发者感到熟悉、容易上手。纯配置文件型的构建系统固然好用但实际项目里经常需要定制运行构建系统不了解的任意脚本对应genrule写循环、条件分支甚至做完整的配置文件生成。Bazel 反其道而行之它给你一门能做以上所有事情的语言同时能把底层细节封装成易于使用的rule 或 macro。由于这一切都是代码所以可以lint、格式化、版本化——仓库根目录的 .bazelversion 正是专门用来锁定 Bazel 版本的文件。仓库里这类构建即代码的实践非常丰富bazel 目录下堆积了大量.bzl宏与规则例如 bazel/modular_cc_binary.bzl、bazel/modular_cc_library.bzl、bazel/modular_cc_test.bzlC 的统一封装bazel/config.bzl 中定义了构建配置的清单MODULAR_CONFIGS [ default, debug_modular, debug_everything, dev, ci_build, release, production, asan, tsan, ubsan, coverage, ]此外还有 lint 用的 bazel/lint/buildifier_wrapper.py对 BUILD 文件做格式检查与 bazel/lint/check_licenses.mojo 则是一个标记文件声明适用于整个源码树排除外部依赖的仓库级特性并忽略.venv、.pixi、node_modules等目录——这也是构建配置即代码的一部分。构建图内省把构建描述成代码还带来一项衍生能力构建图内省build graph introspection。你可以问出这个测试依赖哪些 target这两个提交之间有哪些 target 发生了变化这类问题。这对大型仓库的变更影响分析、CI 增量构建都极有价值而纯配置文件型的构建系统很难提供这种查询能力。常见质疑的理性回应某某语言自己的工具更好文档坦率承认这一点它可能确实更好——uv和pixi对 Python 项目堪称出色CMake虽有缺陷但已是 C 构建的事实标准、因而获得大量支持Go 和 Rust 的工具链也非常流畅。这些都是完全合理的工具但它们不适合这个仓库。特别值得注意的是Bazel 并不排斥这些语言专属工具仓库就用uv来解析并生成uv.lock文件把 Python 依赖拉入 Bazel 构建。换句话说Bazel 的定位不是所有场景下最好的构建系统而是整体上足够好并提供统一接口——这样一位 C 开发者改了 Python 代码后不需要问我该怎么运行它而是像构建其他任何 target 一样直接构建/测试即可。文档还点出了这条理念背后的团队哲学我们的目标不是让某一位开发者快 50%而是让所有开发者都快 10%。Bazel 太严格 / 太复杂 / 太难用了文档承认这里确实有道理但每个槽点背后都有设计原因严格是为了正确回到前面几乎不需要bazel clean的论点——如果构建不声明输入就可能偷偷使用任意东西参与构建或测试那样根本无法判断何时该重建/重测规则看似复杂例如规则实现与声明分离是为了让 Bazel 能分三阶段loading、analysis、execution工作以换取速度——这是典型的一个例子配置 vs 语言很多语言用配置文件构建而这个仓库经常需要为某些 target 做定制所以需要可编程的构建语言这对本仓库是必要的但对其他代码库未必完全可以理解CLI 面向构建工程师Bazel 开箱即用的命令行体验对最终用户不一定友好因此仓库提供了./bazelw run format这类高层原语来弥合差距。在这个仓库中实际使用 Bazel从./bazelw开始虽然仓库文档聚焦于为什么选择 Bazel但它也点出了日常使用的入口仓库提供了./bazelw这样的封装。仓库根目录的 bazelw 是一个 bazelisk 启动脚本它做了几件关键的事固定版本内置version1.27.0的 bazelisk并按平台darwin-arm64 / linux-amd64 / linux-arm64下载对应二进制校验完整性下载后用sha256sum/shasum校验 SHA-256 哈希不匹配即失败退出从工具分发层面保证 hermetic导出BAZEL环境变量脚本注释明确说明这是为了让rules_go依赖能找到 bazel 可执行文件否则 CI 中会报exec: bazel: executable file not found in $PATH首次使用自动安装把 bazelisk 缓存在build/bazelisk-...路径之后直接复用。实际使用方式与常规 Bazel 工作流一致只是把bazel替换为./bazelw# 构建某个 target ./bazelw build //Mojo/stdlib/... # 运行测试 ./bazelw test //AsyncRT/unittests:All # 仓库提供的格式化原语见文档 FAQ 部分 ./bazelw run format在此基础上.bazelrc 通过import %workspace%/bazel/internal/common.bazelrc与import %workspace%/build/wrapper.bazelrc分层加载公共配置与生成配置并允许开发者通过本地 gitignored 的local.bazelrc覆盖默认设置try-import %workspace%/local.bazelrc被刻意放在最后保证本地配置优先。默认构建模式为--compilation_modedbg且--//:modular_configdefault而 sanitizer 与覆盖率等模式则由 bazel/config.bzl 中列出的asan、tsan、ubsan、coverage配置驱动其具体实现同样沉淀在 bazel/internal/cc-toolchain/BUILD.bazel 的 feature 体系中。对想要深入理解该仓库构建体系的读者建议按以下顺序阅读先读本文依据的 max/docs/why-bazel.md 建立全局认知再看根目录 bazelw、.bazelrc、MODULE.bazel、REPO.bazel 四个文件了解仓库级配置随后进入 bazel 目录浏览各类.bzl规则与 bazel/config.bzl最后对照任一业务目录如 AsyncRT/BUILD.bazel、Support/BUILD.bazel理解 BUILD 文件如何落地。若需了解仓库的开发流程与 Bazel 使用约定max/docs/development.md 是官方入口。小结回到最初的问题Bazel 并不是为一切而生的构建系统但它同时满足了 Modular 代码库的三项硬性需求——通过 hermetic 与严格输入输出声明同时获得速度与正确性通过语言无关的规则体系统一 Mojo、C、Python 乃至未来更多语言通过 Starlark 把构建变成可 lint、可格式化、可版本化、可内省的代码资产。文档中的每一个论点都能在仓库的 bazelw、.bazelrc、MODULE.bazel 与 bazel/internal/cc-toolchain/BUILD.bazel 中找到对应的工程实现——这正是文档讲道理、源码做证明的最佳范式。理解了这份论证你也就理解了为什么在这样一个仓库里为什么我机器上跑不起来的问题可以真正成为历史。【免费下载链接】mojoThe Modular Platform (includes MAX Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考