Cross-compiling

Using a prebuilt bundle

If the target you're compiling to, already has a prebuilt package:

  • x86_64-pc-windows-gnu
  • x86_64-pc-windows-msvc
  • x86_64-apple-darwin
  • aarch64-apple-darwin
  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu

Add the target via rustup, then invoke the build:

rustup target add <your target> # replace with one of the targets above
cargo build --target=<your target> --features=fltk-bundled

For aarch64-unknonw-linux-gnu, you might have to specify the linker:

CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc cargo build --target=aarch64-unknown-linux-gnu --features=fltk-bundled

You can specify the linker in a .cargo/config.toml file so you won't have to pass it to the build command:

# .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

Then:

cargo build --target=aarch64-unknown-linux-gnu --features=fltk-bundled

Using cross

If you have Docker installed, you can try cross.

cargo install cross
cross build --target=x86_64-pc-windows-gnu # replace with your target, the Docker daemon has to be running, no need to add via rustup

If your target requires external dependencies, like on Linux, you would have to create a custom docker image and use it for your cross-compilation via either:

1- a Cross.toml file + Dockerfile:

For example, for a project of the following structure:

myapp
     |_src
     |    |_main.rs    
     |
     |_Cargo.toml
     |
     |_Cross.toml
     |
     |_arm64-dockerfile

The arm64-dockerfile (the name doesn't matter, just make sure Cross.toml points to the file) contents:

FROM ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge

ENV DEBIAN_FRONTEND=noninteractive

RUN dpkg --add-architecture arm64 && \
    apt-get update && \
    apt-get install --assume-yes --no-install-recommends \
    libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 \
    libxinerama-dev:arm64 libxcursor-dev:arm64 \
    libxrender-dev:arm64  libxfixes-dev:arm64  libgl1-mesa-dev:arm64 \
    libglu1-mesa-dev:arm64 libasound2-dev:arm64 libpango1.0-dev:arm64

Notice the architecture appended to the library package's name like: libx11-dev:arm64.

The Cross.toml contents:

[target.aarch64-unknown-linux-gnu]
dockerfile = "./arm64-dockerfile"

2- Configuring Cargo.toml:

[package.metadata.cross.target.aarch64-unknown-linux-gnu]
pre-build = [""" \
    dpkg --add-architecture arm64 && \
    apt-get update && \
    apt-get install --assume-yes --no-install-recommends \
    libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 \
    libxinerama-dev:arm64 libxcursor-dev:arm64 \
    libxrender-dev:arm64  libxfixes-dev:arm64  libgl1-mesa-dev:arm64 \
    libglu1-mesa-dev:arm64 libasound2-dev:arm64 libpango1.0-dev:arm64 \
    """]

Then run cross:

cross build --target=aarch64-unknown-linux-gnu

(This might take a while for the first time)

Using a cross-compiling C/C++ toolchain

The idea is that you need a C/C++ cross-compiler and a Rust target installed via rustup target add as mentioned in the previous scenario.

For Windows and MacOS, the system compiler would already support targetting the supported architectures. For example, on MacOS, if you can already build fltk apps using your system compiler, you can target a different architecture using (assuming you have an intel x86_64 mac):

rustup target add aarch64-apple-darwin
cargo build --target=arch64-apple-darwin

Linux to 64-bit Windows

Assuming you would like to cross-compile from Linux to 64-bit Windows, and are already able to build on your host machine:

  • You'll need to add the Rust target using:
rustup target add x86_64-pc-windows-gnu # depending on the arch
  • Install a C/C++ cross-compiler like the Mingw toolchain. On Debian-based distros, you can run:
apt-get install mingw-w64 # or gcc-mingw-w64-x86-64

On RHEL-based distros:

dnf install mingw64-gcc

On Arch:

pacman -S mingw-w64-gcc

On Alpine:

apk add mingw-w64-gcc
  • Add a .cargo/config.toml in your project root (or HOME dir if you want the setting to be global), and specify the cross-linker and the archiver:
# .cargo/config.toml
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-gcc-ar"
  • Run the build:
cargo build --target=x86_64-pc-windows-gnu

x64 linux-gnu to aarch64 linux-gnu

Another example is building from x86_64 debian-based distro to arm64 debian-based distro: Assuming you already have cmake already installed.

  • You'll need to add the Rust target using:
rustup target add aarch64-unknown-linux-gnu
  • Install a C/C++ cross-compiler like the Mingw toolchain. On Debian-based distros, you can run:
apt-get install g++-aarch64-linux-gnu
  • Add the required architecture to your system:
sudo dpkg --add-architecture arm64
  • You might need to add the following mirrors to /etc/apt/sources.list:
sudo sed -i "s/deb http/deb [arch=amd64] http/" /etc/apt/sources.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s) main multiverse universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-security main multiverse universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-backports main multiverse universe" | sudo tee -a /etc/apt/sources.list
echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-updates main multiverse universe" | sudo tee -a /etc/apt/sources.list

The first command changes the current mirrors to reflect your current amd64 system. The others add the arm64 ports to your /etc/apt/sources.list file.

  • Update your package manager's database:
sudo apt-get update
  • Install the required dependencies for your target architecture:
sudo apt-get install libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 libxinerama-dev:arm64 libxcursor-dev:arm64 libxrender-dev:arm64 libxfixes-dev:arm64 libpango1.0-dev:arm64 libgl1-mesa-dev:arm64 libglu1-mesa-dev:arm64 libasound2-dev:arm64

Notice the :arm64 suffix in the packages' name.

  • Run the build:
CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc cargo build --target=aarch64-unknown-linux-gnu

You can specify the linker in a .cargo/config.toml file so you won't have to pass it to the build command:

# .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

Then:

cargo build --target=aarch64-unknown-linux-gnu

Using docker

Using a docker image of the target platform directly can save you from the hassle of cross-compiling to a different linux target using cross. You'll need a Dockerfile which pulls the target you want and install the Rust and C++ toolchains and the required dependencies. For example, building for alpine linux:

FROM alpine:latest AS alpine_build
RUN apk add rust cargo git cmake make g++ pango-dev fontconfig-dev libxinerama-dev libxfixes-dev libxcursor-dev
COPY . .
RUN cargo build --release

FROM scratch AS export-stage
COPY --from=alpine_build target/release/<your binary name> .

And run using:

DOCKER_BUILDKIT=1 docker build --file Dockerfile --output out .

Your binary will be in the ./out directory.

Note on alpine, if you install Rust via rustup, you might have to point the musl-gcc and musl-g++ to the appropriate toolchain in your dockerfile (before running cargo build):

RUN ln -s /usr/bin/x86_64-alpine-linux-musl-gcc /usr/bin/musl-gcc
RUN ln -s /usr/bin/x86_64-alpine-linux-musl-g++ /usr/bin/musl-g++

You would also need to add "-C target-feature=-crt-static" to RUSTFLAGS due to this rust toolchain issue: https://github.com/rust-lang/rust/issues/61328

i.e.

FROM alpine:latest AS alpine_build
ENV RUSTUP_HOME="/usr/local/rustup" CARGO_HOME="/usr/local/cargo" PATH="/usr/local/cargo/bin:$PATH" RUSTFLAGS="-C target-feature=-crt-static"
RUN apk add git curl cmake make g++ pango-dev fontconfig-dev libxinerama-dev libxfixes-dev libxcursor-dev

RUN ln -s /usr/bin/x86_64-alpine-linux-musl-gcc /usr/bin/musl-gcc
RUN ln -s /usr/bin/x86_64-alpine-linux-musl-g++ /usr/bin/musl-g++

RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable-x86_64-unknown-linux-musl

COPY . .
RUN cargo build --release

FROM scratch AS export-stage
COPY --from=alpine_build target/release/<your binary name> .

Another example to compile from amd64 linux-gnu to arm64 linux-gnu:

FROM ubuntu:bionic AS ubuntu_build

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qq
RUN	apt-get install -y --no-install-recommends lsb-release g++-aarch64-linux-gnu g++ cmake curl tar git make
RUN apt-get install -y ca-certificates && update-ca-certificates --fresh && export SSL_CERT_DIR=/etc/ssl/certs
RUN	dpkg --add-architecture arm64 
RUN sed -i "s/deb http/deb [arch=amd64] http/" /etc/apt/sources.list
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s) main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-security main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-backports main multiverse universe" | tee -a /etc/apt/sources.list 
RUN echo "deb [arch=arm64] http://ports.ubuntu.com/ $(lsb_release -c -s)-updates main multiverse universe" | tee -a /etc/apt/sources.list 
RUN	apt-get update -qq && apt-get install -y --no-install-recommends -o APT::Immediate-Configure=0 libx11-dev:arm64 libxext-dev:arm64 libxft-dev:arm64 libxinerama-dev:arm64 libxcursor-dev:arm64 libxrender-dev:arm64 libxfixes-dev:arm64 libpango1.0-dev:arm64 libgl1-mesa-dev:arm64 libglu1-mesa-dev:arm64 libasound2-dev:arm64
RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable --profile minimal -y

ENV PATH="/root/.cargo/bin:$PATH" \
	CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ \
	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++ \
    PKG_CONFIG_PATH="/usr/lib/aarch64-linux-gnu/pkgconfig/:${PKG_CONFIG_PATH}"

RUN rustup target add aarch64-unknown-linux-gnu

COPY . .

RUN  cargo build --release --target=aarch64-unknown-linux-gnu

FROM scratch AS export-stage
COPY --from=ubuntu_build target/aarch64-unknown-linux-gnu/release/<your binary name> .

Using a CMake toolchain file

The path to the file can be passed to CFLTK_TOOLCHAIN env variable:

CFLTK_TOOLCHAIN=$(pwd)/toolchain.cmake cargo build --target=<target architecture>

In newer versions of CMake (above 3.20), you can directly set the CMAKE_TOOLCHAIN_FILE environment variable.

The contents of the CMake toolchain file usually set the CMAKE_SYSTEM_NAME as well as the cross-compilers. Another thing which needs to be set on Linux/BSD is the PKG_CONFIG_EXECUTABLE and PKG_CONFIG_PATH. A sample toolchain file:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(triplet aarch64-linux-gnu)
set(CMAKE_C_COMPILER /usr/bin/${triplet}-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/${triplet}-g++)
set(ENV{PKG_CONFIG_EXECUTABLE} /usr/bin/${triplet}-pkg-config)
set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:/usr/lib/${triplet}/pkgconfig")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

Note the CMAKE_SYSTEM_PROCESSOR is usually the value of uname -m on the target platform, other possible values can be found here. We set the triplet variable in this example to aarch64-linux-gnu, which is the prefix used for the gcc/g++ compilers, as well as the cross-compiling aware pkg-config. This triplet is also equivalent to the Rust triplet aarch64-unknown-linux-gnu. The PKG_CONFIG_PATH is set to the directories containing the .pc files for our target, since these are required for the cairo and pango dependencies on Linux/BSD. The last 4 options just tell CMake to not mix the include/library paths of both host/target.

Another toolchain file targetting windows (using the mingw toolchain):

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR AMD64)

set(triplet x86_64-w64-mingw32)
set(CMAKE_C_COMPILER /usr/bin/${triplet}-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/${triplet}-g++)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

Using cargo xwin

If you need to target windows and the msvc compiler/abi, you can install cargo-xwin:

cargo install cargo-xwin

And build your project using:

cargo xwin build --release --target x86_64-pc-windows-msvc

Using the fltk-config feature:

FLTK provides a script called fltk-config which acts like pkg-config. It tracks the installed FLTK lib paths and the necessary cflags and ldflags. Since fltk-rs requires FLTK 1.4, and most distros don't provide it at the time of writing this, you would have to build FLTK from source for the target you require. However, once distros start distributing FLTK 1.4, it should as simple as (targetting arm64 gnu linux):

dpkg --add-architecture arm64
apt-get install libfltk1.4-dev:arm64

cargo build --target=aarch64-unknown-linux-gnu --features=fltk-config

If you need to build FLTK for a different architecture, you would need to use a CMake toolchain file (using the one from before):

git clone https://github.com/fltk/fltk --depth=1
cd fltk
cmake -B bin -G Ninja -DFLTK_BUILD_TEST=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=/full/path/to/toolchain/file.cmake
cmake --build bin
cmake --instal bin # might need sudo in a hosted env
# then for your proj
cargo build --target=aarch64-unknown-linux-gnu --features=fltk-config