使用 rustup 将 Rust 带到任何地方

2016 年 5 月 13 日 · Brian Anderson

交叉编译(Cross-compilation)是一个令人望而生畏的术语,但它代表了一种常见的需求。

  • 你想用你的笔记本电脑为 Android、iOS 或你的路由器构建应用程序。

  • 你想在你的 Mac 上编写、测试和构建代码,但部署到你的 Linux 服务器上。

  • 你希望基于 Linux 的构建服务器能够为你发布的所有平台生成二进制文件。

  • 你想构建一个可以在任何 Linux 平台部署的超便携二进制文件。

  • 你想使用 EmscriptenWebAssembly 定位到浏览器。

换句话说,你想在一个“宿主”(host)平台上进行开发/构建,但获得一个可在不同的“目标”(target)平台上运行的最终二进制文件

多亏了 LLVM 后端,理论上一直可以交叉编译 Rust 代码:只需告诉后端使用不同的目标即可!实际上,无畏的黑客们已经将 Rust 移植到了嵌入式系统上,例如树莓派 3裸机 ARM运行 OpenWRT 的 MIPS 路由器,以及许多其他平台。

但在实践中,要让它工作起来,你需要准备很多东西:合适的 Rust 标准库,一个交叉编译 C 工具链,包括 C 库的链接器、头文件和二进制文件等等。这通常涉及仔细阅读各种博客文章和软件包安装程序,才能将所有东西都准备妥当。而且,对于每一对宿主和目标平台,所需的工具集都可能不同。

Rust 社区一直在努力实现“一键式交叉编译”的目标。我们希望通过运行一个简单的命令,就能为特定的宿主/目标对提供完整的设置。今天,我们很高兴地宣布,这项工作的主要部分已达到测试版(beta)状态:我们正在为广泛的目标平台构建 Rust 标准库的二进制文件,并通过一个名为 rustup 的新工具将其提供给您。

rustup 简介

核心来说,rustup 是一个 Rust 的工具链管理器(toolchain manager)。它可以下载并切换所有受支持平台上的 Rust 编译器和标准库副本,并跟踪 Rust 的 nightly、beta 和发布版(release)通道(channels),以及特定版本。通过这种方式,rustup 类似于 Ruby 和 Python 的 rvmrbenbpyenv 等工具。在本帖的其余部分,我将详细介绍所有这些功能以及它们适用的场景。

今天,rustup 是一个命令行应用程序,我将向您展示它的一些功能示例,但它也是一个 Rust 库,并且最终这些功能有望在适当的地方通过图形界面呈现——特别是在 Windows 上。设置交叉编译最终应该只需在 Rust 安装程序中勾选一个框即可。

我们的目标不仅仅是管理 Rust 工具链:要实现真正的“一键式”交叉编译体验,还需要设置 C 工具链。该功能目前尚未发布,但我们希望在接下来的几个月内将其整合进来。

基本工具链管理

让我们从一些简单的内容开始:安装多个 Rust 工具链。在这个示例中,我创建了一个新的库 'hello',然后使用 rustc 1.8 测试它,接着使用 rustup 在 1.9 beta 版上安装并测试同一个 crate。

$ cargo new hello && cd hello
$ rustc --version
rustc 1.8.0 (db2939409 2016-04-11)
$ cargo test
   Compiling hello v0.1.0 (file:///home/user/hello)
     Running target/debug/hello-b4f774924ded32e4

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests hello

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
$ rustup install beta
info: syncing channel updates for 'beta-x86_64-unknown-linux-gnu'
info: latest update on 2016-04-11, rust version 1.9.0-beta (e4e8b6668 2016-04-11)
info: downloading component 'cargo'
info: downloading component 'rust-docs'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-docs'
info: installing component 'rust-std'
info: installing component 'rustc'

  beta-x86_64-unknown-linux-gnu installed - rustc 1.9.0-beta (e4e8b6668 2016-04-11)

$ rustup run beta rustc --version
rustc 1.9.0-beta (e4e8b6668 2016-04-11)
$ rustup run beta cargo test
   Compiling hello v0.1.0 (file:///home/user/hello)
     Running target/debug/hello-f4f25bc615ec63f5

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured

   Doc-tests hello

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured

这是验证您的代码在下一个 Rust 版本上是否工作的简单方法。这是一个优秀的 Rust 公民行为!

我们可以使用 rustup show 来查看已安装的工具链,并使用 rustup update 使其与 Rust 的发布版本保持同步。

$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/user/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu (default)
beta-x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.8.0 (db2939409 2016-04-11)

$ rustup update
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
info: syncing channel updates for 'beta-x86_64-unknown-linux-gnu'

   stable-x86_64-unknown-linux-gnu unchanged - rustc 1.8.0 (db2939409 2016-04-11)
     beta-x86_64-unknown-linux-gnu unchanged - rustc 1.9.0-beta (e4e8b6668 2016-04-11)

info: cleaning up downloads & tmp directories

最后,rustup 还可以使用 rustup default 命令更改默认工具链。

$ rustc --version
rustc 1.8.0 (db2939409 2016-04-11)
$ rustup default 1.7.0
info: syncing channel updates for '1.7.0-x86_64-unknown-linux-gnu'
info: downloading component 'rust'
info: installing component 'rust'
info: default toolchain set to '1.7.0-x86_64-unknown-linux-gnu'

  1.7.0-x86_64-unknown-linux-gnu installed - rustc 1.7.0 (a5d1e7a59 2016-02-29)

$ rustc --version
rustc 1.7.0 (a5d1e7a59 2016-02-29)

在 Windows 上,Rust 同时支持 GNU 和 MSVC ABI。你可能想从 Windows 上的默认稳定工具链(它定位 32 位 x86 架构和 GNU ABI)切换到一个定位 64 位 MSVC ABI 的稳定工具链。

$ rustup default stable-x86_64-pc-windows-msvc
info: syncing channel updates for 'stable-x86_64-pc-windows-msvc'
info: downloading component 'rustc'
info: downloading component 'rust-std'
...

  stable-x86_64-pc-windows-msvc installed - rustc 1.8.0-stable (db2939409 2016-04-11)

在这里,“stable”工具链名称后面附加了一个额外的标识符,表示编译器的架构,在本例中是 x86_64-pc-windows-msvc。这个标识符被称为“目标三元组(target triple)”:“target”是因为它指定了编译器生成(目标)机器代码的平台;而“triple”是出于历史原因(现在许多情况下“三元组”实际上是四元组)。目标三元组是我们指代特定常见平台的基本方式;默认情况下,rustc 知道其中的 56 个,而今天的 rustup 可以获取 14 个目标的编译器和 30 个目标的标准库。

示例:在 Linux 上构建静态二进制文件

既然我们已经具备了基础要素,让我们将它们应用于一个简单的交叉编译任务:为 Linux 构建一个超便携的静态二进制文件。

Linux 的一个日益受到赞赏的独特特性是其稳定的 syscall 接口。由于 Linux 内核在维护向后兼容的内核接口方面付出了巨大的努力,因此可以分发不依赖动态库的 ELF 二进制文件,这些文件可以在任何版本的 Linux 上运行。除了是使 Docker 成为可能的功能之一外,它还允许开发人员构建自包含的应用程序,并将其部署到运行 Linux 的任何机器上,无论它是 Ubuntu、Fedora 还是任何其他发行版,也无论它们安装的软件库组合如何。

今天的 Rust 依赖于 libc,在大多数 Linux 系统上这意味着 glibc。完全静态链接 glibc 在技术上具有挑战性,这在使用它生成真正的独立二进制文件时会带来困难。幸运的是,存在另一种选择:musl,这是一个小型现代化的 libc 实现,可以轻松地进行静态链接。自版本 1.1 以来,Rust 就与 musl 兼容,但直到最近,开发人员仍然需要自己构建编译器才能从中受益。

有了这些背景知识,让我们来演示如何编译一个静态链接的 Linux 可执行文件。在这个示例中,您需要在 Linux 上运行——也就是说,您的宿主平台(host platform)是 Linux,您的目标平台(target platform)也是 Linux,只是不同的风味:musl。(是的,即使两个目标都是 Linux,这在技术上仍然是交叉编译)。

我将在 Ubuntu 16.04 上运行(使用这个 Docker 镜像)。我们将构建基本的 hello world 程序。

rust:~$ cargo new --bin hello && cd hello
rust:~/hello$ cargo run
   Compiling hello v0.1.0 (file:///home/rust/hello)
     Running `target/debug/hello`
Hello, world!

这是使用默认的 x86_64-unknown-linux-gnu 目标。您可以看到它有很多动态依赖项。

rust:~/hello$ ldd target/debug/hello
        linux-vdso.so.1 =>  (0x00007ffe5e979000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fca26d03000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fca26ae6000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fca268cf000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca26506000)
        /lib64/ld-linux-x86-64.so.2 (0x000056104c935000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fca261fd000)

要改为编译 musl,请使用参数 --target=x86_64-unknown-linux-musl 调用 cargo。如果我们直接尝试这样做,将会收到一个错误:

rust:~/hello$ cargo run --target=x86_64-unknown-linux-musl
   Compiling hello v0.1.0 (file:///home/rust/hello)
error: can't find crate for `std` [E0463]
error: aborting due to previous error
Could not compile `hello`.
...

错误提示编译器找不到 std。这当然是因为我们还没有安装它。

要开始交叉编译,您需要为目标平台获取标准库。以前,这是一个容易出错的手动过程——就像我之前提到的那些博客文章一样。但是有了 rustup,这只是日常工作流程的一部分。

rust:~/hello$ rustup target add x86_64-unknown-linux-musl
info: downloading component 'rust-std' for 'x86_64-unknown-linux-musl'
info: installing component 'rust-std' for 'x86_64-unknown-linux-musl'
rust:~/hello$ rustup show
installed targets for active toolchain
--------------------------------------

x86_64-unknown-linux-gnu
x86_64-unknown-linux-musl

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.8.0 (db2939409 2016-04-11)

所以我现在运行的是针对 64 位 x86 Linux 的 1.8 工具链,这由 x86_64-unknown-linux-gnu 目标三元组指示,现在我也可以目标 x86_64-unknown-linux-musl 了。很酷。毫无疑问,我们已经准备好构建一个可以发布到云端的漂亮的静态链接二进制文件了。让我们试试看:

rust:~/hello$ cargo run --target=x86_64-unknown-linux-musl
   Compiling hello v0.1.0 (file:///hello)
     Running `target/x86_64-unknown-linux-musl/debug/hello`
Hello, world!

然后……就成功了!对它运行 ldd 以证明它是真的静态链接:

rust:~/hello$ ldd target/x86_64-unknown-linux-musl/debug/hello
        not a dynamic executable

现在,把这个 hello 二进制文件复制到任何运行 Linux 的 x86_64 机器上,它都能正常运行。

如需更高级地使用 musl,请考虑 rust-musl-builder,这是一个为 musl 开发设置的 Docker 镜像,它方便地包含了为 musl 编译的常用 C 库。

示例:在 Android 上运行 Rust

再举一个例子。这次是从 Linux 构建 Android 应用,即从 x86_64-unknown-linux-gnu 构建 arm-linux-androideabi。这也可以从 OS X 或 Windows 上完成,不过在 Windows 上的设置略有不同。

要为 Android 构建,我们需要添加 Android 目标,所以让我们再设置一个 'hello, world' 项目并安装它。

rust:~$ cargo new --bin hello && cd hello
rust:~/hello$ rustup target add arm-linux-androideabi
info: downloading component 'rust-std' for 'arm-linux-androideabi'
info: installing component 'rust-std' for 'arm-linux-androideabi'
rust:~/hello$ rustup show
installed targets for active toolchain
--------------------------------------

arm-linux-androideabi
x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.8.0 (db2939409 2016-04-11)

那么,如果我们尝试直接构建 'hello' 项目而不进行进一步安装,会发生什么呢?

rust:~/hello$ cargo build --target=arm-linux-androideabi
   Compiling hello v0.1.0 (file:///home/rust/hello)
error: linking with `cc` failed: exit code: 1
... (lots of noise elided)
error: aborting due to previous error
Could not compile `hello`.

问题在于我们还没有支持 Android 的链接器,所以让我们稍作一下,谈谈为 Android 构建。要进行 Android 开发,我们需要 Android NDK。它包含 rustc 创建 Android 二进制文件所需的链接器。要仅构建针对 Android 的 Rust 代码,我们只需要 NDK,但对于实际开发,我们还需要 Android SDK

在 Linux 上,使用以下命令下载并解压它们(此处不包含输出):

rust:~/home$ cd
rust:~$ curl -O https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz
rust:~$ tar xzf android-sdk_r24.4.1-linux.tgz
rust:~$ curl -O https://dl.google.com/android/repository/android-ndk-r10e-linux-x86_64.zip
rust:~$ unzip android-ndk-r10e-linux-x86_64.zip

我们还需要创建 NDK 所称的 “独立工具链(standalone toolchain)”。我们将把它放在一个名为 android-18-toolchain 的目录中。

rust:~$ android-ndk-r10e/build/tools/make-standalone-toolchain.sh \
      --platform=android-18 --toolchain=arm-linux-androideabi-clang3.6 \
      --install-dir=android-18-toolchain --ndk-dir=android-ndk-r10e/ --arch=arm
Auto-config: --toolchain=arm-linux-androideabi-4.8, --llvm-version=3.6
Copying prebuilt binaries...
Copying sysroot headers and libraries...
Copying c++ runtime headers and libraries...
Copying files to: android-18-toolchain
Cleaning up...
Done.

注意这些命令的几个事项。首先,我们下载的 NDK android-ndk-r10e-linux-x86_64.zip 不是最新版本(撰写本文时是 'r11c')。Rust 的 std 是针对 r10e 构建的,并链接到 NDK 中不再包含的符号。所以目前我们必须使用旧版本的 NDK。其次,在构建独立工具链时,我们向 make-standalone-toolchain.sh 传递了参数 --platform=android-18。这里的 "18" 是 Android API 等级(API level)。目前,Rust 的 arm-linux-androideabi 目标是针对 Android API level 18 构建的,理论上应该与后续的 Android API level 向前兼容。因此,我们选择 level 18 以获得 Rust 当前允许的最大 Android 兼容性。

最后一步是告诉 Cargo 在哪里找到 Android 链接器,它位于我们刚刚创建的独立 NDK 工具链中。为此,我们在 .cargo/config 文件中配置 arm-linux-androideabi 目标,设置 'linker' 值。同时,我们还可以将此项目的默认目标设置为 Android,这样就无需在每次调用 cargo 时都加上 --target 选项了。

[build]
target = "arm-linux-androideabi"

[target.arm-linux-androideabi]
linker = "/home/rust/android-18-toolchain/bin/arm-linux-androideabi-clang"

现在让我们回到 'hello' 项目目录并再次尝试构建:

rust:~$ cd hello
rust:~/hello$ cargo build
   Compiling hello v0.1.0 (file:///home/rust/hello)

成功了!当然,仅仅构建成功还不是终点。你还需要将你的代码打包成 Android APK。为此,你可以使用 cargo-apk

Rust 在其他地方

Rust 是一个有潜力在任何拥有 CPU 的设备上运行的软件平台。在这篇文章中,我向您展示了使用 rustup 工具,Rust 已经可以做到的一小部分。今天,Rust 运行在您日常使用的大多数平台上。明天,它将无处不在。

那么接下来您可以期待什么呢?

在接下来的几个月里,我们将继续消除 Rust 交叉编译的障碍。今天,rustup 提供了标准库的访问权限,但正如我们在本文中看到的那样,交叉编译不仅仅是 rustc + std。获取和配置链接器以及 C 工具链是最令人烦恼的部分——每种宿主和目标平台的组合都需要略微不同的设置。我们希望简化这一点,并将在 rustup 中添加“NDK 支持”。这意味着什么取决于具体情况,但我们将从需求量最大的平台(如 Android)开始,并尝试尽可能自动化非 Rust 工具链组件的检测、安装和配置。例如,在 Android 上,我们希望除了接受许可协议之外,自动化所有基本初始设置。

除此之外,还有多项工作正在进行中,以改进 Rust 交叉编译工具,包括 xargo(可用于为 rustup 不支持的目标构建标准库)和 cargo-apk(从 Cargo 包构建 Android 包)。

最后,Rust 前景中最令人兴奋的平台并不是系统语言的传统目标:web。今天,使用 Emscripten,通过将 LLVM IR 转换为 JavaScript(或 JavaScript 的 asm.js 子集),在 web 上运行 C++ 代码变得非常容易。而即将推出的 WebAssembly (wasm) 标准将巩固 web 平台作为编程语言的一等目标。

在不久的将来,Rust 拥有独特的优势,将成为功能最强大且最易于使用的 wasm 目标语言。 使 Rust 如此容易移植到真实硬件上的特性,使得将 Rust 移植到 wasm 几乎微不足道。对于包含垃圾收集器等复杂运行时的语言来说,情况并非如此。

Rust 已经移植到 Emscripten(至少两次),但代码尚未完全合并。然而,今年夏天这一切正在发生:Rust + Emscripten。Rust 在 Web 上。Rust 无处不在。

后记

虽然许多人报告使用 rustup 成功了,但它仍处于测试版(beta)阶段,存在一些重要的未解决错误,并且尚未成为 Rust 官方推荐的安装方法(尽管您应该尝试一下)。我们将继续征求反馈意见,完善功能并修复错误。然后,我们将通过将其嵌入到像一个合适的 Windows 安装程序那样的 GUI 中来改进 rustup 在 Windows 上的安装体验。

届时,我们很可能会更新 www.rust-lang.org 上的下载说明,推荐使用 rustup。我预计所有现有的安装方法仍然可用,包括非 rustup 的 Windows 安装程序,但届时我们的重点将是通过 rustup 来改善安装体验。 rustup 本身也有可能被打包进 Homebrew 和 apt 等软件包管理器。

如果您想亲自尝试 rustup,请访问 www.rustup.rs 并按照说明操作。然后,请在专门的论坛帖子中留下反馈,或在 issue tracker 上提交错误报告。有关 rustup 的更多信息可在 README 中找到。

致谢

没有许多个人的帮助,Rust 不会成为如此强大的系统。感谢 Diggory Blake 创建了 rustup,感谢 Jorge Aparicio 修复了许多交叉编译错误并记录了过程,感谢 Tomaka 在 Android 上开拓了 Rust,感谢 Alex Crichton 为 Rust 的众多平台创建了发布基础设施。

感谢所有 rustup 贡献者:Alex Crichton, Brian Anderson, Corey Farwell, David Salter, Diggory Blake, Jacob Shaffer, Jeremiah Peschka, Joe Wilm, Jorge Aparicio, Kai Noda, Kamal Marhubi, Kevin K, llogiq, Mika Attila, NODA, Kai, Paul Padier, Severen Redwood, Taylor Cramer, Tim Neumann, trolleyman, Vadim Petrochenkov, V Jackson, Vladimir, Wayne Warren, Yasushi Abe, Y. T. Chung