0

I am building a small utility library with c++ and thus far I have pretty much always consumed my libraries in place, i.e. they were always mixed with my actual code that consumes the library which means that header files could be reliably be located.

I am trying to get this cleaned up now by separating my code from the developed library and so I added an install target to my cmake file, this is all working (just for reference, the cmake file is below):

cmake_minimum_required(VERSION 3.16.0)
project(foo VERSION 1.0.0 LANGUAGES CXX)

add_library(${CMAKE_PROJECT_NAME} STATIC)
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR})
add_subdirectory(src)

# install the library
include(GNUInstallDirs)
install(TARGETS ${CMAKE_PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)

And, for reference, within the src directory, there are CMakeLists.txt files for each source file in their own folder that add their relevant code to the target, which looks like this:

target_sources(${CMAKE_PROJECT_NAME} PRIVATE bar.cpp PUBLIC bar.hpp)
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY PUBLIC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/bar.hpp)

My project structure looks something like this:

project_root
└── src
    ├── dir1
    │   └── dir2
    │       └── bar.hpp
    └── dir3
        └── dir4
            └── baz.hpp

Since I have added target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}) to my top level CMakeLists.txt file, I can do include statements in my header files such as

// bar.hpp
#include "src/dir3/dir4/baz.hpp"

Now my problem starts when I am installing the library, all header files are essentially copied into the same directory, i.e. my directory structure for the header files now becomes

${CMAKE_INSTALL_PREFIX}
└── include
    ├── bar.hpp
    └── baz.hpp

This will obviously break the library, as it is still trying to find baz.hpp in bar.hpp using the project's directory structure which was removed during the install step.

My question(s) thus is first more related to library design:

  • What are common ways to organise header files after install? Is it common that the project directory structure is removed and header files now live all next to each other?
  • If the above is true (or a common approach), how would one change the header files to remove the relative path?

I suppose the following answers are related but different (reasons provided in parenthesis):

  • How replace string in a file with value of current directory using CMake (though this assumes that I have a *.hpp.in file in which I have specified cmake variables that are substituted, in my case all header files are actual code, not configuration files and I would like to keep it this way. I suppose this answer could be modified to replace strings in files?)
  • CMake install header files and maintain directory hierarchy (this would work if I wanted to keep the directory structure, though for some reason I feel more gravitated towards having all header files side by side (as I only have about 5 header files, I feel it would be overkill to distribute them over several folders. For larger libraries this may make more sense)
  • Installing C header files for libraries with many headers (well, this is specific for autotools (which I am trying to desparately avoid) and also there is no accepted answer, also this question is more aimed at a manual approach, I would like cmake to do all the heavy lifting for me)
  • Distribution and Linking of C Header Files and Libraries (more of a basic question about libraries and header file distribution in general, my question is focused on cmake. I can easily solve this issue by manually editing files but want cmake to do that for me)

Related to my questions above, whenever I use external dependencies, it is common to include a single header such as #include <foo.hpp> which itself just collects include statements to the other header files such as

// foo.hpp
#include "bar.hpp"
#include "baz.hpp"

Of course I can manually create this file and copy it over as part of the installation process, but this fields wrong, this file ought to be generated by cmake based on the header files added to the project. I guess I am half way there, I am able to extract the header files as

set(HEADERS "")
get_target_property(TARGET_SOURCES ${CMAKE_PROJECT_NAME} SOURCES)
foreach(SOURCE ${TARGET_SOURCES})
    get_filename_component(EXTENSION ${SOURCE} EXT)
    if(EXTENSION STREQUAL ".hpp" OR EXTENSION STREQUAL ".h")
        list(APPEND HEADERS ${SOURCE})
    endif()
endforeach()

though at this point I am not sure if it is not just easier to write a small python utility script which can do all of the above quite well and without hassle or if it is worth bloating up my cmake file and let it handle everything (which feels more correct, but this is where I am at a loss due to lack of experience developing libraries and deploying them. I have looked around libraries I commonly use but they don't seem to enforce good software engineering practices (let's just say there is a lot of file globbing going on and I am pretty sure the cmake files I have looked at glorify the days before modern cmake came along, (you know, the good stuff, setting compiler flags within cmake ...).

tom
  • 361
  • 3
  • 11
  • "What are common ways to organise header files after install? Is it common that the project directory structure is removed and header files now live all next to each other?" - Some projects provides tree-structure of their header files, some other projects provides flat structure. A decision is up to you. "how would one change the header files to remove the relative path?" - Normally, header files are not changed during the installation. That is, in your source tree you need to maintain same structure of the header files as in the install tree. – Tsyvarev Jul 06 '23 at 10:05
  • Despite of detailed description, which is shipped with the code and references to other questions, your question is badly fit for Stack Overflow: it asks **many questions**, and some of them (like installation hierarchy) are **opinion-based**. Your route could be: 1. Choose installation structure of your headers. 2. Inside header files write `#include <>` statements according to the chosen installation hierarchy. 3. In the source tree place the header files and choose include directories for make these `#include <>` working. 4. Specify those directories via `target_include_directories`. – Tsyvarev Jul 06 '23 at 10:18
  • thanks for your comments and thoughts. I figured I just stay with the directory structure, that made my life a lot easier. I'll post a solution that is working for me – tom Jul 07 '23 at 04:19

1 Answers1

0

Figured it out, not 100% satisfied but this is working. I've decided to stick with the answer provided here: CMake install header files and maintain directory hierarchy

This takes care of copying over all header files and retaining the directory structure (I would have preferred a flat directory structure but hey ho ...). For completeness, I have added this to my cmake file

# install the library
include(GNUInstallDirs)
install(TARGETS ${CMAKE_PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  # PUBLIC_HEADER no longer needed
  # PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)

install(DIRECTORY "${CMAKE_SOURCE_DIR}/src/"
  DESTINATION "include/${PROJECT_NAME}/src"
  FILES_MATCHING
  PATTERN "*.hpp"
  PATTERN "*.tpp"
)

Then, for the second part, I loop over all *.hpp files, remove the ${PROJECT_SOURCE_DIR} from the header's path and add it to a list, which I then just dump to a file:

set(HEADERS "// top level include file for ${PROJECT_NAME}\n")
get_target_property(TARGET_SOURCES ${CMAKE_PROJECT_NAME} SOURCES)
foreach(SOURCE ${TARGET_SOURCES})
  get_filename_component(EXTENSION ${SOURCE} EXT)
  if(EXTENSION STREQUAL ".hpp")
    string(REPLACE ${PROJECT_SOURCE_DIR}/ "" RELATIVE_SOURCE ${SOURCE})
    list(APPEND HEADERS "#include \"${RELATIVE_SOURCE}\"\n")
  endif()
endforeach()

set(HEADER_MASTER ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.hpp)
file(WRITE ${HEADER_MASTER} ${HEADERS})

Finally, I simply copy this file during install

install(FILES ${HEADER_MASTER}
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)

This works, but there may be a cleaner version of this. If so, I am all ears!

tom
  • 361
  • 3
  • 11