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.