参考自官方教程 https://cmake.org/cmake/help/latest/guide/tutorial/index.html.
基于cmake 3.26.3

目录

  1. 最基础的工程
  2. 指定c++版本
  3. 传递宏
  4. 自建库的方法
  5. 为自建库添加开关
  6. 更好的链接库的方法
  7. 安装
  8. 传递宏
  9. GoogleTest

最基础的工程

目录结构如下:

.
├── 1.cpp
└── CMakeLists.txt

其中1.cpp:

#include <iostream>

int main()
{
	std::cout<<"hello cmake"<<std::endl;
	return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.12)
project(basic)
add_executable(${PROJECT_NAME} 1.cpp)

编译的两种方法:
方法1

mkdir build
cd build
cmake ..
make

方法2

mkdir build
cmake -S . -B build #-S指定源码路径,-B指定构建路径
cmake --build build

此时目录变为:

.
├── 1.cpp
├── CMakeLists.txt
└── build
    ├── CMakeCache.txt
    ├── CMakeFiles
    ├── Makefile
    ├── basic
    └── cmake_install.cmake

执行:

[~/code/cmake_learn/basic]$ ./build/basic      
hello cmake

指定c++版本

方法1:

set(CMAKE_CXX_STANDARD 20) #指定默认使用c++20
set(CMAKE_CXX_STANDARD_REQUIRED ON) #不满足c++20时报错

可以指定的c++版本可以通过CXX_STANDARD查看。

方法2:

#注意要写在add_executable之后
target_compile_feature(${PROJECT_NAME} PUBLIC cxx_std_20)

所有支持的feature可以通过CMAKE_KNOWN_FEATURES查看。此外还有CMAKE_C_KNOWN_FEATURES,CMAKE_CUDA_KNOWN_FEATURES等。

传递宏

通过configure_file可以将CMakeLists.txt中定义的参数通过形如@VAR@的宏的形式传入源文件,其流程如下: configure_file
首先在工程目录中添加config.h.in:

.
├── 1.cpp
├── CMakeLists.txt
├── build
└── config.h.in

1.传递版本号
project中添加版本号:

project(basic VERSION 1.2) #此时名为basic_VERSION_MAJOR和basic_VERSION_MINOR的两个变量会被内部自动创建

2.添加自定义的变量:

set(VAR1 "this is var1")

3.修改config.h.in文件为:

#define version_major @basic_VERSION_MAJOR@
#define version_minor @basic_VERSION_MINOR@
#define var1 "@VAR1@"

4.关联到cmake

#cmake将修改config.h.in,并将修改后的结果复制到config.h
configure_file(config.h.in config.h) 
#由于生成的config.h在构建目录(本例中即build目录)里,因此需要将改目录包含进来
#注意要写到add_executable之后
target_include_directories(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}") 

5.完整的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.12)
project(basic VERSION 1.2)

set(VAR1 "this is var1")
configure_file(config.h.in config.h)

add_executable(${PROJECT_NAME} 1.cpp)

target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20)
target_include_directories(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}")

6.修改1.cpp为:

#include <iostream>
#include "config.h"

int main()
{
	std::cout<<version_major<<","<<version_minor<<std::endl;
	std::cout<<var1<<std::endl;
	return 0;
}

7.编译:

cmake -S . -B build
cmake --build build

之后build目录中便会出现config.h文件:

#define version_major 1
#define version_minor 2
#define var1 "this is var1"

可见cmake将定义的变量传入了config.h文件,而1.cpp中include了config.h,因此执行后结果如下:

[~/code/cmake_learn/basic/build]$ ./basic
1,2
this is var1

自建库的方法

根目录下创建strlib目录,中间放入strlib.h,str.cpp,CMakeLists.txt文件:

.
├── 1.cpp
├── CMakeLists.txt
├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── Makefile
│   ├── basic
│   ├── cmake_install.cmake
│   ├── config.h
│   ├── mathlib
│   └── strlib
├── config.h.in
└── strlib
    ├── CMakeLists.txt
    ├── strlib.cpp
    └── strlib.h

/strlib/CMakeLists.txt:

add_library(strlib strlib.cpp)

/strlib/strlib.h:

#pragma once
#include <iostream>

namespace strlib{
	std::string hello();
}

/strlib/strlib.cpp

#include "strlib.h"

namespace strlib{
	std::string hello(){
		return "hello";
	}
}

至此完成了名为strlib的库的准备,然后在/CMakelists.txt中添加这个库:

add_subdirectory(strlib) #添加库
#add_executable....
target_include_directories(
	${PROJECT_NAME} PUBLIC 
	"${PROJECT_BINARY_DIR}" 
	"${PROJECT_SOURCE_DIR}/strlib" #指向库里面的头文件
)
target_link_libraries(
	${PROJECT_NAME} PUBLIC 
	strlib #链接库
)

之后就可以在1.cpp中使用strlib了:

#include <iostream>
#include "config.h"
#include "strlib.h"

int main()
{
	std::cout<<version_major<<","<<version_minor<<std::endl;
	std::cout<<var1<<std::endl;
	std::cout<<strlib::hello()<<std::endl;
	return 0;
}

编译后执行输出:

[~/code/cmake_learn/basic/build]$ ./basic 
1,2
this is var1
hello

为自建库添加开关

也就是通过option指令来在cmake时通过参数来选择是否将库包含入源文件:

option(USE_STRLIB "if use strlib" ON) #添加USE_STRLIB选项作为开关

之后将库的include和link方式修改为根据list添加,然后根据USE_STRLIB的ON与否来向list中添加内容:

if(USE_STRLIB)
	add_subdirectory(strlib)
	list(APPEND extra_include "${PROJECT_SOURCE_DIR}/strlib")
	list(APPEND extra_lib strlib)
endif()
target_include_directories(${PROJECT_NAME} PUBLIC ${extra_include})
target_link_libraries(${PROJECT_NAME} PUBLIC ${extra_lib})

当USE_STRLIB为ON时,strlib被添加进两个list,最终加入target。 在源文件中我们还需要知道USE_STRLIB是否被设置成了ON,这时就需要之前的configure_file指令了。在config.h.in中添加:

#cmakedefine USE_STRLIB

当USE_STRLIB=ON时,生成的config.h会被添加#define USE_STRLIB
当USE_STRLIB=OFF时,生成的config.h会被添加/* #udef USE_STRLIB*/
于是源文件include了config.h之后就可以通过判断USE_STRLIB是否被定义来决定是否使用strlib:

#include <iostream>
#include "config.h"
#ifdef USE_STRLIB
	#include "strlib.h"
#endif

int main()
{
	std::cout<<version_major<<","<<version_minor<<std::endl;
	std::cout<<var1<<std::endl;
#ifdef USE_STRLIB
	std::cout<<strlib::hello()<<"(lib)"<<std::endl;
#else
	std::cout<<"hello(plain)"<<std::endl;
#endif
	return 0;
}

完整的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.12)
project(basic VERSION 1.2)

#--- 设置config.h.in的指令注意要写在configure_file前面 ---#
set(VAR1 "this is var1")
option(USE_STRLIB "if use the strlib" ON)
#-----------------------------------------------------#
configure_file(config.h.in config.h)

add_executable(${PROJECT_NAME} 1.cpp)

if(USE_STRLIB)
	add_subdirectory(strlib)
	list(APPEND extra_include "${PROJECT_SOURCE_DIR}/strlib")
	list(APPEND extra_lib strlib)
endif()

target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20)
target_include_directories(
	${PROJECT_NAME} PUBLIC 
	"${PROJECT_BINARY_DIR}" 
	${extra_include}
)
target_link_libraries(
	${PROJECT_NAME} PUBLIC 
	${extra_lib}
)

之后在编译时就可以通过传入-DUSE_STRLIB来开关strlib,
不使用时:

[~/code/cmake_learn/basic/build]$ cmake .. -DUSE_STRLIB=OFF;make;./basic
...
1,2
this is var1
hello(plain)

使用时:

[~/code/cmake_learn/basic/build]$ cmake .. -DUSE_STRLIB=ON;make;./basic
...
1,2
this is var1
hello(lib)

更好的链接库的方法

在strlib的CMakeLists.txt里include需要的头文件而不在basic的CMakeLists.txt里include。
strlib的CMakeLists.txt:

add_library(strlib strlib.cpp)
target_include_directories(strlib INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")

这样一来basic的CMakeLists.txt中就可以不用再引用strlib的头文件了:

cmake_minimum_required(VERSION 3.12)
project(basic VERSION 1.3)

set(VAR1 "this is var1")
option(USE_STRLIB "if use the strlib" ON)
configure_file(config.h.in config.h)

add_executable(${PROJECT_NAME} 1.cpp)

if(USE_STRLIB)
	add_subdirectory(strlib)
	list(APPEND extra_lib strlib)
endif()

target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20)
target_include_directories(
	${PROJECT_NAME} PUBLIC 
	"${PROJECT_BINARY_DIR}" 
)
target_link_libraries(
	${PROJECT_NAME} PUBLIC 
	${extra_lib}
)

安装

有时我们希望可以直接用指令的方式调用程序,在cmake中通过install就可以做到。 在这个例子中打算用date库做一个简单的查询本地时间的程序。

1.工程目录下ctime.cpp和cmakelist文件,并创建timelib目录,cd进去克隆date库:

mkdir timelib
cd timelib
git clone https://github.com/HowardHinnant/date.git

在内部创建timelib.cpp,timelib.h以及cmakelist.txt文件。
之后项目的结构如下:

.
├── CMakeLists.txt
├── ctime.cpp
└── timelib
    ├── CMakeLists.txt
    ├── date
    ├── timelib.cpp
    └── timelib.h

按照用timelib封装date的思路,timelib/CMakeLists.txt如下:

add_library(timelib timelib.cpp date/src/tz.cpp)

find_package(CURL REQUIRED)

add_subdirectory(date)

target_compile_features(timelib PRIVATE cxx_std_14)

target_include_directories(timelib 
    INTERFACE 
    "${CMAKE_CURRENT_SOURCE_DIR}"
    PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/date/include"
)

target_link_libraries(timelib PRIVATE CURL::libcurl)

说明:

  • 根据date的安装文档需要用到curl,所以cmakelist中链接了curl库:
    find_package(CURL REQUIRED)
    ...
    target_link_libraries(timelib PRIVATE CURL::libcurl)
    
  • macOS上推荐c++14:
    target_compile_features(timelib PRIVATE cxx_std_14)
    
  • timelib自身的头文件目录需要暴露给工程目录下的源文件,因此是INTERFACE;自身的源文件和date的源文件需要使用date下的include目录,因此是PRIVATE:
    target_include_directories(timelib 
      INTERFACE 
      "${CMAKE_CURRENT_SOURCE_DIR}"
      PRIVATE
      "${CMAKE_CURRENT_SOURCE_DIR}/date/include"
    )
    
  • 当然为了省事直接一个PUBLIC也是没问题的:
    target_include_directories(timelib 
      PUBLIC 
      "${CMAKE_CURRENT_SOURCE_DIR}"
      "${CMAKE_CURRENT_SOURCE_DIR}/date/include"
    )
    

2.工程目录下的cmakelist.txt如下:

cmake_minimum_required(VERSION 3.5)
project(ctime)

add_subdirectory(timelib)

add_executable(ctime ctime.cpp)

target_link_libraries(ctime PUBLIC timelib)

install(TARGETS ctime DESTINATION bin)

最后一行的install指令指定将生成的build目录下的ctime可执行文件安装到bin目录下(默认为/usr/local/bin)。

3.具体实现如下
ctime.cpp:

#include "timelib.h"
#include <iostream>

int main()
{
	std::cout<<timelib::get_current_time()<<std::endl;
	return 0;
}

timelib.h:

#pragma once
#include <iostream>

namespace timelib
{
	std::string get_current_time();
}

timelib.cpp:

#include "date/tz.h"
#include <iostream>

namespace timelib
{
	std::string get_current_time()
	{
		using namespace date;
		using namespace std::chrono;
		auto local = make_zoned(current_zone(), system_clock::now());		
		return format("%Y-%m-%d %H:%M:%S %Z (%A)", local);
	}
}

4.编译安装:

mkdir build
cd build
cmake ..
make

a)默认的安装路径是/usr/local/bin需要root权限:

sudo cmake --install .
#或者sudo make install

b)如果没有root权限可以安装到用户目录:

#如果没有则会在$HOME下自动创建bin目录
cmake --install . --prefix "$HOME"

这种情况下需要在~/.zshrc(或~/.bashrc)中将$HOME/bin加入path,添加:

export PATH="$PATH:$HOME/bin"

之后source ~/.zshrc(或~/.bashrc)

5.安装好之后就可以用指令的方式调用程序了:

~/code/cmake_learn/ctime]$ ctime   
2023-04-15 20:21:27.076781 JST (Saturday)

具体可以查看github仓库

传递宏

add_defintions命令为源文件的编译添加-D指令,也就是添加宏。 通过定义cmake变量来控制add_defintions的执行,从而可以实现宏的开关/传递:

option(DEFINE_TEST_TREE "test tree" OFF)
if(DEFINE_TEST_TREE)
    add_definitions(-DTEST_TREE) #cmake官方更推荐下面的指令
    #add_compile_definitions(TEST_TREE)
endif()

设置DEFINE_TEST_TREE=ON即可在源文件中产生一个TEST_TREE的宏定义

cmake .. -DDEFINE_TEST_TREE=ON
make
#ifdef TEST_TREE
printf("test tree\n");
#endif

需要注意的是:

  • 如果存在subdirectory,那么开关需要写在subdirectory前面以保证传递
  • cmake指令中设置的DEFINE_TEST_TREE会被cache,所以最好根据需要显式的设置。
  • 设置结果可以通过COMPILE_DEFINITIONS查看:
    get_directory_property( DirDefs COMPILE_DEFINITIONS)
    message( "COMPILE_DEFINITIONS = ${DirDefs}" )
    

GoogleTest

GoogleTest是常用的单元测试框架,详细可以参考官方教程

cmake中添加gtest

cmake_minimum_required(VERSION 3.10)
project(hello)

enable_testing()

find_package(GTest REQUIRED)

include(GoogleTest)

add_executable(test_hello test_hello.cpp hello.cpp)
target_link_libraries(test_hello GTest::GTest GTest::Main)

gtest_discover_tests(test_hello)

test_hello.cpp如下

#include "gtest/gtest.h"
#include <iostream>

TEST(HelloTest, equal) {
    EXPECT_EQ(1,1.1) << "value not match";    
}

TEST(HelloTest, little_than) {
    EXPECT_LT(1,2);
}

正常编译运行./test_hello后得到如下结果

$ ./test_hello 
Running main() from ../googletest/src/gtest_main.cc
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from HelloTest
[ RUN      ] HelloTest.equal
/home/jiamian/gtest_test/test_hello.cpp:39: Failure
Expected equality of these values:
  1
  1.1
value not match
[  FAILED  ] HelloTest.equal (0 ms)
[ RUN      ] HelloTest.little_than
[       OK ] HelloTest.little_than (0 ms)
[----------] 2 tests from HelloTest (0 ms total)

[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] HelloTest.equal

可以发现第一个测试equal没有通过,第二个测试little_than通过了。