无需打开Xcode构建并发布Mac和iOS应用
最近我听到几位苹果相关播客主播吐槽Xcode有多难用,说苹果得优化这款工具,让Mac和iOS应用的"随心开发"体验更好。他们说得没错,但我实在好奇:为什么他们还要打开Xcode?其实只要提前做一点准备,你完全可以彻底告别Xcode界面,随心所欲地开发Mac和iOS应用。
要是你对下面的操作步骤有疑问,直接把这篇博客丢给Claude Code或者你常用的大语言模型代码工具就行——解决这类问题本来就是它们的本职工作。
核心要点速览
- 必须安装Xcode.app,但永远不用打开它。
xcodebuild、notarytool、stapler和devicectl这些工具都内嵌在Xcode中,直接在Shell里就能运行。 - 仅需一次GUI(或交互式终端)操作:登录Apple ID、创建开发者ID证书、存储公证密码。完成后,所有构建和部署都可在无界面环境下进行。
- Mac应用发布全靠一个脚本——
scripts/release.sh,只需编写一次,就能自动执行完整流程:归档→开发者ID签名→公证→钉入公证凭证→安装至/Applications目录。 - 签名基于证书与钥匙串。签名密钥存储在登录钥匙串中,
xcodebuild会自动识别,无需在代码仓库中存储任何敏感信息。
唯一有点麻烦的就是一次性初始配置,我们先把这部分搞定。
安装Xcode
Xcode是必须安装的,没有替代方案,因为应用构建依赖的工具都在Xcode.app内部。
安装完成后,请确保已将命令行工具链切换为Xcode自带版本,而非独立的/Library/Developer/CommandLineTools。执行以下命令检查,若输出为/Applications/Xcode.app/Contents/Developer,则说明配置正确:
❯ xcode-select -p
/Applications/Xcode.app/Contents/Developer
如果返回的是独立CommandLineTools的路径,请执行以下命令切换到Xcode:
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
注意:"Command Line Tools"这个名称容易混淆
通过xcode-select --install安装的是独立Command Line Tools包,对应路径为/Library/Developer/CommandLineTools,仅包含clang、git等基础工具,没有iOS SDK、notarytool、devicectl等完整开发所需的组件。
完整的工具链位于Xcode.app内部(路径为/Applications/Xcode.app/Developer),包含所有开发所需工具。只要安装了Xcode,就无需再安装独立Command Line Tools。
安装XcodeGen
仅靠Xcode及其命令行工具无法自动生成和管理项目,这时需要用到XcodeGen。你可以从GitHub下载,也可用Homebrew安装:
brew install xcodegen
简单来说,Xcode项目本质上是macOS显示为文件的文件夹,包含应用创建和编译所需的所有信息。但Xcode会频繁修改其中的文件和引用,给Git版本控制带来麻烦。
XcodeGen通过一个project.yml(YAML格式)文件存储所有项目配置,每次构建时都会根据该文件重新生成完整的.xcodeproj文件夹。只需将YAML文件提交到Git,.xcodeproj文件夹可直接忽略。
一次性配置Xcode
我们需要先完成Xcode的初始配置,之后就再也不用打开它了。
接受Xcode许可并安装附加组件
你可以直接在Xcode界面中接受许可并安装组件,也可通过命令行完成:
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
在Xcode中配置Apple开发者账号
打开Xcode,点击「设置」→「账号」,点击「+」添加你的Apple ID。
注意:分发和公证应用需要付费的Apple开发者账号
只有经过公证的应用,才能在你的Mac和iOS设备上正常安装运行,不会被系统判定为恶意软件而删除。
创建开发者ID应用证书
账号配置完成后,创建「开发者ID应用证书」(路径:设置→账号→你的Apple ID→管理证书…→+→开发者ID应用),用于签名发布版.app包。
请注意,开发者ID应用证书与Apple开发证书是两个不同的证书:
- Apple开发证书用于在自己的设备上构建和运行应用,比如部署到iPhone、本地调试等场景。
- 开发者ID应用证书用于生成能通过Gatekeeper验证、可在他人Mac上运行的公证版
.app包。发布脚本需要的是这第二种证书。
在Xcode中创建证书后,证书和对应的私钥会自动安装到登录钥匙串中。私钥是签名的核心,无法重新下载,请务必妥善保管,不要删除,并做好钥匙串备份。
如果有疑问,可咨询你常用的大语言模型,让它帮你完成配置——反正之后也是它帮你操作Xcode。
最后一步:
在终端中存储公证凭证(仅需一次)
公证是将签名后的应用上传至Apple进行恶意软件扫描的过程。notarytool通过预存在钥匙串中的配置文件进行身份验证,该配置需要通过交互式命令创建一次,过程中会提示输入应用专用密码,这一步无法跳过:
xcrun notarytool store-credentials App-Name
# 按提示粘贴应用专用密码
这里有几点需要注意:
- 配置文件名称与应用同名。不要复用其他应用的配置文件——在你的设备上可能能用,但在他人设备上会莫名失效。
- 应用专用密码≠Apple ID密码。需在appleid.apple.com的「登录与安全→应用专用密码」页面生成。
- 修改Apple ID密码后,应用专用密码会自动失效。如果公证时出现
401 invalid credentials错误,几乎都是因为需要重新生成应用专用密码,而非你的配置出了问题。
执行以下命令确认凭证已成功存储:
xcrun notarytool history --keychain-profile App-Name
题外话:我把应用专用密码存在1Password密码库中,并授权Claude Code访问。这样每次创建新应用时,我只需让它帮我创建公证凭证,它会自动从1Password中获取密码。毕竟使用大语言模型的初衷,就是省去这些不想手动完成的操作。
创建Local.xconfig文件并加入.gitignore
正式签名需要团队ID和Bundle前缀,我把这些信息放在Local.xconfig文件中:
cp Local.xcconfig.example Local.xcconfig
# 编辑Local.xcconfig文件,设置:
# BUNDLE_PREFIX = 你的真实前缀
# DEVELOPMENT_TEAM = 你的团队ID
同样,如果有疑问,可让Claude Code或你常用的大语言模型帮你创建这个文件。
配置自动化工具
创建发布脚本
我的应用部署流程由仓库内scripts文件夹中的release.sh脚本处理。没有它,就没有自动化构建流水线。
我让Claude Code帮我生成了这个脚本,当时我大概是这么说的:我想要一个无需打开Xcode,就能完成归档、开发者ID签名、公证、钉入凭证,并将应用安装到/Applications目录的脚本。脚本要能完整执行整个流程,任何步骤出错都要立即终止并明确提示。
我不需要向它解释具体流程,因为这些都是公开的标准流程:用xcodebuild归档,用-exportArchive和ExportOptions.plist导出,用notarytool --wait提交,用stapler附加凭证,用spctl验证。大语言模型知道这些官方标准流程,它只需要我提供项目专属信息:scheme名称、团队ID、公证配置文件名称、安装路径。
它生成第一版脚本后,我们运行测试,发现问题后再修复。这个循环不是失败,而是正常的开发过程。我始终把AI工作流视为迭代优化的过程,不用多久就能稳定下来,真正投入使用。
以下是我某个应用仓库中的实际脚本:
#!/usr/bin/env bash
# scripts/release.sh — 生成经过开发者ID签名、公证的MY-APP-NAME.app,并安装至/Applications目录。
#
# 前置要求(仅需一次):Xcode已登录Apple ID、已订阅付费开发者计划、已配置notarytool凭证配置文件。默认使用"MY-APP-NAME"配置文件;可通过MY-APP-NAME_NOTARY_PROFILE=<名称>覆盖。
#
# 使用方法:./scripts/release.sh
set -euo pipefail
PROJECT="MY-APP-NAME.xcodeproj"
SCHEME="MY-APP-NAME-macOS"
APP_NAME="MY-APP-NAME"
TEAM_ID="YOURTEAMID"
NOTARY_PROFILE="${MY-APP-NAME_NOTARY_PROFILE:-MY-APP-NAME}"
BUILD_DIR="build"
ARCHIVE_PATH="$BUILD_DIR/$APP_NAME.xcarchive"
EXPORT_PATH="$BUILD_DIR/Export"
APP_PATH="$EXPORT_PATH/$APP_NAME.app"
INSTALL_DIR="/Applications"
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
cd "$(dirname "$0")/.."
step() { printf "\n\033[1;36m▸ %s\033[0m\n" "$*"; }
fail() { printf "\n\033[1;31m✗ %s\033[0m\n" "$*" >&2; exit 1; }
step "预检查"
command -v xcodegen >/dev/null || fail "未安装xcodegen(执行brew install xcodegen安装)。"
if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then
fail "notarytool配置文件'$NOTARY_PROFILE'不存在。请执行xcrun notarytool store-credentials创建。"
fi
step "重新生成项目"
xcodegen generate
rm -rf "$BUILD_DIR"; mkdir -p "$BUILD_DIR"
step "归档(Release版本)"
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
-derivedDataPath "$BUILD_DIR/derived" -archivePath "$ARCHIVE_PATH" \
-allowProvisioningUpdates archive
step "导出经过开发者ID签名的应用"
cat > "$BUILD_DIR/ExportOptions.plist" <<EOF
<?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>method</key><string>developer-id</string>
<key>teamID</key><string>$TEAM_ID</string>
<key>signingStyle</key><string>automatic</string>
</dict>
</plist>
EOF
xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" -exportPath "$EXPORT_PATH" \
-exportOptionsPlist "$BUILD_DIR/ExportOptions.plist" -allowProvisioningUpdates
[ -d "$APP_PATH" ] || fail "导出的应用未找到,路径:$APP_PATH。"
step "提交公证(上传至Apple,可能需要几分钟)"
ditto -c -k --keepParent "$APP_PATH" "$BUILD_DIR/notarize.zip"
xcrun notarytool submit "$BUILD_DIR/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --wait
step "钉入公证凭证"
xcrun stapler staple "$APP_PATH"
step "验证Gatekeeper兼容性"
spctl -a -vvv -t exec "$APP_PATH"
step "安装至$INSTALL_DIR/$APP_NAME.app"
pkill -x "$APP_NAME" 2>/dev/null || true
rm -rf "$INSTALL_DIR/$APP_NAME.app"
cp -R "$APP_PATH" "$INSTALL_DIR/"
[ -x "$LSREGISTER" ] && "$LSREGISTER" -f "$INSTALL_DIR/$APP_NAME.app" >/dev/null 2>&1 || true
step "验证已安装的应用包"
xcrun stapler validate "$INSTALL_DIR/$APP_NAME.app"
spctl -a -vvv -t exec "$INSTALL_DIR/$APP_NAME.app"
printf "\n\033[1;32m✓ %s已完成公证、凭证钉入并安装成功。\033[0m\n" "$APP_NAME"
看起来复杂,其实就是一系列标准化步骤。这也是为什么你应该和大语言模型沟通,告诉它你的需求,让它帮你搭建工作流。
脚本中有几点值得注意:
set -euo pipefail会在任何命令执行失败时立即终止脚本,避免出现看似成功的半成品状态。cd "$(dirname "$0")/.."确保脚本无论从哪个目录执行,都会切换到仓库根目录,所以无论你在仓库根目录还是子目录下,都能正常运行./scripts/release.sh。- 预检查环节会在开始耗时的归档操作前,先确认
xcodegen已安装且公证配置文件存在,避免白忙活半天最后失败。 - 最后两步会重新验证已安装的应用包,而非仅验证导出的包。虽然有点冗余,但之前我遇到过复制过程中应用包损坏的情况,宁愿让脚本提前发现问题,也不想三天后被Gatekeeper删除应用才察觉。
创建CLAUDE.md或AGENTS.md
release.sh让你只需一条命令就能完成部署,而CLAUDE.md(如果用其他模型,则命名为AGENTS.md)则能让AI工具无需每次都询问,直接使用这些命令。
我和Claude反复沟通构建流程后,让它帮我生成了CLAUDE.md文件。现在每次创建新应用,我只需让它参考我其他应用的仓库,沿用相同的方法即可。
## 构建命令
```bash
# 修改project.yml或添加源文件后,重新生成Xcode项目
xcodegen generate
# 单元测试(仅针对YOUR-APP-NAMEKit;速度快,无需Xcode构建)
swift test
# macOS应用构建
xcodebuild -project YOUR-APP-NAME.xcodeproj -scheme YOUR-APP-NAME-macOS \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build
发布流程(开发者ID签名+公证)
./scripts/release.sh # 归档→开发者ID导出→公证→钉入凭证→安装
上述xcodebuild命令使用CODE_SIGNING_ALLOWED=NO,生成的是临时签名版本:适合CI和快速本地检查,但会被Gatekeeper拒绝,且iCloud KVS/应用组等权限无法生效(缺少团队前缀)。如果需要一个能通过隔离验证、支持iCloud同步的正式菜单栏应用,请使用scripts/release.sh。该脚本会用开发者ID签名,通过YOUR-APP-NAMEnotarytool钥匙串配置文件完成公证,钉入凭证,并安装至/Applications/YOUR-APP-NAME.app。
## 配置完成
以上就是所有一次性配置步骤。从现在起,所有操作都无需鼠标。
## 无GUI构建流程详解
以下所有操作均通过命令行完成,Xcode.app全程无需启动——这些工具虽然内嵌在Xcode中,但都可以独立运行。Claude Code也是通过Shell执行这些命令的。
### 快速无签名检查
如果只是想验证代码是否能编译、测试是否通过,完全不需要签名:
单元测试——纯SPM,无需Xcode构建
swift test
编译macOS应用(临时签名,无正式签名——适合CI/本地快速验证)
xcodebuild -project TZed.xcodeproj -scheme TZed-macOS
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build
编译iOS应用+小组件扩展(模拟器版本)
xcodebuild -project TZed.xcodeproj -scheme TZed-iOS
-destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
`CODE_SIGNING_ALLOWED=NO`会生成**临时签名**版本:可以在模拟器编译运行,但会被Gatekeeper拒绝,且iCloud KVS、应用组等权限无法生效。这是快速迭代的常用方式。
### Mac应用发布流水线
只需一条命令,就能完成完整的可发布流程——就是第二部分提到的脚本:
./scripts/release.sh
归档、开发者ID导出、公证、钉入凭证、验证、安装,一气呵成。任何步骤失败都会立即终止并提示。如果需要使用其他公证配置文件,可通过如下方式覆盖:`TZED_NOTARY_PROFILE=<名称> ./scripts/release.sh`。
### 无GUI部署至真实iPhone
iOS没有公证步骤——这是Mac独有的分发机制。将构建包部署到已连接的iPhone,只需使用Xcode工具链中的`xcodebuild`和`devicectl`:
为真实设备构建并签名(使用Apple开发证书+配置文件)
xcodebuild -project TZed.xcodeproj -scheme TZed-iOS
-destination 'generic/platform=iOS'
-allowProvisioningUpdates
-derivedDataPath build/ios archive -archivePath build/TZed-iOS.xcarchive
通过UDID将构建好的.app安装至已连接设备
xcrun devicectl device install app
--device <设备UDID> build/ios/…/TZed.app
执行`devicectl list devices`可查看已连接和配对的设备及其UDID。设备构建使用**Apple开发证书**(而非开发者ID证书)和开发配置文件,`-allowProvisioningUpdates`会自动从Apple获取所需配置文件。
## 无GUI环境下的代码签名原理
如果你之前只通过Xcode界面勾选框完成签名,有必要了解背后的原理——因为构建时完全不需要GUI参与。
**私钥是签名的核心**。创建开发者ID应用证书时,Apple会颁发证书,同时你的Mac会生成对应的私钥,两者都会存入登录钥匙串。`codesign`(`xcodebuild`会调用它)使用私钥对二进制文件签名,证书(可追溯至Apple根证书)会嵌入应用中,供他人验证。
**自动签名会自动选择身份**。发布脚本使用`signingStyle: automatic`,因此`xcodebuild`会根据团队ID自动选择正确的签名身份,并实时从Apple获取所需配置文件。无需将配置文件提交到代码仓库。
**权限在签名时生效**。每个目标都有一个`.entitlements`文件(包含沙箱、网络客户端、iCloud KVS、应用组等权限)。只有当应用使用真实团队身份签名时,这些权限才会生效——这也是临时签名版本无法发布的另一个原因:没有团队前缀,iCloud和应用组权限会静默失效,你可能会花一小时排查为什么键值存储是空的。
**公证≠签名**。签名用于证明应用的开发者身份,公证则是独立的步骤:Apple会扫描已签名的应用是否包含恶意软件,并颁发凭证;钉入操作会将该凭证附加到应用中,让Gatekeeper在离线状态下也能信任应用。对于无界面菜单栏应用(`LSUIElement`),公证是避免被XProtect标记的关键。
**敏感信息永远不会进入Git**。签名私钥存储在登录钥匙串中,公证用的应用专用密码存储在notarytool钥匙串配置文件中,两者都不会写入代码仓库。
你可以手动验证任何已签名的构建包:
codesign -dv --verbose=4 /Applications/TZed.app # 查看签名者及证书信息 spctl -a -vvv -t exec /Applications/TZed.app # 检查Gatekeeper是否允许运行 stapler validate /Applications/TZed.app # 检查是否已钉入公证凭证
## AI工具的实际工作方式
这里没有什么魔法。Claude Code只是通过普通的非交互式Shell执行所有操作——没有特殊的"构建"服务器或插件,就是用`xcodebuild`、`xcrun notarytool`、`xcrun stapler`、`spctl`、`codesign`、`devicectl`、`xcodegen`和`swift`这些标准命令行工具,和我们手动使用的工具完全一样。
关键的衔接文件是第二部分提到的`CLAUDE.md`,它告诉AI工具公证配置文件的命名规则、两种构建路径,以及发布需要执行`release.sh`。这样Claude就能直接运行命令,无需我每次都重复解释流程。
唯一需要交互式操作的步骤是`notarytool store-credentials`,这是有意为之的选择,而非技术限制:你可以通过`--password`参数将密码写入脚本,但这样会把应用专用密码留在Shell历史记录中。手动输入一次,让钥匙串或1Password保管,后续所有操作就能完全自动化。
## 为什么永远不需要打开Xcode
对比GUI流程和无界面流程,你会发现两者一一对应:
| 任务 | GUI方式 | 无界面方式 |
|---|---|---|
| 生成项目 | Xcode管理`.xcodeproj` | 基于`project.yml`执行`xcodegen generate` |
| 构建 | ⌘B/运行按钮 | `xcodebuild … build` |
| 归档 | 产品→归档 | `xcodebuild … archive` |
| 导出已签名应用 | 归档管理器→分发 | `xcodebuild -exportArchive` |
| 公证 | 归档管理器上传 | `xcrun notarytool submit --wait` |
| 钉入凭证 | 归档管理器自动完成 | `xcrun stapler staple` |
| 安装至/Applications | 拖拽操作 | `cp -R`+`lsregister` |
| 部署至iPhone | 运行至设备 | `xcodebuild archive`+`devicectl device install` |
GUI仅在一次性凭证配置时需要用到。之后,整个开发生命周期都可通过脚本自动化,而`release.sh`正是将这一流程固化下来的工具,也让AI工具能全程接管,我则可以去做比盯着进度条更有意义的事。
如果你想亲自尝试,步骤如下:安装Xcode和xcodegen,完成凭证配置,然后和Claude一起创建你自己的`release.sh`和`CLAUDE.md`。这部分实际工作最多只需一两个小时。之后,"发布新版本"就只是一句话的事。完成第一个应用的配置后,Claude Code还可以将相同的配置复制到后续应用中。