How to use cmake file( GET_RUNTIME_DEPENDENCIES in an install statement?
Asked Answered
A

2

19

How do you use file(GET_RUNTIME_DEPENDENCIES...) in a cmake install scripted statement? I can't find an example of this usage online, and the statement in the documentation and errors messages of using [[ ]] embedded custom scripting is not clear to me.

The impression I get is that at install time, this can be used to locate file dependencies of your cmake target and potentially bring them over with your install action, making it usable in standalone form.

For example, my application depends on QT and the expectation is that if this is configured correctly, the QT dlls needed for this application will be copied over to the bin. (I just want to be sure I don't have a misunderstanding of it's function in this context as well). It may not directly copy the files but I assume provides a list of files to copy that install will then process (all done at install time).

My naive attempt to just throw something at it to start is:

set(TARGET_NAME "myapp")

#  installation settings
install(TARGETS ${TARGET_NAME}
    [[
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RES
        UNRESOLVED_DEPENDENCIES_VAR UNRES
        CONFLICTING_DEPENDENCIES_PREFIX CONFLICTING_DEPENDENCIES
        EXECUTABLES ${TARGET_NAME}
    )]]

    RUNTIME DESTINATION "${INSTALL_X_BIN}" COMPONENT libraries
    LIBRARY DESTINATION "${INSTALL_X_LIB}" COMPONENT libraries
)

However this of course gives me:

CMake Error at applications/CMakeLists.txt:117 (install):
install TARGETS given target " file(GET_RUNTIME_DEPENDENCIES

      RESOLVED_DEPENDENCIES_VAR RES
      UNRESOLVED_DEPENDENCIES_VAR UNRES
      CONFLICTING_DEPENDENCIES_PREFIX CONFLICTING_DEPENDENCIES
      EXECUTABLES ${TARGET_NAME}
  )" which does not exist.


-- Configuring incomplete, errors occurred!

I feel silly about this like I'm missing something pretty basic.

Aldarcie answered 13/7, 2020 at 21:20 Comment(1)
If you have found that command in the documentation, then it is clearly states about usage of this command inside install(CODE) or with install(SCRIPT). And not with install(TARGETS) as you have tried. You may perform experiments with this command alone, in simple scenarios without install.Packston
A
35

Zeroth, an update

As of the next version of CMake (3.21), you may not want to use file(GET_RUNTIME_DEPENDENCIES) in some cases. (Which would be a good thing, as it works... poorly. It has no ability to differentiate between 32-bit and 64-bit shared libraries, for one thing, so it's irritatingly common to get wrong-arch libs returned on Linux. Then again, this development won't change that fact.)

If you're on Windows, the most common platform to require GET_RUNTIME_DEPENDENCIES logic, the next version of CMake is looking to take another stab at this (hopefully, fourth(?) time's the charm) with a new generator expression: $<TARGET_RUNTIME_DLLS:target>.

It's documented as the "List of DLLs that the target depends on at runtime. This is determined by the locations of all the SHARED and MODULE targets in the target's transitive dependencies. [...] This generator expression can be used to copy all of the DLLs that a target depends on into its output directory in a POST_BUILD custom command."

Considering I currently have custom logic in a CMakeLists.txt to do precisely that, because it's the only way to make the library's unit tests executable from the build directory, I'm hopeful this new expression makes that a bit easier.

Further update...

($<TARGET_RUNTIME_DLLS> won't fix the problems with file(GET_RUNTIME_DEPENDENCIES), but some commits just merged into CMake's upcoming 3.21 branch purport to, by teaching it how to distinguish between libraries for different architectures. Hooray!)

First, a caveat

You mentioned Qt. No matter what you do here, this method is unlikely to work for Qt all by itself, because there's no way using only the runtime dependencies of a program/library that you can discover any Qt plugins or other components that your installation may also require. Qt's dependencies are more complex than just libraries.

(My answer here demonstrates how to obtain Qt plugin information for bundling purposes, using the QCocoaIntegrationPlugin QPA on macOS as an example. All of Qt's plugins are represented by their own IMPORTED CMake targets, in recent releases, so it's typically possible to write install(CODE ...) scripting which picks up those targets using generator expressions in a similar manner to the following code.)

file(GET_RUNTIME_DEPENDENCIES)

As Tsyvarev noted in comments, GET_RUNTIME_DEPENDENCIES is intended to be used in the install stage, not the configure stage. As such, it needs to be placed in an install(CODE ...) or install(SCRIPT ...) statement, which will cause the code evaluation to be delayed until after the build is complete. (In fact, install(CODE ...) inserts the given code right into the current directory's cmake_install.cmake script. You can examine the results just by looking at that file, without even having to run the install.)

The delayed evaluation also comes with a few wrinkles. Primarily: The code doesn't understand targets. The targets no longer exist at the install stage. So, to include any target info, you have to use generator expressions to insert the correct values.

While the CMake documentation indicates that variable references and escapes aren't evaluated inside bracket arguments, generator expressions are. So, you can compose the CODE wrapped in [[ ]] to avoid escaping everything.

You still have to be careful about variable expansion / escaping. Most variables (including any you create) aren't available in the install context — only a few are, like CMAKE_INSTALL_PREFIX. You have to either expand or set any others.

There are, AFAICT, no generator expressions to access arbitrary variables. There are some for specific variables/values, but you can't say something like $<LIST:MY_LIST_VAR> or $<VALUE:MY_STRING_VAR> to combine variables and bracket arguments.

So, if you want to use variables from the configure context in the CODE, where they'll be evaluated at install time, the easiest thing to do is to "transfer" them into the install script by set()-ing a variable in the CODE.

file(INSTALL TYPE SHARED_LIBRARY)

To install shared library dependencies, you can use the same file(INSTALL) command that CMake itself uses in cmake_install.cmake if you build a shared library target. It uses the TYPE SHARED_LIBRARY option to add some extra processing. The FOLLOW_SYMLINK_CHAIN option is also especially handy. Together they'll make file(INSTALL) both resolve symbolic links in the source files, and automatically recreate them in the destination path.

Example code

So all in all, you'd want to do something like this:

set(MY_DEPENDENCY_PATHS /path/one /path/two)

# Transfer the value of ${MY_DEPENDENCY_PATHS} into the install script
install(CODE "set(MY_DEPENDENCY_PATHS \"${MY_DEPENDENCY_PATHS}\")")

install(CODE [[
  file(GET_RUNTIME_DEPENDENCIES
    LIBRARIES $<TARGET_FILE:mylibtarget>
    EXECUTABLES $<TARGET_FILE:myprogtarget>
    RESOLVED_DEPENDENCIES_VAR _r_deps
    UNRESOLVED_DEPENDENCIES_VAR _u_deps
    DIRECTORIES ${MY_DEPENDENCY_PATHS}
  )
  foreach(_file ${_r_deps})
    file(INSTALL
      DESTINATION "${CMAKE_INSTALL_PREFIX}/lib"
      TYPE SHARED_LIBRARY
      FOLLOW_SYMLINK_CHAIN
      FILES "${_file}"
    )
  endforeach()
  list(LENGTH _u_deps _u_length)
  if("${_u_length}" GREATER 0)
    message(WARNING "Unresolved dependencies detected!")
  endif()
]])

* – (Note that using the DIRECTORIES argument on a non-Windows system will cause CMake to emit a warning, as files' dependencies are supposed to be resolvable using only the current environment.)

If the code gets too complex, there's always the option to create a separate script file copy_deps.cmake in the ${CMAKE_CURRENT_SOURCE_DIR} and use install(SCRIPT copy_deps.cmake). (A previous version of this answer suggested using file(GENERATE...) to build the script — that won't work, as the file isn't written until after processing the CMakeLists.txt.)

Ankerite answered 22/9, 2020 at 11:51 Comment(7)
@Tomerikoo Whoops! Thanks, stupid sticky L key.Ankerite
No problem! Actually BenS brought it up in an answer below (now deleted)Anus
BTW, anyone interested in this topic as it pertains to Windows, specifically, should check out the significant update to the saga I just posted in an answer to a different question. (Spoiler: You're writing your Find modules wrong. We ALL are. And it suddenly matters!)Ankerite
Thanks for this example. I wonder, is it reasonable to expect mere mortals to figure this out on their own? The documentation is frustratingly lacking here.Lysenkoism
@Lysenkoism It is poorly-documented, and I think the CMake developers know it is, because they keep adding new ways to avoid using the poorly-documented commands. First there was $<TARGET_DLLS>, for Windows. And now with CMake 3.21+, we have install(RUNTIME_DEPENDENCY_SET) (and install(IMPORTED_RUNTIME_ARTIFACTS) which can add non-installed target dependencies to the set; install(TARGETS) adds installed deps if you give it a RUNTIME_DEPENDENCY_SET arg) — those are configure-time commands that generate file(GET_RUNTIME_DEPENDENCIES) calls, ideally making the above unnecessary.Ankerite
But even with those (better-documented) options now available, their documentation is scattered all over the command reference, which doesn't exactly make for easy synthesis or big-picture comprehension. Installation of project outputs, as a topic, could really use a consolidated guide along the lines of the Using Dependencies Guide or Importing and Exporting Guide.Ankerite
(Also note, I meant $<TARGET_RUNTIME_DLLS> where I wrote $<TARGET_DLLS> above. Realized after the edit window had expired that it didn't sound quite right...)Ankerite
P
4

Building onto this answer (thanks!) I created a recursive version for collecting all library dependencies and their dependants (and so on..) for a given executable:

install(CODE [[
  function(install_library_with_deps LIBRARY)
    file(INSTALL
      DESTINATION "${CMAKE_INSTALL_PREFIX}/lib"
      TYPE SHARED_LIBRARY
      FOLLOW_SYMLINK_CHAIN
      FILES "${LIBRARY}"
    )
    file(GET_RUNTIME_DEPENDENCIES
      LIBRARIES ${LIBRARY}
      RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
      UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
    )
    foreach(FILE ${RESOLVED_DEPS})
      if(NOT IS_SYMLINK ${FILE})
        install_library_with_deps(${FILE})
      endif()
    endforeach()
    foreach(FILE ${UNRESOLVED_DEPS})
      message(STATUS "Unresolved from ${LIBRARY}: ${FILE}")
    endforeach()
  endfunction()
  file(GET_RUNTIME_DEPENDENCIES
    EXECUTABLES  $<TARGET_FILE:myexecutable>
    RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
    UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
  )
  foreach(FILE ${RESOLVED_DEPS})
    install_library_with_deps(${FILE})
  endforeach()
  foreach(FILE ${UNRESOLVED_DEPS})
    message(STATUS "Unresolved: ${FILE}")
  endforeach()
]])

I also think its relevant to note that some variables (like CMAKE_INSTALL_PREFIX) can be used in the inner scope as they are, while others (like CMAKE_PREFIX_PATH) need to be re-set explicitly.

Going from here one might want to exclude specific system directories, this here likely collects too much.

Philipson answered 4/8, 2022 at 20:14 Comment(2)
Thanks for this, it looks useful! Yeah, CMAKE_INSTALL_PREFIX is pretty much the only variable that still exists at install time -- and it's critical to use it, because remember that it can be CHANGED at install time from the value it had at configure time. (That's also why it's important to never use it at configure time, because hard coding it into the build makes your build un-relocatable.)Ankerite
Other vars, like CMAKE_PREFIX_PATH, are probably best used in the construction of configure-time targets which then can be expanded into the install code via generator expressions, like you've done above with $<TARGET_FILE:executable>. But, there may be cases where "forwarding" a variable is warranted, certainly.Ankerite

© 2022 - 2024 — McMap. All rights reserved.