DEV Community

Dimitri Merejkowsky
Dimitri Merejkowsky

Posted on • Originally published at dmerej.info on

Let's Build Chuck Norris! - Part 1: CMake and Ninja

Originally published on my blog.

Note: This is part 1 of the Let’s Build Chuck Norris! series.

The core library

We want a ChuckNorris class with a getFact() method that returns a random Chuck Norris fact.

Let’s start with a hard-coded answer for now:

include/ChuckNorris.hpp:

#pragma once
#include <string>

class ChuckNorris {
  public:
    ChuckNorris();
    std::string getFact();
};
Enter fullscreen mode Exit fullscreen mode

src/ChuckNorris.cpp:

#include <ChuckNorris.hpp>

ChuckNorris::ChuckNorris()
{
}

std::string ChuckNorris::getFact()
{
  return "Chuck Norris can slam a revolving door.";
}
Enter fullscreen mode Exit fullscreen mode

We have a header in include/ChuckNorris.hpp containing the class declaration, and a file in src/ChuckNorris.cpp containing the class definition.

The test program

To make sure the library can indeed be used in other programs, let’s add a src/main.cpp to check we manage to get the hard-coded fact:

src/main.cpp:

#include <ChuckNorris.hpp>
#include <iostream>

int main()
{
  ChuckNorris chuckNorris;
  std::string fact = chuckNorris.getFact();
  std::cout << fact << std::endl;
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

All we have to do is run the program without arguments, and check that something gets displayed. That will probably be enough to check our library works.

Targets

Let’s enumerate what we have to do now to build everything:

  • Make sure that the include/ directory is used when we compile code in src/
  • Build a library using src/ChuckNorris.cpp
  • Build an executable named cpp_demo using src/main.cpp that links with the above library.

We can say we have two targets: a library called chucknorris, and an executable named cpp_demo:

Here’s what the commands to build them look like on Linux:

# Compile the .cpp file into a .o
$ g++ -c -I include/ src/ChuckNorris.cpp -o libchucknorris.o
# Create an archive containing the .o
$ ar qc libchucknorris.a libchucknorris.o
# Run ranlib so that the archive can be used by the linker
$ ranlib libchucknorris.a
# Compile main.cpp into main.o
$ g++ -c -I include/ src/main.cpp -o main.o
# Tell g++ to link everything into an executable
$ g++ main.o libchucknorris.a -o cpp_demo
# Run the executable we've just built:
$./cpp_demo
Chuck Norris can slam a revolving door.
Enter fullscreen mode Exit fullscreen mode

Phew!

Keeping track of all this by hand would get tedious very soon, but fortunately there are tools that can help us.

CMake

CMake lets us describe the targets we want to build in code:

cmake_minimum_required(VERSION 3.10)

project(ChuckNorris)

add_library(chucknorris
    include/ChuckNorris.hpp
    src/ChuckNorris.cpp
)

target_include_directories(
  chucknorris
  PUBLIC
    "include"
)

add_executable(cpp_demo
  src/main.cpp
)

target_link_libraries(cpp_demo chucknorris)
Enter fullscreen mode Exit fullscreen mode

The add_library and add_executable commands describe the two targets (named chucknorris and cpp_demo) and the sources used to build them, and the target_link_libraries command tells CMake that cpp_demo depends on chucknorris.

The target_include_directories informs CMake that there are header files in the include directories that should be used both when builing the library itself, but also by consumers of the library. (We used the -I include/ flag both for building libchucknorris.o and main.co)

If the headers were used only to compile the library, we would have used the PRIVATE keyword, and if they were used only by consumers of the library, we would have used the INTERFACE keyword instead. You can read more about this in the CMake documentation.

Ninja

Now that we have described what we want to build and how, we still need to perform the build itself.

CMake does not know how to actually perform the build. Instead it generates files that will be used by an other tool. It’s called a CMake generator.

There are plenty of generators available, but for now we’ll only talk about Ninja. I’ve already explain why I prefer using CMake with Ninja in an other blog post.

In our first attempt, we generated all the binaries (libchucknorris.a, the .o files and the cpp_demo executable) directly in the current working directory. It’s cleaner to have them put inside a dedicated build folder instead:

  • We’ll only have to put the build folder into our .gitignore file, instead of a bunch of files
  • If we want to, we can have several build folders, all using the same sources, but using different compilers or flags without risking mixing incompatible binaries.

So let’s create a folder named build/default and call CMake, asking it to use the Ninja generator. CMake uses the current working directory as the build folder, and you must specify the path to the folder containing the CMakeLists.txt file as the last argument on the command line:

$ mkdir -p build/default
$ cd build/default
$ cmake -GNinja ../..
Enter fullscreen mode Exit fullscreen mode

And now we use ninja to build build and run our executable from the build folder:

$ cd build/default
$ ninja
[1/4] Building CXX object CMakeFiles/chucknorris.dir/src/ChuckNorris.cpp.o
[2/4] Building CXX object CMakeFiles/cpp_demo.dir/src/main.cpp.o
[3/4] Linking CXX static library libchucknorris.a
[4/4] Linking CXX executable cpp_demo
$ ./cpp_demo
Chuck Norris can slam a revolving door.
Enter fullscreen mode Exit fullscreen mode

Done!

Note that CMake and Ninja cooperate so that you only rebuild what’s need to be rebuilt.

If we change just the main.cpp, we just have to rebuild main.cpp.o and relink the cpp_demo:

$ ninja
[1/2] Building CXX object CMakeFiles/cpp_demo.dir/src/main.cpp.o
[2/2] Linking CXX executable cpp_demo
Enter fullscreen mode Exit fullscreen mode

But if we change ChuckNorris.cpp, everything except main.cpp.o needs to be rebuilt:

[1/3] Building CXX object CMakeFiles/chucknorris.dir/src/ChuckNorris.cpp.o
[2/3] Linking CXX static library libchucknorris.a
[3/3] Linking CXX executable cpp_demo
Enter fullscreen mode Exit fullscreen mode

And if we change the CMakeLists.txt file, Ninja will re-run CMake for us:

$ ninja
[0/x] Re-running CMake...
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/build/default
...
Enter fullscreen mode Exit fullscreen mode

That’s all for the first part. Stay tuned for part 2, where we’ll introduce an external dependency and get rid of the hard-coded fact.


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the feedback page for more ways to get in touch with me.

Top comments (1)

Collapse
 
itylergarrett profile image
tyler garrett

Nice read! #chucnorris approves, #ninja usage ftw.