Getting an idea, planning it out, implementing your Rust code, and releasing the final build is a long process full of unexpected issues. Cross compilation of your code will allow you to reach more users, but it requires knowledge of building executables for different runtime environments. Luckily, this post will help in getting your Rust application running on multiple architectures, including x86 for Windows.
Overview
You want to vet your idea with as many users as possible, so you need to be able to compile your code for multiple architectures. Your users have their own preferences on what machines and OS to use, so we should do our best to meet them in their preferred set-up. This is why it’s critical to pick a language or framework that lends itself to support multiple ways to export your code for multiple target environments with minimal developer effort. Also, it’d be better to have tooling in place to help automate this export process.
If we invest some time in the beginning to pick the right coding language and automation tooling, then we’ll avoid the headaches of not being able to reach a wider audience without the use of cumbersome manual steps. Basically, we need to remove as many barriers as possible between our code and our audience.
This post will cover building a custom Docker image, instantiating a container from that image, and finally using that container to cross compile your Rust code. Your code will be compiled, and an executable will be created for your target environment within your working directory.
What You’ll Need
- Your Rust code (to help you get started, you can use the source code from this git repo)
- The latest version of Docker Desktop
Getting Started
My Rust directory has the following structure:
. ├── Cargo.lock ├── Cargo.toml └── src └── main.rs
The lock file and toml
file both share the same format. The lock file lists packages and their properties. The Cargo program maintains the lock file, and this file should not be manually edited. The toml
file is a manifest file that specifies the metadata of your project. Unlike the lock file, you can edit the toml
file. The actual Rust code is in main.rs
. In my example, the main.rs
file contains a version of the game Snake that uses ASCII art graphics. These files run on Linux machines, and our goal is to cross compile them into a Windows executable.
The cross compilation of your Rust code will be done via Docker. Download and install the latest version of Docker Desktop. Choose the version matching your workstation OS — and remember to choose either the Intel or Apple (M-series) processor variant if you’re running macOS.
Creating Your Docker Image
Once you’ve installed Docker Desktop, navigate to your Rust directory. Then, create an empty file called Dockerfile
within that directory. The Dockerfile
will contain the instructions needed to create your Docker image. Paste the following code into your Dockerfile
:
FROM rust:latest RUN apt update && apt upgrade -y RUN apt install -y g++-mingw-w64-x86-64 RUN rustup target add x86_64-pc-windows-gnu RUN rustup toolchain install stable-x86_64-pc-windows-gnu WORKDIR /app CMD ["cargo", "build", "--target", "x86_64-pc-windows-gnu"]
Setting Up Your Image
The first line creates your image from the Rust base image. The next command upgrades the contents of your image’s packages to the latest version and installs mingw, an open source program that builds Windows applications.
Compiling for Windows
The next two lines are key to getting cross compilation working. The rustup program is a command line toolchain manager for Rust that allows Rust to support compilation for different target platforms. We need to specify which target platform to add for Rust (a target specifies an architecture which can be compiled into by Rust). We then install that toolchain into Rust. A toolchain is a set of programs needed to compile our application to our desired target architecture.
Building Your Code
Next, we’ll set the working directory of our image to the app folder. The final line utilizes the CMD
instruction in our running container. Our command instructs Cargo, the Rust build system, to build our Rust code to the designated target architecture.
Building Your Image
Let’s save our Dockerfile
, and then navigate to that directory in our terminal. In the terminal, run the following command:
docker build . -t rust_cross_compile/windows
Docker will build the image by using the current directory’s Dockerfile
. The command will also tag this image as rust_cross_compile/windows
.
Running Your Container
Once you’ve created the image, then you can run the container by executing the following command:
docker run --rm -v ‘your-pwd’:/app rust_cross_compile/windows
The -rm
option will remove the container when the command completes. The -v
command allows you to persist data after a container has existed by linking your container storage with your local machine. Replace ‘your-pwd’
with the absolute path to your Rust directory. Once you run the above command, then you will see the following directory structure within your Rust directory:
. ├── Cargo.lock ├── Cargo.toml └── src └── main.rs └── target └── debug └── x86_64-pc-windows-gnu └── debug termsnake.exe
Running Your Rust Code
You should now see a newly created directory called target
. This directory will contain a subdirectory that will be named after the architecture you are targeting. Inside this directory, you will see a debug directory that contains the executable file. Clicking the executable allows you to run the application on a Windows machine. In my case, I was able to start playing the game Snake:
Running Rust on armv7
We have compiled our application into a Windows executable, but we can modify the Dockerfile
like the below in order for our application to run on the armv7 architecture:
FROM rust:latest RUN apt update && apt upgrade -y RUN apt install -y g++-arm-linux-gnueabihf libc6-dev-armhf-cross RUN rustup target add armv7-unknown-linux-gnueabihf RUN rustup toolchain install stable-armv7-unknown-linux-gnueabihf WORKDIR /app ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc CC_armv7_unknown_Linux_gnueabihf=arm-linux-gnueabihf-gcc CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++ CMD ["cargo", "build", "--target", "armv7-unknown-linux-gnueabihf"]
Running Rust on aarch64
Alternatively, we could edit the Dockerfile
with the below to support aarch64:
FROM rust:latest RUN apt update && apt upgrade -y RUN apt install -y g++-aarch64-linux-gnu libc6-dev-arm64-cross RUN rustup target add aarch64-unknown-linux-gnu RUN rustup toolchain install stable-aarch64-unknown-linux-gnu WORKDIR /app ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ CMD ["cargo", "build", "--target", "aarch64-unknown-linux-gnu"]
Another way to compile for different architectures without going through the creation of a Dockerfile
would be to install the cross project, using the cargo install -f cross
command. From there, simply run the following command to start the build:
cross build --target x86_64-pc-windows-gnu
Conclusion
Docker Desktop allows you to quickly build a development environment that can support different languages and frameworks. We can build and compile our code for many target architectures. In this post, we got Rust code written on Linux to run on Windows, but we don’t have to limit ourselves to just that example. We can pick many other languages and architectures. Alternatively, Docker Buildx is a tool that was designed to help solve these same problems. Checkout more documentation of Buildx here.