Building and exporting static and shared libs - best practice?

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Building and exporting static and shared libs - best practice?

Rich T
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



--

Powered by www.kitware.com

Please keep messages on-topic and check the CMake FAQ at: http://www.cmake.org/Wiki/CMake_FAQ

Kitware offers various services to support the CMake community. For more information on each offering, please visit:

CMake Support: http://cmake.org/cmake/help/support.html
CMake Consulting: http://cmake.org/cmake/help/consulting.html
CMake Training Courses: http://cmake.org/cmake/help/training.html

Visit other Kitware open-source projects at http://www.kitware.com/opensource/opensource.html

Follow this link to subscribe/unsubscribe:
https://cmake.org/mailman/listinfo/cmake