
简介面向iOS开发、测试及需要研究IPA打包流程的技术人员这份ipa转换工具围绕.app等软件包向IPA格式转换、代码签名与配置信息调整展开可用于理解非App Store来源应用的打包、签名及安装前处理思路。压缩包为rar格式共31个文件约447KB包含可执行程序、C源码与头文件、工程配置、plist属性表、图标及png素材等既保留可运行工具也便于从源码层面梳理转换逻辑与元数据修改环节。已有10869人学习下载说明其在iOS应用打包与分发研究场景中具有一定参考价值。读者可据此了解IPA结构、签名所需配置及工程组织方式并在合法合规前提下用于开发测试、学习排错与流程验证。1. ipa转换工具的真面目zip 换个后缀为什么装不上有人拿到一个.app目录或者一个早年留下的.deb包想装到测试机上第一反应是右键压缩、把后缀改成.ipa结果安装器只回一句“无法安装此 App”连原因都不给。ipa转换工具真正要做的不是压缩而是把内容摆成Payload/xxx.app这个固定结构再让签名、架构、Info.plist 这三件事互相对齐。做内测分发、维护历史版本包、给私有渠道出包的移动端开发和运维基本都绕不开这一步。下面按体检、打包、重签、排错的顺序走一遍中间会把几条踩过的坑标出来。2. 拆开一个 ipa转换前的结构体检与三种输入源分流十次转换失败里有七次问题出在输入本身不合格而不是打包命令写错。所以动手之前先做体检比事后对着安装器的报错猜要省时间得多。这一章先把 ipa 的容器结构说清楚再给一套五分钟能跑完的检查命令最后按输入源分流不同来源的包补的东西完全不一样。2.1 容器结构Payload 是唯一硬约束ipa 本质就是一个 ZIP但安装器对它的内部布局有强约束根目录下必须有一个名为Payload的目录Payload里放且只放一个.app包。这条约束之外的东西都是元数据可有可无但少了关键项同样装不上。路径是否必需作用缺失或写错的后果Payload/必需应用包的容器目录名字大小写敏感安装器直接拒绝提示无法安装Payload/xxx.app/必需应用本体含 Info.plist 与主二进制同上且无法定位可执行文件Payload/xxx.app/Info.plist必需包标识、版本号、最低系统、设备族标识非法或版本缺失时安装失败Payload/xxx.app/embedded.mobileprovision真机必需描述文件限定可安装的设备范围签名校验不通过提示描述文件缺失Payload/xxx.app/CFBundleExecutable必需主二进制权限必须是 0755权限丢失时安装成功但启动即退出SwiftSupport/可选提交商店时补 Swift 运行时库内测分发不受影响商店提交可能被拒Symbols/可选崩溃符号文件不影响安装影响事后定位iTunesMetadata.plist可选商店侧元数据不影响安装有两处容易忽略。一是Payload里不能出现第二个.app有些包解压时带了旧版本残留安装器会挑错对象二是.app目录名不必和CFBundleIdentifier一致但保持一致能省掉后面排查时的困惑。手工组包时我一般让目录名等于CFBundleExecutable跟 Xcode 出来的产物对齐。2.2 五分钟体检unzip 与 plutil 逐项过拿到任何输入包先解到一个干净的临时目录里看结构不要在原目录反复折腾否则残留文件会污染下一次打包。# 1. 解到独立目录-d 指定目标避免污染当前工作区 mkdir -p /tmp/ipa-check cd /tmp/ipa-check unzip -q /path/to/input.ipa -d unpacked # 2. 看第一层正常情况下只有一个 Payload ls -la unpacked # 3. 定位 .app-maxdepth 1 防止匹配到 Frameworks 里的嵌套包 APP$(find unpacked/Payload -maxdepth 1 -name *.app -type d | head -n 1) echo $APP # 4. 只取值不带格式-extract 配 raw 适合塞进脚本 plutil -extract CFBundleIdentifier raw $APP/Info.plist plutil -extract CFBundleVersion raw $APP/Info.plist plutil -extract MinimumOSVersion raw $APP/Info.plist # 5. 主二进制架构与权限这两项决定能不能装、装完能不能起 BIN$APP/$(plutil -extract CFBundleExecutable raw $APP/Info.plist) lipo -info $BIN ls -l $BIN这几行里参数都有讲究。unzip -d指定解包目标目录不写会把几十个文件倒进当前目录find -maxdepth 1只扫一层否则Frameworks里的.framework也会被-name *.app之外的规则误伤plutil -extract ... raw把值当纯字符串输出适合在脚本里做比较不带raw会输出带引号的格式。lipo -info告诉你二进制里有哪些架构出现i386、armv7这种老架构混在里面说明这个包年头不短后面很可能还要处理MinimumOSVersion的问题。Info.plist 在成品包里通常是二进制格式cat出来是一堆乱码别用文本编辑器硬看一律走plutil -p或plutil -extract。注意plutil -extract在键不存在时返回非零退出码脚本里如果开了set -e一次缺键就会中断整个流程。取值前先用plutil -p看一眼有哪些键或者对这一步单独做容错。体检的判据可以固化成一张小表跑完对着看检查项合格判据不合格的典型表现根目录结构只有Payload一个目录多出__MACOSX、Payload嵌套两层.app数量恰好 1 个出现 2 个安装器挑中旧包CFBundleIdentifier非空、无空格、反斜杠空值或带非法字符装到一半失败MinimumOSVersion不高于目标设备系统版本提示需要更高版本系统主二进制架构含arm64只有armv7新设备直接不认主二进制权限-rwxr-xr-x-rw-r--r--装完闪退描述文件存在且未过期装到授权外设备时报签名无效2.3 三种输入源分流.app、.xcarchive、.deb输入源的差异决定了后面要不要补签名、要不要补运行时库先分流再动手。输入源能否直接转需要补的东西常见失败点.app目录可以最省事结构合规即可签名看目标设备符号链接被展开、权限位丢失.xcarchive可以走导出流程ExportOptions 配置、签名身份导出方法选错描述文件不匹配.deb包需要先拆包从data.tar里取/Applications下的.app签名体系不同必须重签从设备导出的包可以需重组结构重新构造Payload目录沙盒路径混入、资源不完整.app目录最好处理因为它已经是解包后的形态只要补齐结构就能压。.xcarchive是 Xcode 归档产物里面已经带了签名信息和 dSYM走第 3 章的导出命令比手工压更稳。.deb麻烦一些它本质是ar归档里面套data.tar应用通常在/Applications目录下取出来之后原来的签名对真机无效必须重新签一遍。从设备上导出的包则要留意路径里带容器 UUID重组结构时别把Documents、Library这些运行时目录一起打进去那些不属于应用包本体。3. 打包成 ipa 的两条路手工 Payload 与 xcodebuild 导出结构搞清楚之后真正的打包动作只有两种手工组目录再压缩或者交给构建工具导出。前者可控、能处理历史遗留包后者省心、签名和描述文件由工具链对齐。实际工作里两条路都要会因为总有些包是工具链处理不了的。3.1 手工打包zip -ry 里每个参数都不是摆设先把工作区搭干净再压。顺序错了会多出一层目录这是最常见的返工原因。# 从体检目录拿到 APP 变量重新建一个只有 Payload 的工作区 WORK/tmp/ipa-build rm -rf $WORK mkdir -p $WORK/Payload # -a 保留权限位与符号链接比 cp -R 更接近原始状态 cp -a $APP $WORK/Payload/ # 关键必须 cd 到 Payload 的父目录再压包内路径才是 Payload/xxx.app cd $WORK zip -qry /tmp/output.ipa Payloadzip的三个选项各管一件事-q静默避免上万行输出刷屏-r递归少了它Payload目录里只有一个空壳-y把符号链接按链接本身存进去而不是跟进去把目标文件复制一份。第三条是 Framework 能不能跑起来的分水岭很多“装上了但一启动就闪退”的包问题就出在少了这个-y。同一条路还有一个等价工具行为更贴近系统打包# --keepParent 保留 Payload 这一层--sequesterRsrc 处理资源分支 ditto -c -k --sequesterRsrc --keepParent $WORK/Payload /tmp/output.ipaditto是 macOS 自带工具-c创建归档-k用 ZIP 格式--keepParent保证顶层目录是Payload--sequesterRsrc把资源分支单独收纳。它对符号链接和权限位的处理比zip更保守打包带 Framework 的包时我更倾向用它。压完一定要回查不要拿结果直接装机。# 看包内第一层路径与符号链接条目第一列是 l 表示链接 zipinfo -1 /tmp/output.ipa | head -20 zipinfo -l /tmp/output.ipa | grep -E ^l | head -10zipinfo -1只列路径适合确认顶层是不是Payload/zipinfo -l带属性列第一列出现l就是符号链接。这一步花三秒能省掉后面半小时的闪退排查。3.2 官方出口xcodebuild -exportArchive 与 ExportOptions.plist有.xcarchive的时候优先走这条签名、描述文件、Swift 运行时都由工具链处理不用手工拼。# 导出归档为 ipa-allowProvisioningUpdates 允许自动补描述文件 xcodebuild -exportArchive \ -archivePath /path/to/App.xcarchive \ -exportPath /path/to/export \ -exportOptionsPlist /tmp/ExportOptions.plist \ -allowProvisioningUpdates配置用 PlistBuddy 现场生成避免手写 XML 时把键名拼错# 先删旧配置PlistBuddy 在文件不存在时会自动创建 rm -f /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :method string ad-hoc /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :signingStyle string manual /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :teamID string TEAMID0000 /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :provisioningProfiles dict /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :provisioningProfiles:com.example.demo string \AdHoc Profile\ /tmp/ExportOptions.plist /usr/libexec/PlistBuddy -c Add :stripSwiftSymbols bool true /tmp/ExportOptions.plistmethod决定分发类型内测用ad-hoc企业内部分发用enterprise上商店用app-store选错会直接导致导出的包装不到目标设备。signingStyle设成manual时必须把provisioningProfiles里的键值对上键是包标识值是描述文件名称两者任一写错都会报描述文件缺失。teamID用security find-identity -v -p codesigning里的团队前缀核对一遍再填。3.3 一个可复用的 Python 转换脚本把上面的检查串成脚本团队里谁出包都用同一套逻辑能省掉大量“我这能装你那不能装”的扯皮。#!/usr/bin/env python3 把 .app 目录打成结构合规的 ipa并做最小校验 import argparse import os import plistlib import shutil import subprocess import sys import tempfile def load_info(app_dir): with open(os.path.join(app_dir, Info.plist), rb) as f: return plistlib.load(f) def build(app_dir, out_path): app_dir os.path.abspath(app_dir.rstrip(/)) info load_info(app_dir) bundle_id info.get(CFBundleIdentifier, ) if not bundle_id or in bundle_id: sys.exit(CFBundleIdentifier 缺失或含空格先修 Info.plist 再打包) tmp tempfile.mkdtemp(prefixipak-) payload os.path.join(tmp, Payload) os.makedirs(payload) # symlinksTrue 保留框架里的链接这是最容易被忽略的一行 shutil.copytree(app_dir, os.path.join(payload, os.path.basename(app_dir)), symlinksTrue) # 用系统 zip 而不是 zipfile标准库不会把符号链接存成链接 subprocess.run([zip, -qry, os.path.abspath(out_path), Payload], cwdtmp, checkTrue) # 回查顶层路径确认没有多套一层目录 names subprocess.run([zipinfo, -1, os.path.abspath(out_path)], capture_outputTrue, textTrue, checkTrue).stdout if not names.startswith(Payload/): shutil.rmtree(tmp) sys.exit(包内顶层不是 Payload/检查源目录层级) shutil.rmtree(tmp) print(fok {bundle_id} - {out_path}) if __name__ __main__: p argparse.ArgumentParser() p.add_argument(app, help.app 目录路径) p.add_argument(-o, --out, defaultoutput.ipa) build(p.parse_args().app, p.parse_args().out)脚本里有两处是刻意的选择。copytree(symlinksTrue)保证复制阶段不把链接变成实体文件打包阶段调系统zip而不是zipfile因为标准库在写入时会把符号链接内容展开打出来的包体积变大、签名对不上。最后那段回查读zipinfo -1的第一行确认前缀是Payload/能挡住“在错误目录执行压缩”这类低级但高频的失误。3.4 重签名codesign 的顺序、entitlements 与 --deep 的坑包结构对了但签名对不上的情况非常普遍尤其是接手的旧包。重签的核心是从内到外一层一层来。# 1. 看本机有哪些可用签名身份记下完整名称 security find-identity -v -p codesigning # 2. 把原包的 entitlements 导出来当模板改包标识时只改需要改的键 codesign -d --entitlements :- $APP /tmp/ent.plist # 3. 先签框架每个 .framework 单独签别指望外层一步到位 find $APP/Frameworks -maxdepth 2 -name *.framework -type d | while read -r fw; do codesign -f -s $IDENTITY --timestampnone $fw done # 4. 再签扩展扩展需要自己的描述文件和 entitlements find $APP/PlugIns -maxdepth 1 -name *.appex -type d | while read -r ex; do codesign -f -s $IDENTITY --entitlements /tmp/ent.plist --timestampnone $ex done # 5. 最后签外层 .app并做严格校验 codesign -f -s $IDENTITY --entitlements /tmp/ent.plist --timestampnone $APP codesign --verify --strict --verbose2 $APP-f覆盖已有签名不改包的话其实不需要但处理别人给的包时基本都要加-s后面跟证书名称用上一步find-identity输出的完整串写简称匹配不上--entitlements决定这个包能拿到哪些系统能力导出的模板改包标识时只动application-identifier和com.apple.developer.team-identifier两个键--timestampnone在内测场景下避免联网取时间戳出包快一些正式分发则应该去掉。--verify --strict会把资源封印、签名完整性一起校验比默认模式严格。--deep不适合作为默认操作。它用同一个身份把嵌套内容全部重签一遍扩展自己的 entitlements 会被覆盖掉而且不会更新描述文件结果就是主包能起、分享扩展和小组件失效。4. 装上却闪退签名链、架构与资源完整性的三层排查比“装不上”更难受的是“装上了打不开”。安装器通过只代表结构合规启动阶段还有签名验证、动态库加载、系统版本三道关。这一章按从外到内的顺序排先看签名再看架构最后看嵌套组件。4.1 codesign --verify 报错对照表签名问题的报错文案很固定逐条对表比搜索快。报错片段含义处置code object is not signed at all完全没有签名或签名被剥离按内到外重新签一遍invalid signature (code or signature have been modified)签名后内容被改动过改完必须重签改 Info.plist 也算A valid provisioning profile for this executable was not found描述文件和签名身份不匹配核对application-identifier与描述文件The executable was signed with invalid entitlementsentitlements 超出描述文件授权范围用描述文件里的 Entitlements 段做模板CSSMERR_TP_NOT_TRUSTED证书链不受信任本机需安装对应中间证书bundle format unrecognized, invalid, or unsuitable目录结构不合法回查 Payload 层级与.app数量resource fork, Finder information, or similar detritus not allowed包里混进了扩展属性打包前清理xattr最后一类值得单独说从网络下载或从聊天工具传过来的包经常带扩展属性签名工具会因此拒绝。# 清掉扩展属性后再签-r 递归-c 清除全部属性 xattr -cr $APP4.2 架构与 MinimumOSVersion设备为什么拒绝安装签完能装但起不来下一步看架构和最低系统版本。# 主二进制支持的架构必须包含目标设备的 arm64 BIN$APP/$(plutil -extract CFBundleExecutable raw $APP/Info.plist) lipo -info $BIN # 看构建版本里的 platform 与 minos比单纯看 MinimumOSVersion 更准 xcrun vtool -show-build $BIN | head -30 # 设备族1 是手机2 是平板数组里没有目标类型就装不上 plutil -extract UIDeviceFamily json -o - $APP/Info.plistlipo -info输出里如果只有armv7新设备一律不认vtool -show-build能看到platform和minos有些包的 Info.plist 写着低版本系统但二进制里minos更高两者矛盾时以二进制为准安装器会按更严的那个卡。UIDeviceFamily是个数组只写了[1]的包在平板上会提示不兼容做通用包时确认是[1, 2]。MinimumOSVersion和二进制minos不一致属于常见的手工改包副作用有人为了让老设备装上直接把 Info.plist 里的版本号改低但二进制里的加载指令没变启动阶段照样失败。改这一项必须连带重新编译或至少确认二进制本身支持。4.3 嵌套二进制Frameworks、PlugIns、Watch 目录的签名顺序现代应用的签名不是一层是三层甚至四层。任何一层漏签或签错身份表现都是主包能启动、某个功能静默失效。层级典型路径是否需要独立描述文件签名顺序动态库Frameworks/*.dylib否第 1 步框架Frameworks/*.framework否第 2 步应用扩展PlugIns/*.appex是第 3 步手表应用Watch/*.app是第 4 步外层应用.app本体是最后一步顺序反过来一定会翻车因为外层签名时会计算内层内容的封印内层改动后封印失效。排查时用逐层校验# --deep 只用于校验不用来签名它会遍历嵌套内容 codesign --verify --deep --strict --verbose2 $APP # 单独看某个扩展的签名详情与 entitlements codesign -dv --verbose4 $APP/PlugIns/Share.appex codesign -d --entitlements :- $APP/PlugIns/Share.appex--deep在校验场景下好用能一次扫出哪一层坏了签名场景下不要用前面说过原因。看完报错定位到具体层级再回到 3.4 节按顺序补签。5. 避坑记录五条把包做废的操作这一章是血泪经验合集。每条都按现象、原因、解决的顺序写遇到相似症状可以直接对号入座。5.1 用系统右键“压缩”打包包内多出一堆杂物现象安装器提示无法安装或者校验时报结构不合法用zipinfo -1看第一层根本不是Payload/。原因macOS 访达的压缩功能会额外写入__MACOSX目录和.DS_Store还可能把当前目录名当成顶层目录压出来的结构是Payload/Payload/xxx.app或者带着一堆隐藏文件。解决一律用zip -qry或ditto -c -k --sequesterRsrc --keepParent打完用zipinfo -1看前几行顶层必须是Payload/。打包前顺手find . -name .DS_Store -delete清一遍。5.2 压缩时漏了 -yFramework 里的符号链接被展开现象安装正常一启动就闪退崩溃日志里出现找不到某个动态库的提示。原因.framework目录里通常有一组指向Versions/Current的符号链接zip不带-y会把链接跟进去复制成实体文件包体积翻倍签名封印和实际内容对不上。解决压缩时加-y或者改用ditto。打包后用zipinfo -l过滤第一列为l的条目数量应该和源目录里的链接数一致。5.3 改了包标识却没重签描述文件对不上现象装到一半失败提示描述文件与包标识不匹配。原因只改了 Info.plist 里的CFBundleIdentifierembedded.mobileprovision里的application-identifier还是旧值签名也已经失效。解决包标识、描述文件、entitlements、签名四件事必须一起改。用security cms -D -i embedded.mobileprovision导出描述文件内容确认里面的application-identifier后缀和新标识一致再按 3.4 节的顺序重签。5.4 跨平台解压再压缩权限位全丢现象安装成功但点图标就退出日志里提示权限不足。原因在不保留 Unix 权限的环境里解压再打包主二进制权限从 0755 变成 0644安装器解包后没有执行权限。解决整条链路尽量放在 macOS 上做。实在要在别处拆包最后一步回到 macOS 重新组包并显式修权限# 主二进制必须可执行其他资源保持原样 chmod 755 $APP/$(plutil -extract CFBundleExecutable raw $APP/Info.plist)5.5 用 --deep 一把梭扩展组件签名失效现象主应用能启动分享扩展、小组件、通知服务点开就崩。原因codesign --deep用同一个身份和同一份 entitlements 重签所有嵌套内容扩展自己的权限声明被覆盖描述文件也没有跟着更新。解决从内到外手工签每个.appex带自己的 entitlements主包最后签。签完用codesign --verify --deep --strict校验但签名动作本身不要加--deep。6. 进阶用法把转换和校验串成一条命令前面几章的东西拆开看都不复杂真正出问题的是“漏做一步”。我现在的做法是把它固化成脚本出包只跑一条命令人只负责看结果。#!/bin/bash # 用法: ./make_ipa.sh path/to/App.app out.ipa set -euo pipefail APP$1; OUT$2 BIN$APP/$(plutil -extract CFBundleExecutable raw $APP/Info.plist) # 闸一结构与关键字段 [ -f $APP/Info.plist ] || { echo 缺 Info.plist; exit 1; } [ -x $BIN ] || { echo 主二进制不可执行; exit 1; } plutil -extract CFBundleIdentifier raw $APP/Info.plist /dev/null # 闸二架构里必须有 arm64 lipo -info $BIN | grep -q arm64 || { echo 缺 arm64; exit 1; } # 闸三清扩展属性后打包避免资源封印告警 xattr -cr $APP WORK$(mktemp -d); mkdir -p $WORK/Payload cp -a $APP $WORK/Payload/ (cd $WORK zip -qry $OLDPWD/$OUT Payload) # 闸四回查顶层路径与符号链接数量 zipinfo -1 $OUT | head -1 | grep -q ^Payload/ || { echo 顶层结构错误; exit 1; } rm -rf $WORK echo ok $OUT四道闸分别卡结构、架构、打包方式和结果回查。前两道在压之前能挡掉大部分输入问题第三道清扩展属性避免签名阶段的资源封印告警第四道在压之后是最后一道后悔药顶层路径不对就直接失败不会把坏包流到测试手里。脚本里set -euo pipefail不是装饰中间任何一步返回非零都会立刻停下比一路跑完再回头找问题省事。如果包要发给外部测试再补一步签名校验codesign --verify --deep --strict --verbose2 $APP输出里出现valid on disk和satisfies its Designated Requirement才算过。有一件事值得单独提醒这套流程只解决“格式转换”和“签名对齐”不解决授权问题。转换出来的包能不能装到某台设备上取决于描述文件里的设备列表这一点改脚本改不出来。接手别人的包时先问清楚目标设备的授权范围比事后对着报错猜要快得多。从那以后我每次出包都强制走一遍这个脚本哪怕只是改了个版本号也不跳步骤——跳过一次就会在某个没有日志的夜里补回来。希望帮到你。本文还有配套的精品资源点击获取