Introduction
I’ve been making a C++ game engine from scratch in the past few months, and one of the first things I worked on after getting the basics working was making sure that the engine compiled and worked on all the platforms that I’m planning on supporting. Building the macos version has been particularly difficult because I don’t have a mac, and unlike everything else I’m targeting I can’t just cross compile from Linux.
In this blog post I will explain how I solved this problem by making my macos builds using CI/CD tools (specifically appveyor), through a toy example of an application that uses SDL2 and Lua.
The complete project that I describe in this blog post can be found here.
The toy application
The test application is all in a single main.cpp
file, and doesn’t really do anything interesting.
|
|
CMake project
I like to use CMake for my cross-platform projects, so that’s how I’m setting up this example. We could just as easily do this directly with a Makefile. The CMakeLists.txt
file is the following.
|
|
If you are familiar with CMake the non-macos-specific lines should be easy to parse. For the macos lines:
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
is needed to make sure that the application is built as a universal binary that works on both x86_64 and the new arm-based macs.set(CMAKE_OSX_DEPLOYMENT_TARGET 10.11)
sets the minimum version of macos that will be needed to run the application. I’m setting it to10.11
here because that’s the minimum required by the SDL2 version I’m linking to.- The
MACOSX_BUNDLE
option inadd_executable
tells CMake to make an app bundle out of the executable. Note that you will probably want to customise the app bundle for your own application when you make builds (for example by adding an icon), but explaining how to do that is out of the scope of this blog post.
Building with appveyor
The CMake project described above builds without problems on Linux, and it would build on a mac if you had a mac. But you don’t have a mac, or you wouldn’t be reading this!
My solution is to set up a git repository for the application, and connect it to appveyor. The following assumes that you already know how to do that.
The configuration of appveyor for this task is quite simple. The content of appveyor.yml
is
|
|
This creates a simple job using a macos worker, with all the commands needed for the build inside the mac.sh
bash script.
The bash script
There are 4 things that we need to do in the bash script:
- Build Lua, as there are no precompiled releases
- Download SDL2 and add it to the worker
- Build the application bundle using CMake
- Create an artifact from the application bundle so that we can download it after it’s built
To make things simpler, I will split the mac.sh
file in 4 parts to go over each of these things separately.
Building Lua
|
|
This part is probably the most complicated one, and it’s due to the fact that the Lua Makefile is not set up to build a shared library, so we need to add an extra rule to do that for us.
Setting MACOSX_DEPLOYMENT_TARGET
is needed to make sure that the compiler sets the appropriate minimum version (otherwise the worker version is used by default)
Once liblua.dylib
has been compiled, we copy it with the appropriate header files to $HOME/Library/Frameworks/
, where CMake will be able to find them.
Getting SDL2
This one is quite easy, since there are prebuilt version already set up as macos frameworks.
|
|
Building the app bundle
|
|
The first half here is just standard CMake building. Things get more complicated in the second half because we need to make sure that the dependencies (SDL2 and Lua) are bundled inside the .app
and are loaded from there by our executable.
To achieve this, we do the following:
- Copy
SDL2.framework
andliblua.dylib
inside the app bundle, in the same directory as the executable (i.e.,SDL2_Lua_test.app/Contents/MacOS/
) - Set the
rpath
of the executable so that it loads dynamic libraries from the folder where it is located (@executable_path
)
I suggest reading here for more information on the rpath steps.
Creating the artifact
Finally, we do this:
|
|
This allows us to download the app bundle we created from the ‘Artifacts’ of the current build on appveyor.
That’s it, it’s all done!
Some tips
Some additional tips if you are interested in trying this approach:
- Testing the build by changing the bash file, pushing to git, and restarting the appveyor build is time consuming and extremely frustrating. I strongly recommend setting up appveyor for ssh access so you can test from the inside in real time!
- You can also set up appveyor to deploy the artifact somewhere, for example as a github release.
- This approach works also for Windows and Linux builds!