I’ve used Debian Linux (specifically Debian Testing, the rolling, regularly-updated version) as my standard desktop environment for years, I’m very happy with it and comfortable developing software for it. But in the Rust community, it’s considered polite to make sure your libraries and tools also support Windows: it’s a tier-1 platform for the Rust compiler, anything that might wind up in Firefox needs Windows support, and so forth. Not least, retep998 has put a huge effort into making Rust work nicely on Windows, including patiently explaining Windows quirks to an often POSIX-centric audience, and it would be a shame to disappoint him.
Therefore, when I set out to write a library that involved path handling, I decided to make sure it worked on Windows as well as Linux. The actual path-handling turned out to be rather knotty, as most cross-platform code tends to be, but cross-compiling was much easier than I expected.
Unlike many other compilers
(such as gcc
),
every copy of the Rust compiler can compile code
for any supported target platform,
so the actual “compilation” part is easy.
Where normally you’d run something like:
cargo build
…instead you can run:
cargo build --target x86_64-pc-windows-gnu
and you’ll get a proper, working…
error[E0463]: can't find crate for `std`
|
= note: the `x86_64-pc-windows-gnu` target may not be installed
…OK, it doesn’t actually work. While Rust will happily compile your crate for whatever target you want, actually making a working program also requires a copy of the standard library built for that target.
Since I’m using my rustbud tool
to manage my Rust toolchain,
I just mention it in my rustbud-spec.toml
:
[toolchain.optional]
rust-std = ["x86_64-pc-windows-gnu"]
…and then every time I activate that environment, I’ll have the Windows version of the stdlib available. If you’re using rustup instead (as most people probably are), this is the command you’ll need:
rustup target add x86_64-pc-windows-gnu
And now when we build our program…
error: linking with `gcc` failed: exit code: 1
|
= note: "gcc" "-Wl,--enable-long-section-names" "-fno-use-linker-plugin" "-Wl,--nxcompat" "-nostdlib" "-m64"
[...skip lots of lines ...]
= note: /usr/bin/ld: unrecognized option '--enable-long-section-names'
/usr/bin/ld: use the --help option for usage information
collect2: error: ld returned 1 exit status
…it still doesn’t work.
When cross-compiling,
the compiler is only part of the picture.
The compiler takes your source code
and turns it into machine-code
that the target CPU can execute,
but the linker takes all that machine-code
and bundles it together into an executable
that the target operating system can load and run.
In order to produce Windows programs,
we need a linker that understands the Windows .exe
file format.
Luckily,
Debian provides packages for the whole gcc
toolchain
built to cross-compile Windows executables,
including the linker we need.
You can install it with:
sudo apt install mingw-w64
…and then you’ll have a program named x86_64-w64-mingw32-gcc
which is exactly the one Rust wants.
Unfortunately,
as the previous error suggested,
Rust tries to use gcc
instead of this new name,
so it’s still going to break.
To tell Rust which linker to use,
add the following to ~/.cargo/config
:
[target.x86_64-pc-windows-gnu]
linker = "/usr/bin/x86_64-w64-mingw32-gcc"
(If you set $CARGO_HOME
to point at some other location,
you can put this config file there instead,
but if you put it in the default location
it’ll work regardless of what $CARGO_HOME
is set to.)
Now we can finally build an executable for Windows!
$ cargo build --target x86_64-pc-windows-gnu
Compiling windows-demo v0.1.0 (file:///home/st/windows-demo)
Finished dev [unoptimized + debuginfo] target(s) in 1.36 secs
$ ls target/x86_64-pc-windows-gnu/debug/*.exe
target/x86_64-pc-windows-gnu/debug/windows-demo.exe
But… now that we’ve built it, how do we even know it works?
Testing Windows software ultimately requires a copy of Windows to test against, but running a full Windows VM can take a lot of CPU power and RAM, not to mention the awkwardness of getting your program into the VM and the test-results back out.
Although it’s not Real Windows, testing with Wine can be a fast and easy way to shake out the most glaring portability problems, and it’s pretty easy to get it going:
Install Wine itself, with support for 64-bit Windows (since we’re working with x86_64 here):
sudo apt install wine wine64
Install the package that teaches Linux to use Wine when trying to run .exe:
sudo apt install wine-binfmt
Make a temporary directory for Wine to keep its fake-Windows files in
(I usually put it in Cargo’s target/
directory,
so it will get cleaned up along with everything else):
mkdir target/wineprefix
export WINEPREFIX=$PWD/target/wineprefix
Now I can run the executable I built earlier:
$ cargo run --target x86_64-pc-windows-gnu
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/x86_64-pc-windows-gnu/debug/windows-demo.exe`
Hello, world!
You can run tests the same way:
$ cargo test --target x86_64-pc-windows-gnu
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/x86_64-pc-windows-gnu/debug/deps/windows_demo-3e68a12a37931af9.exe
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
The first time you run a command like this,
Wine may complain bitterly as it sets up the $WINEPREFIX
directory,
but after that it should be happy enough.
So far,
I’ve always told Rust to use the target x86_64-pc-windows-gnu
,
which means 64-bit Windows.
However,
there’s still a lot of 32-bit Windows installations
out there in the world,
so maybe you’d like to produce 32-bit binaries.
The “32-bit Windows” target
is i686-pc-windows-gnu
,
but unfortunately it can’t easily be used from Linux.
Issue 32859 has all the details
but as I understand it,
MinGW can be configured in different ways.
While there’s a single, obviously correct configuration
for 64-bit Windows,
there’s two possible configurations with different trade-offs
for 32-bit Windows,
and Debian and Rust have different opinions about
which one is “best”.
My current recommendation is to ignore 32-bit Windows,
or use a service like AppVeyor
that provides a Real Windows installation to build on.
This entire document has talked about MinGW, the Windows port of gcc, but among actual Windows developers Microsoft’s own Visual C++ compiler is more popular, and of course it follows Windows’ platform conventions much more closely.
Unfortunately,
using MSVC from Linux is vastly more complicated
than just apt install
ing the right cross-compiler,
and requires
(among other things)
installing Visual Studio on a Windows machine
so you can copy the required files and directories
across to Linux.
Personally, since I’ll eventually need a Windows VM anyway for testing, and I’d need a Windows VM to install MSVC, I’d rather compile my Rust program in the same VM. If you really want to try it yourself, though, there are official instructions.