Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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

  1. 1

    Leave the kind word out of add_library and turn BUILD_SHARED_LIBS on by default, so the same project can still build a static copy. Add an ALIAS with 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 STATIC or SHARED, BUILD_SHARED_LIBS decides 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. 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) and set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) instead, above add_library. Those variables only set the default for targets created after them, so after Step 1's add_library they 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-only on the .so lists 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_DEFINE as PUBLIC, 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. 3

    Set VERSION to the full version and SOVERSION to 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. 4

    For each program you ship with the library, set INSTALL_RPATH relative to the program's own folder. On Windows, the DLL goes into bin/, next to the programs, instead.

    Example

    set_target_properties(wordtally-count PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
    

    After installing, readelf -d stage/bin/wordtally-count shows a RUNPATH of $ORIGIN/../lib.

    • Linux — $ORIGIN/../lib.
    • macOS — @loader_path/../lib.
    • Windows — nothing to set. Step 6's RUNTIME DESTINATION puts the DLL in bin/.

    Why: cmake --install replaces a program's build-tree search path with INSTALL_RPATH, which is empty unless you set it. A relative path keeps working wherever bin/ and lib/ are copied together.

  5. 5

    Use $<BUILD_INTERFACE:...> for the folders inside this build, and $<INSTALL_INTERFACE:...> for the installed include folder.

    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 include later. An absolute source-folder path in the package would point at your machine.

  6. 6

    Write install(TARGETS ... EXPORT ...) with a destination for each kind of file, then install the headers, including the generated export.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, LIBRARY and ARCHIVE send each one to its conventional folder. EXPORT is what lets Step 7 describe the target.

  7. 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.cmake holds 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 that include().

    Why: find_package reads the config file, and the targets file creates the imported target wordtally::wordtally, with its include folder, definitions and location. SameMajorVersion matches the SONAME rule from Step 3: any 1.x satisfies a request for 1.2, and 2.0 doesn't.

  8. 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/stage
    

    The consumer's CMakeLists.txt needs 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 .exe after 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_PRESET after add_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 SOVERSION to the full version, so a patch release gets a new SONAME and existing programs won't load it.
  • Setting INSTALL_RPATH to an absolute folder that exists only on your machine.
  • Forgetting to install the generated export.h, so users' #include of the public header fails.
  • Leaving EXPORT out of install(TARGETS ...), so there's no export set for install(EXPORT ...) to write.
  • Hard-coding the library's include folder and file in the consumer instead of linking the :: target.