Cross-platform game distribution
š This page was last updated ~12 years ago. Just so you know.
ooc makes it easy to compile your application on all major platforms (Windows, OSX, Linux) - the compiler itself runs there, and the SDK supports all these platforms with basic functionality: data structures, file handling, time handling, networking, etc.
But between getting your application running on your dev environment with all the libraries installed, and getting it into a neat package for your users to run without having to install any dependencies by hand, thereās a bag of tricks. Fortunately, I have found the time to figure most of them out. Iāll try to explain these in detail here as clearly as possible, here in this article.
The basics
All your .ooc files are compiled by rock (and gcc/clang) into a single executable file. Letās pretend weāre working on a game project named āfoobarā. On Windows, the resulting executable will be named āfoobar.exeā, and on Linux/OSX, itāll be named āfoobarā.
But wait, thatās not all! Chances are, your game is loading assets (music, graphics, levels). Thatās a dependency. For those to load correctly, the CWD (current working directory) should be set correctly when running your executable. Weāll see how to do that correctly on all platforms.
And even when the CWD is correct, weāre not done yet: your game also probably depends on external libraries, such as SDL, freeimage, and so on. If you compile it naively on your dev environment, theyāll be dynamically linked. When running the application, your OS is looking for the libraries in the default path, such as /usr/lib, /usr/lib64, /Library/Frameworks and so on.
However, you canāt assume that your target user will have all dependencies installed like you did, especially if heās on a distribution or version other than the one you compiled the game on. For this too, we have OS-specific tricks that Iāll expose here.
Windows
Iāll be handling Windows first, since it seems to be the easiest case - however it is also the hardest to automate (from my current workflow at least).
Setting up your dev environment
The best way to build C (and thus ooc) programs from the command line on Windows is to use MinGW and MSYS. MinGW is a version of the GCC compiler collection for Windows, with a runtime based on msvcrt, ie. native to Windows. Youāll find that some functions are missing from your comfy unix-y environment, unlike cygwin - but do not be afraid! The ooc sdk is versioned specifically to use the Win32 API when needed (for file handling and time routines, for example).
MSYS is a port of some command-line utilities, such as the bash shell, and a
cmd.exe-based command line. It also handles path translation, so that in your
shell, /usr/
refers to an actual location on your Windows drive, for example
C:\MinGW\MSYS\1.0\usr\
and so on.
However, the MinGW shell is less than convenient for every-day use. My biggest gripes with it are the way it handles resizing windows (spoiler: cmd.exe style, where you have a separate ābuffer sizeā that you can adjust in the options, and a āwindow sizeā that cannot be bigger than the buffer size), and the way it handles copy/paste (spoiler: cmd.exe style, where you have to go: window contextual menu, Edit -> Mark, then draw a rectange in your window by click-dragging with your mouse, then Edit -> Copy, and itās finally in your clipboard).
A much neater alternative is Console, an acceptable command-line for Windows. You might want to install that, and take a look at the preferences to make it as comfy as possible. I made it gnome-terminal like (Ctrl-Page Up/Down to navigate tabs, Ctrl-Shift-T to open a new tab, Ctrl-Shift-W to close one, Ctrl-Shift-C/V to copy/paste, the only difference is that Console2 requires shift to be pressed while highlighting text for copy - but I can live with that).
You need to set the startup command to C:\MinGW\MSYS\1.0\bin\sh.exe -login
(or wherever sh.exe lives) to get an MSYS environment in there.
Then, youāll want to grab msysgit so you get the full git goodness
in your unix-like environment. Youāll want to add the path to msysgitās bin
directory to your PATH environment variable in your ~/.bashrc
on MSYS. Hereās
what mine looks like:
export OOC_LIBS="/c/Dev"
export PATH="/usr/local/bin:/c/Dev/rock/bin:/c/Program Files (x86)/Git/bin:$PATH"
If you donāt do that, git will only be accessible from the āGit Bashā terminal that msysgit installs. But I donāt recommend using this, you should have all you need in a single place so that you can use as many terminals as you want in Console2 without worrying if itās a āgitā terminal or an āeverything elseā terminal.
Once youāve done all that, you can go ahead and install rock to compile ooc code.
The basic process goes like this: I suggest you decide on a single directory
where to put all your ooc development files - for this example, Iām going to go
with /c/Dev/
. Note: if you donāt have wget on your system, grab it with
mingw-get install msys-wget
cd /c/Dev/
git clone https://github.com/fasterthanlime/rock.git
cd rock
make quick-rescue
If everything goes well, you should have a bin/rock.exe
that works. And if
you followed my .bashrc advice earlier, itās even already in your path!
Building/installing dependencies
Most cross-platform libraries have MinGW ādev packagesā that you can get if you look around, or they have MSVC dev packages that you can turn into MinGW-compatible dev packages with a bit of fiddling. However, thatās not satisfactory for me - I want to build everything from source, and get/install any package with a single command instead of having to search for it online.
For that purpose, Iāve ported homebrew to run in an MSYS environment, on Windows. To grab it, simply follow these instructions in your bash shell:
cd /usr
git clone https://github.com/nddrylliog/homebrew-mingw.git local
Note: homebrew-mingw is still very much experimental, it assumes a few
things (that you have msysgit in your path, that MinGW is installed to
C:\MinGW
, etc.). You can work with it and it built over 40 packages
flawlessly for me, but thereās work to do. Contributions, bugfixes, and package
upgrades are welcome!
Perhaps one of the first packages to install with homebrew is pkg-config. Itāll allow you to work with most C libraries, setting the correct paths for your C compilers so that it finds headers and libraries. To install it, simply do:
brew install pkg-config
Optionally, you can pass -v
or --verbose
to brew, so that it shows which
commands it is running. I find that it helps time pass by faster, otherwise
staring at a āCompilingā line for a few minutes is unsettling. One thing youāll
notice is that the same packages will take a lot longer (Iām talking 3-4x
longer) to compile with MinGW (ie. GCC for Windows) than they did with Clang on
OSX. I have no idea why MinGW is so slow (even on an SSD). If someone finds
out, please, please tell me. The executables produced by MinGW seem to be
running speedily, though!
Building your program
Building your program in ooc is as simple as running:
rock
(Optionally, pass -v
or --verbose
to see the commands rock runs)
In your programās directory, as long as you follow our conventions. The conventions for a program named āfoobarā and that uses ooc-sdl, should be as follows: the directory should be named āfoobarā and have a file named āfoobar.useā in it. The contents of the file would look like this:
Name: foobar
Description: Foobar is a game of some sort
SourcePath: source
Main: foobar.ooc
Requires: sdl
That way, rock can find your .ooc source files (all shoved in source/, possibly
neatly organized in packages), and knows which one is the main executable. The
Requires
clause specifies which ooc libraries are need, and in turn, the
.use
files of those libraries give instructions to the C compiler on where to
find the headers and link to the right libraries.
If everything went fine, you should have a foobar.exe
in your foobar
directory. You can run it either in bash with ./foobar
or by simply double
clicking it in the Windows explorer!
However, that .exe depends on .dll files (dynamic libraries) that are scattered across your system. Thatās fine when developing, but not when you package your application! Hence the next section.
Packaging your program
Windows has a few dangerous but convenient conventions in place. For example, when launching an executable when double-clicking on it, or from a shortcut, it will look for .dll files first in the directory of the executable.
Weāll use that to our advantage to package our application. The first step to package your application for Windows is to find out on which .dll files your application relies to launch. Steve P. Miller released a convenient application called depends.exe.
Opening it on your application will let you know the .dlls itās loading at runtime. Then, you can just distribute them along (in the same folder as) your .exe file, and youāre good to go!
For minimal distribution, just releasing a .zip of your assets, .exe and .dll files is enough. For a full installer, you might be interested in NSIS
OSX
Building your program
You can pretty much follow the instructions from the Windows section, except everything
is much simpler: you basically have to install Homebrew, and then you can install
rock with brew install rock
, and compile your ooc program as usual.
Packaging your program
On OSX, the common way to distribute a game is to have an āAppā. Apps are just folders, in our example named foobar.app, with a standard directory structure. Hereās an example directory structure for the foobar application bundle:
foobar.app
Contents
Info.pList
MacOS
assets
foobar
wrapper
Resources
libs
somelib.dylib
The pList file should look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleGetInfoString</key>
<string>Foobar</string>
<key>CFBundleExecutable</key>
<string>wrapper</string>
<key>CFBundleIdentifier</key>
<string>com.yourdomain.www</string>
<key>CFBundleName</key>
<string>legithief</string>
<key>CFBundleIconFile</key>
<string>legithief.icns</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>IFMajorVersion</key>
<integer>0</integer>
<key>IFMinorVersion</key>
<integer>1</integer>
</dict>
</plist>
Thereās still two tricks up our sleeve, though. First off, although we have created an app bundle, our dynamic libraries are still not in there. Weāve made a directory for it, but weāre not going to copy them by hand: thereās a much better solution in our case.
Enter dylibbundler, a utility that will:
- Identify the libraries your program uses
- Copy these libraries to a
libs
directory within your app bundle (as seen above in the directory structure) - Modify your executable so that the runtime library search path points to
the
libs
directory inside your app bundle.
Before messing with dylibbundler, we need to compile our ooc program with an
additional flag passed to the C compiler, to reserve enough room to set the
library search path. That flag is -headerpad_max_install_names
. As you may or
may not know, you can pass options to the C compiler by prefixing them with +
in the rock command line. So our build command becomes:
rock +-headerpad_max_install_names
And we can copy that executable to Foobar.app/Contents/MacOS
. Then, we can
use dylibbundler to copy the libs and alter the executable. That command should
look like:
dylibbundler -od -b -x ./Foobar.app/Contents/MacOS/helloworld \
-d ./Foobar.app/Contents/libs/
If everything went right, you should have a bunch of .dylib files in the libs
directory.
Finally, we have one additional trick that we need to apply: weāre all set on
dynamic libraries (.dylib files) when your application is launching, but what
about the CWD (current working directory)? As-is, it wonāt be the
Contents/MacOS
directory inside your app bundle, but it needs to be.
For that purpose, we use a wrapper shell script that will cd to the right directory and then launch the actual executable. Hereās an example wrapper file:
#!/bin/bash
cd "${0%/*}"
./foobar
Donāt forget to make it executable (chmod +x ./wrapper) and put it into
Contents/MacOS
. Also, keep in mind your Info.pListās CFBundleExecutable
should be set to wrapper
and not to foobar
.
When all that is done, your .app should launch on double-click from Finder.
Time to package it into a zip (zip -r Foobar.zip Foobar.app
) and upload it
somewhere!
For more advanced install methods on OSX, you might want to take a look at how to create a .dmg, or how to use PackageMaker.
A final note for OSX: when people encounter errors running your application,
ask them to run it from the console with open Foobar.app
to see error
messages. And before blaming your app, run plutil Info.pList
to check your
info file for syntax errors.
Linux
On Linux, we face the same challenges as on Windows and OSX: 1) making sure your application can find dynamic libraries, and 2) making sure it can find its assets.
In my case, I had an additional challenge: Iām building from a 64-bit version of Ubuntu 12.10, but to be as universal as possible, I want to produce a 32-bit executable.
Building your program
Getting and building rock is as simple as cloning it, building it with make quick-rescue
, and adding it to your path. (For more detailed instructions, see
the Windows section above).
Once weāve done that, we can compile our application with the same commands as on other platforms. However, if youāre on 64-bit and want to compile for 32-bit, you have a few modifications to make.
First, you want to install gcc-multilib
and ia32-libs
on Ubuntu. Then, you
want to pass -m32
to rock, which will then pass it to the C compiler. And
finally, since weāll use the same trick as on OSX to distribute dynamic
libraries along the executable, weāll need to pass -Wl,-R,libs
to the
compiler (-Wl is a trick to pass options from the C compiler to the linkerā¦
itās layers upon layers).
So, our final ooc build command becomes:
rock -m32 +-Wl,-R,libs
Note that there are some peculiarities depending on the libs. For example, for
SDL, the 32-bit dev libs on Ubuntu expose not -lSDL
, but -lSDL1.2
. And whatās
more, even with the correct path, it wouldnāt link. So in my case, I had to
tweak ooc-sdl/sdl.use
to have this Libs line:
Libs: /usr/lib/i386-linux-gnu/libSDL-1.2.so.0
And then it compiled and ran fine. Note that you wouldnāt have this problem if instead of relying on Ubuntuās dev packages, you compiled your own SDL version. Perhaps it is time to port homebrew to Linux? I know Iām fed up with the discrepancies between distributions and the whims of packagers.
Packaging your program
And then we have one task left: actually copying dynamic libraries (that happen to be .so files on linux - so much for consistency) to our newly-created libs directory.
For that, we donāt have a tool like dylibbundler for OSX, at least not that I know of. So weāll have to do with a few basic tools. Donāt be afraid though, itās easier than it looks!
The first thing we want to do is to know which libraries our program depends
on. Thanks to ldd
, thatās dead easy. Running ldd foobar
should output
something like:
linux-gate.so.1 => (0xf76df000)
libSDL-1.2.so.0 => /usr/lib/i386-linux-gnu/libSDL-1.2.so.0 (0xf761a000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7470000)
libasound.so.2 => /usr/lib/i386-linux-gnu/libasound.so.2 (0xf737d000)
libm.so.6 => /lib/i386-linux-gnu/libm.so.6 (0xf7351000)
libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xf734c000)
libpulse.so.0 => /usr/lib/i386-linux-gnu/libpulse.so.0 (0xf72f9000)
libX11.so.6 => /usr/lib/i386-linux-gnu/libX11.so.6 (0xf71c2000)
*snip*
Thanks to a wee bit of command-line trickery, we can copy all these libraries into our libs folder:
cp $(ldd foobar | cut -d ' ' -f 3) libs/
What are we doing here? Weāre breaking down the output of ldd by fields separated by spaces, using the cut command. Then weāre passing all these paths to cp, and telling it where to copy them. If it went fine, you should have a bunch of .so files in libs/.
Since we compiled our executable with the -R
linker option, running ldd foobar
again from the directory of our executable should know display this
kind of output instead:
linux-gate.so.1 => (0xf77ab000)
libSDL-1.2.so.0 => libs/libSDL-1.2.so.0 (0xf770f000)
libc.so.6 => libs/libc.so.6 (0xf7565000)
libasound.so.2 => libs/libasound.so.2 (0xf7472000)
libm.so.6 => libs/libm.so.6 (0xf7446000)
libdl.so.2 => libs/libdl.so.2 (0xf7441000)
libpulse.so.0 => libs/libpulse.so.0 (0xf73ee000)
libX11.so.6 => libs/libX11.so.6 (0xf72b7000)
*snip*
One last thing: you want to make sure that whatever happens, your executable is ran from the right directory. In order to do that, we can create a launcher script, just like we did for OSX. Our wrapper file might look like this:
#!/usr/bin/env bash
SCRIPTPATH=$(cd $(dirname $0); pwd -P)
cd $SCRIPTPATH && ./foobar
Donāt forget to chmod +x wrapper
, and voila! Portable linux app,
cross-distribution, and runs on 32-bit and 64-bit, normally. If you want to
provide both a 32-bit and a 64-bit version of your app, nothing prevents you
from making two separate builds. A word of warning though: itās easy to cross-
compile from 64-bit to 32-bit, but Iām almost certain the inverse is not.
For starters (demos, etc.) Iād slap that binary, libs, and assets directory in
a āfoobarā directory, itself in a .tar.bz2 archive (tar cjvf foobar.tar.bz2 foobar
) and itād be enough. For more advanced installation meansā¦ well,
stay tuned. I have explored it a little bit but between Loki installer
(obsolete), 0install (requires a client), autopackage (website down), and
listaller (requires a client), I donāt know where to turn.
You might want to take a look at makeself to make self-extracting archives, but anything that requires using the command-line is probably a bad idea. Iād suggest publishing on Desura and Steam, but I must admit I still have no experience there!
And on a final note, I would recommend against having anything to do with the Ubuntu Store, seeing as they seem to have dishonest business practices
A note on static linking
Astute readers might have noticed the visible absence of static linking anywhere in this article. Hereās the short version: static linking is unsupported on all the platforms we talked above.
And hereās the long version: most libraries and platforms have given up on static linking. For OSX, applications have been for a long while depending on Frameworks, which are just a bunch of headers and dynamic libraries shoved in a folder. For Windows, everybodyās using DLLs because theyāve never been a big issue since you can just put them alongside your executable (trust me, DLL āhellā is nothing compared to its OSX and Linux variantsā¦)
So, you might succeed statically linking your application, but itās almost guaranteed that you will run into strange issues and in general, youāll be swimming against the current: the methods outlined above just work, without hassle.
Summary
Distributing games mean making sure it can find assets and the dynamic libraries it needs. Often, it means distributing dynamic libraries (.dll on Windows, .dylib on OSX, .so on Linux) in your applicationās folder or a subfolder.
On Windows, the OS will look for .dlls alongside your executable. On Mac, the
best thing to do is to create an app bundle, put the libs in there and use
dylibbundler to adjust your executableās runtime library search path. On Linux,
you can simulate an app bundle with a directory, and directly pass
-Wl,-R,libs
to the C compiler when building your app, using ldd
, cut
and
cp
to copy the relevant libraries to your libs subfolder.
If youāre building on a 64-bit OS, you should produce at least 32-bit binaries. On Ubuntu, it requires installing additional packages. On other distributions, your mileage may vary. You may end up having to work in a 32-bit chroot.
Often, a .zip or .tar.bz2 with your application it will be enough, especially if the executable runs on a double-click in the most used file manager on the target OS. More advanced options exists, and digital publishing platforms each have their own requirements.
All in all, I hope you learned something today and that this article will be a point of reference from now on for people in need.
As a reminder, thereās a small, but growing, community around making games with the ooc programming language, hosted at oocgaming.org. The folks there will be happy to discuss packaging - and other issues - with you at length.
Here's another article just for you:
Declarative memory management
It feels like an eternity since Iāve started using Rust, and yet I remember vividly what it felt like to bang my head against the borrow checker for the first few times.
Iām definitely not alone in that, and thereās been quite a few articles on the subject! But I want to take some time to present the borrow checker from the perspective of its benefits, rather than as an opponent to fend with.