├── 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 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 | true
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
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 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | 1447318601954
104 |
105 | 1447318601954
106 |
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 |
136 |
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 |
--------------------------------------------------------------------------------