cmake使用教程(十)-关于file

22,430

【cmake系列使用教程】

cmake使用教程(一)-起步

cmake使用教程(二)-添加库

cmake使用教程(三)-安装、测试、系统自检

cmake使用教程(四)-文件生成器

cmake使用教程(五)-cpack生成安装包

cmake使用教程(六)-蛋疼的语法

cmake使用教程(七)-流程和循环

cmake使用教程(八)-macro和function

cmake使用教程(九)-关于安卓的交叉编译

cmake使用教程(十)-关于file

这个系列的文章翻译自官方cmake教程:cmake tutorial

示例程序地址:github.com/rangaofei/t…

不会仅仅停留在官方教程。本人作为一个安卓开发者,实在是没有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选项。