CMake入门
1. CMake的介绍:
当多人开发同一个项目时,最终要输出一个可执行文件或者共享库(dll、so等),就可以使用cmake了
所有操作都是通过 CMakeLists.txt(严格区分大小写) 来完成的 ----> 简单
2. CMake的helloworld编译:
步骤一,写一个helloworld:
#include <iostream>
int main() {
std::cout << "hello world" << std::endl;
return 0;
}
步骤二,写CMakeLists.txt
PROJECT (HELLO)
SET(SRC_LIST main.cpp)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir " ${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
步骤三,使用cmake,生成makefile文件
cmake .
输出:
cmake .
-- The C compiler identification is AppleClang 12.0.0.12000032
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Library/Developer/CommandLineTools/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- This is BINARY dir /Users/xuesong/SONGSONG
-- This is SOURCE dir /Users/xuesong/SONGSONG
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/xuesong/SONGSONG
步骤四,使用make命令编译:
make
输出:
[ 50%] Building CXX object CMakeFiles/hello.dir/main.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
步骤五,最终生成了hello的可执行程序
3. CMakeLists语法介绍:
PROJECT 关键字:
可以用来指定工程的名字和支持的语言,默认支持所有语言;
PROJECT (HELLO)
指定了工程的名字,并且支持所有语言(建议)
PROJECT (HELLO CXX)
指定了工程的名字,并且支持语言是C++
PROCECT (HELLO C CXX)
指定了工程的名字,并且支持语言是C和C++
该指令隐式定义了两个CMAKE的变量:
<projectname>_BINARY_DIR
本例中是 HELLO_BINARY_DIR
<projectname>_SOURCE_DIR
本例中是 HELLO_SOURCE_DIR
MESSAGE关键字就可以直接使用这两个变量,当前都指向当前的工作目录,后面会涉及到外部编译。
此时有一个问题,如果改了工程名,这两个变量名也会改变,所以又定义了两个预定义变量:PROJECT_BINARY_DIR
和 PROJECT_SOURCE_DIR
,这两个变量和 HELLO_BINARY_DIR、HELLO_SOURCE_DIR是一致的。
SET关键字:
用来显式的指定变量,例如
SET(SRC_LIST main.cpp)
即使用来指定源文件列表
MESSAGE关键字:
MESSAGE关键字类似于C语言中的printf函数,用于将变量的值显示到终端,这样就可以查看cmake构建工程时CMakeLists.txt中某些变量的值是否正确。但是,MESSAGE要比printf函数的功能强大,它可以 终止 编译系统的构建,而且这通常是我们想要的效果。
语法格式:
MESSAGE([<mode>] "message text" ...)
mode的值包括 FATAL_ERROR
、WARNING
、AUTHOR_WARNING
、STATUS
、VERBOSE
等
FATAL_ERROR:产生CMake Error,会停止编译系统的构建过程;
STATUS:最常用的命令,常用于查看变量值,类似于编程语言中的DEBUG级别信息。
“message text”为显示在终端的内容。
ADD_EXECUTABLE关键字:
生成可执行文件,例如:
ADD_EXECUTABLE(hello ${SRC_LIST})
生成的可执行文件名是“hello”,源文件读取变量SRC_LIST中的内容。
语法的基本规则:
- 使用
${}
获取某个变量的值,例如${SRC_LIST}
获取SRC_LIST变量的值; - 指令中的参数使用
()
小括号括起来,参数之间使用空格或分号分隔开; - 指令是大小写无关的,参数和变量 是大小写相关的,CMake中推荐指令全部大写。
4. CMake内部构建和外部构建:
内部构建就是在项目内部,有CMakeLists.txt的地方,直接 cmake .
,比如上面的方式就是内部构建,它的缺点是会在项目目录下生成许多临时文件。
外部构建就是不直接在项目下面运行cmake,而是自己建立一个接收cmake之后产生的临时文件的文件夹(如 build/),然后在该文件下面调用 cmake <CMakeList_path>
来构建。
例如:
mkdir build
cd build
cmake ..
5. 让HelloWorld更像一个工程:
- 为工程添加一个子目录src,用来放置工程源代码
- 添加一个子目录doc,用来放置这个工程的文档hello.txt
- 在工程目录添加文本文件COPYRIGHT,README
- 在工程目录添加一个 runhello.sh 脚本,用来调用hello二进制
- 将构建后的目标文件放入构建目录的bin子目录
- 将doc目录的内容以及COPYRIGHT/README安装到/usr/share/doc/cmake
然后在 build/ 目录下执行 cmake ..
ADD_SUBDIRECTORY命令:
命令格式:
ADD_SUBDIRECTORY([source_dir] [binary_dir] [EXCLUDE_FROM_ALL])
[source_dir]: 源文件路径;
[binary_dir]: 中间二进制与目标二进制存放路径;
[EXCLUDE_FROM_ALL]:将这个目录从编译过程中排除。
在项目中CMake配置文件一般为 树形结构,项目总的CMake配置文件调用各模块的CMake配置文件;
6. 使用CMake来进行安装:
make:编译程序、库、文档等
make install:安装已经编译好的程序、复制文件树中的文件到指定的位置
make unistall:卸载已经安装的程序
make clean:删除由make命令产生的文件
make distclean:删除由./configure产生的文件
make check:测试刚刚编译的软件(某些程序可能不支持)
make installcheck:检查安装的库和程序(某些程序可能不支持)
cmake INSTALL 命令:INSTALL用于指定在安装时运行的规则,
通俗的说,就是在运行cmake生成makefile之后,make intall 时都需要安装哪些内容。
它可以用来安装很多内容,可以包括 目标二进制、动态库、静态库、文件、目录、脚本等。
INSTALL(TARGETS <target> ... [...]) // TARGETS: 库(.a, .so)
INSTALL({FILES | PROGRAMS} <file> ... [...]) // FILES: 文件 PROGRAMS: 二进制(可执行)文件
INSTALL(DIRECTORY <dir> ... [...]) // DIRECTORY: 目录
INSTALL(SCRIPT <file> [...]) // SCRIPT: 脚本
INSTALL(CODE <code> [...]) //
INSTALL(EXPORT <export-name> [...]) //
举例:
在 项目的顶层目录下先手动创建COPYRIGHT、README、runhello.sh文件
顶层的CMakeLists.txt中加入INSTALL命令:
然后在 build/ 目录下 cmake ..
, make
, make install
:
在share目录下就拷贝了上述几个文件:
7. CMake构建动态库和静态库:
顶层目录:
顶层CMakeLists.txt的内容:
lib/ 目录下的内容:
lib目录下的CMakeLists.txt的内容:
STATIC
表示生成静态库,SHARED
表示生成动态库,生成的库文件格式是 libXXX.a
或 libXXX.so
在 build/ 目录下执行 cmake ..
和 make
,就可以在 build/lib/ 目录下查看到生成的库文件:
ADD_LIBRARY 命令:
命令格式:
ADD_LIBRARY(<name> [ STATIC | SHARED | MODULE ]
[EXCLUDE_FROM_ALL]
[source1] [source2][...])
添加名为 name
的库,库的文件件可指定,也可用 target_sources()
后续指定。
库的类型是 STATIC(静态库)/SHARED(动态库)/MODULE(模块库)
之一。
如何同时构建动态库和静态库:
其他的不需要改动,lib/ 目录下的CMakeLists.txt 文件:
将动态库和静态库文件编译的名字都设置为“hello”即可。
编译后 在 build/lib/ 目录下就同时生成了 libhello.a 和 libhello.so:
SET_TARGET_PROPERTIES 命令:
命令格式:
SET_TARGET_PROPERTIES(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)
这个命令是设置目标的属性,设置之后可以使用 GET_PROPERTY()
或者GET_TARGET_PROPERTY()
命令获取目标的属性值。
SET_TARGET_PROPERTIES 命令除了可以实现同时生成动态库和静态库,还可以用来 设置动态库的版本号:
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)
VERSION指代动态库版本,SOVERSION指代API版本。
8. 共享库与头文件的安装和使用:
使用CMake安装库文件:
在 lib/ 目录下的CMakeLists.txt中 加入INSTALL命令:
执行 cmake ..
, make
, make install
,安装头文件和共享库:
INSTALL命令在安装库文件时:
INSTALL(TARGETS MyLib
EXPORT MyLibTargets
LIBRARY DESTINATION lib # 动态库安装路径
ARCHIVE DESTINATION lib # 静态库安装路径
RUNTINE DESTINATION bin # 可执行文件安装路径
PUBLIC_HEADER DESTINATION include # 头文件安装路径
)
PS:为什么Linux安装程序都要放到 “/usr/local” 目录下:
Linux的软件安装目录也是有讲究的,理解这一点,在对系统管理是有益的。
/usr: 系统级的目录,可以理解为C:/Windows/,/usr/lib 可以理解为 C:/Windows/System32;
/usr/local: 用户级的程序目录,可以理解为 C:/Program Files/,用户自己编译的软件默认会安装到这个目录下;
/opt:用户级的程序目录,可以理解为 D:/Software,opt有可选的意思,这里可以用于放置第三方大型软件(或游戏等),当你不需要时,直接 rm -rf 删除即可。在硬盘容量不够时,也可将 /opt 单独挂载到其他磁盘上使用。
源码放哪里:
/usr/src:系统级的源码目录
/usr/local/src: 用户级的源码目录
使用CMake链接库文件:
在另外的目录下编写一个main.cpp,其中包含上面的hello.h头文件:
// main.cpp
#include <hello.h>
int main() {
func();
return 0;
}
此时运行make
编译时会报错,找不到hello.h头文件,这是因为上面安装hello.h时指定的安装目录是 “include/hello”:
直接的解决方法有两种:
- hello.h 在cmake install安装时直接安装到 /usr/local/include 目录下,取消 hello/这一级目录;
- main.cpp 源文件中使用 #include <hello/hello.h> 的方式指名包含的路径。
然而,在项目中,更普遍的做法是使用 cmake中的 INCLUDE_DIRECTORIES
命令解决这个问题:
INCLUDE_DIRECTORIES 命令:
命令格式:
INCLUDE_DIRECTORIES([AFTER | BEFORE] [SYSTEM] dir1 [dir2 ...])
将指定目录添加到编译器的头文件搜索路径之下,指定的目录被解释成当前源码路径的相对路径。
PS:#include <> 与 #include “” 的区别:
-
使用场景不同:
#include <>
一般用于包含系统头文件,例如 stdlib.h,stdio.h 等;
#include ""
一般用于包含自定义的头文件,例如 test.h,declare.h 等; -
查找的目录不同:
#include <>
编译器直接从系统类库目录里查找头文件,例如 在Linux gcc编译环境下,一般为/usr/include
和/usr/local/include
目录;如果类库目录下查找失败,编译器会终止查找,直接报错:No such file or directory
#include ""
默认从项目当前目录查找头文件,即项目工程文件所在的目录。如果在项目当前目录下查找失败,再从项目配置的头文件引用目录查找头文件。
在加入 INCLUDE_DIRECTORIES
命令之后,就可以解决 hello.h头文件找不到的问题了:
这是因为没有链接 hello.so 库,所以找不到func()函数的定义。
解决方法有两种:
- LINK_DIRECTORIES 命令,添加非标准共享库的搜索路径(某个路径);
- TARGET_LINK_LIBRARIES 命令,添加需要链接的共享库(没某个具体的库)(注意此关键字在CMakeListst.txt中必须位于 ADD_EXECUTABLE 之后)。
命令格式:
TARGET_LINK_LIBRARIES(<target>
<PRIVATE | PUBLIC | INTERFACE> <item> ...
[<PRIAVTE | PUBLIC | INTERFACE> <item> ...] ...)
对于静态库也是相同的方式:
TARGET_LINK_LIBRARIES(main libhello.a)
此时解决了make时找不到func()函数定义的问题,并可以成功生成main可执行文件。
但是在执行 ./main 时,又报错找不到 libhello.so 动态库:
使用 ldd
查看main的依赖中抓不到libhello.so的路径:
这是因为,默认情况下,编译器只会使用 /lib
和 /usr/lib
这两个目录下的库文件,如果不指定 --prefix,会将库安装在 /usr/local/lib
目录下;当运行程序需要链接动态库时,提示找不到相关的.so库,就会报错。
解决方法有两个:
- 手动将 /usr/local/lib 目录下的 libhello.so 拷贝到 /usr/lib 目录下;
- 在CMakeListsts.txt 中加入prefix,指定so库的安装路径。
CMAKE_INSTALL_PREFIX 命令:
CMAKE_INSTALL_PREFIX 是cmake内置变量,用于指定cmake执行install目标时,安装的路径前缀。
方法一: 在执行cmake时指定:
cmake -D CMAKE_INSTALL_PREFIX=<目标路径>
方法二: 在 CMakeLists.txt 中设置变量:
SET(CMAKE_INSTALL_PREFIX <install_path>)
在编译.so库文件时指定install安装路径:
make install 时可以看到成功安装到了 /usr/lib 路径下:
重新回到main工程下,执行./main,可以正常执行了: