在 Windows 和 Powershell 上使用 Docker 交叉编译 rust 程序

我有一个 rust 程序原来是跑在 windows 下,现在需要在 linux 下运行,花了不少时间, 记录一下心得。

问题

有经验的朋友应该知道,rust 其实本身对跨平台做得还是不错的, 有时候你甚至只要加一个输出平台的 target,就能编出对应平台的执行档,但是我们这次没有这么简单, 因为这个程序的依赖库需要 OpenSSL。

OpenSSL 是用 C 编写的,不同平台需要有不同的已编译的库,动态库编译也还不算困难, 但是要求运行程序的环境也要有对应的 so 文件;静态库编译的程序可以避免这个问题, 但是静态编译却比较麻烦。

思路

到这里我们起码有了一个目标,就是搭建一个 linux 的编译环境,准备 OpenSSL 的静态库, 准备静态库可能会需要我们自行编译 OpenSSH 源码。

显然这次我们没有这么做,因为我在寻找解决方案的时候发现,也许这个脏活累活已经有人帮我们做了: 有已经配置好的 docker 镜像。

正好我的环境已经装了 docker desktop,那试一下我想也未尝不可。

messense/rust-musl-cross:x86_64-musl(failed)

TLDR: 以下部分最后失败了,不过包含了一些理解和说明的过程,如果你赶时间,可以直接看下一部分。

首先我找到的是 messense/rust-musl-cross:x86_64-musl 这个镜像,于是又看到一个新名词, musl,简单了解了下,这是一个 c 的静态链接库,看上去非常符合我的需求,因为我也是不喜欢动态依赖, 部署最好是一个执行档解决所有问题。根据说明,这样可以执行编译:

alias rust-musl-builder='docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:armv7-musleabihf'
rust-musl-builder cargo build --release

看上去很简单,第一行起了一个别名,第二行执行 build,但是实际上隐藏了很多细节。 需要你对 bash 和 docker 都有一定的了解。首先我们得知道这是 bash 命令, 因为我这里是 windows powershell,所以需要改一下写法,不过我对 powershell 也不是很熟, 不确定有没有 alias。不过既然可以用 alias,那么我们应该也可以写一个长命令:

docker run --rm -it -v"$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release

这样应该就可以了,但是我想可能也有朋友和我一样也有疑问:它怎么知道我要编哪份代码的? 其实这行命令不是在哪里都可以执行的,需要在程序的根目录执行。有这个解释, 可能我们才比较清楚,$(pwd)是拿到当前目录,-v 是把这个目录映射到后面的容器内路径。

理解了这一点,那么我们就知道,其实也不用在程序的根目录执行这个命令,直接敲上去程序的路径就好:

docker run --rm -it -v"d:/src/rs/test":/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release

第一次运行可能要先把 docker 镜像 pull 下来,这个会花不少时间,接下来真正执行时, 镜像中的 cargo 会从 crate.io 更新,然后抓下来所有的依赖包, 这个也会花不少时间,对于有些网络环境,可能设置代理会快一点,这里不再展开。

不过抓完编译时,我这里还是报错了:

run pkg_config fail: "Could not run `\"pkg-config\" \"--libs\" \"--cflags\" \"openssl\"`\nThe pkg-config command could not be found.\n\nMost likely, you need to install a pkg-config package for your OS.\nTry `apt install pkg-config`, or `yum install pkg-config`,\nor `pkg install pkg-config`, or `apk add pkgconfig` depending on your distribution.\n\nIf you've already installed it, ensure the pkg-config command is one of the\ndirectories in the PATH environment variable.\n\nIf you did not expect this build to link to a pre-installed system library,\nthen check documentation of the openssl-sys crate for an option to\nbuild the library from source, or disable features or dependencies\nthat require pkg-config."

  --- stderr
  thread 'main' panicked at '

  Could not find directory of OpenSSL installation, and this `-sys` crate cannot
  proceed without this knowledge. If OpenSSL is installed and this crate had
  trouble finding it,  you can set the `OPENSSL_DIR` environment variable for the
  compilation process.

  Make sure you also have the development packages of openssl installed.
  For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.

  If you're in a situation where you think the directory *should* be found
  automatically, please open a bug at https://github.com/sfackler/rust-openssl
  and include information about your system as well as this message.

  $HOST = x86_64-unknown-linux-gnu
  $TARGET = x86_64-unknown-linux-musl
  openssl-sys = 0.9.78

  ', /root/.cargo/registry/src/github.com-1ecc6299db9ec823/openssl-sys-0.9.78/build/find_normal.rs:191:5
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...

这和我自己在 wsl 上编译报错的内容差不多,提示的意思是环境没有配置 pkg-config, 没有装 OpenSSL 的开发包,不过这两个我也装过,依然是无法正常编译, 感觉自己解决这个问题可能要花不少时间,所以才想有没有已配置好的镜像, 结果看上去这个镜像没有包含 OpenSSL,官方 GitHub issue 也确认了这个问题,由于他的上游移除了 OpenSSL,所以这个镜像也不含 OpenSSL。

如果不含 OpenSSL,那感觉这个镜像也没什么意义, 官方给的解决方案是增加 openssl-src 的 crate,我试了下好像也没什么用。 于是我去找了下一个镜像。

ekidd/rust-musl-builder

这个镜像是已经注明配好了 OpenSSL,所以估计我们可以开箱即用:

docker run --rm -it -v"d:/src/rs/test":/home/rust/src ekidd/rust-musl-builder cargo build --release

只有一个警告:

WARN rustc_codegen_ssa::back::link Linker does not support -static-pie command line option. Retrying with -static instead.
    Finished release [optimized] target(s) in 8m 58s

我在 wsl 下面跑了一下,也是可以正常运行的。

总结

最后解决看上去非常简单,但是其实走到这一步也花了我很多时间,原本想偷懒不自己搭 OpenSSL 的环境, 但是最后还是感觉找工具也费了不少事。其实主要还是对 docker 的命令不太熟悉。 所以趁还没忘记赶紧记下来,这样下次再遇到应该就少走弯路了:

  1. 准备 docker 环境

  2. 使用以下命令编译

    docker run --rm -it -v"程序实际路径":/home/rust/src ekidd/rust-musl-builder cargo build --release