目录
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 模块路径设置一下。 如: 根目录下由文件夹
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. 最后选中项目右键点击运行项目即可。