交叉编译是一个对于常见需求来说比较正式的术语
-
你想用你的笔记本电脑为 Android、iOS 或你的路由器构建一个应用程序。
-
你想在你的 Mac 上编写、测试和构建代码,但将其部署到你的 Linux 服务器上。
-
你想让你的基于 Linux 的构建服务器为所有你发布的平台生成二进制文件。
-
你想构建一个可以发布到任何 Linux 平台的超便携二进制文件。
-
你想使用 Emscripten 或 WebAssembly 来针对浏览器。
换句话说,你想在一个“主机”平台上开发/构建,但得到一个在不同“目标”平台上运行的最终二进制文件。
由于 LLVM 后端的存在,原则上一直可以交叉编译 Rust 代码:只需告诉后端使用不同的目标!事实上,勇敢的黑客们已经将 Rust 部署到嵌入式系统上,例如 Raspberry Pi 3、裸机 ARM、运行 OpenWRT 的 MIPS 路由器,以及许多其他系统。
但在实践中,你需要做很多事情才能使其正常工作:合适的 Rust 标准库、一个交叉编译的 C 工具链,包括链接器、头文件和 C 库的二进制文件,等等。这通常需要仔细阅读各种博客文章和软件包安装程序,才能让一切都“恰到好处”。而且,对于每对主机和目标平台,所需的工具集可能都不一样。
Rust 社区一直在努力实现“一键式交叉编译”的目标。我们希望通过运行单个命令,为给定的主机/目标对提供完整的设置。今天,我们很高兴地宣布,这项工作中的大部分已经进入测试阶段:我们正在为各种目标构建 Rust 标准库的二进制文件,并通过一个名为 rustup 的新工具将它们提供给你。
介绍 rustup
本质上,rustup 是 Rust 的工具链管理器。它可以下载和切换所有支持平台的 Rust 编译器和标准库的副本,并跟踪 Rust 的 nightly、beta 和 release 通道,以及特定版本。从这个意义上说,rustup 与 Ruby 和 Python 的 rvm、rbenv 和 pyenv 工具类似。在本文的剩余部分,我将介绍所有这些功能以及它们在哪些情况下有用。
今天,rustup 是一个命令行应用程序,我将向你展示一些它的示例,但它也是一个 Rust 库,最终这些功能预计将在适当的情况下通过图形界面呈现——尤其是在 Windows 上。最终,设置交叉编译应该只需要在 Rust 安装程序中勾选一个框。
我们的目标不仅仅是管理 Rust 工具链:为了获得真正的“一键式”交叉编译体验,它还需要设置 C 工具链。该功能今天还没有发布,但我们希望在未来几个月内将其整合进来。
基本工具链管理
让我们从一个简单的例子开始:安装多个 Rust 工具链。在这个例子中,我创建一个新的库“hello”,然后使用 rustc 1.8 测试它,然后使用 rustup 安装并测试同一个 crate 在 1.9 beta 上。
这是一种简单的方法,可以验证你的代码是否在下一个 Rust 版本上正常工作。这是良好的 Rust 公民精神!
我们可以使用 rustup show
来查看已安装的工具链,并使用 rustup update
来保持它们与 Rust 的发布版本同步。
最后,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
。这个标识符被称为“目标三元组”:“目标”是因为它指定了编译器为其生成(目标)机器代码的平台;“三元组”则是出于历史原因(在许多情况下,“三元组”实际上是四元组)。目标三元组是我们用来引用特定常见平台的基本方法;rustc
默认情况下知道 56 个目标三元组,而 rustup
今天可以获取 14 个编译器和 30 个标准库。
示例:在 Linux 上构建静态二进制文件
现在我们已经有了基本组件,让我们将它们应用到一个简单的交叉编译任务中:为 Linux 构建一个超便携的静态二进制文件。
Linux 的一个独特功能,也是越来越受到人们重视的功能,是它稳定的系统调用接口。由于 Linux 内核在维护向后兼容的内核接口方面付出了巨大的努力,因此可以分发没有动态库依赖项的 ELF 二进制文件,这些二进制文件可以在任何版本的 Linux 上运行。除了是使 Docker 成为可能的特性之一之外,它还允许开发人员构建自包含的应用程序,并将它们部署到运行 Linux 的任何机器上,无论它是 Ubuntu、Fedora 还是任何其他发行版,以及它们安装的软件库的具体组合。
今天的 Rust 依赖于 libc,在大多数 Linux 上,这意味着 glibc。完全静态链接 glibc 在技术上具有挑战性,这在使用它来生成真正独立的二进制文件时会带来困难。幸运的是,存在一个替代方案:musl,这是一个小型、现代的 libc 实现,可以轻松地进行静态链接。Rust 从 1.1 版本开始就与 musl 兼容,但直到最近,开发人员才需要构建自己的编译器才能从中受益。
有了这些背景知识,让我们逐步完成编译静态链接的 Linux 可执行文件的过程。在这个例子中,你需要运行 Linux——也就是说,你的主机平台将是 Linux,你的目标平台也将是 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 所谓的 “独立工具链”。我们将其放在名为 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 级别。目前,Rust 的 arm-linux-androideabi
目标是针对 Android API 级别 18 构建的,理论上应该与后续的 Android API 级别向前兼容。因此,我们选择级别 18 来获得 Rust 目前允许的最大 Android 兼容性。
我们最后要做的事情是告诉 Cargo 在哪里可以找到 Android 链接器,它位于我们刚刚创建的独立 NDK 工具链中。为此,我们在 .cargo/config
中使用“linker”值配置 arm-linux-androideabi
目标。在进行此操作的同时,我们将把此项目的默认目标设置为 Android,这样我们就不必一直使用 --target
选项调用 cargo。
[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 的设备上运行的潜力。在这篇文章中,我向您展示了 Rust 使用 rustup 工具可以做的事情。今天,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 的成功,但它仍然处于测试阶段,存在一些 关键的未解决错误,并且尚未成为 Rust 的官方推荐安装方法(尽管您应该尝试一下)。我们将继续征求反馈,进行改进,并修复错误。然后,我们将通过 将其嵌入到类似于 Windows 正式安装程序的 GUI 中 来改进 rustup 在 Windows 上的安装体验。
到那时,我们可能会更新 www.rust-lang.org 上的下载说明 以推荐 rustup。我预计所有现有的安装方法都将保留,包括非 rustup Windows 安装程序,但在那时,我们的重点将是通过 rustup 改进安装体验。rustup 本身也可能被打包到 Homebrew 和 apt 等包管理器中。
如果您想亲自尝试 rustup,请访问 www.rustup.rs 并按照说明操作。然后在 专门的论坛主题 上留下反馈,或在问题跟踪器上 提交错误。有关 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