SDL: cmake: allow hidapi to use libusb on Windows

From 7341d5f3616e1e5772d409bb06aae132009afb55 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Wed, 29 Nov 2023 22:08:09 +0100
Subject: [PATCH] cmake: allow hidapi to use libusb on Windows

When using SDL_HIDAPI_LIBUSB_SHARED=ON, extract the dll file name
from the import library.
---
 CMakeLists.txt        |   3 +-
 cmake/macros.cmake    | 193 ++++++++++++++++++++++++++++++++++++++++++
 cmake/sdlchecks.cmake |   6 +-
 3 files changed, 197 insertions(+), 5 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 282a2591a5f1e..7002af4867ca9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -133,8 +133,7 @@ endif()
 # The hidraw support doesn't catch Xbox, PS4 and Nintendo controllers,
 #  so we'll just use libusb when it's available. libusb does not support iOS,
 #  so we default to yes on iOS.
-#  TODO: Windows can support libusb, the hid.c file just depends on Unix APIs
-if((WINDOWS AND NOT WINDOWS_STORE) OR IOS OR TVOS OR VISIONOS OR ANDROID)
+if(IOS OR TVOS OR VISIONOS OR ANDROID)
   set(SDL_HIDAPI_LIBUSB_AVAILABLE FALSE)
 else()
   set(SDL_HIDAPI_LIBUSB_AVAILABLE TRUE)
diff --git a/cmake/macros.cmake b/cmake/macros.cmake
index c04c760a7bec5..656f4e0907182 100644
--- a/cmake/macros.cmake
+++ b/cmake/macros.cmake
@@ -100,6 +100,199 @@ function(SDL_detect_linker)
   endif()
 endfunction()
 
+function(read_absolute_symlink DEST PATH)
+  file(READ_SYMLINK "${PATH}" p)
+  if(NOT IS_ABSOLUTE "${p}")
+    get_filename_component(pdir "${PATH}" DIRECTORY)
+    set(p "${pdir}/${p}")
+  endif()
+  get_filename_component(p "${p}" ABSOLUTE)
+  set("${DEST}" "${p}" PARENT_SCOPE)
+endfunction()
+
+function(win32_implib_identify_dll DEST IMPLIB)
+  cmake_parse_arguments(ARGS "NOTFATAL" "" "" ${ARGN})
+  if(CMAKE_DLLTOOL)
+    execute_process(
+            COMMAND "${CMAKE_DLLTOOL}" --identify "${IMPLIB}"
+            RESULT_VARIABLE retcode
+            OUTPUT_VARIABLE stdout
+            ERROR_VARIABLE stderr)
+    if(NOT retcode EQUAL 0)
+      if(NOT ARGS_NOTFATAL)
+        message(FATAL_ERROR "${CMAKE_DLLTOOL} failed.")
+      else()
+        set("${DEST}" "${DEST}-NOTFOUND" PARENT_SCOPE)
+        return()
+      endif()
+    endif()
+    string(STRIP "${stdout}" result)
+    set(${DEST} "${result}" PARENT_SCOPE)
+  elseif(MSVC)
+    get_filename_component(CMAKE_C_COMPILER_DIRECTORY "${CMAKE_C_COMPILER}" DIRECTORY CACHE)
+    find_program(CMAKE_DUMPBIN NAMES dumpbin PATHS "${CMAKE_C_COMPILER_DIRECTORY}")
+    if(CMAKE_DUMPBIN)
+      execute_process(
+              COMMAND "${CMAKE_DUMPBIN}" "-headers" "${IMPLIB}"
+              RESULT_VARIABLE retcode
+              OUTPUT_VARIABLE stdout
+              ERROR_VARIABLE stderr)
+      if(NOT retcode EQUAL 0)
+        if(NOT ARGS_NOTFATAL)
+          message(FATAL_ERROR "dumpbin failed.")
+        else()
+          set(${DEST} "${DEST}-NOTFOUND" PARENT_SCOPE)
+          return()
+        endif()
+      endif()
+      string(REGEX MATCH "DLL name[ ]+:[ ]+([^\n]+)\n" match "${stdout}")
+      if(NOT match)
+        if(NOT ARGS_NOTFATAL)
+          message(FATAL_ERROR "dumpbin did not find any associated dll for ${IMPLIB}.")
+        else()
+          set(${DEST} "${DEST}-NOTFOUND" PARENT_SCOPE)
+          return()
+        endif()
+      endif()
+      set(result "${CMAKE_MATCH_1}")
+      set(${DEST} "${result}" PARENT_SCOPE)
+    else()
+      message(FATAL_ERROR "Cannot find dumpbin, please set CMAKE_DUMPBIN cmake variable")
+    endif()
+  else()
+    if(NOT ARGS_NOTFATAL)
+      message(FATAL_ERROR "Don't know how to identify dll from import library. Set CMAKE_DLLTOOL (for mingw) or CMAKE_DUMPBIN (for MSVC)")
+    else()
+      set(${DEST} "${DEST}-NOTFOUND")
+    endif()
+  endif()
+endfunction()
+
+function(get_actual_target)
+  set(dst "${ARGV0}")
+  set(target "${${dst}}")
+  set(input "${target}")
+  get_target_property(alias "${target}" ALIASED_TARGET)
+  while(alias)
+    set(target "${alias}")
+    get_target_property(alias "${target}" ALIASED_TARGET)
+  endwhile()
+  message(DEBUG "get_actual_target(\"${input}\") -> \"${target}\"")
+  set("${dst}" "${target}" PARENT_SCOPE)
+endfunction()
+
+function(target_get_dynamic_library DEST TARGET)
+  set(result)
+  get_actual_target(TARGET)
+  if(WIN32)
+    # Use the target dll of the import library
+    set(props_to_check IMPORTED_IMPLIB)
+    if(CMAKE_BUILD_TYPE)
+      list(APPEND props_to_check IMPORTED_IMPLIB_${CMAKE_BUILD_TYPE})
+    endif()
+    list(APPEND props_to_check IMPORTED_LOCATION)
+    if(CMAKE_BUILD_TYPE)
+      list(APPEND props_to_check IMPORTED_LOCATION_${CMAKE_BUILD_TYPE})
+    endif()
+    foreach (config_type ${CMAKE_CONFIGURATION_TYPES} RELEASE DEBUG RELWITHDEBINFO MINSIZEREL)
+      list(APPEND props_to_check IMPORTED_IMPLIB_${config_type})
+      list(APPEND props_to_check IMPORTED_LOCATION_${config_type})
+    endforeach()
+
+    foreach(prop_to_check ${props_to_check})
+      if(NOT result)
+        get_target_property(propvalue "${TARGET}" ${prop_to_check})
+        if(propvalue AND EXISTS "${propvalue}")
+          win32_implib_identify_dll(result "${propvalue}" NOTFATAL)
+        endif()
+      endif()
+    endforeach()
+  else()
+    # 1. find the target library a file might be symbolic linking to
+    # 2. find all other files in the same folder that symolic link to it
+    # 3. sort all these files, and select the 1st item on Linux, and last on Macos
+    set(location_properties IMPORTED_LOCATION)
+    if(CMAKE_BUILD_TYPE)
+      list(APPEND location_properties IMPORTED_LOCATION_${CMAKE_BUILD_TYPE})
+    endif()
+    foreach (config_type ${CMAKE_CONFIGURATION_TYPES} RELEASE DEBUG RELWITHDEBINFO MINSIZEREL)
+      list(APPEND location_properties IMPORTED_LOCATION_${config_type})
+    endforeach()
+    if(APPLE)
+      set(valid_shared_library_regex "\\.[0-9]+\\.dylib$")
+    else()
+      set(valid_shared_library_regex "\\.so\\.([0-9.]+)?[0-9]")
+    endif()
+    foreach(location_property ${location_properties})
+      if(NOT result)
+        get_target_property(library_path "${TARGET}" ${location_property})
+        message(DEBUG "get_target_property(${TARGET} ${location_propert}) -> ${library_path}")
+        if(EXISTS "${library_path}")
+          get_filename_component(library_path "${library_path}" ABSOLUTE)
+          while (IS_SYMLINK "${library_path}")
+            read_absolute_symlink(library_path "${library_path}")
+          endwhile()
+          message(DEBUG "${TARGET} -> ${library_path}")
+          get_filename_component(libdir "${library_path}" DIRECTORY)
+          file(GLOB subfiles "${libdir}/*")
+          set(similar_files "${library_path}")
+          foreach(subfile ${subfiles})
+            if(IS_SYMLINK "${subfile}")
+              read_absolute_symlink(subfile_target "${subfile}")
+              while(IS_SYMLINK "${subfile_target}")
+                read_absolute_symlink(subfile_target "${subfile_target}")
+              endwhile()
+              get_filename_component(subfile_target "${subfile_target}" ABSOLUTE)
+              if(subfile_target STREQUAL library_path AND subfile MATCHES "${valid_shared_library_regex}")
+                list(APPEND similar_files "${subfile}")
+              endif()
+            endif()
+          endforeach()
+          list(SORT similar_files)
+          message(DEBUG "files that are similar to \"${library_path}\"=${similar_files}")
+          if(APPLE)
+            list(REVERSE similar_files)
+          endif()
+          list(GET similar_files 0 item)
+          get_filename_component(result "${item}" NAME)
+        endif()
+      endif()
+    endforeach()
+  endif()
+  if(result)
+    string(TOLOWER "${result}" result_lower)
+    if(WIN32 OR OS2)
+      if(NOT result_lower MATCHES ".*dll")
+        message(FATAL_ERROR "\"${result}\" is not a .dll library")
+      endif()
+    elseif(APPLE)
+      if(NOT result_lower MATCHES ".*dylib.*")
+        message(FATAL_ERROR "\"${result}\" is not a .dylib shared library")
+      endif()
+    else()
+      if(NOT result_lower MATCHES ".*so.*")
+        message(FATAL_ERROR "\"${result}\" is not a .so shared library")
+      endif()
+    endif()
+  else()
+    get_target_property(target_type ${TARGET} TYPE)
+    if(target_type MATCHES "SHARED_LIBRARY|MODULE_LIBRARY")
+      # OK
+    elseif(target_type MATCHES "STATIC_LIBRARY|OBJECT_LIBRARY|INTERFACE_LIBRARY|EXECUTABLE")
+      message(SEND_ERROR "${TARGET} is not a shared library, but has type=${target_type}")
+    else()
+      message(WARNING "Unable to extract dynamic library from target=${TARGET}, type=${target_type}.")
+    endif()
+    # TARGET_SONAME_FILE is not allowed for DLL target platforms.
+    if(WIN32)
+      set(result "$<TARGET_FILE_NAME:${TARGET}>")
+    else()
+      set(result "$<TARGET_SONAME_FILE_NAME:${TARGET}>")
+    endif()
+  endif()
+  set(${DEST} ${result} PARENT_SCOPE)
+endfunction()
+
 function(check_linker_supports_version_file VAR)
   SDL_detect_linker()
   if(CMAKE_C_COMPILER_LINKER_ID MATCHES "^(MSVC)$")
diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake
index eb9906bb82c50..aec4f479faeb7 100644
--- a/cmake/sdlchecks.cmake
+++ b/cmake/sdlchecks.cmake
@@ -1053,10 +1053,10 @@ macro(CheckHIDAPI)
         cmake_pop_check_state()
         if(HAVE_LIBUSB_H)
           set(HAVE_LIBUSB TRUE)
-          FindLibraryAndSONAME("usb-1.0" LIBDIRS ${PC_LIBUSB_LIBRARY_DIRS})
-          if(SDL_HIDAPI_LIBUSB_SHARED AND USB_1.0_LIB_SONAME)
+          target_get_dynamic_library(dynamic_libusb LibUSB::LibUSB)
+          if(SDL_HIDAPI_LIBUSB_SHARED AND dynamic_libusb)
             set(HAVE_HIDAPI_LIBUSB_SHARED ON)
-            set(SDL_LIBUSB_DYNAMIC "\"${USB_1.0_LIB_SONAME}\"")
+            set(SDL_LIBUSB_DYNAMIC "\"${dynamic_libusb}\"")
             sdl_link_dependency(hidapi INCLUDES $<TARGET_PROPERTY:LibUSB::LibUSB,INTERFACE_INCLUDE_DIRECTORIES>)
           else()
             sdl_link_dependency(hidapi LIBS LibUSB::LibUSB PKG_CONFIG_SPECS "${LibUSB_PKG_CONFIG_SPEC}" CMAKE_MODULE LibUSB)