본문 바로가기
Embedded

[Embedded] Build using CMake

by llHoYall 2021. 10. 13.

CMake is an open-source, cross-platform build tool.

In this posting, we will use CMake to build a C project.

Prerequisite

CMake

You can install the latest CMake on this page.

https://cmake.org/download/

I installed it with Homebrew.

$ brew install cmake

VSCode Extensions

If you are using VSCode, you can install these extensions.

It will help you a lot.

Single File Project

Let's create a project.

// src/main.c

#include <stdio.h>

int main(int argc, char* argv[]) {
    printf("Hello CMake!\n");
    return 0;
}

If you build a project using CMake, the CMakeLists.txt file is needed.

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)

project(cmake_example VERSION 1.0.0)

add_executable(cmake_example src/main.c)

The name is clear, so there is nothing to explain.

Now, build our project.

$ cmake .
$ cmake --build .

Then, you can see the executable file.

Run it!!

$ ./cmake_example

It works well~ We did it!

Add Header File

Let's extend our project with a header file.

// src/version/version.h

#ifndef __VERSION_H__
#define __VERSION_H__

#define VERSION   (1)

#endif

// src/main.c

#include <stdio.h>
#include "version/version.h"

int main(int argc, char* argv[]) {
    printf("Hello CMake!\n");
    printf("Version: %d\n", VERSION);
    return 0;
}

Build a project again and run it.

Cool!!

Multiple File Project

Let's extend our project more.

We will separate the version into a different file.

// src/version/version.h

#ifndef __VERSION_H__
#define __VERSION_H__

#define VERSION   (1)

extern void print_version(int version);

#endif

// src/version/version.c

#include <stdio.h>
#include "version.h"

void print_version(int version) {
  printf("Version: %d\n", version);
}

// src/main.c

#include <stdio.h>
#include "version/version.h"

int main(int argc, char* argv[]) {
    printf("Hello CMake!\n");
    print_version(VERSION);
    return 0;
}

And, add the new source file into our CMake configuration.

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)

project(cmake_example VERSION 1.0.0)

add_executable(cmake_example src/main.c src/version/version.c)

It works well.

List Up Source Files Using Glob Pattern

It is tiresome to list up the source files whenever it is added into our project.

Let's use a glob pattern!!

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)

project(cmake_example VERSION 1.0.0)

file(GLOB_RECURSE SOURCES
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
  "${CMAKE_CURRENT_SOURCE_DIR}/src/version/*.c"
)

add_executable(cmake_example ${SOURCES})

It is the same functioning as previous, but quite efficient.

Using Library

Lastly, let's learn how to make and use static libraries.

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)

project(cmake_example VERSION 1.0.0)

file(GLOB_RECURSE SOURCES
  "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c"
)

add_library(verlib STATIC
  "${CMAKE_CURRENT_SOURCE_DIR}/src/version/version.c"
)

# for entire projects
link_libraries(verlib)

add_executable(cmake_example ${SOURCES})

# for specific project
target_link_libraries(cmake_example verlib)

add_library(LIBNAME STATIC FILES...) makes a static library with specified files.

target_link_libraries(PROJECTNAME LIBNAME) links a static library to the project.

Useful Tips

Let me introduce a few more tips.

Include Directories

# CMakeLists.txt

# for entire projects
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/version")

# for specific project
target_include_directories(cmake_example
  PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src/version"
)

We can specify the include directories.

// src/main.c

#include "version.h"

Then, we can easily include the header file in the specified directories.

It is convenient, but I prefer the specifying whole path because it gives us more information when we read the code.

The choice is yours.

Definitions

Sometimes we need definitions in the build system.

# CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)

project(cmake_example VERSION 1.0.0)

# for entire projects
add_definitions(
  -DOPTION
  -DOPTION_STR="opt"
)

file(GLOB_RECURSE SOURCES
  "${PROJECT_SOURCE_DIR}/src/*.c"
)

add_library(verlib STATIC
  "${CMAKE_CURRENT_SOURCE_DIR}/src/version/version.c"
)

link_libraries(verlib)

add_executable(cmake_example ${SOURCES})

# for specific project
target_compile_definitions(cmake_example
  PUBLIC OPTION
  PUBLIC OPTION_STR="opt"
)

target_compile_definitions() will help us in this case.

// src/main.c

#include <stdio.h>
#include "version/version.h"

int main(int argc, char* argv[]) {
    printf("Hello CMake!\n");
    print_version(VERSION);
#ifdef OPTION
    printf("Option: %s\n", OPTION_STR);
#endif
    return 0;
}

We can use it in our code like the above example.

Change Toolchain

We can change the toolchain configuration.

# CMakeLists.txt

set(CMAKE_C_COMPILER "/usr/bin/gcc")
set(CMAKE_C_FLAGS "-std=c11 -Os -g3 -Wall -Wextra -Wshadow")

You can change the toolchain configuration using set() and built-in variables like the above example.

You can also make your own variable using set().

Debug Print

We can print the CMake variables.

# CMakeLists.txt

include(CMakePrintHelpers)

message("${PROJECT_SOURCE_DIR}")
message(PROJECT_BINARY_DIR=${PROJECT_BINARY_DIR})
cmake_print_variables(CMAKE_CURRENT_SOURCE_DIR)

The result looks like the below.

This is sometimes useful.

Specify Build Output Folder

# CMakeLists.txt

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)

This example code makes bin and lib folders and creates executable and library files in those folders, respectively.

Intermediate Build Output Folder

The intermediate build outputs are generated in the current folder.

So, we can easily generate it in the folder that we want.

$ mkdir build && cd build
$ cmake ..
$ cmake --build .

Conclusion

CMake helps us to build easily C project.

I hope you enjoy with CMake like me.

댓글