# SPDX-FileCopyrightText: 2021 FeRD (Frank Dana) <ferdnyc@gmail.com>
#
# SPDX-License-Identifier: CC0-1.0

#[==================================================================[.rst:
IMPORT_UTILS
------------

Tools for CMake on WIN32 to correct targets which have their
IMPORTED_LOCATION property set with an import library (.lib, .dll.a),
by discovering the corresponding DLL and updating the target's
IMPORTED_LOCATION and IMPORTED_IMPORT, as well as the properties
IMPORTED_LOCATION_<CONFIG> and IMPORTED_IMPORT_<CONFIG> for each
configuration listed in the target's IMPORTED_CONFIGURATIONS.

Writing Find modules that create ``SHARED IMPORTED`` targets with the
correct ``IMPORTED_IMPORT`` and ``IMPORTED_LOCATION`` properties is a
requirement for ``$<TARGET_RUNTIME_DLLS>`` to work correctly. (Probably
``IMPORTED_RUNTIME_DEPENDENCIES`` as well.)

Functions Provided
^^^^^^^^^^^^^^^^^^

Currently there is only one public API call here:

  fix_imports(TARGET target [PACKAGE_NAME package] [PATHS dir1 ...])

``target``  should be an ``IMPORTED`` target which has (or may have) its ``IMPORTED_LOCATION`` set to a ``.lib`` or ``.dll.a`` file (probably
supplied by ``find_library()``). It can have multiple configurations
defined as ``IMPORTED_LOCATION_<CONFIG>``, as long as they are listed
in its ``IMPORTED_CONFIGURATIONS`` property. Each one will be corrected.

``fix_imports`` will attempt to locate the corresponding ``.dll`` file
for the import library, set the target's ``IMPORTED_LOCATION[_<CONFIG>]``
to that ``.dll`` file's path, and move the import-library path over to
the ``IMPORTED_IMPORT[_<CONFIG>]`` property where it belongs.

The optional ``PACKAGE_NAME package`` argument can be used to provide
the prefix for the cache variable, ``<package>_RUNTIME_DLL[_<CONFIG>]``,
that will be used to store the DLL file's path. If no ``PACKAGE_NAME``
is provided, one will be generated by stripping all colons (``:``)
from the target name. So, ``fix_imports(TARGET My::lib)`` will set the
cache variable ``Mylib_RUNTIME_DLL[_<CONFIG>]``.

The optional ``PATHS`` argument can be followed by one or more
paths which will be added to the locations searched for the DLL.
By default it looks in the location of the import library,
and in a ``bin/`` directory next to the import library's parent.
(So, if the import library is in ``C:\some\path\lib\``, the DLL will
be looked for there and in ``C:\some\path\bin\``.) It also checks
the directories listed in the ``PATH`` environment variable.

Because it operates on already-defined targets, ``fix_imports()`` can
either be called from a Find module (to repair targets as they're
being created), or from ``CmakeLists.txt`` while generating the build
system, to fix up targets created by external Find modules. (Even
many of the Find modules distributed with CMake itself fail to properly
configure targets on Windows systems. In many cases ``fix_imports`` can
correct the ``IMPORTED_`` paths of targets produced by those modules.)

Dependencies
^^^^^^^^^^^^

``fix_imports`` relies on the ``dlltool.exe`` utility. The path can
be set by defining ``DLLTOOL_EXECUTABLE`` in the cache prior to
including this module, if it is not set import_utils will attempt to
locate ``dlltool.exe`` using ``find_program()``.

Revision history
^^^^^^^^^^^^^^^^
2021-12-28 - Renamed to import_utils / fix_imports()
2021-12-27 - Added support for extra search PATHS
2021-12-10 - Support no IMPORTED_LOCATION with IMPORTED_CONFIGURATIONS
2021-11-28 - Reworked to operate on a target, instead of variables
2021-11-18 - Updated docs to remove CACHE mentions, fixed formatting
2021-10-14 - Initial version

Author: FeRD (Frank Dana) <ferdnyc@gmail.com>
License: CC0-1.0 (Creative Commons Universal Public Domain Dedication)
#]==================================================================]
include_guard(DIRECTORY)
include(CMakeParseArguments)

if (NOT WIN32)
  # Make fix_imports() a no-op
  function(fix_imports)
  endfunction()
  return()
endif()

if (NOT DEFINED DLLTOOL_EXECUTABLE)
  find_program(DLLTOOL_EXECUTABLE
    NAMES dlltool dlltool.exe
    DOC "The path to the DLLTOOL utility"
  )
  if (DLLTOOL_EXECUTABLE STREQUAL "DLLTOOL_EXECUTABLE-NOTFOUND")
    message(WARNING "DLLTOOL not available, cannot continue")
    return()
  endif()
  message(DEBUG "Found dlltool at ${DLLTOOL_EXECUTABLE}")
endif()


macro(_get_if_set out tgt prop)
  # Checks whether the property 'prop' is defined on 'tgt',
  # sets the BOOL variable 'out_SET',
  # and if it is defined, reads the property value into
  # the variable 'out'
  unset(${out})
  unset(${out}_SET)
  get_property(${out}_SET TARGET ${tgt} PROPERTY ${prop} SET)
  if (${out}_SET)
    get_property(${out} TARGET ${tgt} PROPERTY ${prop})
  endif()
endmacro()


function(_find_dll _outvar _implib _cachevar _paths)
  # (Win32 only)
  # Uses dlltool.exe to find the name of the dll associated with the
  # supplied import library.
  message(DEBUG "Looking up dll name for import library ${_implib}")
  execute_process(COMMAND
    "${DLLTOOL_EXECUTABLE}" -I "${_implib}"
    OUTPUT_VARIABLE _dll_name
    OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  message(DEBUG "DLLTOOL returned ${_dll_name}, finding...")

  # Check the directory where the import lib is found
  get_filename_component(_implib_dir ".." REALPATH
                         BASE_DIR "${_implib}")
  message(DEBUG "Checking import lib directory ${_implib_dir}")

  # Add a check in ../../bin/, relative to the import library
  get_filename_component(_bindir "../../bin" REALPATH
                         BASE_DIR "${_implib}")
  message(DEBUG "Also checking ${_bindir}")

  find_program(${_cachevar}
    NAMES ${_dll_name}
    HINTS
      ${_bindir}
      ${_implib_dir}
    PATHS
      ENV PATH
      ${_paths}
  )
  set(${_outvar} "${${_cachevar}}" PARENT_SCOPE)
endfunction()


macro(fix_imports)
# fix_imports(TARGET target [PACKAGE_NAME package] [PATHS paths])
#
# (Win32 only)
# Changes the properties of the specified IMPORTED target so that its
# IMPORTED_LOCATION is a .dll file, instead of an import library.
# The import library path is moved to IMPORTED_IMPORT instead.
#
# Required arguments:
#   TARGET target
#   - The name of the target to operate on
# Optional arguments:
#   PACKAGE_NAME package
#   - The prefix to use on a cache variable name, which will be
#     <package>_RUNTIME_DLL (or <package>_RUNTIME_DLL_<CONFIG>).
#     That variable or variables will hold the discovered DLL path.
#
#     If PACKAGE_NAME is not set by the caller, the target name will
#     be stripped of any colons (:) and used as the variable name prefix.
#     (e.g. ``fix_imports(TARGET My::lib)`` will create Mylib_RUNTIME_DLL
#     in the cache, if it finds a DLL to match the IMPORTED_LOCATION.)
#
#  PATHS dir1 ... dirN
#  - One or more directory locations which will be added to the
#    ``find_program()`` call used to locate the DLL
#
  set(options)
  set(oneValueArgs TARGET PACKAGE_NAME)
  set(multiValueArgs PATHS)
  cmake_parse_arguments(FixImports
    "${options}"
    "${oneValueArgs}"
    "${multiValueArgs}"
    ${ARGN}
  )

  if(NOT DEFINED FixImports_TARGET OR NOT TARGET ${FixImports_TARGET})
    message(SEND_ERROR
      "fix_imports() not called with a valid target argument: '${FixImports_TARGET}'")
    return()
  endif()
  if(DEFINED FixImports_PACKAGE_NAME)
    set(_cachevar "${FixImports_PACKAGE_NAME}_RUNTIME_DLL")
  else()
    # Strip all colons from the target's name and use that as
    # the prefix for our cache variable name, since we weren't
    # given a package name to use.
    string(REPLACE ":" "" _cachevar "${FixImports_TARGET}_RUNTIME_DLL")
  endif()
  if(DEFINED FixImports_PATHS)
    set(_search_paths "${FixImports_PATHS}")
  else()
    set(_search_paths)
  endif()

  # Look up which configs we (might) have to find DLLs for, beyond
  # the anonymous default one that we'll always check
  set(_no_type_suffixes TRUE)
  _get_if_set(_cfgs ${FixImports_TARGET} IMPORTED_CONFIGURATIONS)
  if(_cfgs_SET)
    set(_no_type_suffixes FALSE)
    foreach(_c IN LISTS _cfgs)
      list(APPEND _suffixes "_${_c}")
    endforeach()
  else()
    set(_suffixes "")
  endif()

  # Match the import library for each configuration (including
  # the anonymous one) up with the corresponding runtime DLL,
  # adding "_<CONFIGURATION>" as the suffix for all property names
  # when processing one of the IMPORTED_CONFIGURATIONS types.
  foreach(_sfx IN ITEMS "" LISTS _suffixes)
    _get_if_set(_lib ${FixImports_TARGET} IMPORTED_LOCATION${_sfx})
    _get_if_set(_implib ${FixImports_TARGET} IMPORTED_IMPLIB${_sfx})
    message(DEBUG "${FixImports_TARGET} IMPORTED_LOCATION${_sfx}: '${_lib}'")
    message(DEBUG "${FixImports_TARGET} IMPORTED_IMPLIB${_sfx}: '${_implib}'")
    if(NOT _lib_SET OR _lib_SET AND "${_lib}" STREQUAL "")
      # Don't complain if we still have build type suffixes to process
      if("${_sfx}" STREQUAL "" AND ${_no_type_suffixes})
        message(SEND_ERROR
          "Target ${FixImports_TARGET} properties incorrectly defined!")
      endif()
      continue()
    elseif("${_lib}" MATCHES "\\.dll$")
      message(DEBUG
        "${FixImports_TARGET}.${_lib} is already a DLL path, ignoring")
      continue()
    endif()
    if(NOT "${_lib}" MATCHES "(\\.lib|\\.dll\\.a)$")
      message(SEND_ERROR
        "Target ${FixImports_TARGET} property IMPORTED_LOCATION${_sfx} "
        "does not appear to contain an import library path: '${_lib}'.")
      continue()
    endif()

    set(_implib "${_lib}")
    unset(_lib)
    if(EXISTS "${_implib}")
      _find_dll(_dll "${_implib}" "${_cachevar}${_sfx}" "${_search_paths}")
      if(NOT "${_dll}" STREQUAL "${_cachevar}${_sfx}-NOTFOUND")
        set_target_properties(${FixImports_TARGET} PROPERTIES
          IMPORTED_IMPLIB${_sfx} "${_implib}"
          IMPORTED_LOCATION${_sfx} "${_dll}"
      )
      endif()
      message(DEBUG "Set ${FixImports_TARGET} IMPORTED_LOCATION${_sfx} to ${_dll}")
    endif()
  endforeach()
endmacro()
