|
| 1 | +# MacOSFramework.cmake |
| 2 | +# Build macOS framework bundles from scratch, with proper header hierarchy support. |
| 3 | +# |
| 4 | +# This module provides functions to create macOS framework bundles manually, |
| 5 | +# bypassing CMake's built-in FRAMEWORK support which doesn't preserve header |
| 6 | +# directory structures. |
| 7 | + |
| 8 | +# Create a macOS framework bundle from a shared library target. |
| 9 | +# |
| 10 | +# Usage: |
| 11 | +# create_macos_framework( |
| 12 | +# TARGET <target> |
| 13 | +# FRAMEWORK_NAME <name> |
| 14 | +# IDENTIFIER <bundle-identifier> |
| 15 | +# VERSION <version> |
| 16 | +# C_HEADERS <list of C header files> |
| 17 | +# [CXX_HEADERS <list of C++ header files with subdirs>] |
| 18 | +# [HEADER_BASE_DIR <base directory to strip from header paths>] |
| 19 | +# [HEADER_SUBDIR <subdirectory name under Headers, defaults to FRAMEWORK_NAME>] |
| 20 | +# ) |
| 21 | +# |
| 22 | +# The framework will be created in CMAKE_CURRENT_BINARY_DIR. |
| 23 | +# C_HEADERS are installed flat under Headers/<header-subdir>/ |
| 24 | +# CXX_HEADERS preserve their directory structure relative to HEADER_BASE_DIR. |
| 25 | +# |
| 26 | +function(create_macos_framework) |
| 27 | + set(options "") |
| 28 | + set(oneValueArgs TARGET FRAMEWORK_NAME IDENTIFIER VERSION HEADER_BASE_DIR HEADER_SUBDIR) |
| 29 | + set(multiValueArgs C_HEADERS CXX_HEADERS) |
| 30 | + cmake_parse_arguments(FW "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) |
| 31 | + |
| 32 | + if(NOT FW_TARGET) |
| 33 | + message(FATAL_ERROR "create_macos_framework: TARGET is required") |
| 34 | + endif() |
| 35 | + if(NOT FW_FRAMEWORK_NAME) |
| 36 | + message(FATAL_ERROR "create_macos_framework: FRAMEWORK_NAME is required") |
| 37 | + endif() |
| 38 | + if(NOT FW_IDENTIFIER) |
| 39 | + message(FATAL_ERROR "create_macos_framework: IDENTIFIER is required") |
| 40 | + endif() |
| 41 | + if(NOT FW_VERSION) |
| 42 | + message(FATAL_ERROR "create_macos_framework: VERSION is required") |
| 43 | + endif() |
| 44 | + |
| 45 | + # Use a concrete output directory (CMAKE_CURRENT_BINARY_DIR) for the framework |
| 46 | + # This avoids issues with generator expressions in OUTPUT paths |
| 47 | + set(_framework_dir "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}.framework") |
| 48 | + set(_versions_dir "${_framework_dir}/Versions") |
| 49 | + set(_version_a_dir "${_versions_dir}/A") |
| 50 | + |
| 51 | + # Headers are placed in Headers/<header-subdir>/ to support existing include patterns. |
| 52 | + # By default, header-subdir is the framework name, but can be overridden with HEADER_SUBDIR. |
| 53 | + # Users should add -I <framework>/Headers to their include path. |
| 54 | + if(FW_HEADER_SUBDIR) |
| 55 | + set(_header_subdir "${FW_HEADER_SUBDIR}") |
| 56 | + else() |
| 57 | + set(_header_subdir "${FW_FRAMEWORK_NAME}") |
| 58 | + endif() |
| 59 | + set(_headers_dir "${_version_a_dir}/Headers/${_header_subdir}") |
| 60 | + set(_resources_dir "${_version_a_dir}/Resources") |
| 61 | + |
| 62 | + # Marker file to track framework creation |
| 63 | + set(_framework_marker "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}.framework.marker") |
| 64 | + |
| 65 | + # Generate Info.plist content |
| 66 | + set(_info_plist_content "<?xml version=\"1.0\" encoding=\"UTF-8\"?> |
| 67 | +<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> |
| 68 | +<plist version=\"1.0\"> |
| 69 | +<dict> |
| 70 | + <key>CFBundleDevelopmentRegion</key> |
| 71 | + <string>en</string> |
| 72 | + <key>CFBundleExecutable</key> |
| 73 | + <string>${FW_FRAMEWORK_NAME}</string> |
| 74 | + <key>CFBundleIdentifier</key> |
| 75 | + <string>${FW_IDENTIFIER}</string> |
| 76 | + <key>CFBundleInfoDictionaryVersion</key> |
| 77 | + <string>6.0</string> |
| 78 | + <key>CFBundleName</key> |
| 79 | + <string>${FW_FRAMEWORK_NAME}</string> |
| 80 | + <key>CFBundlePackageType</key> |
| 81 | + <string>FMWK</string> |
| 82 | + <key>CFBundleShortVersionString</key> |
| 83 | + <string>${FW_VERSION}</string> |
| 84 | + <key>CFBundleVersion</key> |
| 85 | + <string>${FW_VERSION}</string> |
| 86 | +</dict> |
| 87 | +</plist>") |
| 88 | + |
| 89 | + # Write Info.plist to build directory (will be copied by custom command) |
| 90 | + set(_info_plist_file "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}_Info.plist") |
| 91 | + file(WRITE "${_info_plist_file}" "${_info_plist_content}") |
| 92 | + |
| 93 | + # Build list of commands to copy headers |
| 94 | + set(_header_copy_commands "") |
| 95 | + set(_header_dependencies "") |
| 96 | + |
| 97 | + # Process C headers (flat structure) |
| 98 | + foreach(_header ${FW_C_HEADERS}) |
| 99 | + get_filename_component(_header_name "${_header}" NAME) |
| 100 | + list(APPEND _header_copy_commands |
| 101 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_header_name}" |
| 102 | + ) |
| 103 | + list(APPEND _header_dependencies "${_header}") |
| 104 | + endforeach() |
| 105 | + |
| 106 | + # Process C++ headers (preserve directory structure) |
| 107 | + if(FW_CXX_HEADERS AND FW_HEADER_BASE_DIR) |
| 108 | + foreach(_header ${FW_CXX_HEADERS}) |
| 109 | + # Get relative path from base directory |
| 110 | + file(RELATIVE_PATH _rel_path "${FW_HEADER_BASE_DIR}" "${_header}") |
| 111 | + get_filename_component(_rel_dir "${_rel_path}" DIRECTORY) |
| 112 | + |
| 113 | + if(_rel_dir) |
| 114 | + list(APPEND _header_copy_commands |
| 115 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${_headers_dir}/${_rel_dir}" |
| 116 | + ) |
| 117 | + endif() |
| 118 | + list(APPEND _header_copy_commands |
| 119 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_rel_path}" |
| 120 | + ) |
| 121 | + list(APPEND _header_dependencies "${_header}") |
| 122 | + endforeach() |
| 123 | + elseif(FW_CXX_HEADERS) |
| 124 | + # No base dir specified, install flat |
| 125 | + foreach(_header ${FW_CXX_HEADERS}) |
| 126 | + get_filename_component(_header_name "${_header}" NAME) |
| 127 | + list(APPEND _header_copy_commands |
| 128 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_header_name}" |
| 129 | + ) |
| 130 | + list(APPEND _header_dependencies "${_header}") |
| 131 | + endforeach() |
| 132 | + endif() |
| 133 | + |
| 134 | + # Create custom command to build the framework |
| 135 | + # Note: We use a marker file as OUTPUT since the actual output path depends on the target |
| 136 | + add_custom_command( |
| 137 | + OUTPUT "${_framework_marker}" |
| 138 | + # Clean up any existing framework directory to ensure clean symlinks |
| 139 | + COMMAND ${CMAKE_COMMAND} -E rm -rf "${_framework_dir}" |
| 140 | + |
| 141 | + # Create directory structure |
| 142 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${_version_a_dir}" |
| 143 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${_headers_dir}" |
| 144 | + COMMAND ${CMAKE_COMMAND} -E make_directory "${_resources_dir}" |
| 145 | + |
| 146 | + # Copy the dylib |
| 147 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${FW_TARGET}>" "${_version_a_dir}/${FW_FRAMEWORK_NAME}" |
| 148 | + |
| 149 | + # Copy Info.plist |
| 150 | + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_info_plist_file}" "${_resources_dir}/Info.plist" |
| 151 | + |
| 152 | + # Copy headers |
| 153 | + ${_header_copy_commands} |
| 154 | + |
| 155 | + # Create symlinks (Current -> A) |
| 156 | + COMMAND ${CMAKE_COMMAND} -E create_symlink "A" "${_versions_dir}/Current" |
| 157 | + |
| 158 | + # Create top-level symlinks |
| 159 | + COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/${FW_FRAMEWORK_NAME}" "${_framework_dir}/${FW_FRAMEWORK_NAME}" |
| 160 | + COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/Headers" "${_framework_dir}/Headers" |
| 161 | + COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/Resources" "${_framework_dir}/Resources" |
| 162 | + |
| 163 | + # Create marker file |
| 164 | + COMMAND ${CMAKE_COMMAND} -E touch "${_framework_marker}" |
| 165 | + |
| 166 | + DEPENDS ${FW_TARGET} ${_header_dependencies} |
| 167 | + COMMENT "Building ${FW_FRAMEWORK_NAME}.framework" |
| 168 | + VERBATIM |
| 169 | + ) |
| 170 | + |
| 171 | + # Create a target that depends on the framework being built |
| 172 | + add_custom_target(${FW_TARGET}_framework ALL |
| 173 | + DEPENDS "${_framework_marker}" |
| 174 | + ) |
| 175 | + |
| 176 | + # Set properties on the framework target for use by install commands |
| 177 | + set_target_properties(${FW_TARGET} PROPERTIES |
| 178 | + MACOS_FRAMEWORK_OUTPUT_DIR "${_framework_dir}" |
| 179 | + MACOS_FRAMEWORK_NAME "${FW_FRAMEWORK_NAME}" |
| 180 | + ) |
| 181 | +endfunction() |
| 182 | + |
| 183 | +# Install a framework created by create_macos_framework |
| 184 | +# |
| 185 | +# Usage: |
| 186 | +# install_macos_framework( |
| 187 | +# TARGET <target> |
| 188 | +# DESTINATION <install-dir> |
| 189 | +# [COMPONENT <component>] |
| 190 | +# ) |
| 191 | +# |
| 192 | +function(install_macos_framework) |
| 193 | + set(options "") |
| 194 | + set(oneValueArgs TARGET DESTINATION COMPONENT) |
| 195 | + set(multiValueArgs "") |
| 196 | + cmake_parse_arguments(FW "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) |
| 197 | + |
| 198 | + if(NOT FW_TARGET) |
| 199 | + message(FATAL_ERROR "install_macos_framework: TARGET is required") |
| 200 | + endif() |
| 201 | + if(NOT FW_DESTINATION) |
| 202 | + message(FATAL_ERROR "install_macos_framework: DESTINATION is required") |
| 203 | + endif() |
| 204 | + |
| 205 | + get_target_property(_framework_name ${FW_TARGET} MACOS_FRAMEWORK_NAME) |
| 206 | + if(NOT _framework_name) |
| 207 | + message(FATAL_ERROR "install_macos_framework: Target ${FW_TARGET} does not have MACOS_FRAMEWORK_NAME property. Did you call create_macos_framework first?") |
| 208 | + endif() |
| 209 | + |
| 210 | + # Install the entire framework directory |
| 211 | + set(_component_arg "") |
| 212 | + if(FW_COMPONENT) |
| 213 | + set(_component_arg COMPONENT ${FW_COMPONENT}) |
| 214 | + endif() |
| 215 | + |
| 216 | + get_target_property(_framework_dir ${FW_TARGET} MACOS_FRAMEWORK_OUTPUT_DIR) |
| 217 | + install( |
| 218 | + DIRECTORY "${_framework_dir}" |
| 219 | + DESTINATION "${FW_DESTINATION}" |
| 220 | + ${_component_arg} |
| 221 | + USE_SOURCE_PERMISSIONS |
| 222 | + ) |
| 223 | +endfunction() |
0 commit comments