Cronet双端持续集成发布实践

背景

  • Cronet是chromium项目的网络协议栈,其编译产出环境需要Ubuntu系统加Chromium的编译工具。
  • 公司内部无法提供满足需求的编译发布服务,因此只能自建编译产出系统。
  • 团队已有的编译系统只能发布Cronet Android,随着iOS版开始接入产品,我们需要支持Cronet iOS的编译发布。
  • 已有的编译发布流程如下:
    cronet_old_compile
    注:icode为公司的代码托管平台,Agile为公司的持续集成及发布平台,这里不做详细描述。

其中,Agile可以允许用户自定义编译脚本,因此我们用python在团队自运维的机器上做了编译服务,当需要编译的时候由agile触发自定义脚本,自定义脚本再远程触发我们自建编译服务。待编译完成后再回收编译产物。

技术难点

  • 需要做到一次代码提交同时触发双端(Android & iOS)编译发布,并且经过调研,公司内部也没有产品线做双端同步编译发布,这应该是首创。
  • IDC内无Mac编译机,iOS编译发布需要自建Mac编译环境,IDC向办公区网络发起网络请求时,存在端口隔离,无法直接连通。

解决方案

  • 在现有编译server基础上,新增iOS编译流程控制,包括触发iOS编译及回收iOS产物。
  • 参考其他iOS端研发团队做法,使用公司提供的jenkins平台对iOS编译机进行调度,解决IDC与自建server无法直接连通问题。
    cronet_new_compile

如升级后的编译server,编译服务新增了iOS的编译控制。

对于技术难点2,为了解决办公区mac端口隔离,我们使用了jenkins,公司内部也提供了jenkins服务。jenkins的网络模式是客户机(mac编译机)启动jenkins客户端主动向服务端发起一个长连接。因此避免了端口隔离问题。

具体代码

这里仅列出新增的iOS编译控制相关代码
其中涉及到了python的jenkins库,jenkins在Python中的api可参见:python-jenkins

触发iOS编译
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
"""
Judge if we need ios build and start it.
"""
ios_task = CompileIosTask(self.data, self.buildnum, self.patchnum)
#根据代码库中build.py中的开关决定是否开启iOS build task
if self.data.get('cronet_ios', 'false').lower() == 'true':
    ios_build = True
    try:
        #开启额外的线程用于iOS编译,与编译android做法类似
        thread_ios = threading.Thread(target=ios_task.run)
        thread_ios.start()
        self.write_pipe.write('start ios build task success')
    except Exception as e:
        errmsg = 'start ios task to thread failed:' + str(e.message)
        print errmsg
        self.write_pipe.write(FAILED_MAGIC + '\n' * 256)
        return
 
 
 
#调用jenkins api 启动ios编译job并回收状态
class CompileIosTask(object):
    """Summary of class here
    Build the ios and get its artifact
    """
    def __init__(self, data, buildnum, patchnum):
        self.api = Jenkins(JENKINS_URL, username = JENKINS_USERNAME, password = JENKINS_PASSWORD)
        self.api.poll()
        self.data = data
        self.buildnum = buildnum
        self.patchnum = patchnum
        self.is_good = False
 
    def build(self):
        """Summary of function here
        Start the ios compilation until it finish
        """
        branch_name = self.data.get('branch', 'master').strip('\n').strip(' ')
        #启动job,并把commit id、版本号作为参数传递过去
        try:
            self.job = self.api.get_job(JENKINS_JOB)
            self.job.invoke(build_params = dict({'GIT_ID': branch_name, 'BUILD_NUM': self.buildnum, 'PATCH_NUM': self.patchnum}))
            sleep(1.)
        except Exception as e:
            print 'init jenkins job error:' + str(e.message)
            return False
 
        while self.job.is_queued():
            print 'jenkins queuing...'
            sleep(20.)
        # 回收状态
        try:
            self.build = self.job.get_last_build_or_none()
            if self.build is None:
                print 'get jenkins build empty'
                return False
        except Exception as e:
            print 'get jenkins build error:' + str(e.message)
            return False
 
        while self.build.is_running():
            print 'ios building...'
            sleep(20.)
 
        self.build = self.job.get_last_build()
        print 'The last build status is ' + str(self.build.get_status())
        return self.build.is_good()
回收iOS编译产物

由于Android 和 iOS编译不在同一台机器上,最终我们需要将产物回收合并到一起,因此需要通过jenkins平台拉取iOS的编译产物。

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
def get_artifact(self):
        """Summary of function here
        Get the artifact of ios from jenkins ftp
        """
        retry_cnt = 10
        ftp = FTP()
        timeout = 30
        host = '***.baidu.com'
        port = 8xxx
        output_dir = os.path.join(GIT_DIR, CRONET_OUTPUT_DIR)
        try:
            if os.path.exists(output_dir) == False:
                os.makedirs(output_dir)
        except Exception as e:
            print 'find output dir err:' + str(e.message)
            return False
 
        while True:
            #访问ftp服务器获取对应产物
            try:
                ftp.connect(host, port, timeout)
                ftp.login('userx', 'abcxxx.')
                print ftp.getwelcome()
                ftp.cwd('%s/%s/ios_out' % (JENKINS_JOB, self.build.get_number()))
                filepath = output_dir + '/turbonet_ios.zip'
                print 'the file path of ios artifact is: ' + filepath
                file = open(filepath, 'wb')
                ftp.retrbinary('RETR turbonet_ios.zip', file.write)
                break
            except Exception as e:
                #失败重试10次
                print 'Ftp error: ' + str(e.message)
                if retry_cnt > 0:
                    retry_cnt -= 1
                    sleep(2.)
                else:
                    print 'download artifact of job:%s failed' % self.build.get_number()
                    return False
        return True

总体收益

  • 该方案实现了android和iOS双平台的集成及发布,整个编译发布过程对组内开发者是透明的,开发者只需进行icode提交,及在agile上回收编译产物即可,操作体验上与之前无diff。
  • 该方案实现了android和iOS并行编译发布,相对于简单的agile串行编译,时间由T(Android) + T(iOS) 优化为 Max(T(Android), T(iOS))。
  • 方案注意到了兼容性和功能解耦,通过开关控制可做到android编译、android+iOS编译这些组合,方便一些特殊编译发布需求或者回滚,如果开发者提交了iOS功能之前的代码,也不会造成编译失败。