Using CMake for framework with main() in dynamic library

I’m building a framework that needs to use CUDA internally, which is currently annoyingly complicated to use with Bazel, which is the build system I’m very familiar with. For this reason as well as wider compatibility, I’m planning on using CMake for the library. This is actually my first project that I’m using CMake for, so I’m somewhat confused with how to use it given the constraints that I have as well as how to set up everything in an idiomatic fashion.

Some of the things I’d like to do:

  1. The framework is itself a shared library that is dynamically linked by the user.
  2. I don’t want to maintain a separate public header, so I’d like to use the same main headers for both the external users as well as the code in the internal library.
  3. The main() function is contained in the shared object and calls back into client code, i.e. the linker needs to be told that it’s fine to have unresolved symbols in the dynamic library.

To be more concrete, here’s the most bare bones structure that I can come up with, which could be used as a template for the build system I need:

shared library (i.e. my project):
foo/foo.h
foo/internal/log.h
foo/foo.cc

client application:
bar/bar.cc

Contents of files as follows:

foo/foo.h:

#pragma once

#include <iostream>
#include <string>

#ifdef FOO_SHARED_LIB
#include "internal/log.h"
#else
static void LogClient(std::string msg) {
  std::cout << "Client: " << msg << std::endl;
}
#define LOG(msg) LogClient(msg)
#endif

Here the point is that the folder named “internal” should not actually have its contents copied with the installation (later I would want to figure out how to run tools like “unifdef” on the code to strip out the internal branch).

foo/foo.cc:

#include "foo.h"

extern void PrintHello();

int main() {
  LOG("Printing hello");
  PrintHello();
  return 0;
}

foo/internal/log.h:

#pragma once

#include <iostream>
#include <string>

static void LogInternal(std::string msg) {
  std::cout << "Library: " << msg << std::endl;
}

#define LOG(msg) LogInternal(msg)

bar/bar.cc:

#include <foo/foo.h>

void PrintHello() {
  LOG("hello");
}

To compile this manually to what I want, I would do:

clang++ -shared -fpic foo/foo.cc -DFOO_SHARED_LIB -o bin/libfoo.so
clang++ bar/bar.cc -o bin/bar -lfoo -I. -Lbin/
LD_LIBRARY_PATH="bin/" bin/bar

and running it would give me the output:

$ LD_LIBRARY_PATH="bin/" bin/bar 
Library: Printing hello
Client: hello

There are a couple of things here regarding code organization:

  1. I’d like to header to be next to the code and not in a specific “include” folder. This means I would need to somehow specify the headers that I want installed and where to put them, i.e . “foo/foo.h” should be discoverable to library users as “#include <foo/foo.h>”
  2. Certain headers like the ones under “internal/” should not be installed.
  3. When building the library itself, the “FOO_SHARED_LIB” preprocessor define should be set.

I’m trying to piece together a cmake file for this that follows modern idioms. Some of the points like allowing unresolved symbols in the linked shared library made me trip up with trying to produce anything that works on MacOS X. I only managed to put together something working by manually using “non-modern” cmake commands that I found in some old tutorials.

I assume that an expert on cmake could whip together working templates for the above bare bones project in no time, so I could continue from there.

I watched the following video on CMake

and created a simple CMakeLists.txt based on the advice:

cmake_minimum_required(VERSION 3.15)

project(Foo
  VERSION 0.1
  DESCRIPTION "Foo project"
  LANGUAGES CXX
)

###############################################################################
# Define library.
###############################################################################

add_library(Foo SHARED
  src/foo.cc
)
add_library(Foo::Foo ALIAS Foo)

###############################################################################
# Library dependencies.
###############################################################################

if (APPLE)
  set_target_properties(Foo PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif()

include(FetchContent)
FetchContent_Declare(
  absl
  GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
  GIT_TAG 20190808
)
FetchContent_MakeAvailable(absl)

target_link_libraries(Foo
  PRIVATE
    absl::strings
)
###############################################################################
# Library target options.
###############################################################################

target_compile_definitions(Foo
  PRIVATE
    FOO_SHARED_LIB=1
)

target_compile_options(Foo
  PRIVATE
    -Wall
)

target_compile_features(Foo
  PUBLIC
    cxx_std_17
)

target_include_directories(Foo
  PUBLIC
    $<INSTALL_INTERFACE:include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
  PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)

###############################################################################
# Library installation.
###############################################################################

install(TARGETS Foo EXPORT FooTargets
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES DESTINATION include
)

install(EXPORT FooTargets
  FILE FooTargets.cmake
  NAMESPACE Foo::
  DESTINATION lib/cmake/Foo
)

Now if I do:

mkdir build; cd build
cmake ..
make && sudo make install

I see the following:

Install the project…
– Install configuration: “”
– Installing: /usr/local/lib/libFoo.dylib
– Old export file “/usr/local/lib/cmake/Foo/FooTargets.cmake” will be replaced. Removing files [/usr/local/lib/cmake/Foo/FooTargets-noconfig.cmake].
– Installing: /usr/local/lib/cmake/Foo/FooTargets.cmake
– Installing: /usr/local/lib/cmake/Foo/FooTargets-noconfig.cmake

For my test application, I made the following CMakeLists.txt file:

cmake_minimum_required(VERSION 3.15)

project(Bar
  VERSION 0.1
  DESCRIPTION "Bar application"
  LANGUAGES CXX
)

include(GNUInstallDirs)

add_executable(Bar
  src/bar.cc
)

target_compile_features(Bar PUBLIC cxx_std_17)

target_include_directories(Bar
  PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)

find_package(Foo REQUIRED)
target_link_libraries(Bar PRIVATE Foo::Foo)

Now trying to compile the project, I get:

$ cmake ..
-- The CXX compiler identification is AppleClang 11.0.0.11000033
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:22 (find_package):
  By not providing "FindFoo.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "Foo", but
  CMake did not find one.

  Could not find a package configuration file provided by "Foo" with any of
  the following names:

    FooConfig.cmake
    foo-config.cmake

  Add the installation prefix of "Foo" to CMAKE_PREFIX_PATH or set "Foo_DIR"
  to a directory containing one of the above files.  If "Foo" provides a
  separate development package or SDK, be sure it has been installed.

In other words, my problems are the following:

  1. No headers were installed. I also don’t want to pollute /usr/local/include with my headers, I want headers installed under /usr/local/include/foo/

  2. I would like to manually specify which headers should be installed (some of them are internal to the library).

  3. The project “Foo” never created a FooConfig.cmake, but only a FooTargets.cmake. It seems like this file is not being searched for at all. I would have also assumed that CMake would by default look into /usr/local/lib/cmake

It’s easy to hack something to the above files to make them work, but I would like to fix them in the right way. What’s the right way to make things work?

For installing headers, see the install(FILES) command.

For the package configuration file (FooConfig.cmake), see the cmake-packages(7) manual.