mirror of
https://github.com/projectM-visualizer/projectm.git
synced 2026-03-31 03:33:39 +00:00
* Fix macOS framework build to properly include headers
The previous implementation using CMake's built-in FRAMEWORK property
had two issues:
1. Headers were not copied into the framework at all
2. PUBLIC_HEADER flattens directory structure, breaking C++ interface
This replaces the CMake FRAMEWORK support with a custom MacOSFramework
cmake module that:
- Builds proper framework bundles from scratch
- Preserves header directory hierarchy (Audio/, Renderer/ subdirs)
- Creates correct symlink structure (Versions/A, Current, etc.)
- Generates Info.plist with bundle metadata
Also adds CI test script (scripts/test-macos-framework.sh) that validates:
- Framework directory structure
- Header completeness
- Linkability (compile and link test program)
Fixes the empty framework issue reported after ef00cfc8e.
* Fix review issues in macOS framework build
- Add missing Renderer/TextureTypes.hpp to C++ framework headers
- Skip pkg-config generation for playlist in framework mode
- Use stored framework path property for install instead of TARGET_FILE_DIR
- Show compiler errors on linkability test failure instead of suppressing
- Fix comment about framework output location
* Add framework CI jobs and harden test script
New CI job matrix (build-framework):
- Tests framework builds on both arm64 and x86_64
- Tests with and without C++ interface
- Runs strict validation after build AND after install
- Verifies installed frameworks match build output
Test script improvements:
- Exhaustive header lists (all C API + all C++ headers)
- Exact header count validation (catches stale/unexpected files)
- Strict mode (STRICT=1) where SKIPs become FAILs
- Info.plist CFBundleExecutable validation
- Symlink target verification (Current, Headers, Resources)
- Mach-O dylib binary type check
- dylib install name validation
- Flexible framework search across build tree and install prefix
- Test pass counter in summary
---------
Co-authored-by: Mischa <mish@Kensington.local>
224 lines
8.7 KiB
CMake
224 lines
8.7 KiB
CMake
# MacOSFramework.cmake
|
|
# Build macOS framework bundles from scratch, with proper header hierarchy support.
|
|
#
|
|
# This module provides functions to create macOS framework bundles manually,
|
|
# bypassing CMake's built-in FRAMEWORK support which doesn't preserve header
|
|
# directory structures.
|
|
|
|
# Create a macOS framework bundle from a shared library target.
|
|
#
|
|
# Usage:
|
|
# create_macos_framework(
|
|
# TARGET <target>
|
|
# FRAMEWORK_NAME <name>
|
|
# IDENTIFIER <bundle-identifier>
|
|
# VERSION <version>
|
|
# C_HEADERS <list of C header files>
|
|
# [CXX_HEADERS <list of C++ header files with subdirs>]
|
|
# [HEADER_BASE_DIR <base directory to strip from header paths>]
|
|
# [HEADER_SUBDIR <subdirectory name under Headers, defaults to FRAMEWORK_NAME>]
|
|
# )
|
|
#
|
|
# The framework will be created in CMAKE_CURRENT_BINARY_DIR.
|
|
# C_HEADERS are installed flat under Headers/<header-subdir>/
|
|
# CXX_HEADERS preserve their directory structure relative to HEADER_BASE_DIR.
|
|
#
|
|
function(create_macos_framework)
|
|
set(options "")
|
|
set(oneValueArgs TARGET FRAMEWORK_NAME IDENTIFIER VERSION HEADER_BASE_DIR HEADER_SUBDIR)
|
|
set(multiValueArgs C_HEADERS CXX_HEADERS)
|
|
cmake_parse_arguments(FW "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
|
|
|
if(NOT FW_TARGET)
|
|
message(FATAL_ERROR "create_macos_framework: TARGET is required")
|
|
endif()
|
|
if(NOT FW_FRAMEWORK_NAME)
|
|
message(FATAL_ERROR "create_macos_framework: FRAMEWORK_NAME is required")
|
|
endif()
|
|
if(NOT FW_IDENTIFIER)
|
|
message(FATAL_ERROR "create_macos_framework: IDENTIFIER is required")
|
|
endif()
|
|
if(NOT FW_VERSION)
|
|
message(FATAL_ERROR "create_macos_framework: VERSION is required")
|
|
endif()
|
|
|
|
# Use a concrete output directory (CMAKE_CURRENT_BINARY_DIR) for the framework
|
|
# This avoids issues with generator expressions in OUTPUT paths
|
|
set(_framework_dir "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}.framework")
|
|
set(_versions_dir "${_framework_dir}/Versions")
|
|
set(_version_a_dir "${_versions_dir}/A")
|
|
|
|
# Headers are placed in Headers/<header-subdir>/ to support existing include patterns.
|
|
# By default, header-subdir is the framework name, but can be overridden with HEADER_SUBDIR.
|
|
# Users should add -I <framework>/Headers to their include path.
|
|
if(FW_HEADER_SUBDIR)
|
|
set(_header_subdir "${FW_HEADER_SUBDIR}")
|
|
else()
|
|
set(_header_subdir "${FW_FRAMEWORK_NAME}")
|
|
endif()
|
|
set(_headers_dir "${_version_a_dir}/Headers/${_header_subdir}")
|
|
set(_resources_dir "${_version_a_dir}/Resources")
|
|
|
|
# Marker file to track framework creation
|
|
set(_framework_marker "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}.framework.marker")
|
|
|
|
# Generate Info.plist content
|
|
set(_info_plist_content "<?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>CFBundleDevelopmentRegion</key>
|
|
<string>en</string>
|
|
<key>CFBundleExecutable</key>
|
|
<string>${FW_FRAMEWORK_NAME}</string>
|
|
<key>CFBundleIdentifier</key>
|
|
<string>${FW_IDENTIFIER}</string>
|
|
<key>CFBundleInfoDictionaryVersion</key>
|
|
<string>6.0</string>
|
|
<key>CFBundleName</key>
|
|
<string>${FW_FRAMEWORK_NAME}</string>
|
|
<key>CFBundlePackageType</key>
|
|
<string>FMWK</string>
|
|
<key>CFBundleShortVersionString</key>
|
|
<string>${FW_VERSION}</string>
|
|
<key>CFBundleVersion</key>
|
|
<string>${FW_VERSION}</string>
|
|
</dict>
|
|
</plist>")
|
|
|
|
# Write Info.plist to build directory (will be copied by custom command)
|
|
set(_info_plist_file "${CMAKE_CURRENT_BINARY_DIR}/${FW_FRAMEWORK_NAME}_Info.plist")
|
|
file(WRITE "${_info_plist_file}" "${_info_plist_content}")
|
|
|
|
# Build list of commands to copy headers
|
|
set(_header_copy_commands "")
|
|
set(_header_dependencies "")
|
|
|
|
# Process C headers (flat structure)
|
|
foreach(_header ${FW_C_HEADERS})
|
|
get_filename_component(_header_name "${_header}" NAME)
|
|
list(APPEND _header_copy_commands
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_header_name}"
|
|
)
|
|
list(APPEND _header_dependencies "${_header}")
|
|
endforeach()
|
|
|
|
# Process C++ headers (preserve directory structure)
|
|
if(FW_CXX_HEADERS AND FW_HEADER_BASE_DIR)
|
|
foreach(_header ${FW_CXX_HEADERS})
|
|
# Get relative path from base directory
|
|
file(RELATIVE_PATH _rel_path "${FW_HEADER_BASE_DIR}" "${_header}")
|
|
get_filename_component(_rel_dir "${_rel_path}" DIRECTORY)
|
|
|
|
if(_rel_dir)
|
|
list(APPEND _header_copy_commands
|
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_headers_dir}/${_rel_dir}"
|
|
)
|
|
endif()
|
|
list(APPEND _header_copy_commands
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_rel_path}"
|
|
)
|
|
list(APPEND _header_dependencies "${_header}")
|
|
endforeach()
|
|
elseif(FW_CXX_HEADERS)
|
|
# No base dir specified, install flat
|
|
foreach(_header ${FW_CXX_HEADERS})
|
|
get_filename_component(_header_name "${_header}" NAME)
|
|
list(APPEND _header_copy_commands
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_header}" "${_headers_dir}/${_header_name}"
|
|
)
|
|
list(APPEND _header_dependencies "${_header}")
|
|
endforeach()
|
|
endif()
|
|
|
|
# Create custom command to build the framework
|
|
# Note: We use a marker file as OUTPUT since the actual output path depends on the target
|
|
add_custom_command(
|
|
OUTPUT "${_framework_marker}"
|
|
# Clean up any existing framework directory to ensure clean symlinks
|
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_framework_dir}"
|
|
|
|
# Create directory structure
|
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_version_a_dir}"
|
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_headers_dir}"
|
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_resources_dir}"
|
|
|
|
# Copy the dylib
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${FW_TARGET}>" "${_version_a_dir}/${FW_FRAMEWORK_NAME}"
|
|
|
|
# Copy Info.plist
|
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_info_plist_file}" "${_resources_dir}/Info.plist"
|
|
|
|
# Copy headers
|
|
${_header_copy_commands}
|
|
|
|
# Create symlinks (Current -> A)
|
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "A" "${_versions_dir}/Current"
|
|
|
|
# Create top-level symlinks
|
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/${FW_FRAMEWORK_NAME}" "${_framework_dir}/${FW_FRAMEWORK_NAME}"
|
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/Headers" "${_framework_dir}/Headers"
|
|
COMMAND ${CMAKE_COMMAND} -E create_symlink "Versions/Current/Resources" "${_framework_dir}/Resources"
|
|
|
|
# Create marker file
|
|
COMMAND ${CMAKE_COMMAND} -E touch "${_framework_marker}"
|
|
|
|
DEPENDS ${FW_TARGET} ${_header_dependencies}
|
|
COMMENT "Building ${FW_FRAMEWORK_NAME}.framework"
|
|
VERBATIM
|
|
)
|
|
|
|
# Create a target that depends on the framework being built
|
|
add_custom_target(${FW_TARGET}_framework ALL
|
|
DEPENDS "${_framework_marker}"
|
|
)
|
|
|
|
# Set properties on the framework target for use by install commands
|
|
set_target_properties(${FW_TARGET} PROPERTIES
|
|
MACOS_FRAMEWORK_OUTPUT_DIR "${_framework_dir}"
|
|
MACOS_FRAMEWORK_NAME "${FW_FRAMEWORK_NAME}"
|
|
)
|
|
endfunction()
|
|
|
|
# Install a framework created by create_macos_framework
|
|
#
|
|
# Usage:
|
|
# install_macos_framework(
|
|
# TARGET <target>
|
|
# DESTINATION <install-dir>
|
|
# [COMPONENT <component>]
|
|
# )
|
|
#
|
|
function(install_macos_framework)
|
|
set(options "")
|
|
set(oneValueArgs TARGET DESTINATION COMPONENT)
|
|
set(multiValueArgs "")
|
|
cmake_parse_arguments(FW "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
|
|
|
if(NOT FW_TARGET)
|
|
message(FATAL_ERROR "install_macos_framework: TARGET is required")
|
|
endif()
|
|
if(NOT FW_DESTINATION)
|
|
message(FATAL_ERROR "install_macos_framework: DESTINATION is required")
|
|
endif()
|
|
|
|
get_target_property(_framework_name ${FW_TARGET} MACOS_FRAMEWORK_NAME)
|
|
if(NOT _framework_name)
|
|
message(FATAL_ERROR "install_macos_framework: Target ${FW_TARGET} does not have MACOS_FRAMEWORK_NAME property. Did you call create_macos_framework first?")
|
|
endif()
|
|
|
|
# Install the entire framework directory
|
|
set(_component_arg "")
|
|
if(FW_COMPONENT)
|
|
set(_component_arg COMPONENT ${FW_COMPONENT})
|
|
endif()
|
|
|
|
get_target_property(_framework_dir ${FW_TARGET} MACOS_FRAMEWORK_OUTPUT_DIR)
|
|
install(
|
|
DIRECTORY "${_framework_dir}"
|
|
DESTINATION "${FW_DESTINATION}"
|
|
${_component_arg}
|
|
USE_SOURCE_PERMISSIONS
|
|
)
|
|
endfunction()
|