草庐IT

iOS 底层原理38:自动化打包(一)Xcode + Shell脚本

Style_月月 2023-03-28 原文

iOS 底层原理 文章汇总

Apple提供的常规打包方式主要是由Xcode支持的,下面展开来聊聊

Xcode打包

Xcode的打包主要分为两步:

  • Archive:对target进行编译、归档,生成.xcarchive 文件
  • Export:对生成的.xcarchive 文件进行进一步的处理,生成不同渠道的ipa包,进行分发

Archive编译归档

Archive主要是对target进行编译、归档,生成.xcarchive 文件。可在Xcode -> Window -> Organizer -> Archives中查看

archive_01.jpg

archive_02

archive_03

这里所说的归档,主要就对项目源码进行编译后,再将编译生成的各种文件、资源、记录统一封装到一个文件中,便于管理和回溯。

随机选择一个.xcarchive文件,点击show in Finder,可以看到一个.xcarchive 后缀的文件


xcarchive_01.jpg

xcarchive_02.jpg

这个.xcarchive文件包含了应用、符号表信息以及其他的资源,可以右键 -> 显示包内容进行查看,主要包含以下文件:


xcarchive文件类型
  • BCSymbolMaps:Xcode 对 BitCode 符号表进行混淆(Symbol Hiding)后生成的对照表,和 dSYM 文件会一一对应

  • dSYMs:存储此次编译的符号表(debug symbols),用来符号化解析崩溃堆栈

  • info.plist: 项目target的配置文件

  • Products:存储此次编译生成的的 App 包(.app)。虽然这个文件包含了App 运行需要的可执行文件以及其它资源,但是和最终用户下载的版本会有所不同。后续的 export 操作会对其进行进一步处理。

  • SCMBlueprint:如果 Xcode 打开了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夹会存储此次编译的版本控制信息,包括使用的 git 版本、仓库、分支等。如果需要回溯此次编译的源码版本,可以在这个文件中找到对应的信息。

  • SwiftSupport:在 Target 的 Build Settings 中打开了ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES,此次编译使用的 Swift 版本对应的标准库文件(.dylib)会被放到这个文件夹中。发布App时,也会被复制到ipa bundle中。在iOS 12.2及以上,swift的ABI趋于稳定,已经不用再自带链接库了,因此ipa包节省了一定的体积

Export打包分发

Export主要是的对生成的.xcarchive 文件进行进一步的处理,生成不同渠道的ipa包,进行分发,Organize -> Archive文件 -> Distribute App

Export_01

这里对应4种分发渠道:AppStore、Ad Hoc、Enterprise和Development,然后一步步往下操作即可。


Export_02.jpg

以上4中渠道对应的打包method分别是app-store、ad-hoc、enterprise、development,可以在导出的文件夹中ExportOptions.plist文件看到对应参数,如下所示

method参数.jpg

最终Export导出的文件夹中主要包含以下4种文件


导出ipa的文件夹内容.jpg
  • DistributionSummary.plist:包含ipa所支持的架构、bitcode、证书、embeddedBinaries(非系统的动态库相关)、entitlements(apns环境、application-identifier等)、profile等相关信息

  • ExportOptions.plist:ipa包导出的配置文件,主要包含包导出方式、签名方式、App Developer team的id等

  • MIStore.ipa:应用的ipa包,包含了App所需的签名、二进制包、资源等

  • Packaging.log:打包相关的日志

如果想要查看ipa中的内容,可以将.ipa改成.zip,然后解压,也可使用命令解压
zip -0 -y -r myAppName.ipa Payload/,其中是.app的文件,查看其包内容,主要包含以下几部分

  • MachO可执行文件:具体的介绍可查看iOS逆向 12:Mach-O文件(上)iOS逆向 12:Mach-O文件(下)
  • 签名文件:App 的签名信息会被放到 _CodeSignature 文件夹中。
  • info.plist:存储 App 主要信息的 plist 文件也会被一并打包到 ipa 中。
  • entitlements:App包含的相关权限,在这个文件中通过 xml 的格式将这些授权记录下来,例如Push notifications、App Group等
  • App Plugins:如果App实现了 App Extension扩展,扩展的包会以 .appex 的后缀存储在 PlugIns 文件夹中,随着App的安装一起安装到用户手机上
  • 链接库:App 运行所需要的各种链接库会被放入 Frameworks 文件夹。
  • 资源文件:App 运行需要的各种资源文件也是 ipa 体积的大头。常见的有
    • 各种多媒体资源:图片、音视频
    • xib 文件:.nib.storyboardc
    • 各种打包的资源 .bundle
    • 其它类型的资源:字体、数据库、证书等等

除了Xcode自带的工具application loader上传ipa,还可以通过Transporter上传。Transporter可以以简单轻松的方式将内容交付到Apple,提供如下功能:

  • 只需要将ipa包拖放到Transporter中即可开始使用
  • 同时验证和上传多个文件以快速交付
  • 查看交付进度(警告、错误和交付日志),以便快速修复交付问题
  • 查看历史交付记录(包含日期、时间)

所以,综上所述,通过Xcode提供的打包方式比较繁琐,需要人为操作许多步骤,那么我们是否可以通过其他的方式进行简化呢?答案是当然可以呀。

简化打包操作的方式的主要有两种

  • Shell打包脚本
  • Jenkins部署自动打包

这里我们主要介绍Shell打包脚本的方式,主要分为以下几步

  • 配置打包相关的名称:.xcworkspace的名字、scheme名称、编译方式、打包方式、profile文件、bundleID等
  • 配置打包的相关路径:导出路径、archive文件路径、ipa文件路径、ExportOptions.plist文件路径
  • 清理工程:通过xcodebuild clean清理
  • 编译:通过xcodebuild archive编译、归档工程
  • 导出ipa:通过xcodebuild命令将.xcarchive导出为ipa文件
  • 校验并上传AppStore:如果是AppStore的包,通过xcrun altool校验并上传AppStore

使用方式:进入podfile所在目录,在终端执行.sh文件,例如:sh xxxx.sh

具体的shell脚本如下(注:使用时,需要自行填充其中的xxxxx所在的脚本代码)

#!/bin/bash

# 前提:enterprise打包时,需要切换到 企业账户 及 BundleID

# 使用方法:
# step1: 将该脚本放在工程的根目录下(跟.xcworkspace文件or .xcodeproj文件同目录)
# step2: 根据情况修改下面的参数
# step3: 打开终端,执行脚本。(输入sh,然后将脚本文件拉到终端,会生成文件路径,然后enter就可)

echo "-----------开始执行脚本-----------"

# =============项目自定义部分(自定义好下列参数后再执行该脚本)=================== #
echo "请选择打包方式 ? [1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]"

read number
while([[ $number != 1 ]] && [[ $number != 2 ]] && [[ $number != 3 ]] && [[ $number != 4 ]])
do
echo "Error! Should enter 1 or 2 or 3 or 4"
echo "请选择打包方式 ? [ 1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]"
read number
done

#-----------脚本配置信息-----------
# .xcworkspace的名字,必填
workspace_name="xxxxx"

# 指定项目的scheme名称(也就是工程的target名称),必填
scheme_name="xxxxx"

# 指定要打包编译的方式 : Release,Debug。一般用Release。必填
build_configuration="Release"

# method,打包的方式。方式分别为 development, ad-hoc 。必填
method="enterprise"

#  下面两个参数只是在手动指定Pofile文件的时候用到,如果使用Xcode自动管理Profile,直接留空就好
# (跟method对应的)mobileprovision文件名,需要先双击安装.mobileprovision文件.手动管理Profile时必填
# mobileprovision_name="9d8c7290-4345-4ebf-82d4-a74cab2ea40b"
mobileprovision_name=""

# 项目的bundleID,手动管理Profile时必填
bundle_identifier=""
# if [[ $number == 1 ]]; then
#     bundle_identifier="com.mi.global.sho"
# else
#     bundle_identifier="com.mi.global.shop"
# fi

# 每次编译后是否Build自动加1,
# 可以修改该常量的值,以决定编译后还是打包后Build自动加1
# #  0: 每次打包后Build自动加1  
# #  1: 每次编译后Build自动加1  
DEBUG_ENVIRONMENT_SYMBOL=0


# 根据选项配置不同的包
if [ $number == 1 ];then 
build_configuration="Debug"
method="enterprise"
DEBUG_ENVIRONMENT_SYMBOL=1
elif [[ $number == 2 ]]; then
build_configuration="Release"
method="enterprise"
DEBUG_ENVIRONMENT_SYMBOL=1
elif [[ $number == 3 ]]; then
build_configuration="Release"
method="ad-hoc"
DEBUG_ENVIRONMENT_SYMBOL=1
else 
build_configuration="Release"
method="app-store"
DEBUG_ENVIRONMENT_SYMBOL=1
fi

echo "--------------------脚本配置参数检查--------------------"
echo "\033[33;1mworkspace_name = ${workspace_name}"
echo "scheme_name = ${scheme_name}"
echo "build_configuration = ${build_configuration}"
echo "bundle_identifier = ${bundle_identifier}"
echo "method = ${method}"
echo "mobileprovision_name = ${mobileprovision_name} \033[0m"

# =======================脚本的一些固定参数定义(无特殊情况不用修改)====================== #
# 获取当前脚本所在目录
script_dir="$( cd "$( dirname "$0"  )" && pwd  )"
# 工程根目录
project_dir=$script_dir

# 指定输出导出文件夹路径
export_path="$project_dir/Build"
# 指定输出归档文件路径
export_archive_path="$export_path/$scheme_name.xcarchive"
# 指定输出ipa文件夹路径
export_ipa_path="$export_path/"
# 指定导出ipa包需要用到的plist配置文件的路径
export_options_plist_path="$project_dir/ExportOptions.plist"

echo "--------------------脚本固定参数检查--------------------"
echo "\033[33;1mproject_dir = ${project_dir}"
echo "export_path = ${export_path}"
echo "export_archive_path = ${export_archive_path}"
echo "export_ipa_path = ${export_ipa_path}"
echo "export_options_plist_path = ${export_options_plist_path}\033[0m"

# =======================自动打包部分(无特殊情况不用修改)====================== #
echo "------------------------------------------------------"
echo "\033[32m开始构建项目  \033[0m"
# 进入项目工程目录
cd ${project_dir}

# 指定输出文件目录不存在则创建
if [ -d "$export_path" ];
then rm -rf "$export_path"
fi

/usr/bin/xcrun xcodebuild -UseNewBuildSystem=YES -xcconfig InnerXcconfig/innerInner/tt.xcconfig

# 编译前清理工程
xcodebuild clean -workspace ${workspace_name}.xcworkspace \
                 -scheme ${scheme_name} \
                 -configuration ${build_configuration}

xcodebuild archive -workspace ${workspace_name}.xcworkspace \
                   -scheme ${scheme_name} \
                   -configuration ${build_configuration} \
                   -archivePath ${export_archive_path}

#  检查是否构建成功
#  xcarchive 实际是一个文件夹不是一个文件所以使用 -d 判断
if [ -d "$export_archive_path" ] ; then
    echo "\033[32;1m项目构建成功 ? ? ?  \033[0m"
else
    echo "\033[31;1m项目构建失败 ? ? ?  \033[0m"
    exit 1
fi
echo "------------------------------------------------------"

echo "\033[32m开始导出ipa文件 \033[0m"

# 先删除export_options_plist文件
if [ -f "$export_options_plist_path" ] ; then
    #echo "${export_options_plist_path}文件存在,进行删除"
    rm -f $export_options_plist_path
fi

# 根据参数生成export_options_plist文件
/usr/libexec/PlistBuddy -c  "Add :method String ${method}"  $export_options_plist_path
/usr/libexec/PlistBuddy -c  "Add :provisioningProfiles:"  $export_options_plist_path
/usr/libexec/PlistBuddy -c  "Add :provisioningProfiles:${bundle_identifier} String ${mobileprovision_name}"  $export_options_plist_path
/usr/libexec/PlistBuddy -c  "Add :compileBitcode bool NO" $export_options_plist_path


xcodebuild  -exportArchive \
            -archivePath ${export_archive_path} \
            -exportPath ${export_ipa_path} \
            -exportOptionsPlist ${export_options_plist_path} \
            -allowProvisioningUpdates

# 检查文件是否存在
if [ -f "$export_ipa_path/$scheme_name.ipa" ] ; then
    echo "\033[32;1m导出 ${scheme_name}.ipa 包成功 ?  ?  ?   \033[0m"
    open $export_path
else
    echo "\033[31;1m导出 ${scheme_name}.ipa 包失败 ? ? ?     \033[0m"
    exit 1
fi

# 删除export_options_plist文件(中间文件)
if [ -f "$export_options_plist_path" ] ; then
    #echo "${export_options_plist_path}文件存在,准备删除"
    rm -f $export_options_plist_path
fi

# 输出打包总用时
echo "\033[36;1m使用AutoPackageScript打包总用时: ${SECONDS}s \033[0m"
echo "------------------------------------------------------"

# AppStore上传到xxx
if [ $number == 4 ];then 
        # 将包上传AppStore  
        ipa_path="$export_ipa_path/$scheme_name.ipa"
        # 上传AppStore的密钥ID、Issuer ID
        api_key="xxxxx"
        issuer_id="xxxxx"

    echo "--------------------AppStore上传固定参数检查--------------------"
    echo "ipa_path = ${ipa_path}"
    echo "api_key = ${api_key}"
    echo "issuer_id = ${issuer_id}"

# 校验 + 上传 方式1
    # # 校验指令
    # cnt0=`xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose`
    # echo $cnt0
    # cnt=`echo $cnt0 | grep “No errors validating archive” | wc -l`

    # if [ $cnt = 1 ] ; then
    #     echo "\033[32;1m校验IPA成功?  ?  ? \033[0m"
    #     echo "------------------------------------------------------"
    #     cnt0=`xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"`
    #     echo $cnt0
    #     cnt=`echo $cnt0 | grep “No errors uploading” | wc -l`
    #     if [ $cnt = 1 ] ; then
    #         echo "\033[32;1m上传IPA成功?  ?  ? \033[0m"
    #         echo "------------------------------------------------------"
            
    #     else
    #         echo "\033[32;1m上传IPA失败? ? ?   \033[0m"
    #         echo "------------------------------------------------------"
    #     fi
    # else
    #     echo "\033[32;1m校验IPA失败? ? ?   \033[0m"
    #     echo "------------------------------------------------------"
    # fi

# 校验 + 上传 方式2
    # 验证
    validate="xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"
    echo "running validate cmd" $validate
    validateApp="$($validate)"
    if [ -z "$validateApp" ]; then
        echo "\033[32m校验IPA失败? ? ?   \033[0m"
        echo "------------------------------------------------------"
    else
        echo "\033[32m校验IPA成功?  ?  ?  \033[0m"
        echo "------------------------------------------------------"
        
        # 上传
        upload="xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"
        echo "running upload cmd" $upload
        uploadApp="$($upload)"
        echo uploadApp
        if [ -z "$uploadApp" ]; then
            echo "\033[32m传IPA失败? ? ?   \033[0m"
            echo "------------------------------------------------------"
        else
            echo "\033[32m上传IPA成功?  ?  ? \033[0m"
            echo "------------------------------------------------------"
        fi

    fi

fi

exit 0

这里额外补充下xcodebuild、xcrun命令的知识:

xcodebuild
用于编译xcode中的projects和workspaces,可通过man xcodebuild查看文档

iOS_lowlevel_38_01_12.jpg

常见的命令格式如下:

  • 清理工程:
xcodebuild clean -workspace [xcworkspace路径] \
                -scheme [scheme名称] \
                -configuration [编译方式Release或Debug]
  • 编译工程
xcodebuild archive -workspace [xcworkspace路径] \
                   -scheme [scheme名称]\
                   -configuration [编译方式Release或Debug] \
                   -archivePath [.xcarchive文件路径]
  • 导出ipa
xcodebuild  -exportArchive \
            -archivePath [.xcarchive文件路径] \
            -exportPath [.ipa文件路径] \
            -exportOptionsPlist [ExportOptions.plist文件路径] \
            -allowProvisioningUpdates

xcrun
用于运行或定位开发工具以及属性,也可以通过man xcrun查看具体文档

xcrun文档.jpg

常见的命令格式如下:

  • 校验ipa
xcrun altool --validate-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose
  • 上传ipa到AppStore
xcrun altool --upload-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose

其中上传所需的api key 和 issuer id需要在AppStore Connect中配置和获取。使用具有账户管理权限的账号登陆App Store Connect 选择 用户与访问 >密钥 查询信息:issuser和apiKey(密钥 ID),如下所示,

密钥.jpg

并将公钥下载到本地,并将下载好的p8文件保存到需要放到一个固定目录下

P8文件放入以下文件中

./private_keys
~/private_keys
~/.private_keys
~/.appstoreconnect/private_keys

除了使用xcrun altool上传ipa,我们还可以使用Transpoter,但相对来说,Shell脚本更方便,实现了本地自动化打包上传。

综上所述,shell脚本打包整体流程如下


shell脚本打包流程图示

有关iOS 底层原理38:自动化打包(一)Xcode + Shell脚本的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  3. ruby-on-rails - 如何在 ruby​​ 交互式 shell 中有多行? - 2

    这可能是个愚蠢的问题。但是,我是一个新手......你怎么能在交互式ruby​​shell中有多行代码?好像你只能有一条长线。按回车键运行代码。无论如何我可以在不运行代码的情况下跳到下一行吗?再次抱歉,如果这是一个愚蠢的问题。谢谢。 最佳答案 这是一个例子:2.1.2:053>a=1=>12.1.2:054>b=2=>22.1.2:055>a+b=>32.1.2:056>ifa>b#Thecode‘if..."startsthedefinitionoftheconditionalstatement.2.1.2:057?>puts"f

  4. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  5. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  6. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  7. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  8. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  9. ruby-on-rails - rbenv:从 RVM 移动到 rbenv 后,在 Jenkins 执行 shell 中找不到命令 - 2

    我从Ubuntu服务器上的RVM转移到rbenv。当我使用RVM时,使用bundle没有问题。转移到rbenv后,我在Jenkins的执行shell中收到“找不到命令”错误。我内爆并删除了RVM,并从~/.bashrc'中删除了所有与RVM相关的行。使用后我仍然收到此错误:rvmimploderm~/.rvm-rfrm~/.rvmrcgeminstallbundlerecho'exportPATH="$HOME/.rbenv/bin:$PATH"'>>~/.bashrcecho'eval"$(rbenvinit-)"'>>~/.bashrc.~/.bashrcrbenvversions

  10. ruby-on-rails - 有没有一种工具可以在编码时自动保存对文件的增量更改? - 2

    我最喜欢的Google文档功能之一是它会在我工作时不断自动保存我的文档版本。这意味着即使我在进行关键更改之前忘记在某个点进行保存,也很有可能会自动创建一个保存点。至少,我可以将文档恢复到错误更改之前的状态,并从该点继续工作。对于在MacOS(或UNIX)上运行的Ruby编码器,是否有具有等效功能的工具?例如,一个工具会每隔几分钟自动将Gitcheckin我的本地存储库以获取我正在处理的文件。也许我有点偏执,但这点小保险可以让我在日常工作中安心。 最佳答案 虚拟机有些人可能讨厌我对此的回应,但我在编码时经常使用VIM,它具有自动保存功

随机推荐