Welcome to Part II of the modern CMake workflow series. In this post we will create a simple example of a small project which will help us to really grasp the concepts explained on Part I. Also I'll share this small project on a github repository which once cloned it will help you start a new C++ project with all the conventions mentioned in Part I in place.
What are we building?
OK so lets build a short and easy example of a module, something useful and not too complicated for the purpose of this blog post. For this example we will make a simple system module with a Logger which will be a private interface of the spdlog library.
To start building this module we will need the following:
- Create our module directory inside the src folder. The name of the parent folder will be our module's name in this example it will be called system.
- Inside our module's directory we need to create the public header's directory and we will follow Part I naming conventions. So for our example we will use the pn namespace, which stands for project namespace, it will be as follows: include/pn/system/.
Here is an expanded view of the project's tree:
The System Module
We'll build a simple interface to the spdlog logger, we will start with a singleton pattern to build this feature. First we need a public header so other libraries that link to this module may be able to log to the console even though they won't have access to the spdlog library.
This is a simple header, we defined the Logger class which is a singleton with static methods, every method corresponds to the different types of logs spdlog has and each receives a string parameter as a message.
Now that we have our Logger specification it is time to write the implementation of it:
The implementation is simple, we just instantiate our Logger object and refer every type of log to the corresponding spdlog function.
The Main Executable
To create our main executable we first need to create a CMakeLists.txt where we will tell the build environment about it, add the subdirectory of the system module and link the main executable with our system library.
And now the main executable with the main function which just calls each type of logs:
You might have notice the version header, which is just a header with pre-processor functions defining the version and description of the project:
#pragma once
#include <string>
#define PN_VERSION_MAJOR 0
#define PN_VERSION_MINOR 0
#define PN_VERSION_PATCH 1
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define PN_VERSION "v" TOSTRING(PN_VERSION_MAJOR) "." TOSTRING(PN_VERSION_MINOR) "." TOSTRING(PN_VERSION_PATCH)
#define PN_NAME "Project Name"
#define PN_DESCRIPTION "Project Description"
#define PN_COPYRIGHT "http://hugomarquez.mx \t (C) 2022"
#define PN_INFO "\n" PN_NAME ": " PN_DESCRIPTION "\n" PN_VERSION "\n" PN_COPYRIGHT "\n"
Test-Driven Development
For our testing purposes we need to create a CMakeLists.txt, which its sole concern will be finding the testing framework library, the main testing executable with the linkage of our modules.
And for our test executable we have the following example:
// This tells Catch to provide a main() - only do this in one cpp file
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
// Here we include our system module's test following the directory naming convention
#include "./pn/system/LoggerTest.h"
Finally lets build our test! This will be very simple because our Logger class only refers the spdlog functions with static methods. So we only need to test that the singleton pattern is not returning a null or empty object.
Putting it all together!
Getting our dependencies
To get our dependencies from conan we navigate to the build folder and run the following command:
$ conan install ../
This will download our dependencies and the modules required to find them using CMake:
Generating and compiling our build files
To generate our build files, which in my case I'll be using makefiles, we need to run the cmake generator inside the build directory:
$ cmake ../
Running the tests
We have several options to run our tests, we can use CTest or just simply running the test executable, I'm going to show both versions of it:
Running our main executable
Finally it is time to run our main executable, when we run it we should see all our logs being displayed on the console:
Thank you for reading my post, it was a long journey but we had fun... right? Either way I really hope you found this helpful and I'll be leaving you with the github repository if you would like to use this as a template for your new C++ projects.
Bottoms up! 🍻