At GDC I remember seeing Brendon Chung’s talk about Atom Zombie Smasher, where he mentioned how it runs on Mono, even on Windows. Besides making the game’s behavior more consistent between Windows, Linux and Mac, it also allowed him to remove the need to install the .NET framework before running the game, since he can just bundle Mono with his game.
This is attractive to me for a few reasons:
- It means all Windows players could play my game just by unzipping it — no separate step to install the .NET framework, no need for an installer to do it for them
- It means I don’t have to target .NET 2.0 to try to keep Vista/Win7 users from needing to install the .NET framework — I can use nice 4.0 features
- If I can get my game running on Mono, I can probably run on Linux and Mac as well
So I started looking into Mono. It turns out they have a utility called
mkbundle that bundles your app, and all of the assemblies it depends upon, into a single executable, and the only thing it needs to run is a mono DLL you can distribute along with it. The .NET assemblies provided by Mono are MIT/X11 licensed, but the runtime is LGPL, so while you can embed Mono’s
System.dll into your executable, you have to either dynamically link to the runtime DLL or otherwise provide your program to users in such a way that they could use a different Mono runtime if they wanted.
mkbundle is kind of broken on Windows right now, a lot of which is due to Cygwin drift since the tool was written. Luckily, it’s not so broken that you can’t fix it yourself. I mostly followed this StackOverflow answer — but there are a few changes I made to make it a lot simpler!
- First, make sure your app works under Mono on Windows. If it doesn’t, none of this will be any use to you.
- Install Mono for Windows from here. Click the Windows button in the top row (the stable release) and choose “Mono for Windows, Gtk#, and XSP.” I used version 2.10.8, I can’t guarantee that anything else will work exactly as described. You can install to the normal directory.
- Install Cygwin (UNIX-like environment for Windows.) Make sure you have selected the packages gcc-mingw, mingw-zlib1, mingw-zlib-devel, and pkg-config.
- Launch a Cygwin Terminal using the shortcut created by the Cygwin installer.
- Navigate to the directory your game assemblies are (e.g. your
bin/Releasefolder.) Note that in cygwin, a path like
C:\some\pathis expressed as
/cygdrive/c/some/path— note the slash direction and the way drive letters work! Backslashes are escape characters in bash so if you just type
\cygdrive\c\Fooit thinks you meant
- Create an environment variable
$MONOpointing to the Mono installation. On 64-bit Windows use this:
The point here is to use the DOS short filename compatibility feature to point to the right Program Files (or Program Files (x86)) directory without creating a path with spaces in it, which
- Add Mono’s
bindirectory to your
$PATHso you can run
- Add Mono’s
lib/pkgconfigdirectory to your
$PKG_CONFIG_PATHenvironment variable. Usually this starts out empty so just do:
- Now here is the magic part that makes this far less laborious — specify a value for
$CCthat invokes the mingw32 version of GCC:
export CC="i686-pc-mingw32-gcc -U _WIN32"
See, Cygwin used to bundle a version of GCC that supported the
-mno-cygwinflag to produce executables that didn’t need
cygwin1.dll. The GCC 4.x it includes does not support it any longer. That is why you have to install the gcc-mingw package. Since some people may also have normal gcc installed in Cygwin, I want to unambiguously refer to the mingw32 version of GCC.
-U _WIN32basically removes the predefined
_WIN32preprocessor definition before GCC compiles the C code that
mkbundlegenerates. The Win32 code path in that generated code is broken right now (it uses an undefined function
g_utf16_to_utf8that looks like it comes from glib.) Luckily it works if you turn off that code path — the only thing you will miss is supporting Unicode command line arguments. If this is a problem you can use the more complicated procedure from this StackOverflow post to intercept the C code before it is compiled & fix it yourself. That post doesn’t describe how to fix the UTF thing, but it lets you edit the generated C code before recompiling it.
- Now you can finally run
mkbundle. Replace the assembly names as necessary:
mkbundle YourGame.exe OpenTK.dll --deps -o Output.exe
mono-2.0.dllinto the current directory since the resulting EXE you just created depends on it:
cp $MONO/bin/mono-2.0.dll .
- Now hopefully your app will run!
You just have to distribute the output EXE file alongside
mono-2.0.dlland it should just magically work on any Windows PC.
Making a reusable script
I have combined all of these steps into a shell script here: https://gist.github.com/3005371. Once you have this as a script, you can recreate the bundle in one step whenever you want. Note that you will have to customize the script a little for it to work for your exact case.
How big is it?
You may be wondering how big your
mono-2.0.dll are, seeing as it’s pulling in a Mono runtime and a bunch of System assemblies and so on.
Well, in my test app, which just uses OpenTK to do some OpenGL drawing, the file sizes are as follows:
Output EXE: 13.4 MB
mono-2.0.dll: 2.6 MB
Both of the above, zipped at highest compression: 5.6 MB
5.6 MB is not bad at all compared to the size of textures and music you are likely to include in a game!
Making it even smaller
If you want the EXE file to be smaller even after it is unzipped, you can pass the
-z flag to
mkbundle and it will compress the embedded assemblies. (That is what the mingw-zlib* Cygwin packages are for.) This will make the EXE file smaller, although it may make startup times a little slower. That took my example EXE from 13.4 MB to 4.6 MB. Of course, now it won’t compress as much when you zip it.
There are also “linker” tools, like
monolinker, that can strip assemblies so only referenced code is used. For example, if I only used OpenGL and not the OpenAL functionality of OpenTK, it could theoretically remove the OpenAL stuff from the OpenTK assembly automatically. In practice, it’s not so simple. I tried using
monolinker and could not get it to work — it just made an OpenTK assembly that crashed when you tried to use it. I suspect
monolinker got a little overzealous and removed something that was necessary. If I could get this to work it would reduce the executable size even further.
But, honestly, I think the executable is small enough already. Any further compression would be nice, but is not necessary.
Extra DLLs required sometimes?
The TODO file in the
mkbundle source suggests that these DLLs may also need to be distributed with your program:
My program does not seem to require them, but if you use more framework functionality they may be required. I can see Atom Zombie Smasher includes some of them (specifically libglib and libgthread.) These libraries appear to all be either LGPL, X11 or zlib licensed. You may want to double check the exact terms of the licenses yourself, but it seems like it will just be fine to copy them out of the Mono bin directory and include them with your app.
mkbundle tool works out of the box on Ubuntu for me, and I am going to investigate the options for making a native-looking OS X application eventually as well.
If I have time I will try to submit a patch to Mono so
mkbundle “just works” on Windows (provided a Cygwin install.)