Simple CMake tutorial (miniob version)
A Simple Introduction to CMake
First, build an intuition: CMake is platform-independent. You won't specify which compiler or linker to use, nor will you write shell commands. It's best to think of it as a new object-oriented language.
Minimum Example
First, look at this minimum example:
cmake_minimum_required(VERSION 3.8)
project(Calculator LANGUAGES CXX)
add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
target_include_directories(calclib PUBLIC include)
target_compile_features(calclib PUBLIC cxx_std_11)
add_executable(calc apps/calc.cpp)
target_link_libraries(calc PUBLIC calclib)
Bold text indicates required items.
cmake_minimum_required(VERSION 3.8)
- Specifies the CMake version standard to use.
project(Calculator LANGUAGES CXX)
- Defines project properties; here the name is
Calculator
. The project name is not related to the target file. - Most built-in CMake functions follow this syntax:
function([MODE ...] target ATTR1 val1 ATTR2 val2 ...)
, whereval
can be a single element or a list (separated by spaces or semicolons).
- Defines project properties; here the name is
add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
:- Links
calclib.cpp
andlib.hpp
as a static library intocalclib
(here,calclib
is a library).
- Links
target_include_directories(calclib PUBLIC include)
- Specifies the include directory for the target (here, the
include
directory in the project path).
- Specifies the include directory for the target (here, the
target_compile_features(calclib PUBLIC cxx_std_11)
- Specifies compilation options (here, equivalent to
--std=c++11
).
- Specifies compilation options (here, equivalent to
add_executable(calc apps/calc.cpp)
- Defines an executable file and compiles it using
calc.cpp
.
- Defines an executable file and compiles it using
target_link_libraries(calc PUBLIC calclib)
- Links
calclib
as a library to the executablecalc
.
- Links
miniob
This is a project with a nested structure:
- minidb
- benchmark
- deps/common
- src
- obclient
- observer
- test/perf
- tools
- unittest
- (a set of unit tests)
Only minidb
and observer
are analyzed here.
minidb (Root Project)
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 20)
project(minidb)
MESSAGE(STATUS "This is Project source dir " ${PROJECT_SOURCE_DIR})
MESSAGE(STATUS "This is PROJECT_BINARY_DIR dir " ${PROJECT_BINARY_DIR})
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
OPTION(ENABLE_ASAN "Enable build with address sanitizer" ON)
OPTION(WITH_UNIT_TESTS "Compile miniob with unit tests" ON)
OPTION(CONCURRENCY "Support concurrency operations" OFF)
OPTION(STATIC_STDLIB "Link std library static or dynamic, such as libgcc, libstdc++, libasan" OFF)
- Function names are case-insensitive; within functions, keywords are recommended to be uppercase, and variables lowercase.
set(CMAKE_CXX_STANDARD 20)
- Equivalent to an assignment statement.
CMAKE_CXX_STANDARD
= 20 - If a variable hasn't appeared before, it should be a built-in keyword. Refer to the manual (RTFM) for specifics.
- Equivalent to an assignment statement.
MESSAGE(STATUS "This is Project source dir " ${PROJECT_SOURCE_DIR})
- Print command.
${foobar}
indicates dereferencing.
[cmake] -- This is Project source dir /home/junyu33/Desktop/github/miniob
OPTION(foobar "comment" ON/OFF)
- Can be understood as a
set
for boolean types.
- Can be understood as a
MESSAGE(STATUS "HOME dir: $ENV{HOME}")
#SET(ENV{variable name} value)
IF(WIN32)
MESSAGE(STATUS "This is windows.")
ADD_DEFINITIONS(-DWIN32)
ELSEIF(WIN64)
MESSAGE(STATUS "This is windows.")
ADD_DEFINITIONS(-DWIN64)
ELSEIF(APPLE)
MESSAGE(STATUS "This is apple")
# normally __MACH__ has already been defined
ADD_DEFINITIONS(-D__MACH__ )
ELSEIF(UNIX)
MESSAGE(STATUS "This is UNIX")
ADD_DEFINITIONS(-DUNIX -DLINUX)
ELSE()
MESSAGE(STATUS "This is UNKNOW OS")
ENDIF(WIN32)
- Note that
${foobar}
can also be dereferenced inside quotes. - Here,
WIN32
,WIN64
,APPLE
, etc., are all built-in keywords. - Conditions considered true in
IF ELSEIF
include:ON
,YES
,TRUE
,Y
, or any non-zero number.
- Conditions considered false include:
0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,""
, or strings ending with-NOTFOUND
.
ADD_DEFINITIONS
: Follows the same syntax as adding-D
compilation options.
# This is for clangd plugin for vscode
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -Wall -Werror")
IF(DEBUG)
MESSAGE(STATUS "DEBUG has been set as TRUE ${DEBUG}")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O0 -g -DDEBUG ")
ADD_DEFINITIONS(-DENABLE_DEBUG)
ELSEIF(NOT DEFINED ENV{DEBUG})
MESSAGE(STATUS "Disable debug")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O2 -g ")
ELSE()
MESSAGE(STATUS "Enable debug")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O0 -g -DDEBUG")
ADD_DEFINITIONS(-DENABLE_DEBUG)
ENDIF(DEBUG)
- Note that
CMAKE_COMMON_FLAGS
is a user-defined variable. You can observe that additions here are incremental. ENV
: Retrieves system environment variables.ENV{foo}
fetches the environment variablefoo
.
IF (CONCURRENCY)
MESSAGE(STATUS "CONCURRENCY is ON")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -DCONCURRENCY")
ADD_DEFINITIONS(-DCONCURRENCY)
ENDIF (CONCURRENCY)
MESSAGE(STATUS "CMAKE_CXX_COMPILER_ID is " ${CMAKE_CXX_COMPILER_ID})
IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND ${STATIC_STDLIB})
ADD_LINK_OPTIONS(-static-libgcc -static-libstdc++)
ENDIF()
ADD_LINK_OPTIONS
: Similar to addingDEFINITION
as mentioned earlier, this specifies link options.STREQUAL
: Equivalent to==
. Note thatSTREQUAL
has higher precedence thanAND
.
IF (ENABLE_ASAN)
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -fno-omit-frame-pointer -fsanitize=address")
IF ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND ${STATIC_STDLIB})
ADD_LINK_OPTIONS(-static-libasan)
ENDIF()
ENDIF()
IF (CMAKE_INSTALL_PREFIX)
MESSAGE(STATUS "CMAKE_INSTALL_PREFIX has been set as " ${CMAKE_INSTALL_PREFIX} )
ELSEIF(DEFINED ENV{CMAKE_INSTALL_PREFIX})
SET(CMAKE_INSTALL_PREFIX $ENV{CMAKE_INSTALL_PREFIX})
ELSE()
SET(CMAKE_INSTALL_PREFIX /tmp/${PROJECT_NAME})
ENDIF()
MESSAGE(STATUS "Install target dir is " ${CMAKE_INSTALL_PREFIX})
IF (DEFINED ENV{LD_LIBRARY_PATH})
SET(LD_LIBRARY_PATH_STR $ENV{LD_LIBRARY_PATH})
string(REPLACE ":" ";" LD_LIBRARY_PATH_LIST ${LD_LIBRARY_PATH_STR})
MESSAGE(" Add LD_LIBRARY_PATH to -L flags " ${LD_LIBRARY_PATH_LIST})
LINK_DIRECTORIES(${LD_LIBRARY_PATH_LIST})
ENDIF ()
string(REPLACE srcStr dstStr dstVal srcVal)
: Similar functions includeREGEX REPLACE
.LINK_DIRECTORIES
: Similar toTARGET_LINK_DIRECTORIES
, but not targeting a specific object.
IF (EXISTS /usr/local/lib)
LINK_DIRECTORIES (/usr/local/lib)
ENDIF ()
IF (EXISTS /usr/local/lib64)
LINK_DIRECTORIES (/usr/local/lib64)
ENDIF ()
INCLUDE_DIRECTORIES(. ${PROJECT_SOURCE_DIR}/deps /usr/local/include)
# ADD_SUBDIRECTORY(src bin) bin is the target directory, which can be omitted
ADD_SUBDIRECTORY(deps)
ADD_SUBDIRECTORY(src/obclient)
ADD_SUBDIRECTORY(src/observer)
ADD_SUBDIRECTORY(test/perf)
ADD_SUBDIRECTORY(benchmark)
ADD_SUBDIRECTORY(tools)
EXISTS
: Checks if a path exists.INCLUDE_DIRECTORIES
: Similar toTARGET_INCLUDE_DIRECTORIES
, but not targeting a specific object.ADD_SUBDIRECTORY
: If the subdirectory contains a CMake project (e.g.,CMakeLists.txt
), recursively execute the project in the subdirectory.
IF(WITH_UNIT_TESTS)
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -fprofile-arcs -ftest-coverage")
enable_testing()
ADD_SUBDIRECTORY(unittest)
ENDIF(WITH_UNIT_TESTS)
SET(CMAKE_CXX_FLAGS ${CMAKE_COMMON_FLAGS})
SET(CMAKE_C_FLAGS ${CMAKE_COMMON_FLAGS})
MESSAGE(STATUS "CMAKE_CXX_FLAGS is " ${CMAKE_CXX_FLAGS})
INSTALL(DIRECTORY etc DESTINATION .
FILE_PERMISSIONS OWNER_WRITE OWNER_READ GROUP_READ WORLD_READ)
enable_testing()
: Built-in command to enable testing.- This command should be in the source directory root because ctest expects to find a test file in the build directory root. This command is automatically invoked when the CTest module is included, except if the BUILD_TESTING option is turned off.
INSTALL
: For theDIRECTORY
option, install files frometc
to the current directory with permissionsOWNER_WRITE OWNER_READ GROUP_READ WORLD_READ
.
Observer
MESSAGE(STATUS "This is CMAKE_CURRENT_SOURCE_DIR dir " ${CMAKE_CURRENT_SOURCE_DIR})
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR})
FILE(GLOB_RECURSE ALL_SRC *.cpp *.c)
SET(MAIN_SRC main.cpp)
MESSAGE("MAIN SRC: " ${MAIN_SRC})
FOREACH (F ${ALL_SRC})
IF (NOT ${F} STREQUAL ${MAIN_SRC})
SET(LIB_SRC ${LIB_SRC} ${F})
ENDIF()
MESSAGE("Use " ${F})
ENDFOREACH (F)
SET(LIBEVENT_STATIC_LINK TRUE)
FIND_PACKAGE(Libevent CONFIG REQUIRED)
FILE(GLOB_RECURSE ...)
:ALL_SRC = $(find . \( -name "*.c" -o -name "*.cpp" \))
FOREACH
:F
is the loop variable,${ALL_SRC}
is the collection to iterate over.FIND_PACKAGE
: Searches for configuration files in theLibevent
package, withREQUIRED
indicating a mandatory dependency.
SET(LIBRARIES common pthread dl libevent::core libevent::pthreads libjsoncpp.a)
# Specify target file locations
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
MESSAGE("Binary directory:" ${EXECUTABLE_OUTPUT_PATH})
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
MESSAGE("Archive directory:" ${LIBRARY_OUTPUT_PATH})
ADD_EXECUTABLE(observer ${MAIN_SRC})
TARGET_LINK_LIBRARIES(observer observer_static)
ADD_LIBRARY(observer_static STATIC ${LIB_SRC})
INCLUDE (readline)
MINIOB_FIND_READLINE()
INCLUDE(foobar)
: Includes the specified CMake file; if the file does not exist, searches for a file namedfoobar.cmake
inCMAKE_MODULE_PATH
.
# readline.cmake
MACRO (MINIOB_FIND_READLINE)
FIND_PATH(READLINE_INCLUDE_DIR readline.h PATH_SUFFIXES readline)
FIND_LIBRARY(READLINE_LIBRARY NAMES readline)
IF (READLINE_INCLUDE_DIR AND READLINE_LIBRARY)
SET(HAVE_READLINE 1)
ELSE ()
MESSAGE("cannot find readline")
ENDIF()
ENDMACRO (MINIOB_FIND_READLINE)
MACRO
: Defines a macro.find_path (<VAR> name1 [path1 path2 ...])
: Searches for a folder containingreadline.h
in subdirectories with the suffixreadline
, and saves the result toREADLINE_INCLUDE_DIR
.find_library (<VAR> name1 [path1 path2 ...])
: Searches for a library file namedreadline
.
Each library name given to the NAMES option is first considered as a library file name and then considered with platform-specific prefixes (e.g. lib) and suffixes (e.g. .so). Therefore one may specify library file names such as libfoo.a directly. This can be used to locate static libraries on UNIX-like systems.
[cmake] readline include dir: /usr/include/readline
[cmake] readline library: /usr/lib/libreadline.so
IF (HAVE_READLINE)
TARGET_LINK_LIBRARIES(observer_static ${READLINE_LIBRARY})
TARGET_INCLUDE_DIRECTORIES(observer_static PRIVATE ${READLINE_INCLUDE_DIR})
ADD_DEFINITIONS(-DUSE_READLINE)
MESSAGE ("observer_static use readline")
ELSE ()
MESSAGE ("readline is not found")
ENDIF()
SET_TARGET_PROPERTIES(observer_static PROPERTIES OUTPUT_NAME observer)
TARGET_LINK_LIBRARIES(observer_static ${LIBRARIES})
# Target must be defined after ADD_EXECUTABLE; programs are not subject to this restriction.
# Default permissions for TARGETS and PROGRAMS are OWNER_EXECUTE, GROUP_EXECUTE, and WORLD_EXECUTE, i.e., 755 permissions. Programs typically handle script-like files.
# Types include RUNTIME/LIBRARY/ARCHIVE, prog
INSTALL(TARGETS observer observer_static
RUNTIME DESTINATION bin
ARCHIVE DESTINATION lib)
SET_TARGET_PROPERTIES
: Sets properties for the target; here, it sets the target name toobserver
.install(TARGETS <target>... [...])
: Deploys the runtime paths forobserver
andobserver_static
targets tobin
, and archive file paths tolib
.
Miscellaneous Issues
What exactly does CMake do?
From a personal perspective, CMake provides a platform-independent abstraction, and the CMake build process essentially "translates" its language into platform-specific compilation and linking commands, assembling them into various files (on Linux, these would be the Makefile
and related dependency files).
We can find the corresponding, machine-generated Makefile
in the CMake build directory.
How to achieve functionality similar to make -B
or make -n
in CMake?
Please RTFM
For the former, you can use the --fresh
parameter.
For the latter, you can use --trace
to achieve it. Additionally, using make --trace-expand
can expand the corresponding variables.
Where does DEBUG
come from in the following code?
# This is for clangd plugin for vscode
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -Wall -Werror")
IF(DEBUG)
MESSAGE(STATUS "DEBUG has been set as TRUE ${DEBUG}")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O0 -g -DDEBUG ")
ADD_DEFINITIONS(-DENABLE_DEBUG)
ELSEIF(NOT DEFINED ENV{DEBUG})
MESSAGE(STATUS "Disable debug")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O2 -g ")
ELSE()
MESSAGE(STATUS "Enable debug")
SET(CMAKE_COMMON_FLAGS "${CMAKE_COMMON_FLAGS} -O0 -g -DDEBUG")
ADD_DEFINITIONS(-DENABLE_DEBUG)
ENDIF(DEBUG)
You did not check build.sh
:
function build
{
set -- "${BUILD_ARGS[@]}"
case "x$1" in
xrelease)
do_build "$@" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDEBUG=OFF
;;
xdebug)
do_build "$@" -DCMAKE_BUILD_TYPE=Debug -DDEBUG=ON
;;
*)
BUILD_ARGS=(debug "${BUILD_ARGS[@]}")
build
;;
esac
}
There is a compilation option -DDEBUG=ON
in this script. By tracing as described above or adding a log at the relevant location, you can confirm that the DEBUG
variable comes from the runtime parameters of cmake
.