iOS自動(dòng)打包(升級(jí)版)

之前簡(jiǎn)單寫過一個(gè)iOS自動(dòng)打包腳本,在實(shí)際使用過程中逐步增加了一些新的feature:

  • 為方便使用,將部分參數(shù)做成可選并提供默認(rèn)值;

  • 打adhoc包自動(dòng)讀取最近的提交日志,最多30條;參考時(shí)間線是最后一次更新adhoc包的時(shí)間;

  • app內(nèi)環(huán)境切換開關(guān)不依賴Debug/Release,通過腳本控制自定義預(yù)編譯宏來(lái)把控;

  • 打線上包時(shí),自動(dòng)從主工程同步version和build到Notification;

  • xcode11去掉了Loader.app, xcrun altool增加了驗(yàn)證參數(shù);

都是一些簡(jiǎn)單的feature,大概說(shuō)一下實(shí)現(xiàn)吧:

1、參數(shù)解析,由原來(lái)的sys.argv改成argparse,可設(shè)置可選參數(shù)、默認(rèn)參數(shù)、固定參數(shù)值選項(xiàng);

2、讀取git提交日志,使用第三方python庫(kù)git,這里順便做了個(gè)簡(jiǎn)單的過濾(受益于提交規(guī)范化);

3、腳本控制自定義預(yù)編譯宏來(lái)控制app內(nèi)環(huán)境切換,借助pbxproj庫(kù)來(lái)修改project.pbxproj中的配置(這里有個(gè)小問題,覆寫pbxproj文件xcode會(huì)識(shí)別到文件損壞,但是覆寫完成后xcode并不能自動(dòng)恢復(fù)正常,需要重啟xcode);

4、版本號(hào)同步,首先從蒲公英讀取最新的build號(hào),然后+1設(shè)置到當(dāng)前的build,借用plistlib修改plist文件;

5、xcode11沒有了Loader.app,上傳和驗(yàn)證app直接用xcrun altool,另外增加了apiKey和apiIssuer參數(shù)(還有個(gè)額外的p8文件需要放置到當(dāng)前用戶目錄下"~/.appstoreconnect/private_keys/AuthKey_xxx.p8"),原先的用戶名和密碼不需要了;

具體代碼如下(代碼中的xxx部分做了脫敏):

import argparse
import os
import plistlib
import re
import shutil
import sys
import time
import zipfile
import git
import requests
from pbxproj import XcodeProject

# iOS項(xiàng)目群機(jī)器人
DING_ROBOT_URL1 = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'

# 測(cè)試群機(jī)器人
DING_ROBOT_URL2 = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'

# iOS提審交流群機(jī)器人
DING_ROBOT_URL3 = 'https://oapi.dingtalk.com/robot/send?access_token=xxx'


# 蒲公英設(shè)置
USER_KEY = "xxx"
API_KEY = "xxx"
APP_KEY = "xxx"

# appstore
APPSTORE_API_KEY = 'xxx'
ISSUER_ID = 'xxx'

# 上一次上傳app時(shí)間戳
lastBuildCreated = 0


def syncBuildVersionWithPGY():
    success = False
    global lastBuildCreated
    url = 'http://www.pgyer.com/apiv2/app/view'
    data = {
        '_api_key': API_KEY,
        'appKey': APP_KEY
    }
    r = requests.post(url, data=data)
    if r.status_code == 200:
        json = r.json()
        buildVersion = json['data']['buildBuildVersion']
        lastBuildCreated = json['data']['buildCreated']
        # 修改工程build號(hào)
        infoPlistPath = os.path.join(args.workPath, 'xxx/Info.plist')
        if os.path.exists(infoPlistPath):
            rewriteBuildVersion(infoPlistPath, str(int(buildVersion) + 1))
            success = True
        else:
            print(infoPlistPath, '不存在!')
    else:
        print('獲取蒲公英最新build失敗...')
    # 上一次上傳app時(shí)間戳
    if lastBuildCreated:
        lastBuildCreated = time.mktime(time.strptime(lastBuildCreated, '%Y-%m-%d %H:%M:%S')) - 2 * 60
    return success


def rewriteBuildVersion(infoPlist, buildNumStr):
    with open(infoPlist, 'rb') as f:
        plist = plistlib.load(f)
        plist['CFBundleVersion'] = buildNumStr
        appVersion = plist['CFBundleShortVersionString']

    with open(infoPlist, 'wb') as f:
        plistlib.dump(plist, f)

    # 打線上包,同步Notification的版本號(hào)
    infoPlistPath = os.path.join(args.workPath, 'Notification/Info.plist')
    if args.distribution == 'app-store' and os.path.exists(infoPlistPath):
        with open(infoPlistPath, 'rb') as f:
            plist = plistlib.load(f)
            plist['CFBundleVersion'] = buildNumStr
            plist['CFBundleShortVersionString'] = appVersion

        with open(infoPlistPath, 'wb') as f:
            plistlib.dump(plist, f)


def rewriteXcodePrecompileMacro():
    """
    覆寫工程配置
    1、ad-hoc包,支持APP內(nèi)切換環(huán)境 (MT_ENV_SWITCHABLE=1);
    2、app-store包,不支持APP內(nèi)切換環(huán)境 (MT_ENV_SWITCHABLE=0);
    """
    project = XcodeProject.load(os.path.join(args.workPath, 'xxx.xcodeproj/project.pbxproj'))
    buildConfigurations = project.get_build_phases_by_name('XCBuildConfiguration')
    buildSettings = None
    # print(buildConfigurations)
    for configuration in buildConfigurations:
        if args.distribution == 'ad-hoc' and args.build == configuration.name:
            # 內(nèi)測(cè)包
            buildSettings = configuration.buildSettings
        elif args.distribution == 'app-store' and configuration.name == 'Release':
            # 線上包
            buildSettings = configuration.buildSettings

        # 覆寫預(yù)編譯宏
        if buildSettings and buildSettings['SDKROOT']:
            if not buildSettings['GCC_PREPROCESSOR_DEFINITIONS']:
                # None
                if args.distribution == 'ad-hoc':
                    buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = 'MT_ENV_SWITCHABLE=1'
                else:
                    buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = 'MT_ENV_SWITCHABLE=0'
            elif type(buildSettings['GCC_PREPROCESSOR_DEFINITIONS']) == str:
                # 字符串
                if buildSettings['GCC_PREPROCESSOR_DEFINITIONS'].startswith('MT_ENV_SWITCHABLE') \
                        or len(buildSettings['GCC_PREPROCESSOR_DEFINITIONS'].strip()) == 0:
                    if args.distribution == 'ad-hoc':
                        buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = 'MT_ENV_SWITCHABLE=1'
                    else:
                        buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = 'MT_ENV_SWITCHABLE=0'
                else:
                    if args.distribution == 'ad-hoc':
                        buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = [buildSettings['GCC_PREPROCESSOR_DEFINITIONS'], 'MT_ENV_SWITCHABLE=1']
                    else:
                        buildSettings['GCC_PREPROCESSOR_DEFINITIONS'] = [buildSettings['GCC_PREPROCESSOR_DEFINITIONS'], 'MT_ENV_SWITCHABLE=0']
            else:
                # 數(shù)組
                for setting in buildSettings['GCC_PREPROCESSOR_DEFINITIONS']:
                    if setting.startswith('MT_ENV_SWITCHABLE'):
                        # 刪除舊設(shè)置
                        buildSettings['GCC_PREPROCESSOR_DEFINITIONS'].remove(setting)
                        break
                # 添加新設(shè)置
                if args.distribution == 'ad-hoc':
                    buildSettings['GCC_PREPROCESSOR_DEFINITIONS'].append('MT_ENV_SWITCHABLE=1')
                else:
                    buildSettings['GCC_PREPROCESSOR_DEFINITIONS'].append('MT_ENV_SWITCHABLE=0')
    project.save()
    # 覆寫配置文件后需重啟xcode
    os.system('killall Xcode')
    time.sleep(2)
    os.system('open %s' % (os.path.join(args.workPath, 'xxx.xcworkspace')))


def cleanResult():
    if os.path.exists(export_path):
        shutil.rmtree(export_path)


def archive():
    os.chdir(args.workPath)
    if args.distribution == 'ad-hoc':
        plistName = 'adhoc_config.plist'
        configuration = args.build

    else:
        plistName = 'appstore_config.plist'
        configuration = 'Release'

    # clean
    os.system('xcodebuild clean -workspace xxx.xcworkspace -scheme xxx -configuration %s' % configuration)
    # archive
    os.system('xcodebuild archive -workspace xxx.xcworkspace -scheme xxx -configuration %s -archivePath %s' % (configuration, xcarchive_path))
    # export
    os.system('xcodebuild -exportArchive -archivePath %s -exportPath %s '
              '-exportOptionsPlist %s' % (xcarchive_path, export_path, os.path.join(bundlePlistDir(), plistName)))


def bundlePlistDir():
    if getattr(sys, 'frozen', False):
        # we are running in a PyInstaller bundle
        basedir = sys._MEIPASS
    else:
        # we are running in a normal Python environment
        basedir = os.path.dirname(__file__)
    return os.path.join(basedir, 'plist')


def uploadToPGY():
    print('開始上傳至蒲公英...')
    ipa_path = os.path.join(export_path, 'xxx.ipa')
    if os.path.exists(ipa_path):
        url = 'https://qiniu-storage.pgyer.com/apiv1/app/upload'
        data = {
            'uKey': USER_KEY,
            '_api_key': API_KEY,
            'installType': '1'
        }
        files = {'file': open(ipa_path, 'rb')}
        r = requests.post(url, data=data, files=files)
        if r.status_code == 200:
            responseJSON = r.json()
            sendMessageForAdhoc(responseJSON['data']['appVersion'], responseJSON['data']['appBuildVersion'])
            print('上傳成功!')
        else:
            print('上傳失?。。?!')
            print(r.content)
        cleanResult()
    else:
        print('目標(biāo)ipa不存在')


def uploadToAppStore():
    ipa_path = os.path.join(export_path, 'xxx.ipa')
    if os.path.exists(ipa_path):
        # validate
        print('開始驗(yàn)證App...\n')
        command = 'xcrun altool --validate-app -f %s --apiKey %s --apiIssuer %s -t ios --output-format xml' \
                  % (ipa_path, APPSTORE_API_KEY, ISSUER_ID)
        ret = os.system(command)
        if ret != 0:
            print('App驗(yàn)證失敗!!!')
            cleanResult()
            exit(0)

        # upload
        print('開始上傳至itunes connect...')
        command = 'xcrun altool --upload-app -f %s  --apiKey %s --apiIssuer %s -t ios --output-format xml' \
                  % (ipa_path, APPSTORE_API_KEY, ISSUER_ID)
        ret = os.system(command)
        if ret == 0:
            plist = retrievePlistFromIpa(ipa_path)
            sendMessageForAppstore(plist['CFBundleShortVersionString'], plist['CFBundleVersion'])
            print('上傳成功!')
        else:
            print('上傳失敗!!!')
        cleanResult()
    else:
        print('目標(biāo)ipa不存在')


def retrievePlistFromIpa(ipa_path):
    ipa_file = zipfile.ZipFile(ipa_path)
    plist_path = findPlistPath(ipa_file)
    plist_data = ipa_file.read(plist_path)
    plist_root = plistlib.loads(plist_data)
    return plist_root


def findPlistPath(zip_file):
    name_list = zip_file.namelist()
    pattern = re.compile(r'Payload/[^/]*.app/Info.plist')
    for path in name_list:
        m = pattern.match(path)
        if m is not None:
            return m.group()


def sendMessageForAdhoc(version, build):
    # 讀取最近的變更日志
    logs = readRecentCommitLogs(lastBuildCreated)
    msg = 'iOS內(nèi)測(cè)版又雙叒叕更新啦!!!\n版本: %s (build %s)\n下載地址: https://www.pgyer.com/xxx' % (version, build)
    if len(logs) > 0:
        msg += '\n\n更新日志:\n'
    for log in logs:
        msg += log + '\n'
    # at群里的測(cè)試
    data = {'msgtype': 'text', 'text': {'content': msg}, 'at': {"atMobiles": ['xxx']}}
    requests.post(DING_ROBOT_URL1, json=data)


def sendMessageForAppstore(version, build):
    msg = '最新發(fā)布版已上傳至itunes connect!\n版本: %s (build %s)' % (version, build)
    data = {'msgtype': 'text', 'text': {'content': msg}}
    requests.post(DING_ROBOT_URL3, json=data)


def validLog(log):
    if re.match('^feat|fix|chore|perf|refactor\s*[:, :].+', log):
        return True
    return False


def readRecentCommitLogs(limitTimestamp):
    repo = git.Repo(os.path.join(args.workPath, '../'))
    tmpCache = 'commitLog.temp'
    logs = []
    # 最多顯示30條有效日志
    countLimit = 30
    if limitTimestamp <= 0:
        # 當(dāng)限制時(shí)間線無(wú)效時(shí),最多顯示15條有效日志
        countLimit = 15
    #
    with open(tmpCache, 'w+') as f:
        # 拉取日志并緩存
        for item in repo.iter_commits():
            if item.committed_date <= limitTimestamp:
                break
            if item.message.startswith('Merge pull request') or item.message.startswith('Merge branch'):
                continue
            f.write(item.message)

        # 從緩存中過濾日志
        f.seek(0)
        while True:
            logStr = f.readline()
            if not logStr:
                break
            logStr = logStr.strip()
            if validLog(logStr):
                logs.append(logStr)
                print(logStr)
                if len(logs) == countLimit:
                    break
    os.remove(tmpCache)
    return logs


if __name__ == '__main__':
    param_parser = argparse.ArgumentParser()
    param_parser.add_argument('-build', type=str, help='編譯選項(xiàng)', default='Debug', choices=['Debug', 'Release'])
    param_parser.add_argument('-distribution', type=str, help='發(fā)布方式', default='ad-hoc', choices=['ad-hoc', 'app-store'])
    param_parser.add_argument('workPath', type=str, help='工程路徑')
    args = param_parser.parse_args()

    program_dir = os.path.abspath(os.path.dirname(sys.argv[0]))
    export_path = os.path.join(program_dir, 'export')
    xcarchive_path = os.path.join(export_path, 'xxx.xcarchive')
    # 校驗(yàn)workPath
    pbxprojPath = os.path.join(args.workPath, 'xxx.xcodeproj/project.pbxproj')
    if not os.path.exists(pbxprojPath):
        print('工程路徑錯(cuò)誤?。?!\n')
        print(pbxprojPath, '不存在!')
        exit(0)
    # 從蒲公英同步當(dāng)前build號(hào)
    print('同步蒲公英build版本...')
    if not syncBuildVersionWithPGY():
        exit(0)
    # 設(shè)置預(yù)編譯宏
    print('設(shè)置預(yù)編譯宏...')
    rewriteXcodePrecompileMacro()
    #
    cleanResult()
    archive()
    if args.distribution == 'ad-hoc':
        # 上傳至蒲公英
        uploadToPGY()
    else:
        # 上傳至appstore
        uploadToAppStore()


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容