懒癌晚期患者(iOS or Android自动打包)

关于打包

发布的打包是兴奋,测试的打包是耗时耗力,有时候,做了一半的东西,突然给中断,要求你给另一个项目打包个测试,这时候,你要打开另一个项目,编译,打包,上传,通知测试,一个流程下来,十几分钟过去了,但是不可能就这样看着干等十几分钟,很多人都会选择继续之前的工作,于是······,也是是一个钟,两个钟,那个工作做完了或者刚好想起打包的事,才回来查看打包完了没有,上传再通知测试,所以,把一切交给程序,想当于多线程,让程序帮我们自己把编译,打包,上传,通知测试这一流程简化为一句代码,然后我们可以继续做我们的手头的工作了

若需要最终结果,请把鼠标滚到最后,整个脚本的代码都贴在最后,前面可忽略

基于手头的项目,打包的流程


接到打包任务的时候,执行打包脚本,这个脚本会先拉取最新的项目(web app)代码,并将代码copy到相对应的sdk中(iOS or android),然后不同的平台执行不同的命令进行打包,android平台可以选择是否打渠道包,最后打包完上传发邮件通知测试人员,一切尽在一行代码中,爹妈不用再操心你老忘事啦

打包脚本


先根据不同的平台写好脚本,再进行合并

iOS

  1. 命令行进入项目sdk
  2. 执行xcodebuild clean,清除信息
  3. 执行xcodebuild build,重新编译,生成.app
  4. 使用xcun打包成.ipa
  5. 上传fir.im
  6. 发邮件通知相关人员
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
28
29
30
31
32
33
34
35
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
104
105
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
# -*- coding: utf-8 -*-
import os
import sys
import time
import hashlib
import shutil
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
# 是否使用了coocaPods
hasPods = False
# hbuilder appid
hbuilder_appid = 'H59A30D64'
# 项目名称
project_name = __file__.split(".")[0]
# 项目根目录
project_path = os.path.dirname(os.path.realpath(__file__))
# 打包后.app目录
app_path = project_path + "/build/ios/" + project_name + "/app"
# 打包后ipa目录
ipa_path = project_path + "/build/ios/" + project_name + "/ipa"
# web app 目录
web_app_path = project_path + "/web_project/" + project_name
# sdk目录
ios_sdk_path = project_path + "/hbuilder_ios_sdk/" + project_name
# sdk web app 目录
ios_sdk_web_app_path = ios_sdk_path + '/' + project_name + '/Pandora/apps/' + hbuilder_appid + '/www/'
# 指定项目下编译目录
build_path = "build"
# firm的api token
fir_api_token = "dd25b68cd04f903721fd31b1f83a0c4e"
from_addr = "xxx@sina.com"
password = "xxxx"
smtp_server = "smtp.sina.com"
to_addr = 'xxx@qq.com'
def init_dir():
print ''
print '------- 开始目录检查 -------'
check_dir(app_path)
check_dir(ipa_path)
print '------- 完成目录检查 -------'
def check_dir(path):
print '检查目录 => ' + path
if os.path.exists(path):
print '目录存在 => ' + path
pass
else:
print '目录不存在,创建 => ' + path
os.makedirs(path)
def git_pull():
print ''
print '------- Git pull start -------'
os.chdir(web_app_path)
os.system('git pull')
print '------- Git pull end -------'
def copy_file_to_sdk():
print ''
print '------- 开始清除旧资源 -------'
print '1. 清除 sdk 旧文件 => ' + ios_sdk_web_app_path
# 清空sdk中的旧代码
shutil.rmtree(ios_sdk_web_app_path)
print '2. 复制新文件'
# 复制新的web app代码资源到sdk中
shutil.copytree(web_app_path,ios_sdk_web_app_path)
print '3. 删除.git仓库信息'
# 去除git仓库信息
os.chdir(ios_sdk_web_app_path)
os.system('rm -rf .gitignore;rm -rf .git;rm -rf .project;')
print '------- 清除旧资源完成 -------'
# 清理项目 创建build目录
def clean_project_mkdir_build():
print ''
print '------- clean xcode project start -------'
os.chdir(ios_sdk_path)
os.system('xcodebuild clean SYMROOT=%s' % app_path)
print '------- clean xcode project end -------'
def build_project():
print ''
print '------- build release start -------'
os.chdir(ios_sdk_path)
if hasPods:
print('isPods...')
os.system ('xcodebuild -workspace %s.xcworkspace -scheme %s -configuration release -derivedDataPath %s ONLY_ACTIVE_ARCH=NO || exit' % (project_name,project_name,app_path))
# CONFIGURATION_BUILD_DIR=./build/Release-iphoneos
else:
os.system('xcodebuild -configuration release build SYMROOT=%s' % app_path)
#build/Release-iphoneos/xx.app
print '------- build release end -------'
# 打包ipa
def build_ipa():
print ''
print '------- build ipa start -------'
global ipa_filename
ipa_filename = time.strftime(project_name + '_%Y%m%d-%H%M%S.ipa',time.localtime(time.time()))
os.system ('xcrun -sdk iphoneos PackageApplication -v %s -o %s/%s'%(app_path + ('/Build/Products/Release-iphoneos/' if hasPods else '/Release-iphoneos/') + project_name + '.app',ipa_path,ipa_filename))
print '------- build ipa end -------'
#上传
def upload_fir():
print ''
print '------- upload fir start -------'
if os.path.exists("%s/%s" % (ipa_path,ipa_filename)):
print('watting...')
# 直接使用fir 有问题 这里使用了绝对地址 在终端通过 which fir 获得
ret = os.system("/usr/bin/fir p '%s/%s' -T '%s'" % (ipa_path,ipa_filename,fir_api_token))
else:
print("没有找到ipa文件,上传终止!!!")
print '------- upload fir end -------'
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# 发邮件
def send_mail():
print("开始发送")
msg = MIMEText('xx iOS测试项目已经打包完毕,请前往 http://fir.im/xxxxx 下载测试!', 'plain', 'utf-8')
msg['From'] = _format_addr('自动打包系统 <%s>' % from_addr)
msg['To'] = _format_addr('xx测试人员 <%s>' % to_addr)
msg['Subject'] = Header('xx iOS客户端打包程序', 'utf-8').encode()
server = smtplib.SMTP(smtp_server,25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
print("发送完毕~!")
def main():
init_dir()
git_pull()
copy_file_to_sdk()
# 清理并创建build目录
clean_project_mkdir_build()
# 编译coocaPods项目文件并 执行编译目录
build_project()
# 打包ipa 并制定到桌面
build_ipa()
# 上传fir
upload_fir()
# 发邮件
send_mail()
# 执行
main()

目前的项目是web app,使用HBuilder工具开发,所以这脚本只适用于该类项目,若为原生项目,按需修改

使用此脚本需要修改hbuilder_appid为相应的值,若使用官方的sdk进行离线打包,hasPodsfalse,若自己配置的离线打包sdk,使用了pod管理第三方,需要设置为true

直接命令行执行:

1
python ios_pack.py

期间,会生成 xx.app

紧接着,将xx.app打包成 xx.ipa

最后,上传到fir.im

android

  1. 进入项目sdk
  2. 执行./gradlew clean,清除信息
  3. 执行./gradlew assembleRelease,编译出签名的apk
  4. 打渠道包
  5. 上传fir.im

编译打包apk


先在Android studio中配置:

配置完,生成:

1
2
3
4
5
6
7
8
signingConfigs {
config {
keyAlias 'android'
keyPassword '123456'
storeFile file('/Users/crw/Downloads/test/hbuilder_android_sdk/HBuilder-Hello2/android.keystore')
storePassword '123456'
}
}

配置打包签名:

生成代码:

1
2
3
4
5
6
7
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
signingConfig signingConfigs.config
}
}

命令行进入安卓sdk项目所在根目录,执行:

1
➜ TestDemo ./gradlew build

生成的apk如图:

渠道包


安卓经常会发布在不同的平台,所以对应平台会打出不同的渠道包
这里需要几个东西,上面签名的apk,一个渠道文件(channel_list.txt),一个渠道包脚本(channel.py),一个对apk优化的脚本(zpalign_batch.sh或者zpalign_batch.bat

channel_list.txt

1
2
test
baidu

channel.py

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
28
29
30
31
32
33
34
35
#coding=utf-8
import zipfile
import shutil
import os
import sys
if __name__ == '__main__':
apkFile = sys.argv[1]
apk = apkFile.split('.apk')[0]
# print apkFile
emptyFile = 'xxx.txt'
f = open(emptyFile, 'w')
f.close()
with open('channel_list.txt', 'r') as f:
contens = f.read()
lines = contens.split('\n')
os.mkdir('./release')
#print lines[0]
for line in lines:
channel = 'channel_' + line
destfile = './release/%s_%s.apk' % (apk, channel)
shutil.copy(apkFile, destfile)
zipped = zipfile.ZipFile(destfile, 'a')
channelFile = "META-INF/{channelname}".format(channelname=channel)
zipped.write(emptyFile, channelFile)
zipped.close()
os.remove('./xxx.txt')
#mac
os.system('chmod u+x zipalign_batch.sh')
os.system('./zipalign_batch.sh')
#windows
#os.system('zipalign_batch.bat')

zpalign_batch.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if [ -d "release" ]
then
if [ -d "release/zipaligned" ]
then
rm -rf "release/zipaligned"
fi
mkdir "release/zipaligned"
for file in ./release/*
do
if test -f $file
then
echo "${file##*/}"
zipalign -v 4 $file release/zipaligned/${file##*/}
fi
done
else
echo "No release folder, can not go on zipalign"
fi

zpalign_batch.bat

1
2
3
4
5
6
7
8
9
10
@echo off
echo begin running
if exist release\zipaligned (
rd /s /q release\zipaligned
)
md release\zipaligned
for /f "delims=" %%i in ('dir release\*.apk /b') do (
zipalign -v 4 release\%%i release\zipaligned\%%i
)
echo end running

在以上文件放在同一目录,执行:

1
python channel.py app-release.apk

结果如图:

脚本合并


以上是iOSandroid的不同打包方式,对于懒癌晚期的我们来说,太复杂了,好了,现在可以忘记上面的东西,只要记住下面的合并完的脚本,先看下项目目录:

  • archive用于存放打包后的apkipa
  • hbuilder_android_sdk存放打包的android sdk
  • hbuilder_ios_sdk存放打包的iOS sdk
  • web_project存放我们的web app项目
  • channel_list.txtandroid,打包渠道
  • zipalign_batch.batzipalign_batch.sh,对apk进行优化
  • UTea.py,主要代码,UTea为对应项目名,项目不同,按需修改为项目名

终端cd到当前位置,输入下面命令,go 你:

1
2
3
python UTea.py android
或者
python UTea.py ios

以上为androidiOS各自生成的最终文件,用于上传到fir.im

差点忘记贴代码🙄️:

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
28
29
30
31
32
33
34
35
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
104
105
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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# -*- coding: utf-8 -*-
import os
import sys
import time
import zipfile
import hashlib
import shutil
from email import encoders
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
result_upload_fileName = ''
# hbuilder appid
hbuilder_appid = 'H59A30D64'
# 项目名称
project_name = __file__.split('.')[0]
# 项目根目录
project_path = os.path.dirname(os.path.realpath(__file__))
# web app 目录
web_app_path = project_path + '/web_project/' + project_name
# iOS
# sdk目录
ios_sdk_path = project_path + '/hbuilder_ios_sdk/' + project_name
# sdk web app 目录
ios_sdk_web_app_path = ios_sdk_path + '/' + project_name + '/Pandora/apps/' + hbuilder_appid + '/www/'
# 打包后.app目录
app_path = ios_sdk_path + '/build/Release-iphoneos/' + project_name + '.app'
# 打包后.app目录 pod
app_path_pod = ios_sdk_path + '/build/Build/Products/Release-iphoneos/' + project_name + '.app'
# 打包后ipa目录
ipa_path = project_path + '/archive/ios/' + project_name
# 指定项目下编译目录
build_path = 'build'
# 是否使用了coocaPods
hasPods = os.path.exists(ios_sdk_path + '/' + project_name + '.xcworkspace')
# android
# sdk目录
android_sdk_path = project_path + '/hbuilder_android_sdk/' + project_name
# sdk web app 目录
android_sdk_web_app_path = android_sdk_path + '/assets/apps/' + hbuilder_appid + '/www/'
# 打包后apk目录
apk_path = android_sdk_path + '/app/build/outputs/apk/app-release.apk'
# 签名后渠道包apk路径
apk_channel_path = project_path + '/archive/android/' + project_name
# 渠道文件
channel_path = project_path + '/channel_list.txt'
# firm的api token
fir_api_token = ‘xxxx’
from_addr = ‘xxx@sina.com'
password = ‘xxx’
smtp_server = 'smtp.sina.com'
to_addr = ‘xxx@qq.com'
def checkArgv():
global is_Android
if len(sys.argv) < 2:
result = raw_input('您要打包的是iOS项目?Y/N ')
is_Android = result == 'N' or result == 'n'
else:
is_Android = sys.argv[1] != 'ios'
print '------- 开始打包' + ('Android' if is_Android else 'iOS') + '项目 -------'
def init_dir():
print ''
print '------- 开始目录检查 -------'
check_dir(apk_channel_path)
check_dir(ipa_path)
print '------- 完成目录检查 -------'
def check_dir(path):
print '检查目录 => ' + path
if os.path.exists(path):
print '目录存在 => ' + path
print '清除资源'
os.chdir(path)
os.system('rm -rf *')
else:
print '目录不存在,创建 => ' + path
os.makedirs(path)
def git_pull():
print ''
print '------- Git pull start -------'
os.chdir(web_app_path)
os.system('git pull')
print '------- Git pull end -------'
def copy_file_to_sdk():
print ''
print '------- 开始清除旧资源 -------'
tmp_sdk_web_app_path = android_sdk_web_app_path if is_Android else ios_sdk_web_app_path
print '1. 清除 sdk 旧文件 => ' + tmp_sdk_web_app_path
# 清空sdk中的旧代码
shutil.rmtree(tmp_sdk_web_app_path)
print '2. 复制新文件'
# 复制新的web app代码资源到sdk中
shutil.copytree(web_app_path,tmp_sdk_web_app_path)
print '3. 删除.git仓库信息'
# 去除git仓库信息
os.chdir(tmp_sdk_web_app_path)
os.system('rm -rf .gitignore;rm -rf .git;rm -rf .project;')
print '------- 清除旧资源完成 -------'
# 清理项目 创建build目录
def clean_project():
print ''
print '------- clean project start -------'
tmp_sdk_path = android_sdk_path if is_Android else ios_sdk_path
os.chdir(tmp_sdk_path)
if is_Android:
os.system('./gradlew clean')
else:
os.system('xcodebuild clean')
print '------- clean project end -------'
def build_project():
print ''
print '------- build release start -------'
tmp_sdk_path = android_sdk_path if is_Android else ios_sdk_path
os.chdir(tmp_sdk_path)
if is_Android:
os.system('./gradlew assembleRelease')
build_apk()
else:
if hasPods:
print('isPods...')
os.system ('xcodebuild -workspace %s.xcworkspace -scheme %s -configuration release -derivedDataPath %s ONLY_ACTIVE_ARCH=NO || exit' % (project_name,project_name,build_path))
# CONFIGURATION_BUILD_DIR=./build/Release-iphoneos
else:
os.system('xcodebuild -configuration release build')
#build/Release-iphoneos/xx.app
build_ipa()
print '------- build release end -------'
# 打包渠道apk 保存在特定位置
def build_apk():
print ''
print '------- build channelApk start -------'
apkFile = apk_path
apk = (apk_path.split('/')[-1]).split('.apk')[0]
# print apkFile
emptyFile = 'xxx.txt'
f = open(emptyFile, 'w')
f.close()
with open(channel_path, 'r') as f:
contens = f.read()
lines = contens.split('\n')
global result_upload_fileName
#print lines[0]
for line in lines:
channel = 'channel_' + line
tmp_time = time.strftime('_%Y%m%d-%H%M%S.apk',time.localtime(time.time())) + ''
destfile = (apk_channel_path + '/%s_%s' + tmp_time) % (project_name, channel)
result_upload_fileName = destfile
shutil.copy(apkFile, destfile)
zipped = zipfile.ZipFile(destfile, 'a')
channelFile = "META-INF/{channelname}".format(channelname=channel)
zipped.write(emptyFile, channelFile)
zipped.close()
os.remove('./xxx.txt')
os.chdir(project_path)
#mac
os.system('chmod u+x zipalign_batch.sh')
os.system('./zipalign_batch.sh %s' % apk_channel_path)
#windows
#os.system('zipalign_batch.bat')
print '------- build channelApk end -------'
# 打包ipa 并且保存在桌面
def build_ipa():
print ''
print '------- build ipa start -------'
ipa_fileName = time.strftime(project_name + '_%Y%m%d-%H%M%S.ipa',time.localtime(time.time()))
os.system ('xcrun -sdk iphoneos PackageApplication -v %s -o %s/%s' % (app_path_pod if hasPods else app_path,ipa_path,ipa_fileName))
global result_upload_fileName
result_upload_fileName = ipa_path + '/' + ipa_fileName
print '------- build ipa end -------'
#上传
def upload_fir():
print ''
print '------- upload fir start -------'
global result_upload_fileName
if os.path.exists(result_upload_fileName):
print('watting...')
# 直接使用fir 有问题 这里使用了绝对地址 在终端通过 which fir 获得
ret = os.system("/usr/bin/fir p '%s' -T '%s'" % (result_upload_fileName,fir_api_token))
else:
print("没有找到ipa文件,上传终止!!!")
print '------- upload fir end -------'
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# 发邮件
def send_mail():
print("开始发送")
msg = MIMEText('xx测试项目已经打包完毕,请前往 http://fir.im/xxxxx 下载测试!', 'plain', 'utf-8')
msg['From'] = _format_addr('自动打包系统 <%s>' % from_addr)
msg['To'] = _format_addr('xx测试人员 <%s>' % to_addr)
msg['Subject'] = Header('xx 客户端打包程序', 'utf-8').encode()
server = smtplib.SMTP(smtp_server,25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
print("发送完毕~!")
def main():
checkArgv()
init_dir()
git_pull()
copy_file_to_sdk()
# 清理并创建build目录
clean_project()
# 编译coocaPods项目文件并 执行编译目录
build_project()
# 上传fir
upload_fir()
# 发邮件
send_mail()
# 执行
main()

第一次使用,需要手动配置xcode的签名以及保证项目编译通过,android需要配置签名文件,然后按照demo的格式,可以自己修相关路径,原本是想用一个python脚本,打包多个项目,但是目前的项目需要一个id,这个id属于随机生成,若每次需要输入太过麻烦,便改为,一个项目对应一个脚本
自动打包终于告一段落了,之前写的iOS自动打包,所缺的android打包也补上了,可以继续当一名懒癌晚期患者了

参考文章:

  1. android studio关于命令行打包apk
  2. 记——加快gradle 构建速度的经验
  3. 未提供 -tsa 或 -tsacert, 此 jar 没有时间戳。如果没有时间戳, 则在签名者证书的到期
  4. Android多渠道打包之Python打包
  5. Xcode升级到8.3后,unable to find utility “PackageApplication”
  6. Android APK优化工具Zipalign详解