The cmake repository contains some examples from my learning.
1. Why CMake?
There are many Make tools, such as GNU Make, Qt's qmake, Microsoft's MS nmake, BSD Make (pmake), Makepp, and so on. These Make tools follow different specifications and standards, and the Makefile formats they execute vary widely. This creates a serious problem: if software is meant to be cross-platform, it must be guaranteed to compile on different platforms. With the Make tools above, you would have to write a Makefile for each standard, which would be maddening. CMake is a tool designed to solve exactly this problem: it first lets developers write a platform-independent CMakeList.txt file to customise the whole build process, and then generates the required native Makefiles and project files according to the target user's platform.
2. make install
Customising the paths of the compiled .o files and .h files
2.1 CMakeLists.txt in the main directory
cmake_minimum_required(VERSION 2.8) project(demo1) #PROJECT_SOURCE_DIR/CMAKE_SOURCE_DIR/_SOURCE_DIR: the path of the folder containing the nearest CMakeLists.txt file that contains the PROJECT() command. #PROJECT_BINARY_DIR/CMAKE_BINARY_DIR/_BINARY_DIR: the directory where the cmake command is run, i.e. where the project build takes place. configure_file( "${PROJECT_SOURCE_DIR}/config.h.in" "${PROJECT_BINARY_DIR}/config.h" ) option(USE_MYMATH "Use the provide library" ON) if(USE_MYMATH) include_directories("${PROJECT_SOURCE_DIR}/math") add_subdirectory(math) set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions) endif(USE_MYMATH) aux_source_directory(. DIR_SRCS) # Specify the generated exe file add_executable(Demo1 ${DIR_SRCS}) target_link_libraries(Demo1 ${EXTRA_LIBS}) # Specify the output directories install(TARGETS Demo1 DESTINATION bin) install(FILES "${PROJECT_BINARY_DIR}/config.h" DESTINATION include)- cmake_minimum_required: the minimum required CMake version
- project: the project name
- configure_file: reads the configuration file; just write config.h.in here, and config.h will be generated automatically
- option: a switch with two values, ON and OFF
- Variables inside if don't need ${}
- include_directories: the paths in which to look for header files
- add_subdirectory: adds a subfolder, which holds some of the classes needed
- set: assigns the values that follow to the first variable; here:
- set(EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
- EXTRA_LIBS ${EXTRA_LIBS} means: if EXTRA_LIBS already has a value, append to it; otherwise it is empty
- Same as path=${path}:/usr/
- aux_source_directory: finds the source files to build in the current folder
- add_executable: the name of the generated executable
- target_link_libraries: links the sub-libraries (the functions and classes main needs)
- install(TARGETS Demo1 DESTINATION bin): the path for the .o files; the default prefix is /usr/local/, so here it is /usr/local/bin
- You can use set to modify the built-in variable
CMAKE_INSTALL_PREFIX - install(FILES “${PROJECT_BINARY_DIR}/config.h” DESTINATION include): the path for the .h files, here /usr/local/include’
2.2 CMakeLists.txt in the math directory
aux_source_directory(. DIR_LIB_SRCS) # Build the MathFunctions library add_library (MathFunctions ${DIR_LIB_SRCS}) install(TARGETS MathFunctions DESTINATION bin) install(FILES MathFunctions.h DESTINATION include)
2.3 Adding tests to the project
CMake provides a testing tool called CTest. All we need to do is call a series of add_test commands in the CMakeLists file in the project root directory.
# Enable testing enable_testing() # Test whether the program runs successfully add_test (test_run Demo 5 2) # Test whether the help message is shown correctly add_test (test_usage Demo) set_tests_properties (test_usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent") # Test 5 squared add_test (test_5_2 Demo 5 2) set_tests_properties (test_5_2 PROPERTIES PASS_REGULAR_EXPRESSION "is 25") # Test 10 to the power of 5 add_test (test_10_5 Demo 10 5) set_tests_properties (test_10_5 PROPERTIES PASS_REGULAR_EXPRESSION "is 100000") # Test 2 to the power of 10 add_test (test_2_10 Demo 2 10) set_tests_properties (test_2_10 PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")Or use a macro
# Define a macro to simplify testing macro (do_test arg1 arg2 result) add_test (test_${arg1}_${arg2} Demo ${arg1} ${arg2}) set_tests_properties (test_${arg1}_${arg2} PROPERTIES PASS_REGULAR_EXPRESSION ${result}) endmacro (do_test) # Use the macro to run a series of data tests do_test (5 2 "is 25") do_test (10 5 "is 100000") do_test (2 10 "is 1024")
3. configure_file
- configure_file(input output options)
- Copies a file (given by the input argument) to the specified location (given by the output argument) and modifies its contents according to options.
- The configure_file command is generally used for custom build options or custom macros. According to the rules specified by options, configure_file automatically converts the cmakedefine keywords and their contents in the input file.
Demo
```
#cmakedefine var1
#cmakedefine var2 "@var2@" #Note: the name between the @@ must match the variable name after cmakedefine
#cmakedefine var3 "${var3}" #Note: the name inside ${} must match the variable name after cmakedefine
```
4. Getting the Git hash and version with CMake: a complete example
GitHub code
Sometimes we need to record information such as the version number, the Git hash and the build time in a project, but obviously we don't want to fill in the Git hash and build time by hand. Here is a way to write this information into a header file and then compile it into a .so library or an executable.
That way, these values can be obtained through an interface provided by the library or from the output printed by the executable.
4.1 The utils.cmake file
```
# Used to get the Git hash and branch
macro(get_git_hash _git_hash)
find_package(GIT QUIET)
if(GIT_FOUND)# GIT_FOUND is a built-in variable
execute_process( # cmake: execute_process runs an external command
# COMMAND is the command-line command to run
# GIT_EXECUTABLE is the path to the git executable; -1 means the last commit
#--pretty=: display commit history in another format; options include oneline, short, medium, full, fuller, email, raw and format:<string>; the default is medium, e.g.:
# --pretty=oneline: one line per commit, showing only the hash and the commit message (--oneline can also be used as a standalone option)
# --pretty=format:” ": controls the format of the displayed records, e.g.:
# %H full hash of the commit object
# %h abbreviated hash of the commit object
# %T full hash of the tree object
# %t abbreviated hash of the tree object
# %P full hash of the parent object
# %p abbreviated hash of the parent object
# %an author name
# %ae author email address
# %ad author date (format can be customised with the -date= option)
# %ar author date, relative ("how long ago")
# %cn committer name
# What is the difference between author and committer?
# Relationship between author and committer: the author is the one who modified the program, the committer is the one who committed the code (how could someone else pull your changes and commit them if you never committed them yourself?)
# In fact, the author is the person who actually made the change, and the committer is the person who finally committed that work to the repository. So when you publish a patch for a project and a core member merges it into the project, you are the author and that core member is the committer (got it)
# %ce committer email address
# %cd commit date (format can be customised with the -date= option)
# %cr commit date, relative ("how long ago")
# %s commit message
COMMAND ${GIT_EXECUTABLE} log -1 --pretty=format:%h
OUTPUT_VARIABLE ${_git_hash} # store the output string in a variable
# OUTPUT_VARIABLE: specifies the variable that receives what is written to standard output;
OUTPUT_STRIP_TRAILING_WHITESPACE # strip the trailing newline from the string
ERROR_QUIET # silence execution errors
WORKING_DIRECTORY # working directory
${CMAKE_CURRENT_SOURCE_DIR}
)
endif()
endmacro()
# get git branch
macro(get_git_branch _git_branch) # start of the macro
find_package(Git QUIET) # find Git; QUIET suppresses errors
if(GIT_FOUND)
execute_process( # run a child process
COMMAND ${GIT_EXECUTABLE} symbolic-ref --short -q HEAD
OUTPUT_VARIABLE ${_git_branch} # store the output string in a variable
OUTPUT_STRIP_TRAILING_WHITESPACE # strip the trailing newline from the string
ERROR_QUIET # silence execution errors
WORKING_DIRECTORY # working directory
${CMAKE_CURRENT_SOURCE_DIR}
)
endif()
endmacro() # end of the macro
```
- Use macro to define two macros
- Use execute_process to run external commands
- OUTPUT_VARIABLE captures what is printed to the command line and assigns it to a variable
4.2 CMakeLists.txt
```
cmake_minimum_required(VERSION 2.9)
include(utils.cmake)
project(demo_git_config)
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")
message("Build timestamp is ${BUILD_TIMESTAMP}")
set(VERSION_MAJOR 0)
set(VERSION_MINOR 0)
set(VERSION_PATCH 1)
message("Version is ${VERSION_MAJOR} ${VERSION_MINOR} ${VERSION_PATCH}")
set(GIT_HASH "")
get_git_hash(GIT_HASH)
message("Git hash is ${GIT_HASH}")
set(GIT_BRANCH "")
get_git_branch(GIT_BRANCH)
message("Git branch is ${GIT_BRANCH}")
configure_file(
"${PROJECT_SOURCE_DIR}/include/utils.h.in"
"${PROJECT_SOURCE_DIR}/include/utils.h"
)
add_executable(${PROJECT_NAME} main.cpp)
include_directories(
${PROJECT_SOURCE_DIR}/include
)
install(TARGETS ${PROJECT_NAME}
DESTINATION ${PROJECT_SOURCE_DIR})
```
- Calls the macros
4.3 utils.h.in
```
#ifndef UTILS_H_IN
#define UTILS_H_IN
#define VERSION_MAJOR @VERSION_MAJOR@
#define VERSION_MINOR @VERSION_MINOR@
#define VERSION_PATCH @VERSION_PATCH@
#define BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"
#define GIT_BRANCH "@GIT_BRANCH@"
#define GIT_HASH "@GIT_HASH@"
#endif // UTILS_H_IN
```
4.4 Run cmake .
The generated utils.h is as follows
```
#ifndef UTILS_H_IN
#define UTILS_H_IN
#define VERSION_MAJOR 0
#define VERSION_MINOR 0
#define VERSION_PATCH 1
#define BUILD_TIMESTAMP "2022-11-26 21:35:07"
#define GIT_BRANCH "master"
#define GIT_HASH "0e1f110"
#endif // UTILS_H_IN
```
4.5 Link the .o files with make to build the executable
The output is as follows
```
(base) moule@mouledeMacBook-Air demo_git_config % ./demo_git_config
version is 0, 0, 1
timestamp is 2022-11-26 21:35:07
git is master, 0e1f110
```