前言
这段时间刚刚学习了一段时间的 Python, 加上自己是做 iOS 开发的, 就想着用 Python 来做一个自动化打包, 可以自动完成打包, 上传到蒲公英, 并且发送邮箱给测试人员.
一是可以减少打包功夫, 二来可以练练手, 结合自己的工作来输出一点东西. 废话不多说, 直接上代码...
原理
就是使用 xcodebuild 来控制 Xcode 进行一系列的操作, 从而完成打包的操作.
为什么要做这个?
在我们日常开发的时候, 特别是在内部测试的时间, 有可能需要频繁的打包, 打包的工作比较繁琐, 需要等待点击下一步, 选择之类, 影响了开发的节奏.(开玩笑, 我能有啥节奏...), 为什么不能直接运行, 然后完成所有的操作呢?
思路:
从网上查找了一些关于 xcodebuild 来打包的资料, 从而得到:
找到对应的项目
clean 项目
archive 项目
export IPA
上传蒲公英
发送邮件
收工
思路有了, 动手起来.
运行环境
Python, Xcode
这些需要大家直接去搭建好环境...
准备工作
下载安装 pycharm(这只是我开发 Python 的工具而已, 大家可以根据自己喜欢的来选择)
注册并认证蒲公英 (不认证的话, 是不能上传的)
邮箱开启 POP3/SMTP 服务 (我使用的是 QQ 邮箱), 记录下 16 位授权码
一个 ExportOptions.plist 文件, 这个下面会解释为什么需要还有怎么生成!
一份 iOS 项目代码→_→
完整代码
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- # @Time : 2018/11/14 11:04 AM
- # @Author : liangk
- # @Site :
- # @File : auto_archive_ios.py
- # @Software: PyCharm
- import os
- import requests
- import webbrowser
- import subprocess
- import time
- import smtplib
- from email.mime.text import MIMEText
- from email import encoders
- from email.header import Header
- from email.utils import parseaddr, formataddr
- project_name = 'TestArchive' # 项目名称
- archive_workspace_path = '/Users / 用户 / Desktop/TestArchive' # 项目路径
- export_directory = 'archive' # 输出的文件夹
- ipa_download_url = 'https://www.pgyer.com/XXX' #蒲公英的 App 地址
- # 蒲公英账号 USER_KEY,API_KEY
- USER_KEY = 'XXXXXXXXXXXXXXXXXXXX'
- API_KEY = 'XXXXXXXXXXXXXXXXXXXX'
- from_address = 'XXXXXXXXXXXXXXXXXXXX@qq.com' # 发送人的地址
- password = 'XXXXXXXXXXXXXXXXXXXX' # 邮箱密码换成他提供的 16 位授权码
- to_address = 'XXXXXXXXXXXXXXXXXXXX@qq.com' # 收件人地址, 可以是多个的
- smtp_server = 'smtp.qq.com' # 因为我是使用 QQ 邮箱..
- class AutoArchive(object):
- """自动打包并上传到蒲公英, 发邮件通知"""
- def __init__(self):
- pass
- def clean(self):
- print("\n\n=========== 开始 clean 操作 ===========")
- start = time.time()
- clean_command = 'xcodebuild clean -workspace %s/%s.xcworkspace -scheme %s -configuration Release' % (
- archive_workspace_path, project_name, project_name)
- clean_command_run = subprocess.Popen(clean_command, shell=True)
- clean_command_run.wait()
- end = time.time()
- # Code 码
- clean_result_code = clean_command_run.returncode
- if clean_result_code != 0:
- print("=======clean 失败, 用时:%.2f 秒 =======" % (end - start))
- else:
- print("=======clean 成功, 用时:%.2f 秒 =======" % (end - start))
- self.archive()
- def archive(self):
- print("\n\n=========== 开始 archive 操作 ===========")
- # 删除之前的文件
- subprocess.call(['rm', '-rf', '%s/%s' % (archive_workspace_path, export_directory)])
- time.sleep(1)
- # 创建文件夹存放打包文件
- subprocess.call(['mkdir', '-p', '%s/%s' % (archive_workspace_path, export_directory)])
- time.sleep(1)
- start = time.time()
- archive_command = 'xcodebuild archive -workspace %s/%s.xcworkspace -scheme %s -configuration Release -archivePath %s/%s' % (
- archive_workspace_path, project_name, project_name, archive_workspace_path, export_directory)
- archive_command_run = subprocess.Popen(archive_command, shell=True)
- archive_command_run.wait()
- end = time.time()
- # Code 码
- archive_result_code = archive_command_run.returncode
- if archive_result_code != 0:
- print("=======archive 失败, 用时:%.2f 秒 =======" % (end - start))
- else:
- print("=======archive 成功, 用时:%.2f 秒 =======" % (end - start))
- # 导出 IPA
- self.export()
- def export(self):
- print("\n\n=========== 开始 export 操作 ===========")
- print("\n\n========== 请你耐心等待一会~===========")
- start = time.time()
- # export_command = 'xcodebuild -exportArchive -archivePath /Users/liangk/Desktop/TestArchive/myArchivePath.xcarchive -exportPath /Users/liangk/Desktop/TestArchive/out -exportOptionsPlist /Users/liangk/Desktop/TestArchive/ExportOptions.plist'
- export_command = 'xcodebuild -exportArchive -archivePath %s/%s.xcarchive -exportPath %s/%s -exportOptionsPlist %s/ExportOptions.plist' % (
- archive_workspace_path, export_directory, archive_workspace_path, export_directory, archive_workspace_path)
- export_command_run = subprocess.Popen(export_command, shell=True)
- export_command_run.wait()
- end = time.time()
- # Code 码
- export_result_code = export_command_run.returncode
- if export_result_code != 0:
- print("======= 导出 IPA 失败, 用时:%.2f 秒 =======" % (end - start))
- else:
- print("======= 导出 IPA 成功, 用时:%.2f 秒 =======" % (end - start))
- # 删除 archive.xcarchive 文件
- subprocess.call(['rm', '-rf', '%s/%s.xcarchive' % (archive_workspace_path, export_directory)])
- self.upload('%s/%s/%s.ipa' % (archive_workspace_path, export_directory, project_name))
- def upload(self, ipa_path):
- print("\n\n=========== 开始上传蒲公英操作 ===========")
- if ipa_path:
- # https://www.pgyer.com/doc/api 具体参数大家可以进去里面查看,
- url = 'http://www.pgyer.com/apiv1/app/upload'
- data = {
- 'uKey': USER_KEY,
- '_api_key': API_KEY,
- 'installType': '1',
- 'updateDescription': description
- }
- files = {'file': open(ipa_path, 'rb')}
- r = requests.post(url, data=data, files=files)
- if r.status_code == 200:
- # 是否需要打开浏览器
- # self.open_browser(self)
- self.send_email()
- else:
- print("\n\n=========== 没有找到对应的 ipa===========")
- return
- @staticmethod
- def open_browser(self):
- webbrowser.open(ipa_download_url, new=1, autoraise=True)
- @staticmethod
- def _format_address(self, s):
- name, address = parseaddr(s)
- return formataddr((Header(name, 'utf-8').encode(), address))
- def send_email(self):
- # https://www.pgyer.com/XXX App 地址
- # 只是单纯的发了一个文本邮箱, 具体的发附件和图片大家可以自己去补充
- msg = MIMEText('<html><body><h1>Hello</h1>' +
- '<p>╮(╯_╰)╭<a href="https://www.pgyer.com/XXX"> 应用已更新, 请下载测试 </a>╮(╯_╰)╭</p>' +
- '<p > 蒲公英的更新会有延迟, 具体版本时间以邮件时间为准 </p>' +
- '</body></html>', 'html', 'utf-8')
- msg['From'] = self._format_address(self, 'iOS 开发团队 <%s>' % from_address)
- msg['Subject'] = Header('来自 iOS 开发团队的问候......', 'utf-8').encode()
- server = smtplib.SMTP(smtp_server, 25) # SMTP 协议默认端口是 25
- server.set_debuglevel(1)
- server.login(from_address, password)
- server.sendmail(from_address, [to_address], msg.as_string())
- server.quit()
- print("=========== 邮件发送成功 ===========")
- if __name__ == '__main__':
- description = input("请输入内容:")
- archive = AutoArchive()
- archive.clean()
关于 ExportOptions.plist 文件
因为 Xcode 9+ 默认不允许访问钥匙串的内容, 必须要设置 allowProvisioningUpdates 才会允许, Python 的 Xcode 插件目前无法支持此项完成打包流程.
解决步骤如下:
1, 手动 Xcode10 打包, 导出 ExportOptions.plist 文件;
2, 编辑 ExportOptions.plist 文件, 配置 provisioningProfiles 对应填入 Bundle identifier 及证书关联配置文件 (打包时自动匹配或手动填入证书, provisioningProfiles 需配置的必填信息可自动生成);
3, 提供 ExportOptions.plist 文件路径供 Python 脚本调用 (详请参看 Python 脚本代码).
具体的内容
- <?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>compileBitcode</key>// 是否编译 bitcode
- <true/>
- <key>method</key>
- <string>ad-hoc</string>/
- <key>provisioningProfiles</key>
- <dict>
- <key > 文件 bundle id</key>
- <string>Adhoc_ID</string>
- </dict>
- <key>signingCertificate</key>// 证书签名
- <string > 这里填证书签名 </string>
- <key>signingStyle</key>
- <string>manual</string>
- <key>stripSwiftSymbols</key>
- <true/>
- <key>teamID</key>
- <string>AANCCUK4M3</string>//TeamID
- <key>thinning</key>
- <string><none></string>
- </dict>
- </plist>
分析
xcodebuild archive -workspace XXX.xcworkspace -scheme XXX -configuration Release -archivePath XXX CONFIGURATION_BUILD_DIR ./dir ODE_SIGN_IDENTITY = 证书 PROVISIONING_PROFILE = 描述文件 UUID
文件 | 说明 |
---|---|
-workspace XXX.xcworkspace | XXX.xcworkspace 需要编译工程的工作空间名称,如果工程不是. xcworkspace 的,可以不需要 - workspace XXX.xcworkspace 这段话 |
-scheme XXX | XXX 是工程名称,-scheme XXX 是指定构建工程的名称 |
-configuration Release | 填入打包的方式是 Debug 或 Release,就跟在 Xcode 中编译前需要在 Edit scheme 的 Build configuration 中选择打出来的包是 Debug 还是 Release 包一样,-configuration 就是配置编译的 Build configuration |
-archivePath XXX | 配置生成. xcarchive 的路径, |
ODE_SIGN_IDENTITY = 证书 | 配置打包的指定证书,如果该工程的 Xcode 已经配置好了证书,那么不加入这段话也可以,打包出来的证书就是 Xcode 中配置好的。 |
PROVISIONING_PROFILE = 描述文件 UUID | 配置打包的描述文件,同上,Xcode 已经配置好了就不用在填入这段话了 |
CONFIGURATION_BUILD_DIR | 配置编译文件的输出路径,如果需要用到. xcarchive 文件内部的 dSYM 等文件,可以使用改字段指定输出路径。 |
问题一
配置一下 compileBicode=NO 即可
来源: https://juejin.im/post/5bed3657518825604e0e4289