参考

工程文件

Cmake Practice.PPT

三、初试 cmake – cmake 的 helloworld

  1. // main.c
  2. #include <stdio.h>
  3. int main()
  4. {
  5. printf("Hello World from t1 Main!\n");
  6. return 0;
  7. }
  1. // CMakeLists.txt
  2. cmake_minimum_required (VERSION 3.8)
  3. PROJECT(T1)
  4. SET(SRC_LIST "main.c")
  5. MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
  6. MESSAGE(STATUS "This is SOURCE dir " ${HELLO_SOURCE_DIR})
  7. ADD_EXECUTABLE(hello ${SRC_LIST})
  1. Cmake Practice/t2/01> ls -n
  2. CMakeLists.txt
  3. main.c
  • cmake . ```shell Cmake Practice/t2/01$ cmake . CMake Deprecation Warning at CMakeLists.txt:1 (cmake_minimum_required): Compatibility with CMake < 2.8.12 will be removed from a future version of CMake.

    Update the VERSION argument value or use a … suffix to tell CMake that the project does not need compatibility with older versions.

— This is BINARY dir /cygdrive/c/Users/XXXXXX-Home/Desktop/cmake/01 — This is SOURCE dir /cygdrive/c/Users/XXXXXX-Home/Desktop/cmake/01 — Configuring done — Generating done — Build files have been written to: /cygdrive/c/Users/XXXXXX-Home/Desktop/cmake/01

  1. ```shell
  2. Cmake Practice/t2/01$ ls -n
  3. CMakeFiles
  4. CMakeCache.txt
  5. CMakeLists.txt
  6. cmake_install.cmake
  7. main.c
  8. Makefile
  • make

    1. Cmake Practice/t2/01$ make
    2. [ 50%] Building C object CMakeFiles/hello.dir/main.c.o
    3. [100%] Linking C executable hello.exe
    4. [100%] Built target hello
    1. Cmake Practice/t2/01$ ls -n
    2. CMakeFiles
    3. CMakeCache.txt
    4. CMakeLists.txt
    5. cmake_install.cmake
    6. hello.exe
    7. main.c
    8. Makefile
    1. Cmake Practice/t2/01$ .\hello.exe
    2. Hello World from t1 Main!

    PROJECT

    PROJECT(projectname [CXX] [C] [Java])

  • 可以用这个指令定义工程名称,并可指定工程支持的语言,支持的语言列表是可以忽略的,默认情况表示支持所有语言

这个指令隐式的定义了两个 cmake 变量

  • <projectname>_BINARY_DIR, 即T1_BINARY_DIR
  • <projectname>_SOURCE_DIR, 即T1_SOURCE_DIR

采用的是内部编译,两个变量目前指的都是工程所在路径Cmake Practice/t2/01,后面会讲到外部编译,两者所指代的内容会有所不同
cmake 系统预定义了 PROJECT_BINARY_DIRPROJECT_SOURCE_DIR 变量,他们的值分别跟 T1_BINARY_DIRT1_SOURCE_DIR 一致

为了统一起见,建议以后直接使用PROJECT_BINARY_DIRPROJECT_SOURCE_DIR,即使修改了工程名称,也不会影响这两个变量。如果使用了<projectname_SOURCE_DIR>,修改工程名称后,需要同时修改这些变量。

SET

SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])

  • 目前SET的作用是用来显式的定义变量

比如我们用到的是SET(SRC_LIST main.c),如果有多个源文件,也可以定义成:SET(SRC_LIST main.c t1.c t2.c)

MESSAGE

MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)
用于向终端输出用户定义的信息

  • SEND_ERROR,产生错误,生成过程被跳过
  • STATUS,输出前缀为—的信息
  • FATAL_ERROR,立即终止所有cmake过程

栗子中, 使用的是 STATUS 信息输出,演示了由 PROJECT 指令定义的两个隐式变量 HELLO_BINARY_DIRHELLO_SOURCE_DIR

ADD_EXECUTABLE

ADD_EXECUTABLE(hello ${SRC_LIST})

  • 定义了这个工程会生成一个文件名为 hello 的可执行文件,相关的源文件是 SRC_LIST 中定义的源文件列表
  • 本例中也可以直接写成ADD_EXECUTABLE(hello main.c)

基本语法规则

  1. 变量使用${}方式取值,但是在 IF 控制语句中是直接使用变量名。

  2. 指令(参数1 参数2...)

参数使用括弧括起,参数之间使用空格或分号分开。
以上面的 ADD_EXECUTABLE指令为例,如果存在另外一个 func.c 源文件,就要写成:

  • ADD_EXECUTABLE(hello main.c func.c)
  • 或者ADD_EXECUTABLE(hello main.c; func.c)
  1. 指令是大小写无关的,参数和变量是大小写相关的。但,推荐你全部使用大写指令。

上面的 MESSAGE 指令已经用到了这条规则:

  • MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR})
  • 也可以写成:MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)

作为工程名 HELLO 和生成的可执行文件 hello 是没有任何关系的。hello 定义的可执行文件的文件名,你完全可以写成:ADD_EXTCUTABLE(t1 main.c),编译后会生成一个 t1 可执行文件。

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)

    四、更好一点的 Hello World

    本节目标
  1. 为工程添加一个子目录 src,用来放置工程源代码
  2. 添加一个子目录 doc,用来放置这个工程的文档 hello.txt
  3. 在工程目录添加文本文件 COPYRIGHT,README
  4. 在工程目录添加一个 runhello.sh 脚本,用来调用 hello 二进制
  5. 将构建后的目标文件放入构建目录的 bin 子目录
  6. 最终安装这些文件:将 hello 二进制与 runhello.sh 安装至//bin,将doc 目录的内容以及 COPYRIGHT/README 安装到//share/doc/cmake/t2

外部构建

所谓的 out-of-source 外部编译,一个最大的好处是,对于原有的工程没有任何影响,所有动作全部发生在编译目录。通过这一点,也足以说服我们全部采用外部编译方式构建工程

通过外部编译进行工程构建,HELLO_SOURCE_DIR仍然指代工程目录,即Cmake Practice/t2/01,而HELLO_BINARY_DIR则指代编译路径,即Cmake Practice/t2/01/build

  • tree
    1. Cmake Practice/t2/01$ tree
    2. .
    3. ├── CMakeLists.txt
    4. ├── COPYRIGHT
    5. ├── README
    6. ├── build
    7. ├── doc
    8. └── hello.txt
    9. ├── runhello.sh
    10. └── src
    11. ├── CMakeLists.txt
    12. └── main.c
    ```c

    include

int main() { printf(“Hello World from t2 Main!\n”);

  1. return 0;

}

  1. ```
  2. // /01/src/CMakeLists.txt
  3. ADD_EXECUTABLE(hello main.c)
  1. // CMakeLists.txt
  2. PROJECT(T2)
  3. ADD_SUBDIRECTORY(src bin)

cmake

  1. Cmake Practice/t2/01/build$ cmake ..
  2. -- The C compiler identification is GNU 11.2.0
  3. -- Detecting C compiler ABI info
  4. -- Detecting C compiler ABI info - done
  5. -- Check for working C compiler: /usr/bin/cc - skipped
  6. -- Detecting C compile features
  7. -- Detecting C compile features - done
  8. -- Configuring done
  9. -- Generating done
  10. -- Build files have been written to: /home/ningliuwsl/CPP/Cmake Practice/t2/01/build
  1. Cmake Practice/t2/01/build$ ls
  2. CMakeCache.txt CMakeFiles Makefile bin cmake_install.cmake
  • make

    1. Cmake Practice/t2/01/build$ make
    2. [ 50%] Building C object bin/CMakeFiles/hello.dir/main.o
    3. [100%] Linking C executable hello
    4. [100%] Built target hello
  • bin

    1. Cmake Practice/t2/01/build/bin$ ./hello
    2. Hello World from t2 Main!

    ADD_SUBDIRECTORY

ADD_SUBDIRECTORY(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

  • 这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置

EXCLUDE_FROM_ALL

  • 将这个目录从编译过程中排除
  • 比如,工程的 example,可能就需要工程构建完成后,再进入 example 目录单独进行构建(当然,你也可以通过定义依赖来解决此类问题)

ADD_SUBDIRECTORY(src bin)

  • src 子目录加入工程,并指定编译输出(包含编译中间结果)路径为 bin 目录
  • 如果不进行 bin 目录的指定,那么编译结果(包括中间结果)都将存放在 build/src 目录(这个目录跟原有的 src 目录对应),指定 bin 目录后,相当于在编译时将 src 重命名为 bin,所有的中间结果和目标二进制都将存放在 bin 目录

SUBDIRS 指令

  • SUBDIRS(dir1 dir2...),但是这个指令已经不推荐使用。它可以一次添加多个子目录,并且,即使外部编译,子目录体系仍然会被保存。
  • 如果我们在上面的例子中将 ADD_SUBDIRECTORY (src bin) 修改为 SUBDIRS(src),那么在 build 目录中将出现一个 src 目录,生成的目标代码 hello 将存放在 src 目录中。

换个地方保存目标二进制

  1. // src/CMakeLists.txt
  2. ADD_EXECUTABLE(hello main.c)
  3. SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
  4. SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)

可执行二进制的输出路径为 build/bin和库的输出路径为 build/lib

通过SET指令重新定义EXECUTABLE_OUTPUT_PATHLIBRARY_OUTPUT_PATH变量来指定最终的目标二进制的位置(指最终生成的 hello 或者最终的共享库,不包含编译生成的中间文件)

<projectname>_BINARY_DIRPROJECT_BINARY_DIR变量,它们指的是编译发生的当前目录,如果是内部编译,就相当于PROJECT_SOURCE_DIR也就是工程代码所在目录,如果是外部编译,指的是外部编译所在目录,也就是本例中的 build 目录。

应该把这两条指令写在工程的 CMakeLists.txt 还是 src 目录下的CMakeLists.txt,把握一个简单的原则,在哪里ADD_EXECUTABLEADD_LIBRARY,如果需要改变目标存放路径,就在哪里加入上述的定义

如何安装/INSTALL

安装的需要有两种,一种是从代码编译后直接 make install 安装,一种是打包时的指定目录安装。

最简单的手工编写的 Makefile

  1. DESTDIR=
  2. install:
  3. mkdir -p $(DESTDIR)/usr/bin
  4. install -m 755 hello $(DESTDIR)/usr/bin

可以通过make install 将 hello 直接安装到 /usr/bin 目录,也可以通过 make install DESTDIR=/tmp/test 将它安装在 /tmp/test/usr/bin 目录,打包时这个方式经常被使用。

稍微复杂一点的是还需要定义 PREFI, 一般 autotools 工程,会运行这样的指令:
./configure --prefix=/usr 或者 ./configure --prefix=/usr/local 来指定 PREFIX
上面的 Makefile 就可以改写成

  1. DESTDIR=
  2. PREFIX=/usr
  3. install:
  4. mkdir -p $(DESTDIR)/$(PREFIX)/bin
  5. install -m 755 hello $(DESTDIR)/$(PREFIX)/bin

如何使用cmake安装helloworld?

  • INSTALL
    • 用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等
  • CMAKE_INSTALL_PREFIX
    • 类似于 configure 脚本的 --prefix,常见的使用方法看起来是这个样子:cmake -DCMAKE_INSTALL_PREFIX=/usr

如果没有定义 CMAKE_INSTALL_PREFIX 会安装到什么地方?

  • CMAKE_INSTALL_PREFIX 的默认定义是 /usr/local

目标文件的安装

  1. INSTALL(TARGETS targets...
  2. [
  3. [ARCHIVE|LIBRARY|RUNTIME]
  4. [DESTINATION <dir>]
  5. [PERMISSIONS permissions...]
  6. [
  7. CONFIGURATIONS [Debug|Release|...]
  8. ]
  9. [COMPONENT <component>]
  10. [OPTIONAL]
  11. ] [...])

TARGETS

  • 后面跟的就是我们通过 ADD_EXECUTABLE 或者 ADD_LIBRARY 定义的目标文件,可能是可执行二进制、动态库、静态库

目标类型相对应的有三种

  • ARCHIVE 特指静态库
  • LIBRARY 特指动态库
  • RUNTIME 特指可执行目标二进制

DESTINATION

  • 定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候 CMAKE_INSTALL_PREFIX其实就无效了。如果你希望使用 CMAKE_INSTALL_PREFIX 来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/<DESTINATION 定义的路径>

举个简单的例子

  1. INSTALL(TARGETS myrun mylib mystaticlib
  2. RUNTIME DESTINATION bin
  3. LIBRARY DESTINATION lib
  4. ARCHIVE DESTINATION libstatic
  5. )
  • 可执行二进制 myrun 安装到 ${CMAKE_INSTALL_PREFIX}/bin 目录
  • 动态库 mylib 安装到 ${CMAKE_INSTALL_PREFIX}/lib 目录
  • 静态库 mystaticlib 安装到 ${CMAKE_INSTALL_PREFIX}/libstatic 目录

特别注意的是不需要关心 TARGETS 具体生成的路径,只需要写上 TARGETS 名称就可以了

普通文件的安装

  1. INSTALL(FILES files... DESTINATION <dir>
  2. [PERMISSIONS permissions...]
  3. [CONFIGURATIONS [Debug|Release|...]]
  4. [COMPONENT <component>]
  5. [RENAME <name>] [OPTIONAL]
  6. )

可用于安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。

如果默认不定义权限 PERMISSIONS,安装后的权限为:

  • OWNER_WRITE, OWNER_READ, GROUP_READ,和 WORLD_READ,即 644 权限。

非目标文件的可执行程序安装(比如脚本之类)

  1. INSTALL(PROGRAMS files... DESTINATION <dir>
  2. [PERMISSIONS permissions...]
  3. [CONFIGURATIONS [Debug|Release|...]]
  4. [COMPONENT <component>]
  5. [RENAME <name>] [OPTIONAL]
  6. )

跟 FILES 指令使用方法一样,唯一的不同是安装后权限为:

  • OWNER_EXECUTE, GROUP_EXECUTE, 和 WORLD_EXECUTE,即 755 权限。

目录的安装

  1. INSTALL(DIRECTORY dirs... DESTINATION <dir>
  2. [FILE_PERMISSIONS permissions...]
  3. [DIRECTORY_PERMISSIONS permissions...]
  4. [USE_SOURCE_PERMISSIONS]
  5. [CONFIGURATIONS [Debug|Release|...]]
  6. [COMPONENT <component>]
  7. [[PATTERN <pattern> | REGEX <regex>]
  8. [EXCLUDE] [PERMISSIONS permissions...]] [...]
  9. )
  • DIRECTORY 后面连接的是所在 Source 目录的相对路径,但务必注意:abcabc/有很大的区别。
    • 如果目录名不以/结尾,那么这个目录将被安装为目标路径下的 abc
    • 如果目录名以/ 结尾,代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。
  • PATTERN用于使用正则表达式进行过滤
  • PERMISSIONS 用于指定 PATTERN 过滤后的文件权限

来看一个例子

  1. INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj
  2. PATTERN "CVS" EXCLUDE
  3. PATTERN "scripts/*"
  4. PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
  5. GROUP_EXECUTE GROUP_READ
  6. )
  • icons 目录安装到 /share/myproj,将 scripts/中的内容安装到 /share/myproj
  • 不包含目录名为 CVS的目录
  • 对于 scripts/*文件指定权限为 OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ

安装时 CMAKE 脚本的执行

INSTALL([[SCRIPT ] [CODE ]] [...])

  • SCRIPT 参数用于在安装时调用 cmake 脚本文件(也就是 .cmake 文件)
  • CODE 参数用于执行 CMAKE 指令,必须以双引号括起来。比如:INSTALL(CODE "MESSAGE(\"Sample install message.\")")

安装还有几个被标记为过时的指令,比如 INSTALL_FILES 等,这些指令已经不再推荐使用,所以,这里就不再赘述了。

修改 Helloworld 支持安装

补上未添加的文件

  • 添加 doc目录及文件

    1. cd /backup/cmake/t2
    2. mkdir doc
    3. vi doc/hello.txt随便填写一些内容并保存。
  • 在工程目录添加 runhello.sh 脚本

    1. ./hello
  • 添加工程目录中的 COPYRIGHTREADME

    1. touch COPYRIGHT
    2. touch README

改写各目录的 CMakeLists.txt

安装 COPYRIGHT, README,直接修改主工程文件 CMakelists.txt,加入以下指令

  1. // 安装 COPYRIGHT, README
  2. INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)

安装 runhello.sh,直接修改主工程文件 CMakeLists.txt,加入如下指令

  1. // 安装 runhello.sh
  2. INSTALL(PROGRAMS runhello.sh DESTINATION bin)

安装 doc 中的 hello.txt

  • 通过在 doc 目录建立 CMakeLists.txt 并将 doc 目录通过 ADD_SUBDIRECTORY 加入工程来完成
  • 另一种方法是直接在工程目录通过 INSTALL(DIRECTORY)来完成

因为 hello.txt 要安装到 /prefix/share/doc/cmake/t2,所以我们不能直接安装整个 doc 目录,这里采用的方式是安装 doc 目录中的内容,也就是使用 doc/

  1. // 安装 doc 中的 hello.txt
  2. INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)

安装 hello

  • src目录的 CMakeLists.txt文件中添加如下内容,以安装 hello/<prefix>/bin
    1. // 安装 hello
    2. INSTALL(TARGETS hello RUNTIME DESTINATION bin)

改写完毕

  1. PROJECT(HELLO C)
  2. ADD_SUBDIRECTORY(src bin)
  3. ## 安装 COPYRIGHT, README
  4. INSTALL(FILES COPYRIGHT README DESTINATION share/doc/cmake/t2)
  5. ## 安装 runhello.sh
  6. INSTALL(PROGRAMS runhello.sh DESTINATION bin)
  7. ## 安装 doc 中的 hello.txt
  8. INSTALL(DIRECTORY doc/ DESTINATION share/doc/cmake/t2)
  1. ## src/CMakeLists.txt
  2. ADD_EXECUTABLE(hello main.c)
  3. SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
  4. SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
  5. ## 安装 hello
  6. INSTALL(TARGETS hello RUNTIME DESTINATION bin)

尝试我们修改的结果

现在进入 build 目录进行外部编译,注意使用 CMAKE_INSTALL_PREFIX 参数,这里我们将它安装到了/home/XXX/CPP/Cmake Practice/t2目录

  1. cmake -DCMAKE_INSTALL_PREFIX="/home/XXX/CPP/Cmake Practice/t2" ..

或者修改CMakeLists.txt

Windows 系统:

  • set (CMAKE_INSTALL_PREFIX "C:\Users\XXX\Desktop\Cmake Practice\t3")有问题
  • set (CMAKE_INSTALL_PREFIX "/cygdrive/c/Users/XXX/Desktop/Cmake Practice/t3")
  1. set (CMAKE_INSTALL_PREFIX "/home/ningliuwsl/CPP/Cmake Practice/t2")

make

  1. Cmake Practice/t2/02/build$ make
  2. [ 50%] Building C object bin/CMakeFiles/hello.dir/main.o
  3. [100%] Linking C executable hello
  4. [100%] Built target hello

make install

  1. Cmake Practice/t2/02/build$ make install
  2. Consolidate compiler generated dependencies of target hello
  3. [100%] Built target hello
  4. Install the project...
  5. -- Install configuration: ""
  6. -- Installing: /home/ningliuwsl/CPP/Cmake Practice/t2/share/doc/cmake/t2/COPYRIGHT
  7. -- Installing: /home/ningliuwsl/CPP/Cmake Practice/t2/share/doc/cmake/t2/README
  8. -- Installing: /home/ningliuwsl/CPP/Cmake Practice/t2/bin/runhello.sh
  9. -- Up-to-date: /home/ningliuwsl/CPP/Cmake Practice/t2/share/doc/cmake/t2
  10. -- Installing: /home/ningliuwsl/CPP/Cmake Practice/t2/share/doc/cmake/t2/hello.txt
  11. -- Installing: /home/ningliuwsl/CPP/Cmake Practice/t2/bin/hello

安装结果

  1. Cmake Practice/t2/$ tree
  2. .
  3. ├── bin
  4. ├── hello
  5. └── runhello.sh
  6. └── share
  7. └── doc
  8. └── cmake
  9. └── t2
  10. ├── COPYRIGHT
  11. ├── README
  12. └── hello.txt

五、静态库和动态库的构建

本节的任务:

  • 建立一个静态库和动态库,提供HelloFunc函数供其他程序编程使用,HelloFunc向终端输出Hello World字符串。
  • 安装头文件与共享库。

建立共享库

创建lib目录

  1. Cmake Practice/t3$ mkdir lib

建立 CMakeLists.txt

  1. PROJECT(HELLOLIB)
  2. ADD_SUBDIRECTORY(lib)

lib 目录下建立两个源文件 hello.chello.h

  1. // hello.c
  2. #include "hello.h"
  3. void HelloFunc()
  4. {
  5. printf("Hello World\n");
  6. }
  1. // hello.h
  2. #ifndef HELLO_H
  3. #define HELLO_H
  4. #include <stdio.h>
  5. void HelloFunc();
  6. #endif
  1. # lib/CMakeLists.txt
  2. SET(LIBHELLO_SRC hello.c)
  3. ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC})

编译共享库

  1. Cmake Practice/t3/build$ make
  2. [ 50%] Building C object lib/CMakeFiles/hello.dir/hello.o
  3. [100%] Linking C shared library libhello.so
  4. [100%] Built target hello
  1. Cmake Practice/t3/build/lib$ ls
  2. CMakeFiles Makefile cmake_install.cmake libhello.so

指定 libhello.so 生成的位置,可以通过在主工程文件 CMakeLists.txt 中修改 ADD_SUBDIRECTORY(lib) 指令来指定一个编译输出位置或者在 lib/CMakeLists.txt中添加 SET(LIBRARY_OUTPUT_PATH <路径>) 来指定一个新的位置。

ADD_LIBRARY

  1. ADD_LIBRARY(libname [SHARED|STATIC|MODULE]
  2. [EXCLUDE_FROM_ALL]
  3. source1 source2 ... sourceN
  4. )

不需要写全 libhello.so,只需要填写 hello 即可,cmake 系统会自动为你生成 libhello.X

类型有三种:

  • SHARED,动态库
  • STATIC,静态库
  • MODULE,在使用 dyld 的系统有效,如果不支持 dyld,则被当作 SHARED 对待

EXCLUDE_FROM_ALL

  • 参数的意思是这个库不会被默认构建,除非有其他的组件依赖或者手工构建

添加静态库

按照一般的习惯,静态库名字跟动态库名字应该是一致的,只不过后缀是.a 罢了

添加静态库

  1. ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})

然后再在 build 目录进行外部编译,我们会发现,静态库根本没有被构建,仍然只生成了一个动态库。因为 hello 作为一个 target 是不能重名的,所以,静态库构建指令无效。

  1. ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})
  1. Cmake Practice/t3/build/lib$ ls
  2. CMakeFiles Makefile cmake_install.cmake libhello.so libhello_static.a

这种结果显示不是我们想要的,我们需要的是名字相同的静态库和动态库,因为 target 名称是唯一的,所以,我们肯定不能通过 ADD_LIBRARY 指令来实现了。这时候我们需要用到另外一个指令:SET_TARGET_PROPERTIES

SET_TARGET_PROPERTIES

  1. SET_TARGET_PROPERTIES(target1 target2 ...
  2. PROPERTIES prop1 value1
  3. prop2 value2 ...
  4. )

这条指令可以用来设置输出的名称,对于动态库,还可以用来指定动态库版本和 API 版本

在本例中,我们需要作的是向lib/CMakeLists.txt 中添加一条:

  1. SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")

这样,我们就可以同时得到 libhello.so/libhello.a 两个库了。

GET_TARGET_PROPERTY

SET_TARGET_PROPERTIES相对应

  1. GET_TARGET_PROPERTY(VAR target property)

lib/CMakeListst.txt 中添加

  1. GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)
  2. MESSAGE(STATUS This is the hello_static OUTPUT_NAME:” ${OUTPUT_VALUE})

如果没有这个属性定义,则返回 NOTFOUND

实际构建中没有发现构建 libhello.a 时,就会清理掉libhello.so

最终的构建结果,发现,libhello.a 已经构建完成,位于 build/lib 目录中,但是 libhello.so 却消失了。这个问题的原因是:cmake 在构建一个新的 target时,会尝试清理掉其他使用这个名字的库,因此,在构建 libhello.a 时,就会清理掉 libhello.so

为了回避这个问题,比如再次使用 SET_TARGET_PROPERTIES 定义 CLEAN_DIRECT_OUTPUT 属性。

lib/CMakeLists.txt 中添加:

  1. SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
  2. SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)

这时候,我们再次进行构建,会发现 build/lib 目录中同时生成了 libhello.solibhello.a

动态库版本号

按照规则,动态库是应该包含一个版本号的,可以看一下系统的动态库,一般情况是

  1. libhello.so.1.2
  2. libhello.so ->libhello.so.1
  3. libhello.so.1->libhello.so.1.2

为了实现动态库版本号需要使用 SET_TARGET_PROPERTIES 指令。

SET_TARGET_PROPERTIES

  1. SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)
  • VERSION 指代动态库版本
  • SOVERSION 指代 API 版本

将上述指令加入 lib/CMakeLists.txt 中,重新构建看看结果

  1. Cmake Practice/t3/build/lib$ ls
  2. CMakeFiles Makefile cmake_install.cmake libhello.a libhello.so libhello.so.1 libhello.so.1.2

安装共享库和头文件

需要将 libhello.a, libhello.so.x 以及 hello.h 安装到系统目录,才能真正让其他人开发使用, 在本例中我们将 hello 的共享库安装到 /lib 目录,将 hello.h 安装到 /include/hello 目录

lib/CMakeLists.txt 中添加如下指令

  1. INSTALL(TARGETS hello hello_static
  2. LIBRARY DESTINATION lib
  3. ARCHIVE DESTINATION lib
  4. )
  5. INSTALL(FILES hello.h DESTINATION include/hello)

注意,静态库要使用 ARCHIVE关键字

六、如何使用外部共享库和头文件

建立src目录,编写源文件 main.c

  1. // src/main.c
  2. #include <hello.h>
  3. int main(void)
  4. {
  5. HelloFunc();
  6. return 0;
  7. }

工程CMakeLists.txt

  1. PROJECT(HELLO)
  2. ADD_SUBDIRECTORY(src)

src/CMakeLists.txt

  1. ADD_EXECUTABLE(main main.c)

引入头文件搜索路径

  1. Cmake Practice\t4\build> make
  2. [ 50%] Building C object src/CMakeFiles/main.dir/main.c.o
  3. /cygdrive/c/Users/ning.liu/Desktop/Cmake Practice/t4/src/main.c:1:10: 致命错误:hello.hNo such file or directory
  4. 1 | #include <hello.h>
  5. | ^~~~~~~~~
  6. 编译中断。
  7. make[2]: *** [src/CMakeFiles/main.dir/build.make:76src/CMakeFiles/main.dir/main.c.o] 错误 1
  8. make[1]: *** [CMakeFiles/Makefile2:98src/CMakeFiles/main.dir/all] 错误 2
  9. make: *** [Makefile:91all] 错误 2

为了让我们的工程能够找到 hello.h 头文件,我们需要引入一个新的指令INCLUDE_DIRECTORIES

INCLUDE_DIRECTORIES

  1. INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)

这条指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面

可以通过两种方式来进行控制搜索路径添加的方式:

  • CMAKE_INCLUDE_DIRECTORIES_BEFORE,通过 SET 这个 cmake变量为 on,可以将添加的头文件搜索路径放在已有路径的前面。
  • 通过 AFTER 或者 BEFORE 参数,也可以控制是追加还是置前。

src/CMakeLists.txt中添加一个头文件搜索路径

  1. INCLUDE_DIRECTORIES("/home/XXX/CPP/Cmake Practice/t3/include/hello")

进入 build 目录,重新进行构建

  1. Cmake Practice\t4\build> make
  2. [ 50%] Building C object src/CMakeFiles/main.dir/main.c.o
  3. [100%] Linking C executable main.exe
  4. /usr/lib/gcc/x86_64-pc-cygwin/11/../../../../x86_64-pc-cygwin/bin/ld: CMakeFiles/main.dir/main.c.o:main.c:(.text+0xe): undefined reference to `HelloFunc'
  5. collect2: 错误:ld 返回 1
  6. make[2]: *** [src/CMakeFiles/main.dir/build.make:97:src/main.exe] 错误 1
  7. make[1]: *** [CMakeFiles/Makefile2:98:src/CMakeFiles/main.dir/all] 错误 2
  8. make: *** [Makefile:91:all] 错误 2

为target添加共享库

需要完成的任务是将目标文件链接到 libhello
需要引入两个新的指令:LINK_DIRECTORIESTARGET_LINK_LIBRARIES

LINK_DIRECTORIES

  1. LINK_DIRECTORIES(directory1 directory2 ...)

这个指令非常简单,添加非标准的共享库搜索路径,比如,在工程内部同时存在共享库和可执行二进制,在编译时就需要指定一下这些共享库的路径。这个例子中我们没有用到这个指令。

TARGET_LINK_LIBRARIES

  1. TARGET_LINK_LIBRARIES(target library1
  2. <debug | optimized> library2
  3. ...)

这个指令可以用来为target 添加需要链接的共享库,本例中是一个可执行文件,但是同样可以用于为自己编写的共享库添加共享库链接。

为了解决前面遇到的 HelloFunc 未定义错误,需要做的是向 src/CMakeLists.txt 中添加如下指令:

  • TARGET_LINK_LIBRARIES(main hello)
  • TARGET_LINK_LIBRARIES(main "/home/XXX/CPP/Cmake Practice/t3/lib/libhello.so")

运行 main

  1. Cmake Practice/t4/build/src$ ./main
  2. Hello World from t3 Main!

检查一下 main 的链接情况

  1. Cmake Practice/t4/build/src$ ldd main
  2. linux-vdso.so.1 (0x00007ffdda5de000)
  3. libhello.so.1 => /home/ningliuwsl/CPP/Cmake Practice/t3/lib/libhello.so.1 (0x00007fa20e9a4000)
  4. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa20e7aa000)
  5. /home/linuxbrew/.linuxbrew/lib/ld.so => /lib64/ld-linux-x86-64.so.2 (0x00007fa20e9ab000)

链接到静态库

  1. Cmake Practice/t4/build$ ldd src/main
  2. linux-vdso.so.1 (0x00007ffe1a9fd000)
  3. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f31a7afa000)
  4. /home/linuxbrew/.linuxbrew/lib/ld.so => /lib64/ld-linux-x86-64.so.2 (0x00007f31a7cf6000)

特殊的环境变量CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH

务必注意,这两个是环境变量而不是 cmake 变量。
使用方法是要在 bash 中用 export 或者在 csh 中使用 set 命令设置或者 CMAKE_INCLUDE_PATH=/home/include cmake ..等方式。

这两个变量主要是用来解决以前 autotools 工程中 --extra-include-dir 等参数的支持的。
也就是,如果头文件没有存放在常规路径(/usr/include,/usr/local/include 等),则可以通过这些变量就行弥补。

我们以本例中的 hello.h 为例,它存放在 /usr/include/hello 目录,所以直接查找肯定是找不到的。
前面我们直接使用了绝对路径 INCLUDE_DIRECTORIES(/usr/include/hello) 告诉工程这个头文件目录。

为了将程序更智能一点,我们可以使用 CMAKE_INCLUDE_PATH来进行,使用 bash 的方法如下:

export CMAKE_INCLUDE_PATH=/usr/include/hello

然后在头文件中将 INCLUDE_DIRECTORIES(/usr/include/hello) 替换为:

FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORY(${myHeader})
ENDIF(myHeader)

FIND_PATH

FIND_PATH 用来在指定路径中搜索文件名

FIND_PATH(myHeader NAMES hello.h PATHS /usr/include /usr/include/hello)

这里我们没有指定路径,但是,cmake 仍然可以帮我们找到 hello.h 存放的路径,就是因为我们设置了环境变量 CMAKE_INCLUDE_PATH

如果你不使用 FIND_PATHCMAKE_INCLUDE_PATH 变量的设置是没有作用的,你不能指望它会直接为编译器命令添加参数 -I<CMAKE_INCLUDE_PATH>

以此为例,CMAKE_LIBRARY_PATH 可以用在 FIND_LIBRARY中。

同样,因为这些变量直接为 FIND 指令所使用,所以所有使用 FIND_ 指令的 cmake 模块都会受益。

七、cmake 常用变量和常用环境变量

cmake 变量引用的方式

使用${}进行变量的引用。在 IF 等语句中,是直接使用变量名而不通过${}取值。

cmake 自定义变量的方式

主要有隐式定义和显式定义两种

  • 前面举了一个隐式定义的例子,就是 PROJECT 指令,他会隐式的定义<projectname>_BINARY_DIR<projectname>_SOURCE_DIR两个变量。
  • 显式定义的例子我们前面也提到了,使用 SET 指令,就可以构建一个自定义变量了。
    • SET(HELLO_SRC main.SOURCE_PATHc)PROJECT_BINARY_DIR 可以通过 ${HELLO_SRC} 来引用这个自定义变量了。

cmake常用变量

CMAKE_BINARY_DIR, PROJECT_BINARY_DIR, <projectname>_BINARY_DIR

这三个变量指代的内容是一致的,如果是 in source 编译,指得就是工程顶层目录,如果是 out-of-source 编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR 跟其他指令稍有区别,现在,你可以理解为他们是一致的。

CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR, <projectname>_SOURCE_DIR

这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。也就是在 in source 编译时,他跟 CMAKE_BINARY_DIR 等变量一致。PROJECT_SOURCE_DIR 跟其他指令稍有区别,现在,你可以理解为他们是一致的。

CMAKE_CURRENT_SOURCE_DIR

指的是当前处理的 CMakeLists.txt 所在的路径,比如上面我们提到的 src 子目录。

CMAKE_CURRRENT_BINARY_DIR

如果是 in-source 编译,它跟 CMAKE_CURRENT_SOURCE_DIR 一致,如果是 out-ofsource 编译,他指的是 target 编译目录。使用我们上面提到的 ADD_SUBDIRECTORY(src bin)可以更改这个变量的值。使用 SET(EXECUTABLE_OUTPUT_PATH <新路径>)并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。

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)

EXECUTABLE_OUTPUT_PATH , LIBRARY_OUTPUT_PATH

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

PROJECT_NAME

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

cmake 调用环境变量的方式

$ENV{NAME}

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

设置环境变量的方式是:SET(ENV{变量名} 值)

  • CMAKE_INCLUDE_CURRENT_DIR
    • 自动添加 CMAKE_CURRENT_BINARY_DIRCMAKE_CURRENT_SOURCE_DIR 到当前处理的 CMakeLists.txt。相当于在每个 CMakeLists.txt 加入
    • INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
  • CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE
    • 将工程提供的头文件目录始终至于系统头文件目录的前面,当你定义的头文件确实跟系统发生冲突时可以提供一些帮助。
  • CMAKE_INCLUDE_PATHCMAKE_LIBRARY_PATH 在上一节已经提及。

系统信息

  • CMAKE_MAJOR_VERSION,CMAKE 主版本号,比如 2.4.6 中的 2
  • CMAKE_MINOR_VERSION,CMAKE 次版本号,比如 2.4.6 中的 4
  • CMAKE_PATCH_VERSION,CMAKE 补丁等级,比如 2.4.6 中的 6
  • CMAKE_SYSTEM,系统名称,比如 Linux-2.6.22
  • CMAKE_SYSTEM_NAME,不包含版本的系统名,比如 Linux
  • CMAKE_SYSTEM_VERSION,系统版本,比如 2.6.22
  • CMAKE_SYSTEM_PROCESSOR,处理器名称,比如 i686
  • UNIX,在所有的类 UNIX 平台为 TRUE,包括 OS X 和 cygwin
  • WIN32,在所有的 win32 平台为 TRUE,包括 cygwin

主要的开关选项

MAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS,用来控制 IF ELSE 语句的书写方式,在下一节语法部分会讲到。

BUILD_SHARED_LIBS

  • 这个开关用来控制默认的库编译方式,如果不进行设置,使用 ADD_LIBRARY 并没有指定库类型的情况下,默认编译生成的库都是静态库。

CMAKE_C_FLAGS

  • 设置C编译选项,也可以通过指令ADD_DEFINITIONS()添加。

CMAKE_CXX_FLAGS

  • 设置C++编译选项,也可以通过指令ADD_DEFINITIONS()添加

八、cmake 常用指令

基本指令

ADD_DEFINITIONS

向C,C++编译器添加-D定义,比如:
ADD_DEFINITIONS(-DENABLE_DEBUG -DABC),参数之间用空格分割。
如果你的代码中定义了#ifdef ENABLE_DEBUG #endif,这个代码块就会生效。
如果要添加其他的编译器开关,可以通过 CMAKE_C_FLAGS 变量和 CMAKE_CXX_FLAGS 变量设置。

ADD_DEPENDENCIES

定义 target 依赖的其他 target,确保在编译本 target 之前,其他的 target 已经被构建。
ADD_DEPENDENCIES(target-name depend-target1 depend-target2 ...)

ADD_EXECUTABLEADD_LIBRARYADD_SUBDIRECTORY

见前面的介绍

ADD_TEST , ENABLE_TESTING

ENABLE_TESTING指令用来控制 Makefile 是否构建 test 目标,涉及工程所有目录。语法很简单,没有任何参数,ENABLE_TESTING(),一般情况这个指令放在工程的主CMakeLists.txt 中。

ADD_TEST 指令的语法是ADD_TEST(testname Exename arg1 arg2 ...)

  • testname 是自定义的 test 名称
  • Exename 可以是构建的目标文件也可以是外部脚本等等
  • 后面连接传递给可执行文件的参数。

如果没有在同一个 CMakeLists.txt 中打开ENABLE_TESTING()指令,任何 ADD_TEST 都是无效的。

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_TEST(mytest hello)
ENABLE_TESTING()
Cmake Practice/t1/build$ make test
Running tests...
Test project /home/ningliuwsl/CPP/Cmake Practice/t1/build
    Start 1: mytest
1/1 Test #1: mytest ...........................   Passed    0.00 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.00 sec

AUX_SOURCE_DIRECTORY

基本语法是:AUX_SOURCE_DIRECTORY(dir VARIABLE)

作用是发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表。因为目前 cmake 还不能自动发现新添加的源文件。

AUX_SOURCE_DIRECTORY(. SRC_LIST)
ADD_EXECUTABLE(main ${SRC_LIST})

也可以通过后面提到的 FOREACH 指令来处理这个 LIST

CMAKE_MINIMUM_REQUIRED

其语法为 CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])

比如

CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR)

如果 cmake 版本小与 2.5,则出现严重错误,整个过程中止。

[EXEC_PROGRAM](https://cmake.org/cmake/help/latest/command/exec_program.html)

CMakeLists.txt 处理过程中执行命令,并不会在生成的 Makefile 中执行

具体语法为:

EXEC_PROGRAM(Executable [directory in which to run]
                 [ARGS <arguments to executable>]
                 [OUTPUT_VARIABLE <var>]
                 [RETURN_VALUE <var>])

用于在指定的目录运行某个程序,通过 ARGS 添加参数,如果要获取输出和返回值,可通过 OUTPUT_VARIABLERETURN_VALUE 分别定义两个变量。
这个指令可以帮助你在 CMakeLists.txt 处理过程中支持任何命令,比如根据系统情况去 修改代码文件等等。

举个简单的例子,我们要在 src 目录执行 ls 命令,并把结果和返回值存下来。
可以直接在 src/CMakeLists.txt中添加:

EXEC_PROGRAM(ls ARGS "../*.c" OUTPUT_VARIABLE LS_OUTPUT RETURN_VALUE LS_RVALUE)
# IF(NOT LS_RVALUE)
IF(NOT "${LS_RVALUE}")
MESSAGE(STATUS "ls result: " ${LS_OUTPUT})
ENDIF()

cmake3.0后已经废弃exec_program, 用[execute_process](https://cmake.org/cmake/help/latest/command/execute_process.html#command:execute_process)替换

FILE

文件操作指令,基本语法为:

FILE(WRITE filename "message to write"... )
        FILE(APPEND filename "message to write"... )
        FILE(READ filename variable)
        FILE(GLOB  variable [RELATIVE path] [globbing
expressions]...)
        FILE(GLOB_RECURSE variable [RELATIVE path]
             [globbing expressions]...)
        FILE(REMOVE [directory]...)
        FILE(REMOVE_RECURSE [directory]...)
        FILE(MAKE_DIRECTORY [directory]...)
        FILE(RELATIVE_PATH variable directory file)
        FILE(TO_CMAKE_PATH path result)
        FILE(TO_NATIVE_PATH path result)

INCLUDE

用来载入 CMakeLists.txt 文件,也用于载入预定义的 cmake 模块

INCLUDE(file1 [OPTIONAL]) 

INCLUDE(module [OPTIONAL])
  • OPTIONAL 参数的作用是文件不存在也不会产生错误

你可以指定载入一个文件,如果定义的是一个模块,那么将在 CMAKE_MODULE_PATH中搜 索这个模块并入。
载入的内容将在处理到 INCLUDE 语句是直接执行。

INSTALL

参考前面的安装部分

FIND_

FIND_FILE(<VAR> name1 path1 path2 ...)

  • VAR变量代表找到的文件全路径,包含文件名

FIND_LIBRARY(<VAR> name1 path1 path2 ...)

  • VAR变量表示找到的库全路径,包含库文件名
    FIND_LIBRARY(libX X11 /usr/lib)
    IF(NOT libX)
    MESSAGE(FATAL_ERROR “libX not found”)
    ENDIF(NOT libX)
    

FIND_PATH(<VAR> name1 path1 path2 ...)

  • VAR 变量代表包含这个文件的路径

FIND_PROGRAM(<VAR> name1 path1 path2 ...)

  • VAR 变量代表包含这个程序的全路径

FIND_PACKAGE(<name> [major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS] [componets...]])

  • 用来调用预定义在 CMAKE_MODULE_PATH 下的 Find<name>.cmake 模块,你也可以自己定义Find<name>模块,通过SET(CMAKE_MODULE_PATH dir)将其放入工程的某个目录 中供工程使用

控制指令

IF

基本语法为

IF(expression)
          # THEN section.
          COMMAND1(ARGS ...)
          COMMAND2(ARGS ...)
          ...
        ELSE(expression)
          # ELSE section.
          COMMAND1(ARGS ...)
          COMMAND2(ARGS ...)
          ...
ENDIF(expression)

另外一个指令是 ELSEIF,总体把握一个原则,凡是出现 IF 的地方一定要有对应的 ENDIF。出现 ELSEIF 的地方,ENDIF 是可选的

表达式的使用方法如下:

  • IF(var),如果变量不是:0``N, NO, OFF, FALSE, NOTFOUND<var>_NOTFOUND 时,表达式为真。
  • IF(NOT var),与上述条件相反。
  • IF(var1 AND var2),当两个变量都为真是为真。
  • IF(var1 OR var2),当两个变量其中一个为真时为真。
  • IF(COMMAND cmd),当给定的cmd确实是命令并可以调用是为真。
  • IF(EXISTS dir)或者IF(EXISTS file),当目录名或者文件名存在时为真。
  • IF(file1 IS_NEWER_THAN file2),当 file1file2 新,或者 file1/file2 其中有一个不存在时为真,文件名请使用完整路径。
  • IF(IS_DIRECTORY dirname),当dirname是目录时,为真

  • IF(VAR MATCHES regex)

    IF(variable MATCHES regex)
    IF(string MATCHES regex)
    

    当给定的变量或者字符串能够匹配正则表达式 regex 时为真

    IF("hello" MATCHES "ell")
    MESSAGE("true")
    ENDIF("hello" MATCHES "ell")
    
  • 数字比较表达式

    IF(variable LESS number) 
    IF(string LESS number) 
    IF(variable GREATER number) 
    IF(string GREATER number) 
    IF(variable EQUAL number) 
    IF(string EQUAL number)
    
  • 按照字母序的排列进行比较

    IF(variable STRLESS string)
    IF(string STRLESS string)
    IF(variable STRGREATER string) 
    IF(string STRGREATER string) 
    IF(variable STREQUAL string)
    IF(string STREQUAL string)
    
  • IF(DEFINED variable): 如果变量被定义,为真

判断平台差异

IF(WIN32)
    MESSAGE(STATUS “This is windows.”) 
    # 作一些 Windows 相关的操作
ELSE(WIN32)
    MESSAGE(STATUS “This is not windows”) 
    # 作一些非 Windows 相关的操作
ENDIF(WIN32)

使用CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS开关, SET(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)

IF(WIN32)
ELSE()
ENDIF()

配合 ELSEIF 使用,可能的写法是这样

IF(WIN32)
#do something related to WIN32 
ELSEIF(UNIX)
#do something related to UNIX
ELSEIF(APPLE)
#do something related to APPLE
ENDIF(WIN32)

WHILE

WHILE(condition)
          COMMAND1(ARGS ...)
          COMMAND2(ARGS ...)
          ...
ENDWHILE(condition)

其真假判断条件可以参考IF 指令

FOREACH

FOREACH 指令的使用方法有三种形式

列表

FOREACH(loop_var arg1 arg2 ...)
          COMMAND1(ARGS ...)
          COMMAND2(ARGS ...)
          ...
ENDFOREACH(loop_var)

使用的 AUX_SOURCE_DIRECTORY

AUX_SOURCE_DIRECTORY(. SRC_LIST) 
    FOREACH(F ${SRC_LIST})
        MESSAGE(${F})
ENDFOREACH(F)

范围

FOREACH(loop_var RANGE total) 
ENDFOREACH(loop_var)

0total1为步进。

FOREACH(VAR RANGE 10) 
MESSAGE(${VAR}) 
ENDFOREACH(VAR)
0
1
2
3
4
5
6
7
8
9
10

范围和步进

FOREACH(loop_var RANGE start stop [step]) 
ENDFOREACH(loop_var)

start 开始到 stop 结束,以 step 为步进

FOREACH(A RANGE 5 15 3) 
    MESSAGE(${A}) 
ENDFOREACH(A)
5
8
11
14

这个指令需要注意的是,直到遇到 ENDFOREACH 指令,整个语句块才会得到真正的执行

九、复杂的例子:模块的使用和自定义模块

本章我们将着重介绍系统预定义的 Find 模块的使用以及自己编写 Find 模块,系统中提供了其他各种模块,一般情况需要使用 INCLUDE 指令显式的调用,FIND_PACKAGE 指令是一个特例,可以直接调用预定义的模块。

其实使用纯粹依靠 cmake 本身提供的基本指令来管理工程是一件非常复杂的事情,所以,cmake 设计成了可扩展的架构,可以通过编写一些通用的模块来扩展 cmake。

在本章,我们准备首先介绍一下 cmake 提供的 FindCURL 模块的使用。然后,基于我们前面的 libhello 共享库,编写一个 FindHello.cmake 模块。

使用 FindCURL 模块

  • t5/src/main.c ```c // t5/src/main.c

    include

    include

    include

    include

FILE *fp;

int write_data(void ptr, size_t size, size_t nmemb, void stream) { int written = fwrite(ptr, size, nmemb, (FILE *)fp); return written; }

int main() { const char path = “/tmp/curl-test”; const char mode = “w”; fp = fopen(path, mode); curl_global_init(CURL_GLOBAL_ALL); CURLcode res; CURL *curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, “http://www.linux-ren.org“); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data); curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); res = curl_easy_perform(curl); curl_easy_cleanup(curl); }

这段代码的作用是通过 `curl` 取回 https://www.baidu.com/ 的首页并写入 `/tmp/curl-test` 文件中。

建立主工程文件 `CMakeLists.txt`

PROJECT(CURLTEST) ADD_SUBDIRECTORY(src)

建立 `src/CMakeLists.txt`

ADD_EXECUTABLE(curltest main.c)

现在自然是没办法编译的,需要添加 `curl`的头文件路径和库文件

<a name="UErbZ"></a>
### 方法 1
直接通过 `INCLUDE_DIRECTORIES `和 `TARGET_LINK_LIBRARIES` 指令添加

我们可以直接在 `src/CMakeLists.txt` 中添加:

INCLUDE_DIRECTORIES(/usr/include) TARGET_LINK_LIBRARIES(curltest curl)

然后建立 `build` 目录进行外部构建即可。

现在我们要探讨的是使用 cmake 提供的 `FindCURL` 模块。

<a name="fWVOs"></a>
### 方法2
向 `src/CMakeLists.txt` 中添加

FIND_PACKAGE(CURL) IF(CURL_FOUND) INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(curltest ${CURL_LIBRARY}) ELSE(CURL_FOUND) MESSAGE(FATAL_ERROR ”CURL library not found”) ENDIF(CURL_FOUND)

对于系统预定义的 `Find<name>.cmake` 模块,使用方法一般如上例所示

每一个模块都会定义以下几个变量:

- `<name>_FOUND` 
- `<name>_INCLUDE_DIR or <name>_INCLUDES`
- `<name>_LIBRARY or <name>_LIBRARIES`

你可以通过`<name>_FOUND`来判断模块是否被找到,如果没有找到,按照工程的需要关闭某些特性、给出提醒或者中止编译,上面的例子就是报出致命错误并终止构建。<br />如果 `<name>_FOUND` 为真,则将 `<name>_INCLUDE_DIR` 加入 `INCLUDE_DIRECTORIES`, 将 `<name>_LIBRARY` 加入 `TARGET_LINK_LIBRARIES` 中。

再来看一个复杂的例子,通过 `<name>_FOUND` 来控制工程特性

SET(mySources viewer.c) SET(optionalSources) SET(optionalLibs) FIND_PACKAGE(JPEG) IF(JPEG_FOUND) SET(optionalSources ${optionalSources} jpegview.c) INCLUDE_DIRECTORIES( ${JPEG_INCLUDE_DIR} ) SET(optionalLibs ${optionalLibs} ${JPEG_LIBRARIES} ) ADD_DEFINITIONS(-DENABLE_JPEG_SUPPORT) ENDIF(JPEG_FOUND) IF(PNG_FOUND) SET(optionalSources ${optionalSources} pngview.c) INCLUDE_DIRECTORIES( ${PNG_INCLUDE_DIR} ) SET(optionalLibs ${optionalLibs} ${PNG_LIBRARIES} ) ADD_DEFINITIONS(-DENABLE_PNG_SUPPORT) ENDIF(PNG_FOUND) ADD_EXECUTABLE(viewer ${mySources} ${optionalSources} ) TARGET_LINK_LIBRARIES(viewer ${optionalLibs}

通过判断系统是否提供了 `JPEG` 库来决定程序是否支持 `JPEG` 功能

<a name="G3ut7"></a>
## 编写属于自己的 FindHello 模块
如何自定义 `FindHELLO` 模块并使用这个模块构建工程

- 建立 cmake 目录, 在cmake目录中定义 `cmake/FindHELLO.cmake` 模块

FIND_PATH(HELLO_INCLUDE_DIR hello.h /usr/include/hello /usr/local/include/hello) FIND_LIBRARY(HELLO_LIBRARY NAMES hello PATH /usr/lib /usr/local/lib) IF(HELLO_INCLUDE_DIR AND HELLO_LIBRARY) SET(HELLO_FOUND TRUE) ENDIF(HELLO_INCLUDE_DIR AND HELLO_LIBRARY) IF(HELLO_FOUND) IF(NOT HELLO_FIND_QUIETLY) MESSAGE(STATUS “Found Hello: ${HELLO_LIBRARY}”) ENDIF(NOT HELLO_FIND_QUIETLY) ELSE(HELLO_FOUND) IF(HELLO_FIND_REQUIRED) MESSAGE(FATAL_ERROR “Could not find hello library”) ENDIF(HELLO_FIND_REQUIRED) ENDIF(HELLO_FOUND)

回顾一下 `FIND_PACKAGE` 指令

FIND_PACKAGE( [major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS] [componets…]])

前面的 `CURL` 例子中我们使用了最简单的 `FIND_PACKAGE` 指令,其实他可以使用多种参数,`QUIET`参数,对应与我们编写的`FindHELLO`中的 `HELLO_FIND_QUIETLY`,如果不指定 这个参数,就会执行:`MESSAGE(STATUS "Found Hello: ${HELLO_LIBRARY}")`

`REQUIRED` 参数,其含义是指这个共享库是否是工程必须的,如果使用了这个参数,说明这个链接库是必备库,如果找不到这个链接库,则工程不能编译。对应于`FindHELLO.cmake`模块中的 `HELLO_FIND_REQUIRED` 变量。

同样,我们在上面的模块中定义了 `HELLO_FOUND`, `HELLO_INCLUDE_DIR`,`HELLO_LIBRARY` 变量供开发者在`FIND_PACKAGE` 指令中使用

- 建立 `src/main.c`
```c
#include <hello.h>
int main()
{
     HelloFunc();
    return 0; 
}
  • 建立 src/CMakeLists.txt 文件
    FIND_PACKAGE(HELLO)
    IF(HELLO_FOUND)
      ADD_EXECUTABLE(hello main.c)
      INCLUDE_DIRECTORIES(${HELLO_INCLUDE_DIR})
      TARGET_LINK_LIBRARIES(hello ${HELLO_LIBRARY})
    ENDIF(HELLO_FOUND)
    
    为了能够让工程找到 FindHELLO.cmake 模块(存放在工程中的 cmake 目录)我们在主工程文件 CMakeLists.txt 中加入:SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

使用自定义的 FindHELLO 模块构建工程

采用外部编译的方式,建立 build 目录,进入目录运行

cmake ..