How to make a subproject export a link directory of a library dependency

I have this dependency graph:

my_project
   |—> static linking perceptualdiff
       |—> (static linking with dynamic loading) FreeImage (built as a shared library)

I can correctly build perceptualdiff on its own. It will generate FreeImage.dll and FreeImage.lib in out/build/x64-Debug/_deps/freeimage-build.

And I can correctly build my_project as well. It will generate:

  • pdiff.lib in build/NinjaMultiConfig/test/_deps/perceptualdiff-build/Debug.
  • FreeImage.dll and FreeImage.lib in build/NinjaMultiConfig/test/_deps/freeimage-build/Debug.

However, when I try to run my project.exe, it won’t be able to find FreeImage.dll.

I understand I’m only adding pdiff.lib’s folder to my_project’s library path. How can I tell perceptualdiff’s CMake to “export” FreeImage.dll’s folder to whoever-links-against-pdiff’s library path?

FreeImage’s CMakeLists.txt:

add_definitions(-DOPJ_STATIC -DLIBRAW_NODLL)
add_library(FreeImage SHARED ${FreeImage_SRC})

perceptualdiff’s CMakeLists.txt:

include(FetchContent)
FetchContent_Declare(FreeImage
    GIT_REPOSITORY https://github.com/rturrado/FreeImage.git
    GIT_TAG "eb1a2ad20a6579854b56cc73eb97b7e1aa2c5861"
)
FetchContent_MakeAvailable(FreeImage)

add_library(pdiff lpyramid.cpp rgba_image.cpp metric.cpp)
target_include_directories(pdiff PUBLIC ${FreeImage_SOURCE_DIR}/Source)
target_link_libraries(pdiff PUBLIC FreeImage)

My project’s CMakeLists.txt:

FetchContent_Declare(perceptualdiff
    GIT_REPOSITORY https://github.com/rturrado/perceptualdiff.git
    GIT_TAG "eb58ae5ee2d30d947c04249f7b430c4953c00d6c"
)
FetchContent_MakeAvailable(
    perceptualdiff
)

add_executable(${PROJECT_NAME}_test ${app_sources})
target_include_directories(${PROJECT_NAME}_test PUBLIC
    ${perceptualdiff_SOURCE_DIR}
)
target_compile_features(${PROJECT_NAME}_test PRIVATE cxx_std_23)
target_link_libraries(${PROJECT_NAME}_test PUBLIC
    pdiff
)

Windows does not have an RPATH equivalent; the PATH environment variable just needs to be set up properly. Instead, you might consider copying the DLL as needed using file(GET_RUNTIME_DEPENDENCIES) or the $<TARGET_RUNTIME_DLLS> generator expression.

1 Like

Many thanks! Using $<TARGET_RUNTIME_DLLS> worked like a charm. Now, what would be the solution if I wanted my project to compile in non-Windows systems as well?

In case anyone wants to know the exact details of what worked for me, I just added the following lines:

  • at the CMakeLists.txt where I was creating my_project binary, and
  • after all the add_executable, target_compile_features, target_link_libraries, and target_compile_options commands:
# Copy DLLs the target depends on
add_custom_command(
    TARGET ${PROJECT_NAME}_test POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:${PROJECT_NAME}_test> $<TARGET_FILE_DIR:${PROJECT_NAME}_test>
    COMMAND_EXPAND_LISTS
)

See $<TARGET_RUNTIME_DLLS> documentation.
See GitHub commit diff.

macOS and Linux (or any ELF platform) have different strategies. On macOS, the library ID dictates this (it’s complicated, but rpath only takes effect if @rpath/ is actually used). On Linux, “rpath” is the term to look for. Basically, libraries say “I need libA.so.4” and the rpaths are searched for a library with that name.

Many thanks for your answer.

It’s a bit of a shame that there are actually three different solutions, one for each system. But I guess it’s good to know about them at least.

Updating my previous answer now that I’ve also implemented the solution to my problem for Unix-like systems (I haven’t tested this for MacOS):

# Shared libraries
if(WIN32)
    # Copy DLLs the target depends on
    add_custom_command(
        TARGET ${PROJECT_NAME}_test POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:${PROJECT_NAME}_test> $<TARGET_FILE_DIR:${PROJECT_NAME}_test>
        COMMAND_EXPAND_LISTS
    )
elseif(UNIX)
    if(APPLE)
        set(CMAKE_MACOSX_RPATH 1)
    endif()
    set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
endif()

That is, for Linux, setting CMAKE_INSTALL_RPATH_USE_LINK_PATH to TRUE worked for the situation described in this post. As far as I understand the effects of enabling this variable, it appends to the runtime search path (rpath) of all the targets the directories containing the binaries of the libraries that are linked against. In this case, it appends to my_project’s rpath the folder where FreeImage.so lives.

I have to update again my last answer.

I faced a problem when using CMAKE_INSTALL_RPATH_USE_LINK_PATH.

I was building a docker, including a binary, the_modern_c++_challenge_test, which made use of a shared library, libFreeImage.so. I was placing both in a directory in the docker. When I tried executing the binary, it would fail with an error saying it couldn’t find the shared library. ldd the_modern_c++_challenge_test would report libFreeImage.so was expected to be found at an absolute path in my machine, where I had built the project (something like /home/rturrado/projects/the_modern_c++_challenge/out/build.../_deps/free-image-build/Release/libFreeImage.so).

The “fix” I used was to directly add ./ to the runtime path via the linker properties. I am not completely sure this is a proper fix, but it works for this particular case, because it tells the OS to go and search for the shared library at the same directory where the binary lives (as I said in the previous answer, I haven’t tested this on MacOS).

elseif(UNIX)
    if(APPLE)
        set(CMAKE_MACOSX_RPATH 1)
    endif()
    set_target_properties(${PROJECT_NAME}_test PROPERTIES
        SKIP_BUILD_RPATH FALSE
        BUILD_WITH_INSTALL_RPATH FALSE
        INSTALL_RPATH ""
        INSTALL_RPATH_USE_LINK_PATH FALSE
        LINK_FLAGS "-Wl,-rpath,./"
    )
endif()

You want $ORIGIN/ or @loader_path/ instead of ./ for Linux(-like) and macOS platforms, respectively.

Hi Ben,

I don’t know if I may be doing something wrong, but using @loader_path is not working for me.

This is the CMake section where I set the link flags for Unix systems:

    set_target_properties(${PROJECT_NAME}_test PROPERTIES
        SKIP_BUILD_RPATH FALSE
        BUILD_WITH_INSTALL_RPATH FALSE
        INSTALL_RPATH ""
        INSTALL_RPATH_USE_LINK_PATH FALSE
        LINK_FLAGS "-Wl,-rpath,@loader_path/"
    )

And this is the output of objdump -x the_modern_c++_challenge_test | grep RPATH:

RPATH @loader_path/:/home/rturrado/projects/the_modern_cpp_challenge/out/build/unixlike-gcc-debug-github/_deps/freeimage-build/Debug

Best regards,
Roberto.

@loader_path is for macOS; $ORIGIN is for Linux (and other ELF-using platforms).

Sorry about that. You explained it perfectly fine in your first answer but I didn’t read it correctly.

I had tried setting $ORIGIN before:
LINK_FLAGS "-Wl,-rpath,$ORIGIN/"
The objdump was showing (notice the / and the blank before it):
RPATH /:<absolute path to libFreeImage.so folder>
And it obviously didn’t work in systems where the absolute path wasn’t valid.

I’ve retried now, after doing some research on how to correctly set $ORIGIN (notice the single quotes around $ORIGIN):
LINK_FLAGS "-Wl,-rpath,'$ORIGIN/'"
The objdump now shows:
RPATH $ORIGIN/:<absolute path to libFreeImage.so folder>
And it works.

So the whole CMake command would be:

    set_target_properties(${PROJECT_NAME}_test PROPERTIES
        SKIP_BUILD_RPATH FALSE
        BUILD_WITH_INSTALL_RPATH FALSE
        INSTALL_RPATH ""
        INSTALL_RPATH_USE_LINK_PATH FALSE
        LINK_FLAGS "-Wl,-rpath,'$ORIGIN/'"
    )