ios 自动化打包-基于xcodeproj

背景

因公司做的都是一些独立部署的项目,经常会遇到以下这种情况。项目经理新拿到一个项目,过来说XX给我包打一下。简而言之就是代码都是同一份代码,但是需要替换app里面的图标&启动图&三方key&标题&bundleid等等等。虽然不是什么复杂的工作,但是设想一下,当你写着代码,你需要贮藏掉改动的代码,检出新打包分支的代码,还需要上苹果开发者后台申请证书,导出moboileprovision文件。(开发者后台还卡的要死 -_-),弄好证书后还要将代码里的配置修改掉,替换掉三方的appkey,替换掉域名等一些列操作。差不多都个把小时花进去了。而且公司一个项目至少还得打2个包。还有些时候打完包,测试说app请求失败,发现是域名打错了,又得重新打包。这做的都是一些重复性极高的事情,对于自身的提升来说毫无意义。因此想到让代码来代替我们做这部分的工作。理想情况,起一个web服务 让项目经理自行去修改配置文件,然后导出相对应的包(再配个模块应用功能选择,这不就是涂鸦的管理后台吗 ),现有资源无法匹配,自身技术还不能实现,因此先实现一个本地自动化的脚本提高效率。

思路

  1. 从git上拉取代码
  2. 修改bundleid、displayname、provision file等
  3. 修改三方key、baseurl等
  4. 修改appIcon、其他资源图片
  5. 打包导出

准备

需要用到的工具

  • 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

修改前项目

地址

20220319141448.jpg

20220319141506.jpg
20220319141523.jpg

在ruby脚本项目中创建这3个文件夹 用于存放相关信息

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

开始

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: 证书名称 可以从钥匙串中获取 注意替换成自己的


20220319175143.jpg

// 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()

使用

  1. 将json文件内容修改为自己想要的内容
  2. 把provision file文件拖入到mobileprovision文件夹内
  3. 进入到根目录 执行
ruby repackage.rb

执行成功 获取到ipa包 对应的资源已全部修改完毕


20220319173705.jpg
20220319172029.jpg
20220319170535.jpg

至此一个自动化打包的脚本就完成了,但是证书部分还是得手动去添加。推荐一个功能十分强大的工具 能实现证书部分的自动化: fastlane。等成功实现证书部分的自动化之后再写一篇记录一下。最后贴上源码git地址,仅供参考。

参考文献
ruby菜鸟教程
iOS自动打包之xcodeproj
xcodeproj官方文档
chunky_png等

你可能感兴趣的:(ios 自动化打包-基于xcodeproj)