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()
⭐️如果对你有用的话,希望可以点点赞,感谢了⭐️
欢迎学习交流。
文章会涉及部分工具类,具体实现可以参照源码,本文章不做解释了 ↩︎