Hi! The playable demo of Outer Wonders has been live on itch.io for Windows and Linux for three weeks, and we're already working on improvements for it. Supporting Linux specifically was no easy task, as the documentation about building and deploying games for Linux is scarce and often intricate. Today's blog post will tell you more about the process that we designed to achieve excellent Linux support for Outer Wonders !
The basics : compiling
Game developers making use of a game engine generally have access to some feature enabling them to build their games for each platform. Most commercial game engines support Linux, although the quality of this support is variable.
Using a custom game engine requires more manual work, especially when it comes to building for Linux.
First of all, it is essential to make use of technologies that guarantee excellent support for most target platorms, in order to remain productive. Custom game engines are very often made using the C or C++ language (such game engines are called native), or based on more accessible and feature-rich technologies such as HTML5/JavaScript, in the case of some recent games. At Utopixel, we're using a custom game engine based on the Rust programming language, as well as SDL2.
Rust and SDL2 grouped together are the common language that we use to build Outer Wonders for all platforms at once, to quote this blog post. In other words, as of today, approximately 99% of our code base is platform-agnostic, and thanks to the tooling provided by Rust, we are now capable of building Outer Wonders for various platforms easily, including Linux.
As a native programming language (that is, a language that can be turned into an application that can be executed by the target platform without additional work), Rust supports compilation for multiple platforms.
Linux-specific process
Although significant effort has been put into making this section as accessible as possible, this section is fairly technical and intricate, and will be of interest to you only if you enjoy reading about the technological aspects of video game development. If not, we recommend you skip to the next section: Stringent testing.
Because we develop Outer Wonders using Windows-powered devices, we've had to read up a lot of documentation in order to design a reliable build process for Linux.
The first piece of documentation we read in order to build Outer Wonders for Linux is the itch.io developer documentation, which provides great guidance on this topic.
We've followed most of the tips enumerated in this documentation, and when we couldn't follow some specific tip, we designed alternative ways to fulfill the same objective as pursued by these tips: achieving great cross-distribution portability.
Enumerating external dependencies
The first major step to achieving decent cross-distribution portability is to enumerate all of the external dependencies required to run the game, that is, the libraries and packages that must be installed on the player's system prior to running the game.
If you are a Linux user, you probably know a bit about the concept of dependency: dependencies are files installed either by default along with the distribution itself, or installed by commands such as apt install package
, dnf install package
and pacman -S package
.
When in doubt, you can easily determine the dependencies of an application. Tools such as objdump
, ldd
and readelf
(provided as part of the binutils
package that can be downloaded from most Linux distributions) can give information about the dependencies of an application.
For instance, an application that just prints "Hello, world!" in a terminal can be analyzed by the sample command below:
$ readelf -d /path/to/application
Dynamic section at offset 0x2dc8 contains 27 entries :
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x11e8
...
which tells us that our application depends on libc
(a core component installed on all Linux systems by default) only.
In the case of Outer Wonders, we made the decision to keep our external dependency count as low as possible. SDL2 is one of those dependencies; most of the remaining dependencies are libraries installed by default on all Linux systems, such as libm
, libdl
and libc
. This greatly mitigates the risk of incompatibility, although special attention must be paid to libc
.
As a Rust application, Outer Wonders also depends on Rust-based components, but dependency management in Rust is such that all of these Rust-based components are directly bundled in the application itself, which, again, greatly mitigates the risk of incompatibility.
Because of all of these reasons, we've focused mostly on 2 external dependencies: SDL2 and glibc
. The building process we set up requires a few specific adjustments to process both of these dependencies properly.
Keeping an eye the minimal required glibc
version
One of the great sources of incompatibility is libc
, or, rather, glibc
, its most prevalent variant installed by default on most Linux distributions. Most applications depending on libc
actually depend on glibc
.
There are many, many versions of glibc
(and you'll find a complete list of these versions here) and, the lower the version your application depends on is, the better its cross-distribution compatibility will be.
Because most build tools are based on the glibc
version installed on the developer's system, the itch.io developer documentation clearly recommends building a Linux application using an old distribution.
Once an application has been built, the minimal version of glibc
required by the application can be determined. The command below:
$ objdump -T /path/to/application
/path/to/application: file format elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 puts
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main
0000000000000000 w D *UND* 0000000000000000 __gmon_start__
0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize
provides additional information about the libc
dependency of an application. In the example above, our application depends on a feature called puts
provided by version 2.2.5 of glibc
, among others. By reading lines containing a GLIBC_
label, we can determine the minimal required version: it is the highest version that you'll find in this list.
In the example above, the minimal version of glibc
required by our application is version 2.2.5, which dates back from 2002. Cross-platform compatibility is therefore likely to be great!
In more elaborate applications, this will seldom be the case, though. As an example, external dependencies (such as SDL2, which is used by Outer Wonders) generally depend on glibc
as well. Because of this, the minimal version of glibc
required by external dependencies should be checked as well.
Once this first analysis has been carried out, developers need to figure out what features cause the application to depend on recent versions of glibc
.
In the case of Outer Wonders, we weren't able to set up old Linux distributions, because the installation of such distributions depends on online repositories where installable dependencies are stored, and repositories containing the packages for old distributions are either removed or archived. This is why we fell back on Ubuntu 20.04.
We quickly figured out that Outer Wonders depended on math functions connected to the concept of power/exponentiation (pow
, exp
, and log
), all of which systematically cause the application to depend on glibc
2.27, a version dating from 2018, which is far less than ideal for cross-distribution compatibility. We also figured out that removing references to these features would allow Outer Wonders to depend on glibc
2.18, which dates from 2013. This was much more acceptable!
We started working on looking for all references to these features so we could remove them. It turns out these features were used only once, upon parsing numbers from the game's files/assets. Such numbers could possibly have a format such as 1e15
, meaning "10 to the power of 15", which is the reason why power/exponentiation functions are used. However, handling such situations was not relevant to us, as we only needed to parse fairly small integers (unless we wanted to account for future screens with potentially absurdly high definitions like 16,000,000×9,000,000 pixels?). We removed the code handling such irrelevant situations and therefore achieved compatibility with glibc
2.18!
Providing a portable build of SDL2
SDL2 is a core component of the engine powering Outer Wonders. Despite this, SDL2 happens to be a fully external dependency, as it is a library that is generally not installed by default on most Linux distributions.
Therefore, how can we ensure that Outer Wonders can access SDL2's features and start properly on the player's hardware? There are 3 solutions that come to our mind to solve this:
- Require the player to install the
libsdl2
dependency themselves before starting the game; - Check for the presence of the
libsdl2
dependency on startup, and, install it automatically if it was not installed before starting the game; - Bundle a copy of the
libsdl2
dependency along with Outer Wonders.
All 3 solutions have their pros and cons, and none of them is truly perfect:
- The first solution is convenient for the developer, but far from ideal for the player, even if it can contribute to saving a bit of storage space on their computer. Implementing this solution means the player will run into a possibly cryptic error message on startup if the dependency is not installed. There is a major risk of losing the player here.
- The second solution sounds interesting, but incurs two risks. It allows the player to save a bit of storage space just like the first solution, but adds significant complexity for the developer (automatic dependency installation is a rather distribution-specific task!) and contributes to a frustrating first experience of the game for the player (the player needs to wait for dependencies to be installed before they can play).
- The last solution makes the application highly portable (meaning: no startup issue), but requires potentially time-consuming work for the developer (the developer needs to make sure that the build of the dependency that they provide with the game is actually compatible with the majority of Linux distributions, and, should updates and fixes for the dependency be published, the burden of updating the dependency is on the shoulders of the application developer!) and contributes to slightly higher storage space occupation (each game deployed using this solution will install a distinct copy of the same dependency, while a dependency should ideally be installed only once!)
The recommended solution for deploying a game on itch.io is actually the third solution, because this solution is the one that achieves the highest degree of portability, and creates the best experience for the player.
However, this solution involves bundling external dependencies and the application together, which is a potentially time-consuming task. To achieve maximal portability, it is indeed recommended that the developer creates their own build of the dependencies, so these dependencies are as distribution-agnostic as possible.
The good part is, the process for building SDL2 this way is fairly well documented. All the developer has to do is download the SDL2 source code, install the dependencies enumerated in this page, and execute the following in a terminal :
./configure.sh; make; sudo make install
from the SDL2's source code root directory, in order to build a libSDL2-2.0.so.0
file (this file will most likely be created in the /usr/local/bin
folder). This distribution-independent file can then be copied and provided along with any build of the application.
Providing this file along with the application may not be enough to make the application use the features provided by SDL2 properly, though. The connection between the application and the dependency must be declared explicitly.
One way to make this declaration is to define whay Linux calls the run-time library search path (more often referred to as the rpath) of the application, which is the folder where Linux looks for dependencies first upon running an application.
There is a convenient tool to achieve this, called patchelf
. This command can alter applications in some very specific ways, including this one:
patchelf --set-rpath '$ORIGIN' /path/to/application
The command above resets the application's rpath to the $ORIGIN
value. This $ORIGIN
value actually references the folder where the application itself is located. To put it more simply, the command above means: "Alter the application in a way such that, when someone starts it, Linux looks for its dependencies in this application's folder first".
This is the final step towards making the application fully portable! All that's left to do now is to ship the application along with the libSDL2-2.0.so.0
file previously built, and the application can now run on a wide range of Linux distributions!
In the case of Outer Wonders, we implemented this process successfully, allowing us to ship Outer Wonders on itch.io. To further improve the game's portability, we had to shave some minor features off SDL2. This is because SDL2 provides access to the aforementioned power/exponentiation math functions (pow
, exp
, and log
) causing SDL2 itself to depend on glibc
2.27, which was definitely undesirable.
We've had no bug report about Outer Wonders on Linux so far, including from users running expert-centric, complex distributions such as Arch Linux, so we consider this process to be stable!
Stringent testing
Nothing beats stringent testing at ensuring such an intricate process is reliable.
Our testing process is simple: all the developer has to do is start the application on a clean install of any Linux distribution. This will test the application's behavior on a system where our application's dependencies are not installed.
Getting an error message such as:
/path/to/application: error while loading shared libraries: libSDL2-2.0.so.0: cannot open shared object file: No such file or directory
most likely means that, either the patchelf
command was not properly performed, or the dependency (our libSDL2-2.0.so.0
file) was not shipped properly along with the application (the patchelf
command we mentioned before is such that the dependency must be located in the same directory as the application itself).
On the other hand, if the application starts properly:
then there's a high chance that the application is fully functional and highly portable!
And this concludes this technical blog post! Join our Discord server, follow us on Twitter, Facebook and Instagram to read our news and play weekly puzzles! Subscribe to our RSS feed to keep informed about our latest blog posts.
See you soon!