Cronet静态库编译解决方案

背景

由于公司产品需求,需要在iOS编译发布中支持静态库包,而我们默认的编译发布是动态库,chromium官方也是只支持动态库。

这个目标实际就是能使用ninja命令配合一定的编译参数就可以编译出静态库+在发布时能将静态产物抛给Agile。这里面预计设计两方面的改动:第一个就是我们的库代码,第二个是编译server。

解决方案

1.调研静态库编译方案

参考我们各个组件的编译方式(比如component(“net”)最终会编译出libnet.a),发现它们在产出目录中的.ninja文件中都是调用的alink(可参见build/toolchain/mac下tool(“alink”)),那么研究了一下alink的实现,实际上是调用了python build/toolchain/mac/filter_libtool.py libtool -static xxx TurboNet.rsp,这样的命令来生成静态库的,前提是要存在.rsp(ninja -d keeprsp即可产生,里面记录了该组件中所有的.o文件),那么大概也能猜出来执行这个命令,就是将.rsp中记录的.o文件全部打包最后生成.a。

我们实测执行这条命令也是可以顺利产出静态库,所以最早我的想法就是在我们的编译体系中(.gn文件)插入这么个命令,在gn中有action()、exec_script()可以用于执行脚本。不过实验了一会儿,发现实在不太好控制,原因是action、exe_script默认都是在编译最开始就会进行执行,而我的需要其实是要等到该组件所有.o文件+.rsp文件都产出了,再执行脚本,这个时序非常难控制,最后不得已放弃了。由此也可以看出gn这套google搞出来的编译系统,想要个人做一些灵活的改动(比如编译时序的更改)还是比较困难的。

然后注意到其实我们很多静态库都是使用static_library()这个gn中内置的工具来做的,因此把希望寄托在它身上,尝试了一下还是不行,原因在于static_library中只会把你定义为source的源文件给编译并打包成.a,所有的deps它完全不管,这不符合我们需求,我们想它能像shared_library一样将deps的东西也链接起来。

找了一些例子发现无论是gn中的componets 还是 static_library,在产出目录的.ninja中都能发现最后都落到了alink上(这种衔接在gn文件中找不到,应该是gn内部做的转换),而且alink后面还确实接了所有需要的.o。而我写的static_library在alink里面就只有source的那几个文件,所以总感觉还是有些猫腻,应该是可以有办法通过static_library使得alink能找到所有的.o的。

最后折腾了一番,在chromium论坛里找到了答案chromium.org/forum(这个帖子就是我发的[捂脸表情])
其实我们在static_library中加上complete_static_lib = true,即可编译出完整的静态库(包含所有deps)。

说到这里,觉得像类似gn这种,也算是一个规模不小的工具了,其中有蛮多技巧,在chromium也由专门的代码区供gn的commit,但确实平时也没精力去深入研究,只能是靠问题驱动。
附gn帮助文档:gn/reference.md,内容其实蛮多的。
解决了关键问题后,为了兼容现有编译以及满足不同编译需求,还做了一些措施,包括

  • 新增ios_release | debug.gn,编译server优先以该文件中的编译参数来进行编译。
    新增了is_turbonet_static开关,只有为true的时候才会编译出静态库。

  • 修改了build/config/ios/rules.gni中的template(“ios_framework_bundle”),当is_turbonet_static=true,执行static_library,否则执行默认的shared_library.

2.满足同时发布动态库 & 静态库需求

步骤1做完后,当时觉得就可以了,当时的想法是在平时的代码提交及合入,只编正常的动态库,在分支发布时,如果需要静态库,则要需要修改库代码的ios_debug | release.gn再push到icode触发agile编译,才能拿到静态库产出,实际上就是静态库、动态库需要分两次编译,并且对应两个不同的commit。结果这个方案遭到组里较多的challenge。

后面自己想想确实这样做非常不优雅,对RE来说也是非常难受的一件事,因为每次发布的时候,都需要多提交一次修改、多等待一次编译,最后产出还是分两次从agile上获取,如果我们某个版本由于比如修复bug发布次数多了的话,这个真有点逼死人的感觉。这个方案的唯一好处就是不会对现有的编译体系造成冲击,静态库也只是作为一个commit,各自独立,互不干涉。

权衡之后,还是决定去做动态库&静态库同时编译发布,这部分就主要是设计ios编译server的修改,包括如下:

  • 1.新增静态库编译脚本compile_static.py,专门用于静态库编译。
    当然,由于静态库 & 动态库都编译会导致编译时间double(这种就是对现有编译体系造成冲击的最大体现),因此又做了个改进,判断一下当前commit是否在分支上,因为我们是主干开发+分支发布,平时提交评审&合入主干,就不要执行这种对于的编译步骤了,可以节约挺多时间的。
    另外为什么我没有把静态编译放在现有编译脚本里(对应编译server的compile_task.py),一方面是现在compile_task.py中的步骤已经比较繁琐,而且每个compile_task当初设计就是为了一次编译,如果强行再附加静态编译,并不是很好添加。另外一方面觉得这个东西本身就是一个多余 & 临时的东西(只是因为我们现在需要同时发布两种),不应该长久存在,因此就单独写,不去影响正常流程。

  • 2.在库代码中新增baidu/build目录,其中存放一些特殊的gn args文件,比如现在就存放着给手百的静态库args,步骤1中的compile_static.py执行时就会去寻找库代码baidu/build下的args,用它来进行编译。这样做也是避免把编译args写死在编译服务端,为以后可能的编译配置修改打个基础。

  • 3.在jenkins平台配置编译步骤,在正常编译完成后,执行该脚本尝试静态库编译。并在最后分不同文件夹兜住所有的静态 & 动态编译产出。
    由于jenkins平台可以灵活的增删编译步骤、执行脚本等,因此借用它的能力,把动态库 & 静态库的编译 & 打包做出来了,而不是把这坨都交给编译server去做,尽量减轻编译server的负担,由于jenkins平台实际上是一个具有上帝视角的角色,放在这上面,感觉看起来也很舒服。

以下为compile_static.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Author:
# Created Time: 2017/03/31 19:45
# File Name: compile_static.py
# Description:
#
"""
Description: it is used for ios static library compilation.
"""
# Copyright (c) 2015 Baidu.com, Inc. All Rights Reserved
#
import os
import fcntl
import sys
import signal
import subprocess
import shutil
from time import sleep
from config import GIT_DIR
from config import GN_ARGS
mbdbox_release = 'baidu/build/ios_mbdbox_release.gn'
mbdbox_debug = 'baidu/build/ios_mbdbox_debug.gn'
def is_on_branch():
"""Summary of function here
Judge if the commit is in branch
"""
cmd = 'git rev-parse --verify HEAD'
p = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
p.wait()
if p.returncode == 0:
commit_hash = p.stdout.read().strip('\n').strip()
else:
print 'get local head commit failed'
return False
cmd = 'git branch -r --contains %s | grep -v master' % commit_hash
p = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
p.wait()
return p.returncode == 0 and 'origin' in p.stdout.read().strip('\n').strip()
def non_block_read(output):
"""Summary of function here
Read the popen status with non block
"""
fd = output.fileno()
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
try:
return output.read()
except:
return ''
def run_command(cmd):
"""Summary of function here
Run command
"""
cmd_env = os.environ.copy()
cmd_env['LANG'] = 'en_US.UTF-8'
cmd_env['LC_CTYPE'] = 'en_US.UTF-8'
cmd_env['LC_ALL'] = 'en_US.UTF-8'
p = subprocess.Popen(cmd, shell=True,
preexec_fn=os.setsid,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=cmd_env)
while p.poll() is None:
data = non_block_read(p.stdout)
data += non_block_read(p.stderr)
if (data):
print data
sleep(0.5)
data = non_block_read(p.stdout)
data += non_block_read(p.stderr)
if data:
print 'data: ' + data
return p.returncode
def Main():
"""Summary of function here
Main function
"""
os.chdir(GIT_DIR)
if is_on_branch() == False:
print 'It is the commit which not in branch'
return
if os.path.exists('ios_debug.gn') == False:
print 'The code is not adapted for static library'
return
try:
shutil.copy(mbdbox_release, 'out/Cronet/' + GN_ARGS)
shutil.copy(mbdbox_debug, 'out/Cronet/Debug/' + GN_ARGS)
except Exception as e:
print 'Error in copy static build config: ' + str(e)
sys.exit(-1)
ret_codes = []
cmds = [
'gn gen out/Cronet',
'gn gen out/Cronet/Debug',
'ninja -d keeprsp -C out/Cronet -j 16 cronet_package',
'ninja -d keeprsp -C out/Cronet/Debug -j 16 cronet_package',
]
for cmd in cmds:
ret_codes.append(run_command(cmd))
for ret in ret_codes:
if ret != 0:
print 'some error happened'
sys.exit(-1)
if __name__ == "__main__":
Main()%