MENU

披着CLion的外衣实则在讲CMake

• January 29, 2023 • Read: 5226 • 学习笔记,C++

CLion 配置

安装和基础设置

至于 CLion 安装和基础设置,网上教程一大把,而且不是学习重点,根据自己需求配置即可。

工具链配置

这个配置是进行 C++ 开发的关键,因为这个编译工具链就意味着 C++ 的编译环境。

按下图点开对应的信息,如果你任何编译工具链都没有添加,由于新版本的 CLion 它会自带一个 mingw 的编译套件,所以默认会有一个 CLion 自带的 mingw 编译工具链。
如下图所示我的编译工具链稍微有点丰富,有 msvc、g++、clang++、mingw,作为一个刚刚入门学编程的新手,我建议编译工具链这一块暂时就没必要了解了,但在 CLion 中编译的具体配置流程我认为还是有必要讲清楚。

ECC2NHNK0W3FXP.png

CLion 中添加编译工具链非常简单,你本机把对应工具链的路径加入到了环境变量,那么在你点击 + 对应编译链类型后,会自动扫描到,如果实在没有扫描到,那么也可以自己填入对应的路径,整个编译链包括:

  1. cmake,用于跨平台以及简化底层编译脚本的工具。
  2. cmake 生成更底层的编译命令(对应上述的 Build Tool),比如 gmake 也就是解析.makefile 文件进行命令执行,比如 ninja 解析 .ninja 文件进行命令执行(编译速度比 makefile 更快)。
  3. C 语言的编译器(clang/gcc/cl 等等)。
  4. C++ 的编译器(clang++/g++/cl 等等)。

如果是 mingw,那么上述的一套都是包含的,只需要把 Toolset 这个选项选择为 mingw 对应的目录即可,选择好后,CLion 会自动识别上述四件套的位置。

接下来简单介绍如何添加一些工具链:

CMake 配置项

X04TEKS7F84K2TFNI1.png

如上图所示,第二个 CMake 选项就是我们现在要讲的,而这两个正好也是整个开发环境中最重要的东西,第一个编译工具链决定了 CLion 中已经识别了本机有哪些编译环境,而第二个 CMake 选项,则是用于配置 cmake 基于哪些配置项生成。

所以我们现在应该了解了 CLion 是如何去编译项目生成可执行文件的了。

  1. 通过 cmake 配置选项运行整个项目的 CMakeList.txt
  2. 生成 makefile 或其他底层脚本后再通过对应的工具去执行这个脚本
  3. 运行编译好的程序

而我们现在讲的就是添加 cmake 配置选项,如果你手动写 cmake 命令的话,那样对应的就是命令行参数了。

S2MKM8_ZB3X_QBSOB.png

上述图片中已经解释了一些配置的作用。这些配置项一般是不常改动,使用默认值就行,比如 Build options 是执行最后的脚本所用的参数,默认为 -j 12,比如如果是 makefile,那么就是 make -j12

下面是大家可能需要进行一些配置的选项:

  1. Build type:这是程序最终编译的类型,意味着编译器该以何种程度对源代码进行优化,比如 Debug 版本一般再 gcc 中对应 o2 的优化,release 版本对应 o3 的优化,两者一般存在 10 倍左右的性能差距。
  2. Toolchain:这是前面所说的编译工具链,一般来说,想要切换编译器,你切换这个选项就行了,默认使用 default 工具链。
  3. Generator:这是前面所说的工具链中的较为底层的脚本的运行工具,可以是 makefile 或者 ninja,不选的话也是默认工具链里的那个。
  4. CMake options:这个是 cmake 运行时可以加入的命令行参数,比如我们可以-D 来定义对应的变量控制对应的 cmake 行为,甚至于前面的 Build type 我们完全可以不写(当然这是 CLion,这个空必须得被填充),然后使用 -DCMAKE_BUILD_TYPE=Release,这个变量可以决定最终 cmake 生成的执行脚本是按照 release 的标准去运行的,又比如 -DBUILD_SHARED_LIBS=ON,那么最终是会生成动态库而不是静态库,我上图中的 -DENABLE_TEST=ON 是内部的 cmake 有定义一个变量默认为 OFF 值,如果为 ON 时会加入测试代码为子项目。

现在 cmake 在 CLion 中的配置项已经讲完了,简单实践一下来体验之前讲的 CLion 到整个运行的流程:

  1. 通过 cmake 配置选项运行整个项目的 CMakeList.txt。
  2. 生成 makefile 或其他底层脚本后再通过对应的工具去执行这个脚本。 我们先看一眼上一步 cmake 生成的文件(放出了两个不同的配置项产生的脚本,第一个使用的 Generator 为 ninja,第二个使用的为 gmake):
    UAF6YJ6JWP4U3DJF2K5XW.png

    如果想要继续执行这个脚本,应该在 CLion 中执行对应的源代码,CLion 会自动识别入口点函数,然后给出可执行的按钮。点击执行后,不仅会直接对应的 makefile 或 build.ninja 还会顺便把这个程序运行到 CLion 内置的终端环境中。
  3. 运行编译好的程序:这一步已经在第二步一并执行了。

CMake 的使用与实战

经过上述文字和图片讲解,我们很自然的想到,整个 CLion 运行 C++ 代码其实就是在运行 cmake 和 makefile(或 build.ninja),第二个过程我们参与不了,但是第一个 cmake 的编写过程我们却需要一直接触。

下面用 CLion 新建项目自动生成的 cmake 模板来简单对 cmake 语法热热身。

cmake_minimum_required(VERSION 3.22)
project(untitled)

set(CMAKE_CXX_STANDARD 17)

add_executable(untitled main.cpp)
  • cmake_minimum_required 命令:规定了编译本项目的 cmake 工具至少需要 3.22 版本。
  • project 命令:规定了本项目的项目名称,同时也根据这个传入的值生成了一堆变量,常用的如下:

    1. PROJECT_NAME :项目名称
    2. PROJECT_BINARY_DIR :项目的二进制文件目录,即编译后的可执行文件和库文件的输出目录
    3. PROJECT_SOURCE_DIR :项目的源文件目录,即包含 CMakeLists.txt 文件的目录

    举个简单例子说明上述变量的作用:
    比如一个测试的子项目中的 CMakeList.txt,可能需要写下面的语句(先不管 file 命令),由于是作为直接的子项目,那么里面肯定不会存在 project 语句,所以 PROJECT_SOURCE_DIR 变量表示的仍然是整个项目的根目录,直接通过 ${} 的形式来使用它即可,这样就不需要关心相对或绝对路径了。

    file(GLOB SONIC_TEST_FILES
         "${PROJECT_SOURCE_DIR}/tests/*.h"
         "${PROJECT_SOURCE_DIR}/tests/*.cpp"
    )
  • set 命令:设置对应变量为对应的值,该变量存在,则修改该变量的值,如果不存在则会创建并初始化为对应的值,这里对 set 的使用是设置了 CMAKE_CXX_STANDARD 变量为 17,这个变量可以控制最终编译采用的 C++ 版本,这里是使用 C++17。
  • add_executable 命令:这是用于生成可执行程序的命令,第一个参数为该执行程序最终编译后生成的文件名,后面跟着的都是需要编译的源代码。

对于新手而言,其实不太需要自己手写 cmake,因为 CLion 会在你新建源文件的时候把相应源文件添加到 add_excutable 命令的后面,但项目稍微大一点或者说引入了很多外部库,那么大概率会抛弃 CLion 的这种自动化了。

常用的 CMake 变量

下面只列出了部分变量的作用,更多的变量请查看文档:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html

  • PROJECT_NAME :项目名称
  • PROJECT_BINARY_DIR :项目的二进制文件目录,即编译后的可执行文件和库文件的输出目录
  • PROJECT_SOURCE_DIR :项目的源文件目录,即包含 CMakeLists.txt 文件的目录
  • CMAKE_BINARY_DIR :当前 CMake 运行的二进制文件目录,通常和 PROJECT_BINARY_DIR 是同一个目录
  • CMAKE_SOURCE_DIR :当前 CMake 运行的源文件目录,通常和 PROJECT_SOURCE_DIR 是同一个目录
  • CMAKE_C_STANDARD :指定 C 语言的标准版本
  • CMAKE_CXX_STANDARD :指定 C++ 语言的标准版本
  • CMAKE_CXX_FLAGS :指定编译 C++ 代码时使用的编译选项
  • CMAKE_C_FLAGS :指定编译 C 代码时使用的编译选项
  • CMAKE_EXE_LINKER_FLAGS :指定链接可执行文件时使用的链接选项
  • CMAKE_SYSTEM_NAME :指定当前操作系统名称(如 Windows、Linux 等)
  • CMAKE_SYSTEM_PROCESSOR :指定当前处理器的类型(如 x86、x86_64 等)
  • CMAKE_CXX_COMPILER_ID :指定了当前使用的 C++ 编译器,同理可得 C 的编译器对应的名字。
对这些变量做一个简单的实践

通过 message 打印出 PROJECT_BINARY_DIR、PROJECT_SOURCE_DIR、CMAKE_BINARY_DIR、CMAKE_SOURCE_DIR 来加以验证,目录结构如下:

.
├── CMakeLists.txt
├── main.cpp
└── sub
    └── CMakeLists.txt
main:
cmake_minimum_required(VERSION 3.14)
project(main)

add_subdirectory(sub)

message(STATUS "main:${PROJECT_NAME}\n  pro-src:${PROJECT_SOURCE_DIR}\n pro-bin:${PROJECT_BINARY_DIR}\n cmake-src:${CMAKE_SOURCE_DIR}\n cmake-bin:${CMAKE_BINARY_DIR}")

sub:
project(sub)

message(STATUS "sub:${PROJECT_NAME}\n  pro-src:${PROJECT_SOURCE_DIR}\n pro-bin:${PROJECT_BINARY_DIR}\n cmake-src:${CMAKE_SOURCE_DIR}\n cmake-bin:${CMAKE_BINARY_DIR}")

打印信息如下:我们发现 CMake 对应的变量没有变化,而 Prject 有了变量,因为我们在 sub 也使用了 project 命令。

QPUOIN6QRMBZK5OR.png

通过变量检测环境执行不同的 cmake 代码:

# 判断当前的操作系统
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
target_link_libraries(my-logger PUBLIC fmt-header-only pthread)
message(STATUS "Now is Linux")
elseif (CMAKE_SYSTEM_NAME MATCHES "Windows")
target_link_libraries(my-logger PUBLIC fmt-header-only ws2_32)
message(STATUS "Now is windows")
endif ()
# 判断当前使用的编译器
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# Do something for GCC
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Intel")
# Do something for Intel C++
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Microsoft")
# Do something for Microsoft Visual C++
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
`# Do something for Clang`
endif()

# 判断当前的系统架构
if (CMAKE_SYSTEM_PROCESSOR MATCHES "i.86|x86|x86_64|AMD64")
# Do something for x86 architecture
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm|aarch64)")
# Do something for ARM architecture
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(mips|mipsel|mips64)")
# Do something for MIPS architecture
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(powerpc|ppc64)")
# Do something for PowerPC architecture
endif()

通过调整链接时的 flag 防止动态链接,因为如果你是使用 Windows 平台下的编译工具链,CLion 有些时候最终链接并不是采用静态链接,导致你最终生成的可执行程序没法直接执行,这个时候你就需要使用下面的命令来强制静态链接了:

set(CMAKE_EXE_LINKER_FLAGS "-static")

常用的 CMake 命令

下列只列出了部分命令,如果你以后有需要用到的其他命令,请前往官网进行查询:cmake.org/cmake/help/…

我个人较为常用的命令:

  1. project:用于定义项目名称、版本号和语言。
  2. add_executable:用于添加可执行文件。第一个参数很重要,被称为 target,可以作为 target_xxx 命令的接收对象。
  3. add_library:用于添加库文件,可以创建静态库或动态库。第一个参数很重要,被称为 target,可以作为 target_xxx 命令的接收对象。简单使用如下

    add_library(test_lib a.cc b.cc) #默认生成静态库
    add_library(test_lib SHARED a.cc b.cc) #默认生成静态库
  4. add_definitions:用于添加宏定义,注意该命令没有执行顺序的问题,只要改项目中用了该命令定义宏,那么所有的源代码都会被定义这个宏 add_definitions(-DFOO -DBAR ...)
  5. add_subdirectory:用于添加子项目目录,如果有该条语句,就先会跑去执行子项目的 cmake 代码,这样会导致一些需要执行后立马生效的语句作用不到,比如 include_directories 和 link_directories 如果执行在这条语句后面,则他们添加的目录在子项目中无法生效。有些命令如 target_include_directories 和 target_link_directories 是根据目标 target 是否被链接使用来生效的,所以这些命令的作用范围与执行顺序无关,且恰好同一个 cmake 项目中产生的库文件是可以直接通过名称链接的,无论链接对象是在子目录还是父目录
  6. target_link_libraries:用于将可执行文件或库文件链接到库文件或可执行文件。身为 target_xxx 的一员,很明显第二个参数也可以进行权限控制。
  7. include_directories:用于指定头文件搜索路径,优点是简单直接,缺点是无法进行权限控制,一旦被执行后,后续的所有代码都能搜索到对应的文件路径。
  8. target_include_directories:指定头文件搜索路径,并将搜索路径关联到一个 target 上,这里的 target 一般是指生成可执行程序命令里的 target 或者生成库文件的 target,与上一个命令的不同点在于可以设置导出权限,比如现在我写了一个项目,这个项目引入了其他库,但是我不想让其他库的符号暴露出去(毕竟使用这个项目的人只关注这个项目的接口,不需要关注其他依赖的接口)可以通过 PRIVATE 将头文件搜索目录设置不导出的权限。
  9. link_directories:与前面的 include_directories 命令类似,添加的是库的搜索路径。
  10. target_link_directories:和前面的 include 版本一样的,只是改成了库路径。
  11. if\elseif\endif ,在编程语言立马已经用烂了,现在主要是了解 if(condition) 中的条件到底如何判断的,以及内部都支持哪些操作,比如大于等于啥的,这方面直接看官方文档吧,非常好懂:cmake.org/cmake/help/…
  12. aux_source_directory:这个指令简单实用,第一个参数传递一个文件目录,它会扫描这里面所有的源文件放到第二个参数定义的变量名中。注意第一个参数只能是文件夹。aux_source_directory(${PROJECT_SOURCE_DIR} SRC)
  13. file:可以说是上面那个命令的增强版本,但如果熟悉这个命令的朋友肯定很快站出来反对,因为这个命令实在是太强大了,你如果翻一翻这个官方文档就会发现它具备几乎文件系统的所有功能,什么读写文件啊,什么从网上下载文件,本地上传文件之类的它都有,计算文件的相对路径,路径转化等等。但我们平时用到的最多的命令还是用来获取文件到变量里。比如 file(GLOB FILES "文件路径表示 1" "文件路径表示 2" ...) GLOB 会产生一个由所有匹配 globbing 表达式的文件组成的列表,并将其保存到第二个参数定义的变量中。Globbing 表达式与正则表达式类似,但更简单,比如如果要实现前一个命令的功能可以这么写:file(GLOB SRC "${PROJECT_SOURCE_DIR}/*.cc"),如果 GLOB 换成 GLOB_RECURSE ,那么上述命令将递归的搜寻其子目录的所有符合条件的文件,而不仅仅是一个层级。
  14. execute_process:用于执行外部的命令,如下的示例代码是执行 git clone 命令,执行命令的工作目录在 ${CMAKE_BINARY_DIR}/deps/

    execute_process(COMMAND git clone https://github.com/<username>/<repository>.git
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/deps/<repository>)
  15. message:打印出信息用于 debug。
  16. option:用于快速设置定义变量并赋值为对应的 bool 值,常被用于判断某些操作是否执行。
  17. find_package:用于查找外界的 package,其实就是查找外界对应的 <package>Config.cmakeFind<package>.cmake 文件,这些文件里有外界包对应的变量信息以及库和头文件的各种路径信息。我们需要注意一些有关 find_package 命令查找 Config.cmake 路径的变量:

    • CMAKE_PREFIX_PATH 变量是一个路径列表,CMake 会在这些路径中搜索包的 Config.cmake 文件。
    • <Package>_DIR 变量是指向包的 Config.cmake 文件的路径。如果你手动设置了这个变量,那么 find_package 命令就可以找到包的信息。

    同时他的一些常用参数如下:

    • CONFIG :显式指定 find_package 去查找 <package>Config.cmake 文件,一般只要你在变量里面指定了 <package>Config.cmake 的路径,那么该参数填不填都没差别。我建议最好还是带上该参数比较好。
    • REQUIRED :该参数表示如果没找到,那么直接产生 cmake 错误,退出 cmake 执行过程,如果没有 REQUIRED,则即使没找到也不会终止编译。
    • PATHS :这个参数的效果和前面的变量类似,也是指定查找的路径。
    • COMPONENTS :用于指定查找的模块,模块分离在不同的文件中,需要使用哪个就指定哪个模块。典型的就是使用 Qt 时的 cmake 代码,比如 find_package(Qt5 COMPONENT Core Gui Widgets REQUIRED)
    • VERSION:可能有很多个不同版本的包,则需要通过该参数来指定,如:find_package(XXX VERSION 1.2.3)
  18. include:从文件或模块加载并运行 CMake 代码。我用这个命令实际上只是为了使用 FetchContent 这个 module 的功能,该功能是从 cmake3.11 开始支持的,使用该 module 前需要通过 include 命令加载该模块,命令如下:include(FetchContent)
  19. FetchContent:这是一个模块功能,它用来从代码仓库中拉取代码,例如我要把最近写的日志库引入到当前的项目中使用(注意这中间不会有任何代理,所以拉取 GitHub 的仓库可能失败):

    include(FetchContent)# 引入功能模块
    
    FetchContent_Declare(
            my-logger           #项目名称
            GIT_REPOSITORY https://github.com/ACking-you/my-logger.git #仓库地址
            GIT_TAG        v1.6.2  #仓库的版本tag
            GIT_SHALLOW TRUE    #是否只拉取最新的记录
    )
    FetchContent_MakeAvailable(my-logger)
    
    add_excutable(main ${SRC})
    # 链接到程序进行使用
    target_link_libraries(main my-logger)

    这样引入第三方库的好处显而易见,优点类似于包管理的效果了,但缺少了最关键的中心仓库来确保资源的有效和稳定。参考 golang 再做个 proxy 层级就好了。
    同样可以拉取最新的 googletest 可以使用下列语句:

    FetchContent_Declare(
            googletest
            GIT_REPOSITORY https://github.com/google/googletest.git
            GIT_TAG        release-1.12.1
            GIT_SHALLOW TRUE
    )
    # For Windows: Prevent overriding the parent project's compiler/linker settings
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
    FetchContent_MakeAvailable(googletest)
    
    target_link_libraries(main gtest_main)
  20. function/endfunction :在 cmake 中用于定义函数,复用 cmake 代码的命令。第一个参数为函数的名称,后面为参数的名称,使用参数和使用变量时一样的,但是如果参数是列表类型,则在传入的时候就会被展开,然后与函数参数依次对应,多余的参数被 ARGN 参数吸收。

更多较为常用的命令:

  • add_custom_command:添加自定义规则命令,同样也是执行外界命令,但多了根据依赖和产物判断执行时机的作用。
  • install:添加 install 操作。
  • string:对 string 的所有操作,比如字符串替换啥的。
  • list:对 list 的所有操作,比如列表处理之类的。
  • foreach:cmake 中的 for 循环。
  • ...

利用上述命令实现 Qt 开发中调用 uic 工具把 大量的 .ui 文件转化为 .cpp 和 .h 文件,并实现当 ui 文件更新时或 .cpp/.h 文件不存在时才创建对应的 .cpp/.h 文件。

# 函数功能实现

function(get_ui_source)
foreach (item ${ARGN})
set(UIC_EXE_PATH ${VCPKG_ROOT}/installed/x64-windows/tools/qt5/bin/uic.exe)
get_filename_component(name ${item} NAME_WLE)
string(PREPEND name "ui_")
set(output_h ${PROJECT_SOURCE_DIR}/ui_gen/${name}.h)
set(output_cpp ${PROJECT_SOURCE_DIR}/ui_gen/${name}.cpp)
file(TIMESTAMP ${item} ui_time)

# 当.h 文件已经存在时,仅当.ui 文件被更新了才重新生成.h 文件

if (EXISTS ${output_h})
file(TIMESTAMP ${output_h} h_time)
if (ui_time GREATER h_time)
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_h})
endif ()
else ()
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_h})
endif ()

# 当.cpp 文件已经存在时,仅当.ui 文件被更新了才重新生成.cpp 文件

if (EXISTS ${output_cpp})
file(TIMESTAMP ${output_cpp} cpp_time)
if (ui_time GREATER cpp_time)
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_cpp})
endif ()
else ()
execute_process(COMMAND ${UIC_EXE_PATH} ${item} -o ${output_cpp})
endif ()
endforeach ()
endfunction()

Henry 2023-01-29 【此文自用】修改、转载自:https://juejin.cn/post/7184793007302901820

版权属于:字节星球/肥柴之家 (转载请联系作者授权)
原文链接:https://www.bytecho.net/archives/2225.html
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

Last Modified: March 2, 2023
Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

12 Comments
  1. 上海网站建设 上海网站建设 IP属地:江苏     Windows    Google Chrome

    感谢分享

  2. 小熊 小熊 IP属地:四川     iPhone    Safari

    大佬回归,开工大吉,不对开学大吉@(太开心)

    1. Henry Henry     Windows    Google Chrome

      @小熊开学还早

    2. 小熊 小熊 IP属地:四川     iPhone    Safari

      @Henry我申诉拦截,结果说大佬的站有恶意内容,呸,我先删两天通过了再加回来@(黑线)

    3. Henry Henry     Windows    Google Chrome

      @小熊什么情况

    4. 小熊 小熊 IP属地:四川     iPhone    Safari
      [私密评论] 仅管理员及评论双方可见
    5. Henry Henry     Windows    Google Chrome
      [私密评论] 仅管理员及评论双方可见
    6. 小熊 小熊 IP属地:四川     Windows    Google Chrome
      [私密评论] 仅管理员及评论双方可见
    7. Henry Henry     Windows    Google Chrome
      [私密评论] 仅管理员及评论双方可见
    8. 小熊 小熊 IP属地:四川     iPhone    Safari
      [私密评论] 仅管理员及评论双方可见
    9. Henry Henry     Windows    Google Chrome
      [私密评论] 仅管理员及评论双方可见
    10. 小熊 小熊 IP属地:四川     iPhone    Safari

      @Henry好呢,大佬,辛苦@(太开心)