背景
因公司做的都是一些独立部署的项目,经常会遇到以下这种情况。项目经理新拿到一个项目,过来说XX给我包打一下。简而言之就是代码都是同一份代码,但是需要替换app里面的图标&启动图&三方key&标题&bundleid等等等。虽然不是什么复杂的工作,但是设想一下,当你写着代码,你需要贮藏掉改动的代码,检出新打包分支的代码,还需要上苹果开发者后台申请证书,导出moboileprovision文件。(开发者后台还卡的要死 -_-),弄好证书后还要将代码里的配置修改掉,替换掉三方的appkey,替换掉域名等一些列操作。差不多都个把小时花进去了。而且公司一个项目至少还得打2个包。还有些时候打完包,测试说app请求失败,发现是域名打错了,又得重新打包。这做的都是一些重复性极高的事情,对于自身的提升来说毫无意义。因此想到让代码来代替我们做这部分的工作。理想情况,起一个web服务 让项目经理自行去修改配置文件,然后导出相对应的包(再配个模块应用功能选择,这不就是涂鸦的管理后台吗 ),现有资源无法匹配,自身技术还不能实现,因此先实现一个本地自动化的脚本提高效率。
思路
- 从git上拉取代码
- 修改bundleid、displayname、provision file等
- 修改三方key、baseurl等
- 修改appIcon、其他资源图片
- 打包导出
准备
需要用到的工具
- ruby
- xcodeproj 用于修改项目配置
#安装方法
gem install xcodeproj
- mobileprovision-read 用于读取mobileprovision 相关配置信息
#安装方法
curl https://raw.githubusercontent.com/0xc010d/mobileprovision-read/master/main.m | clang -framework Foundation -framework Security -o /usr/local/bin/mobileprovision-read -x objective-c -
- chunky_png 用于读取图片
gem install chunky_png
修改前项目
地址
在ruby脚本项目中创建这3个文件夹 用于存放相关信息
- exportplist: export ipa文件需要用到ExportOptions.plist文件
- images-appIcon 用于存放需要替换的appIcon
- images-other 用于存放其他的图片资源
- mobileprovision 用于存放provision file 文件
开始
1.从git上拉取代码
def gitcloneCode(path, barch)
#替换成自己的代码库地址
puts "下拉代码"
targetGitUrl = path
targetBranch = barch
system "git clone -b #{targetBranch} #{targetGitUrl}"
system "git branch"
puts "代码下拉完成"
end
2. 修改bundleid、displayname、provision file等
可以使用一个json 去配置读取相关信息:config.json
// team: 证书名称 可以从钥匙串中获取 注意替换成自己的
// json文件上半部分 是项目配置信息 下半部分是代码里的一些配置信息 例如三方sdk key等 注意要和工程项目中的名称对应起来,方便修改
// 有需要其他配置 自行添加
{
"appname": "测试2",
"bundleid": "com.ch.test2",
"version": "1.1",
"build": "10086",
"team": "-----------",
"AAA": "AAAA2",
"BBB": "BBBB2",
"CCC": "CCCC2"
}
读取配置信息
#========================================读取配置信息
jsonPath = 'config.json'
json = File.read(jsonPath)
configObj = JSON.parse(json)
puts "解析json数据#{configObj}"
#bundleid
bundleid = configObj['bundleid']
#todaybundleid
todaybundleid = configObj['todaybundleid']
#appname
appname = configObj['appname']
#version
version = configObj['version']
#build
build = configObj['build']
#team
team = configObj['team']
读取 mobileprovision
mobileprovision_name = %x(ls mobileprovision).split(' ')[0].split('.')[0]
todaymobileprovision_name = %x(ls todaymobileprovision).split(' ')[0].split('.')[0]
mobileprovision_path = "mobileprovision/" + %x(ls mobileprovision).split(' ')[0]
todaymobileprovision_path = "todaymobileprovision/" + %x(ls todaymobileprovision).split(' ')[0]
mobileprovision_uuid = %x(mobileprovision-read -f #{mobileprovision_path} -o UUID)
todaymobileprovision_uuid = %x(mobileprovision-read -f #{todaymobileprovision_path} -o UUID)
teamId = %x(mobileprovision-read -f #{mobileprovision_path} -o TeamIdentifier).strip
修改基本配置
def updatePlist(path, key, value)
puts "修改 infoplist: #{path}, #{key}: #{value}"
infoPlistHash = Xcodeproj::Plist.read_from_path(path)
infoPlistHash[key] = value
Xcodeproj::Plist.write_to_path(infoPlistHash, path)
puts "修改 infoplist完成"
end
###更新app应用名称 path: plist路径 name: 目标名称
def updateAppName(path, name)
updatePlist(path, 'CFBundleDisplayName', name)
end
#打开proj
#=======================================更改proj信息
projName = 'repackage.xcodeproj'
targetName = 'repackage'
proj_path = projpath + '/' + projName
puts '解析完成'
#修改app名称
#infolist 路径
infoPlistPath = projpath + '/repackage/info.plist'
updateAppName(infoPlistPath, appname)
#打开proj
proj = Xcodeproj::Project.open(proj_path)
puts "打开了项目#{proj}"
proj.targets.each do |target|
# puts target.copy_files_build_phases
if target.to_s == targetName
target.build_configurations.each do |b|
#修改版本号
b.build_settings['MARKETING_VERSION'] = version
#修改build
b.build_settings['CURRENT_PROJECT_VERSION'] = build
#修改bundleid
b.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = bundleid
#修改team
b.build_settings['team'] = certificate_name
#PROVISIONING_PROFILE_SPECIFIER
b.build_settings['PROVISIONING_PROFILE_SPECIFIER'] = mobileprovision_name
#PROVISIONING_PROFILE
b.build_settings['PROVISIONING_PROFILE'] = mobileprovision_uuid
end
end
proj.save()
puts "修改基本信息完成"
3.修改三方key、baseurl等
# 自定义修改文件内容 obj(hash)
def updateCustomFileContent(path, obj)
#目标文本
targetTxt = ""
File.open(path, 'r') do |f|
targetTxt = f.read()
end
File.open(path, 'r') do |f|
fs = f.readlines
resf = targetTxt
fs.each do |line|
obj.each_key do |key|
#匹配有对应字段的行 若则匹配有误 请自行修改正则
regex = /#{key}(.*?)=/
if regex.match(line)
#替换文本
newline = modifierTxt(line, obj[key])
resf = resf.gsub(line, newline)
end
end
end
#写入文件
File.open(path, 'w') do |f|
f.write(resf)
end
end
end
puts "修改内部文件"
puts configObj
commonFilePaths = [
"#{proj_path}/Config.h"
]
commonFilePaths.each do |path|
updateCustomFileContent(path, configObj)
end
puts "内部文件内容修改完成"
4.修改appIcon、其他资源图片
注意不能使用jpg格式 不然chunky_png可能会读取不出来
替换appIcon 只需将不同尺寸的Icon添加到images/appIcon文件夹下就好 已经通过chunky_png 实现解析尺寸覆盖原来的图片
替换其他资源图片 需要和项目中的资源文件夹名称相同 具体查看源码
# 自定义修改文件内容 obj(hash)
def updateCustomFileContent(path, obj)
#目标文本
targetTxt = ""
File.open(path, 'r') do |f|
targetTxt = f.read()
end
File.open(path, 'r') do |f|
fs = f.readlines
resf = targetTxt
fs.each do |line|
obj.each_key do |key|
#匹配有对应字段的行 若则匹配有误 请自行修改正则
regex = /#{key}(.*?)=/
if regex.match(line)
#替换文本
newline = modifierTxt(line, obj[key])
resf = resf.gsub(line, newline)
end
end
end
#写入文件
File.open(path, 'w') do |f|
f.write(resf)
end
end
end
#替换appIcon
def updateAppIcon(iconPath)
puts iconPath
#替换icon
oriIconPath = "images/appIcon"
iconNames = {
40 => ["[email protected]"],
58 => ["[email protected]"],
60 => ["[email protected]"],
80 => ["[email protected]"],
87 => ["[email protected]"],
120 => ["[email protected]", "[email protected]"],
180 => ["[email protected]"],
1024 => ["icon.png"]
}
#删除原先文件
Dir.foreach(iconPath) do |f|
if File::file?("#{iconPath}/#{f}")
puts "del----#{iconPath}/#{f}"
File::delete("#{iconPath}/#{f}")
end
end
images = []
Dir.foreach(oriIconPath) do |name|
if name.include?('png')
img_path = "#{oriIconPath}/#{name}"
img = ChunkyPNG::Image.from_file(img_path)
img_wid = img.dimension.width
targetNames = iconNames[img_wid]
if targetNames == nil
next
end
iconNames.delete(img_wid)
targetNames.each do |targetName|
puts targetName
scale = "1x"
if targetName.include?("x")
scale = /(?<=@).*?(?=.png)/.match(targetName).to_s
end
target_path = "#{iconPath}/#{targetName}"
FileUtils.cp(img_path, target_path)
puts scale.class
size = img_wid / scale.to_i
puts size
puts size.class
puts size.to_s == "1024"
idiom = "iphone"
if size.to_s == "1024"
idiom = "ios-marketing"
end
puts idiom
obj = {
"filename" => targetName,
"idiom" => idiom,
"scale" => scale,
"size" => "#{size}x#{size}",
}
images.push(obj)
puts images
end
end
end
#写入json文件
img_json = {
"images" => images,
"info" => {
"version" => 1,
"author" => "xcode"
}
}
json_path = "#{iconPath}/Contents.json"
File.open(json_path, 'w') do |f|
f.write(img_json.to_json)
end
end
#遍历一个文件夹
def browseImageDirectory(route, target_path)
filepath = "images/other#{route}"
puts filepath
Dir.foreach(filepath) do |subPath|
# puts subPath
# puts File::directory?(subPath)
if subPath != ".." && subPath != "."
if subPath.include?(".imageset")
puts subPath
#删除原来的文件
to_path = "#{target_path}#{route}/#{subPath}"
puts to_path
Dir.foreach(to_path) do |f|
if File::file?("#{to_path}/#{f}")
File::delete("#{to_path}/#{f}")
end
end
#转移里面的image
images = []
Dir.foreach("#{filepath}/#{subPath}") do |file|
if file.include?("@2x") || file.include?("@3x")
puts file
#替换图片
FileUtils.cp("#{filepath}/#{subPath}/#{file}", "#{to_path}/#{file}")
#拼凑json文件
scale = /(?<=@).*?(?=.png)/.match(file)
puts scale
obj = {
"idiom" => "universal",
"filename" => file,
"scale" => scale,
}
images.push(obj)
puts images
end
end
img_json = {
"images" => images,
"info" => {
"version" => 1,
"author" => "xcode"
}
}
puts img_json
#写入json文件
json_path = "#{to_path}/Contents.json"
File.open(json_path, 'w') do |f|
f.write(img_json.to_json)
end
elsif File::directory?("#{filepath}/#{subPath}")
#如果是文件夹 继续遍历
browseImageDirectory("#{route}/#{subPath}", target_path)
end
end
end
end
#替换其他图片 1.必须原工程中存在且文件夹名称相同 2.只替换2x 3x文件
def updateOtherImages(path)
browseImageDirectory("", path)
end
# icon等资源文件替换
def updateImages(path)
puts "开始替换图片"
updateAppIcon("#{path}/AppIcon.appiconset")
updateOtherImages(path)
puts "替换图片结束"
end
#修改icon
#====================
updateImages("#{projpath}/repackage/Assets.xcassets")
5.打包导出
导出需要用到ExportOptions.plist 文件 将获取到的信息 填充到我们之前准备好的文件中
#==================打包===================
puts "开始打包"
build_path = "#{workpath}/build"
archive_path = "#{build_path}/app.xcarchive"
if File::exists?(build_path)
system "rm -rf #{build_path}"
end
FileUtils.makedirs(build_path)
#1.archive
archive_flag = system "xcodebuild archive -project #{proj_path} -scheme #{targetName} -configuration Release -archivePath #{archive_path}"
if !archive_flag
puts "archive 失败"
exit 1
end
puts "archive完成 开始导出ipa "
method = judgeMobileProvisionType(mobileprovision_path)
#生成plist
plistTxt = ""
File.open('exportplist/ExportOptions.plist','r') do |f|
plistTxt = f.read()
end
plistTxt = plistTxt.gsub("$method", method)
plistTxt = plistTxt.gsub("$boundid", bundleid)
plistTxt = plistTxt.gsub("$mobileprofilename", mobileprovision_name)
plistTxt = plistTxt.gsub("$teamID", teamId)
plist_path = "#{build_path}/ExportOptions.plist"
File.open(plist_path,'w') do |f|
f.write(plistTxt)
end
# 导出ipa
ipa_path = "#{build_path}/app"
result = system "xcodebuild -exportArchive -archivePath #{archive_path} -exportPath #{ipa_path} -exportOptionsPlist #{plist_path}"
if result
puts "导出成功"
else
puts "导出失败"
exit 0
end
#删除archive
FileUtils.cp("#{ipa_path}/#{targetName}.ipa", "#{build_path}/app.ipa")
system("rm -rf #{ipa_path}")
system("rm -rf #{plist_path}")
system("rm -rf #{archive_path}")
system("open #{build_path}")
printInterestingLog()
使用
- 将json文件内容修改为自己想要的内容
- 把provision file文件拖入到mobileprovision文件夹内
- 进入到根目录 执行
ruby repackage.rb
执行成功 获取到ipa包 对应的资源已全部修改完毕
至此一个自动化打包的脚本就完成了,但是证书部分还是得手动去添加。推荐一个功能十分强大的工具 能实现证书部分的自动化: fastlane。等成功实现证书部分的自动化之后再写一篇记录一下。最后贴上源码git地址,仅供参考。
参考文献
ruby菜鸟教程
iOS自动打包之xcodeproj
xcodeproj官方文档
chunky_png等