cmake用法总结

Git使用

clone带子模块项目

git clone project.git project2

cd project2

git submodule init

git submodule update

cd ..

 

引入

cmake是跨平台的编译工具

先看一个简单案例

rule-1

# 主编译规则目标是产生可执行文件testc

# 1) cmake基础配置包括最低支持版本工程名等

cmake_minimum_required(VERSION 3.2)

project(testc) #可选工程名与编译目标不是没有直接对应关系

 

# 2) 执行子编译规则,编译子模块

add_subdirectory(src/dll0) #参数为子模块编译规则所在目录

 

# 3) 本规则编译器配置,主要是各类编译选项

set(CMAKE_CXX_STANDARD 11)

add_compile_options(/MD)

 

# 4) 搜索与本编译规则相关的源文件

include_directories(./src ./src/dll0)

aux_source_directory(. DIRSRCS) # 搜索指定目录下所有源文件,.c/.cpp/.cxx

message(STATUS ${DIRSRCS})

 

# 5) 执行编译及链接

add_executable(testc main.cpp src/func.cpp)

target_link_libraries(testc dll0)

# 子编译规则目标是产生动态库文件dll0

aux_source_directory(. DIRSRCS)

link_libraries(${all_libs}) // 指定链接依赖库

add_library(dll0 SHARED ${DIRSRCS}) // 产生动态库

install (TARGETS dll0 DESTINATION <path>) //将目标安装到指定路径下

上面例子中,工程的目录如下:

testc/

CMakeLists.txt #主编译规则

| main.cpp

| src/

| | func.cpp

| | func.h

| | dll0/

| | | CMakeLists.txt #子编译规则

| | | func0.h

| | | func0.cpp

利用cmake工具编译c/c++项目的关键就是编译规则的指定,默认情况下一个CMakeLists.txt表示一条编译规则,一条编译规则的目标是产生一个或多个编译目标(或可执行文件或动态库或静态库)

 

利用cmake编译c/c++项目,编译过程基本可以归纳为rule-1中的5个步骤。cmake本身需要学习的成本在于掌握cmake提供的内置变量及方法。

 

内置变量

变量名

说明

备注

CMAKE_CXX_STANDARD

设置CXX标准98,11,14等

对VC编译器无效

CMAKE_BINARY_DIR

PROJECT_BINARY_DIR

<projectname>_BINARY_DIR

这三个变量指代的内容是一致的。in-source build表示工程顶层目录;out-of-source build表示工作路径,

 

CMAKE_SOURCE_DIR

PROJECT_SOURCE_DIR

<projectname>_SOURCE_DIR

这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。

 

CMAKE_CURRENT_SOURCE_DIR

指的是当前处理的 CMakeLists.txt 所在的路径, 比如rule-1中的 dll0/ 子目录。

 

CMAKE_CURRRENT_BINARY_DIR

编译目标的存放路径

如果是 in-source 编译,它跟 CMAKE_CURRENT_SOURCE_DIR 一致, 如果是 out-of-source 编译,他指的是 target 编译目录。

使用我们上面提到的 ADD_SUBDIRECTORY(src bin)可以更改这个变量的值。

使用 SET(EXECUTABLE_OUTPUT_PATH <新路径>)并不会对这个变量造成影响, 它仅仅修改了最终目标文件存放的路径。

v3.11.2后无此变量

CMAKE_CURRENT_LIST_FILE

输出调用这个变量的 CMakeLists.txt 的完整路径

 

CMAKE_CURRENT_LIST_LINE

输出这个变量所在的行

 

CMAKE_MODULE_PATH

这个变量用来定义自己的 cmake 模块所在的路径。如果工程比较复杂,有可能会自己编写一些 cmake 模块, 这些 cmake 模块是随你的工程发布的, 为了让 cmake 在处理CMakeLists.txt 时找到这些模块,你需要通过 SET 指令,将自己的 cmake 模块路径设置一下。

如:

根目录下由文件夹

cmake

| modular1.cmake #cmake模块以.cmake为后缀

| modular2.cmake

SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

这时候你就可以通过 INCLUDE 指令来调用自己的模块了。

include(modular1)

设置后,cmake就会在include时自动在这个路径中去找模块

EXECUTABLE_OUTPUT_PATH / LIBRARY_OUTPUT_PATH

分别用来重新定义最终结果的存放目录,前面我们已经提到了这两个变量。

设置LIBRARY_OUTPUT_PATH后,便无需调用install()安装库文件了

注意,这两个变量设置后,会影响编译目标的存放路径

PROJECT_NAME

返回通过 PROJECT 指令定义的项目名称

 

环境变量相关

$ENV{NAME}

使用$ENV{NAME}指令可以调用系统的环境变量

比如

MESSAGE(STATUS “HOME dir: $ENV{HOME}”)

设置环境变量的方式是:

SET(ENV{变量名} 值)

 

CMAKE_INCLUDE_CURRENT_DIR

自动添加 CMAKE_CURRENT_BINARY_DIR 和 CMAKE_CURRENT_SOURCE_DIR 到当前处理

的 CMakeLists.txt。相当于在每个 CMakeLists.txt 加入:

INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}

${CMAKE_CURRENT_SOURCE_DIR})

启用:

set(CMAKE_INCLUDE_CURRENT_DIR ON)

 

CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE

将工程提供的头文件目录始终至于系统头文件目录的前面,当你定义的头文件确实跟系统发生冲突时可以提供一些帮助。

默认为OFF, 启用设置为ON

 

CMAKE_INCLUDE_PATH / CMAKE_LIBRARY_PATH

 

 

 

内置指令

execute_process

CMake可以通过execute_process调用shell命令,其使用如下:

execute_process(COMMAND <cmd1> [args1...]]

                [COMMAND <cmd2> [args2...] [...]]

                [WORKING_DIRECTORY <directory>]

                [TIMEOUT <seconds>]

                [RESULT_VARIABLE <variable>]

                [OUTPUT_VARIABLE <variable>]

                [ERROR_VARIABLE <variable>]

                [INPUT_FILE <file>]

                [OUTPUT_FILE <file>]

                [ERROR_FILE <file>]

                [OUTPUT_QUIET]

                [ERROR_QUIET]

                [OUTPUT_STRIP_TRAILING_WHITESPACE]

                [ERROR_STRIP_TRAILING_WHITESPACE])

按照上述格式,我写的测试CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.7)

message("Test cmake call shell command")

execute_process(COMMAND echo "Hello")

 

案例1:

想获取shell指令的输出,比如搜索当前目录下所有.c文件,并放入变量OUT_VAR

execute_process(

    COMMAND ls

    COMMAND grep -n "\\.c"

    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}

    OUTPUT_VARIABLE OUT_VAR

    RESULT_VARIABLE RET_VAR)

 

案例2

我们考虑这样一种场景:

现有目录结构如下:

|-cmakeTest

   |-shell.sh

   |-CMakeLists.txt

   |-src

      |-main.c

我们需要把 src/main.c 提取到 cmakeTest 目录下,对其进行编译。我们的做法是这样子的:shell.sh 脚本执行复制文件操作,CMakeLists.txt 调用 shell.sh 获得 main.c 并完成构建。

 

main.c 是这样的:

#include <stdio.h>

 

int main()

{

    printf("Hello CMake!\n");

    return 0;

}

 

shell.sh 是这样的:

echo "Begin"

cp  ../$1/main.c  ../main.c

echo "End"

 

注意 cp 语句中,main.c 的路径写法,还有$1参数。一会儿解释为什么是这样的。

重点来了,CMakeLists.txt 是这样的:

cmake_minimum_required (VERSION 2.6)

execute_process(COMMAND sh ../shell.sh src)

aux_source_directory(. SRC_LIST)

add_executable(main ${SRC_LIST})

 

CMakeLists.txt 通过 execute_process() 命令调用了 shell.sh 并传入了参数 src 。这里的 src 就是 shell.sh 里面 cp 语句的$1,cp 语句展开后就是:

cp ../src/main.c ../main.c

 

至于这奇怪的 ../ 路径,是因为 execute_process() 命令开起了一个子线程来执行 shell 命令,这个子线程的工作目录和当前工作目录一致。我会采用外部构建方式执行 CMake,也就是新建 build 目录,进入 build 目录执行 CMake,于是子线程的工作目录也就是当前工作目录——build,所以需要../才能够正确找到需要的文件。

 

完整的演示如下:

$ mkdir build

$ cd build

$ cmake ..

-- The C compiler identification is GNU 4.8.4

-- The CXX compiler identification is GNU 4.8.4

-- Check for working C compiler: /usr/bin/cc

-- Check for working C compiler: /usr/bin/cc -- works

-- Detecting C compiler ABI info

-- Detecting C compiler ABI info - done

-- Check for working CXX compiler: /usr/bin/c++

-- Check for working CXX compiler: /usr/bin/c++ -- works

-- Detecting CXX compiler ABI info

-- Detecting CXX compiler ABI info - done

Begin

End

-- Configuring done

-- Generating done

-- Build files have been written to: /????/CMakeTest/build

$ make

Scanning dependencies of target main

[100%] Building C object CMakeFiles/main.dir/main.c.o

Linking C executable main

[100%] Built target main

$ ./main

Hello CMake!

 

cmake加载链接库

cl编译器与gcc加载动态库区别:

windows系统与linux系统中动态库形式分别为dll和so。当程序以静态方式加载动态库时,需要在程序产生的链接阶段将动态库的信息包括函数地址,导出符号等写入到程序中,程序运行时才能根据这些信息找到动态库并从中得到函数的地址。如,linux下gcc –lmylib这句指令将执行找寻libmylib.so动态库,并将其中链接信息写入链接目标。而windows中则可在代码中加入#pragma comment(“libmylib.lib”)编译指令加载动态库的导入库。(注:windows中动态库的导入信息和执行代码是分离的,所以产生一个动态库既有dll文件又有lib文件)

 

find_library

find_library指令用于查找库,如windows下实际寻找.lib(可以是静态库也可以是动态库的导入库),linux下则查询.so和.a。查找成功后会将库文件名放在VAR变量中,表示找到。

这个指令用在用户知道库文件路径时使用。

 

find_package

find_package与find_library类似,也是寻找库,不过他的目的是寻找某个已经安装的库的所有依赖库文件,比如find_package(boost)。当调用此指令时,cmake先找寻用户cmake模块中Findxxx.cmake执行,xxx与find_package()指令中指定的名字须相同(可忽略大小写),若无则再查询cmake自带的库查找脚本。

当程序中需要编译多个组件时,也可以将组件的编译脚本写到Findxxx.cmake中,并通过调用find_package(xxx)编译安装。

注:cmake会默认在CMAKE_MODULE_PATH中的路径下找Findxxx.cmake,所以需要将本地路径加入进去

set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake)

 

cmake语法检测

cmake提供一些宏可以区分编译的平台和编译器,比如MSVC宏若为TRUE则表示默认编译器为cl。但是单纯知道编译器有时候也不够,比如gcc是比较主流的编译器,但每个版本对c++的标注你支持不同,为了使cmake能够区分具体编译器的版本,或对某种语法的支持,可以采取编译代表语法的代码片段的方式。

如:通过check_cxx_source_compiles(“int main(){_sprintf_s(\“hello\”)}”)来检测编译器是否支持_sprintf_s()函数

 

list 和 set

set(var a b c d e)创建了一个这样的列表:a;b;c;d;e。 set(var “a b c d e”)创建了一个字符串或只有一个元素的列表。

注意当

execute_process(COMMAND gcc ${LFLAG} ${OBJS} -o libst.so)编译时,${OBJS}必须是列表而非字符串或一个元素!

 

条件if

if(expression)

  # then section.

  COMMAND1(ARGS ...)

  COMMAND2(ARGS ...)

  ...

elseif(expression2)

  # elseif section.

  COMMAND1(ARGS ...)

  COMMAND2(ARGS ...)

  ...

else()

  # else section.

  COMMAND1(ARGS ...)

  COMMAND2(ARGS ...)

  ...

endif()

例子:

if(" ${CMAKE_SOURCE_DIR}" STREQUAL " ${CMAKE_BINARY_DIR}")

  message(FATAL_ERROR "

FATAL: In-source builds are not allowed.

       You should create a separate directory for build files.

")

endif()

STREQUAL 是 CMAKE 的关键字,用于字符串比较,相同返回 true

${CMAKE_SOURCE_DIR} 是 CMAKE 的自保留变量(拿来用就可以,含义已经确定),文件路径

${CMAKE_BINARY_DIR}是输出路径

 

关系操作符

NOT

非,NOT E1

AND

与,E1 AND E2

OR

或,E1 OR E2

EXIST

~ E,存在 name 的文件或者目录(应该使用绝对路径),真

COMMAND

~ E,存在 command-name 命令、宏或函数且能够被调用,真

DEFINED 

~ E,变量被定义了,真

EQUAL

E1 ~ E2,变量值或者字符串匹配 regex 正则表达式

LESS

GREATER

STRLESS

E1 ~ E2,变量值或者字符串为有效的数字且满足小于(大于、等于)的条件

STRGREATER 

STREQUAL

 

install

 

使用场景

CMake来编译Qt程序

Qt自带的qmake会出现许多问题(比如文件修改之后有时候qmake不会侦测到不会重新编译,需要手动去编译等),于是开始尝试使用CMake来编写Qt程序。

    CMake对于一些有名的库都有自带文件夹中Modules里。cmake文件查询的支持,比如你需要编写Qt程序,你就可以去cmake_dir/Moudles/查找 FindQt4.cmake这个文件,里面详细讲述了如果你需要用到Qt库,你需要包含的变量和文件,比如他举出了 QT_USE_FILE 这个变量,你直接include在CMake脚本之后,你就不需要手动的include_diectories等等,同时它也会生成QT_LIBRARIES这个变量让你来target_link,因此省去了很多自己需要逐步查询qmake所在路径和Qt库所在路径的问题。

 

比较简单的用法,

find_package(Qt4 4.4.3 REQUIRED QtCore QtGui QtXml)

include(${QT_USE_FILE})

add_executable(myexe main.cpp)

target_link_libraries(myexe ${QT_LIBRARIES})

find_package来查询你需要用到的Qt版本库,之后REQUIRED表示你需要用到Qt中的哪些子库,之后include它生成的文件,link它给你生成的库文件变量,你的Qt简单的Demo就成功了,是不是很简单。

   同时我再来讲一下moc的简单用法,Qt的机制它会查询Q_OBJECT这个宏如果你的文件有这个宏,它的qmake会自动去moc一把生成moc_xxx.cpp文件,然后会内部帮你include他们,所在在IDE端Qt Creator,我们根本察觉不到这个机制在里面,所以IDE用多了有时候确实察觉不到这些比较底层的机制,用手写部署确实有其好处。回归正题,在CMake中,你如何去实现由qmake帮你做的这些步骤呢?答案有很多,我这里列举一个比较简单的用法,就是给target设置属性,

set_target_properties(${target_name} PROPERTIES ${properties_name} ${properties_value})

CMake给Qt提供了AUTOMOC这个属性,可以自动的为给定target的项目的所有需要moc的文件自动moc,所以这个时候我们只需要加一把set_target_properties(myexe PROPERTIES AUTOMOC ON),这个时候,CMake就会去学qmake的那套逻辑来进行自动moc和编译了。

 

案例

为什么要用cmake来构建qt5的项目呢?qt不是有qmake吗?这样,岂不是多此一举?

其实,应用cmake来构建项目还是非常有必要的,特别是当你的项目涉及到很多第三方库的时候,cmake的优势非常突出。

举个简单的例子:

假如我要开发一个基于pcl 1.8.0,vtk 7.0,opencv3.2.0, eigen3, Sophus ……等其他的第三方库的qt5的项目

而不仅仅是只用qt一家的库。

qmake只针对qt自身的库有优势,如果你的项目中需要依赖很多的第三方库,而你又觉得手动配置第三方库的.pro文件挺麻烦的,费力不讨好。

就拿pcl来说吧,其实安装一点都不难,非常简单,即使是源码安装也很容易,顶多是cmake配置项需要花一点时间,而头疼的是当你需要用qmake构建项目的时候,需要配置很多头文件和库文件.

 

这里,我就仅仅以一个很简单的实例,来教大家如何使用cmake构建和管理项目:

首先,我们还是要使用qt creater创建中规中矩的qt5的项目:helloworld

项目中的文件列表如下:项目虽小,五脏俱全,该有的文件都有了(.h .cpp .qrc .ui .pro .png)

我把.png文件和.qrc文件放在了一个新建的resources资源文件夹中

├── helloworld.pro

├── helloworld.pro.user

├── main.cpp

├── resources

│   ├── ico.png

│   └── resources.qrc

├── widget.cpp

├── widget.h

└── widget.ui

 

该项目中唯一需要添加代码的地方是widget.cpp文件,因为我们需要添加一个图标

#include "widget.h"

#include "ui_widget.h"

 

Widget::Widget(QWidget *parent) :

    QWidget(parent),

    ui(new Ui::Widget)

{

    ui->setupUi(this);

    //窗体标题

    this->setWindowTitle("Qt5.1 窗体应用");

    //窗体 ICO 图片,如图不起别名,后缀直接写图片全名。

    this->setWindowIcon(QIcon(":/new/prefix1/ico.png"));

}

 

Widget::~Widget()

{

    delete ui;

}

 

接着在在项目文件夹中手动创建一个CMakeLists.txt文件。内容如下:

cmake_minimum_required(VERSION 2.8.11 FATAL_ERROR)

project(helloworld)

 

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOMOC ON)

set(CMAKE_AUTOUIC ON)

set(CMAKE_AUTORCC ON)

set(RESOURCE_DIR resources/resources.qrc)

find_package(Qt5 REQUIRED Widgets)

qt5_wrap_cpp( MOC widget.h)

qt5_wrap_ui( UIC widget.ui)

qt5_add_resources(RCC resources.qrc)

 

add_executable(helloworld main.cpp widget.cpp widget.h widget.ui ${RESOURCE_DIR})

target_link_libraries(helloworld Qt5::Widgets)

 

最后的项目组成如下:

├── CMakeLists.txt

├── CMakeLists.txt.user

├── helloworld.pro

├── helloworld.pro.user

├── main.cpp

├── resources

│   ├── ico.png

│   └── resources.qrc

├── widget.cpp

├── widget.h

└── widget.ui

最后,你可以关闭之前创建的helloworld项目,直接打开CMakeLists.txt文件,一开始会弹出一个configure窗口,直接configure就可以实现项目配置,即使没有弹出这个窗口也没关系,直接快捷键保存该文件,系统就会直接configure和generate. 最后选中项目右键点击运行项目即可。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章