• iOS持续集成


    最近这两天生完Xcode发现云可以用了,但是碍于把公司代码直接放到公网不太合适。但是也是一个机会偷偷自动化一下打包逻辑,毕竟人为操作的话,有几个时间节点需要等,年纪也大了,一打断就忘记接着干嘛了。还是能机器一气呵成的就机器来吧。
    之前的打包逻辑主要有以下几个步骤。

    1. 修改版本号、build号、项目名称等信息;
    2. 使用xcode执行build archive;
    3. 在organizer中导出包,手动上传到 fir.im;
    4. 在organizer中导出包,手动上传到testflight;

    按照这个思路逐步把逻辑脚本化,脚本的话当然还是用python比较好,人生苦短。

    文件结构

    主要有以下几个文件及文件结构:

    - cicd.py # 主要执行脚本
    - cicd # 放打包产物及配置等信息文件夹
       |- changelog.txt # 版本修改信息,可以在fir.im中查看
       |- ExportOptions.plist # 测试版本自动打包配置
       |- ReleaseExportOptions.plist # 正式版本自动打包配置
       |- build # 编译产物
       |- output # ipa包
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    修改版本号、build号、项目名称等信息

    使用到命令行工具 agvtool
    详细原理及配置参考链接https://blog.csdn.net/xo19882011/article/details/121204208
    配置好相关项目之后转到命令上来:

    agvtool new-marketing-version version # 相应的发布版本
    agvtool new-version -all buildnum # 相应的buildnum
    
    • 1
    • 2

    这样打版本的准备就做好了。

    打版本

    使用到命令行工具 xcodebuild,这个命令的详细介绍还是看帮助文档比较好,然后再结合不太理解的参数搜搜,也参考了一下别人的博客

    xcodebuild archive \
        -workspace xxx.xcworkspace \ # 项目里边一般都有cocospod,所以一般都是 xcworkspace
        -scheme xxx \ # debug 跟 release 不同,使用的时候采用不同的scheme
        -destination generic/platform=iOS \
        -configuration xxx \ # build setting中配置的configuration,一般来说可能是 Debug / Release
        -archivePath cicd/build/xxx.xcarchive \
        -allowProvisioningUpdates \
        -allowProvisioningDeviceRegistration
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后可能会报错,但是只要是产物在一般来说没啥问题。

    导出ipa

    使用到命令行工具 xcodebuild

    xcodebuild -exportArchive \
    	-archivePath cicd/build/debug/xxx.xcarchive \
    	-exportOptionsPlist cicd/ExportOptions.plist \
    	-exportPath cicd/output/debug/
    
    • 1
    • 2
    • 3
    • 4

    其中 ExportOptions.plist文件内容如下,也是对应着xcode导出包的时候每个步骤对应的配置,当前用到的是自动签名。

    
    
    
    
        compileBitcode
        
        method
        development
        signingStyle
        automatic
        stripSwiftSymbols
        
        teamID
        xxxx
        thinning
        <none>
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上传ipa到fir.im

    结合 fir.im 的文档 https://www.betaqr.com/docs,使用到发布应用那项。代码如下:

    print(f'\033[36m--- 上传 ---\033[0m')
    res = requests.post('http://api.bq04.com/apps', json={
      'type': 'ios',
      'bundle_id': 'com.xxxx', # bunldid
      'api_token': 'xxxx' # 相应的 fir.im token
    })
    print('token is ', res.text)
    res = json.loads(res.text)
    binary_key = res['cert']['binary']['key']
    binary_token = res['cert']['binary']['token']
    binary_upload_url = res['cert']['binary']['upload_url']
    icon_key = res['cert']['icon']['key']
    icon_token = res['cert']['icon']['token']
    icon_upload_url = res['cert']['icon']['upload_url']
    res = requests.post(icon_upload_url, data={
      'key': icon_key,
      'token': icon_token,
    }, files={'file': open('图标地址', 'rb')})
    print('upload icon res ', res.content)
    res = requests.post(binary_upload_url, data={
      'key': binary_key,
      'token': binary_token,
      'x:name': 'xxxx',
      'x:version': args.version,
      'x:build': buildnum,
      'x:changelog': open('cicd/changelog.txt', 'r').read(),
    }, files={'file': open('cicd/output/debug/xxxx.ipa', 'rb')})
    print('upload binary res ', res.content)
    
    • 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

    changelog记录一下版本更新内容,方便测试人员确认信息。

    上传 Test Flight

    使用到命令行工具 xcrun altool

      xcrun altool \
        --upload-app \
        -f cicd/output/release/xxx.ipa \
        -t ios \
        --apiKey xxx \
        --apiIssuer xxx \
        --verbose 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中apiKey及apiIssuer在在 app store connect 的用户和访问 -> 密钥中新建及查看。
    新建一个密钥,记录密钥 ID,并下载相应的私钥。把密钥放在以下目录其中之一中

    './private_keys'
    '~/private_keys'
    '~/.private_keys'
    '~/.appstoreconnect/private_keys'
    
    • 1
    • 2
    • 3
    • 4

    apiKey => 密钥ID
    apiIssuer => Issuer ID

    之后没啥问题就能直接上传了。

    完整脚本

    # coding = utf8
    import os
    import sys
    import argparse
    import requests
    import json
    import datetime
    
    WORKSPACE = 'xxxx.xcworkspace'
    BUNDLEID = 'com.xxxx'
    FIRIMTOKEN = 'xxxx'
    ICONPATH = 'Images.xcassets/AppIcon.appiconset/iphone1024.png'
    DISPLAYNAME = 'xxxx'
    TARGETNAME = 'xxxx'
    API_KEY = 'xxxx'
    API_ISSUER = 'xxxx'
    
    DEBUG_SCHEME = 'xxxx'
    DEBUG_CONFIGURE = 'Debug'
    DEBUG_ARCHIVEPATH = 'cicd/build/debug/xxxx.xcarchive'
    DEBUG_exportPath = 'cicd/output/debug/'
    DEBUG_exportOptionsPlist = 'cicd/ExportOptions.plist'
    DEBUG_archivePath = 'cicd/build/debug/xxxx.xcarchive'
    
    RELEASE_SCHEME = 'xxxxDistribute'
    RELEASE_CONFIGURE = 'xxxxDistribute'
    RELEASE_ARCHIVEPATH = 'cicd/build/release/xxxxRelease.xcarchive'
    RELEASE_exportPath = 'cicd/output/release/'
    RELEASE_exportOptionsPlist = 'cicd/ReleaseExportOptions.plist'
    RELEASE_archivePath = 'cicd/build/release/xxxxRelease.xcarchive'
    
    def clean():
      print(f'\033[36m--- 清理文档 ---\033[0m')
      os.system('rm -rf cicd/build/*')
      os.system('rm -rf cicd/output/*')
    
    def configuration(version, buildnum):
      print(f'\033[36m--- 设置版本号 ---\033[0m')
      os.system('agvtool new-marketing-version {}'.format(version))
      os.system('agvtool new-version -all {}'.format(buildnum))
    
    def build(mode):
      print(f'\033[36m--- 打版本 ---\033[0m')
      scheme = ''
      configure = ''
      archivePath = ''
      if mode == 'debug':
        scheme = DEBUG_SCHEME
        configure = DEBUG_CONFIGURE
        archivePath = DEBUG_ARCHIVEPATH
      else:
        scheme = RELEASE_SCHEME
        configure = RELEASE_CONFIGURE
        archivePath = RELEASE_ARCHIVEPATH
    
      os.system('''
        xcodebuild archive \
        -workspace {} \
        -scheme {} \
        -destination generic/platform=iOS \
        -configuration {} \
        -archivePath {} \
        -allowProvisioningUpdates \
        -allowProvisioningDeviceRegistration
      '''.format(WORKSPACE, scheme, configure, archivePath))
    
    def export(mode):
      print(f'\033[36m--- 打ipa版本 ---\033[0m')
      exportPath = ''
      exportOptionsPlist = ''
      archivePath = ''
      if mode == 'debug':
        exportPath = DEBUG_exportPath
        exportOptionsPlist = DEBUG_exportOptionsPlist
        archivePath = DEBUG_archivePath
      else:
        exportPath = RELEASE_exportPath
        exportOptionsPlist = RELEASE_exportOptionsPlist
        archivePath = RELEASE_archivePath
    
      os.system('''
        xcodebuild -exportArchive \
        -archivePath {} \
        -exportOptionsPlist {} \
        -exportPath {}
      '''.format(archivePath, exportOptionsPlist, exportPath))
    
    def uploadFim(buildnum):
      print(f'\033[36m--- 上传 ---\033[0m')
      res = requests.post('http://api.bq04.com/apps', json={
        'type': 'ios',
        'bundle_id': BUNDLEID,
        'api_token': FIRIMTOKEN
      })
      print('token is ', res.text)
      res = json.loads(res.text)
      binary_key = res['cert']['binary']['key']
      binary_token = res['cert']['binary']['token']
      binary_upload_url = res['cert']['binary']['upload_url']
      icon_key = res['cert']['icon']['key']
      icon_token = res['cert']['icon']['token']
      icon_upload_url = res['cert']['icon']['upload_url']
      res = requests.post(icon_upload_url, data={
        'key': icon_key,
        'token': icon_token,
      }, files={'file': open(ICONPATH, 'rb')})
      print('upload icon res ', res.content)
      res = requests.post(binary_upload_url, data={
        'key': binary_key,
        'token': binary_token,
        'x:name': DISPLAYNAME,
        'x:version': args.version,
        'x:build': buildnum,
        'x:changelog': open('cicd/changelog.txt', 'r').read(),
      }, files={'file': open('cicd/output/debug/{}.ipa'.format(TARGETNAME), 'rb')})
      print('upload binary res ', res.content)
    
    def uploadTestFlight():
      print(f'\033[36m--- 上传 Test Flight ---\033[0m')
      os.system('''
      xcrun altool \
        --upload-app \
        -f cicd/output/release/{}.ipa \
        -t ios \
        --apiKey {} \
        --apiIssuer {} \
        --verbose 
      '''.format(TARGETNAME, API_KEY, API_ISSUER))
    
    if __name__ == '__main__':
      parser = argparse.ArgumentParser(description='cicd parser')
      parser.add_argument('-v', type=str, dest='version', metavar='版本号', default='', required=True)
      parser.add_argument('-m', type=str, dest='mode', metavar='模式', default='debug')
      args = parser.parse_args()
      if args.version == '':
        exit(0)
    
      buildnum = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M')
      print('build num ', buildnum)
    
      # 清理
      clean()
      # 配置
      configuration(args.version, buildnum)
      # build
      build(args.mode)
      # export
      export(args.mode)
      
      if args.mode == 'debug':
        # 上传
        uploadFim(buildnum)
      else:
        # 上传testflight
        uploadTestFlight()
    
    • 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
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
  • 相关阅读:
    【Vue】ElementUI核心标签以及在Vue中的使用
    tmux的简单使用
    【华为OD机试真题 python】 水仙花数【2022 Q4 | 100分】
    大语言模型预训练数据集及清洗框架介绍【简单版】
    渗透测试逻辑漏洞挖掘思路
    通俗理解图神经网络GNN
    访问 github 问题解决方法
    【数据结构】二叉树必刷题
    [附源码]java毕业设计健身房管理系统论文2022
    【Hack The Box】windows练习-- Scrambled
  • 原文地址:https://blog.csdn.net/xo19882011/article/details/127998314