最近在做內部分發這塊,很多同事卡在幾個地方:如何正確從 Xcode / CI 打包出 enterprise .ipa,怎麼生成 manifest.plist,把二者託管在 HTTPS 上,然後通過 itms-services:// 給同事一鍵安裝。下面把流程從頭到尾講清楚——口語化、貼近實操,並附上可運行的 Demo(Python + shell),能直接放到 CI 裏跑。
先把整個流程捋一遍
你要做的事其實就是三件:
- 在 Apple 企業賬號裏搞好證書和描述文件(In-House)。
- 用 Xcode / xcodebuild 把 App 打包並導出 enterprise
.ipa。 - 把
.ipa和manifest.plist放到 HTTPS 可訪問的位置(S3/NGINX/CloudFront),然後通過itms-services://?action=download-manifest&url=<manifest>給設備安裝。
下面逐步把每一環的實操、常見坑、以及可運行代碼給你。
先決條件
- 公司必須有 Apple Enterprise Developer Program(企業賬號,不是個人或公司普通賬號)。
- 開發機器 / CI 需要安裝 Xcode(或能跑 xcodebuild 的 macOS 機器)。
- 一個 HTTPS 域名(必須是可信 CA 的 TLS),或者使用 S3 + CloudFront 並綁定證書。iOS 會拒絕不受信任證書。
- 推薦把證書、私鑰、描述文件等用安全方式存放在 CI 的 secret 管理裏,不要明文在腳本里。
證書與描述文件
-
在 Apple 企業賬號裏創建 In-House(Enterprise)分發證書:
- 在開發者賬號頁面 → Certificates → Add → 選擇 “In-House and Ad Hoc” → 上傳 CSR(Certificate Signing Request) → 下載證書
.cer。 - 把證書和對應私鑰導入到 macOS Keychain(或在 CI 上生成
.p12並導入)。
- 在開發者賬號頁面 → Certificates → Add → 選擇 “In-House and Ad Hoc” → 上傳 CSR(Certificate Signing Request) → 下載證書
-
創建 App ID(Bundle ID)並生成 In-House provisioning profile:
- Apple Developer → Identifiers → 新增 App ID(如
com.example.app)。 - Profiles → 新建 In-House Profile,選擇上一步的 App ID 和 In-House 證書 → 下載
.mobileprovision。
- Apple Developer → Identifiers → 新增 App ID(如
- 在本地或 CI 上導入證書(示例命令,CI 上用 secrets):
# 假設你有 base64 編碼的 p12 和密碼(CI secrets)
echo "$P12_BASE64" | base64 --decode > cert.p12
security create-keychain -p "$KEYCHAIN_PW" build.keychain
security import cert.p12 -k ~/Library/Keychains/build.keychain -P "$P12_PWD" -T /usr/bin/codesign
security list-keychains -s ~/Library/Keychains/build.keychain
security unlock-keychain -p "$KEYCHAIN_PW" ~/Library/Keychains/build.keychain
# 允許 codesign 使用私鑰(CI 常見操作)
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" ~/Library/Keychains/build.keychain
提示:如果不做set-key-partition-list,CI 上codesign可能會報“not permitted to use private key”之類的錯誤。
從 Xcode 導出 enterprise IPA(GUI / CLI 都給你)
GUI(Xcode)
- 打開項目 → Product → Archive。
- 在 Organizer 裏選擇新生成的 archive → Distribute App → Enterprise → 選擇正確的 provisioning profile → Export → 得到
.ipa。
CLI(xcodebuild,適合 CI)
- 先 archive:
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath /tmp/MyApp.xcarchive \
clean archive
- 然後導出
.ipa,需要一個exportOptions.plist,示例放在下面:
exportOptions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>enterprise</string>
<key>teamID</key>
<string>YOUR_TEAM_ID</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.myapp</key>
<string>My Enterprise Provisioning Profile Name</string>
</dict>
</dict>
</plist>
導出命令:
xcodebuild -exportArchive \
-archivePath /tmp/MyApp.xcarchive \
-exportOptionsPlist exportOptions.plist \
-exportPath /tmp/MyAppExport
# /tmp/MyAppExport 會包含 MyApp.ipa
Fastlane(推薦 CI)
在 Fastfile 中使用 gym(示例):
lane :enterprise_build do
match(type: "enterprise") # 或者使用手動管理的證書
gym(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
export_method: "enterprise",
export_options: {
provisioningProfiles: { "com.example.myapp" => "My Enterprise Provisioning Profile Name" }
}
)
end
manifest.plist:格式、示例與生成
iOS 內部分發不會直接用 .ipa 鏈接安裝,而是通過 manifest.plist 指定 .ipa 的 URL 與元數據。下面給標準模板並逐字段解釋。
manifest.plist(示例)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://dl.company.com/apps/MyApp-1.2.3.ipa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>com.example.myapp</string>
<key>bundle-version</key>
<string>1.2.3</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>MyApp</string>
</dict>
</dict>
</array>
</dict>
</plist>
字段要點總結:
url必須是 HTTPS 且能直接訪問.ipa(返回 200,不要返回 HTML 登錄頁或 302 重定向)。bundle-identifier必須與.ipa裏Info.plist的CFBundleIdentifier一致。bundle-version要匹配CFBundleShortVersionString(否則可能出現版本不一致問題)。
可運行 Demo
Python 腳本自動生成 manifest.plist
把這個腳本放到你的機器或 CI 上。它會把你提供的 .ipa URL、bundle id、version、title 寫成 manifest.plist。
gen_manifest.py
#!/usr/bin/env python3
"""
gen_manifest.py
Generate manifest.plist for iOS enterprise distribution.
Usage:
python3 gen_manifest.py \
--ipa-url https://dl.company.com/apps/MyApp-1.2.3.ipa \
--bundle-id com.example.myapp \
--version 1.2.3 \
--title "MyApp" \
--output manifest.plist
"""
import argparse
import plistlib
from pathlib import Path
def build_manifest(ipa_url: str, bundle_id: str, version: str, title: str):
# Build the dict matching the required plist structure
manifest = {
"items": [
{
"assets": [
{
"kind": "software-package",
"url": ipa_url
}
],
"metadata": {
"bundle-identifier": bundle_id,
"bundle-version": version,
"kind": "software",
"title": title
}
}
]
}
return manifest
def main():
parser = argparse.ArgumentParser(description="Generate manifest.plist for enterprise distribution")
parser.add_argument("--ipa-url", required=True, help="HTTPS URL to the .ipa")
parser.add_argument("--bundle-id", required=True, help="Bundle ID, e.g. com.example.myapp")
parser.add_argument("--version", required=True, help="App version, e.g. 1.2.3")
parser.add_argument("--title", required=True, help="App title")
parser.add_argument("--output", default="manifest.plist", help="Output filename")
args = parser.parse_args()
manifest = build_manifest(args.ipa_url, args.bundle_id, args.version, args.title)
out_path = Path(args.output)
with out_path.open("wb") as f:
plistlib.dump(manifest, f)
print(f"Generated {out_path.resolve()}")
if __name__ == "__main__":
main()
使用示例
python3 gen_manifest.py \
--ipa-url "https://dl.company.com/apps/MyApp-1.2.3.ipa" \
--bundle-id com.example.myapp \
--version 1.2.3 \
--title "MyApp" \
--output manifest.plist
腳本會生成 manifest.plist,接下來把它和 .ipa 都上傳到 HTTPS 服務(S3/CloudFront 或自建 nginx),然後拼裝安裝鏈接:
itms-services://?action=download-manifest&url=https://dl.company.com/apps/manifest.plist
CI 集成示例腳本
CI 集成示例腳本(打包 → 上傳 → 生成 manifest → 輸出安裝鏈接)
下面是一個簡單的 CI shell 腳本(假設你已經有 .ipa 在 build/MyApp.ipa),並且使用 AWS CLI 上傳到 S3。需要在 CI 環境中配置 AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION。
ci_publish.sh
#!/usr/bin/env bash
set -euo pipefail
IPA_PATH="./build/MyApp.ipa"
S3_BUCKET="s3://your-bucket/path"
DIST_BASE="https://dl.example.com/path" # CloudFront domain or https domain to serve files
# Upload .ipa
aws s3 cp "$IPA_PATH" "${S3_BUCKET}/MyApp-1.2.3.ipa" \
--acl public-read --content-type application/octet-stream
# Generate manifest locally (use python script)
python3 gen_manifest.py \
--ipa-url "${DIST_BASE}/MyApp-1.2.3.ipa" \
--bundle-id com.example.myapp \
--version 1.2.3 \
--title "MyApp" \
--output manifest.plist
# Upload manifest
aws s3 cp manifest.plist "${S3_BUCKET}/manifest.plist" \
--acl public-read --content-type application/xml
echo "Published. Install link:"
echo "itms-services://?action=download-manifest&url=${DIST_BASE}/manifest.plist"
把這個腳本放到你的 CI(GitLab CI / Jenkins / GitHub Actions)裏,跑完會輸出最終安裝鏈接,發給 QA 即可。
託管建議(S3 + CloudFront / nginx 示例)
- 推薦用 S3 + CloudFront:穩定、支持 HTTPS(ACM),比較省心。上傳時設置
Content-Type:.ipa → application/octet-stream,manifest.plist → application/xml。 - 自建 nginx:確保啓用 TLS(可信證書),示例 nginx 配置片段:
server {
listen 443 ssl;
server_name dl.example.com;
ssl_certificate /etc/ssl/certs/your-cert.pem;
ssl_certificate_key /etc/ssl/private/your-key.pem;
location /apps/ {
root /var/www;
types {
application/octet-stream ipa;
application/xml plist;
}
add_header Cache-Control "max-age=3600";
}
}
注意:iOS 對證書要求嚴格,使用自簽名證書通常會失敗(除非設備信任該 CA),所以優先用 Let’s Encrypt / ACM /正規 CA。
終端測試與故障排查(最常見的錯誤 & 驗證命令)
- 先在手機 Safari 打開 manifest.plist 鏈接,確認能訪問(會顯示 XML 內容)。
- curl 驗證(在你本地或 CI 上):
curl -I https://dl.example.com/path/manifest.plist
# 確認返回 200,Content-Type: application/xml
curl -I https://dl.example.com/path/MyApp-1.2.3.ipa
# 確認返回 200
- 檢查 HTTPS 證書鏈:
openssl s_client -connect dl.example.com:443 -showcerts
- 查看本地可用的 codesign identity(調試簽名問題):
security find-identity -v -p codesigning
- 檢查 IPA 內的 bundle id / version(在 macOS 上):
unzip -q MyApp.ipa -d /tmp/myapp_unpack
plutil -p /tmp/myapp_unpack/Payload/MyApp.app/Info.plist
# 或者使用 /usr/libexec/PlistBuddy
/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" /tmp/myapp_unpack/Payload/MyApp.app/Info.plist
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" /tmp/myapp_unpack/Payload/MyApp.app/Info.plist
確認這些值與 manifest.plist 中一致。
- 常見錯誤與含義:
- “未能檢索清單 / Could not retrieve manifest” → manifest URL 不可訪問 / HTTPS 證書問題 / 返回 HTML 而不是 XML。
- “無法安裝應用 / App installation failed” → 簽名或描述文件問題(bundle id 與 profile 不匹配、證書失效或被吊銷)。
- “應用安裝後閃退” → 檢查崩潰日誌(Xcode Devices 窗口或 Console.app)。
用户安裝體驗
- 用 iPhone 的 Safari 打開
itms-services://?action=download-manifest&url=https://.../manifest.plist(通常是你發的二維碼或安裝頁按鈕)。 - 點擊安裝,系統提示確認,點擊安裝。
- 如果是該企業證書第一次在設備上使用,需要去 設置 → 通用 → 描述文件與設備管理(或 “設備管理”)裏信任該企業證書。你最好在安裝頁上寫這個步驟並配圖。
合規與安全提示
- Apple 明確要求企業賬號只用於“公司內部分發給公司員工或受管理設備”,不能對外公開發布給任意用户。濫用會被吊銷企業證書。
- 證書與私鑰要謹慎保管。CI 中導入私鑰要限制權限,並定期輪換。
- 記錄每次發佈的版本、誰可以訪問、過期提醒,建議結合 MDM 管理設備/應用權限。
場景演示
企業內部需要把一個新版本快速下發給 200 名 QA 和 10 台測試設備。推薦流程:
- CI(Jenkins/Fastlane)跑完單元測試後觸發 enterprise lane:archive → export ipa。
- CI 上傳 ipa 到 S3 + CloudFront(HTTPS)。
- CI 調用
gen_manifest.py生成 manifest,上傳到 S3。 - CI 在 Slack 裏發出安裝鏈接(itms-services\://...)。
- QA 用手機打開並安裝。需要時結合 MDM 強制下發並撤回。
這樣既快速又可控,還能把證書管理集中在 CI 的安全區。
總結
- 必須保證證書、描述文件正確並在 Keychain/CI 中可用;
.ipa與manifest.plist必須通過可信 HTTPS 提供;- 推薦把 manifest 生成和上傳放進 CI 流程;
- 常見問題通常是證書、HTTPS、manifest 格式或 bundle id 不一致,按本文提供的命令逐一排查即可;
- 合規上要注意企業簽名的使用範圍與風險。