[cmake 系列使用教程]
cmake 使用教程(一)- 起步
cmake 使用教程(二)- 添加库
cmake 使用教程(三)- 安装, 测试, 系统自检
cmake 使用教程(四)- 文件生成器
cmake 使用教程(五)-cpack 生成安装包
cmake 使用教程(六)- 蛋疼的语法
cmake 使用教程(七)- 流程和循环
cmake 使用教程(八)-macro 和 function
cmake 使用教程(九)- 关于安卓的交叉编译
cmake 使用教程(十)- 关于 file
这个系列的文章翻译自官方 cmake 教程: cmake tutorial https://cmake.org/cmake-tutorial/ .
示例程序地址: https://github.com/rangaofei/tutorial
不会仅仅停留在官方教程. 本人作为一个安卓开发者, 实在是没有 linux c 程序开发经验, 望大佬们海涵. 教程是在 macos 下完成, 大部分 linux 我也测试过, 有特殊说明的我会标注出来. 本教程基于 cmake-3.10.2, 同时认为你已经安装好 cmake.
简介
cmake 中的 file 使用也很简单, 与 c 语言的文件 io 相似. file 命令属于脚本命令, 可以卸载脚本中. 后边讲到的
aux_source_directory
属于工程命令, 不能用在脚本中.
首先来说一个我在编写程序的时候遇到的问题, 在学习 apue 的过程中, 有许多实例小程序, 因为 IDE 用的是 Clion, 所以需要在 cmake 脚本中生成执行文件, 最开始的时候是简单的添加 add_executable 命令来不断的增加新的程序构建, 随着学习的深入, 手动添加太麻烦了, 于是写了一个简单的脚本来半自动构建程序, 目录结构如下:
- apue git:(master) cd src
- src git:(master) tree
- .
- CmakeLists.txt
- part1
- CmakeLists.txt
- copytest.c
- groupid.c
- mycopy.c
- myerror.c
- myls.c
- myshell.c
- mystdcopy.c
- newshell.c
- processid.c
- part11
- CmakeLists.txt
- pthread1.c
- pthread2.c
- pthread3.c
- pthread4.c
- part3
- CmakeLists.txt
- holetest.c
- seektest.c
- unp_1
- CMakeLists.txt
- daytimetcpcli.c
- daytimetcpcliv6.c
- daytimetcpsrv.c
- unp_3
- CMakeLists.txt
- byteorder.c
src 是 apue 的源代码目录, 包含多个章节对应的文件夹和一个主 cmakelist 文件, 章节文件夹下是具体的 c 和 h 文件和一个 cmakelist 文件, 看一下 part3 章节文件夹中的 cmakelist 文件:
- AUX_SOURCE_DIRECTORY(. PART_THREE)
- SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
- SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
- FOREACH (FILE ${PART_THREE})
- MESSAGE(STATUS ${FILE})
- STRING(REPLACE "./" "" LIB_NAME ${FILE})
- STRING(REPLACE ".c" "" LIB_NAME ${LIB_NAME})
- add_executable(${LIB_NAME} ${FILE})
- target_link_libraries(${LIB_NAME} apue.a)
- ENDFOREACH ()
这段代码首先将当前章节的所有文件收入到 PART_THREE 变量中, 以 list 形式存储, 下边两行代码设置了可执行文件和库文件的存储路径, 然后 FOREACHE 循环中输出了所有的可执行文件和链接库, 可执行文件名称是用. c 文件用正则替换无用信息后生成的.
然后在主 cmakelist 中添加
add_subdirectory(part3)
后直接执行即可. 这也是今天要讲的主要内容.
文件写入与追加
- file(WRITE <filename> <content>...)
- file(APPEND <filename> <content>...)
将 content 的内容写入 filename 中, 假如文件不存在则会创建文件, 假如 filename 中包含路径, 则相应的文件夹也会被创建. WRITE 会擦出文件内容, 重新写入 content,APPEND 会在文件末尾追加内容. 写一个最简单的测试:
- file(WRITE test.txt "this is a test to wirte\n")
- file(APPEND test/test.txt "this is a test to append")
- file(APPEND test.txt "this is a test to append")
此时目录结构如下:
- .
- write.cmake
- 0 directories, 1 file
执行该脚本后:
- Stepfile git:(master) cmake -P write.cmake
- Stepfile git:(master) tree
- .
- test
- test.txt
- test.txt
- write.cmake
- 1 directory, 3 files
前边介绍过 configure_file 这个命令, 是用来在构建工程时替换文件内容的, 注意一下区别.
文件的读取
- file(READ <filename> <variable>
- [OFFSET <offset>] [LIMIT <max-in>] [HEX])
这个也比较简单: 将 filename 文件中的内容读取到 variable 总, 可以指定 OFFSET 的值, 也就是开始读取的位置, 指定 LISTMI 的值, 读取的长度, HEX 是否以 16 进制形式读取.
file(STRINGS <filename> <variable> [<options>...])
类似于读取字符码, 而不读取字节码. 这个命令会将 filename 中的字符串读取到 variable 中, 并且 variable 是一个 list, 每个元素保存每行的内容. 二进制文件不会被读取, 并且换行符会被忽略. 举个例子, 我们刚才写入的 test.txt 的文件内容是:
- this is a test to wirte
- this is a test to append
- have tab #这个是我手动添加的
我们读取这个文件并打印结果, 编写 string.cmake 文件如下:
- file(STRINGS test.txt strings)
- foreach(str IN LISTS strings)
- message(STATUS ${str})
- endforeach(str)
因为结果会用 list 保存, 所以用 foreach 循环来查看结果:
- -- this is a test to wirte
- -- this is a test to append
- -- have tab
关于一些选项, 用的不太多:
OPTION | 说明 |
---|---|
LENGTH_MAXIMUM | 读取字符的最大个数 |
LENGTH_MINIMUM | 读取的字符的最少个数 |
LIMIT_COUNT | 提取的不同字符的最大数量 |
LIMIT_INPUT | 限制读取的最大字节 |
LIMIT_OUTPUT | 限制写入变量的最大字节 |
NEWLINE_CONSUME | 不忽略换行符 |
NO_HEX_CONVERSION | 不需要自动转换为 16 进制 |
REGEX | 提取匹配正则表达式的字符串 |
ENCODING | 重新编码 UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE |
文件的 hash 码
file(<HASH> <filename> <variable>)
利用这个命令可以提取出文件的 hash 码
MD5,SHA1,SHA224,SHA256,SHA384,SHA512,SHA3_224,SHA3_256,SHA3_384,SHA3_512
如果看过我的 bomebrew 教程可应该知道, 在生成 formula.rb 文件的时候需要填写打包好的文件的 SHA256 来验证下载文件的完整性, 所以可以利用这个写一个简单的脚本来输出 hash 值, 写一个简单的例子吧:
- file(SHA256 test.txt hash)
- message(STATUS ${hash})
- -- f9bb70f1a2036a73f611858d01a8fb498efc7c83568faf0c74e5a52037492702
收集文件
- file(GLOB <variable>
- [LIST_DIRECTORIES true|false] [RELATIVE <path>]
- [<globbing-expressions>...])
- file(GLOB_RECURSE <variable> [FOLLOW_SYMLINKS]
- [LIST_DIRECTORIES true|false] [RELATIVE <path>]
- [<globbing-expressions>...])
两个命令, 首先讲一下第一个 GLOB:
GLOB 命令将所有匹配
<globbing-expressions>
(可选, 假如不写, 毛都匹配不到)的文件挑选出来, 默认以字典顺序排序.
- file(GLOB files *)
- foreach(file IN LISTS files)
- message(STATUS ${file})
- endforeach(file)
这段代码的意思是挑选出当前文件下的所有文件, 然后打印:
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/filelist.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/hash.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/string.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/test
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/test.txt
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/write.cmake
其实我这个文件夹下的内容如下:
- .
- filelist.cmake
- hash.cmake
- string.cmake
- test
- test.txt
- test.txt
- write.cmake
- 1 directory, 6 files
test 是一个文件夹, 但是在脚本中输出了这个文件夹. 假如我们不想要这个文件夹, 我们可以通过 LIST_DIRECTORIES 设置为 false 即可(默认为 true), 修改第一行代码如下:
- file(GLOB files LIST_DIRECTORIES false *)
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/filelist.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/hash.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/string.cmake
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/test.txt
- -- /Users/rangaofei/Documents/program/tutorial/Stepfile/write.cmake
这次只输出了文件, 而文件夹没有在里边, 假如我们不需要绝对路径, 只需要相对某个文件夹的路径, 则可以通过设置 RELATIVE 的值来设置. 将文件修改如下:
- set(CUR ${CMAKE_CURRENT_SOURCE_DIR})
- file(GLOB files LIST_DIRECTORIES false RELATIVE ${CUR}/.. *)
- foreach(file IN LISTS files)
- message(STATUS ${file})
- endforeach(file)
我们设置了 CUR 为当前的文件夹, 然后设置相对路径为当前文件夹的上级文件夹, 而我的当前文件夹名称为 Stepfile, 则输出会包含当前文件夹的名字 + 文件名字:
- -- Stepfile/filelist.cmake
- -- Stepfile/hash.cmake
- -- Stepfile/string.cmake
- -- Stepfile/test.txt
- -- Stepfile/write.cmake
就是这么蛋疼. 还要说一下这个蛋疼的伪正则匹配, 一般文件是够用的.
*.cxx - 匹配所有的 cxx 结尾的文件
*.vt? - 匹配所有的 vta,...,vtz 等文件
f[3-5].txt - 匹配 f3.txt, f4.txt, f5.txt 这三个文件
cmake 官方不推荐使用 GLOB 来收集文件, 因为在工程或者模块中的 CMakeLists.txt 文件未更改而用 file 搜寻的文件夹下有文件的删除或者增加, cmake 构建并不会知晓, 而是使用旧的 list.
再来讲一下第二个, GLOB_CURSE:
这个命令是用来列出所有子文件夹中的文件和当前所有文件, 具体深度多少我也不知道. 用法基本同上, 只是多了一个 FOLLOW_SYMLINKS 可选项. 2.6.1 版本之前对于链接的文件夹同样会列出所有的链接过去的文件夹下的文件, 因为这样会引起一些麻烦, 所以在以后的版本中去掉了这个属性, 而是将链接当做一个文件, 不会列出链接到的文件夹下的文件. 假如需要列出, 则添加 FOLLOW_SYMLINKS 参数即可.
- cmake_minimum_required(VERSION 3.6)
- # if(POLICY CMP0009)
- # cmake_policy(SET CMP0009 NEW)
- # endif()
- set(CUR ${CMAKE_CURRENT_SOURCE_DIR})
- file(GLOB_RECURSE files FOLLOW_SYMLINKS LIST_DIRECTORIES true RELATIVE ${CUR}/.. *)
- foreach(file IN LISTS files)
- message(STATUS ${file})
- endforeach(file)
这段代码将会列出当前所有文件, 子文件夹中的文件以及链接中的文件.
关于 AUX_SOURCE_DIRECTORY
aux_source_directory(<dir> <variable>)
注意这个命令不能用于 script 中, 他是 project 命令.
寻找 dir 文件夹下所有的源文件, 存入 variable 中. 这个命令与之前的命令有所区别, 因为它只会搜集当前设置语言的文件, cmake 默认的设置语言是 c/cxx, 则会收集到的文件只有这些语言能识别的文件, 比如在 step 中添加如下代码
- aux_source_directory(./ SRCLIST)
- foreach(file IN LISTS SRCLIST)
- message(STATUS ${file})
- endforeach(file)
当前目录结构如下
- .
- CMakeLists.txt
- TutorialConfig.h.in
- build
- tutorial.cxx
看一下输出了什么
-- ./tutorial.cxx
只有一个文件被假如 list 中了.
文件的操作
file(RENAME <oldname> <newname>)
重命名文件或者文件夹
- file(REMOVE [<files>...])
- file(REMOVE_RECURSE [<files>...])
删除指定的文件, REMOVE_RECURSE 则会删除文件和文件夹, 假如不存在, 不会抛出错误.
file(MAKE_DIRECTORY [<directories>...])
递归创建文件, 包括路径中的文件夹
file(RELATIVE_PATH <variable> <directory> <file>)
计算 file 相对于 directory 的相对路径, 存入 variable 中. 类似于前边的收集文件.
- file(TO_CMAKE_PATH "<path>" <variable>)
- file(TO_NATIVE_PATH "<path>" <variable>)
在 cmake 路径和本地路径之间相互转换. cmake 路径使用的是 /
- file(DOWNLOAD <url> <file> [<options>...])
- file(UPLOAD <file> <url> [<options>...])
这两个命令真是让我的菊花紧到极致了. 第一个是从 url 下载文件命名为 file, 第二个是将本地文件 file 上传至 url. 以下的 option 适用于这两个命令
参数 | 说明 |
---|---|
INACTIVITY_TIMEOUT | 超时时间 |
LOG | 将日志写入变量中 |
SHOW_PROGRESS | 显示进度 |
STATUS | a;b 形式,a 是返回的状态码,b 是错误代码,假如没错误,b 是 0(鬼知道,我没试) |
TIMEOUT | 连接超时时间 |
USERPWD : | 用户名和密码 |
HTTPHEADER | http 请求头 |
EXPECTED_HASH ALGO= | 验证算法(适用于下载) |
file(TIMESTAMP <filename> <variable> [<format>] [UTC])
将 filename 文件的时间戳存储在 varibale 中.
- file(GENERATE OUTPUT output-file
- <INPUT input-file|CONTENT content>
- [CONDITION expression])
这个命令不能用于 script, 只能在 project 中才有作用. 在教程四中我们介绍过用 add_custom_command 方式在构建时添加文件, 现在讲的 file 方式, 基本类似, INPUT 和 CONTENT 必须要选一个, 前者是以文件为内容, 后者是以字符串为内容. 在 3.10 版本之后, INPUT 使用的是相对当前文件夹的路径, OUTPUT 使用的是生成的文件夹的路径. 另外, 只有当 condition 为真得时候才会执行生成文件, 并且表达式的值必须返回 0 或者 1. 假如设置的最小版本小于 3.10, 徐志明 CMP0070, 设置为 new, 则与 3.10 相同, 设置为 old, 则是相对路径, 不测试 old 了, 因为以后不会用这个.
在 step1 中 cdCMakeLists.txt 中添加
- if(POLICY CMP0070)
- cmake_policy(SET CMP0070 NEW)
- endif()
- file(GENERATE OUTPUT out.txt
- CONTENT "java"
- )
在 build 文件夹中确实生成了 out.txt, 且内容是 java.
还有两个命令:
- file(<COPY|INSTALL> <files>... DESTINATION <dir>
- [FILE_PERMISSIONS <permissions>...]
- [DIRECTORY_PERMISSIONS <permissions>...]
- [NO_SOURCE_PERMISSIONS] [USE_SOURCE_PERMISSIONS]
- [FILES_MATCHING]
- [[PATTERN <pattern> | REGEX <regex>]
- [EXCLUDE] [PERMISSIONS <permissions>...]] [...])
copy 和 install 文件到指定目标文件夹.
输入的文件是相对于当前文件夹的相对路径, 而目标文件夹则是相对于构建文件夹的相对路径.
COPY 可保留输入文件时间戳, 并优化文件(如果在目标文件夹已经存在相同的文件且时间戳相同). 除非给出显式权限或
NO_SOURCE_PERMISSIONS
, 否则将保留权限(默认值为
- USE_SOURCE_PERMISSIONS
- ).
INSTALL 与 COPY 基本相同, 但是会输出 install 日志, 并且会默认使用
NO_SOURCE_PERMISSIONS
权限.
- file(LOCK <path> [DIRECTORY] [RELEASE]
- [GUARD <FUNCTION|FILE|PROCESS>]
- [RESULT_VARIABLE <variable>]
- [TIMEOUT <seconds>])
类似于 java 中的同步锁.
假如指定了 [DIRECTORY] 选项则锁定 < path>/cmake.lock 文件, 否则锁定 < path > 文件. GUARD 用来确定作用域, 默认是 PROCESS, 其他两个看意思也能明白. RELEASE 用来显式的解锁.
假如没有指定 TIMEOUT 选项, 则系统会一直等到文件被上锁或者发生致命的错误; 假如设置为 0 则会立即执行 lock 操作并返回状态码; 假如设置不为 0, 则会等待相应的时间 (以秒为单位) 来上锁.
假如没有设置 RESULT_VARIABLE, 则任何错误都会阻止程序运行. 设置的话结果会存储到 varobale 中, 成功是 0, 错误会存储错误信息.
请注意, 锁是建议性的 - 不能保证其他进程会尊重此锁, 即锁定同步两个或多个共享某些可修改资源的 CMake 实例. 应用于 DIRECTORY 选项的类似逻辑 - 锁定父目录不会阻止其他 LOCK 命令锁定任何子目录或文件.
试图锁定文件两次是不允许的. 如果它们不存在, 任何中间目录和文件本身都将被创建. RELEASE 操作中忽略 GUARD 和 TIMEOUT 选项.
来源: https://juejin.im/post/5b3ecfef6fb9a04f8c5ebab5