Skip to content

Commit

Permalink
doc: Developer's guide (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
LecrisUT authored Dec 25, 2023
2 parents 746b283 + 82ad246 commit a4f73db
Show file tree
Hide file tree
Showing 21 changed files with 871 additions and 33 deletions.
36 changes: 18 additions & 18 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ if (POLICY CMP0140)
cmake_policy(SET CMP0140 NEW)
endif ()

#[==============================================================================================[
# Basic project definition #
]==============================================================================================]
#[=============================================================================[
# Basic project definition #
]=============================================================================]

list(APPEND CMAKE_MESSAGE_CONTEXT Template)
project(Template
Expand All @@ -36,9 +36,9 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS ON)

#[==============================================================================================[
# Options #
]==============================================================================================]
#[=============================================================================[
# Options #
]=============================================================================]

include(CMakeDependentOption)
include(FeatureSummary)
Expand All @@ -48,9 +48,9 @@ option(TEMPLATE_SHARED_LIBS "Template: Build as a shared library" ${PROJECT_IS_T
option(TEMPLATE_INSTALL "Template: Install project" ${PROJECT_IS_TOP_LEVEL})


#[==============================================================================================[
# Project configuration #
]==============================================================================================]
#[=============================================================================[
# Project configuration #
]=============================================================================]

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

Expand All @@ -69,9 +69,9 @@ if (NOT CMAKE_BUILD_TYPE)
endif ()
set(BUILD_SHARED_LIBS ${TEMPLATE_SHARED_LIBS})

#[==============================================================================================[
# External packages #
]==============================================================================================]
#[=============================================================================[
# External packages #
]=============================================================================]

FetchContent_Declare(fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt
Expand All @@ -93,9 +93,9 @@ feature_summary(
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS ${Template_Info})

#[==============================================================================================[
# Main definition #
]==============================================================================================]
#[=============================================================================[
# Main definition #
]=============================================================================]

# Main project
add_executable(Template_Hello)
Expand All @@ -121,9 +121,9 @@ if (TEMPLATE_TESTS)
add_subdirectory(test)
endif ()

#[==============================================================================================[
# Install or Export #
]==============================================================================================]
#[=============================================================================[
# Install or Export #
]=============================================================================]

# Installation
if (TEMPLATE_INSTALL)
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"tmt": ("https://tmt.readthedocs.io/en/stable", None),
"sphinx-tippy": ("https://sphinx-tippy.readthedocs.io/en/latest", None),
"sphinx-hoverxref": ("https://sphinx-hoverxref.readthedocs.io/en/latest", None),
"cmake-extra": ("https://cmakeextrautils.readthedocs.io/en/latest", None),
}

tippy_rtd_urls = [
Expand All @@ -49,6 +50,7 @@
"https://tmt.readthedocs.io/en/stable",
"https://sphinx-tippy.readthedocs.io/en/latest",
"https://sphinx-hoverxref.readthedocs.io/en/latest",
"https://cmakeextrautils.readthedocs.io/en/latest",
]

copybutton_exclude = ".linenos, .gp, .go"
Expand Down
10 changes: 7 additions & 3 deletions docs/guides/developer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
: glob: true
who-is-the-developer
paradigms/index
tools/index
```

This is a developer's guide intended to explain many of the paradigms used in
this template project
This is a developer's guide intended to explain many of the [design paradigms]
used in this template project and the minimum [CI tooling] that should be setup
in modern projects.

**Coming soon**
[design paradigms]: paradigms/index.md
[CI tooling]: tools/index.md
146 changes: 146 additions & 0 deletions docs/guides/developer/paradigms/export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Exporting

For exporting the project, we will focus on two aspects: what and how to export
([Targets and components]), and how to make the export equivalent to the
[`FetchContent`] users ([`<Project>Config.cmake` and `FetchContent`]).

## Targets and components

Here we have to consider what are the main targets that are installed and
exported by default, and which ones are linked to a `COMPONENT` as used by
[`find_package`]. Generally this is determined by what library or executable
files are linked to the target.

Unfortunately exporting and importing the targets linked to `COMPONENT` is not
straightforward, and it involves quite a lot of boilerplate. To try to decompose
this process, it requires:
- [`install(TARGETS)`][]: Add the installation instructions for the actual
target.
- [`install(EXPORT)`] and [`export(EXPORT)`][]: Link the targets to a specific
`<TargetFile>`/`COMPONENT`. A good format for the `<TargetFile>` is
`<Project>Targets_<Component>.cmake`.
- Configure [`<Project>Config.cmake`] to import the `<TargetFile>` as requested
by the equivalent `COMPONENT`.

This is more easily shown through an example. Let's consider and example project
`Example` with the main library `hello` and optional executable `say-hello`.
Disregarding the most of the paradigms for the sake of brevity, the project
files would look like this:

```{code-block} cmake
: caption: CMakeLists.txt
: emphasize-lines: 9-17,25-33
option(EXAMPLE_EXECUTABLE "Build the say-hello executable" OFF)
add_library(hello)
install(TARGETS hello
EXPORT ExampleTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
export(EXPORT ExampleTargets
FILE ExampleTargets.cmake
NAMESPACE Example::
)
install(EXPORT ExampleTargets
FILE ExampleTargets.cmake
NAMESPACE Example::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Example
)
if (EXAMPLE_EXECUTABLE)
add_executable(say-hello)
install(TARGETS say-hello
EXPORT ExampleTargets_say-hello
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
export(EXPORT ExampleTargets_say-hello
FILE ExampleTargets_say-hello.cmake
NAMESPACE Example::
)
install(EXPORT ExampleTargets_say-hello
FILE ExampleTargets_say-hello.cmake
NAMESPACE Example::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Example
)
endif ()
configure_package_config_file(
ExampleConfig.cmake.in ExampleConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Example
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ExampleConfig.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/Example
)
```
```{code-block}
: caption: ExampleConfig.cmake.in
: emphasize-lines: 3-6
@PACKAGE_INIT@
include(${CMAKE_CURRENT_LIST_DIR}/ExampleTargets.cmake)
if (say-hello IN_LIST Example_FIND_COMPONENTS)
include(${CMAKE_CURRENT_LIST_DIR}/ExampleTargets_say-hello.cmake OPTIONAL RESULT_VARIABLE Example_say-hello_FOUND)
endif ()
check_required_components(Example)
```

:::{caution}
Do not use `-` as separator in the names of the Targets file. The generated
target files will glob for `<TargetFile>-*` and include them automatically.
:::

### `shared` and `static` components

Supporting both shared and static installations can be quite tricky, especially
when these can span multiple additional components. A good resource is Alex
Reinking's [blog post] on this subject, however, implementing this can be quite
tricky. For this I have a helper module [`PackageComps`] that simplifies this
implementation, and abstracts all of this boilerplate.

## `<Project>Config.cmake` and `FetchContent`

In order to make these two approaches equivalent, you only need to make sure
that the targets have appropriate namespaced aliases and that the variables are
exported appropriately.


```{code-block} cmake
: caption: CMakeLists.txt
: emphasize-lines: 2,6-10
add_library(hello)
add_library(Example::hello ALIAS hello)
if(NOT PROJECT_IS_TOP_LEVEL)
return(PROPAGATE
Example_VERSION
Example_VERSION_MAJOR
Example_VERSION_MINOR
Example_VERSION_PATCH
Example_VERSION_TWEAK
)
endif ()
```

:::{attention}
Make sure that [`<Project>_VERSION`] and its equivalents are exported
:::

[blog post]: https://alexreinking.com/blog/building-a-dual-shared-and-static-library-with-cmake.html

[Targets and components]: #targets-and-components
[`<Project>Config.cmake` and `FetchContent`]: #projectconfigcmake-and-fetchcontent
[namespace]: namespace.md

[`FetchContent`]: inv:cmake:cmake:module#module:FetchContent
[`PackageComps`]: inv:cmake-extra:std:doc#cmake_modules/PackageComps
[`find_package`]: inv:cmake:cmake:command#command:find_package
[`install(TARGETS)`]: inv:cmake:cmake:command#command:install(targets)
[`install(EXPORT)`]: inv:cmake:cmake:command#command:install(export)
[`export(EXPORT)`]: inv:cmake:std:label#export(export)
[`<Project>Config.cmake`]: <inv:cmake:std:label#full signature>
[`<Project>_VERSION`]: inv:cmake:cmake:variable#variable:<PROJECT-NAME>_VERSION
108 changes: 108 additions & 0 deletions docs/guides/developer/paradigms/import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Importing dependencies

The modern method of importing dependencies is to use
[`FetchContent_Declare(FIND_PACKAGE_ARGS)`] which allows the user to choose how
to import the dependency: from the system ([`find_package`]) or downloaded/local
source ([`FetchContent`]).

## Required dependency

If the dependency is required, and it is a CMake based project, you can
straightforwardly use [`FetchContent_Declare(FIND_PACKAGE_ARGS)`]. Note that
this feature is only introduced in CMake 3.24. Back-porting this feature can be
quite involved:
```cmake
if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
FetchContent_Declare(ProjectB
...
FIND_PACKAGE_ARGS CONFIG
)
else ()
if (NOT DEFINED FETCHCONTENT_SOURCE_DIR_PROJECTB)
if (NOT DEFINED FETCHCONTENT_TRY_FIND_PACKAGE_MODE)
set(FETCHCONTENT_TRY_FIND_PACKAGE_MODE OPT_IN)
endif ()
if (FETCHCONTENT_TRY_FIND_PACKAGE_MODE MATCHES "(OPT_IN|ALWAYS)")
find_package(ProjectB QUIET CONFIG)
endif ()
endif ()
if (NOT ProjectB_FOUND)
FetchContent_Declare(ProjectB
...
)
endif ()
endif ()
```

See [controlling dependency import] for more details on how to control the
import.

## Optional dependency

For optional dependencies, unfortunately the fallthrough using
[`FetchContent_Declare(FIND_PACKAGE_ARGS)`] does not allow the dependency to not
be imported. For this the only clean design is to use `find_package`, with
[`CMAKE_REQUIRE_FIND_PACKAGE_<PackageName>`] and
[`CMAKE_DISABLE_FIND_PACKAGE_<PackageName>`] to control the dependencies.

## `Find<PackageName>.cmake`

In order to provide compatibility with non-CMake projects, you can provide a
[`Find<PackageName>.cmake`] file to search for this dependency. But keep in mind
that integrating this to make it work with the project's [export] can be quite
tricky, since these modules should not be exported outside of the project, in
order to avoid conflicts with other similarly defined modules.

If you still need such a module, an example module can look as follows:

```{code-block} cmake
: caption: FindExample.cmake
set(CMAKE_REQUIRE_FIND_PACKAGE_Example FALSE)
find_package(Example CONFIG QUIET)
if (Example_FOUND)
find_package_handle_standard_args(Example CONFIG_MODE)
return()
endif ()
pkg_search_module(Example IMPORTED_TARGET
example
)
if (Example_FOUND)
add_library(Example::hello ALIAS PkgConfig::Example)
return ()
endif ()
find_library(Hello_LIBRARY
NAMES hello
)
mark_as_advanced(Hello_LIBRARY)
find_path(Example_INCLUDE_DIR
NAMES hello.h
PATH_SUFFIXES example
)
mark_as_advanced(Example_INCLUDE_DIR)
find_package_handle_standard_args(Example
REQUIRED_VARS Hello_LIBRARY Example_INCLUDE_DIR
)
if(Example_FOUND)
set(Example_INCLUDE_DIRS ${Example_INCLUDE_DIR})
set(Example_LIBRARIES ${Hello_LIBRARY})
add_library(Example::hello UNKNOWN IMPORTED)
set_target_properties(Example::hello PROPERTIES
IMPORTED_LOCATION ${Hello_LIBRARY}
)
target_include_directories(Example::hello INTERFACE ${Example_INCLUDE_DIRS})
target_link_libraries(Example::hello INTERFACE ${Example_LIBRARIES})
endif()
```

[controlling dependency import]: #dependency-import-options
[export]: export.md

[`FetchContent_Declare(FIND_PACKAGE_ARGS)`]: inv:cmake:std:label#fetchcontent-find_package-integration-examples
[`find_package`]: inv:cmake:cmake:command#command:find_package
[`FetchContent`]: inv:cmake:cmake:module#module:FetchContent
[`CMAKE_REQUIRE_FIND_PACKAGE_<PackageName>`]: inv:cmake:cmake:variable#variable:CMAKE_REQUIRE_FIND_PACKAGE_<PackageName>
[`CMAKE_DISABLE_FIND_PACKAGE_<PackageName>`]: inv:cmake:cmake:variable#variable:CMAKE_DISABLE_FIND_PACKAGE_<PackageName>
[`Find<PackageName>.cmake`]: <inv:cmake:std:label#find modules>
14 changes: 14 additions & 0 deletions docs/guides/developer/paradigms/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Design paradigms

```{toctree}
: maxdepth: 1
: titlesonly: true
top-level-cmake
namespace
subproject
test-suite
export
options
import
```
Loading

0 comments on commit a4f73db

Please sign in to comment.