Back

Cross Compiling Rust GTK Projects for Windows

2021-02-18

Table of Contents
  1. Hello, World!
  2. The Dockerfile
  3. docker Commands

I'm going to be taking a stab at writing a gopher client GUI application so I figured I'd just get started with it. I chose rust gtk because I know someone else on hackernews did it so I thought I'll be treading ground that has already been softened!

I hit the ground and it was quite hard.

I was planning to write my code on Centos 7 but it looks like due to mingw, which is the windows development libraries, being insecure, it became a giant pain to actually install the libraries needed to do cross compiling on my machine. Fedora on the other hand had all the correct libraries available and looked to be much simpler to get everything set up.

This docker project does exactly that and it works perfectly!

https://github.com/etrombly/rust-crosscompile

I'm going to write my Dockerfile based on this Dockerfile so as to understand what is really going on in the build system.

Now let's dive in!

Hello, World!

Let's first take a look at just the rust-gtk part. This is completely stolen from the rust-gtk website and is a basic example. The example is what had lulled me into a false sense of confidence.

~/hello/Cargo.toml

toml
[dependencies]

[dependencies.gtk]
version = "0.9.0"
features = ["v3_16"]

[dependencies.gio]
version = ""
features = ["v2_44"]

The first thing we do is update our Cargo.toml with our dependencies. This is quite different from the usual way I bring in dependencies and it would be good to find out why.

~/hello/src/main.rs

rust
extern crate gtk;
extern crate gio;

use gtk::prelude::*;
use gio::prelude::*;

use gtk::{Application, ApplicationWindow, Button};

fn main() {
    let application = Application::new(
        Some("com.github.gtk-rs.examples.basic"),
        Default::default(),
    ).expect("failed to initialize GTK application");

    application.connect_activate(|app| {
        let window = ApplicationWindow::new(app);
        window.set_title("First GTK+ Program");
        window.set_default_size(350, 70);

        let button = Button::with_label("Hello, World!");
        button.connect_clicked(|_| {
            println!("Clicked!");
        });
        window.add(&button);

        window.show_all();
    });

    application.run(&[]);
}

Voila! We have out test gtk program. I haven't looked at the code besides the very gist of it, which is that this program will create a small window with a button that says hello which will also print clicked when the button is clicked.

This is now enough for us to test if out cross compiling will work.

Now let's get to the harder part.

The Dockerfile

Let's start with our Dockerfile and see what the build system is going to look like.

Dockerfile
FROM fedora:latest

#
# Set up system
#
WORKDIR /root
RUN dnf -y update
RUN dnf clean all
RUN dnf install -y git cmake file gcc make man sudo tar
RUN dnf install -y gcc-c++ boost boost-devel

#
# Build peldd to find dlls of exes
#
RUN git clone https://github.com/gsauthof/pe-util
WORKDIR pe-util
RUN git submodule update --init
RUN mkdir build

WORKDIR build
RUN cmake .. -DCMAKE_BUILD_TYPE=Release
RUN make

RUN mv /root/pe-util/build/peldd /usr/bin/peldd
RUN chmod +x /usr/bin/peldd

#
# Add package.sh
#
ADD package.sh /usr/bin/package.sh
RUN chmod +x /usr/bin/package.sh

#
# Install Windows libraries
#
RUN dnf install -y mingw64-gcc 
RUN dnf install -y mingw64-freetype 
RUN dnf install -y mingw64-cairo 
RUN dnf install -y mingw64-harfbuzz 
RUN dnf install -y mingw64-pango 
RUN dnf install -y mingw64-poppler 
RUN dnf install -y mingw64-gtk3 
RUN dnf install -y mingw64-winpthreads-static 
RUN dnf install -y mingw64-glib2-static 

#
# Install rust
#
RUN useradd -ms /bin/bash rustacean
USER rustacean

RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
RUN /home/rustacean/.cargo/bin/rustup update

#
# Set up rust for cross compiling
#
RUN /home/rustacean/.cargo/bin/rustup target add x86_64-pc-windows-gnu
ADD cargo.config /home/rustacean/.cargo/config
ENV PKG_CONFIG_ALLOW_CROSS=1
ENV PKG_CONFIG_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/lib/pkgconfig/
ENV GTK_INSTALL_PATH=/usr/x86_64-w64-mingw32/sys-root/mingw/

#
# Setup the mount point
#
VOLUME /home/rustacean/src
WORKDIR /home/rustacean/src

#
# Build and package executable
#
CMD ["/usr/bin/package.sh"]

Our docker image is going to use the latest version of fedora and one of the key things we need to build is peldd.

To do this we first set up the system and get the usual things installed.

Next we pull pe-util and build the project. This will give us peldd which we can then use to find which dlls we need for our exe.

bash
❯ ./peldd gtk-test-3.exe
USERENV.dll
libcairo-2.dll
libcairo-gobject-2.dll
libgdk-3-0.dll
libgdk_pixbuf-2.0-0.dll
libgio-2.0-0.dll
libglib-2.0-0.dll
libgobject-2.0-0.dll
libgtk-3-0.dll
libpango-1.0-0.dll

Here we can see all the dlls that we need for out gtk-test-3.exe. This will help when we go to package up our exe so that we can run it.

We can use the -t option to get the paths of the dll so that we can then copy them.

Once we have pldd setup, we can then add package.sh to our image. package.sh is a shell script that will run peldd against out executable and create a folder called package that will contain the dlls and executable. This will allow us to copy the file over to Windows and run our exe.

bash
#!/bin/bash

/home/rustacean/.cargo/bin/cargo build --target=x86_64-pc-windows-gnu --release

mkdir -p package
cp target/x86_64-pc-windows-gnu/release/*.exe package

export DLLS="peldd package/*.exe -t --ignore-errors"
for DLL in $DLLS
    do cp "$DLL" package
done

mkdir -p package/share/{themes,gtk-3.0}
cp -r $GTK_INSTALL_PATH/share/glib-2.0/schemas package/share/glib-2.0
cp -r $GTK_INSTALL_PATH/share/icons package/share/icons

cat << EOF > package/share/gtk-3.0/settings.ini
[Settings]
gtk-theme-name = Windows10
gtk-font-name = Segoe UI 10
gtk-xft-rgba = rgb
gtk-xft-antialias = 1
EOF

mingw-strip package/*.dll
mingw-strip package/*.exe

* Note - the export DLLS command should use ticks not quotes.

We use our script to build our rust program. We then create a folder and copy the exe and dlls to our package folder.

We also have some defaults that we need to set for out gui application.

The final mingw-strip commands will bring the executable size down quite a bit.

Now that we have our packaging utility installed and set up, we can now turn to installing the windows libraries we need to do gtk development.

Here we install a whole slew of mingw libraries and we are almost off to the races.

The next step is to install rust and this we can the usual way of using the rust script.

Once we have rust installed, we now need to set up rust for cross compiling. The first thing we do is add a windows target to rust. Next we bring over cargo.config into our image.

toml
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-gcc-ar"

This will set up the linker for rust.

We also set environment variables so that rust will link the correct libraries that the windows executable will need.

Finally we set a volume on our image which is where we will mount the external project we are working on.

The last step in our Dockerfile is our package.sh script which will get run. This script will then do a cargo build with a specific architecture as its target and it will create a package folder containing our executable and dlls that it needs to run.

Voila! We have an image that we can no use to do gtk projects that we can cross compilng to run on windows.

Let's take a quick look at the docker commands so that we can build our projects.

docker Commands

For testing purposes I wrote a quick script to remove, build and start the docker containers.

bash
#!/usr/bin/bash

sudo docker container rm gtk-test
sudo docker build . -t gtk-final
sudo docker create -v "pwd":/home/rustacean/src --name gtk-test gtk-final
sudo docker start gtk-test -ai

* Note - the docker create command should use ticks not quotes.

The first command removes the container named gtk-test.

The second command builds the Dockerfile in the current directory and names the image as gtk-final. This will take awhile as we have quite few steps in our Dockerfile. This step is the image creation.

The third command creates a container where the current working directory is mounted to the image's /home/rustacean/src folder. We give it the name gtk-test and the image we want to use it gtk-final. Now we have the container ready to go.

The last command is to start the gtk-test container and -ai will output the Dockerfile's output to the screen. This way we can see the cargo build process and any errors.

Now our final command should have generated a folder called package in our current directory and when we copy that file over to a windows machine we should be able to run it and get a window that simply says hello world. It should also be clickable and once clicked it should print out a message to the console.

With that we are done! We have a working build system now and we can move to the actual application.