5

Build OS: Windows 10, Cmake 3.16.3.

I use target_link_libraries to link 3rd party .lib file to my .dll library.

But when I use GET_RUNTIME_DEPENDENCIES to install my dll, there is no dependency found.

It happens only on Windows, installing on Linux is ok.

Is there any clues how to solve this problem, or at least how to debug it?

What exact command uses CMake on Windows to determine dependencies?

I call GET_RUNTIME_DEPENDENCIES like this:

file(GET_RUNTIME_DEPENDENCIES
    RESOLVED_DEPENDENCIES_VAR RES
    UNRESOLVED_DEPENDENCIES_VAR UNRES
    CONFLICTING_DEPENDENCIES_PREFIX CONFLICTING_DEPENDENCIES
    EXECUTABLES ${EXECS}
    LIBRARIES ${LIBS} ${MODULES} ${QTPLUGINS_LIBS}
    DIRECTORIES ${RUNTIME_DEPENDENCIES_DIRECTORIES}
    POST_EXCLUDE_REGEXES ${PLATFORM_POST_EXCLUDE_REGEXES}
)

Where LIBS contains my dll but no RES no UNRES contains path to 3rd paty dll.

ephemerr
  • 1,647
  • 15
  • 19
  • Please, provide some **code** (preferably, [mcve]). With the current information a solving the problem is a hard guessing game. – Tsyvarev Feb 03 '20 at 12:06

2 Answers2

4

So, there's a serious nastiness to all this runtime-dependency-finding magic in the newer CMakes, and it's not really their fault at all. The problem is that you, I, and roughly 90% of the rest of the CMake user world have been doing find modules wrong #THISWHOLETIME, and now our chickens have come home to roost because, as you've likely discovered, GET_RUNTIME_DEPENDENCIES / RUNTIME_DEPENDENCY_SET, $<TARGET_RUNTIME_DLLS> will all completely sh*t the bed if you try to use them with targets that have (what I now know to be) broken IMPORTED dependencies created by Find modules that don't properly set them up. So, last month I posted this screed (my earlier link) over at the CMake Discourse forum:

Windows libraries, Find modules, and TARGET_RUNTIME_DLLS

The Windows DLL Question™ has come up numerous times before in one form or another, but it’s cast in a new light by $<TARGET_RUNTIME_DLLS>, so here’s a fresh take on it.

If you’re like me (and roughly 90% of all CMake users/developers out there are like me, from what I’ve been able to observe in public projects’ source trees), your approach to writing Find modules on Windows has probably been something like this:

  1. Use the same code on all three desktop platforms
  2. Let CMake discover .lib / .dll.a import libraries instead of actual DLLs, using find_library().
  3. End up creating your targets as UNKNOWN IMPORTED, because if you try to create SHARED IMPORTED library targets with only an import library it won’t work, but UNKNOWN IMPORTED works just fine so meh.
  4. Set the import library as the target’s IMPORTED_LOCATION since that seems to work fine.
  5. Call it a day, because hey — everything compiles.

That’s served us all for years (decades, really) so we’ve mostly just accepted it as the way CMake works on Windows.

But now along comes $<TARGET_RUNTIME_DLLS>. If you’ve tried to actually use it on Windows, you’ve probably discovered is that while all of your CONFIG-mode package dependencies’ DLLs are captured just fine, the generator expression will cheerfully ignore any targets created from a Find module that’s written like I describe above. …Which is probably most of them. (In my own library build, it was all of them, even the ones I didn’t write.)

For $<TARGET_RUNTIME_DLLS> to work, the IMPORTED target has to be correctly defined as a SHARED library target, and it needs to have its IMPORTED_ properties set correctly: import lib path in IMPORTED_IMPLIB, DLL path in IMPORTED_LOCATION.

So, now I have this new module that uses DLLTOOL.EXE and its handy -I flag to get the name of an import library’s DLL, then looks it up using find_program(). (Simply because find_library() won’t match DLLs, and I wanted to look on the PATH. I could’ve used find_file() but I’m pretty sure I’d have to explicitly give it more paths to search.)

The macro takes one argument, the name of your already-configured variable <prefix>_IMPLIB. (Or <prefix>_IMPLIBS, it’s pluralization agnostic and will follow whichever form your input uses when naming its output variable.)

The variable whose name you pass to it should already contain a valid path for an import library. Typically that’s set by find_library(), even though we’ve all been treating them like runtime libraries (DLLs) when they are not.

Armed with find_library(<prefix>_IMPLIB ...) output, implib_to_dll(<prefix>_IMPLIB) will attempt to discover and automatically populate the corresponding variable <prefix>_LIBRARY with the path to the import lib’s associated runtime DLL.

With all of the correct variables set to the correct values, it’s now possible to properly configure SHARED IMPORTED library targets on Windows. $<TARGET_RUNTIME_DLLS> can then be used to discover and operate on the set of DLLs defined by those target(s).

Kind of a pain in the Find, and really does sort of feel like something CMake could be doing at-least-semi-automatically. But, at least for now it works.

Now I just have to rewrite all of my find modules to use it. Sigh.

ImplibUtils.cmake

#[=======================================================================[.rst:
IMPLIB_UTILS
------------

Tools for CMake on WIN32 to associate IMPORTED_IMPLIB paths (as discovered
by the :command:`find_library` command) with their IMPORTED_LOCATION DLLs.

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

Macros Provided
^^^^^^^^^^^^^^^

Currently the only tool here is ``implib_to_dll``. It takes a single
argument, the __name__ (_not_ value!) of a prefixed ``<prefix>_IMPLIB``
variable (containing the path to a ``.lib`` or ``.dll.a`` import library).

``implib_to_dll`` will attempt to locate the corresponding ``.dll`` file
for that import library, and set the variable ``<prefix>_LIBRARY``
to its location.

``implib_to_dll`` 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 implib_utils will attempt to locate
``dlltool.exe`` using ``find_program()``.

Revision history
^^^^^^^^^^^^^^^^
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)

if (NOT WIN32)
  # Nothing to do here!
  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: implib_to_dll
#
# (Win32 only)
# Uses dlltool.exe to find the name of the dll associated with the
# supplied import library.
macro(implib_to_dll _implib_var)
  set(_implib ${${_implib_var}})
  set(_library_var "${_implib_var}")
  # Automatically update the name, assuming it's in the correct format
  string(REGEX REPLACE
    [[_IMPLIBS$]] [[_LIBRARIES]]
    _library_var "${_library_var}")
  string(REGEX REPLACE
    [[_IMPLIB$]] [[_LIBRARY]]
    _library_var "${_library_var}")
  # We can't use the input variable name without blowing away the
  # previously-discovered contents, so that's a non-starter
  if ("${_implib_var}" STREQUAL "${_library_var}")
    message(ERROR "Name collision! You probably didn't pass "
    "implib_to_dll() a correctly-formatted variable name. "
    "Only <prefix>_IMPLIB or <prefix>_IMPLIBS is supported.")
    return()
  endif()

  if(EXISTS "${_implib}")
    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(${_library_var}
      NAMES ${_dll_name}
      HINTS
        ${_bindir}
        ${_implib_dir}
      PATHS
        ENV PATH
    )
    set(${_library_var} "${${_library_var}}" PARENT_SCOPE)
    message(DEBUG "Set ${_library_var} to ${${_library_var}}")
  endif()
endmacro()
FeRD
  • 1,284
  • 11
  • 22
  • Made some minor adjustments to avoid mentioning CACHE variables, and improve the macro flow. I'm _considering_ changing `implib_to_dll()` so that it takes a name formatted as `_LIBRARY`, _moves_ its value to `_IMPLIB`, and redefines `_LIBRARY` with the DLL path. That would allow the same `find_library(_LIBRARY ...)` calls to be used on Windows and non-Windows platforms, with `implib_to_dll()` then correcting the value of `_LIBRARY` to contain a DLL path like it should. – FeRD Nov 19 '21 at 01:19
  • My only reluctance is that `_LIBRARY` **is** usually a CACHE var, but the variables that the macro sets would not be. So you'd end up with `_LIBRARY` in the cache containing one thing, and `_LIBRARY` as a local variable containing something different, which could end up being confusing if you neglected to update the CACHE variable afterwards? – FeRD Nov 19 '21 at 01:20
  • (I say `_LIBRARY`"should" contain a DLL path, because then it'll always hold the value you set as the `IMPORTED_LOCATION` for a SHARED IMPORTED target. Currently due to the way CMake works, even without this macro, the output of `find_library()` is the `IMPORTED_LOCATION` on non-Windows platforms, but the `IMPORTED_IMPLIB` on Windows platforms. That's already confusing, and a big part of the reason I ended up writing this in the first place.) – FeRD Nov 19 '21 at 01:43
1

GET_RUNTIME_DEPENDENCIES isn't aware of your configure-time variables, so will you need to specify them manually. This answer states you can pass-on the variables to the install step, but I haven't been able to make it work so far. Fortunately, it does support generator expressions.

Another problem in your snippet is it must be called at install time. For example in an install(CODE ...) block.

So with all this in mind, this should get you started.

install(CODE [[
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RES
        UNRESOLVED_DEPENDENCIES_VAR UNRES
        CONFLICTING_DEPENDENCIES_PREFIX CONFLICTING_DEPENDENCIES
        EXECUTABLES $<TARGET_FILE:your_executable_target_name>
        LIBRARIES $<TARGET_FILE:a_lib_target_name>
    )

    message("\n\nFound dependencies :")
    foreach(DEP ${RES})
        message("${DEP}")
    endforeach()
    message("\n\nNot found dependencies :")
    foreach(DEP ${UNRES})
        message("${DEP}")
    endforeach()
]])

Build your install target to see the results.

cmake ..
cmake --build . --target install
scx
  • 2,473
  • 17
  • 32
  • 1
    One small note, the argument to `$` can _only_ be a **target** name. (Like, if you have a target definition `add_library(foo SHARED ...)`, then `$` will insert `libfoo.so` on Linux, `foo.dll` on Windows, etc. But `$` or `$` wouldn't work.) Regarding variable transfer, it can be tricky because you have to make sure the variable ISN'T escaped, in the transfer code — you want it to get expanded. I had to double-check the syntax from my answer to make sure I didn't mess that up (again), but it should work. – FeRD Oct 29 '20 at 03:08
  • So, for example, in your code above you could add `install(CODE "set(my_compiler \"${CMAKE_CXX_COMPILER_ID}\")")` before the `install(CODE...` block you have now. Then you could write `message(STATUS "Built with ${my_compiler}")` in the second block, to have the install process output "-- Built with GNU", "-- Built with Clang", etc... – FeRD Oct 29 '20 at 03:14
  • 1
    @FeRD Good note, I've updated the answer to clarify you must provide target names (and not the actual filenames). The problem with passing config variables is you cannot use them in your generator expressions, which renders them somewhat useless. For ex, `$` errors with `Expression syntax not recognized`. – scx Oct 29 '20 at 16:54
  • That wouldn't be useful even if you could do that, because those variables are evaluated immediately at configure time. By the time the `install(CODE...)` block hits the `cmake_install.txt` for that directory (which happens before the initial `cmake` run is completed) those generator expressions are all gone, replaced with the results of their evaluation. Transferring _regular_ variables in can be handy. Frex, if you need to pass a dynamically-generated list of search paths or `PRE_EXCLUDE_REGEXES` to `file(GET_RUNTIME_DEPENDENCIES..)`. – FeRD Oct 29 '20 at 17:08
  • (I mean, actually you can do `$` _normally_ —- you just can't do it inside a `[[ ]]`-fenced block, because it blocks all variable expansion. Which is handy for not having to escape every single dollar sign and brace when you're writing the `CODE` block, but everything's a tradeoff.) – FeRD Oct 29 '20 at 17:10
  • Can we agree being able to pass in your target names would be useful to provide to TARGET_FILE? I'm not sure I fully understand though. Are you saying the "copied in" variables could be expanded if the block was enclosed in double quotes? ty! – scx Oct 29 '20 at 17:12
  • Oh, I'm saying if you were using a double-quoted block, you wouldn't even _need_ to copy the variables in — just like the generator expressions themselves, they'd be expanded immediately as the `install(CODE...)` command is processed. The down side is, you'd have to escape everything you DIDN'T want immediately substituted. (Like the references to `${UNRES}`, `${DEP}`. etc. in your code. The `[[ ]]` block makes that part of the code far easier to write, at the expense of being able to reference parent-file-context variables.) – FeRD Oct 29 '20 at 17:16
  • OK so it seems the problems I'm facing come from target variables. The following doesn't output anything, though it does compile. `install(CODE "set(TARGET_NAME \"${MY_TARGET}\")")` `install(CODE "message(\"Target Name : ${TARGET_NAME}\")")` ` I'm not sure what kind of cmake voodoo prevents this, but thanks for the assistance. – scx Oct 29 '20 at 17:30