GYP and Chrome

一、简介

GYP是google的一套构建系统,和 cmake 的目的很像。GYP和CMake的主要作用是,从用户编写的一套配置文件,针对不同的工具链生成不同的项目文件(如Makefile/vc projects/xcode projects)。

GYP安装:

svn co http://gyp.googlecode.com/svn/trunk gyp
cd gyp
./setup.py build
./setup.py install

下面是GYP的配置文件的示例:

{
  'targets': [
    {
      'target_name': 'hello',
      'type': 'executable',     
      'sources': [
        'main.cpp'
      ]    
    }
  ]
}

看上去基本就是一个json。它定义了一个名为hello的target。target的类型是executable,表明它是一个可执行文件。如果要编译库,就换成static_library或者shared_library。sources是一个数组,列出所有的源文件。

然后用

# gyp hello.gyp --depth=. -f make

生成makefile。

然后执行make命令即可。

从设计目标上来说,它和autotools还不大一样。autotools只针对make,且仅限于gnu make。autotools的核心是autoconf,如何利用shell脚本在不同的操作系统环境下生成相应的config.h文件。它利用它强大的检测功能很容易适应不同的Unix/Linux环境。

而GYP和CMake都支持各种主流的构建系统。如make/Visual studio/XCode/Ninja。CMake支持构建系统的种类要更多些,比如eclipse cdt、Sublime Text 2、CodeBlocks、Borland C++等等。而这些非主流的东西GYP压根就不会去碰,不敢碰。

按最理想的情况,我们写一套配置规则,在所有平台上都能执行。此时我们可以不关心操作系统是什么。拿autotools来说,假如你要include某个头文件,那么就在autoconf执行的时候检查下有没有这个头文件,然后在真正使用的时候,利用ifdef/else/endif来条件编译,假如有,我们就用它,没有就砍掉某个功能,或者使用替代实现。由于unix种类甚多,差异甚大,按照这样方式写的程序,即便被扔到一个作者从不知晓的陌生环境里,(也许)也能正常运行。

cmake和autotools都是这样的理想主义者,它们试图把不同的工具链的相同部分抽象出来,用一套统一的配置文件来适应不同的平台。迫不得已的时候你可以写 if(OS==WIN) ... else if (OS== Linux) ... 。 于是就这样工作了很多年,很好。而GYP觉得我们不该做这样的过分抽象,构建系统自身应该简洁,把适应不同平台的事情交给程序员自己去做。比如,不同的工具链参数不同,gcc编译多线程程序的时候要加-pthread或-pthreads或-lpthread,而vc则要在4种不同的CRT做出选择。那么,你自己去把不同平台的编译参数挨个标明,GYP不管这事。所以,GYP的项目不可能盲目的去支持Sublime Text 2、eclipse cdt这样的小众玩意儿。为了支持它们,condition会急剧膨胀。

GYP是为Chrome项目开发的,Chrome也是GYP的唯一成功案例。就比如前面编译hello world的时候,加上"--depth=. ",这完全是Chrome的特殊遗留。Chrome是一个有600多万行代码的大型C++项目,它的成功值得借鉴。

虽然GYP和CMake相比还很不成熟,而且很不独立(它几乎是专为Chrome项目服务),但是Chrome本身其实已经给我们贡献了足够多的代码。虽然GYP不像CMake那样自带了很多Module(丰富的FindXXX),但是我们完全可以去Chrome的项目中把那些GYP文件挖出来。

另外,如果你想复用Chrome的代码,那么就得迁就GYP。比如apache的mod_spdy模块,它的SPDY协议的实现就是从Chrome中直接拿去的。为了引用Chrome的代码,mod_spdy就不得不采用GYP做构建。

二、.gyp 文件的格式说明。

.gyp文件基本上就是一个json文件,和标准的json相比,它有两点不同:

  1. 可以用#注释

  2. list或dictionary的最后一个元素后面可以多一个逗号 (便于用程序自动生成这样的文件)

在它最顶层的大括号内,有5种对象:variables、includes、target_defaults、targets、conditions。

  • 'variables': Definitions of variables that can be interpolated and used in various other parts of the file.
  • 'includes': A list of of other files that will be included in this file. By convention, included files have the suffix .gypi (gyp include).
  • 'target_defaults': Settings that will apply to all of the targets defined in this .gyp file.
  • 'targets': The list of targets for which this .gyp file can generate builds. Each target is a dictionary that contains settings describing all the information necessary to build the target.
  • 'conditions': A list of condition specifications that can modify the contents of the items in the global dictionary defined by this .gyp file based on the values of different variablwes. As implied by the above example, the most common use of a conditions section in the top-level dictionary is to add platform-specific targets to the targets list.

Chrome在跨平台问题上采用了一个很有趣的事情,把第三方库的源代码copy到现有项目中,并且静态链接进来。相当于,Chrome为它所有用到的第三方库都做了SVN一个分支,在需要的时候与上游同步。然后它为所有的第三方库都生成了一个GYP项目文件,然后在Chrome中引用这些第三方库的项目文件,完成构建。比如,不管你操作系统有没有装libevent版本是多少,我都用我自己带的这个,然后最终链接成一个无比巨大的exe或ELF文件。

我非常赞同Chrome的这种做法。有些第三方库的接口变动非常快,比如glib,单单为了在linux这一种操作系统下适应不同的glib版本,就得在代码中写大量的条件编译宏。查core dump的时候也困难很多,单是找源代码都能找死一批人(别忘了发行版喜欢 编译前打自己的patch)。另一点是,构建系统本身也得以简化,我不需要去把autoconf/automake/cmake/scons等等全装一遍。最可恨的是,autotools自身还有很多版本,而且各不兼容。要把不同版本的autotools全装上,并且用起来互不干扰,也需要很大技巧。

下面以glib为例演示下如何在一个项目中包含另一个项目。

首先从svn中checkout出zlib的代码

svn co http://src.chromium.org/svn/trunk/src/third\_party/zlib

然后把下面代码保存为test.cpp

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "zlib.h"

#define CHUNK 16384

/* Compress from file source to file dest until EOF on source.
   def() returns Z_OK on success, Z_MEM_ERROR if memory could not be
   allocated for processing, Z_STREAM_ERROR if an invalid compression
   level is supplied, Z_VERSION_ERROR if the version of zlib.h and the
   version of the library linked do not match, or Z_ERRNO if there is
   an error reading or writing the files. */

int main(int argc, char** argv) {
  FILE* source = stdin;
  FILE* dest = stdout;
  int level = Z_DEFAULT_COMPRESSION;

  int flush;
  unsigned have;
  z_stream strm;
  unsigned char in[CHUNK];
  unsigned char out[CHUNK];

  /* allocate deflate state */
  strm.zalloc = Z_NULL;
  strm.zfree = Z_NULL;
  strm.opaque = Z_NULL;
  int ret = deflateInit(&strm, level);
  if (ret != Z_OK) return ret;

  /* compress until end of file */
  do {
    strm.avail_in = fread(in, 1, CHUNK, source);
    if (ferror(source)) {
      deflateEnd(&strm);
      return -1;
    }
    flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;
    strm.next_in = in;

    /* run deflate() on input until output buffer not full, finish
       compression if all of source has been read in */
    do {
      strm.avail_out = CHUNK;
      strm.next_out = out;
      ret = deflate(&strm, flush);   /* no bad return value */
      assert(ret != Z_STREAM_ERROR); /* state not clobbered */
      have = CHUNK - strm.avail_out;
      if (fwrite(out, 1, have, dest) != have || ferror(dest)) {
        (void) deflateEnd(&strm);
        return Z_ERRNO;
      }
    } while (strm.avail_out == 0);

    /* done when last data in file processed */
  } while (flush != Z_FINISH);

  /* clean up and return */
  deflateEnd(&strm);
  return 0;
}

然后把前面的hello.gyp稍作修改

{
  'targets': [
    { 
      'target_name': 'hello',
      'type': 'executable',     
      'sources': [
        'test.cpp'
      ],
      'dependencies': [
        'zlib/zlib.gyp:zlib'
      ]    
    }
  ]
}

就是在target中加入了一个"dependencies"节。它引用了zlib/zlib.gyp这个文件中的zlib这个target。

然后生成makefile

$ gyp hello.gyp --depth=. -D OS=linux -D os_bsd=0 -D clang=0 -f make

加入了一些新的define,是因为zlib/zlib.gyp中用到了这些variable。

然后make

$ make

用ldd看一下生成的结果文件会发现,

# ldd out/Default/hello 
        linux-vdso.so.1 =>  (0x00007fff87c1c000)
        libstdc++.so.6 => /lib64/libstdc++.so.6 (0x0000003b0d000000)
        libm.so.6 => /lib64/libm.so.6 (0x0000003b07000000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003b08400000)
        libc.so.6 => /lib64/libc.so.6 (0x0000003b06400000)
        /lib64/ld-linux-x86-64.so.2 (0x0000003b06000000)

它不依赖于zlib的so/dll。zlib已经被静态链接进去了。

这样当需要在服务器上部署的时候就很容易了。不用再总是去编译、安装第三方的库。

此博客中的热门博文

在windows下使用llvm+clang

少写代码,多读别人写的代码

tensorflow distributed runtime初窥