AppleClang vs Upstream Clang
macOS ships with AppleClang — Apple's fork of LLVM/Clang that uses its own versioning scheme and has platform-specific differences. CMake reports it as CMAKE_CXX_COMPILER_ID = "AppleClang", distinct from upstream "Clang". This distinction matters for feature detection and flag compatibility.
cmake_minimum_required(VERSION 3.21)
project(MacOSDetection LANGUAGES CXX)
# AppleClang has a separate compiler ID
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
message(STATUS "AppleClang ${CMAKE_CXX_COMPILER_VERSION}")
# AppleClang 15.x ~ LLVM 16, AppleClang 16.x ~ LLVM 17
# Version mapping is not 1:1 with upstream!
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "Upstream Clang ${CMAKE_CXX_COMPILER_VERSION}")
endif()
add_library(mylib src/core.cpp)
# Use generator expression to handle both
target_compile_options(mylib PRIVATE
$<$<CXX_COMPILER_ID:AppleClang,Clang>:
-Wall -Wextra -Wpedantic
-Wno-unknown-warning-option
>
)
# Feature that AppleClang may lack vs upstream
# (e.g., some sanitizer features, modules support)
if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND
CMAKE_CXX_COMPILER_VERSION VERSION_LESS "15.0")
message(WARNING "AppleClang < 15 has limited C++20 modules support")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") to catch AppleClang — they are distinct IDs! Use $<CXX_COMPILER_ID:AppleClang,Clang> generator expressions or explicit if() checks for both when you want the same flags for both compilers.
Homebrew Path Configuration
Homebrew installs libraries outside the default system paths, especially on Apple Silicon where the prefix is /opt/homebrew instead of /usr/local. CMake needs help finding these packages.
cmake_minimum_required(VERSION 3.21)
project(HomebrewDemo LANGUAGES CXX)
# Detect Homebrew prefix (works on both Intel and Apple Silicon)
execute_process(
COMMAND brew --prefix
OUTPUT_VARIABLE HOMEBREW_PREFIX
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(HOMEBREW_PREFIX)
message(STATUS "Homebrew prefix: ${HOMEBREW_PREFIX}")
# /opt/homebrew on Apple Silicon, /usr/local on Intel
# Add Homebrew paths for find_package and find_library
list(APPEND CMAKE_PREFIX_PATH "${HOMEBREW_PREFIX}")
# For specific packages (e.g., OpenSSL which is keg-only)
set(OPENSSL_ROOT_DIR "${HOMEBREW_PREFIX}/opt/openssl@3")
# pkg-config path for Homebrew packages
set(ENV{PKG_CONFIG_PATH}
"${HOMEBREW_PREFIX}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}")
endif()
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE OpenSSL::SSL ZLIB::ZLIB)
# Common pattern: pass Homebrew paths at configure time
cmake -G Ninja \
-DCMAKE_PREFIX_PATH="$(brew --prefix)" \
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
-S . -B build
CMAKE_OSX_DEPLOYMENT_TARGET
The deployment target defines the minimum macOS version your binary supports. It affects which system APIs are available and determines backward compatibility. Set this before any project() call or targets for it to take full effect.
# Set BEFORE project() for maximum effect
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum macOS version")
cmake_minimum_required(VERSION 3.21)
project(DeploymentDemo LANGUAGES CXX)
# Verify the setting propagated
message(STATUS "Deployment target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
add_executable(app src/main.cpp)
# Conditionally use newer APIs with availability checks
target_compile_definitions(app PRIVATE
MACOS_MIN_VERSION=130000 # 13.0.0 encoded
)
# Override at configure time
cmake -G Ninja \
-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 \
-DCMAKE_BUILD_TYPE=Release \
-S . -B build
@rpath / @loader_path / @executable_path
macOS uses install names and @rpath tokens (instead of Linux's ELF RPATH) to locate dynamic libraries at runtime. Understanding the three special tokens is essential for creating relocatable applications.
cmake_minimum_required(VERSION 3.21)
project(RpathMacOS LANGUAGES CXX)
add_library(mylib SHARED src/lib.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)
# --- Install name configuration ---
# @rpath/libmylib.dylib — library says "find me via rpath"
set_target_properties(mylib PROPERTIES
INSTALL_NAME_DIR "@rpath"
BUILD_WITH_INSTALL_RPATH TRUE
)
# --- Application embeds rpath search paths ---
set_target_properties(app PROPERTIES
INSTALL_RPATH "@executable_path/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
# For bundle/framework layouts:
# @executable_path/../Frameworks — for .app bundles
# @loader_path/../Frameworks — relative to the binary loading it
install(TARGETS mylib LIBRARY DESTINATION lib)
install(TARGETS app RUNTIME DESTINATION bin)
@executable_path = directory of the main executable. @loader_path = directory of the binary doing the loading (useful for plugins). @rpath = searched from the rpath list embedded in the loading binary. Use @rpath for libraries and set rpath in executables for maximum flexibility.
Universal Binaries
Universal binaries (fat binaries) contain code for multiple architectures — typically arm64 (Apple Silicon) and x86_64 (Intel). CMake 3.19+ supports this natively via CMAKE_OSX_ARCHITECTURES.
cmake_minimum_required(VERSION 3.21)
project(UniversalDemo LANGUAGES CXX)
# Build for both architectures
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Target architectures")
add_library(mylib SHARED src/core.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)
# Architecture-conditional code
target_compile_definitions(app PRIVATE
$<$<STREQUAL:${CMAKE_OSX_ARCHITECTURES},arm64>:TARGET_ARM64>
)
# Build universal binary
cmake -G Ninja \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-DCMAKE_BUILD_TYPE=Release \
-S . -B build-universal
cmake --build build-universal
# Verify the result
lipo -info build-universal/app
# Output: Architectures in the fat file: app are: x86_64 arm64
# Extract single architecture (for debugging)
lipo build-universal/app -thin arm64 -output app-arm64
/opt/homebrew. If you're building a universal binary and link against Homebrew libraries, the link will fail for the x86_64 slice. Solutions: use vcpkg with universal triplets, build dependencies from source for both architectures, or use lipo to merge separately-built binaries.
Framework Search Paths
macOS Frameworks are bundles containing headers, libraries, and resources. CMake can find and link against both system frameworks and third-party frameworks distributed outside the SDK.
cmake_minimum_required(VERSION 3.21)
project(FrameworkDemo LANGUAGES CXX)
# System frameworks are found automatically
find_library(COCOA_FRAMEWORK Cocoa)
find_library(IOKIT_FRAMEWORK IOKit)
find_library(METAL_FRAMEWORK Metal)
find_library(QUARTZCORE_FRAMEWORK QuartzCore)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE
${COCOA_FRAMEWORK}
${IOKIT_FRAMEWORK}
${METAL_FRAMEWORK}
${QUARTZCORE_FRAMEWORK}
)
# Alternative: link framework by name (CMake resolves -framework)
target_link_libraries(app PRIVATE
"-framework CoreFoundation"
"-framework Security"
)
# Custom framework search paths
set(CMAKE_FRAMEWORK_PATH
"/Library/Frameworks"
"$ENV{HOME}/Library/Frameworks"
"${CMAKE_SOURCE_DIR}/frameworks"
)
# Create your own framework
add_library(myframework SHARED src/framework.cpp)
set_target_properties(myframework PROPERTIES
FRAMEWORK TRUE
FRAMEWORK_VERSION A
MACOSX_FRAMEWORK_IDENTIFIER "com.example.myframework"
MACOSX_FRAMEWORK_BUNDLE_VERSION "${PROJECT_VERSION}"
MACOSX_FRAMEWORK_SHORT_VERSION_STRING "${PROJECT_VERSION}"
PUBLIC_HEADER "include/myframework/api.h;include/myframework/types.h"
)
macOS SDK Selection
The macOS SDK determines which APIs are available at compile time. You typically want the latest SDK combined with a lower deployment target to get new API availability attributes while supporting older macOS versions.
# List available SDKs
xcodebuild -showsdks
# Find SDK path
xcrun --show-sdk-path
# /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk
# Configure with specific SDK
cmake -G Ninja \
-DCMAKE_OSX_SYSROOT=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.5.sdk \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-S . -B build
cmake_minimum_required(VERSION 3.21)
project(SdkDemo LANGUAGES CXX)
# CMake detects SDK automatically, but you can override
# set(CMAKE_OSX_SYSROOT "/path/to/SDK")
# Report SDK in use
message(STATUS "macOS SDK: ${CMAKE_OSX_SYSROOT}")
# Use availability attributes for newer APIs
add_executable(app src/main.cpp)
target_compile_options(app PRIVATE
-Wunguarded-availability # Warn on using newer APIs without checks
)
Notarization Preparation
Apple requires notarization for distributed macOS software. This involves code signing with a hardened runtime. CMake can configure the necessary settings for a notarization-ready build.
cmake_minimum_required(VERSION 3.21)
project(NotarizationDemo LANGUAGES CXX)
add_executable(app src/main.cpp)
# Enable hardened runtime (required for notarization)
set_target_properties(app PROPERTIES
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application"
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "YOUR_TEAM_ID"
)
# For Ninja/Makefiles builds, add entitlements manually
if(NOT CMAKE_GENERATOR MATCHES "Xcode")
# Sign after build with hardened runtime
add_custom_command(TARGET app POST_BUILD
COMMAND codesign --force --sign "Developer ID Application: Your Name (TEAM_ID)"
--options runtime
--entitlements "${CMAKE_SOURCE_DIR}/resources/app.entitlements"
"$<TARGET_FILE:app>"
COMMENT "Code signing with hardened runtime"
)
endif()
# Complete notarization workflow
# 1. Build
cmake --build build --config Release
# 2. Create ZIP for notarization
ditto -c -k --keepParent build/app.app app.zip
# 3. Submit for notarization
xcrun notarytool submit app.zip \
--apple-id "dev@example.com" \
--team-id "TEAM_ID" \
--password "@keychain:AC_PASSWORD" \
--wait
# 4. Staple the ticket
xcrun stapler staple build/app.app
System Integrity Protection
System Integrity Protection (SIP) restricts access to system directories and strips DYLD_LIBRARY_PATH from child processes. This affects development workflows and requires careful RPATH configuration.
cmake_minimum_required(VERSION 3.21)
project(SipDemo LANGUAGES CXX)
add_library(mylib SHARED src/lib.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)
# SIP strips DYLD_* variables — don't rely on them!
# Instead, use proper @rpath configuration:
set_target_properties(app PROPERTIES
# Build tree: find libs in their build location
BUILD_RPATH_USE_ORIGIN ON
# Install tree: find libs relative to executable
INSTALL_RPATH "@executable_path/../lib"
)
# For development/testing without install:
# CMake automatically sets build RPATH so tests work
# This is stripped on install (replaced by INSTALL_RPATH)
set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE)
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
macOS Application Bundle with CMake
A complete .app bundle layout that handles frameworks, resources, and code signing for distribution:
cmake_minimum_required(VERSION 3.21)
project(BundleApp VERSION 1.0.0 LANGUAGES CXX)
add_executable(MyApp MACOSX_BUNDLE
src/main.cpp
src/app.cpp
resources/icon.icns
)
set_target_properties(MyApp PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "My Application"
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.myapp"
MACOSX_BUNDLE_ICON_FILE "icon.icns"
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/resources/Info.plist.in"
INSTALL_RPATH "@executable_path/../Frameworks"
)
# Ensure icon ends up in Resources/
set_source_files_properties(resources/icon.icns PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
# Copy frameworks into bundle
add_custom_command(TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_BUNDLE_DIR:MyApp>/Contents/Frameworks"
)