Recipe
Ship a shared library for other projects to use
Use when: you've written a library, and a separate project, built on another day, maybe on another machine, should use it with one find_package line and one target_link_libraries line
Goal: an installed folder, with bin, lib, include and a CMake package, that another project finds by name and links as one :: target, and whose programs still load the library after the folder is moved
Taught in install(), export(), and find_package
Quick recipe
- Build it as a shared library, with a
::alias - Hide everything, then mark the public interface
- Version the file and its SONAME
- Give installed programs a relative search path
- Give the include folders a build side and an install side
- Install the files, and record the target in an export set
- Write the package's label: targets, version and config files
- Install into a test folder and use it from a separate project
-
1
Leave the kind word out of
add_libraryand turnBUILD_SHARED_LIBSon by default, so the same project can still build a static copy. Add anALIASwith the namespaced name users will write.Example
option(BUILD_SHARED_LIBS "Build wordtally as a shared library" ON) add_library(wordtally src/word_tally.cpp src/c_api.cpp) add_library(wordtally::wordtally ALIAS wordtally)Why: with no
STATICorSHARED,BUILD_SHARED_LIBSdecides the kind. It's a global setting, so it also makes any library you add with FetchContent (Chapter 24 §2) shared. The alias means code in this build and code using the installed package spell the target the same way. -
2
Make hidden the default for the library on every compiler, have CMake write the export header, and put its macro on each public class and function.
Example
set_target_properties(wordtally PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON) include(GenerateExportHeader) generate_export_header(wordtally BASE_NAME wordtally EXPORT_FILE_NAME ${CMAKE_CURRENT_BINARY_DIR}/include/wordtally/export.h) if(NOT BUILD_SHARED_LIBS) target_compile_definitions(wordtally PUBLIC WORDTALLY_STATIC_DEFINE) endif()The lab writes
set(CMAKE_CXX_VISIBILITY_PRESET hidden)andset(CMAKE_VISIBILITY_INLINES_HIDDEN ON)instead, aboveadd_library. Those variables only set the default for targets created after them, so after Step 1'sadd_librarythey would do nothing. The target properties above work wherever you put them.In the public header, after
#include "wordtally/export.h":class WORDTALLY_EXPORT WordTally { ... };. To check on Linux,nm -DC --defined-onlyon the.solists the functions you marked, and not the unmarked helpers.- the library is built shared — the macro means export while building and import while using. Nothing else to do.
- the library is built static — define
WORDTALLY_STATIC_DEFINEasPUBLIC, so the library and its users both see an empty macro.
Why: Windows exports nothing unless it's marked, and hiding by default makes Linux and macOS behave the same way. A forgotten mark then fails on your own machine, not on a user's.
-
3
Set
VERSIONto the full version andSOVERSIONto the major version only.Example
set_target_properties(wordtally PROPERTIES VERSION ${PROJECT_VERSION} # libwordtally.so.1.2.0 SOVERSION ${PROJECT_VERSION_MAJOR}) # SONAME libwordtally.so.1- the release keeps the ABI (a new function, a bug fix) — raise the minor or patch number. The SONAME stays the same.
- the release breaks the ABI (a changed class layout, a removed function) — raise the major number, which gives a new SONAME.
Why: programs record the SONAME. A new file with the same SONAME replaces the old one for every program. A new SONAME installs beside the old one, so no program loads a library it wasn't built for.
-
4
For each program you ship with the library, set
INSTALL_RPATHrelative to the program's own folder. On Windows, the DLL goes intobin/, next to the programs, instead.Example
set_target_properties(wordtally-count PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")After installing,
readelf -d stage/bin/wordtally-countshows aRUNPATHof$ORIGIN/../lib.- Linux —
$ORIGIN/../lib. - macOS —
@loader_path/../lib. - Windows — nothing to set. Step 6's
RUNTIME DESTINATIONputs the DLL inbin/.
Why:
cmake --installreplaces a program's build-tree search path withINSTALL_RPATH, which is empty unless you set it. A relative path keeps working whereverbin/andlib/are copied together. - Linux —
-
5
Use
$<BUILD_INTERFACE:...>for the folders inside this build, and$<INSTALL_INTERFACE:...>for the installedincludefolder.Example
include(GNUInstallDirs) target_include_directories(wordtally PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include> # export.h $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)Why: the headers live in your source and build folders now, and in the prefix's
includelater. An absolute source-folder path in the package would point at your machine. -
6
Write
install(TARGETS ... EXPORT ...)with a destination for each kind of file, then install the headers, including the generatedexport.h.Example
install(TARGETS wordtally EXPORT WordTallyTargets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # the .dll LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # the .so or .dylib ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) # a .a/.lib, or the import .lib install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/wordtally/export.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/wordtally)Why: each operating system splits a library into different files, and
RUNTIME,LIBRARYandARCHIVEsend each one to its conventional folder.EXPORTis what lets Step 7 describe the target. -
7
Turn the export set into a targets file with a namespace, generate a version file, and install both beside a small hand-written config file.
Example
include(CMakePackageConfigHelpers) set(config_dir ${CMAKE_INSTALL_LIBDIR}/cmake/WordTally) install(EXPORT WordTallyTargets NAMESPACE wordtally:: DESTINATION ${config_dir}) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/WordTallyConfigVersion.cmake COMPATIBILITY SameMajorVersion) install(FILES cmake/WordTallyConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/WordTallyConfigVersion.cmake DESTINATION ${config_dir})cmake/WordTallyConfig.cmakeholds one line:include("${CMAKE_CURRENT_LIST_DIR}/WordTallyTargets.cmake").- the library needs no other packages — the config file is one
include()of the targets file. - it needs other packages — call
find_dependency()for each one in the config file, before thatinclude().
Why:
find_packagereads the config file, and the targets file creates the imported targetwordtally::wordtally, with its include folder, definitions and location.SameMajorVersionmatches the SONAME rule from Step 3: any 1.x satisfies a request for 1.2, and 2.0 doesn't. - the library needs no other packages — the config file is one
-
8
Install to a prefix you choose, then configure a separate project that finds the package there.
Example
cmake --install build --prefix stage cmake -S consumer -B consumer-build -DCMAKE_PREFIX_PATH=$PWD/stageThe consumer's
CMakeLists.txtneeds only:find_package(WordTally 1.2 CONFIG REQUIRED) target_link_libraries(headline PRIVATE wordtally::wordtally)- Linux or macOS — run the installed program straight from the prefix. Its rpath finds the library.
- Windows — the consumer copies the DLL next to its
.exeafter linking, with$<TARGET_RUNTIME_DLLS:headline>.
Why: a round trip through a real install is the only test that checks the label as well as the files. A library that works inside its own build can still ship a broken package.
Common mistakes
- Setting
CMAKE_CXX_VISIBILITY_PRESETafteradd_library. The variable only affects targets created later, so the library still exports everything. - Putting an absolute source-folder path in
target_include_directories, so the installed package points at the author's machine. - Setting
SOVERSIONto the full version, so a patch release gets a new SONAME and existing programs won't load it. - Setting
INSTALL_RPATHto an absolute folder that exists only on your machine. - Forgetting to install the generated
export.h, so users'#includeof the public header fails. - Leaving
EXPORTout ofinstall(TARGETS ...), so there's no export set forinstall(EXPORT ...)to write. - Hard-coding the library's include folder and file in the consumer instead of linking the
::target.