Files
projectm/cmake/TestMacOSFramework.cmake
Mischa Spiegelmock f67dac9481 Fix macOS framework build to properly include headers (#964)
* 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>
2026-02-16 09:00:52 -08:00

192 lines
6.4 KiB
CMake

# TestMacOSFramework.cmake
# CI test functions for macOS framework validation
#
# This module provides functions to validate that macOS frameworks are
# correctly built with proper structure, headers, and linkability.
# Validate the structure of a macOS framework
#
# Usage:
# test_framework_structure(
# FRAMEWORK_PATH <path-to-framework>
# FRAMEWORK_NAME <name>
# )
#
# Returns FATAL_ERROR if validation fails.
#
function(test_framework_structure)
set(options "")
set(oneValueArgs FRAMEWORK_PATH FRAMEWORK_NAME)
set(multiValueArgs "")
cmake_parse_arguments(TF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT TF_FRAMEWORK_PATH)
message(FATAL_ERROR "test_framework_structure: FRAMEWORK_PATH is required")
endif()
if(NOT TF_FRAMEWORK_NAME)
message(FATAL_ERROR "test_framework_structure: FRAMEWORK_NAME is required")
endif()
message(STATUS "Validating framework structure: ${TF_FRAMEWORK_PATH}")
# Check framework directory exists
if(NOT EXISTS "${TF_FRAMEWORK_PATH}")
message(FATAL_ERROR "Framework does not exist: ${TF_FRAMEWORK_PATH}")
endif()
# Check Versions directory
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions")
message(FATAL_ERROR "Framework missing Versions directory")
endif()
# Check Version A directory
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/A")
message(FATAL_ERROR "Framework missing Versions/A directory")
endif()
# Check Current symlink
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/Current")
message(FATAL_ERROR "Framework missing Versions/Current symlink")
endif()
# Check binary
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/A/${TF_FRAMEWORK_NAME}")
message(FATAL_ERROR "Framework missing binary: Versions/A/${TF_FRAMEWORK_NAME}")
endif()
# Check Headers directory
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/A/Headers")
message(FATAL_ERROR "Framework missing Headers directory")
endif()
# Check Resources directory
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/A/Resources")
message(FATAL_ERROR "Framework missing Resources directory")
endif()
# Check Info.plist
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Versions/A/Resources/Info.plist")
message(FATAL_ERROR "Framework missing Info.plist")
endif()
# Check top-level symlinks
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/${TF_FRAMEWORK_NAME}")
message(FATAL_ERROR "Framework missing top-level binary symlink")
endif()
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Headers")
message(FATAL_ERROR "Framework missing top-level Headers symlink")
endif()
if(NOT EXISTS "${TF_FRAMEWORK_PATH}/Resources")
message(FATAL_ERROR "Framework missing top-level Resources symlink")
endif()
message(STATUS "Framework structure validation passed")
endfunction()
# Validate that expected headers exist in a framework
#
# Usage:
# test_framework_headers(
# FRAMEWORK_PATH <path-to-framework>
# FRAMEWORK_NAME <name>
# EXPECTED_HEADERS <list of expected header paths relative to Headers/<name>/>
# )
#
function(test_framework_headers)
set(options "")
set(oneValueArgs FRAMEWORK_PATH FRAMEWORK_NAME)
set(multiValueArgs EXPECTED_HEADERS)
cmake_parse_arguments(TF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT TF_FRAMEWORK_PATH)
message(FATAL_ERROR "test_framework_headers: FRAMEWORK_PATH is required")
endif()
if(NOT TF_FRAMEWORK_NAME)
message(FATAL_ERROR "test_framework_headers: FRAMEWORK_NAME is required")
endif()
message(STATUS "Validating framework headers: ${TF_FRAMEWORK_PATH}")
set(_headers_dir "${TF_FRAMEWORK_PATH}/Versions/A/Headers/${TF_FRAMEWORK_NAME}")
# Check that headers directory exists
if(NOT EXISTS "${_headers_dir}")
message(FATAL_ERROR "Framework headers directory does not exist: ${_headers_dir}")
endif()
# Check each expected header
foreach(_header ${TF_EXPECTED_HEADERS})
if(NOT EXISTS "${_headers_dir}/${_header}")
message(FATAL_ERROR "Framework missing expected header: ${_header}")
endif()
endforeach()
message(STATUS "Framework headers validation passed")
endfunction()
# Test that a framework can be linked against
#
# Usage:
# test_framework_linkability(
# FRAMEWORK_PATH <path-to-framework>
# FRAMEWORK_NAME <name>
# TEST_SOURCE <source code to compile>
# [INCLUDE_SUBDIR <subdirectory under Headers for includes>]
# )
#
function(test_framework_linkability)
set(options "")
set(oneValueArgs FRAMEWORK_PATH FRAMEWORK_NAME TEST_SOURCE INCLUDE_SUBDIR)
set(multiValueArgs "")
cmake_parse_arguments(TF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT TF_FRAMEWORK_PATH)
message(FATAL_ERROR "test_framework_linkability: FRAMEWORK_PATH is required")
endif()
if(NOT TF_FRAMEWORK_NAME)
message(FATAL_ERROR "test_framework_linkability: FRAMEWORK_NAME is required")
endif()
if(NOT TF_TEST_SOURCE)
message(FATAL_ERROR "test_framework_linkability: TEST_SOURCE is required")
endif()
message(STATUS "Testing framework linkability: ${TF_FRAMEWORK_PATH}")
# Get framework parent directory for -F flag
get_filename_component(_framework_parent "${TF_FRAMEWORK_PATH}" DIRECTORY)
# Determine include path
if(TF_INCLUDE_SUBDIR)
set(_include_path "${TF_FRAMEWORK_PATH}/Headers/${TF_INCLUDE_SUBDIR}")
else()
set(_include_path "${TF_FRAMEWORK_PATH}/Headers/${TF_FRAMEWORK_NAME}")
endif()
# Write test source to a temporary file
set(_test_file "${CMAKE_CURRENT_BINARY_DIR}/framework_link_test.c")
file(WRITE "${_test_file}" "${TF_TEST_SOURCE}")
# Try to compile and link
set(_test_output "${CMAKE_CURRENT_BINARY_DIR}/framework_link_test")
execute_process(
COMMAND ${CMAKE_C_COMPILER}
-F "${_framework_parent}"
-I "${_include_path}"
-framework ${TF_FRAMEWORK_NAME}
"${_test_file}"
-o "${_test_output}"
RESULT_VARIABLE _result
OUTPUT_VARIABLE _output
ERROR_VARIABLE _error
)
if(NOT _result EQUAL 0)
message(FATAL_ERROR "Framework linkability test failed:\n${_error}")
endif()
# Clean up test files
file(REMOVE "${_test_file}" "${_test_output}")
message(STATUS "Framework linkability validation passed")
endfunction()