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.
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.
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.
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 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
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
sourcessection defines which folders to add to the compilation phases. It will recursively add any source files in the specified list of folders. We are adding just one folder named
Sourcewhich doesn’t exist just yet but will be added shortly.
settingssection defines what type of
C/C++behaviour we want the compiler to take when building the project.
HEADER_SEARCH_PATHStells our project to include the
SDL2source library header files.
LIBRARY_SEARCH_PATHStells our project to search into the
Frameworksfolder for library dependencies, such as the
dependenciessection defines what frameworks our project needs, and additionally with the
embed: truedeclares which ones should be copied and bundled into our application output.
project.yml file now.
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
macos folder looked like this:
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.
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.
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
End of part 3