Build Directory as Surrogate for installation

Hello,

I work in a context where we occasionally deploy source code to “customers”. We also have several plugins that link against the main software package that are actively developed. In both cases, its desirable to use the build directory for the main project as a surrogate for an “installation”, discoverable with find_package(MyPackage CONFIG). Right now we’re achieving this through some pretty wonky symlink creation, but I’m wondering if there’s a canonical way to achieve this workflow with CMake?

I assume the common answer is “just install MyPackage after building it”, but I’m hoping there’s a way to achieve what we want with the build directory.

Thanks in advance.

There is absolutely a canonical way to do that, and it revolves around the export(TARGETS) command. (Or, if you’re already using install(EXPORT) to create your exported target set for installation, the very similar export(EXPORT) command.)

Basically, export(EXPORT) : build-tree :: install(EXPORT) : install-tree.

By exporting your CMake package configuration and build targets into the build tree, you can allow dependent projects to point CMAKE_PREFIX_PATH into your build directory and use find_package() to pick up your build artifacts the same way they consume your installed configuration.

So, say you have a library project:

add_library(foo SHARED ${foo_sources})
target_include_directories(foo
  PRIVATE
    src/include
  PUBLIC
    ${CMAKE_INSTALL_INCLUDEDIR}/foo
)
set_target_properties(foo ...)
# etc...

And it installs exported targets:

include(GNUInstallDirs)
install(TARGETS foo
  EXPORT FooTargets
  [<PART> DESTINATION ...]s
)
install(EXPORT FooTargets
  NAMESPACE Foo::
  FILE FooTargets.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Foo
)

Along with a generated CMake configuration, so that your clients’ code can pick up the installed package using find_package(Foo) and target_link_libraries(... Foo::foo):

include(CMakePackageConfigHelpers)
configure_package_config_file(
  "${CMAKE_SOURCE_DIR}/cmake/FooConfig.cmake.in"
  "${CMAKE_BINARY_DIR}/cmake/FooConfig.cmake"
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Foo/
)
install(FILES
  "${CMAKE_BINARY_DIR}/cmake/FooConfig.cmake"
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Foo/
)

To make that same exported configuration available without installing the package, all you need to do is ensure your exported INTERFACE_INCLUDE_DIRECTORIES are properly mapped in both the build and install interfaces:

target_include_directories(foo PUBLIC
  $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/foo>
)

And export your targets into the build directory, in addition to the installed configuration:

export(EXPORT FooTargets
  NAMESPACE Foo::
  FILE "${CMAKE_BINARY_DIR}/cmake/FooTargets.cmake"
)

And any client code that would normally use find_package(Foo) and target_link_libraries(... Foo::foo) to link with your installed library can continue to do exactly the same thing, but pick up your uninstalled build artifacts by simply putting your build dir on the config search path:

$ cmake -B _build -S . \
  -DCMAKE_PREFIX_PATH=/path/to/foo/_build

See the Importing and Exporting Guide for more details.

There can be complications with this approach, of course.

One of the biggest ones is if your internal code uses your header files differently than your downstream library consumers are expected to, and the include directory they get pointed at is only created during the install process.

When installing files, it’s fine if you pull together a bunch of headers scattered all over the src/ tree:

src/types/types.h
src/client/client.h
src/server/server.h
...etc...

…and stick them all together inside a new ${CMAKE_INSTALL_INCLUDEDIR}/foo-1.0/foo/ directory:

${CMAKE_INSTALL_PREFIX}/include/foo-1.0/foo/types.h
${CMAKE_INSTALL_PREFIX}/include/foo-1.0/foo/client.h
${CMAKE_INSTALL_PREFIX}/include/foo-1.0/foo/server.h

But if your exported INTERFACE_INCLUDE_DIRECTORIES is set to ${CMAKE_INSTALL_INCLUDEDIR}/foo-1.0, and library consumers are expected to keep the installed directory name in the header path when #include-ing them:

#include <foo/types.h>
#include <foo/client.h>

…Then you’re going to have a hard time, because that foo-1.0/foo directory doesn’t get created until install time, to be part of your $<INSTALL_INTERFACE:> include paths.

If you’re in that situation, you may have to add code that copies all of the installable headers into the build dir, so that they can be referenced in the include path that gets exported into FooTargets.cmake by export(EXPORT):

# For installed export
set(_headers
  src/types/types.h
  src/client/client.h
  src/server/server.h
)
install(FILES ${_headers}
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/foo-1.0/foo
)
# For build-dir exported interface
file(MAKE_DIRECTORY
  "${CMAKE_BINARY_DIR}/include/foo-1.0/foo"
)
file(COPY ${_headers} DESTINATION
  "${CMAKE_BINARY_DIR}/include/foo-1.0/foo"
)

target_include_directories(foo PUBLIC
  $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include/foo-1.0>
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/foo-1.0>
)

Actually, you should do that copying during the build, using an add_custom_target() scripted COMMAND, e.g.:

add_custom_target(headers ALL
  COMMAND
    ${CMAKE_COMMAND) -E copy_if_different
    ${_headers}
    ${CMAKE_BINARY_DIR}/include/foo-1.0/foo/
  COMMAND_EXPAND_LISTS
)

…The difference being that this version will update the build-dir copy of the headers whenever you build your project, so if a header is modified after initially running cmake the changes will be picked up. With the file(COPY) version any changes made to the header sources after the initial cmake run will be ignored until cmake is manually run again.

Wow, incredible response! What a wealth of information. I’ll see what I can get working using all of this. Many thanks!