Thursday, May 2, 2019

How to include a CMake project in a Xcode project

For an iOS app I'm working on I needed to include a C++ project, have it build according to the current scheme (debug / release) and be able to debug it. I don't know much about C++, but I know that CMake is a popular build tool and for the project I was trying to import it was the preferred way to build it anyway. Turns out there are not a lot of people doing this as most of the information you'll find is either outdated (no, I'm not using Xcode 5 anymore) or requires a lot of background knowledge about both Xcode and CMake - which I'm both lacking.

To save myself and others some time in the future, I'd like to walk through an example of how you can build a CMake project for iOS in Xcode:

1. Create iOS Xcode project

Doh! Assuming you don't have one already. There is nothing specific to do here.

2. Clone the C++ project you'd like to include in your project

I'll be using glog for the purpose of this tutorial. Please note that some projects do not build out of the box on iOS for different reasons. I have created a fork of some projects that I needed on iOS, maybe you are lucky and the one you are looking for is among them:

3. CMake, meet Xcode.

First you need to clone a toolchain for iOS to teach CMake how to compile for iOS. Please note: I was not able to use the latest version at the time of writing this article. Instead, I used a specific commit.
Next, run the following one-liner inside your project folder in the terminal:
mkdir build/ && cd build/ && cmake -G "Xcode" -DCMAKE_TOOLCHAIN_FILE=../ios-cmake/ios.toolchain.cmake ../glog.ios/ && cd ../
I usually place that in a file called build.sh as you will need to rerun this whenever the C++ project changes (i.e. new files added).

The above commands will take a while to complete, but at the end you should see something along the lines of:
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/tom/workspace/cmake-sample.ios/build
Please note that you might have to install CMake first and the Xcode command line tools.

4. Xcode, meet CMake!

If the previous step succeeded and did not report any errors in the last three lines you are already halfway there! Hang tight. You should see a new .xcodeproj-file now in a folder called "build" if you look at your project using the file explorer. In my case it's called glog.xcodeproj. Drag this file into the file panel (on the left) of your Xcode project to link them. It doesn't really matter where exactly you drag it to as the file structure of a Xcode project is purely virtual.
Files created by CMake after running the above commands
This is what your Xcode project might look like after dragging the project created by CMake into it


A few more things left to click-click configure in Xcode: go to your project file and under "General" click the Add-button for "Linked Frameworks and Libraries". In the dialog that pops up you should see your C++ project show up. Add it!
Linking the framework generated by CMake

In the same file under "Build Phases" add your C++ project to "Target dependencies" (note: there are lots of targets showing up here. It's usually the one with a grey house-like icon and the exact same name as your C++ project). If you don't do this, Xcode won't automatically rebuild your C++ project if necessary.
Adding the framework as a build dependency

Last but definitely not least you have to tell Xcode where to find the headers of the C++ project. Again in the project file, under "Build Settings" search for a setting called "Header Search Paths", add the path to the header files and make it "recursive" (should work with "non-recursive" too if you put the correct folder there). One would expect this setting to change automatically, but unfortunately you'll have to do this for each C++ dependency you add.
Specifying path to lo

5. Wrap C++ with Objective-C to use it in Swift

As of now Swift does not support calling C++ directly. Instead you have to call Objective-C, which can call C++. There's a lot of stuff about this online already, so give it a go. Feel free to leave a comment if you can't get it to work.

(A few tips and tricks to fix common quirks)

  • If your project doesn't build and you are completely out of ideas what went wrong, try removing the build-folder, rerunning the commands to create it and restart Xcode (yes, that does help sometimes).
  • Sometimes, cleaning the project twice makes a difference. I swear I'm not kidding.
  • All of this is very unstable in my experience, so it could be that it stops working with the next Mac / Xcode update. CMake versions do matter too (I'm using 3.14.0).

7 comments:

  1. Note that this approach has issues if you are sharing the output with anyone else, particularly the Xcode project that is copied into the parent. CMake uses absolute paths ( no way around this ) and so the Xcode project generated has absolute paths embedded in it. If you check this into source control and someone checks it out, nothing will work.

    ReplyDelete
    Replies
    1. You're absolutely right, this setup is not ideal for a collaborative workflow. It worked well for me so far by requiring everyone to run the CMake command after checking out new changes. To simplify things, I've created a build bash script in the root of the project and ask every collaborator to run that: https://github.com/opendocument-app/OpenDocument.ios/blob/086c13351a7b07c7f1b50b2a0bd216978e92e983/build-device.sh

      Delete
  2. How do you select a development team for the project then? It can't be installed on a real device with the error "Signing for "xxx" requires a development team. Select a development team in the Signing & Capabilities editor." Unlike a normal project, there is no Signing and Capabilities editor.

    ReplyDelete
    Replies
    1. The Xcode project generated from Cmake does not require any signing itself. Instead you include that generated Xcode project in another Xcode project that DOES have signing configured. Check out my app to see it in action: https://github.com/TomTasche/OpenDocument.ios

      Delete
    2. I am also facing the same issue. When I open Xcode solution, choose the target and hit Build, I get the same error to Code sign. My main iOS project is already code signed.

      Here are my observations

      Build main iOS project for devices: BUILD FAILED

      Signing for "xxx" requires a development team. Select a development team in the Signing & Capabilities editor.

      Build main iOS project for simulator: BUILD FAILED

      clang: error: no such file or directory: '/Users/user/Workspace/Cmake/Cmake Sample/build/lib/{C++ project}/core/Debug-iphonesimulator/libxxx.dylib' Command Ld failed with a nonzero exit code

      Build generated Xcode project for devices: BUILD FAILED

      Choosing ALL_BUILD target

      For .app targets: Bundle identifier is missing. xxx doesn't have a bundle identifier. Add a value for PRODUCT_BUNDLE_IDENTIFIER in the build settings editor.

      For .dylib targets: Signing for "xxx" requires a development team. Select a development team in the Signing & Capabilities editor.

      Build generated Xcode project for simulator: BUILD SUCCEEDED

      Choosing ALL_BUILD target

      Delete
  3. I guess now it is mandatory to specify the PLATFORM
    mkdir build/ && cd build/ && cmake -G "Xcode" -DCMAKE_TOOLCHAIN_FILE=../ios-cmake/ios.toolchain.cmake ../glog.ios/ && cd ../ -DPLATFORM=OS64
    worked for me

    ReplyDelete
  4. Thank you, your tutorial helps a lot!

    ReplyDelete