iOS 脚本实现自动构建打包(xcodebuild)---Python版

iOS 脚本实现自动构建打包(xcodebuild)—Python版

在软件开发过程中,尤其是iOS应用的发布,打包是至关重要的环节。为了简化打包的流程,将用Python实现自动构建打包功能。
当然,要想学会自动化打包,就要先了解自动化打包所需要用到的工具及打包过程。

⭐️⭐️⭐️AutoBuildIPA项目地址: https://gitee.com/miniaj/auto-build-ipa.git

了解xcodebuild

1.脚本相关命令

// 1.清除编译过程生成文件
xcodebuild clean -workspace <xxx.workspace> -scheme <schemeName> -configuration <Debug|Release>
// 2.编译项目,保证验证项目正常
xcodebuild build -workspace <xxx.workspace> -scheme <schemeName> -configuration <Debug|Release>
// 3.编译并生成.xcarchive包
xcodebuild archive -archivePath <archivePath> -workspace <XXX.xcworkspace> -scheme <schemeNmae> -configuration <Debug|Release>
// 4.将生成的.archive包导出成ipa文件
xcodebuild  -exportArchive -archivePath <archivePath> -exportPath <exportPath> -exportOptionsPlist <exportOptionsPlistPath>

2.其他命令

// 1.可以看到工程的Target ,scheme,configuration配置参数
cd xxx项目路径下
xcodebuild -list
// 2.可以查看本机有哪些描述文件
open ~/Library/MobileDevice/Provisioning\ Profiles
// 3.可以查看描述文件具体信息
/usr/bin/security cms -D -i xxx.mobileprovision

了解Python具体实现打包流程

文章会涉及部分工具类,具体实现可以参照源码,本文章不做解释了。1

导入相关py库

import datetime
import plistlib
import re
import shutil
import subprocess
import time
import os
from mobileprovision import MobileProvisionModel

def executeCmd(cmd):
	"""
    指定终端指令
    :param cmd: 指令字符串
    :return: 返回输出字符串, 判断是否报错
    """
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
    output, stderrOutput = p.communicate()
	output = output.decode()
	esultCode = p.returncode
	return output, resultCode != 0
	
def dealPrintMsg(self, msg, status=1):
	"""
	处理打印行为
	:param status: 状态
	:param msg: 打印信息
	:return: 无返回值
	"""
	print("{状态: %s, 打印信息: %s}" % (str(status), msg))

1.检查项目基础信息,并复制临时项目进行操作

    def start(self):
        if not os.path.exists(self.projectPath):
            self.dealPrintMsg("项目路径为空, 请检查后重试", 2)
            return
        projectSuffixName = os.path.splitext(self.projectPath)[1]
        # 判断是处理项目空间还是项目
        self.isXcworkspace = projectSuffixName == '.xcworkspace'
        # 获取项目名称
        oldProjectName = FileToolController.backFileNameAndFolderName(self.projectPath, 3)
        if self.projectName == "" or not self.isXcworkspace:
            # 项目名称
            self.projectName = oldProjectName

        self.projectFolder = os.path.dirname(self.projectPath)
        # 拷贝临时项目
        newProjectFolderPath = os.path.join(os.path.dirname(self.projectFolder),
                                            os.path.basename(self.projectFolder) + "_temp" + str(int(time.time())))
        shutil.copytree(os.path.dirname(self.projectPath), newProjectFolderPath)
        self.projectFolder = newProjectFolderPath
        # 操作项目路径
        self.projectPath = os.path.join(newProjectFolderPath, oldProjectName + projectSuffixName)

		# 输出的路径
        self.exportDirectory = self.projectFolder + '/auto_archive'  

2.设置配置信息

    def setConfigAppInfo(self):
        newProjectPath = self.projectPath

        if self.isXcworkspace:
        	# 如果是Xcworkspace项目,需要找到指定的编译项目路径
            for root, dirs, files in os.walk(self.projectFolder):
                for dirName in dirs:
                    if dirName == (self.projectName + ".xcodeproj"):
                        newProjectPath = os.path.join(root, dirName)
                        break
        pbxprojPath = os.path.join(newProjectPath, "project.pbxproj")
        if not os.path.exists(pbxprojPath):
            self.dealPrintMsg("project.pbxproj文件不存在,无法配置项目,跳过")
        else:
            self.dealPrintMsg("===========开始配置应用信息操作===========")
            newProjectFolder = os.path.dirname(newProjectPath)
            newProjectFolderPath = os.path.join(newProjectFolder, self.projectName)
            infoFilePath = os.path.join(newProjectFolderPath, "info.plist")
            
            with open(pbxprojPath, 'r') as file:
                fileTempData = file.read()
                # 获取证书信息
                mpModel = MobileProvisionModel(self.mobileprovisionPath)
				# mpModel.team_identifier 证书团队ID
				# mpModel.name 证书名称
                targetDic = {"DEVELOPMENT_TEAM": str(mpModel.team_identifier),
                             "PROVISIONING_PROFILE_SPECIFIER": str(mpModel.name)}
                
                # 替换项目相关信息
                for key, value in targetDic.items():
                    targetPattern = re.compile(r'(%s.*=.(.*?);)' % key)
                    targetResult = list(set(targetPattern.findall(fileTempData)))
                    for targetArr in targetResult:
                        if len(targetArr) >= 2:
                            oldTeamStr = str(targetArr[0])
                            replaceStr = str(targetArr[1])
                            if replaceStr == "\"\"" or replaceStr == value:
                                continue
                            newTeamStr = oldTeamStr.replace(replaceStr, "%s" % value)
                        fileTempData = fileTempData.replace(oldTeamStr, newTeamStr)

                fileData = fileTempData

            # 写入project.pbxproj文件
            with open(pbxprojPath, 'w', encoding='utf-8') as file:
                file.write(fileData)

3.清理编译文件

    def clean(self):
        self.dealPrintMsg("===========开始clean操作===========")
        start = time.time()
        getXcworkspaceStr = "-workspace" if self.isXcworkspace else "-project"
        cleanOpt = ('xcodebuild clean "%s" "%s" -scheme "%s" -configuration Release' %
                    (getXcworkspaceStr, self.projectPath, self.projectName))
        result, isError = self.executeCmd(cleanOpt)
        end = time.time()

        # clean 结果
        if isError:
            self.dealPrintMsg("%s===========clean失败,用时:%.2f秒===========" % (result, end - start), 2)
        else:
            self.dealPrintMsg(result, 4)
            self.dealPrintMsg("===========clean成功,用时:%.2f秒===========" % (end - start))

4.编译项目,保证验证项目正常

    def buildProj(self):
        self.dealPrintMsg("===========开始build操作===========")
        start = time.time()

        buildOpt = ('xcodebuild build "%s" "%s" -scheme "%s" -configuration Release -destination "generic/platform=iOS"'
                    % (self.getXcworkspaceStr(), self.projectPath, self.projectName))
        result, isError = self.executeCmd(buildOpt)
        end = time.time()

        # build 结果
        if isError:
            self.dealPrintMsg("%s===========build失败,用时:%.2f秒===========" % (result, end - start), 2)
        else:
            self.dealPrintMsg(result, 4)
            self.dealPrintMsg("===========build成功,用时:%.2f秒===========" % (end - start))
            buildPath = self.projectFolder + "/build"
            if os.path.exists(self.projectFolder + "/build"):
                shutil.rmtree(buildPath)

5.编译并生成.xcarchive包

    def archive(self):
        self.dealPrintMsg("===========开始archive操作===========")
        subprocess.call(['rm', '-rf', '%s' % self.exportDirectory])
        time.sleep(1)
        subprocess.call(['mkdir', '-p', '%s' % self.exportDirectory])
        time.sleep(1)

        start = time.time()
        getXcworkspaceStr = "-workspace" if self.isXcworkspace else "-project"
        archiveOpt = ('xcodebuild archive "%s" "%s" -scheme "%s" -configuration Release -archivePath "%s/%s" '
                      '-destination "generic/platform=iOS"' %
                      (getXcworkspaceStr, self.projectPath, self.projectName, self.exportDirectory,
                       self.projectName))

        result, isError = self.executeCmd(archiveOpt)
        end = time.time()

        # archive 结果
        if isError:
            subprocess.call(['rm', '-rf', '%s' % self.exportDirectory])
            self.dealPrintMsg("%s===========archive失败,用时:%.2f秒===========" % (result, end - start), 0)
        else:
            self.dealPrintMsg(result, 4)
            self.dealPrintMsg("===========archive成功,用时:%.2f秒===========" % (end - start))

6.处理plist文件

    def createExportOptions(self):
        self.dealPrintMsg("===========开始处理plist文件操作===========")

        mpModel = MobileProvisionModel(self.mobileprovisionPath)
        # app-store,ad-hoc,enterprise, development
        isDev = mpModel.entitlements.get('aps-environment') != "production"
        methodStr = "development" if isDev else ("app-store-connect" if self.isAppStore else "ad-hoc")
        # print(methodStr)
        signingCertificateStr = "Apple Development" if isDev else "Apple Distribution"

        fileStr = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>destination</key>
    <string>export</string>
    <key>manageAppVersionAndBuildNumber</key>
    <false/>
    <key>method</key>
    <string>%s</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>%s</key>
        <string>%s</string>
    </dict>
    <key>signingCertificate</key>
    <string>%s</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>teamID</key>
    <string>%s</string>
    <key>uploadSymbols</key>
    <false/>
    <key>generateAppStoreInformation</key>
    <true/>
</dict>
</plist>
        ''' % (methodStr, self.bundleId, mpModel.name, signingCertificateStr, mpModel.app_id_prefix)
        with open(self.exportDirectory + "/ExportOptions.plist", 'w+') as file:
            file.write(fileStr)
        self.dealPrintMsg("===========处理plist文件操作完成===========")

7.导出成ipa文件

    def export(self):
        self.dealPrintMsg("===========开始export操作===========")
        start = time.time()
        exportIpaFileName = self.projectName + " " + datetime.datetime.now().strftime("%Y-%m-%d %H-%M-%S")

        if self.outputPath == "":
            exportIpaPath = os.path.join(os.path.dirname(self.projectFolder), exportIpaFileName)
        else:
            if not os.path.exists(self.outputPath):
                os.makedirs(self.outputPath)
            exportIpaPath = os.path.join(self.outputPath, exportIpaFileName)

        exportOpt = (('xcodebuild -exportArchive -archivePath "%s/%s.xcarchive" -exportPath "%s" '
                      '-exportOptionsPlist "%s/ExportOptions.plist"') %
                     (self.exportDirectory, self.projectName, exportIpaPath, self.exportDirectory))
        result, isError = self.executeCmd(exportOpt)
        end = time.time()

        # ipa导出结果
        if isError:
            self.dealPrintMsg("%s===========导出IPA失败,用时:%.2f秒===========" % (result, end - start), 2)
        else:
            self.dealPrintMsg(result, 4)
            self.dealPrintMsg("===========导出IPA成功,用时:%.2f秒===========" % (end - start))

        self.dealPrintMsg("导出IPA文件路径为 === %s" % exportIpaPath)

8.运行脚本

if __name__ == '__main__':
    projectPath1 = "xxx/Test/Test.xcodeproj"  # 项目路径
    mobileprovisionPath1 = "xxx/xxx.mobileprovision" # 证书路径
    projectName1 = "Test" # 编译项目名称

    archive = AutoBuildIPA(projectPath1, mobileprovisionPath1, isAppStore=False, projectName=projectName1)
    archive.start()

⭐️如果对你有用的话,希望可以点点赞,感谢了⭐️
欢迎学习交流。


  1. 文章会涉及部分工具类,具体实现可以参照源码,本文章不做解释了 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wu.Nim

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值