Struggling to export a project

I hate starting a topic that is so similar to so many others, but I haven’t found a clear explanation of how to get from where I am to where I want to be. I’ve read the Importing and Exporting Guide and all the questions here that were even closely related, and I’m still lost.

I’m currently trying to take some “utility” stuff that I use in every (business, not CMake) project, an package it for general use in the company. Currently, this includes 3 directories, each of which builds a library; some are shared, some are static.

Because I hate copy/pasta, I defined a macro to declare a library such that the specific library really only needs to define its sources (but it’s incomplete). This macro looks like this:

macro( DeclLib target linkage )
  
  set(myTarget  ${target})
  set(myDepend  ${target})
  set(myTest    ${target}UnitTest)
  set(myLinkage ${linkage})

  add_library( ${myTarget} ${linkage} )
  cmake_path(GET CMAKE_CURRENT_SOURCE_DIR PARENT_PATH target_inc_dir)
  message(INFO " for target=${target}"
  	       " linkage=${linkage}"
	       " source=${CMAKE_CURRENT_SOURCE_DIR}"
	       " inc=${target_inc_dir}"
	       )

  target_include_directories( ${myTarget} PUBLIC
  			      "$<BUILD_INTERFACE:${target_inc_dir}>"
			      "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
			      )
  file(GLOB target_inc_files
       *.hh *.h *.hpp *.cuh)
### THIS IS PROBLEMATIC ... what is the point of FILE_SET exactly?
#  target_sources( ${myTarget} INTERFACE
#  		  FILE_SET inc
#		  	   TYPE HEADERS
#			   BASE_DIRS ${target_inc_dir}
#  )

  install(FILES ${target_inc_files}
          DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
  )

  install(TARGETS ${myTarget}
          EXPORT foo
	  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
	  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
	  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
	  INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
	  )
endmacro()

If I uncomment out the FILE_SET, I get an error:

  install TARGETS target XXX is exported but not all of its interface file
  sets are installed

This is on the install(TARGETS...) line of the macro, called from each library.

Rather than fight with that, I left it commented out.

After all the subdirs are included, I have:

install(EXPORT      foo
        FILE        foo.cmake
        NAMESPACE   foo::
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/foo
  )

Now, I have an external dependency issue. One of my libraries provides some wrapper around GoogleTest to make it more suitable for our use. This CMakeLists.txt file includes:

DeclLib(gtestApp SHARED)

# build the library
target_sources( ${myTarget}
PRIVATE
  ${target_sources}
)
        
target_include_directories( ${myTarget}
PRIVATE
  # this was required or it wouldn't find the "util" include path
  $<TARGET_PROPERTY:util,INTERFACE_INCLUDE_DIRECTORIES>
)

target_link_libraries( ${myTarget} INTERFACE
  util
  gtest
)

(GoogleTest is brought in by FetchContent in the top-level CMakeLists.txt)

This results in an error:

CMake Error: install(EXPORT "foo" ...) includes target "gtestApp" which requires target "gtest" that is not in any export set.

I don’t want to export gtest; I just want to depend on it. I feel like I’m in a maze of twisty passages, all alike. I’m likely to be eaten by a grue.

First, am I even going in the right direction? Is there a better way to make this available to other projects? My thought is that I’d like for this package to be available using FetchContent like any other third party thing.

Do not use INTERFACE for libraries, use either PUBLIC or PRIVATE!

find_package(GTest REQUIRED)

include(GNUInstallDirs)

macro(DeclLib target linkage)
  set(myTarget ${target})
  set(myDepend ${target})
  set(myTest ${target}UnitTest) # unused? CK
  set(myLinkage ${linkage})
  set(myFileSet ${target}FileSet)
  set(myExportTargets ${target}Targets)

  add_library(${myTarget} ${linkage})
  cmake_path(GET CMAKE_CURRENT_SOURCE_DIR PARENT_PATH target_inc_dir)

  # NOTE: not needed, done by FILE_SET HEADERS
  # target_include_directories(
  #   ${myTarget} PUBLIC "$<BUILD_INTERFACE:${target_inc_dir}>"
  #          "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
  # )

  file(GLOB
       target_inc_files
       *.hh
       *.h
       *.hpp
       *.cuh
  )

  message(STATUS
          "target=${target}\n"
          "   linkage=${linkage}\n"
          "   source=${CMAKE_CURRENT_SOURCE_DIR}\n"
          "   include=${target_inc_dir}\n"
          "   header=${target_inc_files}"
  )

  # cmake-format: off
  target_sources(
    ${myTarget}
    PUBLIC FILE_SET ${myFileSet}
           TYPE HEADERS
           BASE_DIRS ${target_inc_dir}
           FILES ${target_inc_files}
  )
  # cmake-format: on

 # NO! install(FILES ${target_inc_files} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})

  install(TARGETS ${myTarget}
          EXPORT ${myExportTargets}
          LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
          ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
          RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
          # NOT used? INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
          FILE_SET ${myFileSet} # HEADERS
  )
endmacro()

# build the library
DeclLib(util SHARED)
set(target_sources util.cpp)
target_sources(${myTarget} PRIVATE ${target_sources})
target_link_libraries(${myTarget} PUBLIC GTest::gtest)

but this is not all to export your cmake config package in a usable way!

see a working example cmake/install-rules.cmake

Thanks for the detailed response. I have some questions.

Can you explain this? In particular, when I have a static library, and users of my library require another static library, isn’t that precisely what INTERFACE is for?

I brought in GTest with FetchContent. Although the GTest::gtest alias target is created, find_package didn’t work. I get this error:

  Could NOT find GTest (missing: GTEST_LIBRARY GTEST_INCLUDE_DIR
  GTEST_MAIN_LIBRARY)

Here’s how I bring in GTest:

# IMPORTANT: don't inhibit install or CMake will puke
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        f8d7d77c06936315286eb55f8de22cd23c188571 # v1.14.0
  FIND_PACKAGE_ARGS NAMES GTest
)
# The first form is supposed to work, but it doesn't
set(googletest_INSTALL_GTEST ON CACHE INTERNAL "")
set(googletest_BUILD_GMOCK OFF CACHE INTERNAL "")

set(INSTALL_GTEST ON CACHE INTERNAL "")
set(BUILD_GMOCK OFF CACHE INTERNAL "")

FetchContent_MakeAvailable(googletest)

if (NOT TARGET GTest::gtest)
  message(FATAL_ERROR " -- no gtest!")
endif()

Since GTest is available in the context of my project, I just removed the find_package() command. Am I masking another error?

I’m assuming you were trying to infer my intent. Actually, this project has multiple libraries, and I wanted them all installed together. For simplicity, I wrote it as EXPORT foo, since I want all libraries included in the same export. Or is your change significant?

This turned out to be problematic, since GTest was static (and not necessarily PIC). It was easier to make my library static as well. Other than that, what I ended up with is largely like what you wrote. And it all builds. I can even do a make install and everything installs properly (surprisingly, it also installs GTest; I fought trying to stop it, but decided it wasn’t worth my time).

NOW…

I’m trying to use this repo in another project via FetchContent. When it builds, I get the same errors I used to get:

CMake Error: install(EXPORT "foo" ...) includes target "util" which requires target "gtest" that is not in any export set.

I do it this way:

include(FetchContent)

# add the google-test unit testing framework
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        f8d7d77c06936315286eb55f8de22cd23c188571 # v1.14.0
  FIND_PACKAGE_ARGS NAMES GTest
)

FetchContent_Declare(
  foo
  GIT_REPOSITORY git@github.com:my-proj/foo.git
  GIT_TAG        main
)

FetchContent_MakeAvailable(googletest)
FetchContent_MakeAvailable(foo)

(my project is in an Enterprise GitHub, so not publicly visible).

I’ve found FetchContent very helpful to use, but I’m failing in providing.

I read this and it kind of terrified me. It seems like there is a lot of boilerplate in that file that I might have hoped would be automated. I’m left still wondering if there might be a simpler way to do what I want. My goal is to provide a “package” of helpful code for our other projects. I can do the heavy lifting if there is no other way to get there, but I worry who will be able to maintain it when I’m gone.

IMHO you mix to match things:

  • You should not use a macro to build a library
  • Do not mix building, testing, installing this way.
  • You must not export a test dependency gtest, sanitizer, clang-tidy, … flags.
  • FetchContents may be helpful, but if your library links to the fetched lib, you have to install and export it too.

If you use find_package() to import your dependencies, things are much simpler.
They are already installed :grinning:

But if it is a shared library, your exported package must check that is still there.
So you need to use find_dependency()

You will find many guides and examples with different solutions for the same problem. Not all of them are good or up to date.

I would start reading here C++20 Modules, CMake, And Shared Libraries

This is all generated with cmake-init :cowboy_hat_face:

Can you explain why? I am pathological in my commitment to DRY (Don’t Repeat Yourself). The projects I work on are large, with many libraries and executables. A macro seems tailor made to that end. Why is it better to repeat the same boilerplate lines of code in myriad CMakeLists.txt files?

I watched CppCon 2019: Deep CMake For Library Authors - Crascit and learned a couple of things, but nothing relevant to the issue at hand.

I looked at cmake-init. The promise of creating FetchContent-ready projects is appealing. But, as it says it is “an opinionated CMake project initializer”. I haven’t quite comprehended its opinions.

My code layout is similar to this one.

In the end, what ultimately solved my issue was to remove GTest::gtest from the target_link_libraries and just add the include paths like this:

if(TARGET GTest::gtest)
  get_target_property(gtest_includes GTest::gtest INCLUDE_DIRECTORIES)
  target_include_directories( ${myTarget} SYSTEM PRIVATE ${gtest_includes})
else()
  message(FATAL_ERROR "GoogleTest must be present to build")
endif()

I included them as private because I didn’t want users of this project via FetchContent to inherit those paths. This stopped the complaining about not installing GTest.

1 Like