Cross-compiling for Steam Deck from Apple Silicon Mac

Most of the development of my game has been happening on my M2 MacBook Air. One of the main places I’d like to play the game, though, is on my Steam Deck. In order to play the game on my Steam Deck, I need to compile it for the Steam Deck. There’s a number of ways I could do this, but I wanted a solution that would let me compile on my Mac directly with minimal infrastructure fuss (no VMs, network access, user mode emulation, etc).

SteamOS SDK

Poking around, games can run in one of a number of runtimes, each with a standardized set of libraries available. We’ll be going with the latest, “sniper”. You can find information about it on Valve’s web site.

The file we’re interested in is the developer sysroot (latest version as of writing). This has all the libraries that would be needed at runtime, plus all the header files, library symlinks, static archives, and other files we’d need for development. It also contains a compiler ready for use, except that the compiler is compiled for x86_64, while we’re on AArch64, so we can’t run the compiler directly. Also, it’s compiled for Linux, but we’re on macOS.

Building our own Binutils

This one is easy, compared to GCC. The Binutils shipped in the developer sysroot is version 2.35.2-2, according to /var/lib/dpkg/status in the sysroot. You could normally obtain the sources either from upstream or from the Debian archive depending on your preference (they should be identical)… except they aren’t. Upstream has a generated documentation file that Debian removed, and it’s a pain in the butt to regenerate, so use upstream’s tarball. Unpack it as usual.

Next, we do need a patch from Debian, or otherwise ld will look in /usr/lib64 instead of /usr/lib/x86_64-linux-gnu. Pull the “debian” archive from the Debian archive (ha-ha) and extract that inside the source directory. Then we need to apply one of the patches Debian provides:

patch -p1 < debian/patches/129_multiarch_libpath.patch

Next it’s time to configure and build. First, set aside some directory you want to install these tools to. I’ll refer to that directory as $PFX. I’ll also expect that $SYSR points to your unpacked sysroot. Binutils is easy to configure, build, and install, modulo some support for Debian’s patch:

mkdir binutils-2.35.2-build
cd binutils-2.35.2-build
../binutils-2.35.2/configure --prefix=$PFX --target=x86_64-linux-gnu --with-sysroot=$SYSR
DEB_TARGET_MULTIARCH=x86_64-linux-gnu DEB_TARGET_MULTIARCH32=i386-linux-gnu DEB_TARGET_MULTIARCH64=x86_64-linux-gnu make -j10
make -j10 install
cd ..

Easy peasy. Couldn’t be easier. Mostly.

Note that if you drop the --with-sysroot option or fail to apply the Debian patch or forget to set the right DEB_* environment variables during the build, everything will seem to work just fine, right up until you think you’re all done and try to link a complex program, and you get inexplicable ld: warning: libasound.so.2, needed by .../libSDL2.so, not found (try using -rpath or -rpath-link) errors.

Building our own GCC

The compiler shipped in the developer sysroot is GCC (10.3.0-3+steamrt3.1+bsrt3.1, according to /var/lib/dpkg/status in the sysroot). We can build an equivalent GCC for cross-compiling, such that the compiler will run natively for us, but use the SDK’s sysroot and build for x86_64.

Once again, first we’ll need the sources. You can obtain the sources either from upstream, or from the APT repository, but the version in the APT repository requires extra patches to build, or else you run into GCC PR48378. As such, you’ll want to get it directly from upstream. Other sources you’ll need, which could be fetched any way you like, are:

The Debian packages also come with patch series. However, for most of these, we don’t care – we’ll apply patches selectively as-needed.

You’ll need to unpack these in a bit of an unusual way:

  • GCC into gcc-10.3.0
    • GMP into gcc-10.3.0/gmp
    • ISL into gcc-10.3.0/isl
    • MPC into gcc-10.3.0/mpc
    • MPFR into gcc-10.3.0/mpfr

Now an ad-hoc patch. I only encountered the problem necessitating this the second time through these directions; I’m not sure what’s changed since the first time. But it’s needed, or you get an ugly compilation error. Edit gcc/system.h, and find this block:

/* There are an extraordinary number of issues with <ctype.h>.
   The last straw is that it varies with the locale.  Use libiberty's
   replacement instead.  */
#include "safe-ctype.h"

Move it under the block ending in:

# include <cstring>
# include <new>
# include <utility>
#endif

And another patch: Apply apple_silicon.patch attached to Haiku ticket 17191.

Now it’s time to configure. This one’s a doozy. Here I’m also assuming $SYSR points at your sysroot.

mkdir gcc-10.3.0-build
cd gcc-10.3.0-build
../gcc-10.3.0/configure \
  --prefix=$PFX \
  --target=x86_64-linux-gnu \
  --enable-languages=c,c++ \
  --with-sysroot=$SYSR \
  CXX="clang++ -std=gnu++14"

Prefix is needed to tell it where to install. Target is needed to build a cross compiler. Enable languages is needed so that GCC won’t build languages you probably don’t care about like Fortran (it’s OK Fortran I still love you). Sysroot is needed so the resulting compiler can find the include files and libraries it needs. CXX is to work around ISL assuming it has at least C++11 by default; we use this rather than CXXFLAGS because CXXFLAGS overrides some CXXFLAGS settings inside GCC that we’d rather not override. C++11 might be fine but I didn’t test it; C++14 works; C++17 is too new and errors on use of the register keyword inside the sources.

Then build. In this case, you only want to build GCC proper, none of the miscellanea. (For example, libgcc is already in the sysroot, and we’d rather use the one provided in the SDK than building our own.) Hence:

make -j10 all-gcc
make install-gcc

Errors I encountered along the way

If you get errors like this:

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__locale:557:48: error: too many arguments provided to function-like macro invocation
    const char_type* toupper(char_type* __low, const char_type* __high) const
                                               ^

Then you did not apply the patch to gcc/system.h above as described. Apply them and retry. (This problem seems similar to GCC PR63750 and related, but I am not aware of a specific bug report for this one.)

If you get errors like this:

Undefined symbols for architecture arm64:
  "_host_hooks", referenced from:
      gt_pch_save(__sFILE*) in libbackend.a[97](ggc-common.o)
      gt_pch_restore(__sFILE*) in libbackend.a[97](ggc-common.o)
      toplev::main(int, char**) in libbackend.a[291](toplev.o)
ld: symbol(s) not found for architecture arm64

Then you did not apply apple_silicon.patch.

If you get an error like this:

genhooks: No place specified to document hook TARGET_ASM_OPEN_PAREN

Then you used the Debian tarball instead of the upstream tarball. I don’t know what’s different that causes this.

Hacking up the prefix

If you try the compiler now, compiling C files will work OK, but the C++ standard library’s headers will be missing. You can see where it’s looking with prefix/bin/x86_64-linux-gnu -c -v -x c++ /dev/null (leading portion of path omitted in excerpt):

ignoring nonexistent directory "prefix/lib/gcc/x86_64-linux-gnu/10.3.0/../../../../x86_64-linux-gnu/include/c++/10.3.0"
ignoring nonexistent directory "prefix/lib/gcc/x86_64-linux-gnu/10.3.0/../../../../x86_64-linux-gnu/include/c++/10.3.0/x86_64-linux-gnu"
ignoring nonexistent directory "prefix/lib/gcc/x86_64-linux-gnu/10.3.0/../../../../x86_64-linux-gnu/include/c++/10.3.0/backward"
ignoring nonexistent directory "sysroot/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "prefix/lib/gcc/x86_64-linux-gnu/10.3.0/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 prefix/lib/gcc/x86_64-linux-gnu/10.3.0/include
 sysroot/usr/local/include
 prefix/lib/gcc/x86_64-linux-gnu/10.3.0/include-fixed
 sysroot/usr/include/x86_64-linux-gnu
 sysroot/usr/include
End of search list.

Where are the C++ headers in the sysroot? sysroot/usr/include/c++/10 and sysroot/usr/include/x86_64-linux-gnu/c++/10. Where is GCC looking for them? prefix/x86_64-linux-gnu/include/c++/10.3.0 and prefix/x86_64-linux-gnu/include/c++/10.3.0/x86_64-linux-gnu. Unfortunately, GCC wants the architecture-specific headers inside the architecture-independent header directory, which is not how the sysroot has it packaged, so we can’t symlink the directory wholesale.

I’m sure it I could apply one of the Debian patches such that GCC finds the includes in the way Debian packages it, but I don’t know which patch is responsible for this behavior. Instead we have to symlink it file-by-file. (Alternatively we could modify the sysroot, but I wanted to avoid doing that since the sysroot changes more frequently than the compiler version, and it’s quite convenient to just slot in a new sysroot rather than repeat this whole process.) Hence:

(mkdir -p prefix/x86_64-linux-gnu/include/c++/10.3.0; cd prefix/x86_64-linux-gnu/include/c++/10.3.0; ln -s ../../../../../sysroot/usr/include/c++/10/* .)
ln -s ../../../../../sysroot/usr/include/x86_64-linux-gnu/c++/10 prefix/x86_64-linux-gnu/include/c++/10.3.0/x86_64-linux-gnu

After this, compiling a C++ program should work. But linking still won’t – crtbegin.o and libstdc++ are not found. That will need a few more fixes. This time, the search directories (from -Wl,-v when linking) are:

prefix/lib/gcc/x86_64-linux-gnu/10.3.0
sysroot/lib/x86_64-linux-gnu
sysroot/lib/../lib64
sysroot/usr/lib/x86_64-linux-gnu
sysroot/usr/lib/../lib64
prefix/lib/gcc/x86_64-linux-gnu/10.3.0/../../../../x86_64-linux-gnu/lib
sysroot/lib
sysroot/usr/lib

In the sysroot, these are in sysroot/usr/lib/gcc/x86_64-linux-gnu/10, but GCC is looking in prefix/lib/gcc/x86_64-linux-gnu/10.3.0. This directory already exists, and replacing the whole directory leads to other errors like “collect2: fatal error: cannot find ‘ld’”, so the easiest solution is probably to just symlink all the libraries and object files in there to the sysroot:

(cd prefix/lib/gcc/x86_64-linux-gnu/10.3.0; ln -s ../../../../../sysroot/usr/lib/gcc/x86_64-linux-gnu/10/*.{o,a,so} .)

Lastly, linking is still broken because some symlinks in the sysroot are absolute, and are broken once we’ve put it in a subdirectory. This script does just the trick. Now compiling C++ programs should work just fine.

CMake configuration

GCC works, but my project uses CMake. We need to teach CMake how to use our new toolchain. CMake makes this easy. Following the example in the CMake documentation, we only need to replace a few things:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(CMAKE_SYSROOT /Users/example/steamos/sysroot)
# CMAKE_STAGING_PREFIX not needed

set(tools /Users/example/steamos/prefix)
set(CMAKE_C_COMPILER ${tools}/bin/x86_64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/x86_64-linux-gnu-g++)
unset(tools)

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)

Stick this in a toolchain.cmake file somewhere, and when you want to cross-compile a project, stick a --toolchain .../toolchain.cmake on it. Bam!

And there you have it! Painful to set up, but once it’s set up, super easy to use for any project. And fun fact: Once I got my game cross-compiling, it ran on my Steam Deck first try, not even a single hitch once I got the files transferred over. Nice!

Tags: ,

2 Responses to “Cross-compiling for Steam Deck from Apple Silicon Mac”

  1. Apache553 says:

    “genhooks: No place specified to document hook TARGET_ASM_OPEN_PAREN” is caused by Debian packaging, Debian replaced certain files with ’empty’ files due to confliction between their license and DFSG. You may have a look on “https://salsa.debian.org/toolchain-team/gcc/-/blob/master/debian/patches/gcc-gfdl-build.diff” to see Debian people’s work to make their gcc buildable. Just for your information. 😊 Happy coding!

  2. fractoadm says:

    That makes sense. Thanks!