构建脚本

有些包需要编译第三方非 Rust 代码,例如 C 库。其他包需要链接到 C 库,这些库可能位于系统上,也可能需要从源代码构建。还有一些包需要诸如在构建之前进行代码生成(想想解析器生成器)之类的功能。

Cargo 的目标不是取代为这些任务优化的其他工具,但它确实通过自定义构建脚本与它们集成。在包的根目录中放置一个名为 build.rs 的文件将导致 Cargo 编译该脚本并在构建包之前执行它。

// Example custom build script. fn main() { // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo::rerun-if-changed=src/hello.c"); // Use the `cc` crate to build a C file and statically link it. cc::Build::new() .file("src/hello.c") .compile("hello"); }

构建脚本的一些用例包括

  • 构建捆绑的 C 库。
  • 在主机系统上查找 C 库。
  • 从规范生成 Rust 模块。
  • 执行 crate 所需的任何平台特定配置。

以下章节描述了构建脚本的工作原理,示例章节展示了如何编写脚本的各种示例。

注意:可以使用 package.build manifest 键来更改构建脚本的名称,或完全禁用它。

构建脚本的生命周期

在构建包之前,Cargo 会将构建脚本编译成可执行文件(如果尚未构建)。然后它将运行该脚本,该脚本可以执行任意数量的任务。脚本可以通过将特殊格式化的命令(前缀为 cargo::)打印到 stdout 来与 Cargo 通信。

如果构建脚本的任何源文件或依赖项发生更改,则会重建构建脚本。

默认情况下,如果包中的任何文件发生更改,Cargo 将重新运行构建脚本。通常,最好使用 更改检测 部分中描述的 rerun-if 命令,以缩小触发构建脚本再次运行的范围。

一旦构建脚本成功完成执行,包的其余部分将被编译。如果出现错误导致构建停止,脚本应以非零退出代码退出,在这种情况下,构建脚本的输出将显示在终端上。

构建脚本的输入

当构建脚本运行时,构建脚本有许多输入,所有输入都以 环境变量 的形式传递。

除了环境变量之外,构建脚本的当前目录是构建脚本包的源目录。

构建脚本的输出

构建脚本可以将任何输出文件或中间工件保存在 OUT_DIR 环境变量 中指定的目录中。脚本不应修改该目录之外的任何文件。

构建脚本通过打印到 stdout 与 Cargo 通信。Cargo 会将以 cargo:: 开头的每一行解释为将影响包编译的指令。所有其他行都将被忽略。

构建脚本打印的 cargo:: 指令的顺序可能会影响 cargo 传递给 rustc 的参数顺序。反过来,传递给 rustc 的参数顺序可能会影响传递给链接器的参数顺序。因此,您需要注意构建脚本指令的顺序。例如,如果对象 foo 需要链接到库 bar,您可能需要确保库 barcargo::rustc-link-lib 指令出现在链接对象 foo 的指令之后

在正常编译期间,脚本的输出对终端是隐藏的。如果您想直接在终端中查看输出,请使用 -vv 标志以 “非常详细” 模式调用 Cargo。这仅在构建脚本运行时发生。如果 Cargo 确定没有任何更改,它将不会重新运行脚本,请参阅下面的 更改检测 以了解更多信息。

构建脚本打印到 stdout 的所有行都写入到类似 target/debug/build/<pkg>/output 的文件中(确切位置可能取决于您的配置)。stderr 输出也保存在同一目录中。

以下是 Cargo 识别的指令摘要,每个指令在下面详细介绍。

MSRV: 1.77 版本及以上版本需要 cargo::KEY=VALUE 语法。为了支持旧版本,请使用 cargo:KEY=VALUE 语法。

rustc-link-arg 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建受支持的目标(benchmarks、binaries、cdylib crates、examples 和 tests)时。它的用法高度平台特定。它对于设置共享库版本或链接器脚本很有用。

rustc-link-arg-bin 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建名称为 BIN 的二进制目标时。它的用法高度平台特定。它对于设置链接器脚本或其他链接器选项很有用。

rustc-link-arg-bins 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建二进制目标时。它的用法高度平台特定。它对于设置链接器脚本或其他链接器选项很有用。

rustc-link-lib 指令告诉 Cargo 使用编译器的 -l 标志 链接给定的库。这通常用于使用 FFI 链接原生库。

LIB 字符串直接传递给 rustc,因此它支持 -l 支持的任何语法。
目前 LIB 完全支持的语法是 [KIND[:MODIFIERS]=]NAME[:RENAME]

-l 标志仅传递给包的库目标,除非没有库目标,在这种情况下,它将传递给所有目标。这样做是因为所有其他目标都隐式依赖于库目标,并且要链接的给定库应仅包含一次。这意味着如果一个包同时具有库目标和二进制目标,则可以访问给定库中的符号,而二进制文件应通过库目标的公共 API 访问它们。

可选的 KIND 可以是 dylibstaticframework 之一。有关更多详细信息,请参阅 rustc book

rustc-link-arg-tests 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建 tests 目标时。

rustc-link-arg-examples 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建 examples 目标时。

rustc-link-arg-benches 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建 benchmark 目标时。

rustc-link-search 指令告诉 Cargo 将 -L 标志 传递给编译器,以将目录添加到库搜索路径。

可选的 KIND 可以是 dependencycratenativeframeworkall 之一。有关更多详细信息,请参阅 rustc book

如果这些路径在 OUT_DIR 中,它们也会被添加到 动态库搜索路径环境变量 中。不鼓励依赖此行为,因为这会使生成的二进制文件难以使用。一般来说,最好避免在构建脚本中创建动态库(使用现有的系统库是可以的)。

cargo::rustc-flags=FLAGS

rustc-flags 指令告诉 Cargo 将给定的空格分隔的标志传递给编译器。这仅允许 -l-L 标志,并且等效于使用 rustc-link-librustc-link-search

cargo::rustc-cfg=KEY[="VALUE"]

rustc-cfg 指令告诉 Cargo 将给定值传递给编译器的 --cfg 标志。这可以用于编译时检测功能以启用 条件编译。自定义 cfgs 必须使用 cargo::rustc-check-cfg 指令预期,否则使用将需要允许 unexpected_cfgs lint 以避免意外的 cfgs 警告。

请注意,这影响 Cargo 的依赖项解析。这不能用于启用可选依赖项或启用其他 Cargo 功能。

请注意,Cargo 功能 使用 feature="foo" 形式。使用此标志传递的 cfg 值不限于该形式,并且可以仅提供单个标识符或任何任意的键/值对。例如,发出 cargo::rustc-cfg=abc 将允许代码使用 #[cfg(abc)](注意缺少 feature=)。或者可以使用带有 = 符号的任意键/值对,例如 cargo::rustc-cfg=my_component="foo"。键应为 Rust 标识符,值应为字符串。

cargo::rustc-check-cfg=CHECK_CFG

添加到预期配置名称和值的列表,该列表用于在使用 unexpected_cfgs lint 检查可达 cfg 表达式时。

CHECK_CFG 的语法反映了 rustc --check-cfg 标志,有关更多详细信息,请参阅 检查条件配置

该指令可以这样使用

#![allow(unused)] fn main() { // build.rs println!("cargo::rustc-check-cfg=cfg(foo, values(\"bar\"))"); if foo_bar_condition { println!("cargo::rustc-cfg=foo=\"bar\""); } }

请注意,应定义所有可能的 cfgs,无论当前启用了哪些 cfgs。这包括给定 cfg 名称的所有可能值。

建议将 cargo::rustc-check-cfgcargo::rustc-cfg 指令尽可能紧密地组合在一起,以避免拼写错误、遗漏 check-cfg、过时的 cfgs……

另请参阅 条件编译 示例。

MSRV: 从 1.80 版本开始支持

cargo::rustc-env=VAR=VALUE

rustc-env 指令告诉 Cargo 在编译包时设置给定的环境变量。然后可以通过已编译 crate 中的 env! 检索该值。这对于在 crate 的代码中嵌入其他元数据很有用,例如 git HEAD 的哈希或持续集成服务器的唯一标识符。

另请参阅 Cargo 自动包含的环境变量

注意:这些环境变量在运行带有 cargo runcargo test 的可执行文件时也会设置。但是,不鼓励这种用法,因为它将可执行文件绑定到 Cargo 的执行环境。通常,这些环境变量应仅在编译时通过 env! 宏进行检查。

rustc-cdylib-link-arg 指令告诉 Cargo 将 -C link-arg=FLAG 选项 传递给编译器,但仅在构建 cdylib 库目标时。它的用法高度平台特定。它对于设置共享库版本或 runtime-path 很有用。

cargo::error=MESSAGE

error 指令告诉 Cargo 在构建脚本完成运行后显示错误,然后使构建失败。

注意:构建脚本库应仔细考虑是否要使用 cargo::error 与返回 Result。返回 Result 可能更好,并允许调用者决定错误是否致命。然后,调用者可以决定是否使用 cargo::error 显示 Err 变体。

MSRV: 从 1.84 版本开始支持

cargo::warning=MESSAGE

warning 指令告诉 Cargo 在构建脚本完成运行后显示警告。警告仅针对 path 依赖项(即,您在本地工作的依赖项)显示,因此,例如,除非构建失败,否则默认情况下不会发出在 crates.io crates 中打印的警告。可以使用 -vv “非常详细” 标志让 Cargo 显示所有 crates 的警告。

构建依赖项

构建脚本也允许依赖于其他基于 Cargo 的 crates。依赖项通过 manifest 的 build-dependencies 部分声明。

[build-dependencies] cc = "1.0.46"

构建脚本无法访问 dependenciesdev-dependencies 部分中列出的依赖项(它们尚未构建!)。此外,构建依赖项对包本身不可用,除非也在 [dependencies] 表中显式添加。

建议仔细考虑您添加的每个依赖项,权衡对编译时间、许可、维护等的影响。如果构建依赖项和普通依赖项之间共享依赖项,Cargo 将尝试重用该依赖项。但是,这并非总是可能的,例如在交叉编译时,因此请考虑对编译时间的影响。

更改检测

在重建包时,Cargo 不一定知道是否需要再次运行构建脚本。默认情况下,它采取保守的方法,即如果包中的任何文件发生更改(或受 excludeinclude 字段 控制的文件列表),则始终重新运行构建脚本。在大多数情况下,这不是一个好的选择,因此建议每个构建脚本至少发出一个 rerun-if 指令(如下所述)。如果发出这些指令,则 Cargo 仅在给定值发生更改时才重新运行脚本。如果 Cargo 正在重新运行您自己的 crate 或依赖项的构建脚本,并且您不知道为什么,请参阅 FAQ 中的 “为什么 Cargo 正在重建我的代码?”

cargo::rerun-if-changed=PATH

rerun-if-changed 指令告诉 Cargo,如果给定路径的文件已更改,则重新运行构建脚本。目前,Cargo 仅使用文件系统最后修改 “mtime” 时间戳来确定文件是否已更改。它与上次运行构建脚本时的内部缓存时间戳进行比较。

如果路径指向目录,它将扫描整个目录以查找任何修改。

如果构建脚本本质上在任何情况下都不需要重新运行,那么发出 cargo::rerun-if-changed=build.rs 是一种防止它被重新运行的简单方法(否则,如果没有发出 rerun-if 指令,则默认情况下会扫描整个包目录以查找更改)。Cargo 自动处理脚本本身是否需要重新编译,当然,脚本将在重新编译后重新运行。否则,指定 build.rs 是冗余且不必要的。

cargo::rerun-if-env-changed=NAME

rerun-if-env-changed 指令告诉 Cargo,如果给定名称的环境变量的值已更改,则重新运行构建脚本。

请注意,这里的环境变量旨在用于全局环境变量,如 CC 等,无法将其用于像 Cargo 为构建脚本设置的环境变量 这样的环境变量。使用的环境变量是 cargo 调用接收到的环境变量,而不是构建脚本的可执行文件接收到的环境变量。

可以在 Cargo.toml manifest 中设置 package.links 键,以声明包与给定的原生库链接。此 manifest 键的目的是让 Cargo 了解包具有的原生依赖项集,并提供在包构建脚本之间传递元数据的原则性系统。

[package] # ... links = "foo"

此 manifest 声明包链接到 libfoo 原生库。当使用 links 键时,包必须具有构建脚本,并且构建脚本应使用 rustc-link-lib 指令 来链接库。

主要地,Cargo 要求每个 links 值最多只能有一个包。换句话说,禁止两个包链接到同一个原生库。这有助于防止 crates 之间出现重复符号。但是请注意,有一些 约定 可以缓解这种情况。

构建脚本可以生成任意一组键值对形式的元数据。此元数据使用 cargo::metadata=KEY=VALUE 指令设置。

元数据将传递给依赖包的构建脚本。例如,如果包 foo 依赖于链接 bazbar,那么如果 bar 生成 key=value 作为其构建脚本元数据的一部分,则 foo 的构建脚本将具有环境变量 DEP_BAZ_KEY=value(请注意,使用了 links 键的值)。有关如何使用它的示例,请参阅 “使用另一个 sys crate”

请注意,元数据仅传递给直接依赖项,而不是传递依赖项。

MSRV: 1.77 版本及以上版本需要 cargo::metadata=KEY=VALUE。为了支持旧版本,请使用 cargo:KEY=VAUE(不支持的指令被假定为元数据键)。

*-sys

一些链接到系统库的 Cargo 包具有带有 -sys 后缀的命名约定。任何名为 foo-sys 的包都应提供两个主要功能

  • 库 crate 应链接到原生库 libfoo。这通常会先探测当前系统以查找 libfoo,然后再尝试从源代码构建。
  • 库 crate 应为 libfoo 中的类型和函数提供声明,但提供更高级别的抽象。

*-sys 包的集合为链接到原生库提供了一组通用的依赖项。拥有这种与原生库相关的包的约定有很多好处

  • foo-sys 的公共依赖项缓解了每个 links 值一个包的规则。
  • 其他 -sys 包可以利用 DEP_NAME_KEY=value 环境变量来更好地与其他包集成。请参阅 “使用另一个 sys crate” 示例。
  • 公共依赖项允许集中发现 libfoo 本身(或从源代码构建它)的逻辑。
  • 这些依赖项很容易被覆盖

通常会有一个没有 -sys 后缀的配套包,它在 sys 包之上提供安全、高级别的抽象。例如,git2 cratelibgit2-sys crate 提供了高级接口。

覆盖构建脚本

如果 manifest 包含 links 键,则 Cargo 支持使用自定义库覆盖指定的构建脚本。此功能的目的是完全阻止运行有问题的构建脚本,并提前提供元数据。

要覆盖构建脚本,请将以下配置放在任何可接受的 config.toml 文件中。

[target.x86_64-unknown-linux-gnu.foo] rustc-link-lib = ["foo"] rustc-link-search = ["/path/to/foo"] rustc-flags = "-L /some/path" rustc-cfg = ['key="value"'] rustc-env = {key = "value"} rustc-cdylib-link-arg = ["…"] metadata_key1 = "value" metadata_key2 = "value"

使用此配置,如果一个包声明它链接到 foo,则构建脚本将不会被编译或运行,并且将使用指定的元数据。

warningrerun-if-changedrerun-if-env-changed 键不应使用,并且将被忽略。

Jobserver

Cargo 和 rustc 使用为 GNU make 开发的 jobserver 协议 来协调跨进程的并发。它本质上是一个信号量,用于控制并发运行的作业数。并发性可以使用 --jobs 标志设置,该标志默认为逻辑 CPU 的数量。

每个构建脚本从 Cargo 继承一个作业槽,并应努力在运行时仅使用一个 CPU。如果脚本想要并行使用更多 CPU,则应使用 jobserver crate 与 Cargo 协调。

例如,cc crate 可以启用可选的 parallel 功能,该功能将使用 jobserver 协议尝试同时构建多个 C 文件。