ios 自動(dòng)化打包-基于xcodeproj

背景

因公司做的都是一些獨(dú)立部署的項(xiàng)目,經(jīng)常會(huì)遇到以下這種情況。項(xiàng)目經(jīng)理新拿到一個(gè)項(xiàng)目,過來說XX給我包打一下。簡(jiǎn)而言之就是代碼都是同一份代碼,但是需要替換app里面的圖標(biāo)&啟動(dòng)圖&三方key&標(biāo)題&bundleid等等等。雖然不是什么復(fù)雜的工作,但是設(shè)想一下,當(dāng)你寫著代碼,你需要貯藏掉改動(dòng)的代碼,檢出新打包分支的代碼,還需要上蘋果開發(fā)者后臺(tái)申請(qǐng)證書,導(dǎo)出moboileprovision文件。(開發(fā)者后臺(tái)還卡的要死 -_-),弄好證書后還要將代碼里的配置修改掉,替換掉三方的appkey,替換掉域名等一些列操作。差不多都個(gè)把小時(shí)花進(jìn)去了。而且公司一個(gè)項(xiàng)目至少還得打2個(gè)包。還有些時(shí)候打完包,測(cè)試說app請(qǐng)求失敗,發(fā)現(xiàn)是域名打錯(cuò)了,又得重新打包。這做的都是一些重復(fù)性極高的事情,對(duì)于自身的提升來說毫無意義。因此想到讓代碼來代替我們做這部分的工作。理想情況,起一個(gè)web服務(wù) 讓項(xiàng)目經(jīng)理自行去修改配置文件,然后導(dǎo)出相對(duì)應(yīng)的包(再配個(gè)模塊應(yīng)用功能選擇,這不就是涂鴉的管理后臺(tái)嗎?? ),現(xiàn)有資源無法匹配,自身技術(shù)還不能實(shí)現(xiàn),因此先實(shí)現(xiàn)一個(gè)本地自動(dòng)化的腳本提高效率。

思路

  1. 從git上拉取代碼
  2. 修改bundleid、displayname、provision file等
  3. 修改三方key、baseurl等
  4. 修改appIcon、其他資源圖片
  5. 打包導(dǎo)出

準(zhǔn)備

需要用到的工具

  • ruby
  • xcodeproj 用于修改項(xiàng)目配置
#安裝方法
gem install xcodeproj
#安裝方法
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 -
gem install chunky_png

修改前項(xiàng)目

地址

20220319141448.jpg

20220319141506.jpg
20220319141523.jpg

在ruby腳本項(xiàng)目中創(chuàng)建這3個(gè)文件夾 用于存放相關(guān)信息

  • exportplist: export ipa文件需要用到ExportOptions.plist文件
  • images-appIcon 用于存放需要替換的appIcon
  • images-other 用于存放其他的圖片資源
  • mobileprovision 用于存放provision file 文件
20220319144323.jpg

開始

1.從git上拉取代碼
def gitcloneCode(path, barch)
  #替換成自己的代碼庫(kù)地址
  puts "下拉代碼"
  targetGitUrl = path
  targetBranch = barch
  system "git clone -b #{targetBranch} #{targetGitUrl}"
  system "git branch"
  puts "代碼下拉完成"
end
2. 修改bundleid、displayname、provision file等

可以使用一個(gè)json 去配置讀取相關(guān)信息:config.json
// team: 證書名稱 可以從鑰匙串中獲取 注意替換成自己的


20220319175143.jpg

// json文件上半部分 是項(xiàng)目配置信息 下半部分是代碼里的一些配置信息 例如三方sdk key等 注意要和工程項(xiàng)目中的名稱對(duì)應(yīng)起來,方便修改

// 有需要其他配置 自行添加
{
 "appname": "測(cè)試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|
      #修改版本號(hào)
      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|
        #匹配有對(duì)應(yīng)字段的行  若則匹配有誤 請(qǐ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可能會(huì)讀取不出來
替換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|
        #匹配有對(duì)應(yīng)字段的行 若則匹配有誤 請(qǐ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

#遍歷一個(gè)文件夾
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()

使用

  1. 將json文件內(nèi)容修改為自己想要的內(nèi)容
  2. 把provision file文件拖入到mobileprovision文件夾內(nèi)
  3. 進(jìn)入到根目錄 執(zhí)行
ruby repackage.rb

執(zhí)行成功 獲取到ipa包 對(duì)應(yīng)的資源已全部修改完畢


20220319173705.jpg
20220319172029.jpg
20220319170535.jpg

至此一個(gè)自動(dòng)化打包的腳本就完成了,但是證書部分還是得手動(dòng)去添加。推薦一個(gè)功能十分強(qiáng)大的工具 能實(shí)現(xiàn)證書部分的自動(dòng)化: fastlane。等成功實(shí)現(xiàn)證書部分的自動(dòng)化之后再寫一篇記錄一下。最后貼上源碼git地址,僅供參考。

參考文獻(xiàn)
ruby菜鳥教程
iOS自動(dòng)打包之xcodeproj
xcodeproj官方文檔
chunky_png

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容