CMake project review and suggestions (ongoing)

I’m working on my first CMake project, a C library.
I’ll ask for help and suggestions as I make progress (like an “art review”, but for CMake)
I’ve read plenty of material on the subject (documentation included, of course, it’s pretty good), but nevertheless I keep discovering better more modern alternatives everywhere, or I can’t find material at all, so I came to this forum to build a solid understanding of what I need (and why I need it) and very useful “how I would have done that, and why” from experts.

The project layout I came up with is the following:

visualt
│   CMakeLists.txt (1)
│   Doxyfile
│   Doxyfile.in // info such as current version are provided by CMake
│
├───src
│       CMakeLists.txt (2)
│       version.h // i found very ugly to put the generated header in the build directory, i'd rather put it in the same directory, and use it normally. i don't get why should it be in other places.
│       version.h.in // info such as version and build date are provided by CMake
│       visualt.c
│
├───include
│   └───visualt
│           visualt.h
│           visualtUnprefixed.h // alternative header, a wrapper around "visualt.h"
│
├───res // diagnostic and assets for development
│       CMakeLists.txt (3) // target for test.c
│       test.c // not a "test" in the classical meaning, it's just a diagnostic program
│       ...
│
└───examples // small programs that uses the library (with it's own "target_link_libraries()")
    │   CMakeLists.txt (4) // adds every subdirectory
    │
    ├───01
    │   │   CMakeLists.txt (6) // target for example01.c
    │   │   example01 // the target's RUNTIME_OUTPUT_DIRECTORY is set as CMAKE_CURRENT_LIST_DIR, because the executable might require files like "cat.obj"
    │   │   example01.c
    │   │
    │   └───res
    │           cat.obj
    │           cat.txt
    │
    ├───... // the same folder structure is repeated for every example
    │
    └───getch
            CMakeLists.txt (5) // an INTERFACE library (header only) used by examples
            getch.h

I’m stuck on: Installing

Here’s CMakeLists.txt (2), still incomplete:

# set headers
set(private_headers_path "${VisualT_SOURCE_DIR}/src")
set(public_headers_path "${VisualT_SOURCE_DIR}/include")

set(private_headers
    "${private_headers_path}/version.h")
set(public_headers
    "${public_headers_path}/visualt/visualt.h"
    "${public_headers_path}/visualt/visualtUnprefixed.h")

# set source files
set(srcs "visualt.c")

add_library(visualt ${private_headers} ${public_headers} ${srcs})
target_include_directories(visualt
                           PRIVATE ${private_headers_path}
                           PUBLIC ${public_headers_path})
set_target_properties(visualt PROPERTIES
                      PRIVATE_HEADER "${private_headers}"
                      PUBLIC_HEADER "${public_headers}")
target_compile_features(visualt PUBLIC c_std_99)

here’s CMakeLists.txt (1):

# Works with 3.11 and tested through 3.16
cmake_minimum_required(VERSION 3.11...3.16)

# Project name and a few useful settings. Other commands can pick up the results
project(VisualT
        VERSION 3.2.0
        DESCRIPTION "a text-based graphic library"
        LANGUAGES C)

if("${CMAKE_PROJECT_NAME}" STREQUAL "VisualT")
    string(TIMESTAMP VisualT_BUILD_DATE "%d %B %Y" UTC)
    configure_file(
            "${VisualT_SOURCE_DIR}/src/version.h.in"
            "${VisualT_SOURCE_DIR}/src/version.h"
            @ONLY
    )
    configure_file(
            "${VisualT_SOURCE_DIR}/Doxyfile.in"
            "${VisualT_SOURCE_DIR}/Doxyfile"
            @ONLY
    )
endif()

# library code is here
add_subdirectory(src)

# examples are here
add_subdirectory(examples)

# tests and resources are here
add_subdirectory(res)
if(${CMAKE_})
install(TARGETS visualt
        EXPORT VisualT
        RUNTIME DESTINATION bin #temporary
        LIBRARY DESTINATION lib #temporary
        ARCHIVE DESTINATION lib #temporary
        PUBLIC_HEADER DESTINATION include)

install(EXPORT my_library DESTINATION "${lib_dest}")
  1. As you can see I made an attempt to detect if the project is being compiled as the “main project”, or as a subProject. I have no idea if this is the right way to do that. Perhaps I should use CMAKE_BUILD_TYPE to discern?

  2. I’ve used PRIVATE_HEADER and PUBLIC_HEADER properties to avoid using install(FILE). But the main problem remains: I have no idea where I should put the files. I’ve read many discussion about this and I decided to discard the idea of having “one path for all” systems, and I decided instead to make it os-dependent.(picked from here) I guess I should use an if over CMAKE_SYSTEM_NAME's value?

  3. I’d like to use components to modularize a bit the installation (like for the examples, and make them OPTIONAL), but I don’t know how they correlate with export groups. What are the differences?

  4. I’ve seen the INCLUDES DESTINATION but I didn’t get it. What are the differences with target_include_directories()? I understood that they are applied only on the exported target? how? why?

That’s all for now, I hope to untangle everything step by step. In the meantime I’ll keep reading. Thank you!

Files written to during the build should go to the build directory. If the header has per-build information, writing back to the source tree means your version.h is wrong for build trees sharing the same source tree with different contents.

This embeds your configure time into the header. If I configure today and build tomorrow, this value is “wrong” now.

It looks fine. CMAKE_BUILD_TYPE has nothing to do with the main project. It also doesn’t exist for multi-config generators anyways.

Use include(GNUInstallDirs) and use a path relative to CMAKE_INSTALL_INCLUDEDIR in your install(PUBLIC_HEADERS DESTINATION) arguments.

The paths documented in the page you linked are for config.cmake files, not headers.

Export sets each get their own targets.cmake file you need to include from your config.cmake file. How/when you include them depends on your own logic for COMPONENTS in your config.cmake file. They don’t need to correlate (but it’d be easier for you if they did).

INCLUDES DESTINATION seems to be a shortcut for target_include_directories(INTERFACE $<INSTALL_INTERFACE:>). I don’t know why it was added. Convenience maybe?

thank you for your time!

Alright, I’ll take that, no need to reinvent the wheel.

Oof, that’s embarrassing. I totally missed that. I found an alternative here, but that’s not viable with the Doxyfile for example: the file is too large. Is there a common way to add those info (cersion and build date) with CMake or should I look elsewhere?

But I need to make the project cross-platform (at least Linux and Windows), shouldn’t I need to set the installation path accordingly anyway? (with an if over CMAKE_SYSTEM_NAME 's value maybe?).
I’m not sure where GNUInstallDirs would point to under windows. I assume it doesn’t use CMAKE_INSTALL_PREFIX because the various destinations aren’t even under a common folder, like they would be on windows.

I see. I think I’d prefer to have one export group, and many components. I’ll think about that again when I’ll be ready to think about installation customization

ok, sounds good to me. I can’t yet figure the need of a “export-only” include directory, but that’s probably because I don’t need that. (i hope)

Generate a file like this at configure time:

set(some_var_used "value")
# and so on for other variables (logic can be used)
configure_file(path/to/Doxyfile.in path/to/Doxyfile @ONLY)

then schedule to run that file during the build:

add_custom_command(
  OUTPUT path/to/Doxyfile
  DEPENDS path/to/script.cmake path/to/Doxyfile.in
  COMMAND "${CMAKE_COMMAND}" -Danother_var=value -P path/to/script.cmake
  COMMENT "Configuring Doxyfile")

The install prefix still makes sense there; projects rarely splay files everywhere (and if they do…they’re probably not nice to uninstall either). Most of the paths are pretty much the same between the platforms. Any difference can be handled by just modifying how some destinations are built up (typically where config.cmake files go, plugins, config files). The only difference is whether you have multiple architectures in a single install (I recommend deferring that until you have a single-arch install working).

The install will use your install tree, but your build will use your build tree. Since includes are (usually) very different between the two, you’ll likely need something like it eventually.

This is the current root CMakeLists.txt (1)

# Works with 3.11 and tested through 3.16
cmake_minimum_required(VERSION 3.11...3.16)
include(GNUInstallDirs)

# Project name and a few useful settings. Other commands can pick up the results
project(VisualT
        VERSION 3.2.0
        DESCRIPTION "a text-based graphic library"
        LANGUAGES C)

macro(generate_configure_script)
    file(WRITE "${configure_script_path}" "
        configure_file(
                \"${version_in_path}\"
                \"${version_configured_path}\"
                @ONLY
        )
        configure_file(
                \"${doxyfile_in_path}\"
                \"${doxyfile_configured_path}\"
                @ONLY
        )
    ")
endmacro()

if("${CMAKE_PROJECT_NAME}" STREQUAL "VisualT")
    string(TIMESTAMP VisualT_BUILD_DATE "%d %B %Y" UTC)
    set(configure_script_path "${VisualT_BINARY_DIR}/configureScript.cmake")
    set(version_in_path "${VisualT_SOURCE_DIR}/src/version.h.in")
    set(doxyfile_in_path "${VisualT_SOURCE_DIR}/Doxyfile.in")
    set(version_configured_path "${VisualT_BINARY_DIR}/src/version.h")
    set(doxyfile_configured_path "${VisualT_BINARY_DIR}/Doxyfile")

    generate_configure_script()
    add_custom_command(
            OUTPUT "${version_configured_path}" "${doxyfile_configured_path}"
            DEPENDS "${configure_script_path}" "${version_in_path}" "${doxyfile_in_path}"
            COMMAND "${CMAKE_COMMAND}" -P "${configure_script_path}"
            COMMENT "configuring files")
endif()

# library code is here
add_subdirectory(src)
# examples are here
add_subdirectory(examples)
# tests and resources are here
add_subdirectory(res)

install(TARGETS visualt
        EXPORT VisualT
        RUNTIME DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
        PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")

install(EXPORT visualt DESTINATION "${CMAKE_INSTALL_LIBDIR}")

I’ve added the script generation code, and the custom command. I’ve read some documentation but I didn’t quite understand when the command is supposed to be ran:

A target created in the same directory ( CMakeLists.txt file) that specifies any output of the custom command as a source file is given a rule to generate the file using the command at build time.

Well, I’m generating a header and a doxyfile, so this could make sense only for the header.
Anyhow, the visualt target is not in the same CMakeLists (and nor it should be), so, as now, I can’t configure correctly because of the add_library(visualt ...version.h...) in CMakeLists.txt (2).
version.h doesn’t exist yet, as the command hasn’t been executed.

Additional doubts:
I couldn’t find how GNUInstallDirs is supposed to behave under windows. What I understand from the replies and the documentation is that all the locations lead to some subdirectory under CMAKE_INSTALL_PREFIX, named with the word between round brackets in the documentation.
For example, when I read

BINDIR

user executables ( bin )

That means that the variable leads to “CMAKE_INSTALL_PREFIX/bin” (/usr/local/bin for example).
The only exception seems to be OLDINCLUDEDIR, so I won’t use that one.

That said, if I change CMAKE_INSTALL_PREFIX to %ProgramFiles%/VisualT when in CMAKE_HOST_WIN32, everything should install nicely under that path.