[CMake] Building and exporting static and shared libs - best practice?

Rich T rsjtaylor.github at gmail.com
Thu Feb 22 13:07:53 EST 2018


It's often useful to be able to make both static and shared varients of a
library available to users of a package.

My goal is to make the project cmake files as clean as possible, while
supporting usage of either or both types of library by users. It should
work from both build and installed locations, in a reasonably intuitive way.

I've tried a few methods so far, but none feel completely satisfactory. I
was wondering what was currently thought of as best practice with recent
versions of CMake?

1: Don't specify library type

add_library(myLib mySource.cpp)

The type of library is then controlled by the value of BUILD_SHARED_LIBS
This is nice:
* No duplication. Things common to both types are only specified once,
without resorting to custom variables.
* By default you only build one sort of library, so no build overhead.
* Type-dependent logic clearly presented by branching on a single, standard
variable

e.g.
if(NOT BUILD_SHARED_LIBS)
  target_compile_definitions(myLib PUBLIC MYLIB_STATIC_BUILD)
endif()

Using this sort of project to build an installation package that contains
both types of target can be accomplished with a little care.
You build the project twice in different directories, once with
BUILD_SHARED_LIBS and once without, but install to the same prefix.
* Namespace at the point of export based on type (MyPackage::SHARED::myLib
or MyPackage::STATIC::myLib)
* Export to a type-specific config (myLib_exports_static.cmake
myLib_exports_shared.config)
* Have your package config check for and include the exported static and
shared config files using if(EXISTS)
* Some MSVC specific code to prevent shared import lib and static lib
output names conflicting
* On cmake 3.11+, alias one flavour to MyPackage::myLib based on some user
variable for convenience

This all works well, the problem comes with registering a build tree in the
user repository.

export(PACKAGE myPackage)

In a downstream project, only one build location will be arbitrarily chosen
from the user registry.
While you could manually choose either the static or shared tree by setting
myPackage_DIR, you can't have both, at least not from the build tree. Even
if a user only wants one, the fact they might get the 'wrong' version by
default is likely to confuse.

Setting MYLIB_FOUND to FALSE in a package config aborts the search process
rather than continuing to search for other possible configs, else you could
do partial loading from two different build trees in much the same way that
you can merge installations (or search for a shared vs static based on a
user variable)

I've tried calling include_subdirectory(myLibDir) twice in a project, both
before and after setting BUILD_SHARED_LIBS.
This requires you to change the target name as duplicates are not allowed
(for obvious reasons). A variable target name requires a custom variable
anywhere your target is referenced, which is error prone. Also feels a bit
of an abuse of the variable.

You could export to two different packages, but having to do
find_package(MyLibShared) or find_package(MyLibStatic) feels a bit
counterintuitive from the installed perspective if both versions are
distributed together.

2: Create two separate targets, one for each type

One common approach is to assign sources and other build ingredients to a
custom variable, then call any target-modifying functions once for each
target, passing this variable.

set(mySources mySource.cpp)
add_target(myLib_static STATIC ${mySources})
add_target(myLib_shared SHARED ${mySources})

This can be difficult to debug as mistakes are often distant from the
location at which an error is caught. It's also a lot more verbose as every
target method is duplicated.

I think the need for a variable can be mitigated in some circumstances by
using generator expressions

e.g.

target_include_directories(myLib_shared
  PRIVATE $<TARGET_PROPERTY,myLib_static,INCLUDE_DIRECTORIES>
  INTERFACE $<TARGET_PROPERTY,myLib_static,INTERFACE_INCLUDE_DIRECTORIES>)

However, this only works for properties that are identical between
variants, necessitates always building the first target and is even more
verbose that using custom variables.

So from the upstream point of view, the first method is the clear winner,
but its restrictions when importing targets from the build tree can make it
less convenient for downstream users, who may be working on projects that
take the two target approach.

So what's considered the best option at the moment? Am I missing something
obvious?

Thanks,

Rich
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://cmake.org/pipermail/cmake/attachments/20180222/0a3e0e32/attachment.html>


More information about the CMake mailing list