一、什么是cmake
你或许听过好几种 Make 工具,例如 GNU Make ,QT 的 qmake ,微软的 MS nmake,BSD Make(pmake),Makepp,等等。这些 Make 工具遵循着不同的规范和标准,所执行的 Makefile 格式也千差万别。这样就带来了一个严峻的问题:如果软件想跨平台,必须要保证能够在不同平台编译。而如果使用上面的 Make 工具,就得为每一种标准写一次 Makefile ,这将是一件让人抓狂的工作。
CMake就是针对上面问题所设计的工具:它首先允许开发者编写一种平台无关的 CMakeList.txt 文件来定制整个编译流程,然后再根据目标用户的平台进一步生成所需的本地化 Makefile 和工程文件,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。从而做到“Write once, runeverywhere”。显然,CMake 是一个比上述几种 make 更高级的编译配置工具。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等。
在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
1. 编写 CMake 配置文件 CMakeLists.txt 。
2. 执行命令 cmakePATH 或者 ccmakePATH 生成 Makefile 1 1ccmake 和 cmake 的区别在于前者提供了一个交互式的界面。。其中, PATH 是 CMakeLists.txt 所在的目录。
3. 使用 make 命令进行编译。
ccmake 和 cmake 的区别在于前者提供了一个交互式的界面
先看一个简单的例程进行cmake的快速入门:
教程一:
例子一 | 单个源文件 main.c |
例子二 | ==>分解成多个 main.c hello.h hello.c |
例子三 | ==>先生成一个静态库,链接该库 |
例子四 | ==>将源文件放置到不同的目录 |
例子五 | ==>控制生成的程序和库所在的目录 |
例子六 | ==>使用动态库而不是静态库 |
例子一
一个经典的C程序,如何用cmake来进行构建程序呢?
//main.c
#include<stdio.h>
int main()
{
printf("Hello World!/n");
return 0;
}
编写一个 CMakeList.txt 文件(可看做cmake的工程文件):
project(HELLO)
set(SRC_LISTmain.c)
add_executable(hello${SRC_LIST})
然后,建立一个任意目录(比如本目录下创建一个build子目录),在该build目录下调用cmake
· 注意:为了简单起见,我们从一开始就采用cmake的out-of-source 方式来构建(即生成中间产物与源代码分离),并始终坚持这种方法,这也就是此处为什么单独创建一个目录,然后在该目录下执行 cmake 的原因
cmake ..-G"NMake Makefiles"
nmake
或者
cmake ..-G"MinGW Makefiles"
make
即可生成可执行程序 hello(.exe)
目录结构
+
|
+--- main.c
+--- CMakeList.txt
|
/--+ build/
|
+--- hello.exe
cmake 真的不太好用哈,使用cmake的过程,本身也就是一个编程的过程,只有多练才行。
我们先看看:前面提到的这些都是什么呢?
CMakeList.txt
第一行 project 不是强制性的,但最好始终都加上。这一行会引入两个变量
· HELLO_BINARY_DIR 和 HELLO_SOURCE_DIR
同时,cmake自动定义了两个等价的变量
· PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR
因为是out-of-source方式构建,所以我们要时刻区分这两个变量对应的目录
可以通过message来输出变量的值
message(${PROJECT_SOURCE_DIR})
set 命令用来设置变量
add_exectuable 告诉工程生成一个可执行文件。
add_library 则告诉生成一个库文件。
· 注意:CMakeList.txt 文件中,命令名字是不区分大小写的,而参数和变量是大小写相关的。
cmake命令
cmake 命令后跟一个路径(..),用来指出 CMakeList.txt 所在的位置。
由于系统中可能有多套构建环境,我们可以通过-G来制定生成哪种工程文件,通过 cmake -h 可得到详细信息。
要显示执行构建过程中详细的信息(比如为了得到更详细的出错信息),可以在CMakeList.txt内加入:
· SET( CMAKE_VERBOSE_MAKEFILE on )
或者执行make时
· $ make VERBOSE=1
或者
· $ export VERBOSE=1
· $ make
例子二
一个源文件的例子一似乎没什么意思,拆成3个文件再试试看:
· hello.h 头文件
#ifndefDBZHANG_HELLO_
#defineDBZHANG_HELLO_
void hello(constchar* name);
#endif//DBZHANG_HELLO_
· hello.c
#include<stdio.h>
#include"hello.h"
void hello(constchar * name)
{
printf ("Hello %s!/n", name);
}
· main.c
#include"hello.h"
int main()
{
hello("World");
return 0;
}
· 然后准备好CMakeList.txt 文件
project(HELLO)
set(SRC_LISTmain.c hello.c)
add_executable(hello${SRC_LIST})
执行cmake的过程同上,目录结构
+
|
+--- main.c
+--- hello.h
+--- hello.c
+--- CMakeList.txt
|
/--+ build/
|
+--- hello.exe
例子很简单,没什么可说的。
例子三
接前面的例子,我们将 hello.c 生成一个库,然后再使用会怎么样?
改写一下前面的CMakeList.txt文件试试:
project(HELLO)
set(LIB_SRChello.c)
set(APP_SRCmain.c)
add_library(libhello${LIB_SRC})
add_executable(hello${APP_SRC})
target_link_libraries(hellolibhello)
和前面相比,我们添加了一个新的目标 libhello,并将其链接进hello程序
然后想前面一样,运行cmake,得到
+
|
+--- main.c
+--- hello.h
+--- hello.c
+--- CMakeList.txt
|
/--+ build/
|
+--- hello.exe
+--- libhello.lib
里面有一点不爽,对不?
· 因为我的可执行程序(add_executable)占据了hello 这个名字,所以 add_library 就不能使用这个名字了
· 然后,我们去了个libhello 的名字,这将导致生成的库为libhello.lib(或 liblibhello.a),很不爽
· 想生成 hello.lib(或libhello.a) 怎么办?
添加一行
set_target_properties(libhelloPROPERTIES OUTPUT_NAME "hello")
就可以了
例子四
在前面,我们成功地使用了库,可是源代码放在同一个路径下,还是不太正规,怎么办呢?分开放呗
我们期待是这样一种结构
+
|
+--- CMakeList.txt
+--+ src/
| |
| +--- main.c
| /--- CMakeList.txt
|
+--+ libhello/
| |
| +--- hello.h
| +--- hello.c
| /--- CMakeList.txt
|
/--+ build/
哇,现在需要3个CMakeList.txt文件了,每个源文件目录都需要一个,还好,每一个都不是太复杂
· 顶层的CMakeList.txt 文件
project(HELLO)
add_subdirectory(src)
add_subdirectory(libhello)
· src 中的CMakeList.txt 文件
include_directories(${PROJECT_SOURCE_DIR}/libhello)
set(APP_SRCmain.c)
add_executable(hello${APP_SRC})
target_link_libraries(hellolibhello)
· libhello 中的CMakeList.txt 文件
set(LIB_SRChello.c)
add_library(libhello${LIB_SRC})
set_target_properties(libhelloPROPERTIES OUTPUT_NAME "hello")
恩,和前面一样,建立一个build目录,在其内运行cmake,然后可以得到
· build/src/hello.exe
· build/libhello/hello.lib
回头看看,这次多了点什么,顶层的 CMakeList.txt 文件中使用 add_subdirectory 告诉cmake去子目录寻找新的CMakeList.txt 子文件
在 src 的CMakeList.txt 文件中,新增加了include_directories,用来指明头文件所在的路径。
例子五
前面还是有一点不爽:如果想让可执行文件在 bin 目录,库文件在 lib 目录怎么办?
就像下面显示的一样:
+ build/
|
+--+ bin/
| |
| /--- hello.exe
|
/--+ lib/
|
/--- hello.lib
· 一种办法:修改顶级的 CMakeList.txt 文件
project(HELLO)
add_subdirectory(srcbin)
add_subdirectory(libhellolib)
不是build中的目录默认和源代码中结构一样么,我们可以指定其对应的目录在build中的名字。
这样一来:build/src 就成了 build/bin 了,可是除了 hello.exe,中间产物也进来了。还不是我们最想要的。
· 另一种方法:不修改顶级的文件,修改其他两个文件
src/CMakeList.txt 文件
include_directories(${PROJECT_SOURCE_DIR}/libhello)
#link_directories(${PROJECT_BINARY_DIR}/lib)
set(APP_SRCmain.c)
set(EXECUTABLE_OUTPUT_PATH${PROJECT_BINARY_DIR}/bin)
add_executable(hello${APP_SRC})
target_link_libraries(hellolibhello)
libhello/CMakeList.txt 文件
set(LIB_SRChello.c)
add_library(libhello${LIB_SRC})
set(LIBRARY_OUTPUT_PATH${PROJECT_BINARY_DIR}/lib)
set_target_properties(libhelloPROPERTIES OUTPUT_NAME "hello")
例子六
在例子三至五中,我们始终用的静态库,那么用动态库应该更酷一点吧。 试着写一下
如果不考虑windows下,这个例子应该是很简单的,只需要在上个例子的 libhello/CMakeList.txt 文件中的add_library命令中加入一个SHARED参数:
add_library(libhelloSHARED ${LIB_SRC})
可是,我们既然用cmake了,还是兼顾不同的平台吧,于是,事情有点复杂:
· 修改 hello.h 文件
#ifndefDBZHANG_HELLO_
#defineDBZHANG_HELLO_
#if defined _WIN32
#if LIBHELLO_BUILD
#define LIBHELLO_API__declspec(dllexport)
#else
#define LIBHELLO_API__declspec(dllimport)
#endif
#else
#define LIBHELLO_API
#endif
LIBHELLO_API voidhello(const char* name);
#endif//DBZHANG_HELLO_
· 修改 libhello/CMakeList.txt 文件
set(LIB_SRChello.c)
add_definitions("-DLIBHELLO_BUILD")
add_library(libhelloSHARED ${LIB_SRC})
set(LIBRARY_OUTPUT_PATH${PROJECT_BINARY_DIR}/lib)
set_target_properties(libhelloPROPERTIES OUTPUT_NAME "hello")
恩,剩下来的工作就和原来一样了。
二、用cmake设置交叉编译工具链
三、 # Specify cross compiler
四、 SET(CMAKE_SYSTEM_NAME linux)
五、 SET(CMAKE_SYSTEM_PROCESSOR arm)
六、 SET(_CMAKE_TOOLCHAIN_LOCATION /usr/local/toolschain/usr/bin)
七、 SET(_CMAKE_TOOLCHAIN_PREFIX arm-linux-)
八、 SET(CMAKE_C_COMPILER ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}gcc)
九、 SET(CMAKE_CXX_COMPILER ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}g++)
十、 SET(CMAKE_AR ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}ar)
十一、 SET(CMAKE_RANLIB ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}ranlib)
十二、 SET(CMAKE_STRIP ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}strip)
十三、 SET(CMAKE_LINKER ${_CMAKE_TOOLCHAIN_LOCATION}/${_CMAKE_TOOLCHAIN_PREFIX}ld)
十四、
十五、 # Specify searching libraries and includes only in target directory
十六、 SET(CMAKE_FIND_ROOT_PATH /usr/toolschain/usr)
十七、 SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
十八、 SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
十九、 SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
以上是设置交叉编译工具链的例程,不同的交叉编译工具链,需要改变CMAKE_SYSTEM_NAME,CMAKE_SYSTEM_PROCESSOR,_CMAKE_TOOLCHAIN_LOCATION(交叉编译工具链的路径),_CMAKE_TOOLCHAIN_PREFIX的值
三、cmake 命令
cmake命令不分大小写
1,AUX_SOURCE_DIRECTORY
2, INSTALL
定制安装规则
CMakeLists.txt 文件里添加下面两行:
1. # 指定安装路径
2. install (TARGETS Demo DESTINATION bin)
3. install (FILES "${PROJECT_BINARY_DIR}/config.h" DESTINATION include)
通过上面的定制,生成的 Demo 文件将会被复制到 /usr/local/bin 中, config.h 文件则会被复制到 /usr/local/include 中。运用cmake生成makefile之后,执行make命令之后,执行make install命令如下:
1. [ehome@xman Demo5]$ sudo make install
2. [ 50%] Built target MathFunctions
3. [100%] Built target Demo
4. Install the project...
5. -- Install configuration: ""
6. -- Installing: /usr/local/bin/Demo
7. -- Installing: /usr/local/include/config.h
8. [ehome@xman Demo5]$ ls /usr/local/bin
9. Demo
10. [ehome@xman Demo5]$ ls /usr/local/include
11. config.h
顺带一提的是,这里的 /usr/local/ 是默认安装到的根目录,可以通过修改 CMAKE_INSTALL_PREFIX 变量的值来指定这些文件应该拷贝到哪个根目录。
3,ADD_DEPENDENCIES
为顶层目标引入一个依赖关系。
add_dependencies(target-name depend-target1
depend-target2 ...)
让一个顶层目标依赖于其他的顶层目标。一个顶层目标是由命令ADD_EXECUTABLE,ADD_LIBRARY,或者ADD_CUSTOM_TARGET产生的目标。为这些命令的输出引入依赖性可以保证某个目标在其他的目标之前被构建。查看ADD_CUSTOM_TARGET和ADD_CUSTOM_COMMAND命令的DEPENDS选项,可以了解如何根据自定义规则引入文件级的依赖性。查看SET_SOURCE_FILES_PROPERTIES命令的OBJECT_DEPENDS选项,可以了解如何为目标文件引入文件级的依赖性。
4,ADD_CUSTOM_TARGET
添加一个目标,它没有输出;这样它就总是会被构建。
add_custom_target(Name [ALL] [command1[args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[WORKING_DIRECTORY dir]
[COMMENT comment] [VERBATIM]
[SOURCES src1 [src2...]])
用Name选项给定的名字添加一个目标,这个目标会引发给定的那些命令。这个目标没有输出文件,并且总是被认为是过时的,即使那些命令试图去创建一个与该目标同名的文件。使用ADD_CUSTOM_COMMAND命令可以生成一个带有依赖性的文件。默认情况下,没有目标会依赖于自定义目标。使用ADD_DEPENDENCIES命令可以添加依赖于该目标或者被该目标依赖的目标。如果指定了ALL选项,这表明这个目标应该被添加到默认的构建目标中,这样它每次都会被构建(命令的名字不能是ALL)。命令和选项是可选的;如果它们没有被指定,将会产生一个空目标。如果设定了WORKING_DIRECTORY参数,该命令会在它指定的路径下执行。如果指定了COMMENT选项,后跟的参数将会在构件的时候,在命令执行之前,被显示出来。DEPENDS选项后面列出来的依赖目标可以引用add_custom_command命令在相同路径下(CMakeLists.txt)生成的输出和文件。
如果指定了VERBATIM选项,所有传递到该命令的选项将会被合适地转义;这样,该命令调用的构建工具会接收到未经改变的参数。注意,CMake语言处理器会在add_custom_target命令在看到这些参数之前对它们进行一层转义。推荐使用该参数,因为它保证了正确的行为。当未指定该参数时,转义的行为依赖于平台,因为CMake没有针对于特定工具中特殊字符的保护措施。
SOURCES选项指定了会被包含到自定义目标中的附加的源文件。指定的源文件将会被添加到IDE的工程文件中,方便在没有构建规则的情况下能够编辑。
自己的简单理解:当用cmake 生成makefile之后, 使用
make name 命令之后(name为add_custom_target创建的目标,既add_custom_target(name [ALL] [command1[args1...]]
[COMMAND command2 [args2...] ...]
[DEPENDS depend depend depend ... ]
[WORKING_DIRECTORY dir]
[COMMENT comment] [VERBATIM]
[SOURCES src1 [src2...]])
的第一个参数name);[command1[args1...]], [command1[args1...]] 等等add_custom_target参数中的command选项都会被执行,且在[WORKING_DIRECTORYdir]指定的目录下被执行。
5,ADD_CUSTOM_COMMAND
为生成的构建系统添加一条自定义的构建规则。
add_custom_command命令有两种主要的功能;第一种是为了生成输出文件,添加一条自定义命令。
add_custom_command(OUTPUT output1 [output2...]
COMMAND command1 [ARGS][args1...]
[COMMAND command2 [ARGS][args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]]
[IMPLICIT_DEPENDS<lang1> depend1 ...]
[WORKING_DIRECTORY dir]
[COMMENT comment][VERBATIM] [APPEND])
这种命令格式定义了一条生成指定的文件(文件组)的生成命令。在相同路径下创建的目标(CMakeLists.txt文件)——任何自定义命令的输出都作为它的源文件——被设置了一条规则:在构建的时候,使用指定的命令来生成这些文件。如果一个输出文件名是相对路径,它将被解释成相对于构建树路径的相对路径,并且与当前源码路径是对应的。注意,MAIN_DEPENDENCY完全是可选的,它用来向visual studio建议在何处停止自定义命令。对于各种类型的makefile而言,这条命令创建了一个格式如下的新目标:
OUTPUT: MAIN_DEPENDENCY DEPENDS
COMMAND
如果指定了多于一条的命令,它们会按顺序执行。ARGS参数是可选的,它的存在是为了保持向后兼容,以后会被忽略掉。
第二种格式为一个目标——比如一个库文件或者可执行文件——添加一条自定义命令。这种格式可以用于目标构建前或构建后的一些操作。这条命令会成为目标的一部分,并且只有目标被构建时才会执行。如果目标已经构建了,该目标将不会执行。
add_custom_command(TARGET target
PRE_BUILD | PRE_LINK | POST_BUILD
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[WORKING_DIRECTORY dir]
[COMMENT comment] [VERBATIM])
这条命令定义了一个与指定目标的构建过程相关的新命令。新命令在何时执行,由下述的选项决定:
PRE_BUILD - 在所有其它的依赖之前执行;
PRE_LINK - 在所有其它的依赖之后执行;
POST_BUILD - 在目标被构建之后执行;
注意,只有Visual Studio 7或更高的版本才支持PRE_BUILD。对于其他的生成器,PRE_BUILD会被当做PRE_LINK来对待。
如果指定了WORKING_DIRECTORY选项,这条命令会在给定的路径下执行。如果设置了COMMENT选项,后跟的参数会在构建时、以构建信息的形式、在命令执行之前显示出来。如果指定了APPEND选项,COMMAND以及DEPENDS选项的值会附加到第一个输出文件的自定义命令上。在此之前,必须有一次以相同的输出文件作为参数的对该命令的调用。在当前版本下,如果指定了APPEND选项,COMMENT, WORKING_DIRECTORY和MAIN_DEPENDENCY选项会被忽略掉,不过未来有可能会用到。
如果指定了VERBATIM选项,所有该命令的参数将会合适地被转义,以便构建工具能够以原汁原味的参数去调用那些构建命令。注意,在add_custom_command能看到这些参数之前,CMake语言处理器会对这些参数做一层转义处理。推荐使用VERBATIM参数,因为它能够保证正确的行为。当VERBATIM未指定时,CMake的行为依赖于平台,因为CMake没有针对某一种工具的特殊字符采取保护措施。
如果自定义命令的输出并不是实际的磁盘文件,应该使用SET_SOURCE_FILES_PROPERTIES命令将该输出的属性标记为SYMBOLIC。
IMPLICIT_DEPENDS选项请求扫描一个输入文件的隐含依赖关系。给定的语言参数(文中的lang1—译注)指定了应该使用哪种编程语言的依赖扫描器。目前为止,仅支持C和CXX语言扫描器。扫描中发现的依赖文件将会在编译时添加到自定义命令中。注意,IMPLICIT_DEPENDS选项目前仅仅直至Makefile生成器,其它的生成器会忽略之。
如果COMMAND选项指定了一个可执行目标(由ADD_EXECUTABLE命令创建的目标),在构建时,它会自动被可执行文件的位置所替换。而且,一个目标级的依赖性将会被添加进去,这样这个可执行目标将会在所有依赖于该自定义命令的结果的目标之前被构建。不过,任何时候重编译这个可执行文件,这种特性并不会引入一个会引起自定义命令重新运行的文件级依赖。
DEPENDS选项指定了该命令依赖的文件。如果依赖的对象是同一目录(CMakeLists.txt文件)下另外一个自定义命令的输出,CMake会自动将其它自定义命令带到这个命令中来。如果DEPENDS指定了任何类型的目标(由ADD_*命令创建),一个目标级的依赖性将会被创建,以保证该目标在任何其它目标使用这个自定义命令的输出之前,该目标已经被创建了。而且,如果该目标是可执行文件或库文件,一个文件级依赖将会被创建,用来引发自定义命令在目标被重编译时的重新运行。
6, ADD_EXECUTABLE
7,ADD_LIBRARY
8,TARGET_LINK_LIBRARIES
9,LINK_DIRECTORIES
10,INCLUDE_DIRECTORIES
11,ADD_DEFINITIONS
12,SET
13, SET_TARGET_PROPERTIES
14,ADD_SUBDIRECTORY
15,TARGET_INCLUDE_DIRECTORIES
怎么理解这个命令
16, execute_process
execute_process (COMMAND <cmd1> [args1...]]
[COMMAND<cmd2> [args2...] [...]]
[WORKING_DIRECTORY<directory>]
[TIMEOUT<seconds>]
[RESULT_VARIABLE<variable>]
[OUTPUT_VARIABLE<variable>]
[ERROR_VARIABLE<variable>]
[INPUT_FILE<file>]
[OUTPUT_FILE<file>]
[ERROR_FILE <file>]
[OUTPUT_QUIET]
[ERROR_QUIET]
[OUTPUT_STRIP_TRAILING_WHITESPACE]
[ERROR_STRIP_TRAILING_WHITESPACE])
运行cmake 生成makefile的时候执行其所指定的指令 cmd1(执行这条指令时的参数为args1) cmd2等
execute_process命令是exec_program命令的一个较新的功能更加强大的版本。但是为了兼容性的原因,旧的exec_program命令还会继续保留
exec_program(cmd ARGScmdargs1 cmdargs2 ...)
17, exec_program
四、cmake 变量
五、camke流程控制
CMake提供三种程序流控制结构:
1. 条件声明:if
# some_command will be called if the variable'svalue is not:
# empty, 0, N, NO, OFF, FALSE, NOTFOUND, or -NOTFOUND.
if(var)
some_command(...)
endif(var)
2. 循环控制:foreach 和 while
set(VAR a b c)
# loop over a, b,c with the variable f
foreach(f ${VAR})
message(${f})
endforeach(f)
3. 程序定义:宏或函数(函数在CMake2.6以后的版本才支持)。函数建立本地范围内的变量,宏用于全局范围内。
# define a macro hello
macro(hello MESSAGE)
message(${MESSAGE})
endmacro(hello)
# call the macro with the string "hello world"
hello("hello world")
# define a function hello
function(hello MESSAGE)
message(${MESSAGE})
endfunction(hello)
译者注:
函数可以返回,可以用 return()命令返回。如果要从函数中返回值,只能通过参数返回:
#定义函数 get_lib从给定的目录查找指定的库,并把它传回到参数 lib_FILE中
function(get_lib lib_FILE lib_NAME lib_PATH)
#message("lib_name:""${lib_NAME}")
set(__LIB"__LIB-NOTFOUND")
#message("__LIB:""${__LIB}")
find_library(__LIB${lib_NAME} ${lib_PATH})
if(__LIB STREQUAL"__LIB-NOTFOUND")
message("don't find${lib_NAME} librarys in ${lib_PATH}")
return()
endif()
#message("__LIB:""${__LIB}")
set(${lib_FILE} ${__LIB}PARENT_SCOPE)
endfunction(get_lib)
set命令中 PARENT_SCOPE表示传递给函数调用者所拥有的变量