Screwtape's Notepad

Building Rust programs for Windows on Debian Linux

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.

Building an executable

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

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:

  1. Install Wine itself, with support for 64-bit Windows (since we’re working with x86_64 here):

    sudo apt install wine wine64
    
  2. Install the package that teaches Linux to use Wine when trying to run .exe:

    sudo apt install wine-binfmt
    
  3. 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.

What about 32-bit binaries?

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.

What about the MSVC toolchain?

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 installing 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.