CMake 学习记录

小记录

参考:
https://www.zhihu.com/search?type=content&q=premake
https://www.zhihu.com/question/58949190/answer/999701073

口诀:

  1. Declare a target
  2. Declare target's traits
  3. It's all about targets

image
add_definitions 已经被 add_compile_definitions 取代

Windows下是 .dll 和 .lib,Linux下是 .so 和 .a

cmake区分大小写,但是cmake指令不分(比如set和SET)还有函数啥的也不分

cmake中永远用正斜杠 / ,cmake会自动将反斜杠转成正斜杠

image

学习资料

cmake-examples中文版:https://sfumecjf.github.io/cmake-examples-Chinese/
cmake-examples:https://github.com/ttroy50/cmake-examples
CMake “菜谱”:https://www.bookstack.cn/read/CMake-Cookbook/README.md
CMake 官方文档:https://cmake.org/cmake/help/latest/
CMake 中文文档:https://runebook.dev/zh-CN/docs/cmake/-index-
高性能并行编程与优化 - 课件:https://github.com/parallel101/course
More Modern CMake:
https://hsf-training.github.io/hsf-training-cmake-webpage/aio/index.html
https://github.com/hsf-training/hsf-training-cmake-webpage/
Modern CMake:https://cliutils.gitlab.io/modern-cmake/chapters/basics/structure.html

configuration stage / build stage

https://cmake.org/cmake/help/v3.25/manual/cmake.1.html#generate-a-project-buildsystem

cmake-examples

https://sfumecjf.github.io/cmake-examples-Chinese/
https://github.com/ttroy50/cmake-examples

1. basic

1.1 hello-cmake

最基础的当然是

cmake_minimum_required(VERSION 3.5) #设置CMake最小版本
project (hello_cmake) #设置工程名
add_executable(hello_cmake main.cpp) #生成可执行文件

细节就是,在这一行语句中project (hello_cmake) #设置工程名
CMake构建包含一个项目名称,上面的命令会自动生成一些变量,在使用多个项目时引用某些变量会更加容易。
比如生成了: PROJECT_NAME 这个变量。PROJECT_NAME是变量名,${PROJECT_NAME}是变量值,值为hello_cmake

因此上面的代码和下面的一样:

cmake_minimum_required(VERSION 3.5) #设置CMake最小版本
project (hello_cmake) #设置工程名
add_executable(${PROJECT_NAME} main.cpp) #生成可执行文件

1.2 头文件

image
对于头文件的情况,我们这样写:

cmake_minimum_required(VERSION 3.5)#最低CMake版本

project (hello_headers)# 工程名

set(SOURCES
    src/Hello.cpp
    src/main.cpp
)#创建一个变量,名字叫SOURCE。它包含了所有的cpp文件。

add_executable(hello_headers ${SOURCES})#用所有的源文件生成一个可执行文件,因为这里定义了SOURCE变量,所以就不需要罗列cpp文件了
#等价于命令:     add_executable(hello_headers src/Hello.cpp src/main.cpp)

target_include_directories(hello_headers
    PRIVATE 
        ${PROJECT_SOURCE_DIR}/include
)#设置这个可执行文件hello_headers需要包含的库的路径


#PROJECT_SOURCE_DIR指工程顶层目录
#PROJECT_Binary_DIR指编译目录
#PRIVATE指定了库的范围,下一节讲

我们可以用 message 函数来查看,比如我在上面的最后一行加一句:message(${CMAKE_SOURCE_DIR})

我们在 cmake .. 的时候会打印输出:
image
我们在 make 的时候可以这样来详细列出(否则仅显示构建状态):make VERBOSE=1

1.3 Static Library

将源文件直接传递给add_library调用,这是modern CMake的建议。(而不是先把Hello.cpp赋给一个变量)
image

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#库的源文件Hello.cpp生成静态库hello_library
add_library(hello_library STATIC 
    src/Hello.cpp
)
target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)
# target_include_directories为一个目标(可能是一个库library也可能是可执行文件)添加头文件路径。
############################################################
# Create an executable
############################################################
# Add an executable with the above sources
#指定用哪个源文件生成可执行文件
add_executable(hello_binary 
    src/main.cpp
)
#链接可执行文件和静态库
target_link_libraries( hello_binary
    PRIVATE 
        hello_library
)
#链接库和包含头文件都有关于scope这三个关键字的用法。

关于 private pubic interface 的范围

官方文档讲解:https://cmake.org/cmake/help/v3.0/command/target_include_directories.html

  • PRIVATE - 目录被添加到目标(库)的包含路径中。
  • INTERFACE - 目录没有被添加到目标(库)的包含路径中,而是链接了这个库的其他目标(库或者可执行程序)包含路径中
  • PUBLIC - 目录既被添加到目标(库)的包含路径中,同时添加到了链接了这个库的其他目标(库或者可执行程序)的包含路径中

也就是说,根据库是否包含这个路径,以及调用了这个库的其他目标是否包含这个路径,可以分为三种scope。

拿刚才的项目为例,结构如下:
image
那么对于我们的 CMake 命令:

target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)

我们这里 Hello.cpp 和 main.cpp 都是这样包含头文件的:#include "static/Hello.h"

而这里我们要加入 ${PROJECT_SOURCE_DIR}/include,为 PUBLIC 的时候,就会是正确的;为 PRIVATE 的时候,那么此时只有静态库能找到这个路径,对于链接这个静态库的最终可执行文件而言是没有添加这个路径的,也就是 main.cpp 没有添加这个路径,所以在 main.cpp 中会报错
image
而若是换成 INTERFACE ,那么就只有要链接这个静态库的东西添加了这个路径,因此会在 Hello.cpp 中报错:
image
同样,在代码:

target_link_libraries( hello_binary
    PRIVATE 
        hello_library
)

中,若是把 PRIVATE 换成 INTERFACE 也会在 main.cpp 报错,因为 INTERFACE 会让 main.cpp 找不到这个路径。(这告诉CMake在链接期间将hello_library链接到hello_binary可执行文件。 同时,这个被链接的库如果有INTERFACE或者PUBLIC属性的包含目录,那么,这个包含目录也会被传递( propagate )给这个可执行文件。)

这里对于hello_binary,它不是库,所以不会被链接。直接private自己用这个库就行。

建议:

对于公共的头文件,最好在include文件夹下建立子目录。

传递给函数target_include_directories()的目录,应该是所有包含目录的根目录,然后在这个根目录下建立不同的文件夹,分别写头文件。

这样使用的时候,不需要写${PROJECT_SOURCE_DIR}/include,而是直接选择对应的文件夹里对应头文件。下面是例子:#include "static/Hello.h"而不是#include "Hello.h"使用此方法意味着在项目中使用多个库时,头文件名冲突的可能性较小。

1.4 Shared Library

image
CMakeLists:

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#根据Hello.cpp生成动态库
add_library(hello_library SHARED 
    src/Hello.cpp
)
#给动态库hello_library起一个别的名字hello::library
add_library(hello::library ALIAS hello_library)
#为这个库目标,添加头文件路径,PUBLIC表示包含了这个库的目标也会包含这个路径
target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################
#根据main.cpp生成可执行文件
add_executable(hello_binary
    src/main.cpp
)
#链接库和可执行文件,使用的是这个库的别名。PRIVATE 表示
target_link_libraries( hello_binary
    PRIVATE 
        hello::library
)

1.5 build-type、set、CACHE变量

强烈建议看:https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.5 build-type.html

image

cmake_minimum_required(VERSION 3.5)
#如果没有指定则设置默认编译方式
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  #在命令行中输出message里的信息
  message("Setting build type to 'RelWithDebInfo' as none was specified.")
  #不管CACHE里有没有设置过CMAKE_BUILD_TYPE这个变量,都强制赋值这个值为RelWithDebInfo
  set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)

  # 当使用cmake-gui的时候,设置构建级别的四个可选项
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
    "MinSizeRel" "RelWithDebInfo")
endif()


project (build_type)
add_executable(cmake_examples_build_type main.cpp)

image

关于编译器参数:
参考:http://cn.voidcc.com/question/p-fvbzitto-bhq.html
-DNDEBUG 由两部分组成,标记 -D 和参数 NDEBUG 。该标志被用于创建预处理器定义,因此这将创建一个新的预处理器 #define 称为 NDEBUG
-g 是一个编译器开关,以产生调试信息。它与 -D 完全分开创建定义。

在命令行运行CMake的时候, 使用cmake命令行的-D选项配置编译类型:
cmake .. -DCMAKE_BUILD_TYPE=Release
image

1.6 Compile Flags

cmake_minimum_required(VERSION 3.5)
#强制设置默认C++编译标志变量为缓存变量,如CMake(五) build type所说,该缓存变量被定义在文件中,相当于全局变量,源文件中也可以使用这个变量
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)

project (compile_flags)

add_executable(cmake_examples_compile_flags main.cpp)
#为可执行文件添加私有编译定义
target_compile_definitions(cmake_examples_compile_flags 
    PRIVATE EX3
)

在现代CMake中设置C ++标志的推荐方法是专门针对某个目标(target)设置标志,可以通过target_compile_definitions()函数设置某个目标的编译标志。

target_compile_definitions(cmake_examples_compile_flags
    PRIVATE EX3
)

如果这个目标是一个库(cmake_examples_compile_flags),编译器在编译目标时添加定义-DEX3 ,并且选择了范围PUBLIC或INTERFACE,该定义-DEX3也将包含在链接此目标(cmake_examples_compile_flags)的所有可执行文件中。 注意,本语句使用了PRIVATE,所以编译选项不会传递。

image

1.7 Including Third Party Library

https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.7 Including Third Party Library.html
https://www.cnblogs.com/hebohang/p/15990146.html

1.8 设置 C++ 标准

# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.1)

# Set the project name
project (hello_cpp11)

# set the C++ standard to C++ 11
set(CMAKE_CXX_STANDARD 11)

# Add an executable
add_executable(hello_cpp11 main.cpp)

1.9 installing

https://github.com/ttroy50/cmake-examples/tree/master/01-basic/E-installing

2. sub projects

https://sfumecjf.github.io/cmake-examples-Chinese/02-sub-projects/A-basic/
image

小彭老师 CMake 教学

1

https://www.bilibili.com/video/BV1fa411r7zp/?spm_id_from=333.788

关于构建系统、CMake

首先是为什么出现了构建系统(比如 Makefile(GNU Make)、Ninja 之类的)

参考
https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.9 J-building-with-ninja.html
https://github.com/ttroy50/cmake-examples/tree/master/01-basic/J-building-with-ninja
image

比如老师这个例子:
image
有 hello.cpp main.cpp Makefile,我们make一下,会编译出 hello.o 和 main.o,要是我们改变 hello.cpp,我们就知道 main.o 不需要改变,只要再编译一次 hello.cpp 到 hello.o 即可,这便是增量编译。我想,检测可以由操作系统打时间戳,GNU Make 再去检测。
image

为了应对不同平台的差异,于是出现了 CMake,一个构建系统的构建系统,生成的 makefile 之类的再有对应的编译器编译成可执行文件。

想切换成 clang++ 去编译,可以这样写:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++
测试:
image
这里的 -B 就是指定编译的位置,我们这里相当于指定为文件夹 build 了。-D 则是 define 的意思,在前面有讲过。

还能直接指定C++版本:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_STANDARD=17

image

关于 library(动态库、静态库)

比如之前写的一个 printf 的代码,被优化成了 puts 函数,用 objdump 查看(反汇编了)如下:
image
这里的
image
就是一个插桩函数,会去 libc.so 找到对应的位置。
image

Linux是没有与 .so 配套的 .a 的,它会自动生成插桩。可以用 ldd a.out 查看可执行文件 a.out 链接了谁(.so)。

CMake 的一些指令

image

image
上图的 -DMY_MACRO=1 只是为了符合某些人的习惯,其实还是相当于 #define MY_MACRO 1

没有 target 的下面的指令就像是全局的,不推荐使用。

解决菱形依赖的问题:
image

安装第三方库-包管理器:
image

2

https://www.bilibili.com/video/BV16P4y1g7MH/?spm_id_from=333.788

image

现代 CMake 命令行调用:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
cmake --build build --target install

这里的 --build 在Linux中就会调用make,在 Windows 中就会调用 visual studio 的那个 msbuild 去构建。--parallel 4 就代表用 4 个进程并行地构建,并且可以用 --target 来指定要构建的目标,和这个 make install 是一样的。

image

还有就是现代 CMake 中 public 的应用:
image

![image](https://img2022.cnblogs.com/blog/1752640/202203/1752640-20220311192201092-1353477202. png)

一般比较常用的是 Ninja 这个后端,比较快:
image
Ninja 还是跨平台的,Windows和Linux上都能用:
image
但是像 CUDA 在Windows上强制用 MSBuild 而不能用 Ninja。

可以使用 GLOB 来直接加所有的 .cpp 和 .h,但是可能存在删掉或者新增cpp却不被识别的问题,小彭老师推荐加关键字 CONFIGUE_DEPENDS:
image
不过前面说的现代 CMake 推荐都写下来。

可以用 aux_source_directory 来自动根据当前语言搜索所需要的文件:
image

同时还有递归地去搜索的关键词 GLOB_RECURSE(能自动包含所有子文件夹下的文件):
image
但是会把 CMake 构建时build中的测试的临时 cpp 也被加进来:
image

注:在CMake看来编译和链接是一起的,

默认是 Debug 构建模式:
image

image

image

image
https://cmake.org/cmake/help/latest/command/project.html

image

image
image

一般会把 CMAKE_CXX_EXTENSIONS 设置为 OFF,表示不需要有那些GCC的特性(夹带私货!否则MSVC可能会通不过)。

这些变量最好设置在 project 之前,这样编译器在对project的时候启用这个语言的时候就会检测一下,就不容易出错。

image
image
image
image
image

CMAKE常见变量https://blog.csdn.net/fuyajun01/article/details/8891749

小彭老师建议卸载Ubuntu,使用Arch Linux.

一个标准的 CMakeLists.txt 模板

cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

project(MyProjName LANGUAGES C CXX)

if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
    message(WARNING "The binary directory of CMake cannot be the same as source directory!")
endif()

if (NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()

if (WIN32)
    add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()

if (NOT MSVC)
    find_program(CCACHE_PROGRAM ccache)
    if (CCACHE_PROGRAM)
        message(STATUS "Found CCache: ${CCACHE_PROGRAM}")
        set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PROGRAM})
        set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_PROGRAM})
    endif()
endif()

链接库文件

小彭老师推荐用静态库,因为动态库在Windows上有很多坑点。

对象库:https://www.scivision.dev/cmake-object-libraries/
image
image

在自己的项目里可以都用对象库来组织。对象库的一个优点是,如上面的 mylib 作为一个对象库可以指定不同的编译选项,有时候就会做到。还有一点就是可以绕开编译器和操作系统的各种规则,保证跨平台统一性。

image
image

image
image

常见坑点:
image

原因是动态库在内存中的地址是会变化的,他在编译时会指定一个 -fPIC 选项,但是静态库却没有这个选项。静态库不想变换地址而动态库却需要变换地址,因此把一个静态库链到动态库上就会报错:cannot relocate x86_ to loc 之类的错误。

解决方法:要么把静态库变为一个对象库,要么让静态库编译时也生成位置无关的代码(PIC)
image

不过这样就导致那些本来不需要为 PIC 的那些库也变为 PIC 了。

前面的set(CMAKE_POSITION_INDEPENDENT_CODE ON)会把那个文件后面所有的静态库都生成 PIC ,因此我们可以用 set_property 来只针对某一个库:
image

对象的属性

image

注:经尝试,set_property(TARGET TinyRaytracer PROPERTY WIN32_EXECUTABLE TRUE) 必须要 WinMain 才可以,main 入口是不可以的,会报错。

image
image
image
image

image
image

手动拷贝 dll 太麻烦了,有如下几个解决方法:
image
image

链接第三方库

image

image

image
image
image
image

image
image
image
image
image
image
image
image
image

输出与变量

image
image
image
image
image
image
image
image

set(myvar hello world) 相当于 set(myvar "hello;world"),即视作列表。

image

所以如果不确定的时候最好打上引号。

结论:除非确实需要列表,建议始终在你不确定的地方加上引号,例如: set(sources “main.cpp” “mylib.cpp” “C:/Program Files/a.cpp”) message(“${sources}”)

变量与缓存

CMake 的最大坑点就是缓存了。

image
image
image

清除缓存,其实只需删除 build/CMakeCache.txt 就可以了rm build/CMakeCache.txt

image
image
image
image

这个和参数的语法是一样的,就是前面加个 -D 而已。

image
image
image
image
image
image

bool 值还有 TRUE 和 FALSE,但他们是由于历史原因保留的,目前普遍用的是 ON 和 OFF。

image

如上图,设置 WITH_TBB 是一个 BOOL 类型的缓存变量,那么用户使用的时候就可以这样来设置是否开启:
cmake -B build -DWITH_TBB:BOOL=OFF

CMake 对 BOOL 类型缓存的 set 指令提供了一个简写:option
image

image
image

image
image

但是上图的问题是,比如 ccmake(或者Windows的CMake GUI?)会查缓存,如果是不带 CACHE 的这样 set 就不进缓存,于是 ccmake 就看不到。

跨平台与编译器

image
相当于 gcc -DMY_MACRO=233

image
image

虽然名字叫 WIN32,实际上对 32 位 Windows 和 64 位 Windows 都适用。

生成器表达式

image
https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:PLATFORM_ID

这个生成器表达式就相当于,当满足 PLATFORM_ID:Windows 的时候才会定义后面的 MY_NAME="Bill Gates",如果不满足就会定义为一个空字符串。总之就相当于刚才的那三个指令,且自动做了if判断。

image

image
image
image
image
image
image
image
image
image

分支与判断

image
建议同学们始终使用 ON/OFF 避免混淆

image
image
image
image

变量与作用域

image
image
image
image
image
image

image
image
image
image
image
image
image
image

小彭老师小建议

image
image
image

3

https://www.bilibili.com/video/BV1V84y117YU/?spm_id_from=333.999.0.0&vd_source=bd08f0c74da1940eb8682f61aa471b24

推荐的目录组织方式

多套一层名字,防止头文件名重复:
image
image
image

.cmake 文件

image

find_package

image
image
image
image
image
image
image
image
image
image
image

科普:语义版本号(semantic versioning)系统

image

其他记录

怎么判断64位平台和32位平台

CMAKE_SIZEOF_VOID_P 表示 void* 的大小(例如为 4 或者 8),可以使用其来判断当前构建为 32 位还是 64 位

if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(FATAL_ERROR "TinyRaytracer only supports 64-bit platforms")
endif ()

经测试,这一行应该包含在 project 指定语言之后,否则不知道 CMAKE_SIZEOF_VOID_P 是多少上面那个代码就始终会报错。

add_library是纯头文件或者没有源文件

此时不能用 STATIC 啥的,发现会报错:no sources given to target
https://stackoverflow.com/questions/65415872/issue-regarding-cmake-error-no-source-given-to-target

得换成 INTERFACE

CMake 输出路径

https://blog.csdn.net/q610098308/article/details/121157418

CMake warining管理

有时候引入第三方库我不想让它报warning,很烦人,学习如下链接:
https://www.foonathan.net/2018/10/cmake-warnings/

target_compile_options(my_library PRIVATE
     $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
          -Wall>
     $<$<CXX_COMPILER_ID:MSVC>:
          /W4>)

add_compile_definitions 似乎和变量一样遵从“子不影响父”的关系

add_compile_definitions 似乎和变量一样遵从“子不影响父”的关系?

posted @ 2022-03-20 01:03  hebh  阅读(969)  评论(0编辑  收藏  举报