├── history └── test ├── .idea ├── .name ├── vcs.xml ├── modules.xml ├── iOSAutoPackaging.iml ├── misc.xml └── workspace.xml ├── requirements.txt ├── template ├── entitlements.plist └── test.plist ├── Config.py ├── SenderEmail.py ├── Packaging.py ├── PlistEdit.py ├── Client.py ├── Resign.py └── README.md /history/test: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | iOSAutoPackaging -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | binaryplist==0.0.1 2 | biplist==0.9 3 | click==5.1 4 | qiniu==7.0.6 5 | requests==2.9.1 6 | wheel==0.24.0 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/iOSAutoPackaging.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /template/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | 828E9CDH56.* 8 | 9 | get-task-allow 10 | 11 | application-identifier 12 | 828E9CDH56.com.test.test 13 | com.apple.developer.team-identifier 14 | 828E9CDH56 15 | aps-environment 16 | production 17 | 18 | 19 | -------------------------------------------------------------------------------- /Config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'lixinxing' 3 | 4 | #--------打包信息--------- 5 | # 需要打包的项目信息 6 | project_path = '项目所在目录' 7 | workspace_name = 'test.xcworkspace' 8 | scheme_name = 'test' 9 | # 个人证书,用于打包ipa 10 | provisioning_profile = 'testprofile' 11 | 12 | # -------企业发布信息------ 13 | # 用于企业发布的plist文件,只会修改里面的ipa包下载路径,其他的需要改成与自己项目相符 14 | template_plist_name = 'test.plist' 15 | 16 | #-----重新签名信息------ 17 | #重签名需要的plist文件 18 | resign_plist_name = 'entitlements.plist' 19 | #企业证书名字 20 | ep_cer_name = 'test' 21 | # 企业账号,用于重签。需要放入template目录下 22 | ep_provisioning_profile = 'test.mobileprovision' 23 | 24 | # ----上传服务器配置(使用七牛)----- 25 | access_key = 'test' 26 | secret_key = 'test' 27 | bucket_name = 'test' 28 | ipa_base_url = 'test' 29 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /template/test.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | assets 9 | 10 | 11 | kind 12 | software-package 13 | url 14 | https://dn-test.qbox.me/test.ipa 15 | 16 | 17 | kind 18 | full-size-image 19 | needs-shine 20 | 21 | url 22 | https://dn-test.qbox.me/full-120-test.png 23 | 24 | 25 | kind 26 | display-image 27 | needs-shine 28 | 29 | url 30 | https://dn-test.qbox.me/display-60-test.png 31 | 32 | 33 | metadata 34 | 35 | bundle-identifier 36 | com.test.test 37 | bundle-version 38 | 1.0 39 | kind 40 | software 41 | subtitle 42 | Ver1.0 43 | title 44 | test 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /SenderEmail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | #导入smtplib和MIMEText 4 | import smtplib,sys 5 | from email.mime.text import MIMEText 6 | 7 | 8 | class SenderEmail: 9 | 10 | def send_mail(self, sub, content): 11 | ############# 12 | # 要发给谁 13 | mailto_list=["x@devlxx.com", 14 | "13348782277@163.com"] 15 | ##################### 16 | #设置服务器,用户名、口令以及邮箱的后缀 17 | mail_host="smtp.exmail.qq.com" 18 | mail_user="x@devlxx.com" 19 | mail_pass="dear334693" 20 | mail_postfix="devlxx.com" 21 | ###################### 22 | ''' 23 | to_list:发给谁 24 | sub:主题 25 | content:内容 26 | send_mail("aaa@126.com","sub","content") 27 | ''' 28 | me = mail_user+"<"+mail_user+"@"+mail_postfix+">" 29 | msg = MIMEText(content, 'html', 'utf-8') 30 | msg['Subject'] = sub 31 | msg['From'] = me 32 | msg['To'] = ";".join(mailto_list) 33 | try: 34 | s = smtplib.SMTP() 35 | s.connect(mail_host) 36 | s.login(mail_user,mail_pass) 37 | s.sendmail(me, mailto_list, msg.as_string()) 38 | s.close() 39 | return True 40 | except Exception, e: 41 | print str(e) 42 | return False 43 | if __name__ == '__main__': 44 | if SenderEmail().send_mail(u'这是python测试邮件1',u'python发送邮件1'): 45 | print u'发送成功' 46 | else: 47 | print u'发送失败' -------------------------------------------------------------------------------- /Packaging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'lixinxing' 3 | 4 | import subprocess 5 | import Config 6 | import os 7 | import datetime 8 | 9 | 10 | class Packaging: 11 | def newIpa(self): 12 | formt_time = datetime.datetime.now().strftime("%Y%m%d%H%M") 13 | archive_name = Config.scheme_name + formt_time + '.xcarchive' 14 | archivePath = os.path.abspath(os.path.join(os.path.dirname(__file__),'history', archive_name)) 15 | workspacePath = Config.project_path + Config.workspace_name 16 | # xcarchive 17 | archiveCmd = "xctool -workspace " + workspacePath + " -scheme " + Config.scheme_name + ' clean archive -archivePath ' + archivePath 18 | print 'archiveCmd: ',archiveCmd 19 | process = subprocess.Popen(archiveCmd, shell=True) 20 | # 等上一步执行完再执行下一步 21 | process.wait() 22 | 23 | # 打包成ipa包 24 | export_name = Config.scheme_name + formt_time + '.ipa' 25 | exportPath = os.path.abspath(os.path.join(os.path.dirname(__file__),'history', export_name)) 26 | exportCmd = 'xcodebuild -exportArchive -archivePath ' + archivePath + ' -exportPath ' + exportPath + ' -exportFormat ipa -exportProvisioningProfile ' + '\"' +Config.provisioning_profile + '\"' 27 | print 'exportCmd: ',exportCmd 28 | exportProcess = subprocess.Popen(exportCmd, shell=True) 29 | exportProcess.wait() 30 | return exportPath 31 | 32 | 33 | 34 | if __name__ == '__main__': 35 | new_ipa = Packaging().newIpa() 36 | print new_ipa 37 | 38 | 39 | -------------------------------------------------------------------------------- /PlistEdit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'lixinxing' 3 | 4 | import os 5 | import json 6 | from biplist import * 7 | import datetime 8 | import Config 9 | 10 | 11 | class PlistEdit: 12 | # 保存生成的plist,并且将路径返回 13 | def savenewplist(self, plist): 14 | formt_time = datetime.datetime.now().strftime("%Y%m%d%H%M") 15 | save_name = Config.scheme_name + formt_time + '.plist' 16 | plist_path = os.path.abspath(os.path.join(os.path.dirname(__file__),'history', save_name)) 17 | 18 | try: 19 | writePlist(plist, plist_path) 20 | return plist_path 21 | except (InvalidPlistException, NotBinaryPlistException), e: 22 | print 'someting bad happend when writePlist:',e 23 | 24 | # 通过模板生成新的plist文件 25 | def newplist(self,templatename, ipaname): 26 | # 通过此生成外链ipa地址 27 | base_url = Config.ipa_base_url 28 | plist_path = os.path.abspath(os.path.join(os.path.dirname(__file__),'template', templatename)) 29 | print 'plist path :', plist_path 30 | 31 | try: 32 | plist = readPlist(plist_path) 33 | plist["items"][0]['assets'][0]['url'] = base_url + ipaname 34 | ipa_url = plist["items"][0]['assets'][0]['url'] 35 | print ipa_url 36 | # version 暂不关心 37 | # version = plist["items"][0]['metadata']['bundle-version'] 38 | # print version 39 | return self.savenewplist(plist) 40 | 41 | except (InvalidPlistException, NotBinaryPlistException),e: 42 | print 'Not a plist:',e 43 | -------------------------------------------------------------------------------- /Client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # flake8: noqa 3 | 4 | from qiniu import Auth 5 | from qiniu import put_file 6 | from qiniu import put_data 7 | from PlistEdit import PlistEdit 8 | import datetime 9 | import Config 10 | from Packaging import Packaging 11 | from SenderEmail import SenderEmail 12 | from Resign import Resign 13 | 14 | 15 | formt_time = datetime.datetime.now().strftime("%Y-%m-%d") 16 | print 'time:', formt_time 17 | 18 | access_key = Config.access_key 19 | secret_key = Config.secret_key 20 | bucket_name = Config.bucket_name 21 | 22 | q = Auth(access_key, secret_key) 23 | 24 | # 上传本地ipa文件 25 | ipa_path = Packaging().newIpa() 26 | 27 | resign_ipa_path = Resign().start(ipa_path) 28 | 29 | ipa_name = resign_ipa_path.split('/')[-1] 30 | print 'ipa_name: ',ipa_name 31 | 32 | token = q.upload_token(bucket_name, ipa_name) 33 | ret, info = put_file(token, ipa_name, resign_ipa_path, mime_type="application/octet-stream", check_crc=True) 34 | print(info) 35 | assert ret['key'] == ipa_name 36 | 37 | # 上传本地plist文件 38 | template_name = Config.template_plist_name 39 | # 得到生成的plist文件的路径 40 | plist_path = PlistEdit().newplist(template_name, ipa_name) 41 | plist_name = plist_path.split('/')[-1] 42 | token = q.upload_token(bucket_name, plist_name) 43 | ret, info = put_file(token, plist_name, plist_path, mime_type='application/xml', check_crc=True) 44 | # ret, info = put_data(token, plist_name, data) 45 | print(info) 46 | assert ret['key'] == plist_name 47 | 48 | emailTitle = Config.scheme_name + " 新测试包发布".decode('utf-8') 49 | detailTime = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S") 50 | installLink = 'itms-services://?action=download-manifest&url=https://dn-appreleasexx.qbox.me/' + plist_name 51 | 52 | inputNote = raw_input("请输入你的版本更新备注:").decode('utf-8') 53 | print '输入的版本更新备注:', inputNote 54 | 55 | emailHtml = \ 56 | u'\ 57 | \ 58 | \ 59 | \ 60 | \ 61 |

%s

\n\ 62 |

打包时间:%s

\ 63 |

安装方法:点此直接安装(目前只支持iOS系统邮件客户端)

\ 64 |

历史地址:http://www.test.com/test.html

\ 65 |

安装方法:如果安装到iOS9以后弹出提示无法打开,请到设置 - 通用 - 设备管理 - 选择eegsmart的证书,添加到信任,就可以打开APP了。

\n\ 66 |

更新说明:%s

\ 67 | \n\ 68 | ' % (emailTitle, detailTime, installLink, inputNote) 69 | 70 | snederFlag = SenderEmail().send_mail( emailTitle, emailHtml) 71 | 72 | if snederFlag: 73 | print('打包发布成功与邮件发送成功!') 74 | 75 | # 修改html页面并上传 76 | 77 | 78 | -------------------------------------------------------------------------------- /Resign.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'lixinxing' 3 | # 用于重新签名 4 | 5 | import subprocess 6 | import os 7 | import Config 8 | import datetime 9 | 10 | 11 | 12 | class Resign: 13 | 14 | def unzipIpa(self, ipaPath): 15 | zipName = 'resign.zip' 16 | # 复制成.zip后缀 17 | cpCmd = 'cp ' + ipaPath + ' ' + zipName 18 | print cpCmd 19 | process = subprocess.Popen(cpCmd, shell=True) 20 | process.wait() 21 | # 解压zip到指定目录 22 | unzipCmd = 'unzip ' + zipName 23 | print unzipCmd 24 | process = subprocess.Popen(unzipCmd, shell=True) 25 | process.wait() 26 | 27 | #删除无用的zip文件 28 | delZipCmd = 'rm -rf ' + zipName 29 | process = subprocess.Popen(delZipCmd, shell=True) 30 | process.wait() 31 | 32 | def replaceFiles(self): 33 | appName = Config.scheme_name + '.app' 34 | # 先删除需要替换的文件 35 | codisignPath = 'Payload/' + appName + '/_CodeSignature' 36 | targetPath = 'Payload/'+ appName + '/embedded.mobileprovision' 37 | delDirCmd = 'rm -rf ' + codisignPath + ' ' + targetPath 38 | print delDirCmd 39 | process = subprocess.Popen(delDirCmd, shell=True) 40 | process.wait() 41 | 42 | # 企业Profile的绝对路径 43 | profilePath = 'template/' + Config.ep_provisioning_profile 44 | # 需要替换的Profile的地址 45 | targetPath = 'Payload/' + appName + '/embedded.mobileprovision' 46 | replaceCmd = 'cp ' + profilePath + ' ' + targetPath 47 | print replaceCmd 48 | process = subprocess.Popen(replaceCmd, shell=True) 49 | process.wait() 50 | 51 | def codesign(self): 52 | 53 | appName = Config.scheme_name + '.app' 54 | # 重新签名 codesign -f -s $certifierName --entitlements entitlements.plist Payload/test.app 55 | eplistPath = os.path.join(os.path.dirname(__file__), 'template', Config.resign_plist_name) 56 | signPath = 'Payload/' + appName 57 | signCmd = 'codesign -f -s "' + Config.ep_cer_name + '" --entitlements ' + eplistPath + ' ' + signPath 58 | print signCmd 59 | process = subprocess.Popen(signCmd, shell=True) 60 | process.wait() 61 | 62 | def reZipIpa(self): 63 | 64 | # 重新打包 65 | formt_time = datetime.datetime.now().strftime("%Y%m%d%H%M") 66 | resignIpaName = 'Resign' + Config.scheme_name + formt_time + '.ipa' 67 | reZipResultPath = os.path.join(os.path.dirname(__file__), 'history', resignIpaName) 68 | rezipCmd = 'zip -r ' + reZipResultPath + ' ' + 'Payload' 69 | print rezipCmd 70 | process = subprocess.Popen(rezipCmd, shell=True) 71 | process.wait() 72 | 73 | # 删除Payload文件 74 | delPayloadCmd = 'rm -fr ' + 'Payload' 75 | process = subprocess.Popen(delPayloadCmd, shell=True) 76 | process.wait() 77 | return reZipResultPath 78 | 79 | def start(self, ipaPath): 80 | self.unzipIpa(ipaPath) 81 | self.replaceFiles() 82 | self.codesign() 83 | path = self.reZipIpa() 84 | return path 85 | 86 | if __name__ == '__main__': 87 | dir = os.path.join(os.path.dirname(__file__), 'history') 88 | print dir 89 | # 获取各路径 90 | ipaPath = os.path.abspath(os.path.join(dir, 'PEPatient201604091413.ipa')) 91 | new_ipa = Resign().start(ipaPath) 92 | print new_ipa 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 在我们日常的工作中,经常需要打包给测试进行测试,或者给产品经理体验。一次又一次的手动打包,修改plist文件,上传服务器浪费了我们大量宝贵的学习时间。 2 | 这是一个用于自动打包的Python脚本,可以直接打包ipa并生成对应的plist,然后使用企业证书进行重签名,并上传指定的七牛服务器。这所有的动作只需要在终端敲入一行命令即可解决。 3 | 4 | ###功能流程说明 5 | `打包ipa`-->`重签名ipa`-->`生成plist文件`-->`上传服务器`-->`发送邮件` 6 | 7 | ###使用说明(针对`iOS开发者`) 8 | + 1、安装`HomeBrew` 9 | + 安装命令:`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 10 | 11 | + 2、安装`xctool`用于`iOS项目`打包 12 | + `brew install xctool` 13 | 14 | + 3、安装`pip` 15 | + 1.我们先获取`pip`安装脚本:`wget https://bootstrap.pypa.io/get-pip.py` 16 | 如果没有安装`wget`可以执行`brew install wget`安装 17 | + 2.安装pip `sudo python get-pip.py` 18 | 19 | + 4、安装Python虚拟环境virtualenv 20 | + `$ sudo pip install virtualenv` 21 | 22 | + 5、进入下载的项目所在的目录 23 | ```shell 24 | $ cd (you path) 25 | $ virtualenv venv 执行此命令后会在当前目录下创建一个venu文件夹 26 | New python executable in venv/bin/python 27 | Installing distribute............done. 28 | $ venv/bin/pip install -r requirements.txt 29 | ``` 30 | 31 | + 6、配置项目 32 | + 修改`entitlements.plist`文件,不知道修改可以看我的博文[iOS证书及ipa包重签名](http://devlxx.com/ioszheng-shu-ji-ipabao-zhong-qian-ming/) 33 | + 修改`test.plist`,根据这个plist文件来安装app,具体配置方法可以搜索iOS企业发布流程 34 | + 修改`Config.py`文件,如何配置根据注释来。 35 | 36 | + 7、自动打包 37 | + 执行`venv/bin/python Client.py`。上传成功后会让你输入版本注释,输入后点击回车就会发邮件,整个流程就走完了。 38 | > 打包完成后,可以在history文件夹下看到生成的ipa包以及改好的plist文件等 39 | 40 | 41 | ###原理说明 42 | ####archive 43 | 使用`xctool`执行`archive`操作,`xctool`是`FaceBook`开源的一个命令行工具,用来替代苹果的`xcodebuild`工具。下面对xctool的参数和命令进行一个说明。为了能运行shell命令,此项目使用了`Python`的`subprocess`库 44 | + 参数: 45 | ``` 46 | -workspace 需要打包的workspace 后面接的文件一定要是.xcworkspace 结尾的 47 | -scheme 需要打包的Scheme 48 | -configuration 需要打包的配置文件,我们一般在项目中添加多个配置,适合不同的环境 49 | ``` 50 | + 命令: 51 | ``` 52 | clean 清除编译产生的问题,下次编译就是全新的编译了 53 | archive 打包命令,会生成一个.xcarchive的文件 54 | ``` 55 | 注:`archive`命令需要接一个参数:-archivePath 即你存放Archive文件的目录 56 | + 使用说明 57 | + 命令:`xctool -workspace ProjectName.xcworkspace -scheme SchemeName clean archive` 58 | + 样例:`xctool -workspace /Users/lixinxing/Desktop/SchemeName_APP/ProjectName.xcworkspace -scheme SchemeName clean archive -archivePath /Users/lixinxing/Desktop/iOSAutoPackaging/history/test.xcarchive` 59 | 执行这个命令后,会将打好的包命名为`test.xcarchive`放在目录`/Users/lixinxing/Desktop/iOSAutoPackaging/history/` 60 | 61 | ####2、export为ipa包 62 | 这个操作需要用到`xcodebuild`,他是`xocde`的 `Command line tools` 就有的一个命令 63 | + 参数 64 | ``` 65 | -exportArchive 告诉xcodebuild需要导出archive文件 66 | -exportFormat 告诉xcodebuild需要导出的archive文件最后格式 后面接IPA 就是archive文件导出的格式为ipa文件 67 | -archivePath archive文件目录 68 | -exportPath 导出的ipa存放目录 69 | -exportProvisioningProfile 打包用到的ProvisioningProfile 70 | ``` 71 | 72 | + 命令 73 | ``` 74 | xcodebuild -exportArchive -archivePath ${PROJECT_NAME}.xcarchive \ 75 | -exportPath ${PROJECT_NAME} \ 76 | -exportFormat ipa \ 77 | -exportProvisioningProfile ${PROFILE_NAME} 78 | ``` 79 | 80 | + 样例: 81 | ``` 82 | xcodebuild -exportArchive -archivePath /Users/lixinxing/Library/Developer/Xcode/Archives/2015-09-07/SchemeName\ 15-9-7\ 下午2.38.xcarchive -exportPath /Users/lixinxing/Desktop/test/test.ipa -exportFormat ipa -exportProvisioningProfile ProjectNameEnterprise 83 | ``` 84 | 这个例子的含义可以结合上面的参数说明进行理解,这样就完成的打包工作,生成了一个`test.ipa`文件放在`/Users/lixinxing/Desktop/test/`目录下 85 | 86 | ####3、重签名 87 | [iOS证书及ipa包重签名](http://devlxx.com/ioszheng-shu-ji-ipabao-zhong-qian-ming/) 88 | 89 | ####4、生成plist文件 90 | 为了简单,这里生成的plist文件是通过编辑模板plist文件的一些key的value来生成的,这里只改变了plist文件里面ipa包下载地址对应的key。这里对plist文件进行编辑使用的是`Python`的`biplist` 91 | 92 | ####5、上传七牛 93 | 使用七牛提供的`Python SDK`, 上传七牛,具体介绍见[七牛的官方文档](http://developer.qiniu.com/docs/v6/sdk/python-sdk.html),使用可以在Client.py文件中看到。 94 | 95 | ####6、发送邮件 96 | 97 | ####7、TODO 98 | 直接从七牛获取所有的plist文件, 自动生成网页。 99 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | true 36 | 37 | 38 | 39 | 40 | 41 | 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 | 76 | 77 | 78 | 79 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 1447318601954 104 | 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 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | --------------------------------------------------------------------------------