Modern CMake 简介

news/2024/11/17 23:42:16/文章来源:https://www.cnblogs.com/LiuYanYGZ/p/18375027

摘自:https://zhuanlan.zhihu.com/p/76975231

Modern CMake 简介

CMake是一个构建系统生成器(build-system generator)。常见的构建系统,有Visual Studio,XCode,Make等等。CMake可以支持不同平台下构建系统的生成。

CMake的出现已经有接近20年的历史,它的发展过程也初步经历了三个阶段。

  • ~2000 (~v2.x) ,刚刚启动,过程式描述为主。
  • 2000~2014 (v3.0~) ,引入Target概念。
  • 2014~now (~v3.15),有了Target和Property的定义,更现代化。

概 述

现代化的CMake是围绕 Target 和 Property 来定义的,并且竭力避免出现变量variable的定义。Variable横行是典型CMake2.8时期的风格。现代版的CMake更像是在遵循OOP的规则,通过target来约束link、compile等相关属性的作用域。如果把一个Target想象成一个对象(Object),会发现两者的组织方式非常相似:

  • 构造函数:
    • add_executable
    • add_library
  • 成员函数:
    • get_target_property()
    • set_target_properties()
    • get_property(TARGET)
    • set_property(TARGET)
    • target_compile_definitions()
    • target_compile_features()
    • target_compile_options()
    • target_include_directories()
    • target_link_libraries()
    • target_sources()
  • 成员变量
    • Target properties(太多)

在Target中有两个概念非常重要:Build-Requirements 和 Usage-Requirements。这两个概念对于理解为什么现代CMake会如此设计提供了指导意义。

  • Build-Requirements: 包含了所有构建Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。
  • Usage-Requirements:包含了所有使用Target必须的材料。如源代码,include路径,预编译命令,链接依赖,编译/链接选项,编译/链接特性等。这些往往是当另一个Target需要使用当前target时,必须包含的依赖。

传统的CMake和现代化的CMake的主要区别(非语法层面)如下图所示。Traditioncal CMake在设置build-requirements和usage-requirements上都依赖手动输入命令,并且人工维持其作用域(变量的作用域以目录为单位)。而Modern CMake在设置上述requirement均以target为单位,所以在传递target属性到其依赖的下游链条中更自动也更智能。

在Moden CMake中新增了不少关键字,其中最常见的是PUBLIC、PRIVATE、INTERFACE。

  • PRIVATE/INTERFACE/PUBLIC:定义了Target属性的传递范围。
    • PRIVATE: 表示Target的属性只定义在当前Target中,任何依赖当前Target的Target不共享PRIVATE关键字下定义的属性。
    • INTERFACE:表示Target的属性不适用于其自身,而只适用于依赖其的Target。
    • PUBLIC:表示Target的属性既是build-requirements也是usage-requirements。凡是依赖。凡是依赖于当前Target的Target都会共享本属性。

解剖麻雀

我们来尝试写一个实例,看看在CMake v3.13及以后版本中的写法如何。

HelloWorld|___ CMakeLists.txt|___ hello-exe|______ CMakeLists.txt|______ main.cpp|___ hello-lib|______ CMakeLists.txt|______ hello.hpp|______ hello.cpp

以这样一个简单的HelloWorld开启有助于我们快速进入主题。这个项目结构很简单,包含两个子文件夹,hello-exe生成executable,hello-lib生成链接库(动态)。

  • 我们先看下顶层CMakeLists的内容:
# HelloWorld/CMakeLists.txt
cmake_minimum_required(VERSION 3.14)

project(HelloWorld VERSION 1.0.0)

add_subdirectory(hello-lib)
add_subdirectory(hello-exe)

这里没有什么值得多讨论的,与传统CMake一样的写法,定义project名称,版本号,添加子文件夹。

  • 我们接着看hello-lib。首先看源码。

源码比较简单,只是定义一个hello_printer类,并在其cpp中定义成员函数print。请注意头文件中的预编译命令。这在VS中是非常常用的预编译命令,用于导出动态库的符号。而当该库被其他Target调用时,需要使用dllimport导入符号。注意这条预编译命令刚好符合build-requirement和usage-requirement的定义。对于hello-lib而言,定义DLL_EXPORT从而将DLL_API定义为_declspec(dllexport)是build-requirement,而对于该Target的调用者,需要的是不定义DLL_EXPORT。因而需要在定义compile_definitions 时将Dll_EXPORT放在PRIVATE关键词下。

当其他Target使用hello-lib的时候,还需要知道hello.hpp的路径。传统的CMake写法是通过在调用者的CMakeLists.txt中添加includedirectory来实现。但这种写法会依赖库之间的相对路径,一旦调整路径,所有的CMakeLists都将需要更新。在Modern CMake中不必如此,你只需要通过target_include_directories指定hello.hpp的路径,将之纳入INTERFACE(当然PUBLIC)也行。则调用者就可以得到该include路径。

CMakeLists.txt 全文如下:

set(target_name "hello-lib")

add_library(${target_name}  SHAREDhello.cpphello.hpp)

target_include_directories(${target_name} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(${target_name} PRIVATE DLL_EXPORT)
  • 最后看下hello-exe。hello-exe中的CMakeLists.txt就可以比较简单了:
add_executable(hello-exe main.cpp)
target_link_libraries(hello-exe PUBLIC hello-lib)

补充

Modern CMake中还有些有意思的知识点,这里没法一一覆盖,只能稍稍展开。最有意思的点是generator-expression。在现代IDE中,Build-type一般都不是在CMake config期间能确定的。如VS,XCode都支持Multi-configuration,具体使用Debug还是Release是在编译时才确定,那如果Target的依赖路径或者依赖库需要区分Configuration来配置该怎么办呢?在传统CMake中是比较难办的,target_link_libraries提供了一种手段,可以用debug和optimized来区分具体的库名,而其他的编译或链接设置则比较困难。在Modern CMake中,我们可以通过generator-expression来实现。

generator-expression定义为$<...>的形式。该表达式的值有多种形式,而且支持嵌套使用:

  • 条件表达式
    • $<condition:true_string> 当条件为1时,表达式为true_string,否则为空
    • $<IF:condition,true_string,false_string> 当条件为1时,表达式为true_string,否则为false_string
  • 变量表达式
    • $<TARGET_EXISTS:target> 当target存在为1,否则为0
    • $<CONFIG:cfg> 当config为cfg时为1,否则为0。这是非常高频使用的一个表达式,可以通过它来区分Debug/Release等不同的config。如下例所示,通过嵌套使用上述两个表达式,可以达到区分CONFIG来设置依赖库路径的目的。
target_link_directories(${PROJECT_NAME} PUBLIC                                                                                                                                                                      $<$<CONFIG:Debug>:${CONAN_LIB_DIRS_DEBUG}>                                                                                                                                                                        $<$<CONFIG:Release>:${CONAN_LIB_DIRS_RELEASE}>) 
    • ... 太多了,不一一列举。

以上是Modern CMake中常用的内容,还有些如IMPORTED,ALIAS暂时还没用到,等用到再更新吧。

参考

  1. onqtam/awesome-cmake

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/785670.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

048、Vue3+TypeScript基础,基本的子页面和父页面相互通讯

01、main.js代码如下:// 引入createApp用于创建Vue实例 import {createApp} from vue // 引入App.vue根组件 import App from ./App.vueconst app = createApp(App);// App.vue的根元素id为app app.mount(#app)02、App.vue代码如下:<template><div class="app&…

1、拟合、预测、估算器、管道与模型评估

一、拟合和预测:估算器基础 1.1 资源导入、样本定义和训练from sklearn.ensemble import RandomForestClassifier #随机森林分类器 """ random_state=0 将使用固定的随机数种子(在这个例子中是0)来初始化随机数生成器。 这样,无论你的代码运行多少次,只要…

DDD精粹速读(一)

1 你需要知道的 - 战略设计 DDD是一种软件设计和构建方法,其重点在于独立于数据持久化等技术问题,准确表达业务规则。 不幸,DDD 对新手来说极具挑战性,部分原因是它有许多独特的概念需要学习。本文我简要介绍这些重要的思想,以便你能自信继续你的 DDD 旅程。 第一部分将侧…

消息队列作用(解耦、异步、削峰)

原文:消息队列作用(解耦、异步、削峰)图详解一、消息队列简介 简单来说,“消息队列”是在消息的传输过程中保存消息的容器。MQ 全称为 Message Queue,消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信。 消…

C++ 链表

1. 前言 链表:不仅存储 当前元素的数据,还存储着 元素排列顺序 2. 正题 2.1 如何存储节点? 我们可以使用 结构体 数组来存储 链表节点 struct Node {int val; // 可以是 string 或其它复杂的类型int nxt; } node[N];Tip: 下标顺序不是单链表顺序val 代表 元素本身,nxt 代表…

WPF 模拟UWP原生窗口样式——亚克力|云母材质、自定义标题栏样式、原生DWM动画 (附我封装好的类)

先看一下最终效果,左图为使用亚克力材质并添加组合颜色的效果;右图为MicaAlt材质的效果。两者都自定义了标题栏并且最大限度地保留了DWM提供的原生窗口效果(最大化最小化、关闭出现的动画、窗口阴影、拖拽布局器等)。接下来把各部分的实现一个个拆开来讲讲。 一、使用窗口材…

Redis学习(一)

1.通用命令keys * del k1 exists k1 expipe k1 ttl k12.String类型String类型的常见命令 set k1 v1 添加键值对 get k1 v1 获得键值对对应的值 mset k1 v1 k2 v2 一次性设置多个值 mget k1 k2 k3 一次性获取多个键值对的值 incr k1 让k1自增 incrby k1 2 按步长2自增 dec…

C# WebSocket Fleck 源码解读

最近在维护公司旧项目,偶然发现使用Fleck实现的WebSocket主动推送功能,(由于前端页面关闭时WebSocket Server中执行了多次OnClone事件回调并且打印了大量的关闭日志,),后来我特地看了源码,这里做一些分享 github: https://github.com/statianzo/Fleck在源码中,作者在 Sam…

SuperCLUE整理

参考信息: git:https://github.com/CLUEbenchmark/SuperCLUE 官网:https://www.cluebenchmarks.com/superclue.html 23年7月论文 :https://arxiv.org/pdf/2307.15020: 24年4月报告:https://www.cluebenchmarks.com/superclue_2404 24年7月报告:https://www.cluebenchmar…

20240819编译 链接 单片机执行的第一个语句

2024年8月中旬在宝鸡出差,在B站上学习了下。 在linux操作系统中,gdb是调试器,gcc是编译器。 Linux 操作系统 将main.elf文件加载到内存中以后,操作系统为其分配进程,然后main函数就开始执行了。输入 layout asm 则显示出来汇编代码 键入 “starti”的时候 进入到程序里面的…

回溯part02

今天继续学习了回溯:组合求和的进阶 元素可以重复使用:backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数 数组去重:首先数组排序,然后使用used 分割回文子串问题,抽象为组合问题,注意如何判断是否是回文子串5. 39 组合总和(元素可重复…