Apple Build System Overview
Apple's development ecosystem revolves around Xcode as the primary IDE, but CMake can generate native Xcode projects that integrate seamlessly with Apple's tooling. The Xcode generator produces .xcodeproj files that open directly in Xcode, while the Unix Makefiles or Ninja generators work for command-line builds using the same Apple toolchain. Understanding how CMake maps to Apple's build concepts — SDKs, deployment targets, architectures, and code signing — is essential for shipping production-quality software on macOS, iOS, tvOS, watchOS, and visionOS.
# Generate an Xcode project
cmake -G Xcode -B build-xcode -S .
# Generate with Ninja (faster CI builds)
cmake -G Ninja -B build-ninja -S . \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0
# Query available SDKs
xcrun --show-sdk-path --sdk macosx
xcrun --show-sdk-path --sdk iphoneos
xcrun --show-sdk-path --sdk iphonesimulator
# List installed Xcode versions
xcode-select --print-path
sudo xcode-select --switch /Applications/Xcode-15.app
SDK Selection and Deployment Targets
CMake uses CMAKE_OSX_SYSROOT to select the SDK and CMAKE_OSX_DEPLOYMENT_TARGET to set the minimum OS version your binary supports. These map directly to Xcode's SDKROOT and *_DEPLOYMENT_TARGET build settings.
cmake_minimum_required(VERSION 3.25)
project(MyMacApp LANGUAGES CXX)
# SDK selection (auto-detected from xcode-select, or set explicitly)
set(CMAKE_OSX_SYSROOT macosx) # Options: macosx, iphoneos, iphonesimulator, appletvos, watchos
# Deployment target — minimum OS version supported
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum macOS version")
# Verify settings
message(STATUS "SDK: ${CMAKE_OSX_SYSROOT}")
message(STATUS "Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
message(STATUS "Xcode version: ${XCODE_VERSION}")
add_executable(MyApp main.cpp)
target_compile_features(MyApp PRIVATE cxx_std_20)
CMAKE_OSX_DEPLOYMENT_TARGET as a cache variable allows users to override it at configure time without modifying CMakeLists.txt. This is critical for CI pipelines that must build for different minimum OS versions from the same source tree.
App Bundles
macOS applications are distributed as .app bundles — directory structures that contain the executable, resources, frameworks, and metadata. CMake creates bundles automatically when you set the MACOSX_BUNDLE property on an executable target.
flowchart TD
A["MyApp.app/"] --> B["Contents/"]
B --> C["MacOS/"]
B --> D["Resources/"]
B --> E["Frameworks/"]
B --> F["Info.plist"]
B --> G["PkgInfo"]
C --> H["MyApp (executable)"]
D --> I["app.icns"]
D --> J["Assets.car"]
D --> K["en.lproj/"]
E --> L["libfoo.dylib"]
E --> M["MyFramework.framework/"]
style A fill:#132440,color:#fff
style B fill:#16476A,color:#fff
style C fill:#3B9797,color:#fff
style D fill:#3B9797,color:#fff
style E fill:#3B9797,color:#fff
cmake_minimum_required(VERSION 3.25)
project(MyMacApp LANGUAGES CXX)
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0")
# Create app bundle
add_executable(MyMacApp MACOSX_BUNDLE
src/main.cpp
src/AppDelegate.cpp
)
# Configure Info.plist from template
set_target_properties(MyMacApp PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "My Mac App"
MACOSX_BUNDLE_BUNDLE_VERSION "1.0.0"
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.mymacapp"
MACOSX_BUNDLE_COPYRIGHT "Copyright © 2026 Example Inc."
MACOSX_BUNDLE_ICON_FILE "app.icns"
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in"
)
# Copy icon into Resources
set(APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/app.icns")
set_source_files_properties(${APP_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
target_sources(MyMacApp PRIVATE ${APP_ICON})
Resource Copying
App bundles need various resources — icons, XIB/storyboard files, asset catalogs, localization bundles, and data files. CMake provides multiple mechanisms to place files in the correct bundle subdirectory.
# Method 1: MACOSX_PACKAGE_LOCATION property (per source file)
set(RESOURCE_FILES
resources/app.icns
resources/default.png
resources/config.json
)
set_source_files_properties(${RESOURCE_FILES} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
target_sources(MyMacApp PRIVATE ${RESOURCE_FILES})
# Method 2: Localized resources
set(EN_RESOURCES resources/en.lproj/MainMenu.xib)
set_source_files_properties(${EN_RESOURCES} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources/en.lproj"
)
target_sources(MyMacApp PRIVATE ${EN_RESOURCES})
# Method 3: Post-build copy for dynamic content
add_custom_command(TARGET MyMacApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/assets"
"$<TARGET_BUNDLE_CONTENT_DIR:MyMacApp>/Resources/assets"
COMMENT "Copying asset directory into bundle"
)
# Method 4: install(TARGETS) with BUNDLE DESTINATION
install(TARGETS MyMacApp
BUNDLE DESTINATION .
COMPONENT Runtime
)
.xcassets) require compilation by actool and are only properly handled by the Xcode generator. If you use Ninja or Makefiles, you must invoke actool manually via add_custom_command(). For production apps, prefer the Xcode generator for asset catalog support.
Frameworks
Apple frameworks are versioned bundles that package a dynamic library with its public headers, resources, and metadata. CMake builds frameworks by setting the FRAMEWORK property on a library target, producing a proper .framework bundle that other projects can link against.
cmake_minimum_required(VERSION 3.25)
project(MyFramework LANGUAGES CXX)
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0")
# Define the framework library
add_library(MyFramework SHARED
src/Core.cpp
src/Networking.cpp
src/Utilities.cpp
)
# Mark as framework and configure properties
set_target_properties(MyFramework PROPERTIES
FRAMEWORK TRUE
FRAMEWORK_VERSION "A"
MACOSX_FRAMEWORK_IDENTIFIER "com.example.MyFramework"
MACOSX_FRAMEWORK_BUNDLE_VERSION "1.0.0"
MACOSX_FRAMEWORK_SHORT_VERSION_STRING "1.0"
# Public headers go into Headers/ inside the framework
PUBLIC_HEADER "include/MyFramework/Core.h;include/MyFramework/Networking.h;include/MyFramework/Utilities.h"
# Private headers (available to framework internals)
PRIVATE_HEADER "src/Internal.h"
# Framework resources
RESOURCE "resources/config.plist"
# Version compatibility
VERSION "1.0.0"
SOVERSION "1"
)
# Include directories for consumers
target_include_directories(MyFramework
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:MyFramework.framework/Headers>
)
# Install the framework
install(TARGETS MyFramework
FRAMEWORK DESTINATION "Library/Frameworks"
COMPONENT Development
)
Versioning and Umbrella Headers
Apple frameworks support versioning through a symlink-based directory structure. The umbrella header is a single header that includes all public headers, making it easy for consumers to import the entire framework.
// include/MyFramework/MyFramework.h — Umbrella header
#ifndef MYFRAMEWORK_H
#define MYFRAMEWORK_H
#include <MyFramework/Core.h>
#include <MyFramework/Networking.h>
#include <MyFramework/Utilities.h>
#endif // MYFRAMEWORK_H
# Module map for Swift interop and @import support
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
"framework module MyFramework {
umbrella header \"MyFramework.h\"
export *
module * { export * }
}")
# Copy module map into framework
set_source_files_properties(
"${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
PROPERTIES MACOSX_PACKAGE_LOCATION "Modules"
)
target_sources(MyFramework PRIVATE
"${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
)
Universal Binaries
Universal binaries (fat binaries) contain machine code for multiple CPU architectures in a single file. With Apple's transition from Intel (x86_64) to Apple Silicon (arm64), universal binaries ensure apps run natively on both architectures without Rosetta 2 translation.
flowchart LR
A[Source Code] --> B[arm64 Build]
A --> C[x86_64 Build]
B --> D[arm64 Object Files]
C --> E[x86_64 Object Files]
D --> F[arm64 Binary]
E --> G[x86_64 Binary]
F --> H["lipo -create"]
G --> H
H --> I[Universal Binary
arm64 + x86_64]
style H fill:#BF092F,color:#fff
style I fill:#132440,color:#fff
cmake_minimum_required(VERSION 3.25)
project(UniversalApp LANGUAGES CXX)
# Build universal binary with both architectures
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Target architectures")
# Deployment target must support both architectures
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")
add_executable(UniversalApp src/main.cpp)
# Verify architecture at configure time
message(STATUS "Building for architectures: ${CMAKE_OSX_ARCHITECTURES}")
# Per-architecture compile flags (generator expressions)
target_compile_options(UniversalApp PRIVATE
# ARM64-specific optimizations
$<$<STREQUAL:${CMAKE_OSX_ARCHITECTURES},arm64>:-mcpu=apple-m1>
)
Working with lipo
When CMake's built-in universal binary support is insufficient — for example, when combining separately-built architectures from different build trees — use Apple's lipo tool directly.
# Inspect architectures in a binary
lipo -info build/Release/MyApp
# Output: Architectures in the fat file: MyApp are: x86_64 arm64
# Create universal binary from separate builds
cmake -B build-arm64 -S . -DCMAKE_OSX_ARCHITECTURES=arm64
cmake --build build-arm64 --config Release
cmake -B build-x86 -S . -DCMAKE_OSX_ARCHITECTURES=x86_64
cmake --build build-x86 --config Release
# Combine with lipo
lipo -create \
build-arm64/Release/MyApp \
build-x86/Release/MyApp \
-output MyApp-universal
# Verify the result
lipo -info MyApp-universal
file MyApp-universal
# Extract a single architecture
lipo MyApp-universal -thin arm64 -output MyApp-arm64
CMAKE_OSX_ARCHITECTURES to "arm64;x86_64" automatically produces universal binaries without needing to run lipo manually. The Ninja and Makefiles generators also support this natively on CMake 3.19+.
Code Signing
All software distributed on Apple platforms must be code-signed. macOS requires signing for notarization (Gatekeeper), while iOS/tvOS/watchOS require signing for device deployment. CMake configures code signing through Xcode build settings attributes.
cmake_minimum_required(VERSION 3.25)
project(SignedApp LANGUAGES CXX)
add_executable(SignedApp MACOSX_BUNDLE src/main.cpp)
# Code signing configuration via Xcode attributes
set_target_properties(SignedApp PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Development"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABCDE12345"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER ""
)
# For distribution builds (App Store or Developer ID)
# set_target_properties(SignedApp PROPERTIES
# XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application"
# XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
# )
# Enable hardened runtime (required for notarization)
set_target_properties(SignedApp PROPERTIES
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
)
Entitlements
Entitlements declare specific capabilities your app requires — network access, file access, camera, etc. They are specified in a .entitlements plist file and embedded during code signing.
# Point to entitlements file
set_target_properties(SignedApp PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS
"${CMAKE_CURRENT_SOURCE_DIR}/SignedApp.entitlements"
)
# For command-line signing (non-Xcode generator)
add_custom_command(TARGET SignedApp POST_BUILD
COMMAND codesign --force --sign "Developer ID Application: My Company (ABCDE12345)"
--entitlements "${CMAKE_CURRENT_SOURCE_DIR}/SignedApp.entitlements"
--options runtime
--timestamp
"$<TARGET_BUNDLE_DIR:SignedApp>"
COMMENT "Code signing SignedApp.app"
)
# SignedApp.entitlements — example plist content
# Typically created via Xcode or manually:
cat << 'EOF' > SignedApp.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
EOF
dlopen() for plugins, or unsigned memory mapping, you must declare the corresponding entitlements (com.apple.security.cs.allow-jit, com.apple.security.cs.disable-library-validation, etc.) or the app will crash at runtime.
iOS/tvOS/watchOS Cross-Compilation
Building for iOS and other embedded Apple platforms requires cross-compilation. CMake handles this by setting CMAKE_SYSTEM_NAME to the target platform, which triggers platform-specific behavior including SDK resolution, architecture selection, and signing requirements.
# ios-toolchain.cmake — CMake toolchain file for iOS
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "16.0")
# Device build (default)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_OSX_ARCHITECTURES arm64)
# Or use the variable to switch between device and simulator:
# set(CMAKE_OSX_SYSROOT iphonesimulator)
# set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64") # Simulator needs both for M1 + Intel Macs
# Compiler flags for iOS
set(CMAKE_C_FLAGS_INIT "-fembed-bitcode")
set(CMAKE_CXX_FLAGS_INIT "-fembed-bitcode")
# Skip compiler tests that fail during cross-compilation
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Build for iOS device
cmake -B build-ios -S . \
-G Xcode \
-DCMAKE_TOOLCHAIN_FILE=ios-toolchain.cmake \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0
# Build for iOS Simulator
cmake -B build-sim -S . \
-G Xcode \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphonesimulator \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"
# Build using xcodebuild (after CMake generates the project)
xcodebuild -project build-ios/MyApp.xcodeproj \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-destination "generic/platform=iOS"
Simulator vs Device Builds
A common CI pattern builds libraries for both simulator and device, then merges them into an XCFramework. CMake 3.21+ simplifies this with improved iOS support.
cmake_minimum_required(VERSION 3.25)
project(MyIOSLib LANGUAGES CXX)
# Detect platform from CMAKE_SYSTEM_NAME
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
message(STATUS "Building for iOS")
message(STATUS " SDK: ${CMAKE_OSX_SYSROOT}")
message(STATUS " Architectures: ${CMAKE_OSX_ARCHITECTURES}")
message(STATUS " Deployment target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
add_library(MyIOSLib STATIC
src/networking.cpp
src/storage.cpp
src/crypto.cpp
)
target_include_directories(MyIOSLib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# iOS-specific: disable exceptions if needed for size
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
target_compile_options(MyIOSLib PRIVATE -fno-exceptions -fno-rtti)
endif()
install(TARGETS MyIOSLib
ARCHIVE DESTINATION lib
PUBLIC_HEADER DESTINATION include/MyIOSLib
)
Building a Cross-Platform Library for iOS and macOS
A networking library that compiles for macOS (arm64 + x86_64), iOS device (arm64), and iOS Simulator (arm64 + x86_64). The CI pipeline builds all variants and produces a single XCFramework.
# CI script: build all platforms
#!/bin/bash
set -e
# macOS universal
cmake -B build-macos -S . -G Ninja \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0
cmake --build build-macos --config Release
# iOS device
cmake -B build-ios -S . -G Ninja \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0
cmake --build build-ios --config Release
# iOS simulator (universal for M1 + Intel Macs)
cmake -B build-sim -S . -G Ninja \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphonesimulator \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0
cmake --build build-sim --config Release
XCFramework Creation
XCFrameworks are Apple's modern distribution format for binary libraries. They bundle multiple platform variants (macOS, iOS device, iOS Simulator, Mac Catalyst) into a single package with proper architecture separation — solving the "duplicate architecture" problem that plagued universal frameworks when Macs and iPhone Simulators both use arm64.
flowchart TD
A[Source Code] --> B[macOS Build
arm64 + x86_64]
A --> C[iOS Device Build
arm64]
A --> D[iOS Simulator Build
arm64 + x86_64]
B --> E[libMyLib.a
macOS universal]
C --> F[libMyLib.a
iOS device]
D --> G[libMyLib.a
iOS simulator]
E --> H["xcodebuild -create-xcframework"]
F --> H
G --> H
H --> I["MyLib.xcframework/"]
I --> J["macos-arm64_x86_64/"]
I --> K["ios-arm64/"]
I --> L["ios-arm64_x86_64-simulator/"]
style H fill:#BF092F,color:#fff
style I fill:#132440,color:#fff
# After building all platform variants (see iOS section above):
# Create XCFramework from static libraries
xcodebuild -create-xcframework \
-library build-macos/Release/libMyLib.a \
-headers include/ \
-library build-ios/Release/libMyLib.a \
-headers include/ \
-library build-sim/Release/libMyLib.a \
-headers include/ \
-output MyLib.xcframework
# Create XCFramework from frameworks
xcodebuild -create-xcframework \
-framework build-macos/Release/MyLib.framework \
-framework build-ios/Release/MyLib.framework \
-framework build-sim/Release/MyLib.framework \
-output MyLib.xcframework
# Verify the result
ls MyLib.xcframework/
# Info.plist
# ios-arm64/
# ios-arm64_x86_64-simulator/
# macos-arm64_x86_64/
# CMake function to automate XCFramework creation
function(create_xcframework TARGET)
set(options "")
set(oneValueArgs OUTPUT_NAME)
set(multiValueArgs PLATFORMS)
cmake_parse_arguments(XCF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
set(XCF_ARGS "")
foreach(platform ${XCF_PLATFORMS})
list(APPEND XCF_ARGS
-library "${CMAKE_BINARY_DIR}/${platform}/lib${TARGET}.a"
-headers "${CMAKE_CURRENT_SOURCE_DIR}/include"
)
endforeach()
add_custom_target(${TARGET}-xcframework ALL
COMMAND xcodebuild -create-xcframework
${XCF_ARGS}
-output "${CMAKE_BINARY_DIR}/${XCF_OUTPUT_NAME}.xcframework"
COMMENT "Creating ${XCF_OUTPUT_NAME}.xcframework"
)
endfunction()
# Usage:
# create_xcframework(MyLib
# OUTPUT_NAME MyLib
# PLATFORMS macos ios ios-simulator
# )
Xcode Project Generation
CMake's Xcode generator creates fully-featured .xcodeproj files with build configurations, schemes, and proper target dependencies. CMake 3.15+ added CMAKE_XCODE_GENERATE_SCHEME for automatic scheme creation, eliminating the need to manually configure schemes after generation.
cmake_minimum_required(VERSION 3.25)
project(XcodeApp LANGUAGES CXX)
# Enable automatic scheme generation
set(CMAKE_XCODE_GENERATE_SCHEME TRUE)
# Set Xcode-specific build settings
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++20")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++")
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNINITIALIZED_AUTOS "YES_AGGRESSIVE")
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_FUNCTION "YES")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_MOVE "YES")
add_executable(XcodeApp MACOSX_BUNDLE src/main.cpp)
# Scheme environment variables (available in Xcode Run action)
set_target_properties(XcodeApp PROPERTIES
XCODE_GENERATE_SCHEME TRUE
XCODE_SCHEME_ENVIRONMENT "MY_CONFIG_PATH=/tmp/config.json"
XCODE_SCHEME_ARGUMENTS "--verbose;--port=8080"
XCODE_SCHEME_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
)
# Address Sanitizer configuration for Debug scheme
set_target_properties(XcodeApp PROPERTIES
XCODE_SCHEME_ADDRESS_SANITIZER TRUE
XCODE_SCHEME_ADDRESS_SANITIZER_USE_AFTER_RETURN TRUE
XCODE_SCHEME_UNDEFINED_BEHAVIOUR_SANITIZER TRUE
)
Test Targets
Test targets in Xcode need proper scheme configuration to appear in Xcode's test navigator and run via xcodebuild test.
# Enable testing
enable_testing()
# Test executable
add_executable(XcodeAppTests
tests/test_main.cpp
tests/test_networking.cpp
tests/test_storage.cpp
)
target_link_libraries(XcodeAppTests PRIVATE
XcodeAppLib # library under test
GTest::gtest
GTest::gtest_main
)
# Register with CTest
add_test(NAME UnitTests COMMAND XcodeAppTests)
# Configure test scheme
set_target_properties(XcodeAppTests PROPERTIES
XCODE_GENERATE_SCHEME TRUE
XCODE_SCHEME_ADDRESS_SANITIZER TRUE
XCODE_SCHEME_THREAD_SANITIZER TRUE
)
# Build and test from command line
# xcodebuild test -project build/XcodeApp.xcodeproj \
# -scheme XcodeAppTests \
# -destination "platform=macOS"
Swift Integration
CMake 3.15+ supports Swift as a first-class language. You can build pure Swift projects, mixed C++/Swift projects, and even interoperate between C++ and Swift using bridging headers or Swift's C++ interop feature (Swift 5.9+).
cmake_minimum_required(VERSION 3.25)
project(SwiftApp LANGUAGES Swift)
# Pure Swift application
add_executable(SwiftApp
Sources/main.swift
Sources/AppDelegate.swift
Sources/ViewModel.swift
)
# Swift language version
set_target_properties(SwiftApp PROPERTIES
Swift_LANGUAGE_VERSION 5.9
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.swiftapp"
)
# Swift compiler flags
target_compile_options(SwiftApp PRIVATE
-strict-concurrency=complete
-enable-upcoming-feature ExistentialAny
)
# Mixed C++ and Swift project
cmake_minimum_required(VERSION 3.25)
project(MixedApp LANGUAGES CXX Swift)
# C++ library with headers exposed to Swift
add_library(CppCore STATIC
cpp/Engine.cpp
cpp/Physics.cpp
)
target_include_directories(CppCore PUBLIC cpp/include)
# Swift module that uses C++ code via bridging header
add_executable(MixedApp
swift/main.swift
swift/SwiftUI_Views.swift
)
# Link C++ library
target_link_libraries(MixedApp PRIVATE CppCore)
# Bridging header for Swift -> C interop
set_target_properties(MixedApp PROPERTIES
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER
"${CMAKE_CURRENT_SOURCE_DIR}/swift/BridgingHeader.h"
)
# Enable C++ interop (Swift 5.9+)
target_compile_options(MixedApp PRIVATE
$<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default>
)
Bridging Headers
Bridging headers expose C and Objective-C APIs to Swift code. For C++ interop, Swift 5.9 introduced native C++ interoperability that avoids the Objective-C++ bridging layer entirely.
// swift/BridgingHeader.h — Expose C/ObjC APIs to Swift
#ifndef BridgingHeader_h
#define BridgingHeader_h
// C functions
#include "Engine.h"
#include "Physics.h"
// Objective-C classes (if any)
// #import "LegacyManager.h"
#endif
// swift/main.swift — Using C++ types from Swift (Swift 5.9+ interop)
import CppCore // Module name matches the CMake target
func runSimulation() {
// Direct C++ class usage (no bridging header needed with -cxx-interoperability-mode)
var engine = Engine()
engine.initialize(width: 1920, height: 1080)
let physics = Physics(gravity: 9.81)
physics.step(deltaTime: 0.016)
print("Simulation frame: \(engine.frameCount)")
}
-G Xcode.
App Store Preparation
Distributing through the Mac App Store or direct distribution with notarization requires specific build configurations, code signing identities, and post-build workflows. CMake can automate most of this pipeline.
cmake_minimum_required(VERSION 3.25)
project(ProductionApp LANGUAGES CXX)
add_executable(ProductionApp MACOSX_BUNDLE
src/main.cpp
src/AppDelegate.cpp
)
# Production signing configuration
set_target_properties(ProductionApp PROPERTIES
MACOSX_BUNDLE_BUNDLE_NAME "Production App"
MACOSX_BUNDLE_BUNDLE_VERSION "2.1.0"
MACOSX_BUNDLE_SHORT_VERSION_STRING "2.1"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.productionapp"
# App Store distribution
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABCDE12345"
XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "ProductionApp AppStore"
# Required for App Store
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.example.productionapp"
# App category
XCODE_ATTRIBUTE_INFOPLIST_KEY_LSApplicationCategoryType "public.app-category.developer-tools"
)
Archive Builds and Notarization
For distribution outside the App Store (Developer ID), Apple requires notarization — uploading your signed app to Apple's servers for malware scanning, then stapling the notarization ticket to the binary.
# Step 1: Build an archive via xcodebuild
xcodebuild archive \
-project build/ProductionApp.xcodeproj \
-scheme ProductionApp \
-configuration Release \
-archivePath build/ProductionApp.xcarchive \
CODE_SIGN_IDENTITY="Developer ID Application: My Company (ABCDE12345)" \
DEVELOPMENT_TEAM=ABCDE12345
# Step 2: Export from archive
xcodebuild -exportArchive \
-archivePath build/ProductionApp.xcarchive \
-exportPath build/export \
-exportOptionsPlist ExportOptions.plist
# Step 3: Create DMG for distribution
hdiutil create -volname "Production App" \
-srcfolder build/export/ProductionApp.app \
-ov -format UDZO \
build/ProductionApp.dmg
# Step 4: Notarize the DMG
xcrun notarytool submit build/ProductionApp.dmg \
--apple-id "dev@example.com" \
--team-id "ABCDE12345" \
--password "@keychain:AC_PASSWORD" \
--wait
# Step 5: Staple the ticket
xcrun stapler staple build/ProductionApp.dmg
# Verify notarization
spctl --assess --verbose=4 --type execute build/export/ProductionApp.app
# ExportOptions.plist for Developer ID distribution
cat << 'EOF' > ExportOptions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>ABCDE12345</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Developer ID Application</string>
</dict>
</plist>
EOF
Complete CI Pipeline: Build, Sign, Notarize, and Distribute
A GitHub Actions workflow that builds a CMake macOS app, signs it with Developer ID, notarizes it with Apple, and uploads the stapled DMG as a release artifact.
# .github/workflows/macos-release.yml (relevant steps)
# After checkout, install certificates, and provisioning profiles:
# Configure CMake with Xcode generator
cmake -B build -G Xcode \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-DCMAKE_BUILD_TYPE=Release
# Build release configuration
cmake --build build --config Release
# Sign, notarize, staple (using custom script)
./scripts/notarize.sh build/Release/ProductionApp.app
# Create DMG and upload
hdiutil create -volname "My App" \
-srcfolder build/Release/ProductionApp.app \
-ov -format UDZO ProductionApp-$(git describe --tags).dmg
CMake and CocoaPods/SPM
Apple's ecosystem has its own package managers — CocoaPods and Swift Package Manager (SPM). While CMake operates independently, you can integrate dependencies from these ecosystems into CMake-based projects.
# Strategy 1: Use find_package() for system-installed frameworks
# Many CocoaPods eventually install into standard paths
find_library(ALAMOFIRE_LIB Alamofire
PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Pods/Alamofire/lib"
)
# Strategy 2: FetchContent for SPM-compatible packages
include(FetchContent)
# Many C/C++ libraries have both CMake and SPM support
FetchContent_Declare(
swift-argument-parser
GIT_REPOSITORY https://github.com/apple/swift-argument-parser.git
GIT_TAG 1.3.0
)
FetchContent_MakeAvailable(swift-argument-parser)
# Strategy 3: ExternalProject for CocoaPods
include(ExternalProject)
ExternalProject_Add(pods
DOWNLOAD_COMMAND ""
CONFIGURE_COMMAND pod install --project-directory=${CMAKE_SOURCE_DIR}
BUILD_COMMAND ""
INSTALL_COMMAND ""
BUILD_IN_SOURCE TRUE
)
# Practical approach: Pre-built XCFrameworks from CocoaPods/SPM
# Most production projects pre-build dependencies and check in XCFrameworks
# Find pre-built XCFramework
find_library(REALM_LIB Realm
PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64"
)
# Or use a CMake wrapper
add_library(Realm SHARED IMPORTED)
set_target_properties(Realm PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64/Realm.framework/Realm"
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64/Realm.framework/Headers"
)
target_link_libraries(MyApp PRIVATE Realm)
IMPORTED targets. This avoids fighting with incompatible build system assumptions.
Debugging with LLDB
Apple's debugger (LLDB) requires proper debug information format configuration. CMake controls whether debug symbols are embedded in the binary (DWARF) or split into separate symbol files (dSYM bundles), which affects debugging experience, binary size, and crash report symbolication.
cmake_minimum_required(VERSION 3.25)
project(DebuggableApp LANGUAGES CXX)
add_executable(DebuggableApp src/main.cpp)
# Debug information format via Xcode attributes
set_target_properties(DebuggableApp PROPERTIES
# DWARF with dSYM — symbols in separate .dSYM bundle (recommended for release)
XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym"
# Or: DWARF — symbols embedded in binary (faster debug builds)
# XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf"
)
# Per-configuration: embedded for Debug, dSYM for Release
set_target_properties(DebuggableApp PROPERTIES
XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Debug] "dwarf"
XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Release] "dwarf-with-dsym"
)
# Generate dSYM for non-Xcode generators (Ninja/Makefiles)
if(NOT CMAKE_GENERATOR MATCHES "Xcode")
add_custom_command(TARGET DebuggableApp POST_BUILD
COMMAND dsymutil "$<TARGET_FILE:DebuggableApp>"
-o "$<TARGET_FILE:DebuggableApp>.dSYM"
COMMENT "Generating dSYM for DebuggableApp"
)
endif()
# Generate dSYM manually
dsymutil build/Release/DebuggableApp -o build/Release/DebuggableApp.dSYM
# Verify dSYM contains symbols
dwarfdump --uuid build/Release/DebuggableApp
dwarfdump --uuid build/Release/DebuggableApp.dSYM
# Symbolicate a crash log
atos -o build/Release/DebuggableApp.dSYM -l 0x100000000 -arch arm64 0x100003f40
# Upload symbols to crash reporting service
# (Firebase Crashlytics, Sentry, etc.)
# Many services accept the .dSYM bundle directly
# Debug with LLDB from command line
lldb build/Debug/DebuggableApp
# (lldb) breakpoint set --name main
# (lldb) run
# (lldb) bt (backtrace)
# (lldb) frame variable (show local variables)
dsymutil to generate dSYM bundles for crash report symbolication. The Xcode generator handles this automatically when DEBUG_INFORMATION_FORMAT is set to dwarf-with-dsym. Always archive dSYM bundles alongside release builds for future crash analysis.
# Complete debug configuration for production
set_target_properties(DebuggableApp PROPERTIES
# Strip debug symbols from release binary (symbols are in dSYM)
XCODE_ATTRIBUTE_DEPLOYMENT_POSTPROCESSING[variant=Release] "YES"
XCODE_ATTRIBUTE_STRIP_INSTALLED_PRODUCT[variant=Release] "YES"
XCODE_ATTRIBUTE_STRIP_STYLE "non-global"
# Keep symbols for Debug builds
XCODE_ATTRIBUTE_DEPLOYMENT_POSTPROCESSING[variant=Debug] "NO"
XCODE_ATTRIBUTE_GCC_GENERATE_DEBUGGING_SYMBOLS "YES"
# Optimization levels
XCODE_ATTRIBUTE_GCC_OPTIMIZATION_LEVEL[variant=Debug] "0"
XCODE_ATTRIBUTE_GCC_OPTIMIZATION_LEVEL[variant=Release] "s"
)
Conclusion and Next Steps
Apple platform development with CMake requires understanding the unique aspects of Apple's ecosystem — app bundles, frameworks, code signing, and multi-architecture builds. The key takeaways from this article:
- Xcode Generator — produces native
.xcodeprojfiles with full scheme support, asset catalog compilation, and Swift integration - App Bundles —
MACOSX_BUNDLEproperty creates proper.appstructures with Info.plist and resource copying - Frameworks —
FRAMEWORKproperty builds versioned framework bundles with public headers and module maps - Universal Binaries —
CMAKE_OSX_ARCHITECTURESbuilds fat binaries for arm64 + x86_64 - Code Signing —
XCODE_ATTRIBUTE_*properties configure identity, team, entitlements, and hardened runtime - iOS Cross-Compilation —
CMAKE_SYSTEM_NAME=iOSwith proper sysroot and architecture settings - XCFrameworks — the modern distribution format that solves architecture collisions across platforms
- Swift — CMake 3.15+ supports Swift with
enable_language(Swift)and C++ interop via bridging headers - Notarization — mandatory for macOS distribution, automated via
notarytoolandstapler - dSYM — always generate and archive dSYM bundles for release builds to enable crash symbolication
In the next article, we will explore how to use CMake to build and distribute Python extension modules using pybind11 and scikit-build, enabling C++ performance from Python code.