CPack: distributing both shared and static libraries

Before switching to CMake we were able to distribute tarballs containing our library as both a static library and a shared library (at least on Linux and macOS). What is the preferred way to do this with CMake 3.16+?

(Check Section 26.3: Multi Configuration Packages of my book). If you are using CMake 3.16 or later, definitely look at using the cpack -C command line option. CMake 3.16 or later allows you to pass multiple configurations to this option. You are responsible for ensuring that each configuration has already been built when you run cpack. This option only really makes sense if you’re using a multi-config generator, but since there’s now the Ninja Multi-Config generator (since CMake 3.17), this is possible on all major platforms. It can be done with earlier CMake versions and/or single-config generators, but it is much more involved and much less intuitive.

So this would entail creating custom configurations? Like “RelWithDebInfoStatic”, for example? And would our users be able to pick which to use when they run find_package somehow?

No it shouldn’t add any additional requirements unless you want builds that provide both static and shared versions of a target. That isn’t going to work out so good anyway because one target cannot have two different representations (static and shared). You would need to have two different targets in that case. One approach I’ve seen people suggest is to create blahStatic and blahShared targets, then define a blah interface target that links to one of those based on the value of BUILD_SHARED_LIBS or some similar option. Consumers should then only link against blah and the blahStatic and blahShared libraries are somewhat of an internal implementation detail. I say “somewhat” because it would still probably impact consumers in terms of things they might need to add to their own packages, depending on how they are incorporating your library.

Well, I would like to provide both, but mutually exclusively is fine. Maybe like a variable that’s set ahead of find_package(Halide) (in my case) that has it load the appropriate set of exports.

You might want to see how HDF5 did it, not perfect solution but usuable.

Allen

Allen - it looks like HDF5 uses separate packages for static and shared? I was specifically asking how to avoid that. Am I missing something?

No, it packages both into a single install. Only the builds are separated.
In the case of tools, there are static () and shared (-shared).
Libraries are static (lib.lib) and shared (.lib)

Not perfect, but workable. And the find_package can request either or both.

Allen

I still haven’t found a satisfactory solution to this, but this is what I’m planning and I’m interested to hear if anyone has any other perspectives.

@Allen – I looked into what HDF5 does, and I mostly like the structure.
@craig.scott – I’m still unsure how to use cpack -C to combine, say, two Debug builds, one with BUILD_SHARED_LIBS set to YES and the other to NO.

So here are my collected notes:

  1. When packaging for a Linux distribution / homebrew / something else, it is not acceptable to have separate packages for shared and static. They have to be packaged together. No one does sudo apt install libpng-dev-shared libpng-dev-static. It’s just libpng-dev.
  2. There are two ways to have separate shared/static targets:
    a. Via a common object library. This is a no-go, not only because object libraries never behave how you expect (they require export/install afaik) but because it requires compiling all the objects with -fPIC. This adds overhead that defeats the purpose of static libraries for many consumers.
    b. Sources listed twice. This fixes the PIC issue and is correct.
  3. (2b) has the following drawbacks:
    a. Both targets are always built, even if only one is required.
    b. The MyLib::MyLib alias must be consistently used within the MyLib project and must conditionally point to one or the other target, depending on the value of BUILD_SHARED_LIBS.
  4. The user experience should be consistent between find_package via vcpkg or another package manager, and add_subdirectory / FetchContent.
  5. (3a) and (4) imply that it is better to have a single target and multiple build directories.
  6. (4) requires that MyLib::MyLib's type depends on BUILD_SHARED_LIBS.
  7. Controlling find_package via BUILD_SHARED_LIBS is awkward (have to set and reset) and unexpected. There are a few workarounds:
    a. (Ab)use the components system – The user may (must?) specify either “shared” or “static” as a component, which directly determines the type.
    b. Read a “namespaced” (cache?) variable – The value of MyLib_SHARED_LIBS determines the type. This has the advantage of being command-line configurable. The right way to do this might be option + mark_as_advanced.
    c. All of the above. Component > MyLib_SHARED_LIBS > BUILD_SHARED_LIBS. If none are present, a default is chosen or an error is issued.
  8. Regardless, the active configuration (ie. Debug or Release) should be respected. This means that a complete package combines four builds.
  9. Regarding (8), cpack -C "Debug;Release" only takes me so far. Unless I’m missing something, it only helps produce a multi-config package for a single BUILD_SHARED_LIBS setting. If I want it to additionally package both static/shared, I have to compromise on something above.

The workflow I would like is to build Debug and Release via Ninja Multi-Config twice: once into a “shared” build tree and again into a “static” build tree, and then combine all four of them somehow.

It turns out it’s not so scary to merge a few build trees with CPack. Just create a working directory for several build trees that looks like:

build/
|- shared-Debug/
|- shared-Release/
|- static-Debug/
`- static-Release/

Then configure and build your project in each one, changing only CMAKE_BUILD_TYPE and BUILD_SHARED_LIBS. Write a script titled package.cmake, which contains:

include("shared-Release/CPackConfig.cmake")

set(CPACK_INSTALL_CMAKE_PROJECTS
    static-Debug   <PROJ> ALL /
    shared-Debug   <PROJ> ALL /
    static-Release <PROJ> ALL /
    shared-Release <PROJ> ALL /
)

Then a simple cpack --config /path/to/package.cmake from the build directory suffices. Note that this requires a single-config generator like Ninja. Make sure that you generate different targets export files for each of static and shared, then write the appropriate loading logic in your package configuration script. There, you can choose to use components, a variable, a fallback mechanism, or whatever to load the appropriate export script.