CMake GET_RUNTIME_DEPENDENCIES couldn't find dll library when it is linked through lib (import library)?
Asked Answered
A

2

6

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.

Ascocarp answered 3/2, 2020 at 11:34 Comment(1)
Please, provide some code (preferably, minimal reproducible example). With the current information a solving the problem is a hard guessing game.Meow
C
8

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) <[email protected]>
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()
Coryden answered 18/11, 2021 at 8:56 Comment(3)
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 <prefix>_LIBRARY, moves its value to <prefix>_IMPLIB, and redefines <prefix>_LIBRARY with the DLL path. That would allow the same find_library(<prefix>_LIBRARY ...) calls to be used on Windows and non-Windows platforms, with implib_to_dll() then correcting the value of <prefix>_LIBRARY to contain a DLL path like it should.Coryden
My only reluctance is that <prefix>_LIBRARY is usually a CACHE var, but the variables that the macro sets would not be. So you'd end up with <prefix>_LIBRARY in the cache containing one thing, and <prefix>_LIBRARY as a local variable containing something different, which could end up being confusing if you neglected to update the CACHE variable afterwards?Coryden
(I say <prefix>_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.)Coryden
H
0

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
Horsewoman answered 28/10, 2020 at 21:39 Comment(8)
One small note, the argument to $<TARGET_FILE:> can only be a target name. (Like, if you have a target definition add_library(foo SHARED ...), then $<TARGET_FILE:foo> will insert libfoo.so on Linux, foo.dll on Windows, etc. But $<TARGET_FILE:foo.dll> or $<TARGET_FILE:libfoo> 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.Coryden
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...Coryden
@Coryden 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, $<TARGET_FILE:${SOME_VAR}> errors with Expression syntax not recognized.Horsewoman
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..).Coryden
(I mean, actually you can do $<TARGET_FILE:${myvar}> 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.)Coryden
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!Horsewoman
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.)Coryden
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.Horsewoman

© 2022 - 2024 — McMap. All rights reserved.