美文网首页ios进阶酷
ios 自动化打包-基于xcodeproj

ios 自动化打包-基于xcodeproj

作者: chenhao | 来源:发表于2022-03-19 17:38 被阅读0次

    背景

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

    思路

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

    准备

    需要用到的工具

    #安装方法
    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
    

    修改前项目

    地址

    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 => ["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
            #转移里面的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

        本文链接:https://www.haomeiwen.com/subject/qdygdrtx.html