CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。只是 CMake 的组态档取名为CMakeLists.txt。
Cmake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再以一般的建构方式使用。可以通过CMake官方网站获得更多关于cmake 的信息。
可以理解为,编写Makefile难度太大,CMake基于Makefile做了二次开发。
我当前的CMake版本:
$ cmake --version
cmake version 3.5.1
CMake suite maintained and supported by Kitware (kitware.com/cmake).
有些项目有最低版本要求,如果版本过低,可能项目会编译不了,可以升级CMake。
(1)卸载已经安装的旧版的CMake[非必需]。
sudo apt-get autoremove cmake
(2)下载CMake压缩包。
wget https://cmake.org/files/v3.21/cmake-3.21.3-linux-x86_64.tar.gz
(3)解压压缩包。
tar zxvf cmake-3.21.3-linux-x86_64.tar.gz
查看解压后目录:
cmake-3.21.3-linux-x86_64
├── bin
│ ├── ccmake
│ ├── cmake
│ ├── cmake-gui
│ ├── cpack
│ └── ctest
├── doc
│ └── cmake
├── man
│ ├── man1
│ └── man7
└── share
├── aclocal
├── applications
├── bash-completion
├── cmake-3.21
├── emacs
├── icons
├── mime
└── vim
15 directories, 5 files
bin下面有各种cmake家族的产品程序。
(4)创建软链接。
注意,文件路径是可以指定的, 一般选择在/opt 或 /usr 路径下, 这里选择/opt 。
sudo mv cmake-3.21.3-Linux-x86_64 /opt/cmake-3.21.3
sudo ln -sf /opt/cmake-3.21.3/bin/* /usr/bin/
检查版本号:
$ cmake --version
cmake version 3.21.3
CMake suite maintained and supported by Kitware (kitware.com/cmake).
三、CMake的简单使用
(1)首先,建立一个文件夹,用来存放工程文件。
cd cmake
mkdir t1
cd t1
(2)在 t1 目录建立main.c 和 CMakeLists.txt(注意文件名大小写):
main.c 文件内容:
//main.c
#include
int main()
{
printf(“Hello World from t1 Main!\n”);
return 0;
}
CMakeLists.txt 文件内容:
PROJECT(HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
ADD_EXECUTABLE(hello2 ${SRC_LIST})
关于CMakeLists.txt 文件内容的语法,后面会做详细说明。
cmake .
注意命令后面的点号,代表本目录。
执行结果:
CMake Warning (dev) at CMakeLists.txt:4:
Syntax Warning in cmake code at column 37
Argument not separated from preceding token by whitespace.
This warning is for project developers. Use -Wno-dev to suppress it.
-- The C compiler identification is GNU 8.4.0
-- The CXX compiler identification is GNU 8.4.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /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: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- This is BINARY dir /home/fly/workspace/cmakeProj/t1
-- This is SOURCE dir /home/fly/workspace/cmakeProj/t1
-- Configuring done
-- Generating done
-- Build files have been written to: /home/fly/workspace/cmakeProj/t1
系统自动生成了: CMakeFiles, CMakeCache.txt, cmake_install.cmake 等文件,并且生成了 Makefile。
不需要理会这些文件的作用,最关键的是,它自动生成了 Makefile。然后进行工程的实际构建,在这个目录输入 make 命令,大概会得到如下的输出:
[ 25%] Building C object CMakeFiles/hello2.dir/main.c.o
[ 50%] Linking C executable hello2
[ 50%] Built target hello2
[ 75%] Building C object CMakeFiles/hello.dir/main.c.o
[100%] Linking C executable hello
[100%] Built target hello
如果需要看到make 构建的详细过程,可以使用make VERBOSE=1 或者VERBOSE=1 make 命令来进行构建。
这时候,需要的目标文件hello 已经构建完成,位于当前目录,尝试运行一下:
$ ./hello
Hello World from t1 main
CMakeLists.txt这个文件是 cmake 的构建定义文件,文件名是大小写相关的,如果工程存在多个目录,需要确保每个要管理的目录都存在一个CMakeLists.txt。这涉及到关于多目录构建,后面解释。
上面例子中的CMakeLists.txt 文件内容如下:
PROJECT(HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
ADD_EXECUTABLE(hello2 ${SRC_LIST})
(1)PROJECT指令。
语法是:
PROJECT(projectname [CXX] [C] [Java])
可以用这个指令定义工程名称,并可指定工程支持的语言,支持的语言列表是可以忽略的,默认情况表示支持所有语言。这个指令隐式的定义了两个 cmake 变量:
同时cmake 系统也帮助我们预定义了 PROJECT_BINARY_DIR 和PROJECT_SOURCE_DIR变量,他们的值分别跟HELLO_BINARY_DIR 与HELLO_SOURCE_DIR 一致。
为了统一起见,建议以后直接使用PROJECT_BINARY_DIR,PROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。如果使用了
(2)SET指令。
语法是:
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
SET 指令可以用来显式的定义变量即可。比如用到的是SET(SRC_LIST main.c),如果有多个源文件,也可以定义成:SET(SRC_LIST main.c t1.c t2.c)
。
(3)MESSAGE指令。
语法是:
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)
这个指令用于向终端输出用户定义的信息,包含了三种类型:
这里使用的是STATUS 信息输出,演示了由PROJECT 指令定义的两个隐式变量HELLO_BINARY_DIR 和HELLO_SOURCE_DIR。
(4)ADD_EXECUTABLE 指令。
ADD_EXECUTABLE(hello ${SRC_LIST})
定义了这个工程会生成一个文件名为hello 的可执行文件,相关的源文件是 SRC_LIST 中定义的源文件列表, 本例中也可以直接写成ADD_EXECUTABLE(hello main.c)。
在本例使用了$ {} 来引用变量,这是cmake 的变量应用方式,但是,有一些例外,比如在IF 控制语句,变量是直接使用变量名引用,而不需要$ {}。如果使用了$ {}去应用变量,其实IF 会去判断名为${}所代表的值的变量,那当然是不存在的了。
cmake 其实要使用”cmake 语言和语法”去构建,上面的内容就是所谓的 ”cmake 语言和语法”,最简单的语法规则是:
(1)变量使用${}方式取值,但是在IF 控制语句中是直接使用变量名 。
(2)指令(参数1 参数 2…),参数使用括弧括起,参数之间使用空格或分号分开。以上面的ADD_EXECUTABLE 指令为例,如果存在另外一个 func.c 源文件,就要写成:
ADD_EXECUTABLE(hello main.c func.c)
# 或者
ADD_EXECUTABLE(hello main.c;func.c)
(3)指令是大小写无关的,参数和变量是大小写相关的。但,推荐全部使用大写指令。上面的MESSAGE 指令我们已经用到了这条规则:
MESSAGE(STATUS "This is BINARY dir" ${HELLO_BINARY_DIR})
# 也可以写成:
MESSAGE(STATUS "This is BINARY dir ${HELLO_BINARY_DIR}")
这里需要特别解释的是作为工程名的HELLO 和生成的可执行文件 hello 是没有任何关系的。
(4)工程名和执行文件。hello 定义了可执行文件的文件名,也完全可以写成:ADD_EXECUTABLE(t1 main.c)编译后会生成一个t1 可执行文件。
(5)关于语法的疑惑:
cmake 的语法还是比较灵活而且考虑到各种情况,比如:
SET(SRC_LIST main.c)
# 也可以写成
SET(SRC_LIST "main.c")
是没有区别的,但是假设一个源文件的文件名是 fu nc.c(文件名中间包含了空格)。这时候就必须使用双引号,如果写成了 SET(SRC_LIST fu nc.c),就会出现错误,提示找不到fu 文件和nc.c 文件。这种情况,就必须写成:
SET(SRC_LIST "fu nc.c")
此外,可以忽略掉source 列表中的源文件后缀,比如可以写成:
ADD_EXECUTABLE(t1 main)
cmake 会自动的在本目录查找main.c 或者main.cpp等,当然,最好不要偷这个懒,以免这个目录确实存在一个 main.c和一个main。
同时参数也可以使用分号来进行分割。下面的例子也是合法的:
ADD_EXECUTABLE(t1 main.c t1.c)
# 可以写成
ADD_EXECUTABLE(t1 main.c;t1.c).
只需要在编写CMakeLists.txt 时注意形成统一的风格即可。
(6)清理工程。跟经典的autotools 系列工具一样,运行make clean即可对构建结果进行清理。
(7)cmake 并不支持 make distclean,关于这一点,官方是有明确解释的。因为CMakeLists.txt 可以执行脚本并通过脚本生成一些临时文件,但是却没有办法来跟踪这些临时文件到底是哪些。因此,没有办法提供一个可靠的 make distclean 方案。
(8)内部构建。刚才进行的是内部构建(in-source build),而 cmake 强烈推荐的是外部构建(out-of-source build)。内部构建生成的临时文件可能比您的代码文件还要多,而且它生成了一些无法自动删除的中间文件。
(9)外部构建。外部编译的过程如下:
这里需要特别注意的是:通过外部编译进行工程构建,HELLO_SOURCE_DIR 仍然指代工程路径,即/t1;而 HELLO_BINARY_DIR 则指代编译路径,即cmake/t1/build。
四、更像样的CMake工程以后所有的构建我们都将采用 out-of-source 外部构建,约定的构建目录是工程目录下的build 自录。
让前面的Hello World 更像一个工程,需要作的是:
(1)建立t2目录。
mkdir t2
(2)将 t1 工程的 main.c 和 CMakeLists.txt 拷贝到t2 目录中。
cp ./main.c ../t2
cp ./CMakeLists.txt ../t2
# 或者整个目录复制
# cp -r ../t1 ../t2
(3)添加子目录src:
cd t2
mkdir src
mv main.c src/
现在的工程看起来是这个样子:一个子目录src,一个 CMakeLists.txt。
(4)上面提到,需要为任何子目录建立一个 CMakeLists.txt,进入子目录src,编写 CMakeLists.txt 如下:
ADD_EXECUTABLE(hello main.c)
(5)将 t2 工程的 CMakeLists.txt 修改为:
PROJECT(HELLO)
ADD_SUBDIRECTORY(src bin)
建立build 目录,进入build 目录进行外部编译。
mkdir build
cd build
cmake ..
make
构建完成后,生成的目标文件 hello 位于 build/bin 目录中。
(1)ADD_SUBDIRECTORY 指令:
ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。EXCLUDE_FROM_ALL 参数的含义是将这个目录从编译过程中排除,比如,工程的 example,可能就需要工程构建完成后,再进入 example 目录单独进行构建(当然,也可以通过定义依赖来解决此类问题)。
上面的例子定义了将src 子目录加入工程,并指定编译输出(包含编译中间结果)路径为 bin 目录。如果不进行 bin 目录的指定,那么编译结果(包括中间结果)都将存放在 build/src 目录(这个目录跟原有的src 目录对应),指定 bin 目录后,相当于在编译时将src 重命名为bin,所有的中间结果和目标二进制都将存放在bin 目录。
这里需要提一下的是SUBDIRS 指令,使用方法是:
SUBDIRS(dir1 dir2...)
但是这个指令已经不推荐使用。它可以一次添加多个子目录,并且,即使外部编译,子目录体系仍然会被保存。
如果在上面的例子中将ADD_SUBDIRECTORY (src bin)修改为SUBDIRS(src),那么在build 目录中将出现一个src 目录,生成的目标代码 hello 将存放在src 目录中。
不论是SUBDIRS 还是 ADD_SUBDIRECTORY 指令(不论是否指定编译输出目录),我们都可以通过SET 指令重新定义EXECUTABLE_OUTPUT_PATH 和LIBRARY_OUTPUT_PATH 变量来指定最终的目标二进制的位置(指最终生成的hello 或者最终的共享库,不包含编译生成的中间文件)。
SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
前面提到了
所以,上面两个指令分别定义了:可执行二进制的输出路径为build/bin 和库的输出路径为build/lib。
这里没有提到共享库和静态库的构建,所以,暂时可以不考虑第二条指令。
问题是,应该把这两条指令写在工程的 CMakeLists.txt 还是 src 目录下的CMakeLists.txt,把握一个简单的原则:在哪里ADD_EXECUTABLE 或ADD_LIBRARY,如果需要改变目标存放路径,就在哪里加入上述的定义。
在这个例子里,当然就是指src 下的CMakeLists.txt 了。
安装的需要有两种,一种是从代码编译后直接 make install 安装,一种是打包时的指定目录安装。所以,即使最简单的手工编写的Makefile,看起来也是这个样子的:
DESTDIR=
install:
mkdir -p $(DESTDIR)/usr/bin install -m 755 hello $(DESTDIR)/usr/bin
这时就可以通过 make install 将 hello 直接安装到/usr/bin 目录,也可以通过make install ESTDIR=/tmp/test 将它安装在/tmp/test/usr/bin 目录,打包时这个方式经常被使用。
稍微复杂一点的是还需要定义PREFIX,一般autotools 工程,会运行这样的指令:
./configure –prefix=/usr
# 或者
./configure --prefix=/usr/local
#来指定 PREFIX
比如上面的Makefile 就可以改写成:
DESTDIR=
PREFIX=/usr
install:
mkdir -p $(DESTDIR)/$(PREFIX)/bin install -m 755 hello $(DESTDIR)/$(PREFIX)/bin
那么我们CMake的HelloWorld 应该怎么进行安装呢?
这里需要引入一个新的cmake 指令 INSTALL 和一个非常有用的变量:CMAKE_INSTALL_PREFIX。
CMAKE_INSTALL_PREFIX 变量类似于configure 脚本的 –prefix,常见的使用方法是这个样子:
cmake -DCMAKE_INSTALL_PREFIX=/usr .
INSTALL 指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。
INSTALL 指令包含了各种安装类型,需要一个个分开解释:
(1)目标文件的安装:
INSTALL(TARGETS targets...
[[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS
[Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL]
] [...])
参数中的TARGETS 后面跟的就是我们通过ADD_EXECUTABLE 或者ADD_LIBRARY 定义的目标文件&#xff0c;可能是可执行二进制、动态库、静态库。
目标类型也就相对应的有三种&#xff1a;ARCHIVE 特指静态库&#xff0c;LIBRARY 特指动态库&#xff0c;RUNTIME特指可执行目标二进制。
DESTINATION 定义了安装的路径&#xff0c;如果路径以/开头&#xff0c;那么指的是绝对路径&#xff0c;这时候 CMAKE_INSTALL_PREFIX 其实就无效了。如果希望使用CMAKE_INSTALL_PREFIX 来定义安装路径&#xff0c;就要写成相对路径&#xff0c;即不要以/开头&#xff0c;那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/
举个简单的例子&#xff1a;
INSTALL(TARGETS myrun mylib mystaticlib RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)
上面的例子会将&#xff1a;
&#xff08;2&#xff09;普通文件的安装&#xff1a;
INSTALL(FILES files... DESTINATION <dir> [PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component>]
[RENAME <name>] [OPTIONAL])
可用于安装一般文件&#xff0c;并可以指定访问权限&#xff0c;文件名是此指令所在路径下的相对路径。如果默认不定义权限PERMISSIONS&#xff0c;安装后的权限为&#xff1a;
OWNER_WRITE, OWNER_READ, GROUP_READ,和WORLD_READ&#xff0c;即 644 权限。
&#xff08;3&#xff09;非目标文件的可执行程序安装(比如脚本之类)&#xff1a;
INSTALL(PROGRAMS files... DESTINATION <dir>
[PERMISSIONS permissions...] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component>] [RENAME <name>] [OPTIONAL])
跟上面的FILES 指令使用方法一样&#xff0c;唯一的不同是安装后权限为:
OWNER_EXECUTE, GROUP_EXECUTE, 和WORLD_EXECUTE&#xff0c;即755 权限目录的安装。
&#xff08;4&#xff09;目录安装&#xff1a;
INSTALL(DIRECTORY dirs... DESTINATION <dir> [FILE_PERMISSIONS permissions...] [DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS] [CONFIGURATIONS [Debug|Release|...]] [COMPONENT <component>]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]] [...])
这里主要介绍其中的DIRECTORY、PATTERN 以及 PERMISSIONS 参数。
DIRECTORY 后面连接的是所在Source 目录的相对路径&#xff0c;但务必注意&#xff1a;abc 和 abc/有很大的区别。如果目录名不以/结尾&#xff0c;那么这个目录将被安装为目标路径下的abc&#xff0c;如果目录名以/结尾&#xff0c;代表将这个目录中的内容安装到目标路径&#xff0c;但不包括这个目录本身。
PATTERN 用于使用正则表达式进行过滤&#xff0c;PERMISSIONS 用于指定PATTERN 过滤后的文件权限。
看一个例子:
INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
GROUP_EXECUTE GROUP_READ)
这条指令的执行结果是&#xff1a;将 icons 目录安装到
&#xff08;5&#xff09;安装时CMAKE 脚本的执行&#xff1a;
INSTALL([[SCRIPT <file>] [CODE <code>]] [...])
# SCRIPT #参数用于在安装时调用cmake 脚本文件&#xff08;也就是
# CODE 参数用于执行CMAKE 指令&#xff0c;必须以双引号括起来。比如&#xff1a;
INSTALL(CODE "MESSAGE(\"Sample install message.\")")
安装还有几个被标记为过时的指令&#xff0c;比如 INSTALL_FILES 等&#xff0c;这些指令已经不再推荐使用。
下面&#xff0c;就来改写我们的工程文件&#xff0c;让他来支持各种文件的安装&#xff0c;并且&#xff0c;我们要使用 CMAKE_INSTALL_PREFIX 指令。
&#xff08;1&#xff09;首先先补上为添加的文件。添加doc 目录及文件:
cd t2
mkdir doc
vi doc/hello.txt
随便填写一些内容并保存。
&#xff08;2&#xff09;在工程目录添加runhello.sh 脚本&#xff0c;内容为&#xff1a;
hello
&#xff08;3&#xff09;添加工程目录中的COPYRIGHT 和 README&#xff1a;
touch COPYRIGHT
touch README
&#xff08;4&#xff09;下面改写各目录的CMakeLists.txt 文件。
1&#xff09;安装 COPYRIGHT和README&#xff0c;直接修改主工程文件CMakelists.txt&#xff0c;加入以下指令&#xff1a;
INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)
2&#xff09;安装 runhello.sh&#xff0c;直接修改主工程文件CMakeLists.txt&#xff0c;加入如下指令&#xff1a;
INSTALL(PROGRAMS runhello.sh DESTINATION bin)
3&#xff09;安装 doc 中的hello.txt&#xff0c;这里有两种方式&#xff1a;一是通过在doc 目录建立 CMakeLists.txt 并将 doc 目录通过ADD_SUBDIRECTORY 加入工程来完成。另一种方法是直接在工程目录通过INSTALL(DIRECTORY 来完成)&#xff0c;前者比较简单&#xff0c;可以根据兴趣自己完成&#xff0c;我们来尝试后者&#xff0c;顺便演示以下DIRECTORY 的安装。
因为hello.txt 要安装到//share/doc/cmake/t2&#xff0c;所以不能直接安装整个doc 目录&#xff0c;这里采用的方式是安装 doc 目录中的内容&#xff0c;也就是使用”doc/”。在工程文件中添加&#xff1a;
INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)
&#xff08;5&#xff09;现在进入build 目录进行外部编译&#xff0c;注意使用 CMAKE_INSTALL_PREFIX 参数&#xff0c;这里我们将它安装到了/tmp/t2 目录&#xff1a;
cmake -DCMAKE_INSTALL_PREFIX&#61;/tmp/t2/usr ..
&#xff08;6&#xff09;然后运行&#xff1a;
make
sudo make install
&#xff08;7&#xff09;进入/tmp/t2 目录看一下安装结果&#xff1a;
$ tree -L 5 /tmp/t2/usr/
/tmp/t2/usr/
├── bin
│ └── runhello.sh
└── share
└── doc
└── cmakeProj
└── t2
├── COPYRIGHT
├── hello.txt
└── README
5 directories, 4 files
&#xff08;8&#xff09;如果要直接安装到系统&#xff0c;可以使用如下指令&#xff1a;
cmake -DCMAKE_INSTALL_PREFIX&#61;/usr ..
注意&#xff0c;CMAKE_INSTALL_PREFIX 的默认定义是/usr/local&#xff0c;如果没有定义CMAKE_INSTALL_PREFIX 会安装到这个地方。
总结