Finding auxiliary files in a library

I have a library that needs some auxiliary files to accompany the binary and headers.

I tell CMake to install these files along with everything else…

install( TARGETS foo
        EXPORT footargets
)

install( DIRECTORY
         ${CMAKE_SOURCE_DIR}/resources/
         DESTINATION share/foo/resources
)

install( EXPORT footargets
        FILE fooTargets.cmake
        DESTINATION lib/cmake/foo
)
include( CMakePackageConfigHelpers )
# etc.

The library goes on with the canonical steps to create and install the fooConfig.cmake, fooConfigVersion.cmake, and fooTargets.cmake files.

This works reasonably well on the library side (though changes may be needed). The problem I have is on the side of the application that uses this library (and needs to use the resource files).

I successfully FindPackage and can use the imported target to build my program (library and include files are found, etc.). However, how can I get the path to the auxiliary files that accompany the library?

find_package(foo REQUIRED)

# How I find the path to the found share/foo/resources ?

Although the files were installed by the library, they aren’t officially a part of the library target and the CMakePackageConfigHelpers stuff doesn’t know about the files.

Is there a way to add a variable to the target that will get exported and then set by find_package?

Does find_package set a variable identifying the path where the library was found? If so, I could probably build the path I need from there.

OK, I was able to sort it out. Please let me know if there is a better way…

In the library, I had to:

set( RESOURCE_INSTALL_DIR "share/foo/resources" )
include( CMakePackageConfigHelpers )
# generate the config file that includes the exports
configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
        "${CMAKE_CURRENT_BINARY_DIR}/fooConfig.cmake"
        INSTALL_DESTINATION "lib/cmake/foo"
        PATH_VARS RESOURCE_INSTALL_DIR
)

And I had to modify Config.cmake.in

@PACKAGE_INIT@

set( foo_RESOURCE_INSTALL_DIR @CMAKE_INSTALL_PREFIX@/share/foo/resources )

include ( "${CMAKE_CURRENT_LIST_DIR}/fooTargets.cmake" )

Then, when I find_package in the application, I now have:

find_package( foo REQUIRED)
message( STATUS "foo_RESOURCE_INSTALL_DIR = ${foo_RESOURCE_INSTALL_DIR}")

Giving the desired result.

I don’t really understand if the PATH_VARS step is needed, or if the Config.cmake.in is really doing all the work.

OK, a similar problem…

How can I get the variable defined if the library is used by the application via FetchContent?

I managed a perhaps ugly solution by simply setting the variable with PARENT_SCOPE in the library project.

set( foo_RESOURCE_INSTALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/resources PARENT_SCOPE )

I think this should work if someone were to include the library in their project via either add_subdirectory or FetchContent.

I would appreciate being informed of a more elegant or canonical way.

Consider setting a property on your target to specify the location rather than setting a variable. One of the problems with variables, as you’ve discovered, is that they are hard to ensure they are always set in the scope where the consumer needs them. Properties set on a target don’t have that problem, since they are always available anywhere the target is available.

As a general principle, I try to avoid setting package variables in projects I work on. I do everything through target properties where I have the choice. This works whether consumers use find_package() or FetchContent to bring the dependency into their builds, and it also works within the build of the package itself.

Thanks, that does look more elegant. I was unaware that you could set arbitrary properties – I thought you could only set a few hard-coded properties.

This property needs to take a different value depending on whether the library has been installed (i.e. the resources have been copied somewhere under ${CMAKE_INSTALL_DIR} and found via find_package, or if it has been used in-place (say via add_subdirectory or FetchContent).

How can I ‘set_target_property’ appropriately for each case?

Once again, I’m not sure if this is the right way to do it, but it seems to work…

For use within the library and for when it is included via add_subdirectory of FetchContent…

set_target_properties( foo PROPERTIES
        RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../resources
)

set_property( TARGET foo APPEND PROPERTY
        EXPORT_PROPERTIES RESOURCE_DIR
)

Using CMAKE_CURRENT_SOURCE_DIR here is somewhat important because CMAKE_SOURCE_DIR will point to the library when building the library stand-alone, but will point to the parent project when building with FetchContent or add_subdirectory.

Then, for when the library is installed and accessed via Find_Package, I needed to edit my Config.cmake.in template.

@PACKAGE_INIT@

include ( "${CMAKE_CURRENT_LIST_DIR}/fooTargets.cmake" )

set_target_properties( foo PROPERTIES
        RESOURCE_DIR @CMAKE_INSTALL_PREFIX@/share/foo/resources
)

Here, it is important that set_target_properties is called after the include – because the target doesn’t exist until that include is complete.

This is probably obvious to everyone but me, but the Config.cmake.in is just a template for a CMake script that will run at the time the library is imported. We can write arbitrary code there. In this case, we’re over-writing the value of RESOURCE_DIR that is set when the target is imported via the include line.

We don’t need to set EXPORT_PROPERTIES again because it is still set from before.

That’s one way to do it, but it would have the downside that a path on your build machine ends up in the files installed on an end user’s machine. Even if you’re overwriting that path later, you normally don’t want to do this.

Ordinarily, I’d suggest you might be able to avoid leaking into the installed files while still supporting the “export directly from my build dir for local use” scenario by wrapping the EXPORT_PROPERTIES value in a $<BUILD_INTERFACE:...> generator expression. The documentation for EXPORT_PROPERTIES doesn’t say generator expressions are supported though, and that normally means they are not supported. However, the documentation does make the following somewhat ambiguous statement:

Properties containing generator expressions are also not allowed.

I take that to mean you can’t export properties whose values hold generator expressions, but that seems like an odd restriction (but could still be the case). I don’t know if the names of the properties you want to export can be specified with a generator expression, but I suspect it can’t.

I just checked the implementation, and EXPORT_PROPERTIES does not support generator expressions for specifying the property names, so that rules out that idea.

EDIT: Although now I’m wondering if the exported case would use the same generated <package>-config.cmake file and would therefore still pick up your set_target_properties() command in there. If it does, then you wouldn’t need EXPORT_PROPERTIES at all. Sorry I can’t be more specific, I pretty much never use the “export-in-place” scenario, so I don’t recall the finer details of what it does under the covers.