GYP是比Makefile更高层次的一种C/C++(其他语言未知)代码编译工具。通过编写GYP文件,可以生成多种类型的编译工程,如ninja、Makefile和VS工程。相比直接使用Makefile来说,GYP的可读性更强一些(除了括号挺多以外),而且可以同时生成linux下的Makefile和windows下的VS工程,比如我自己虽然是嵌入式开发,但利用GYP仍然可以用VS编写代码,然后在linux下用交叉编译器编译,这样开发起来方便多了。
GYP的语法格式说来也简单,GYP中的内容和JSON格式比较相似,那么需要注意的主要是GYP语法中的各个关键字的意义和使用方法。本文主要从C/C++编译原理和Makefile的角度来总结GYP的学习方法。GYP语法系统的说明可以参考这篇文章:
http://www.cnblogs.com/x_wukong/p/4829598.html
注:本文如无特别说明,本文所述的“代码”主要指的是C/C++代码,别的代码作者暂时没接触
无论是GYP还是Makefile,又或者是cmake。都只是用来管理编译规则的工具,比如哪些是需要编译的文件、哪些是已经编译的文件、哪些文件一起打成库、哪些文件和其他库一起编译成可执行程序以及编译参数编译宏等等。最终的编译、链接还是编译器来做的,代码编译规则的工具只是给编译器提供相应的信息而已。Makefile就是的存在就是这个目的,但由于Makefile中掺杂了许多linux下的脚本指令使其平台间可移植性很差。
接下来会根据使用Makefile编译时常遇到的一些应用场景来叙述GYP是怎么实现的,主要包括了以下几点:
与make有Makefile一样,GYP有对应的代码规则文件,由*.gyp和*.gypi等后缀的文件组成,在编译时GYP工具根据项目的总gyp文件生成对应平台的编译规则,如windows上生成VS2015的工程sln等文件、x86-linux和嵌入式-linux下生成Makefile文件。然后执行VS2015的build或者make完成编译。
由上所述,GYP编译环境布置需要安装一个GYP工具,可自行下载,这里介绍一个地址仅供参考:https://download.csdn.net/download/yb4141/9453028
下载下来之后可以发现GYP工具的实现是用python实现的,所以需要环境里有对应的python。在linux里和windows里GYP的安装都是将GYP工具的执行目录添加到环境变量里去,环境变量的添加这里不再叙述。
gyp指令生成各平台工程如下:
# linux 下调用gyp指令,gyp会根据传入的gyp文件生成对应的Makefile
export CC=gcc #设置C编译器
export CXX=g++ #设置c++编译器
gyp --depth=. -f make xxx.gyp
# windows 下调用gyp指令,gyp会根据传入的gyp文件生成VS的工程
gyp --depth=. xxx.gyp
如上gyp指令根据传入的gyp文件生成对应的工程,所以主要是gyp文件的编写,接下来开始逐步说明gyp文件的编写。
先说可执行程序的编译,不涉及到其他东西,假设可执行程序名称为test,由三个源文件:src1.c、src2.c、src3.c编译生成,在Makefile中写明target(test)、源文件和编译器指令执行make即可生成。而在GYP中也是一样只需要按它的规则提供target、sources和编译指令信息就ok。
指定编译器指令:在linux下,设置好CC环境变量即可,如export CC=gcc (C++用g++),如果是嵌入式平台则用对应的交叉编译器指令。windows写无须设置,GYP会生成VS工程。
然后编写GYP文件:
test.gyp:
{
'targets': [
{
'target_name': 'test', #指定编译的target名称,这里是可执行程序名称
'type': 'executable', #指定target的类型,这里是可执行程序
'sources': [ #指定target包含的代码源文件
'src1.c',
'src2.c',
'src3.c',
],
},
],
}
如果是要建立库文件如libtest.a的编译规则,则环境变量中还需设置ar指令环境变量,如:export AR=ar。然后GYP文件如下:
{
'targets': [
{
'target_name': 'libtest', #指定编译的target名称,这里是库文件名称无后缀
'type': 'static_library', #指定target的类型,这里是静态库,也可设置成动态库
'sources': [ #指定target包含的代码源文件
'src1.c',
'src2.c',
'src3.c',
],
},
],
}
以上就是最基本的可执行程序和库的编译规则编写。然后涉及到了几个GYP关键字解释如下(其实我觉得大多数关键字都是见名则知其意,这里就啰嗦一点):
targets : 基本上每一个GYP文件都包含这个关键字,targets内部描述了需要编译的各个target。
target_name : 指定targets中其中一个target的名称,本例中只有一个,可以建立多个target,只需按格式再来一段target成员即可。
type : 设置当前target的类型,type对应的值也是一系列关键字,我所知道比较常用的主要有以下几个:
executable : 可执行程序,在windows中就是exe文件。
static_library : 静态库,在linux中就是libxxx.a的文件。
shared_library: 动态库,即*.so文件。
none:无类型,作特殊用途。
sources : 设置生成target需要编译的源文件。
在应用开发过程中,当前可执行程序或库依赖其他库文件是常有的事。如前所述最终链接其他依赖库都是编译器做的,我们只需要给编译器提供库的名称和路径,如果是可执行程序依赖这个库还需要提供该库的接口头文件路径。在Makefile中直接在调用编译器指令的语句后追加了库名称、库文件路径和头文件路径来完成,在gyp中也需要提供以上信息才能完成编译和链接。但是考虑到可拓展性等有着不同的写法:
先说最简单的,当前需要编译一个名为test的可执行文件,包含了一个(也可多个)源文件main.c,同时依赖一个库文件,名叫libxx.a,源文件、库文件和头文件路径如下所示:
.
|-- main.c
`-- math
|-- libmath.a
`-- math.h
则gyp文件这样编写:
test.gyp:
{
'targets': [
{
'target_name': 'test',
'type': 'executable',
'sources': [
'main.c',
],
'include_dirs': [ # 指定libmath.a头文件路径
'math'
],
'libraries': [ # 指定链接的头文件路径和名称
'math/libmath.a'
],
'ldflags': [ # 设置链接参数
'-L./math' # 指定链接库的路径
],
},
],
}
关键字解释:
include_dirs : 指定头文件路径关键字,其成员可设置多个,在Makefile中相当于指定多个"-I.... -I..."的gcc参数
libraries : 指定外部库文件关键字,成员可设置多个,其成员可写成上面的形式,也可以写成-lmath。
ldflags : 指定链接参数关键字,成员可设置多个,在Makefile中相当于我们经常设置的LDFALAGS环境变量。
由上可知,gyp和Makefile链接外部库时做的事情都是一样的,即提供头文件路径、库名称和库的路径。但是上面的例子当中是把链接外部库的相关字段写到了test这个target中。如果工程中有其他多个target要调用该库时每个target都写一遍就很不合理了,于是我们可以将链接该库的字段封装到一个target中,需要依赖的其他target可以设置依赖项来完成。
对此test.gyp可以改写如下:
{
'targets': [
{
'target_name': 'libmaths',
'type':'static_library',
'direct_dependent_settings': { #设置依赖当前target的其他target需要设置的参数
'include_dirs': [ # 指定libmath.a头文件路径
'math'
],
'libraries': [ # 指定链接的头文件路径和名称
'math/libmath.a'
],
'ldflags': [ # 设置链接参数
'-L./math' # 指定链接库的路径
],
},
},
{
'target_name': 'test',
'type': 'executable',
'sources': [
'main.c',
],
'dependencies':[ # 设置当前target的依赖项
'libmaths', #依赖libmaths
],
},
],
}
关键字解释:
dependencies :指定当前target依赖项的关键字,依赖项成员可以多个。其依赖的只能是当前gyp或其他gyp文件中的target。如本例中的libmaths。
direct_dependent_settings : 指定依赖当前target的其他target需要设置的相关参数。如本例中test依赖libmaths,则libmaths中设置的direct_dependent_settings其实是为test设置的,其结果与上一个gyp文件一样。
上述两种写法是target依赖外部链接库的情况,这里还有一种情况是target依赖的库也是当前项目中的源码编译而来,在一中说明了怎么建立类型为库的target,而这里只需要将依赖项添加进去即可,例如当前目录下文件如下所示:
.
|-- main.c
`-- a_dir
|-- a.c
`-- a.h
a_dir/a.c 生成链接库liba.a(生成动态库可参考一中所述),main.c依赖liba.a生成可执行程序test。
则gyp文件如下:
{
'targets': [
{
'target_name': 'liba',
'type':'static_library',
'sources': [
'a_dir/a.c',
'a_dir/a.h',
],
'include_dirs': [
'a_dir'
],
'direct_dependent_settings': { #设置依赖当前target的其他target需要设置的参数
'include_dirs': [
'a_dir'
],
},
},
{
'target_name': 'test',
'type': 'executable',
'sources': [
'main.c',
],
'dependencies':[ # 设置当前target的依赖项
'liba',
],
},
],
}
在项目开发过程中,随着代码文件的不断增加和代码路径越来越复杂。一个gyp文件已经写得过于复杂了,这时便需要对各个路径下的各个模块分别写gyp文件,这就相当于对每个路径下都写一个Makefile一样,这样会使得编译规则更加模块化从而便于维护。但是使用多个编译规则文件时就会遇到一个问题,有许多编译规则和参数等是项目全局使用的,比如CFLAGS、LDFLAGS、编译宏、全局变量如最重要的工程的root路径,在Makefile中可以通过设置一些全局变量来实现,也可以通过include其他规则文件实现。在GYP文件中也同样可以使用这两种方法,下面用一个较复杂的例子统一说明:
样例源文件树如下:
.
|-- application
| `-- test
| `-- main.c
|-- base
| |-- base0.c
| |-- base0.h
| |-- base1.c
| `-- base1.h
|-- third_party
| `-- math
| |-- include
| | `-- math.h
| `-- libs
| |-- linux
| | `-- libmath.a
| `-- win32
| `-- libmath.a
`-- utils
|-- utils0.c
|-- utils0.h
|-- utils1.c
`-- utils1.h
在本例中可执行程序test由源文件main.c调用由base0.c base1.c生成的库libbase和由utils0.c utils1.c生成的库libutils和外部库文件libmath.a编译生成。且在linux下链接linux目录下的libmath.a在windows环境下链接win32下的libmath.a。utils0.c中会根据当前系统环境不同(如linux和windows)执行相应的条件编译。现在对此工程编写gyp规则文件。
在编写gyp文件前需要在工程根目录下编写一个脚本来设置GYP的环境变量,以便于对GYP传入当前编译环境和当前工程根目录的绝对路径。此脚本对于linux和windows下各有一个,linux下是shell脚本文件,windows下是批处理文件:
build_linux.sh:
#! /bin/bash
export CC=gcc
export CXX=g++
export AR=ar
export ROOT=`pwd`
export GYP_DEFINES="OS=linux root=${ROOT}" #定义GYP环境变量,传入根目录路径和系统类型
gyp --depth=. -f make all.gyp
build_windows.bat:
set cur_path=%~dp0
set "cur_path=%cur_path:\=/%"
set GYP_DEFINES=OS=win root=%cur_path% #定义GYP环境变量,传入根目录路径和系统类型
gyp --depth=. all.gyp
以上两个脚本都设置了一个环境变量:GYP_DEFINES,这个就是GYP默认引用的环境变量名称,在此环境变量的内容中通过空格隔开来传入多个GYP参数。
项目全局编译规则的设置写在一个规则文件中,其他gyp文件通过include改规则文件即可实现全局参数配置。在GYP中该文件以gypi为后缀,在本例中创建common.gypi在工程根目录下:
common.gypi:
{
'variables': {
'pro_root': '<(root)', #设置工程根目录,<(root)为应用在脚本中GYP_DEFINES中的root变量
},
'target_defaults': { #target默认配置
'conditions': [ #条件配置
['OS=="linux"',{
'defines':['LINUX'],
}],
['OS=="win"',{
'defines':['WIN32'],
}],
],
},
}
关键字解释:
variables : 参数设置关键字。设置当前文件中或者引用当前文件的文件中的参数。用于给各个target来引用,等同于Makefile中的变量。这里设置的是记录工程根目录绝对路径的变量pro_root。
target_defaults : target默认配置关键字,本例中所有include该文件的gyp文件中的所有target都会有该配置。
conditions : 条件配置关键字,通过判断GYP环境变量中的参数或者当前文件中定义的参数值来实现不同的编译配置。
defines : 宏定义关键字,定义编译时候的预处理宏,本例中若是在linux环境下编译则会有"LINUX"宏,若是在windows环境下则会有"WIN32"宏。
现在GYP环境变量设置脚本和全局参数配置文件都做好了,接下来开始编写各个GYP文件,工程中源文件加上编译脚本和GYP相关文件的目录结构如下:
.
|-- all.gyp
|-- build_linux.sh
|-- build_windows.bat
|-- common.gypi
|-- application
| `-- test
| |-- main.c
| `-- test.gyp
|-- base
| |-- base.gyp
| |-- base0.c
| |-- base0.h
| |-- base1.c
| `-- base1.h
|-- third_party
| `-- math
| |-- include
| | `-- math.h
| |-- libs
| | |-- linux
| | | `-- libmath.a
| | `-- win32
| | `-- libmath.a
| `-- math.gyp
`-- utils
|-- utils.gyp
|-- utils0.c
|-- utils0.h
|-- utils1.c
`-- utils1.h
各个GYP文件如下:
base.gyp:
{
'includes': [
'./../common.gypi',
],
'targets': [
{
'target_name': 'libbase',
'type':'static_library',
'sources': [
'base0.c',
'base0.h',
'base1.c',
'base1.h',
],
'include_dirs': [
'<(pro_root)/base',
],
'direct_dependent_settings': { #设置依赖当前target的其他target需要设置的参数
'include_dirs': [
'<(pro_root)/base',
],
},
},
],
}
math.gyp:
{
'includes': [
'./../../common.gypi',
],
'targets': [
{
'target_name': 'libmaths',
'type':'static_library',
'direct_dependent_settings': {
'include_dirs': [
'<(pro_root)/third_party/math/include',
],
'conditions': [
['OS=="linux"',{
'ldflags': [
'-L<(pro_root)/third_party/math/libs/linux',
],
'libraries': [
'<(pro_root)/third_party/math/libs/linux/libmath.a',
],
}],
['OS=="win"',{
'ldflags': [
'-L<(pro_root)/third_party/math/libs/win32',
],
'libraries': [
'<(pro_root)/third_party/math/libs/win32/libmath.a',
],
}],
]
},
},
],
}
utils.gyp:
{
'includes': [
'./../common.gypi',
],
'targets': [
{
'target_name': 'libutils',
'type':'static_library',
'sources': [
'utils0.c',
'utils0.h',
'utils1.c',
'utils1.h',
],
'include_dirs': [
'<(pro_root)/utils',
],
'direct_dependent_settings': {
'include_dirs': [
'<(pro_root)/utils',
],
},
},
],
}
test.gyp:
{
'includes': [
'./../common.gypi',
],
'targets': [
{
'target_name': 'test',
'type':'executable',
'sources': [
'main.c',
],
'dependencies':[
'<(pro_root)/base/base.gyp:libbase',
'<(pro_root)/utils/utils.gyp:libutils',
'<(pro_root)/third_party/math/math.gyp:libmaths.gyp',
],
},
],
}
all.gyp:
{
'includes': [
'common.gypi',
],
'targets': [
{
'target_name': 'pro_all',
'type':'none',
'dependencies':[
'<(pro_root)/application/test/test.gyp:test',
'<(pro_root)/base/base.gyp:libbase',
'<(pro_root)/utils/utils.gyp:libutils',
'<(pro_root)/third_party/math/math.gyp:libmaths.gyp',
],
},
],
}
关键字解释:
includes : 引用外部gypi文件关键字。
如上面的gyp规则,编译时会产生libbase、libutils。可执行文件test由main.c链接libbase.a、libutils.a和libmath.a生成。并且libutils.a能根据宏"LINUX"和"WIN32"进行条件编译。由上面的目录结构可以看出,GYP和Makefile做的工作还有组织方式都是差不多的。区别在于语法上。
以上描述的几个方面例子基本上可以满足日常开发中的常见场景。由上面的叙述能看出来,GYP的编写和Makefile的思路大体是一样的,所以如果是熟悉Makefile或其他规则文件的可以很快上手GYP,因为它的可读性确实比较强。作者开始接触这个东西的时候主要是多看别地儿写的gyp文件。从而知道自己想要描述的规则用GYP文件如何表达。
本文主要是将作者学习过程中运用到的例子叙述出来,相关关键字的解释也是作者自己的理解。如有不足或者不对的地方,欢迎在下方留言告知,谢谢!