a-simple-triangle / Part 3 - Setup MacOS app

In this article we are going to add in the Mac Desktop platform target to our existing workspace. To recap where we got to in our previous article we:

To add the Mac Desktop platform target, we will build upon our existing workspace, continuing the theme of automating the setup scripts.

I think its worth spending a bit of time explaining the structure of a MacOS desktop application. In many ways its similar to our console application, except that it is designed to have bundled up all its dependencies and resources into a single artifact, which is the application itself.

The MacOS desktop application is in fact just a folder containing files. You can right click on one and choose Show Package Contents to view it like a folder structure. Here is an example of our A Simple Triangle application structure:

A Simple Triangle.app
  + Contents
    + _CodeSignature
      CodeResources
    + Frameworks
      SDL2.framework
    Info.plist
    + MacOS
      A Simple Triangle
    PkgInfo
    + Resources

You can see above that our SDL2.framework is actually bundled up into the application structure, and the Contents/MacOS/A Simple Triangle is the actual executable file.


Creating the MacOS platform target

Originally when I was figuring out how to get a MacOS desktop application running with the libraries we are using, I simply opened the Xcode IDE and clicked heaps of buttons and settings until I got things working. There are however some significant drawbacks to this approach which are usually such a high source of grief to Mac/iOS developers that I decided to take a different approach.

Drawback #1

The first drawback I’ll explain is that if you plan to have more than 1 developer working on an Xcode project (say, a team of people which is pretty much all the time) then prepare yourself for constant, soul destroying version control conflicts whenever people want to merge their changes! This is because Xcode stores the representation of a project as a monolithic Xcode proprietary formatted text file which changes every time you change something about the project in the IDE. So, when you have more than 1 person trying to work on the same project at the same time you will almost immediately be forced to start resolving merge conflicts when someone wants to merge.

The file I’m talking about can be found by right clicking on any Xcode .xcodeproj file, then selecting Show Package Contents (yep its just a folder) and observing that there is a file named project.pbxproj. Go ahead and open that file in a text editor to see what’s inside - I’ll wait. Not too pretty huh? Imagine trying to safely resolve a Git merge conflict with that thing - its an all too common scenario in reality.

Drawback #2

The other drawback that I want to avoid is that as you add more source files into your project you would need to manually add them into the Xcode project by hand by either creating them in Xcode itself (we won’t be doing this), or for an external file, right clicking inside the Xcode IDE and choosing Add files or folders. Because we will be adding new C++ header and sources files all the time outside of Xcode (via Visual Studio Code), this becomes a huge pain to try and keep the Xcode projects always in sync with the reality of our file system. I absolutely do not want to have to open Xcode and add/remove source files by hand every time.

How to avoid these drawbacks

We are going to take a different approach using a really neat tool named XcodeGen. This tool allows us to describe what our Xcode project is composed of, along with what its configuration settings are in an easy to read version control friendly file, which when processed by the XcodeGen tool will actually generate the .xcodeproj file for us.

Why would we do this? Because then we can completely ignore our .xcodeproj file for version control and simply regenerate it on demand, totally avoiding any version control conflicts in the process - this resolves drawback #1.

Let me repeat that important bit again in case you missed it:

We can completely ignore our .xcodeproj file for version control and simply regenerate it on demand.

The other advantage of being able to regenerate the Xcode project on demand is that it also provides us with a way to keep the Xcode project 100% in sync with all external source code files - resolving our concerns about drawback #2.

All we need to do is make sure to re-run our project generation any time files are added or removed from our workspace file system so the project is created fresh again.

Note: This obviously means that any changes to project configuration done manually within the Xcode editor will be lost the next time the project is re-generated. The workflow should instead be to update the XcodeGen definition file to make any changes, then re-generating the project again.


Create our new platform target folder

Create a new folder named macos as a sibling to our console folder that we created in the last article, then create a new text file named setup.sh in that folder:

root
  + project
    + console
    + macos
      setup.sh
    + main

Mark the new setup.sh as executable via Terminal as we’ve done before for other scripts (chmod +x setup.sh) then edit it, entering the following script:

#!/bin/bash

# Include the shared scripts.
. ../shared-scripts.sh

# Ask Homebrew to fetch our required programs
fetch_brew_dependency "wget"
fetch_brew_dependency "xcodegen"

fetch_third_party_lib_sdl
fetch_framework_sdl2

Most of that script should look familiar - we did the same thing for the console target. One of the differences here is that we are installing xcodegen via Homebrew and not worrying about cmake this time. You may observe that all the work we put in to create a collection of shared scripts is starting to pay off by being able to easily reuse methods like fetch_brew_dependency to add more dependencies.

We still need to download the SDL2 source library and framework, because the MacOS target will use them similarly to how the console target did. If you want to run ./setup.sh now you can, though we’ll be adding a bit more to it soon to do the xcodegen step.


Creating our XcodeGen definition

The XcodeGen tool has some great documentation that I’d highly recommend reading if you are curious: https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md. We will be creating our definition file as per the documentation, choosing to use the YAML format because it seems to be the preferred format for this tool.

Create a new text file in the root/project/macos folder named project.yml, then enter the following script:

name: A Simple Triangle

options:
  bundleIdPrefix: io.github.marcelbraghetto
  createIntermediateGroups: true
  usesTabs: false
  indentWidth: 4
  tabWidth: 4
  deploymentTarget:
    macOS: "10.12"

settings:
  CLANG_CXX_LANGUAGE_STANDARD: c++17
  CLANG_CXX_LIBRARY: libc++
  GCC_C_LANGUAGE_STANDARD: c11
  CLANG_WARN_DOCUMENTATION_COMMENTS: false

targets:
  A Simple Triangle:
    type: application
    platform: macOS
    info:
      path: Generated/Info.plist
    entitlements:
      path: Generated/app.entitlements
    sources:
      - Source
    settings:
      HEADER_SEARCH_PATHS: $(PROJECT_DIR)/../../third-party/SDL/include
      LIBRARY_SEARCH_PATHS:
        - $(inherited)
        - $(PROJECT_DIR)
        - $(PROJECT_DIR)/Frameworks
    dependencies:
      - framework: Frameworks/SDL2.framework
        embed: true
      - sdk: OpenGL.framework

Quite a few of the settings would be familiar to a Mac/iOS developer and I’d recommend reading both Apple’s and XcodeGen’s documentation to learn about them.

Interesting bits of our definition

Close the project.yml file now.


Running XcodeGen to create our project

Open the setup.sh again for editing, and add the scripts at the bottom so your file looks like this:

#!/bin/bash

# Include the shared scripts.
. ../shared-scripts.sh

# Ask Homebrew to fetch our required programs
fetch_brew_dependency "wget"
fetch_brew_dependency "xcodegen"

fetch_third_party_lib_sdl
fetch_framework_sdl2

# Check to see if we have an existing symlink to our shared main C++ source folder.
if [ ! -d "Source" ]; then
    echo "Linking 'Source' path to '../main/src'"
    ln -s ../main/src Source
fi

# Invoke the xcodegen tool to create our project file.
echo "Generating Xcode project"
xcodegen generate

There is a peculiar piece of script here that needs to be explained. At the time this article was authored, the XcodeGen tool had a pending issue which would affect us because our shared source folder is at ../main/src. To work around this issue, we will simply create a symlink named Source that points to ../main/src, then our definition file will think that Source is in the same folder, avoiding the issue. Hopefully in a later version this issue will have been corrected and we wouldn’t need to do this.

The final command in the script xcodegen generate will trigger the XcodeGen tool to start, which will then read in our project.yml file to know how to generate our project.

Save and close the setup.sh, then run it from Terminal again. You should see some output like this:

$ ./setup.sh 
Fetching Brew dependency: 'wget'.
Dependency 'wget' is already installed, continuing ...
Fetching Brew dependency: 'xcodegen'.
Dependency 'xcodegen' is already installed, continuing ...
SDL library already exists in third party folder.
SDL2.framework already exists ...
Linking 'Source' path to '../main/src'
Generating Xcode project
Loaded project:
  Name: A Simple Triangle
  Targets:
    A Simple Triangle: macOS application
⚙️  Generating project...
⚙️  Writing project...
Created project at <snip>/macos/A Simple Triangle.xcodeproj

Prior to running the setup.sh, our macos folder looked like this:

project.yml
setup.sh

And after running setup.sh it will look like this:

A Simple Triangle.xcodeproj
+ Frameworks
+ Generated
project.yml
setup.sh
+ Source

That feels kinda magical… let’s see if it actually works! Open the A Simple Triangle.xcodproj in Xcode itself:

I wonder what happens if I do absolutely nothing except press the Play button in Xcode:

So, just to highlight what just happened, we simply wrote a YAML definition for an Xcode project, ran the xcodegen tool, then were able to run the desktop application without performing any other actions. I think that is pretty amazing …

Let’s add a new source file and regenerate the project so we can see if this is smoke and mirrors or not!

Note: You will likely want to exit out of Xcode before regenerating the project - weird things could happen if we regenerate it while Xcode is actively using it.

Quit Xcode, then create a new text file named hello.hpp in the root/project/main/src folder. You don’t have to put any code in it.

Run the setup.sh script again in the macos folder, then re-open A Simple Triangle.xcodeproj in Xcode:

There it is - hello.hpp was automatically included in the regenerated Xcode project.


Git ignore

If you were to commit A Simple Triangle into version control, you would want to create a new .gitignore file for the root/platform/macos folder, so it won’t include all the files that get auto generated by running XcodeGen. Here is a sample .gitignore that would achieve this for our project:

A Simple Triangle.xcodeproj
DerivedData
Frameworks
Source
Generated

The code for this article can be found here.

Continue to Part 4: Setup iOS app.

End of part 3