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.
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.
'Embedded' 카테고리의 다른 글
[Embedded] Unittest using GoogleTest on CMake (0) | 2021.10.13 |
---|---|
[C] Preprocessors - #if, #ifdef, #if defined (0) | 2021.06.11 |
[C] Get return address of functions (__builtin_return_address) (0) | 2021.03.02 |
댓글