Example Project using CMakePPLang
This chapter demonstrates how to set up a project to use CMakePPLang, aiming to be both a tutorial for new users setting up their first project using CMakePPLang, as well as a quick reference for experienced users.
This project will create a Greeter class. This class will store a user’s name
in a name
attribute and greet them using a Greeter(hello ...)
member
function. The Greeter(hello ...)
function will print the following
greeting, “Hello, <name>!”, where <name>
is the value of the name
attribute.
Note
The full example project can be found under examples/cmakepplang_example
to check your work or follow along when creating a new project is not
possible/desired.
Project Layout
This project will be a simple layout with a top-level CMakeLists.txt
file,
a cmake
subdirectory where CMake source code will reside, and a tests
directory where the code will be tested. If following along, create these
files and directories now, but leave the files blank. We will populate them in
later sections.
The layout will look like this:
.
├── cmake/
│ ├── get_cmakepp_lang.cmake
│ ├── get_cmake_test.cmake
│ └── greeter/
│ └── greeter_class.cmake
├── CMakeLists.txt
├── docs/
└── tests/
├── CMakeLists.txt
└── greeter/
└── test_greeter_class.cmake
The cmake
directory is where most CMake and CMakePPLang code will exist.
This includes the code to fetch CMakePPLang, cmake/get_cmakepp_lang.cmake
.
The cmake/greeter
subdirectory is used to contain the core code for this
project, effectively namespacing the code. This is where we will create our
Greeter
class.
The docs
directory will house documentation written for the project and
generated API documentation. This step will not be covered in this example,
but see Sphinx and CMinx for methods to write and generate documentation for
your project.
The tests
directory contains unit testing for your CMake and CMakePPLang
code, tested through CMakeTest. This directory should mirror the cmake
directory structure, with test files including the test_
prefix. We will
test our Greeter
class here.
Finally, a CMakeLists.txt
file at the root of the project will be the
entry point for the CMake project. Subdirectories sometimes contain additional
CMakeLists.txt
files for code specific to a specific directory’s function.
In this project, there is an additional CMakeLists.txt
that will fetch
CMakeTest
and include the tests to be run.
Initial CMake Boilerplate
All CMake projects require that a minimum CMake version be set with
cmake_minimum_required()
and a project must be defined with project()
.
Additionally, it is useful to add a toggle for whether to build the project
tests or not that the user can change as needed. We add the
option(BUILD_TESTING
call to add this option.
cmake_minimum_required(VERSION 3.14) # b/c of FetchContent_MakeAvailable
project(CMakePPLangExample VERSION 1.0.0 LANGUAGES NONE)
option(BUILD_TESTING "Should we build and run the unit tests?" OFF)
Fetching CMakePPLang
We will start by setting up the ability to get CMakePPLang automatically in
the project following the instructions at Automatically Downloading and Including CMakePPLang.
First, add the following text to cmake/get_cmakepp_lang.cmake
:
include_guard()
#[[
# This function encapsulates the process of getting CMakePPLang using CMake's
# FetchContent module. We have encapsulated it in a function so we can set
# the options for its configure step without affecting the options for the
# parent project's configure step (namely we do not want to build CMakePPLang's
# unit tests).
#]]
function(get_cmakepp_lang)
# Store whether we are building tests or not, then turn off the tests
set(build_testing_old "${BUILD_TESTING}")
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
# Download CMakePPLang and bring it into scope
include(FetchContent)
FetchContent_Declare(
cmakepp_lang
GIT_REPOSITORY https://github.com/CMakePP/CMakePPLang
)
FetchContent_MakeAvailable(cmakepp_lang)
set(
CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${cmakepp_lang_SOURCE_DIR}/cmake"
PARENT_SCOPE
)
# Restore the previous value
set(BUILD_TESTING "${build_testing_old}" CACHE BOOL "" FORCE)
endfunction()
# Call the function we just wrote to get CMakePPLang
get_cmakepp_lang()
# Include CMakePPLang
include(cmakepp_lang/cmakepp_lang)
Then, add the following line at the bottom of the top-level
CMakeLists.txt
created earlier:
# Bring the 'cmake' directory into scope to include files contained
# within easily
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(get_cmakepp_lang)
Defining the Class
Now we need to define the Greeter
class with the name
attribute and
hello
member function. Create a cmake/greeter/greeter_class.cmake
file and add the following text:
include_guard()
include(cmakepp_lang/cmakepp_lang)
#[[[
# Greeter class to greet the user using their name.
#
# The class definition for the Greeter class starts here with ``cpp_class``.
#]]
cpp_class(Greeter)
#[[[
# :type: str
#
# Name to use in the greeting. This attribute defaults to a blank string
# ("") if it is not set by the user.
#]]
cpp_attr(Greeter name "")
#[[[
# Member function used to say hello to the user.
#
# Notice that there is a ``cpp_member()`` call to provide the function
# parameter typings, followed immediately by a ``function()`` call with
# the dereferenced function name and parameter names.
#
# :param self: Greeter object to use.
# :type self: Greeter
#]]
cpp_member(hello Greeter)
function("${hello}" self)
# Get the value of the 'name' attribute
Greeter(GET "${self}" _name_result name)
# Print the hello message to the user
message("Hello, ${_name_result}!")
endfunction() # End Greeter(hello
cpp_end_class() # End Greeter class
An example of using the Greeter
class can then be added to the bottom of
the top-level CMakeLists.txt
file:
# Include the Greeter class definition
include(greeter/greeter_class)
# Create a Greeter instance
Greeter(CTOR greeter_obj)
# Set the 'name' attribute of the Greeter instance
Greeter(SET "${greeter_obj}" name "John Doe")
# Get the 'name' attribute value of the Greeter instance to check it
Greeter(GET "${greeter_obj}" name_result name)
message("Name attribute value: ${name_result}")
# OUTPUT: Name attribute value: John Doe
# Call the 'Greeter(hello' method for the Greeter instance, which uses the
# 'name' attribute to get the name to print
Greeter(hello "${greeter_obj}")
# OUTPUT: Hello, John Doe!
Testing the Class
CMakeTest is going to be used to test the Greeter
class we just wrote, so
we need to get CMakeTest similarly to how we automatically got CMakePPLang.
Add the following text to cmake/get_cmake_test.cmake
:
include_guard()
#[[
# This function encapsulates the process of getting CMakeTest using CMake's
# FetchContent module. We have encapsulated it in a function so we can set
# the options for its configure step without affecting the options for the
# parent project's configure step (namely we do not want to build CMakeTest's
# unit tests).
#]]
function(get_cmake_test)
# Store whether we are building tests or not, then turn off the tests
set(build_testing_old "${BUILD_TESTING}")
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
# Download CMakeTest and bring it into scope
include(FetchContent)
FetchContent_Declare(
cmake_test
GIT_REPOSITORY https://github.com/CMakePP/CMakeTest
)
FetchContent_MakeAvailable(cmake_test)
set(
CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH}" "${cmake_test_SOURCE_DIR}/cmake"
PARENT_SCOPE
)
# Restore the previous value
set(BUILD_TESTING "${build_testing_old}" CACHE BOOL "" FORCE)
endfunction()
# Call the function we just wrote to get CMakeTest
get_cmake_test()
# Include CMakeTest
include(cmake_test/cmake_test)
Next is to create the test for Greeter
by adding the following text to
tests/greeter/test_greeter_class.cmake
, testing the output of the
Greeter(hello ...)
method:
include(cmake_test/cmake_test)
#[[[
# Tests created with CMakeTest are structured similarly to the C++ testing
# package, `Catch2 <https://github.com/catchorg/Catch2>`__. This structures
# the testing framework as a collection of tests, created with
# ``ct_add_test(NAME "<test_name>")`` containing sections with individual
# unit tests added with ``ct_add_section(NAME "<section_name>")``.
# ``EXPECT_FAIL`` can be added to the ``ct_add_section()`` calls if the
# test is expected to fail instead of succeeding.
#]]
ct_add_test(NAME "test_greeter_class")
function("${test_greeter_class}")
include(greeter/greeter_class)
#[[[
# Test that Greeter(hello prints the correct message
#]]
ct_add_section(NAME "test_hello")
function("${test_hello}")
# Create Greeter instance
Greeter(CTOR _greeter_obj)
# Set the 'name' attribute
Greeter(SET "${_greeter_obj}" name "John Doe")
# Call Greeter(hello
Greeter(hello "${_greeter_obj}")
# Test if the printed message is correct
ct_assert_prints("Hello, John Doe!")
endfunction()
endfunction()
Then, we have to add the tests to the project. At the bottom of the
top-level CMakeLists.txt
, we add the final part to add the tests directory
and allow the tests to be toggled on and off with the BUILD_TESTING
option:
# We can also test the class using CMakeTest
if("${BUILD_TESTING}")
include(CTest)
add_subdirectory(tests)
endif()
Finally, we can complete adding the tests by populating tests/CMakeLists.txt
to get CMakeTest and add the tests/greeter
test directory for CMakeTest:
include(FetchContent)
#[[[
# These tests are performed using CMakeTest, the CMakePP Organization's
# solution for writing tests for your CMake code.
#]]
include(get_cmake_test)
ct_add_dir(greeter)
Building the Project
Now that we have a complete project, it can be built with CMake after
navigating to the top of the project directory, which we will refer to here
as PROJECT_ROOT
. In a terminal, run the following command for your system:
For Unix- or Linux-based systems (including Mac OSX):
cmake -H. -Bbuild -DBUILD_TESTING=ON
For Windows systems:
cmake -S. -Bbuild -DBUILD_TESTING=ON
In these commands, -H
and -S
specify the top-level, root directory for
the project, called the “source directory” in CMake (not to be confused with a
“source code directory”, commonly also called the “source directory”). In this
case, we are assuming that the current directory, .
, is PROJECT_ROOT
.
The directory where build artifacts will appear is specified by -B
,
meaning we are directing build artefacts to PROJECT_ROOT/build
. Finally,
-DBUILD_TESTING=ON
enables unit testing on CMakePPLang, which will be
necessary for development. BUILD_TESTING
defaults to OFF
, so this
argument can be excluded if testing is not needed.
Note
Since CMakePPLang is built during the CMake configuration step, the build step is usually not needed. However, if it becomes necessary, the build step can be run with:
cmake --build build
Running the Tests
If tests were built using the BUILD_TESTING=ON
option, then unit tests from
the build will be included in the build
directory generated above. These
tests are run using CMake’s test driver program, CTest. Navigate into the
build
directory and run the following command to execute the tests:
ctest --output-on-failure
While ctest
can be run with no arguments as well, it is usually useful to
run it with --output-on-failure
, since it will provide all of the output
from a failing test program instead of simply saying the test failed. After
the first run of the tests, it is also useful to include --rerun-failed
to save time by skipping passing tests.