Introduction

CMake is everywhere.

(If you still use Makefile, keep reading, you are missing out in life!)

CMake can look complicated but in fact it is not: has all the complexity of a build system that can build almost anything but, in fact, its use is as simple as it gets.

This is not a complete guide into CMake, but one to get you started. If you want to read more about CMake, I recommend you read more about Modern CMake.

Requirements

To start with CMake, I’d need a CMakeLists.txt file like this:

cmake_minimum_required(VERSION 3.16.0)
project(test_c)
 
add_executable(test test.c)

And a test.c file like this:

#include <stdio.h>
 
int add(int a, int b)
{
  return a + b;
}
 
int main()
{
  int a, b;
 
  printf("Enter number a: ");
  scanf("%d", &a);
  printf("Enter number b: ");
  scanf("%d", &b);
 
  printf("Adding result -> %d", add(a, b));
}

With a project structure as such:

-- CMakeLists.txt
-- test.c

The fun thing about CMake is that you only need to run cmake once:

$ mkdir build
$ cmake -B build/

This will generate the Makefile inside the build/ folder. Next, jump into the directory created and generate the Makefile:

$ cd build
$ make

Once this is done, you will end up with something like this:

-- build
  -- Makefile
  -- test
-- CMakeLists.txt
-- test.c

Running make has generated the test executable. Now, run:

$ ./test

Done 👍🏼


Adding libraries

Adding a library is easy. Let’s start with this directory structure:

-- CMakeLists.txt
-- test.c
-- /test_lib
  -- test_lib.c
  -- test_lib.h

Inside test.c:

#include <stdio.h>
#include "test_lib.h"
 
int add(int a, int b)
{
  return a + b;
}
 
int main()
{
  int a, b;
 
  printf("Enter number a: ");
  scanf("%d", &a);
  printf("Enter number b: ");
  scanf("%d", &b);
 
  printf("Mux result -> %d", mux(a, b)); // Uses `mux` method
}

Inside test_lib.h:

int mux (int a, int b);

Inside test_lib.c:

#include "test_lib.h"
 
int mux (int a, int b) {
  return a * b;
}

Finally, to add this as library to the executable target test, add:

cmake_minimum_required(VERSION 3.16.0)
project(test_c)
 
add_library(test_lib ${CMAKE_CURRENT_SOURCE_DIR}/test_lib/test_lib.c)
target_include_directories(test_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/test_lib)
 
add_executable(test src/test.c)
target_link_libraries(test test_lib)

The add_library creates a target (test_lib) that points to the test_lib.c file and target_link_libraries links the test_lib target to the executable test target.

Hit $ make again 👍🏼

Adding an external library

Imagine the scenario where you have your project and want to include an external library you have seen on Github.

This is your project structure:

CMakeLists.txt
-- /src
  -- test.c
-- /test_lib
  -- test_lib.c
  -- test_lib.h
  -- CMakeLists.txt

The folder src has the file we want to execute and test.c contains the int main() function. The test_lib folder contains one executable file, one header and one CMakeLists.txt.

Inside test_lib/CMakeLists.txt we have the following:

# The library `test_lib` should ideally be one step below
set(TEST_LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR})
 
# Set it up as a library.
add_library(test_lib ${TEST_LIB_PATH}/test_lib.c)
 
# Turn it into a searchable field in PUBLIC. This means any target that includes
# this library is capable of looking for the directory passed below.
target_include_directories(test_lib PUBLIC ${TEST_LIB_PATH})

There, the library is created and placed under a PUBLIC inheritance. If you don’t know which inheritance to choose, leave it blank and CMake will default it to PUBLIC. Nevertheless, this is a library you just downloaded and want to include it inside you project. The magic happens in the root CMakeLists.txt:

cmake_minimum_required(VERSION 3.16.0)
project(
  test_c
  VERSION 1.0
  DESCRIPTION "An example project with CMake"
  LANGUAGES C
)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
 
add_executable(test ${CMAKE_CURRENT_SOURCE_DIR}/src/test.c)
 
add_subdirectory(test_lib)
target_link_libraries(test PRIVATE test_lib)
 
add_custom_command(
  TARGET test
  POST_BUILD
  COMMENT "Running ./test"
  COMMAND ./test
)

You can easily add the external library by calling add_subdirectory and the target library name you’ve created in test_lib/CMakeLists.txt as an argument. Once you add the subdirectory, you need to link it to the executable target, test. You can link it as PRIVATE since you won’t share this subdirectory anywhere else.

Finally, instead of runnning $ make and then $ ./test, you can ask CMake to do that for you by using the add_custom_command method. As an argument of add_custom_command, add COMMAND followed by the command you want to run. The POST_BUILD keyword makes sure the command will only run after building the code.

Hit $ make 👍🏼