背景
因公司做的都是一些獨(dú)立部署的項(xiàng)目监氢,經(jīng)常會遇到以下這種情況绎谦。項(xiàng)目經(jīng)理新拿到一個項(xiàng)目,過來說XX給我包打一下定续。簡而言之就是代碼都是同一份代碼谍咆,但是需要替換app里面的圖標(biāo)&啟動圖&三方key&標(biāo)題&bundleid等等等禾锤。雖然不是什么復(fù)雜的工作,但是設(shè)想一下摹察,當(dāng)你寫著代碼恩掷,你需要貯藏掉改動的代碼,檢出新打包分支的代碼供嚎,還需要上蘋果開發(fā)者后臺申請證書黄娘,導(dǎo)出moboileprovision文件。(開發(fā)者后臺還卡的要死 -_-)查坪,弄好證書后還要將代碼里的配置修改掉寸宏,替換掉三方的appkey,替換掉域名等一些列操作。差不多都個把小時花進(jìn)去了偿曙。而且公司一個項(xiàng)目至少還得打2個包氮凝。還有些時候打完包,測試說app請求失敗望忆,發(fā)現(xiàn)是域名打錯了罩阵,又得重新打包。這做的都是一些重復(fù)性極高的事情启摄,對于自身的提升來說毫無意義稿壁。因此想到讓代碼來代替我們做這部分的工作。理想情況歉备,起一個web服務(wù) 讓項(xiàng)目經(jīng)理自行去修改配置文件傅是,然后導(dǎo)出相對應(yīng)的包(再配個模塊應(yīng)用功能選擇,這不就是涂鴉的管理后臺嗎?? )蕾羊,現(xiàn)有資源無法匹配喧笔,自身技術(shù)還不能實(shí)現(xiàn),因此先實(shí)現(xiàn)一個本地自動化的腳本提高效率龟再。
思路
- 從git上拉取代碼
- 修改bundleid书闸、displayname、provision file等
- 修改三方key利凑、baseurl等
- 修改appIcon浆劲、其他資源圖片
- 打包導(dǎo)出
準(zhǔn)備
需要用到的工具
- ruby
- xcodeproj 用于修改項(xiàng)目配置
#安裝方法
gem install xcodeproj
- mobileprovision-read 用于讀取mobileprovision 相關(guān)配置信息
#安裝方法
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
修改前項(xiàng)目
在ruby腳本項(xiàng)目中創(chuàng)建這3個文件夾 用于存放相關(guān)信息
- 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 去配置讀取相關(guān)信息:config.json
// team: 證書名稱 可以從鑰匙串中獲取 注意替換成自己的
// json文件上半部分 是項(xiàng)目配置信息 下半部分是代碼里的一些配置信息 例如三方sdk key等 注意要和工程項(xiàng)目中的名稱對應(yīng)起來牌借,方便修改
// 有需要其他配置 自行添加
{
"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數(shù)據(jù)#{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應(yīng)用名稱 path: plist路徑 name: 目標(biāo)名稱
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 "打開了項(xiàng)目#{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等
# 自定義修改文件內(nèi)容 obj(hash)
def updateCustomFileContent(path, obj)
#目標(biāo)文本
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|
#匹配有對應(yīng)字段的行 若則匹配有誤 請自行修改正則
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 "修改內(nèi)部文件"
puts configObj
commonFilePaths = [
"#{proj_path}/Config.h"
]
commonFilePaths.each do |path|
updateCustomFileContent(path, configObj)
end
puts "內(nèi)部文件內(nèi)容修改完成"
4.修改appIcon割按、其他資源圖片
注意不能使用jpg格式 不然chunky_png可能會讀取不出來
替換appIcon 只需將不同尺寸的Icon添加到images/appIcon文件夾下就好 已經(jīng)通過chunky_png 實(shí)現(xiàn)解析尺寸覆蓋原來的圖片
替換其他資源圖片 需要和項(xiàng)目中的資源文件夾名稱相同 具體查看源碼
# 自定義修改文件內(nèi)容 obj(hash)
def updateCustomFileContent(path, obj)
#目標(biāo)文本
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|
#匹配有對應(yīng)字段的行 若則匹配有誤 請自行修改正則
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 => ["icon_20pt@2x.png"],
58 => ["icon_29pt@2x.png"],
60 => ["icon_20pt@3x.png"],
80 => ["icon_40pt@2x.png"],
87 => ["icon_29pt@3x.png"],
120 => ["icon_40pt@3x.png", "icon_60pt@2x.png"],
180 => ["icon_60pt@3x.png"],
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
#轉(zhuǎn)移里面的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}")
#如果是文件夾 繼續(xù)遍歷
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 "替換圖片結(jié)束"
end
#修改icon
#====================
updateImages("#{projpath}/repackage/Assets.xcassets")
5.打包導(dǎo)出
導(dǎo)出需要用到ExportOptions.plist 文件 將獲取到的信息 填充到我們之前準(zhǔn)備好的文件中
#==================打包===================
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完成 開始導(dǎo)出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
# 導(dǎo)出ipa
ipa_path = "#{build_path}/app"
result = system "xcodebuild -exportArchive -archivePath #{archive_path} -exportPath #{ipa_path} -exportOptionsPlist #{plist_path}"
if result
puts "導(dǎo)出成功"
else
puts "導(dǎo)出失敗"
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文件內(nèi)容修改為自己想要的內(nèi)容
- 把provision file文件拖入到mobileprovision文件夾內(nèi)
- 進(jìn)入到根目錄 執(zhí)行
ruby repackage.rb
執(zhí)行成功 獲取到ipa包 對應(yīng)的資源已全部修改完畢
至此一個自動化打包的腳本就完成了走哺,但是證書部分還是得手動去添加。推薦一個功能十分強(qiáng)大的工具 能實(shí)現(xiàn)證書部分的自動化: fastlane。等成功實(shí)現(xiàn)證書部分的自動化之后再寫一篇記錄一下丙躏。最后貼上源碼git地址择示,僅供參考。
參考文獻(xiàn)
ruby菜鳥教程
iOS自動打包之xcodeproj
xcodeproj官方文檔
chunky_png等