This is a collection of example C++ projects demonstrating:
- A standard directory structure.
- Concise configurations for CMake and Conan.
- A variety of popular methods for importing dependencies.
Each project defines one package named after the number in its directory name,
e.g. zero
, one
, two
, etc.
cupcake
is a special package that only exports a CMake module.
Each package follows a strict structure that is highly opinionated. The basic idea is to minimize your options and make a decision while following conventions. This yields a few benefits:
- Newcomers can quickly orient themselves to a package.
- Contributors don't have to spend any time thinking about where to place new files.
- Tools can make assumptions that let them handle as much heavy lifting and boilerplate as possible.
Each package is a collection of:
- Zero or more libraries. Each library has public headers.
- Zero or more executables.
- At least one library or executable.
- Zero or more tests. Each test is an executable that returns 0 if and only if it passed.
The package and each library, executable, and test must have a name.
Every appearance of {name}
in this document refers to that name, in context.
These names must use only lowercase letters
(to avoid any problems with case-insensitive filesystems),
and numbers,
and must start with a letter
(to avoid any problems with their use as an identifier).
Separators are prohibited (for now).
If you find yourself wanting a separator,
consider using an initialism for the name instead,
like gmp
for the GNU Multiple Precision Arithmetic Library
or mpfr
for the Multiple Precision Floating-Point Reliable Library.
Conventionally, the name of the main library (if any) and main executable (if
any) should match the name of the package.
For example, a package named curl
might have a library named curl
and an
executable named curl
.
Each package has a "physical" structure in the filesystem and a "logical" structure in its CMake configuration.
/
|- conanfile.py
|- CMakeLists.txt
|- external/
| `- Finddoctest.cmake
|- include/
| `- example/
| `- example.hpp
|- src/
| |- libexample.cpp
| `- example.cpp
`- tests/
|- CMakeLists.txt
`- main.cpp
Each library must have at least one public header.1
Public headers are located under the include
directory.
A library may have a single public header in that directory named {name}.hpp
,
or a directory named {name}
with many public headers
A library with many public headers must put them under a directory named
{name}
.
A library with a header directory must not have headers in that directory
named version.hpp
or export.hpp
because they will be generated.
A library may have source files (i.e. implementation files ending with
extension .cpp
).
A library without sources is called header-only.
A library with sources must put them under the src
directory.
A library with a single source file must name it lib{name}.cpp
.
A library with many sources must put them under a directory named lib{name}
.
A library may have private headers.
They should be placed under its source directory.
An executable is much like a library except that
(a) it must not have public headers,
(b) it must have sources,
and (c) it must drop the lib
prefix for its source file or directory.
Each test is much like an executable except that
its sources must be placed under the tests
directory.
The root directory of a project must have a CMakeLists.txt
.
That listfile must define a CMake project with the package name.
The root listfile must define a target for each library.
A library's target must be named lib{name}
.
If the library is header-only, then the target must be an
INTERFACE
library.
Otherwise, its linkage must be determined by the value of
the conventional CMake option BUILD_SHARED_LIBS
.
The root listfile must define a target for each executable.
An executable's target must be named {name}
.
The root listfile must define an ALIAS
target
nested under the package scope
for each library and executable.
That is, it must be named {package-name}::{target-name}
.
All target references, e.g. in calls to target_link_libraries
,
must use these ALIAS
targets.
The root listfile must import the direct dependencies of all libraries and
executables with find_package
.
(See section Imports below.)
It must not import any dependencies that are not directly used in the root
listfile, e.g. dependencies that are only used by tests.
The root listfile must install every library and executable,
all public headers,
and Package Configuration Files for all library and executable targets.
The exported target names must match the ALIAS
names.
If the project has tests,
then the root listfile must call add_subdirectory(tests)
only when testing
is enabled, i.e. when the conventional CMake option BUILD_TESTING
is ON
(the default).
The tests
directory must have a CMakeLists.txt
.
The tests listfile must define a CMake test for each test.
It must import the direct dependencies of all tests with find_package
.
Any target defined in the tests listfile must be
excluded from the ALL
target.
The tests listfile must not directly or indirectly
install anything.
Every project has direct dependencies,
and some have indirect dependencies.
Every project finds its direct dependencies via calls to find_package
.
find_package
lets builders hook into the import
by supplying their own Find Module (FM) at build time, if desired.
By default, a call to find_package
looks for
a Package Configuration File (PCF) first
(on the CMAKE_PREFIX_PATH
and friends)
and an FM second (on the CMAKE_MODULE_PATH
).2
When a PCF exists for a package, we say that package is installed.
Every project that does not expect to find a PCF for a dependency
defines its own FM for that dependency.
These FMs are effectively fallbacks or defaults,
used when the builder does not supply their own FMs.
They demonstrate a variety of different methods for importing,
including add_subdirectory
, FetchContent
, and ExternalProject
.
Once a package is installed,
its PCF is responsible for importing its direct dependencies.
These PCFs all use find_package
too.3
A package's PCF cannot build its direct dependencies,
and thus it cannot use the same import methods
that it might have used in its FMs,
e.g. add_subdirectory
or FetchContent
.
A project that needed to build a dependency
because it could not find it installed
should install that dependency when it installs itself,
so that its PCF can find the dependency installed.
Every project,
with one exception explained below,
imports doctest
and cupcake
via PCF.
All but zero
directly import one of the other projects, too,
using one of the known import methods.
The package relationships are contrived to test a number of combinations of
import methods for direct and indirect dependencies.
The dependency relationships and import methods are captured in the table
below.
Special notes on select projects follow.
Package | Direct Dependencies | Indirect Dependencies | Required Installation |
---|---|---|---|
zero |
|||
one |
zero via PCF |
zero |
|
two |
zero via add_subdirectory |
||
three |
one via PCF |
zero |
one , zero |
four |
two via PCF |
zero |
two |
five |
zero via FetchContent |
||
six |
one via FetchContent |
zero |
zero |
seven |
two via FetchContent |
zero |
|
eight |
zero via find_library |
zero |
|
nine |
zero via ExternalProject |
||
ten |
zero via cupcake_find_packages |
||
eleven |
zero via PCF |
zero |
zero
: Imports no other packages from this collection.two
: Requires thatzero
be in the subdirectoryexternal/00-upstream
. Installszero
when it is installed so that packages depending ontwo
do not have to know about indirect dependencies.eight
: Importszero
viafind_package
, which finds an FM atexternal/Findzero.cmake
, which usesfind_path
,find_library
, andfind_program
to defineIMPORTED
targets.ten
:cupcake_find_packages
is a special function incupcake
, meaning it requires acupcake.json
metadata file.eleven
: Does not importcupcake
, unlike all of the other projects. This package tests that consumers do not needcupcake
to import a package that usescupcake
.
Footnotes
-
Correct me if I'm wrong, but a library without public headers would be useless. No one would be able to import any of its exports. ↩
-
This is not the CMake default, which looks for FMs first. Instead, it is the default behavior chosen by
cupcake
. ↩ -
Technically,
find_dependency
. ↩