Skip to content

Latest commit

 

History

History
409 lines (290 loc) · 15 KB

build-script-examples.zh.md

File metadata and controls

409 lines (290 loc) · 15 KB

Build Script Examples

以下部分说明了编写构建脚本的一些示例。

一些常见的构建脚本功能可以通过crates.io.查看build-dependencies keyword看看有什么可用的。以下是一些流行箱子的样本[^†]:

  • bindgen-自动生成到C库的Rust FFI绑定。
  • cc-编译C/C++/assembly。
  • pkg-config-使用pkg-config公用事业
  • cmake-运行cmake构建工具来构建本机库。
  • autocfg, rustc_version, version_check-这些箱子提供了基于当前数据实现条件编译的方法rustc例如编译器的版本。

[^†]:此列表不是背书。评估你的依赖关系,看看哪个适合你的项目。

Code generation

由于各种原因,一些Cargo包需要在编译之前生成代码。在这里,我们将浏览一个简单的示例,该示例生成一个库调用,作为构建脚本的一部分。

首先,让我们看看这个包的目录结构:

.
├── Cargo.toml
├── build.rs
└── src
    └── main.rs

1 directory, 3 files

在这里我们可以看到,我们有一个build.rs在中构建脚本和二进制文件main.rs.此程序包有一个基本清单:

# Cargo.toml

[package]
name = "hello-from-generated-code"
version = "0.1.0"

让我们看看构建脚本中的内容:

// build.rs

use std::env;
use std::fs;
use std::path::Path;

fn main() {
    let out_dir = env::var_os("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("hello.rs");
    fs::write(
        &dest_path,
        "pub fn message() -> &'static str {
            \"Hello, World!\"
        }
        "
    ).unwrap();
    println!("cargo:rerun-if-changed=build.rs");
}

这里有几点值得注意:

  • 脚本使用OUT_DIR环境变量来发现输出文件应该位于何处。它可以使用进程的当前工作目录来查找输入文件的位置,但在这种情况下,我们没有任何输入文件。
  • 一般来说,构建脚本不应修改外部的任何文件OUT_DIR.乍一看似乎没什么问题,但当你把这种箱子当作依赖物使用时,确实会造成问题,因为有一个含蓄的来源于.cargo/registry应该是不变的。cargo打包时不允许使用此类脚本。
  • 这个脚本相对简单,因为它只写出一个生成的小文件。人们可以想象,其他更奇特的操作可能会发生,比如从C头文件或其他语言定义生成一个Rust模块。
  • 这个rerun-if-changed instruction告诉Cargo,只有在生成脚本本身发生更改时,才需要重新运行生成脚本。如果没有这一行,如果包中的任何文件发生更改,Cargo将自动运行构建脚本。如果您的代码生成使用了一些输入文件,您可以在这里打印这些文件的列表。

接下来,让我们来看看图书馆本身:

// src/main.rs

include!(concat!(env!("OUT_DIR"), "/hello.rs"));

fn main() {
    println!("{}", message());
}

这才是真正神奇的地方。库正在使用rustc定义的include! macro结合concat!env!宏来包含生成的文件(hello.rs)进入箱子的汇编。

使用这里显示的结构,箱子可以包含构建脚本本身生成的任意数量的文件。

Building a native library

有时需要将一些本地C或C++代码作为包的一部分来构建。这是另一个利用构建脚本在Rust箱子本身之前构建本机库的优秀用例。例如,我们将创建一个Rust库,它调用C来打印“Hello,World!”。

像上面一样,让我们先看看包的布局:

.
├── Cargo.toml
├── build.rs
└── src
    ├── hello.c
    └── main.rs

1 directory, 4 files

和以前很相似!接下来是清单:

# Cargo.toml

[package]
name = "hello-world-from-c"
version = "0.1.0"
edition = "2021"

现在我们不打算使用任何构建依赖项,所以现在让我们看看构建脚本:

// build.rs

use std::process::Command;
use std::env;
use std::path::Path;

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();

    // Note that there are a number of downsides to this approach, the comments
    // below detail how to improve the portability of these commands.
    Command::new("gcc").args(&["src/hello.c", "-c", "-fPIC", "-o"])
                       .arg(&format!("{}/hello.o", out_dir))
                       .status().unwrap();
    Command::new("ar").args(&["crus", "libhello.a", "hello.o"])
                      .current_dir(&Path::new(&out_dir))
                      .status().unwrap();

    println!("cargo:rustc-link-search=native={}", out_dir);
    println!("cargo:rustc-link-lib=static=hello");
    println!("cargo:rerun-if-changed=src/hello.c");
}

这个构建脚本首先将C文件编译成一个对象文件(通过调用gcc)然后将该对象文件转换为静态库(通过调用ar).最后一步是向Cargo公司反馈,告知我们的产品处于良好状态out_dir编译器应该将箱子链接到libhello.a通过-l static=hello选项

请注意,这种硬编码方法有许多缺点:

  • 这个gcc命令本身不能跨平台移植。例如,Windows平台不太可能gcc,甚至不是所有Unix平台都有gcc这个ar指挥部也处于类似的情况。
  • 这些命令不考虑交叉编译。如果我们为Android这样的平台进行交叉编译,那么gcc将生成一个ARM可执行文件。

不过,不用担心,这就是build-dependencies进来会有帮助的!Cargo生态系统有许多软件包,使这类任务变得更容易、便携和标准化。让我们试试这个cc crate从…起crates.io.首先,将其添加到build-dependencies在里面Cargo.toml:

[build-dependencies]
cc = "1.0"

然后重写构建脚本以使用此箱子:

// build.rs

fn main() {
    cc::Build::new()
        .file("src/hello.c")
        .compile("hello");
    println!("cargo:rerun-if-changed=src/hello.c");
}

这个cc crate对C代码的一系列构建脚本需求进行了抽象:

  • 它调用适当的编译器(MSVC for windows,gcc对明哥来说,cc用于Unix平台等)。
  • 这需要时间TARGET通过向正在使用的编译器传递适当的标志,将变量考虑在内。
  • 其他环境变量,例如OPT_LEVEL, DEBUG等等,都是自动处理的。
  • 标准输出和OUT_DIR地点也由cc图书馆

在这里,我们可以开始看到尽可能多地将功能扩展到常见的构建依赖项,而不是在所有构建脚本中复制逻辑的一些主要好处!

回到案例研究,让我们快速看一下src目录:

// src/hello.c

#include <stdio.h>

void hello() {
    printf("Hello, World!\n");
}
// src/main.rs

// Note the lack of the `#[link]` attribute. We’re delegating the responsibility
// of selecting what to link over to the build script rather than hard-coding
// it in the source file.
extern { fn hello(); }

fn main() {
    unsafe { hello(); }
}

我们走了!这将完成我们使用构建脚本本身从Cargo包构建一些C代码的示例。这也说明了为什么在许多情况下,使用构建依赖项是至关重要的,甚至更简洁!

我们还看到了一个简单的例子,说明了构建脚本如何将箱子作为一个依赖项,仅用于构建过程,而不用于运行时的箱子本身。

Linking to system libraries

此示例演示如何链接系统库,以及如何使用构建脚本来支持此用例。

Rust箱子经常想要链接到系统上提供的本机库,以绑定其功能,或者只是将其用作实现细节的一部分。以平台无关的方式执行此操作时,这是一个相当微妙的问题。如果可能的话,最好是尽可能多地将其外包出去,让消费者尽可能容易地做到这一点。

在本例中,我们将创建一个到系统zlib库的绑定。这是一个在大多数提供数据压缩的类Unix系统上常见的库。这已经在libz-sys crate,但对于这个例子,我们将做一个极其简化的版本。退房the source code举个完整的例子。

为了便于找到图书馆的位置,我们将使用pkg-config crate.这个箱子使用系统的pkg-config用于发现有关库的信息的实用程序。它会自动告诉Cargo连接图书馆需要什么。这可能只适用于具有pkg-config安装。让我们从设置清单开始:

# Cargo.toml

[package]
name = "libz-sys"
version = "0.1.0"
edition = "2021"
links = "z"

[build-dependencies]
pkg-config = "0.3.16"

请注意,我们包括了links输入package桌子这告诉Cargo我们正在连接到libz图书馆查看"Using another sys crate"举个例子来说明这一点。

构建脚本相当简单:

// build.rs

fn main() {
    pkg_config::Config::new().probe("zlib").unwrap();
    println!("cargo:rerun-if-changed=build.rs");
}

让我们用一个基本的FFI绑定来总结这个例子:

// src/lib.rs

use std::os::raw::{c_uint, c_ulong};

extern "C" {
    pub fn crc32(crc: c_ulong, buf: *const u8, len: c_uint) -> c_ulong;
}

#[test]
fn test_crc32() {
    let s = "hello";
    unsafe {
        assert_eq!(crc32(0, s.as_ptr(), s.len() as c_uint), 0x3610a686);
    }
}

cargo build -vv查看生成脚本的输出。在一个具有libz已经安装,它可能看起来像这样:

[libz-sys 0.1.0] cargo:rustc-link-search=native=/usr/lib
[libz-sys 0.1.0] cargo:rustc-link-lib=z
[libz-sys 0.1.0] cargo:rerun-if-changed=build.rs

美好的pkg-config做了所有的工作,找到了图书馆,并告诉Cargo它在哪里。

软件包包含库的源代码,如果在系统中找不到源代码,或者设置了功能或环境变量,则静态地构建源代码,这种情况并不少见。例如,真实的libz-sys crate检查环境变量LIBZ_SYS_STATIC或者static功能从源代码而不是使用系统库构建它。退房the source以获取更完整的示例。

Using another sys crate

当使用links关键点,箱子可以设置元数据,这些元数据可以被依赖于它的其他箱子读取。这提供了一种在箱子之间传递信息的机制。在本例中,我们将创建一个C库,该库使用来自真实世界的zliblibz-sys crate.

如果你有一个依赖zlib的C库,你可以利用libz-sys crate自动查找或构建它。这非常适合跨平台支持,例如通常不安装zlib的Windows。libz-sys sets the include metadata告诉其他包在哪里可以找到zlib的头文件。我们的构建脚本可以使用DEP_Z_INCLUDE环境变量。下面是一个例子:

# Cargo.toml

[package]
name = "zuser"
version = "0.1.0"
edition = "2021"

[dependencies]
libz-sys = "1.0.25"

[build-dependencies]
cc = "1.0.46"

这里我们包括libz-sys这将确保只有一个libz在最终库中使用,并允许我们从构建脚本访问它:

// build.rs

fn main() {
    let mut cfg = cc::Build::new();
    cfg.file("src/zuser.c");
    if let Some(include) = std::env::var_os("DEP_Z_INCLUDE") {
        cfg.include(include);
    }
    cfg.compile("zuser");
    println!("cargo:rerun-if-changed=src/zuser.c");
}

具有libz-sys完成所有繁重的工作后,C源代码现在可能包括zlib头,它应该可以找到该头,即使在尚未安装它的系统上也是如此。

// src/zuser.c

#include "zlib.h"

// … rest of code that makes use of zlib.

Conditional compilation

构建脚本可能会发出rustc-cfg instructions它可以启用可在编译时检查的条件。在本例中,我们将了解openssl crate使用它来支持多个版本的OpenSSL库。

这个openssl-sys crate实现构建和链接OpenSSL库。它支持多个不同的实现(比如LibreSSL)和多个版本。它利用了links键,以便它可以将信息传递给其他构建脚本。它经过的事情之一是version_number密钥,即检测到的OpenSSL版本。构建脚本中的代码看起来像like this:

println!("cargo:version_number={:x}", openssl_version);

此指令会导致DEP_OPENSSL_VERSION_NUMBER环境变量设置在任何箱子中,直接取决于openssl-sys.

这个openssl提供更高级别接口的箱子指定openssl-sys作为一种依赖。这个openssl构建脚本可以读取openssl-sys使用DEP_OPENSSL_VERSION_NUMBER环境变量。它用这个来产生一些cfg values:

// (portion of build.rs)

if let Ok(version) = env::var("DEP_OPENSSL_VERSION_NUMBER") {
    let version = u64::from_str_radix(&version, 16).unwrap();

    if version >= 0x1_00_01_00_0 {
        println!("cargo:rustc-cfg=ossl101");
    }
    if version >= 0x1_00_02_00_0 {
        println!("cargo:rustc-cfg=ossl102");
    }
    if version >= 0x1_01_00_00_0 {
        println!("cargo:rustc-cfg=ossl110");
    }
    if version >= 0x1_01_00_07_0 {
        println!("cargo:rustc-cfg=ossl110g");
    }
    if version >= 0x1_01_01_00_0 {
        println!("cargo:rustc-cfg=ossl111");
    }
}

这些cfg然后可以将值与cfg attribute或者cfg macro有条件地包含代码。例如,在OpenSSL 1.1.1中添加了SHA3支持,所以conditionally excluded对于旧版本:

// (portion of openssl crate)

#[cfg(ossl111)]
pub fn sha3_224() -> MessageDigest {
    unsafe { MessageDigest(ffi::EVP_sha3_224()) }
}

当然,使用它时应该小心,因为它使生成的二进制文件更加依赖于构建环境。在本例中,如果二进制文件被分发到另一个系统,它可能没有完全相同的共享库,这可能会导致问题。