让程序编译更优雅的几个CMake命令

简介

本文通过一个工程示例介绍了几个让程序编译更优雅的CMake命令。文末有完整下载地址。该工程示例首先生成一个动态库(libversion.dll:该库主要用于打印版本相关的信息),然后在一个可执行程序(cf_test)中使用该库。

测试环境

系统

Windows 10

编译环境

CLion:2020.1.1

编译工具

CMake:CLion内置

编译器

MinGW:gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)

CMake命令

CMake命令没有很深奥的东西,基本上只要知道有这么个东西,然后搜下命令格式,照着写就行了。很多时候很多东西并不是学不会,而是不知道有这么个东西。本文粗略介绍几个可以让代码编译更自然的CMake命令。

其实不这么写,粗暴的使用脚本也行。不过CMake既然提供了类似的命令,使用的话,会让工程代码看起来更简洁优雅些。本人有代码洁癖,在写代码的时候会尽量使用设计者提供的方式完成想要的功能,其实完全没必要做的这么细,但还是做了。这也是为什么会有此文的原因。

下文介绍的几个命令都有很多参数,只介绍在工程示例中使用的参数。

示例工程CMakeLists相关目录结构如下。下文中使用1,2,3代指对应的CMakeLists文件

|-- cppFollowers
    |-- CMakeLists.txt            # 1
    |-- ...
    |-- test
    |   |-- CMakeLists.txt        # 2
    |   |-- ...
    |-- version
        |-- CMakeLists.txt        # 3
        |-- ...

其中,3会生成动态库libversion.dll,2会链接动态库libversion.dll生成可执行程序,用于测试。

add_subdirectory

当工程文件较多时,直写一个CMakeLists有时会显得有些乱,也不方便管理。使用add_subdirectory可以将指定文件下的CMakeLists添加到build任务列表中。通过该方式可以实现对CMakeList模块化拆分。

示例工程中,当CMake命令执行时会首先执行1。1使用如下命令:

add_subdirectory(version)     # 3对应的目录
add_subdirectory(test)        # 2对应的目录

当1执行到这两条语句时,就会分别执行3和2。

project

project用来设置与工程相关的变量。命令原型如下:

project(<PROJECT-NAME> [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]])

VERSION参数可以对版本信息相关的变量进行设置。

示例工程中,3使用如下命令:

project(version VERSION 0.0.0.1)

会将如下左边的变量设置为右边的值。左边是CMake内置的变量。

PROJECT_NAME version

PROJECT_VERSION 0.0.0.1

PROJECT_VERSION_MAJOR 0

PROJECT_VERSION_MINOR 0

PROJECT_VERSION_PATCH 0

PROJECT_VERSION_TWEAK 1

如果不习惯CMake的命名,可以使用如下命令设置自定义变量。

set(CF_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}")
set(CF_VERSION_MINOR "${PROJECT_VERSION_MINOR}")
set(CF_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
set(CF_VERSION_TWEAK "${PROJECT_VERSION_TWEAK}")
set(CF_VERSION "${PROJECT_VERSION}")
set(CF_VERSION_SUFFIX "cppFollowers")

if (CF_VERSION_SUFFIX)
    set(CF_VERSION_FULL "${CF_VERSION}-${CF_VERSION_SUFFIX}")
else ()
    set(CF_VERSION_FULL "${CF_VERSION}")
endif ()

message("CF_VERSION = ${CF_VERSION}")
message("CF_VERSION_FULL = ${CF_VERSION_FULL}")

message命令用于CMake的输出。输出信息如下图所示:

让程序编译更优雅的几个CMake命令_第1张图片

configure_file

configure_file可以将源文件拷贝到另一个位置的同时将源文件中的变量替换为变量的值。若变量未定义,则替换为空。

示例工程中,3中使用如下命令。

configure_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/config.h.in"
        "${CMAKE_CURRENT_SOURCE_DIR}/config.h"
)

将以下config.h.in文件。

#define CF_VERSION_MAJOR ${CF_VERSION_MAJOR}
#define CF_VERSION_MINOR ${CF_VERSION_MINOR}
#define CF_VERSION_PATCH ${CF_VERSION_PATCH}
#define CF_VERSION_TWEAK ${CF_VERSION_TWEAK}
#define CF_VERSION "${CF_VERSION}"
#define CF_VERSION_FULL "${CF_VERSION_FULL}"

拷贝替换成以下config.h。

#define CF_VERSION_MAJOR 0
#define CF_VERSION_MINOR 0
#define CF_VERSION_PATCH 0
#define CF_VERSION_TWEAK 1
#define CF_VERSION "0.0.0.1"
#define CF_VERSION_FULL "0.0.0.1-cppFollowers"

也许你可能觉得这个命令有些多余,直接定义这个版本信息相关的宏不就行了么?或许你不知道发布版本的版本号呢?又或许发布版本的人不能改源码呢?又或许你想获取CMakeLists文件中别的变量的值呢?这个时候就需要这个命令了。

add_custom_command

add_custom_command可以将自定义的规则添加到目标构建的过程中。

示例工程中,3使用如下命令:

# PRE_BUILD PRE_LINK POST_BUILD all after Linking
# different from what I expected, why??????
add_custom_command(TARGET ${PROJECT_NAME} PRE_BUILD
        COMMENT "pre build..." # show before the commands are executed
        VERBATIM # recommended as it enables correct behavior.
        )

add_custom_command(TARGET ${PROJECT_NAME} PRE_LINK
        COMMAND ${CMAKE_COMMAND} -E echo "pre link..."
        VERBATIM # recommended as it enables correct behavior.
        )

# copy version.h libversion after build target
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E echo "copy version.h $..."
        COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/version.h ${CMAKE_CURRENT_SOURCE_DIR}/../include
        COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/../lib/$ ${CMAKE_CURRENT_SOURCE_DIR}/../bin
        VERBATIM
        )

分别在TARGET构建前PRE_BUILD、链接前PRE_LINK和构建后POST_BUILD执行COMMAND中的指令。PRE_BUILDPRE_LINK只是测试,POST_BUILD将动态库的头文件和库文件分别拷贝到include和bin目录中。

注意,PRE_BUILDPRE_LINKPOST_BUILD都是针对TARGET的构建,并不是针对编译的。测试环境中执行时顺序如下图所示:

让程序编译更优雅的几个CMake命令_第2张图片

可以看到在PRE_LINK对应的命令执行的时候源文件已经编译完成了。

set_directory_properties

上面使用add_custom_command可以在目标构建后拷贝了头文件和库文件,你可能想在目标clean的时候将这两个文件clean掉。set_directory_properties可以实现这个需求。

示例工程中,3使用如下命令:

# rm version.h libversion when make clean
set(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../include/version.h
        ${CMAKE_CURRENT_SOURCE_DIR}/../bin/$
        )
set_directory_properties(PROPERTIES
        ADDITIONAL_MAKE_CLEAN_FILES "${SRCS}"
        )

在执行clean时会将上文使用add_custom_command拷贝的头文件和库文件删除。

install

install命令可以实现对工程的安装。实际上就是指定一些文件,在执行install命令的时候将其拷贝到对应的目录。这个命令可以很方便的将工程编译后的目标文件汇总将其提供给他人。

示例工程中,1使用如下命令:

#install dir prefix
set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_SOURCE_DIR}/install)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ DESTINATION include FILES_MATCHING PATTERN "*.h")
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/lib/ DESTINATION lib)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin/ DESTINATION bin)

在执行install命令时会将工程的头文件,库文件和可执行文件拷贝到install文件中。所有安装的文件都被记录在install_manifest.txt文件中。

遇到的坑

1、CLion的Reload CMake Project有时候Reload不彻底。

当发现CMakelists修改以后不生效时,可以试着将cmake-build-debug文件下的那一坨文件删掉再试下。

2、在windows下使用gcc编译动态库时,对于库中的函数有以下两种说法。其实都是不全面的。

2.1、windows下默认隐藏。实测并不是这样的,实测中发现当有一个函数export后,别的不export的函数才会隐藏。若完全不使用export,所有的函数都是导出的。不知道为什么这么设计,这个问题花费了我五个小时左右时间呀。

2.2、可以使用如下类似命令隐藏函数。在测试环境下貌似并没有什么用,在linux环境下应该有用,家里没有linux环境,没试。

set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_C_FLAGS$ "${CMAKE_C_FLAGS} -fvisibility = hidden")
set(CMAKE_CXX_FLAGS$ "${CMAKE_CXX_FLAGS} -fvisibility = hidden")

3、这个坑比较有意思。在dev分支开发完了需要merge到master分支,由于命令比较多。所以写了一个脚本。

@echo off
:: switched to dev
git checkout dev || GOTO label
:: pull from remote dev
git pull || GOTO label
:: switched to master
git checkout master || GOTO label
:: pull from remote master
git pull || GOTO label
:: merge dev to master
git merge dev || GOTO label
:: push to remote master
git push -u origin master || GOTO label
:: switched to dev
git checkout dev 
:label
pause
exit

第一次使用这个脚本合并分支的时候,执行到中途时,dos窗口提示“批处理文件不存在”。我发现这个脚本确实不在了,很诧异。其实是因为当脚本执行到第7行的时候分支切换到了master,master分支本身没有这个脚本,所以执行出错了。相当于脚本文件将自己删除了,但是系统还在执行脚本文件,然后报错了。

当时在想如果这个执行脚本的这个程序要是我写的,肯定就崩了,因为我根本不会考虑到这种情况。很多时候程序员会觉得用户傻X,比如这种情况,就不是故意的。

于是我手动将dev分支merge到master分支。不过很奇怪,当master分支有这个脚本以后按理说是两个不同的文件,切过去以后脚本竟然还能接着执行。既然能用,就不深究脚本执行的原理了。

源码地址

在公众号后台回复[cppFollowers]获取完整源码地址。为什么我不写一个简单回复,比如[c],就可以获取源码路径呢。其实是有小情绪的,因为整理这篇文章含源码陆续的花费了两周时间。这不是为了CMake优雅么,大部分时间花费在查找测试CMake命令上。

下载完成后建议直接使用默认的master分支。dev分支用于测试新的代码可能会有瑕疵。

本人公众号链接原文:让程序编译更优雅的几个CMake命令

你可能感兴趣的:(工具类,cmake)