快速入门 camke, 掌握 cmake 在 c/c++ 项目构建上魅力
认识 CMake
通过上图我们可以大致了解到: cmake 是一个跨平台项目构建工具, 通过 cmake
命令, 可以将讲简单的工程配置描述文件 CMakeFiles.txt (即指令合集) 构建成 makefile 文件,帮我们省去编写繁杂的 makefile, 然后在通过 make
命令将源代码编译成目标文件
使用 cmake 工具, 首先需要安装 cmake 到本地环境
安装好后, 接下来我们结合工程示例 https://github.com/oksep/cmake-begin ,逐一讲解 cmake 相关概念
简单工程 源码参考: v1_basic , 项目有如下文件结构:
1 2 3 4 5 6 7 8 9 10 . ├── CMakeLists.txt ├── include │ └── head.h └── src ├── add.cpp ├── div.cpp ├── main.cpp ├── mult.cpp └── sub.cpp
我们主要讲解 CMakeLists.txt 每个指令的含义(重点关注注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 cmake_minimum_required (VERSION 3.15 )set (CMAKE_CXX_STANDARD 17 )project (CALC)set (EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR} /bin)file (GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR} /src/*.cpp)file (GLOB HEAD_LIST ${CMAKE_CURRENT_SOURCE_DIR} /include /*.h)include_directories (${PROJECT_SOURCE_DIR} /include )add_executable (app ${SRC_LIST} )
执行 cmake . -B ./build
命令 cmake 会读取 CMakeLists.txt 的相关指令, 在 build 文件夹下生成以下构建文件:
1 2 3 4 5 build ├── CMakeCache.txt ├── CMakeFiles ├── Makefile └── cmake_install.cmake
执行构建命令 cmake --build build
, 生成目标文件 这里实际上是执行了 make 命令的相关操作, 等价于 cd build && make
:
1 2 3 4 5 6 7 [ 16%] Building CXX object CMakeFiles/app.dir/src/add.cpp.o [ 33%] Building CXX object CMakeFiles/app.dir/src/div.cpp.o [ 50%] Building CXX object CMakeFiles/app.dir/src/main.cpp.o [ 66%] Building CXX object CMakeFiles/app.dir/src/mult.cpp.o [ 83%] Building CXX object CMakeFiles/app.dir/src/sub.cpp.o[100% ] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v1_basic/bin/app [100% ] Built target app
执行 ./bin/app
一个简单的计算器程序就可以运行起来了:
1 2 3 4 5 a = 12, b = 4 a + b = 16 a - b = 8 a * b = 48 a / b = 3.000000
生成静态库/动态库 源码参考: v2_gen_lib , 主要讲解 CMakeLists.txt 文件的配置(重点关注注释):
1 2 3 4 5 6 7 8 9 10 11 ...set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR} /lib)add_library (calc_static STATIC ${SRC_LIST} )add_library (calc_shared SHARED ${SRC_LIST} )
这里可以对比 v1_basic 工程中的 CMakeLists.txt 文件, 配置生成了不同的目标文件:
#
command
windows format
linux format
macOS format
可执行文件
add_executable
.exe
/
/
静态库文件
add_library
.lib
.a
.a
动态库文件
add_library
.dll
.so
.dylib
同样执行 cmake 相关命令 cmake . -B build && cmake --build build
, 可以看到在 lib/ 文件夹下, 输出了对应的库文件
1 2 3 lib ├── libcalc_static.a # 静态库 └── libcalc_shared.dylib # 动态库
链接静态库 源码参考: v3_link_static_lib , 项目有如下文件结构:
1 2 3 4 5 6 7 8 ./v3_link_static_lib ├── CMakeLists.txt ├── include │ └── head.h # 将 v2_gen_lib 中的 head.h 头文件拷贝到这里 ├── lib │ └── libcalc.a # 将 v2_gen_lib 生成的静态库拷贝到这里 └── src └── main.cpp
使工程链接静态库, 在 CMakeLists.txt 的主要配置(重点关注注释):
1 2 3 4 5 6 7 8 9 10 11 12 ...include_directories (${PROJECT_SOURCE_DIR} /include )link_directories (${PROJECT_SOURCE_DIR} /lib)link_libraries (calc)add_executable (app ${SRC_LIST} )
执行 cmake . -B build && cmake --build build
后, libcalc.a 被打包到了可执行文件中, 运行 bin/app 静态库 libcalc.a 立即被加载到内存中
链接动态库 源码参考: v4_link_shared_lib
文件目录结构同 v3_link_static_lib, 我们将 v2_gen_lib 生成的动态库libcalc.dylib 放在 lib/ 路径下, CMakeLists.txt 主要差异在(重点关注注释):
1 2 3 4 5 6 7 8 9 10 include_directories (${PROJECT_SOURCE_DIR} /include )link_directories (${PROJECT_SOURCE_DIR} /lib)add_executable (app ${SRC_LIST} )target_link_libraries (app calc)
这里需要注意: 链接动态库, 是在 add_executable 生成 app 后再去配置的
接下来执行 cmake . -B build && cmake --build build
, 运行 bin/app , 只有当真正调用到 calc 相关函数的时候, libcalc.dylib 才会被加载到内存中
子模块 源码参考: v5_nested_project
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ├── CMakeLists.txt ├── calc │ ├── CMakeLists.txt │ └── src ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ └── src ├── test1 │ ├── CMakeLists.txt │ └── calc.cpp └── test2 ├── CMakeLists.txt └── sort.cpp
这里我们定义了四个子模块和一个根模块
计算器模块: calc/CMakeLists.txt 配置生成静态库
排序模块: sort/CMakeLists.txt 配置生成动态库
测试计算器模块: test1/CMakeLists.txt 配置测试链接静态库
测试排序模块: test2/CMakeLists.txt 配置测试链接动态库
根模块
在根模块 CMakeLists.txt 中有如下定义(重点关注注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cmake_minimum_required (VERSION 3.15 )project (test )set (CMAKE_CXX_STANDARD 17 )set (LIB_PATH ${CMAKE_SOURCE_DIR} /lib)set (EXEC_PATH ${CMAKE_SOURCE_DIR} /bin)set (HEAD_PATH ${CMAKE_SOURCE_DIR} /include )set (LIB_CALC calc)set (LIB_SORT sort)set (APPNAME1 test1)set (APPNAME2 test2)add_subdirectory (calc)add_subdirectory (sort)add_subdirectory (test1)add_subdirectory (test2)
子模块 calc 的 CMakeLists.txt 配置(重点关注注释):
1 2 3 4 5 6 7 8 9 10 11 ...file (GLOB SOURCES "src/*.cpp" )include_directories (${HEAD_PATH} )set (LIBRARY_OUTPUT_PATH ${LIB_PATH} )add_library (${LIB_CALC} STATIC ${SOURCES} )
其它子模块: sort/test1/test2 与 calc 模块类似, 都是使用了根模块声明的字段来配置各个子模块
接下来执行 cmake . -B build && cmake --build build
有以下日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- The C compiler identification is AppleClang 15.0.0.15000100 -- The CXX compiler identification is AppleClang 15.0.0.15000100 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done (0.9s) -- Generating done (0.0s) -- Build files have been written to: /Users/sep/Documents/cmake-tutorial/v5_nested_project/build [ 8%] Building CXX object calc/CMakeFiles/calc.dir/src/add.cpp.o [ 16%] Building CXX object calc/CMakeFiles/calc.dir/src/div.cpp.o [ 25%] Building CXX object calc/CMakeFiles/calc.dir/src/mult.cpp.o [ 33%] Building CXX object calc/CMakeFiles/calc.dir/src/sub.cpp.o [ 41%] Linking CXX static library /Users/sep/Documents/cmake-tutorial/v5_nested_project/lib/libcalc.a [ 41%] Built target calc [ 50%] Building CXX object sort/CMakeFiles/sort.dir/src/insert.cpp.o [ 58%] Building CXX object sort/CMakeFiles/sort.dir/src/select.cpp.o [ 66%] Linking CXX shared library /Users/sep/Documents/cmake-tutorial/v5_nested_project/lib/libsort.dylib [ 66%] Built target sort [ 75%] Building CXX object test1/CMakeFiles/test1.dir/calc.cpp.o [ 83%] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v5_nested_project/bin/test1 [ 83%] Built target test1 [ 91%] Building CXX object test2/CMakeFiles/test2.dir/sort.cpp.o[100% ] Linking CXX executable /Users/sep/Documents/cmake-tutorial/v5_nested_project/bin/test2 [100% ] Built target test2
此时项目里新增了各个子模块构建出来的产物:
1 2 3 4 5 6 7 . ├── bin │ ├── test1 │ └── test2 ├── lib │ ├── libcalc.a │ └── libsort.dylib
可以分别执行 bin/test1 和 bin/test2 查看效果
静态库链接静态库 源码参考: v6_static_link_static_lib
1 2 3 4 5 6 7 8 9 10 11 12 13 ├── CMakeLists.txt ├── calc │ ├── CMakeLists.txt │ └── src ├── include │ ├── calc.h │ └── sort.h ├── sort │ ├── CMakeLists.txt │ └── src └── test ├── CMakeLists.txt └── main.cpp
这里是对 v5_nested_project 做的改造, 各个子模块间的依赖关系有:
test -> sort.a(静态库) -> calc.a(静态库)
各模块配置r(重点关注注释):
calc/CMakeLists.txt
1 2 3 4 5 6 7 8 9 ...include_directories (${HEAD_PATH} )set (LIBRARY_OUTPUT_PATH ${LIB_PATH} )add_library (${LIB_CALC} STATIC ${SOURCES} )
sort/CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 ...include_directories (${HEAD_PATH} )link_libraries (${LIB_CALC} )set (LIBRARY_OUTPUT_PATH ${LIB_PATH} )add_library (${LIB_SORT} STATIC ${SOURCES} )
sort/src/insert.cpp
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #include "calc.h" #include "sort.h" void sort_insert (int *arr, int len) { int s = add (1 , 2 ); printf ("sort_insert call add: s = %d\n" , s); ... }
test/CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 ...include_directories (${HEAD_PATH} )link_directories (${LIB_PATH} )link_libraries (${LIB_SORT} )add_executable (test ${SOURCES} )
接下来执行 cmake . -B build && cmake --build build
便可以生成: 链接了静态库 sort (sort 链接了静态库 calc) 的可执行文件 bin/test
静态库链接动态库 源码参考: v7_static_link_shared_lib
这里是对 v6_static_link_static_lib 做的改造, 各个子模块间的依赖关系有:
test -> sort.a(静态库) -> calc.dylib(动态库)
配置静态库 sort 链接动态库 calc 主要有以下修改:
calc/CMakeLists.txt
1 2 3 4 5 6 7 8 include_directories (${HEAD_PATH} )set (LIBRARY_OUTPUT_PATH ${LIB_PATH} )add_library (${LIB_CALC} SHARED ${SOURCES} )
calc/CMakeLists.txt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 include_directories (${HEAD_PATH} )set (LIBRARY_OUTPUT_PATH ${LIB_PATH} )link_directories (${LIB_PATH} )add_library (${LIB_SORT} STATIC ${SOURCES} )target_link_libraries (${LIB_SORT} ${LIB_CALC} )
接下来执行 cmake . -B build && cmake --build build
便可以生成: 链接了静态库 sort (sort 链接了动态库 calc) 的可执行文件 bin/test
CMake 常用指令 除了上面几个案例介绍的指令外, 可以参考 这里 和 官网 对 cmake 指令有更全面的了解
CMake 与 Ninja Ninja 也是一个构建系统, 使用 ninja 构建项目相比 makefile 可以更快, 同时也支持跨平台, Ninja 与 CMake 的关系可以参考下图:
我们可以通过 cmake
生成 .ninja 文件, 然后再用 ninja
命令执行构建生成目标文件
使用 ninja, 首先需要在本地安装:
上面的章节中所用到的命令 cmake . -B build
都是生成的 makefile 构建文件, 这其实是 cmake 工具的生成器 generators 概念
执行 cmake --help
可以查看到 cmake 的 generator 在本机系统支持生成哪些类型的构建文件
1 2 3 4 5 6 7 8 9 Generators The following generators are available on this platform (* marks default): * Unix Makefiles = Generates standard UNIX makefiles. Ninja = Generates build.ninja files. Ninja Multi-Config = Generates build-<Config>.ninja files. Watcom WMake = Generates Watcom WMake makefiles. Xcode = Generate Xcode project files. ...
接下来再以 v1_basic 为案例执行命令 cmake . -B build-ninja -GNinja
, 可以看到在 build-ninja 目录下有:
1 2 3 4 ├── CMakeCache.txt ├── CMakeFiles ├── build.ninja └── cmake_install.cmake
以上是 cmake 生成的 ninja 相关构建文件, 我们可以 cd build-ninja && ninja
执行构建编译, 也可以执行 cmake --build build-ninja
命令来生成可执行文件 bin/app
这里再附贴一下 makefile 与 ninja 在编译速度上更直观的对比演示:
CMake 与 vcpkg vcpkg 是 c/c++ 项目的依赖库管理工具, 可以帮助我们非常方便的管理三方库
下载 vcpkg:
1 git clone https://github.com/microsoft/vcpkg
配置环境变量:
1 2 export VCPKG_ROOT=[path-to-vcpkg] export PATH="$VCPKG_ROOT:$PATH"
vcpkg-smaple 以 smaple 工程 为例, 创建 vcpkg 相关配置:
vcpkg 会自动生成以下两个文件:
vcpkg-configuration.json 仓库配置
vcpkg.json 三方库依赖配置
配置 vcpkg.json 管理三方库:
1 2 3 4 5 6 7 { "dependencies" : [ "cxxopts" , "fmt" , "range-v3" ] }
配置 CMakeLists.txt 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 cmake_minimum_required (VERSION 3.15 )set (CMAKE_CXX_STANDARD 17 )set ( CMAKE_TOOLCHAIN_FILE $ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake CACHE STRING "Vcpkg toolchain file" )project (fibonacci CXX)find_package (fmt REQUIRED)find_package (range-v3 REQUIRED)find_package (cxxopts REQUIRED)add_executable (fibo main.cxx)target_link_libraries (fibo PRIVATE fmt::fmt range-v3::range-v3 cxxopts::cxxopts)
执行 vcpkg install
安装依赖库, 此时工程有以下文件:
1 2 3 4 5 6 ├── CMakeLists.txt ├── build ├── main.cxx ├── vcpkg-configuration.json ├── vcpkg.json └── vcpkg_installed
至此, 可以对 main.cxx 正常引用三方库进行代码编写了, 执行下 cmake --build build
试试效果 😊😊
python bindings 接下我们使用 cmake + vcpkg + pybinding11 来实现 python native bindings 的实战案例, 源码参考: python-binding
同样配置 vcpkg.json 的依赖项:
1 2 3 4 5 { "dependencies" : [ "pybind11" ] }
然后使用 vcpkg install
安装依赖库
在 CMakeFileLists.txt 中配置(重点参考注释):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 set ( CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file" )project (example LANGUAGES CXX)set (LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR} /lib)find_package (pybind11 REQUIRED)include_directories ($pybind11_INCLUDE_DIRS) pybind11_add_module(example example.cpp)
在 example.cpp 中定义向 python 提供的 add()
方法, 声明模块宏 PYBIND11_MODULE(example, m)
:
1 2 3 4 5 6 7 8 9 10 11 #include <pybind11/pybind11.h> namespace py = pybind11;int add (int i, int j) { return i + j; }PYBIND11_MODULE (example, m) { m.def ("add" , &add, "A function which adds two numbers" ); }
执行构建 cmake . -B build && cmake --build build
, 然后进入到 lib 目录, 可以看到生成了对应的 native 库: example.cpython-311-darwin.so
执行 python 解释器, 可以看到成功调用到了 navive 库 add()
方法并返回计算结果:
1 2 3 4 5 6 7 8 /lib > python py base 21 :58 :59 Python 3.11 .5 (main, Sep 11 2023 , 08:31 :25 ) [Clang 14.0 .6 ] on darwinType "help" , "copyright" , "credits" or "license" for more information.>>> import example >>>>>> example.add(1 , 2 )3 >>>