SDL: cmake: add support for creating Apple frameworks with CMake

From 13c294eec38b288570b1ad5a59681f1d7e6cc1f4 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Sat, 21 Jan 2023 01:08:32 +0100
Subject: [PATCH] cmake: add support for creating Apple frameworks with CMake

---
 .github/workflows/main.yml |  25 +++---
 CMakeLists.txt             | 156 +++++++++++++++++++++++++++++--------
 cmake/SDL3Config.cmake.in  |  40 ++++++++--
 3 files changed, 170 insertions(+), 51 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5cc488982f96..fbe0e47bd8b0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -26,7 +26,8 @@ jobs:
         - { name: Windows (ucrt64),         os: windows-latest, shell: 'msys2 {0}', msystem: ucrt64,  msys-env: mingw-w64-ucrt-x86_64 }
         - { name: Ubuntu 20.04,             os: ubuntu-20.04,   shell: sh }
         - { name: Ubuntu 22.04,             os: ubuntu-22.04,   shell: sh }
-        - { name: MacOS,                    os: macos-latest,   shell: sh,    cmake: '-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"' }
+        - { name: MacOS (Framework),  os: macos-latest,   shell: sh,    cmake: '-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DSDL_FRAMEWORK=ON', skip_test_pkgconfig: true }
+        - { name: MacOS (GNU prefix), os: macos-latest,   shell: sh,    cmake: '-DCMAKE_OSX_ARCHITECTURES="x86_64"' }
 
     steps:
     - name: Set up MSYS2
@@ -69,13 +70,13 @@ jobs:
     - name: Configure (CMake)
       run: |
         cmake -S . -B build -G Ninja \
-        -DSDL_TESTS=ON \
-        -DSDL_WERROR=ON \
-        -DSDL_INSTALL_TESTS=ON \
-        -DSDL_VENDOR_INFO="Github Workflow" \
-        -DCMAKE_INSTALL_PREFIX=cmake_prefix \
-        -DCMAKE_BUILD_TYPE=Release \
-        ${{ matrix.platform.cmake }}
+          -DSDL_TESTS=ON \
+          -DSDL_WERROR=ON \
+          -DSDL_INSTALL_TESTS=ON \
+          -DSDL_VENDOR_INFO="Github Workflow" \
+          -DCMAKE_INSTALL_PREFIX=cmake_prefix \
+          -DCMAKE_BUILD_TYPE=Release \
+          ${{ matrix.platform.cmake }}
     - name: Build (CMake)
       run: |
         cmake --build build/ --config Release --verbose --parallel
@@ -92,15 +93,17 @@ jobs:
       run: |
         set -eu
         cmake --install build/ --config Release
-        echo "SDL3_DIR=$(pwd)/cmake_prefix" >> $GITHUB_ENV
         ( cd cmake_prefix; find . ) | LC_ALL=C sort -u
     - name: Verify CMake configuration files
       run: |
         cmake -S cmake/test -B cmake_config_build -G Ninja \
+          -DTEST_SHARED=ON \
+          -DTEST_STATIC=ON \
           -DCMAKE_BUILD_TYPE=Release \
-          -DCMAKE_PREFIX_PATH=${{ env.SDL3_DIR }}
+          -DCMAKE_PREFIX_PATH=$(echo "${{ github.workspace }}/cmake_prefix" | sed  -e 's#\\#/#g')
         cmake --build cmake_config_build --verbose
     - name: Verify sdl3.pc
+      if: ${{ !matrix.platform.skip_test_pkgconfig }}
       run: |
-        export PKG_CONFIG_PATH=${{ env.SDL3_DIR }}/lib/pkgconfig
+        export PKG_CONFIG_PATH=$(echo "${{ github.workspace }}/cmake_prefix/lib/pkgconfig" | sed  -e 's#\\#/#g')
         cmake/test/test_pkgconfig.sh
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 96335ec9d04c..c22bf0288dce 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -443,9 +443,12 @@ option(SDL_SHARED "Build a shared version of the library" ${SDL_SHARED_ENABLED_B
 option(SDL_STATIC "Build a static version of the library" ${SDL_STATIC_ENABLED_BY_DEFAULT})
 option(SDL_TEST   "Build the SDL3_test library" ${SDL_TEST_ENABLED_BY_DEFAULT})
 
+# Apple Frameworks NEED a (shared) SDL3.framework for `#include <SDL3/xxx.h>` to work
+cmake_dependent_option(SDL_FRAMEWORK "Build SDL libraries as Apple Framework" OFF "SDL_SHARED;APPLE" OFF)
+
 dep_option(SDL_STATIC_PIC      "Static version of the library should be built with Position Independent Code" "${CMAKE_POSITION_INDEPENDENT_CODE}" "SDL_STATIC" OFF)
 dep_option(SDL_TESTS           "Build the test directory" OFF SDL_TEST OFF)
-set_option(SDL_INSTALL_TESTS   "Install test-cases" OFF)
+dep_option(SDL_INSTALL_TESTS   "Install test-cases" OFF "NOT SDL_DISABLE_INSTALL;NOT SDL_FRAMEWORK" OFF)
 
 set(HAVE_STATIC_PIC "${SDL_STATIC_PIC}")
 
@@ -2866,11 +2869,18 @@ endif()
 set(SDL_REVISION "SDL-${SDL_REVISION_CENTER}${SDL_REVISION_SUFFIX}")
 
 execute_process(COMMAND "${CMAKE_COMMAND}" -E make_directory "${SDL3_BINARY_DIR}/include/SDL3")
-configure_file("${SDL3_SOURCE_DIR}/include/build_config/SDL_revision.h.cmake"
-  "${SDL3_BINARY_DIR}/include/SDL3/SDL_revision.h")
+configure_file(include/build_config/SDL_revision.h.cmake include/SDL3/SDL_revision.h @ONLY)
+list(APPEND SDL3_INCLUDE_FILES "${SDL3_BINARY_DIR}/include/SDL3/SDL_revision.h")
+
+if(SDL_FRAMEWORK)
+  # With Apple frameworks, headers in the PUBLIC_HEADER property also need to be added as sources
+  list(APPEND SDL3_INCLUDE_FILES ${SDL3_TEST_INCLUDE_FILES})
+  list(APPEND SOURCE_FILES ${SDL3_INCLUDE_FILES})
+endif()
 
-if(CMAKE_STATIC_LIBRARY_PREFIX STREQUAL "" AND CMAKE_STATIC_LIBRARY_SUFFIX STREQUAL ".lib")
-  # Avoid conflict between the dll import library and the static library
+if((CMAKE_STATIC_LIBRARY_PREFIX STREQUAL "" AND CMAKE_STATIC_LIBRARY_SUFFIX STREQUAL ".lib") OR SDL_FRAMEWORK)
+  # - Avoid conflict between the dll import library and the static library
+  # - Create SDL3-static Apple Framework
   set(sdl_static_libname "SDL3-static")
 else()
   set(sdl_static_libname "SDL3")
@@ -3023,7 +3033,10 @@ message(STATUS "")
 message(STATUS " Build Shared Library: ${SDL_SHARED}")
 message(STATUS " Build Static Library: ${SDL_STATIC}")
 if(SDL_STATIC)
-    message(STATUS " Build Static Library with Position Independent Code: ${SDL_STATIC_PIC}")
+  message(STATUS " Build Static Library with Position Independent Code: ${SDL_STATIC_PIC}")
+endif()
+if(APPLE)
+  message(STATUS " Build libraries as Apple Framework: ${SDL_FRAMEWORK}")
 endif()
 message(STATUS "")
 if(UNIX)
@@ -3075,6 +3088,14 @@ endif()
 # Disable precompiled headers on SDL_dynapi.c to avoid applying dynapi overrides
 set_source_files_properties(src/dynapi/SDL_dynapi.c PROPERTIES SKIP_PRECOMPILE_HEADERS 1)
 
+set(SDL_FRAMEWORK_RESOURCES
+  Xcode/SDL/pkg-support/resources/ReadMe.txt
+  LICENSE.txt
+)
+if(SDL_FRAMEWORK)
+  list(APPEND SOURCE_FILES ${SDL_FRAMEWORK_RESOURCES})
+endif()
+
 add_library(SDL3_headers INTERFACE)
 add_library(SDL3::headers ALIAS SDL3_headers)
 set_target_properties(SDL3_headers PROPERTIES
@@ -3123,7 +3144,16 @@ if(SDL_SHARED)
   if(APPLE)
     set_target_properties(SDL3 PROPERTIES
       MACOSX_RPATH TRUE
+      FRAMEWORK "${SDL_FRAMEWORK}"
     )
+    if(SDL_FRAMEWORK)
+      set_target_properties(SDL3 PROPERTIES
+        PUBLIC_HEADER "${SDL3_INCLUDE_FILES}"
+        FRAMEWORK_VERSION "A"
+        MACOSX_FRAMEWORK_IDENTIFIER "org.libsdl.SDL3"
+        RESOURCE "${SDL_FRAMEWORK_RESOURCES}"
+      )
+    endif()
     if(NOT CMAKE_VERSION VERSION_LESS "3.6")
       set_target_properties(SDL3 PROPERTIES
         SOVERSION "${SDL_DYLIB_COMPAT_VERSION}" # SOVERSION corresponds to compatibility version
@@ -3181,6 +3211,18 @@ if(SDL_STATIC)
     OUTPUT_NAME "${sdl_static_libname}"
     POSITION_INDEPENDENT_CODE "${SDL_STATIC_PIC}"
   )
+  if(APPLE)
+    set_target_properties(SDL3-static PROPERTIES
+      FRAMEWORK "${SDL_FRAMEWORK}"
+    )
+    if(SDL_FRAMEWORK)
+      set_target_properties(SDL3-static PROPERTIES
+        FRAMEWORK_VERSION "A"
+        MACOSX_FRAMEWORK_IDENTIFIER "org.libsdl.SDL3-static"
+        RESOURCE "${SDL_FRAMEWORK_RESOURCES}"
+      )
+    endif()
+  endif()
   target_compile_definitions(SDL3-static PRIVATE SDL_STATIC_LIB)
   target_link_libraries(SDL3-static PRIVATE ${SDL_EXTRA_LIBS} ${SDL_EXTRA_LDFLAGS} ${SDL_CMAKE_DEPENDS})
   target_include_directories(SDL3-static
@@ -3215,6 +3257,18 @@ if(SDL_TEST)
   target_link_libraries(SDL3_test PRIVATE $<BUILD_INTERFACE:sdl-global-options>)
   set_target_properties(SDL3_test PROPERTIES
       EXPORT_NAME SDL3_test)
+  if(APPLE)
+    set_target_properties(SDL3_test PROPERTIES
+      FRAMEWORK "${SDL_FRAMEWORK}"
+    )
+    if(SDL_FRAMEWORK)
+      set_target_properties(SDL3_test PROPERTIES
+        FRAMEWORK_VERSION "A"
+        MACOSX_FRAMEWORK_IDENTIFIER "org.libsdl.SDL3_test"
+        RESOURCE "${SDL_FRAMEWORK_RESOURCES}"
+      )
+    endif()
+  endif()
   target_include_directories(SDL3_test
     PRIVATE
       "$<BUILD_INTERFACE:${SDL3_BINARY_DIR}/include-config-$<LOWER_CASE:$<CONFIG>>>"
@@ -3244,9 +3298,18 @@ if(NOT SDL_DISABLE_INSTALL)
   if(WINDOWS AND NOT MINGW)
     set(SDL_INSTALL_CMAKEDIR "${SDL_INSTALL_CMAKEDIR_ROOT}")
     set(LICENSES_PREFIX "licenses/SDL3")
+    set(RESOURCES_PREFIX ".")
+    set(PUBLIC_HEADER_PREFIX "${CMAKE_INSTALL_INCLUDEDIR}/SDL3")
+  elseif(SDL_FRAMEWORK)
+    set(SDL_INSTALL_CMAKEDIR "SDL3.framework/Resources/CMake")
+    set(LICENSES_PREFIX "Resources")
+    set(RESOURCES_PREFIX "Resources")
+    set(PUBLIC_HEADER_PREFIX "Headers")
   else()
     set(SDL_INSTALL_CMAKEDIR "${SDL_INSTALL_CMAKEDIR_ROOT}/SDL3")
     set(LICENSES_PREFIX "${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME}")
+    set(RESOURCES_PREFIX ".")
+    set(PUBLIC_HEADER_PREFIX "${CMAKE_INSTALL_INCLUDEDIR}/SDL3")
   endif()
 
   ##### Installation targets #####
@@ -3257,21 +3320,27 @@ if(NOT SDL_DISABLE_INSTALL)
     install(TARGETS SDL3 EXPORT SDL3Targets
       LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
       ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
-      RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+      RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+      FRAMEWORK DESTINATION "."
+      PUBLIC_HEADER DESTINATION "${PUBLIC_HEADER_PREFIX}"
+      RESOURCE DESTINATION "${RESOURCES_PREFIX}"
+    )
   endif()
 
   if(SDL_STATIC)
     install(TARGETS SDL3-static EXPORT SDL3staticTargets
-      LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
       ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
-      RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+      FRAMEWORK DESTINATION "."
+      RESOURCE DESTINATION "${RESOURCES_PREFIX}"
+    )
   endif()
 
   if(SDL_TEST)
     install(TARGETS SDL3_test EXPORT SDL3testTargets
-      LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
       ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
-      RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
+      FRAMEWORK DESTINATION "."
+      RESOURCE DESTINATION "${RESOURCES_PREFIX}"
+    )
   endif()
 
   ##### sdl3.pc #####
@@ -3309,46 +3378,68 @@ if(NOT SDL_DISABLE_INSTALL)
   # message(STATUS "SDL_PC_STATIC_LIBS: ${SDL_PC_STATIC_LIBS}")
 
   configure_file(cmake/sdl3.pc.in sdl3.pc @ONLY)
-  install(FILES ${SDL3_BINARY_DIR}/sdl3.pc DESTINATION "${SDL_PKGCONFIG_INSTALLDIR}")
+  if(NOT SDL_FRAMEWORK)
+    install(FILES ${SDL3_BINARY_DIR}/sdl3.pc DESTINATION "${SDL_PKGCONFIG_INSTALLDIR}")
+  endif()
 
   ##### CMake Export files #####
 
   include(CMakePackageConfigHelpers)
   configure_package_config_file(cmake/SDL3Config.cmake.in SDL3Config.cmake
-    PATH_VARS CMAKE_INSTALL_PREFIX CMAKE_INSTALL_FULL_BINDIR CMAKE_INSTALL_FULL_INCLUDEDIR CMAKE_INSTALL_FULL_LIBDIR
+    PATH_VARS CMAKE_INSTALL_PREFIX
     INSTALL_DESTINATION "${SDL_INSTALL_CMAKEDIR}"
   )
   write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/SDL3ConfigVersion.cmake"
     COMPATIBILITY AnyNewerVersion
   )
 
+  if(SDL_FRAMEWORK)
+    set(SDL_SDL_INSTALL_CMAKEDIR "SDL3.framework/Resources/CMake")
+
+    # Install SDL3*Config.cmake files in SDL3*.framework/Resources/CMake
+    set(SDL_SDLstatic_INSTALL_CMAKEDIR "SDL3-static.framework/Resources/CMake")
+    set(SDL_SDLstatic_INSTALL_CMAKEFILE "SDL3-staticConfig.cmake")
+
+    set(SDL_SDLtest_INSTALL_CMAKEDIR "SDL3_test.framework/Resources/CMake")
+    set(SDL_SDLtest_INSTALL_CMAKEFILE "SDL3_testConfig.cmake")
+  else()
+    set(SDL_SDL_INSTALL_CMAKEDIR ${SDL_INSTALL_CMAKEDIR})
+
+    # Install SDL3*Targets.cmake files in lib/cmake/SDL3
+    set(SDL_SDLstatic_INSTALL_CMAKEDIR "${SDL_SDL_INSTALL_CMAKEDIR}")
+    set(SDL_SDLstatic_INSTALL_CMAKEFILE "SDL3staticTargets.cmake")
+
+    set(SDL_SDLtest_INSTALL_CMAKEDIR "${SDL_SDL_INSTALL_CMAKEDIR}")
+    set(SDL_SDLtest_INSTALL_CMAKEFILE "SDL3_testTargets.cmake")
+  endif()
+
   install(EXPORT SDL3headersTargets
-    FILE SDL3headersTargets.cmake
+    FILE "SDL3headersTargets.cmake"
     NAMESPACE SDL3::
-    DESTINATION "${SDL_INSTALL_CMAKEDIR}"
+    DESTINATION "${SDL_SDL_INSTALL_CMAKEDIR}"
   )
 
   if(SDL_SHARED)
     install(EXPORT SDL3Targets
-      FILE SDL3Targets.cmake
+      FILE "SDL3Targets.cmake"
       NAMESPACE SDL3::
-      DESTINATION "${SDL_INSTALL_CMAKEDIR}"
+      DESTINATION "${SDL_SDL_INSTALL_CMAKEDIR}"
     )
   endif()
 
   if(SDL_STATIC)
     install(EXPORT SDL3staticTargets
-      FILE SDL3staticTargets.cmake
+      FILE "${SDL_SDLstatic_INSTALL_CMAKEFILE}"
       NAMESPACE SDL3::
-      DESTINATION "${SDL_INSTALL_CMAKEDIR}"
+      DESTINATION "${SDL_SDLstatic_INSTALL_CMAKEDIR}"
     )
   endif()
 
   if(SDL_TEST)
     install(EXPORT SDL3testTargets
-      FILE SDL3testTargets.cmake
+      FILE "${SDL_SDLtest_INSTALL_CMAKEFILE}"
       NAMESPACE SDL3::
-      DESTINATION "${SDL_INSTALL_CMAKEDIR}"
+      DESTINATION "${SDL_SDLtest_INSTALL_CMAKEDIR}"
     )
   endif()
 
@@ -3357,24 +3448,25 @@ if(NOT SDL_DISABLE_INSTALL)
       ${CMAKE_CURRENT_BINARY_DIR}/SDL3Config.cmake
       ${CMAKE_CURRENT_BINARY_DIR}/SDL3ConfigVersion.cmake
       ${SDL3_SOURCE_DIR}/cmake/sdlfind.cmake
-    DESTINATION "${SDL_INSTALL_CMAKEDIR}"
+    DESTINATION "${SDL_SDL_INSTALL_CMAKEDIR}"
     COMPONENT Devel
   )
 
-  install(
-    FILES
-      ${SDL3_INCLUDE_FILES}
-      "${SDL3_BINARY_DIR}/include/SDL3/SDL_revision.h"
-    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/SDL3
-  )
-  if(SDL_TEST)
+  if(NOT SDL_FRAMEWORK)
     install(
-      FILES ${SDL3_TEST_INCLUDE_FILES}
+      FILES
+        ${SDL3_INCLUDE_FILES}
       DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/SDL3
     )
-  endif()
+    if(SDL_TEST)
+      install(
+        FILES ${SDL3_TEST_INCLUDE_FILES}
+        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/SDL3
+      )
+    endif()
 
-  install(FILES "LICENSE.txt" DESTINATION "${LICENSES_PREFIX}")
+    install(FILES "LICENSE.txt" DESTINATION "${LICENSES_PREFIX}")
+  endif()
 endif()
 
 ##### Uninstall target #####
diff --git a/cmake/SDL3Config.cmake.in b/cmake/SDL3Config.cmake.in
index 446d256ca05a..6580674f13e7 100644
--- a/cmake/SDL3Config.cmake.in
+++ b/cmake/SDL3Config.cmake.in
@@ -9,10 +9,15 @@ set_package_properties(SDL3 PROPERTIES
 @PACKAGE_INIT@
 
 set(SDL3_FOUND TRUE)
+set(_sdl3_framework @SDL_FRAMEWORK@)
 
 # Find SDL3::headers
 if(NOT TARGET SDL3::headers)
   include("${CMAKE_CURRENT_LIST_DIR}/SDL3headersTargets.cmake")
+  # Manually add `-F <parent folder of SDL3.framework>` to make sure `#include "SDL3/..."` works.
+  if(_sdl3_framework)
+    set_property(TARGET SDL3::headers APPEND PROPERTY INTERFACE_COMPILE_OPTIONS "SHELL:-F \"@PACKAGE_CMAKE_INSTALL_PREFIX@\"")
+  endif()
 endif()
 set(SDL3_headers_FOUND TRUE)
 
@@ -21,16 +26,35 @@ if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3Targets.cmake")
   include("${CMAKE_CURRENT_LIST_DIR}/SDL3Targets.cmake")
   set(SDL3_SDL3_FOUND TRUE)
 endif()
-if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3staticTargets.cmake")
-  if(ANDROID OR HAIKU)
-    enable_language(CXX)
+
+# Find SDL3::SDL3-static
+if(_sdl3_framework)
+  find_package(SDL3-static CONFIG)
+  if(SDL3-static_FOUND)
+    set(SDL3_SDL3-static_FOUND TRUE)
+  endif()
+else()
+  if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3staticTargets.cmake")
+    if(ANDROID OR HAIKU)
+      enable_language(CXX)
+    endif()
+    include("${CMAKE_CURRENT_LIST_DIR}/SDL3staticTargets.cmake")
+    set(SDL3_SDL3-static_FOUND TRUE)
   endif()
-  include("${CMAKE_CURRENT_LIST_DIR}/SDL3staticTargets.cmake")
-  set(SDL3_SDL3-static_FOUND TRUE)
 endif()
-if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3testTargets.cmake")
-  include("${CMAKE_CURRENT_LIST_DIR}/SDL3testTargets.cmake")
-  set(SDL3_SDL3_test_FOUND TRUE)
+
+# Find SDL3::SDL3_test
+if(_sdl3_framework)
+  find_package(SDL3_test CONFIG)
+  if(SDL3_test_FOUND)
+    enable_language(OBJC)
+    set(SDL3_SDL3_test_FOUND TRUE)
+  endif()
+else()
+  if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/SDL3testTargets.cmake")
+    include("${CMAKE_CURRENT_LIST_DIR}/SDL3testTargets.cmake")
+    set(SDL3_SDL3_test_FOUND TRUE)
+  endif()
 endif()
 
 include("${CMAKE_CURRENT_LIST_DIR}/sdlfind.cmake")