无需打开 Xcode 即可构建并发布 Mac 和 iOS 应用
摘要
一份全面指南,介绍如何完全通过命令行构建并发布 macOS 和 iOS 应用,无需打开 Xcode,使用 XcodeGen 和 Apple 的命令行工具。
暂无内容
查看缓存全文
缓存时间: 2026/07/13 19:54
# 构建并发布 Mac 和 iOS 应用,全程无需打开 Xcode
来源:https://scottwillsey.com/building-and-shipping-mac-and-ios-apps-without-ever-opening-xcode/
最近,我听到不少 Apple 相关的播客主播抱怨 Xcode 有多糟糕,并认为 Apple 需要改进 Xcode,让 Mac 和 iOS 应用的“随性编程”体验更加友好、不再那么晦涩难懂。他们说得没错,但我也不明白为什么他们一开始要打开 Xcode。只要做一些前期准备,你就能尽情地用“随性编程”的方式开发 Mac 和 iOS 应用,再也不用看 Xcode 了。而且,如果你对以下任何步骤有疑问,只需将 Claude Code 或你喜欢的 LLM 编码工具指向这篇博文,让它来处理即可。这正是它的工作——帮你搞定那些你不想操心的事。
## TL;DR
- **Xcode.app 必须安装,但永远不需要打开。**`xcodebuild`、`notarytool`、`stapler` 和 `devicectl` 都位于 Xcode 内部,能从 shell 中完美运行。
- **少数一次性步骤需要 GUI**(或交互式终端):登录你的 Apple ID、创建 Developer ID 证书、存储公证密码。之后,构建和发布完全可以无界面运行。
- **Mac 应用通过一个脚本发布**——`scripts/release.sh`——你只需编写一次。它会执行整个流程:归档 → Developer 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”这个名称可能会引起混淆。** 这是因为存在一个独立的 Command Line Tools 包,通过 `xcode-select --install` 安装,版本位于 `/Library/Developer/CommandLineTools`。它包含 clang 和 git,但不包含 iOS SDK、notarytool、devicectl 以及其他完整应用开发所需的工具。完整的工具链在 Xcode.app 内部,位于 `/Applications/Xcode.app/Developer`,并且具备你所需的一切。如果你已安装 Xcode,就不需要独立的 Command Line Tools。
## 安装 XcodeGen
Xcode 及其命令行工具不足以自动生成和管理 Xcode 项目。为此,你需要 [XcodeGen](https://xcodegen.com/)。你可以从 [GitHub](https://github.com/yonaskolb/XcodeGen) 下载它,或者使用 Homebrew 安装:
```
brew install xcodegen
```
简而言之,Xcode 项目实际上是文件夹,macOS 让它们看起来像文件,其中包含了创建和编译应用所需的所有信息。Xcode 会不断修改文件和文件引用,这会给 git 仓库带来问题。XcodeGen 创建一个 project.yml(YAML)文件来存放所有项目设置,然后每次构建时,它都会使用该 project.yml 文件重新创建整个 `.xcodeproj` 文件夹。只有 YAML 文件需要提交到 git,整个 `.xcodeproj` 可以从 git 中忽略。
## 一次性配置 Xcode
你需要先设置好 Xcode,这样以后就再也不用看它了。
### Xcode 许可证和附加组件
首先,接受其许可证并安装附加组件,可以通过命令行完成:
```
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
```
### 在 Xcode 中设置你的 Apple 开发者账号
接下来,打开 Xcode,点击“设置”→“账户”,然后点击“+”添加你的账号。
**注意:你必须拥有付费的 Apple 开发者账号,才能分发和公证你的应用。** 你会需要对应用进行公证,这样它们才能在你的 Mac 和 iOS 设备上安装,而不会被操作系统认定为恶意软件并删除。
### 创建 Developer ID Application 证书
完成后,创建一个 Developer ID Application 证书(设置 → 账户 → 你的 Apple ID → **管理证书...** → **+** → **Developer ID Application**),这会创建一个用于签名已发布的 `.app` 包的证书。
请注意,Developer ID Application 证书和你的 Apple Development 证书是两回事。**Apple Development** 身份用于在你自己的设备上构建和运行——推送到你的 iPhone、本地调试。**Developer ID Application** 身份用于经过公证的 `.app`,它能通过 Gatekeeper 的检查并在其他人的 Mac 上运行。发布脚本需要使用后者。
在 Xcode 中创建证书会将证书*和*其私钥都安装到你的登录钥匙串中。这个私钥才是真正进行签名的东西,**无法重新下载**——所以不要删除它,并备份你的钥匙串。如果不确定,可以向你的 LLM 询问并让它帮你设置。反正最终也是它替你使用 Xcode。
最后,
### 存储公证凭据——在终端中一次性完成
公证会将你签名的应用上传到 Apple 进行恶意软件扫描。`notarytool` 使用你一次性交互式创建的已存储钥匙串配置文件进行身份验证——它会提示你输入应用专用密码,这个提示无法绕过:
```
xcrun notarytool store-credentials App-Name \
--apple-id "[email protected]" --team-id YOUR-TEAM-ID
# 在提示时粘贴一个应用专用密码
```
这里有一些值得注意的地方:
- **将配置文件名设为应用名称。** 不要借用其他应用的配置文件——这会在你的机器上起作用,但在别人的机器上会静默失败。
- **应用专用密码不是你的 Apple ID 密码。** 在 [appleid.apple.com](https://appleid.apple.com/) → **登录与安全 → 应用专用密码** 生成一个。
- **当你更改 Apple ID 密码时,这些密码会静默失效。** 公证时出现 `401 invalid credentials` 几乎总是意味着“去生成一个新的应用专用密码”,而不是“你的设置有问题”。
确认它已存储:
```
xcrun notarytool history --keychain-profile App-Name
```
顺便提一下,我将应用专用密码存储在 Claude Code 可以访问的 1Password 保管库中。这样,每当我创建新应用时,我可以让**它**为我创建公证凭据,它会知道去 1Password 保管库中查找密码。使用 LLM 的初衷就是避免手动执行那些你不想做的事情。
### 设置本地 .xconfig 文件并将其添加到 .gitignore
实际签名需要你的团队 ID 和包前缀,我将它们放在 Local.xconfig 文件中:
```
cp Local.xcconfig.example Local.xcconfig
# 然后编辑 Local.xcconfig 设置:
# BUNDLE_PREFIX = your.real.prefix
# DEVELOPMENT_TEAM = YOUR-TEAM-ID
```
再次强调,如果不确定,请让 Claude Code 或你选择的 LLM 为你创建这个文件。
### 创建发布脚本
我应用的发布通过一个名为 `release.sh` 的脚本来处理,该脚本位于仓库内的 `scripts` 文件夹中。没有它,我就没有自动化的构建流水线。我让 Claude Code 创建了我的脚本:
我大致告诉 Claude:“我想要归档、用 Developer ID 签名、公证、钉章,然后将此应用安装到 /Applications,无需打开 Xcode。请为我编写一个执行整个流程的脚本,如果任何步骤失败,请大声报错。”
它不需要我解释流水线,因为流水线并非秘密——用 `xcodebuild` 归档,用 `-exportArchive` 和 `ExportOptions.plist` 导出,用 `notarytool --wait` 提交,用 `stapler` 附加票据,用 `spctl` 检查。这是发布 Developer ID Mac 应用的文档化标准方式,模型知道这一点。
它需要**我**提供的是项目特定的内容:方案名称、团队 ID、公证配置文件的名称、将结果安装到哪里。然后它写了初稿,我们运行了,它失败了,我们又修复了。这种循环不是失败模式,而是过程的一部分。我总是将 AI 工作流视为进行中的作品,但不需要太久,你就可以停止调整,开始实际工作。
以下是我一个应用仓库中的实际脚本:
```bash
#!/usr/bin/env bash
# scripts/release.sh — 生成一个 Developer 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 "Pre-flight"
command -v xcodegen >/dev/null || fail "xcodegen not installed (brew install xcodegen)."
if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then
fail "notarytool profile '$NOTARY_PROFILE' missing. Create it with xcrun notarytool store-credentials."
fi
step "Regenerating project"
xcodegen generate
rm -rf "$BUILD_DIR"; mkdir -p "$BUILD_DIR"
step "Archiving (Release)"
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
-derivedDataPath "$BUILD_DIR/derived" -archivePath "$ARCHIVE_PATH" \
-allowProvisioningUpdates archive
step "Exporting Developer ID-signed app"
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 "Exported app not found at $APP_PATH."
step "Notarizing (submitting to Apple, may take a few minutes)"
ditto -c -k --keepParent "$APP_PATH" "$BUILD_DIR/notarize.zip"
xcrun notarytool submit "$BUILD_DIR/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --wait
step "Stapling ticket"
xcrun stapler staple "$APP_PATH"
step "Verifying Gatekeeper acceptance"
spctl -a -vvv -t exec "$APP_PATH"
step "Installing to $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 "Verifying installed bundle"
xcrun stapler validate "$INSTALL_DIR/$APP_NAME.app"
spctl -a -vvv -t exec "$INSTALL_DIR/$APP_NAME.app"
printf "\n\033[1;32m✓ %s notarized, stapled, installed.\033[0m\n" "$APP_NAME"
```
它看起来比实际复杂,但它是一系列你本来就需要知道如何执行的步骤。再次强调,这就是为什么你要与 LLM 对话,告诉它你想要什么,让它帮助你构建工作流程。
一些需要注意的地方:
- `set -euo pipefail` 会让脚本在任何失败命令后立即中止,而不是继续盲目执行。不存在看起来像成功的半完成状态。
- `cd "$(dirname "$0")/.."` 使脚本从仓库根目录运行,无论你在哪里调用它,因此 `./scripts/release.sh` 无论是从仓库根目录还是向下三级目录都能正常工作。
- **前置检查块**会在花费五分钟进行注定在第五步失败的归档之前,检查 `xcodegen` 是否存在以及公证配置文件是否已存储。
- 最后两步会重新验证*已安装*的包,而不仅仅是导出的包。双重保险,但我之前遇到过复制步骤静默损坏包的情况,我宁愿从脚本中得知,而不是三天后 Gatekeeper 删除我的应用时才发现。
### 创建 CLAUDE.md 或 AGENTS.md
`release.sh` 为你提供了单命令发布能力。`CLAUDE.md`(或对于几乎所有其他模型来说是 `AGENTS.md`)能让代理*使用*它,而无需每次都手动告知。我让 Claude 自己在反复讨论构建过程后创建了 CLAUDE.md。现在每当我创建新应用时,我让它参考我另一个应用的仓库并使用相同的方法。
```
## Build commands
```bash
# Regenerate the Xcode project after changing project.yml or adding source files
xcodegen generate
# Unit tests (YOUR-APP-NAMEKit only; fast, no Xcode build required)
swift test
# macOS app
xcodebuild -project YOUR-APP-NAME.xcodeproj -scheme YOUR-APP-NAME-macOS \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO build
## Release (Developer ID + notarization)
```bash
./scripts/release.sh # archive → Developer ID export → notarize → staple → install
```
上述 `xcodebuild` 命令使用了 `CODE_SIGNING_ALLOWED=NO`,会生成一个**ad-hoc**构建:适合 CI 和快速本地检查,但 Gatekeeper 会拒绝它,并且 iCloud KVS / App Group 授权无法绑定(没有团队前缀)。要获得真正的菜单栏构建,能够通过隔离检查并让 iCloud 同步工作,请使用 `scripts/release.sh`。它会用 Developer ID 签名,通过 `YOUR-APP-NAME` notarytool 钥匙串配置文件进行公证,钉章,并安装到 `/Applications/YOUR-APP-NAME.app`。
## 就是这样
这就是整个一次性设置。从现在开始,不需要鼠标。
## 构建实际如何运行——无需 GUI 介入
以下都是纯命令行调用。Xcode.app 不会启动;这些工具位于其内部,但独立运行。这正是 Claude Code 通过其 shell 执行的内容。
### 快速、未签名检查
对于“能否编译/测试是否通过”,你完全不需要签名:
```bash
# 单元测试——纯 SPM,无需 Xcode 构建
swift test
# 编译 macOS 应用(ad-hoc,未签名——适用于 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` 会生成一个**ad-hoc**构建:它可以在模拟器中编译和运行,但 Gatekeeper 会拒绝它,且 iCloud KVS 和 App Group 等授权无法绑定。这是快速的内部循环。
### Mac 发布流水线
一个命令完成整个可发布流程——第二部分中的脚本:归档、Developer ID 导出、公证、钉章、验证、安装。如果任何步骤失败,它会停止并告诉你哪个步骤出了问题。需要不同的公证配置文件?覆盖它:`TZED_NOTARY_PROFILE= ./scripts/release.sh`。
### 部署到真实 iPhone,无界面
iOS 没有公证步骤——那是 Mac 分发的概念。将构建物安装到已连接的 iPhone 上需要 `xcodebuild` 加 `devicectl`,两者都在 Xcode 的工具链中:
```bash
# 为真实设备构建并签名(使用 Apple Development 证书 + 配置文件)
xcodebuild -project TZed.xcodeproj -scheme TZed-iOS \
-destination 'generic/platform=iOS' \
-allowProvisioningUpdates \
-derivedDataPath build/ios archive -archivePath build/TZed-iOS.xcarchive
# 将构建好的 .app 通过 UDID 安装到已连接的设备上
xcrun devicectl device install app \
--device build/ios/.../TZed.app
```
`devicectl list devices` 会列出已连接和配对的设备及其 UDID。设备构建使用 **Apple Development** 身份签名(而不是 Developer ID 身份)。
相似文章
@RayFernando1337: 在云端构建iOS应用!
Devin Cloud Agents 现在支持带有 Xcode 和 iOS 模拟器的 macOS 环境,使得无需 Mac 即可在云端开发 iOS 应用。
@dkundel:你现在就应该构建你梦想的macOS应用!Codex中的“Build macOS App”插件非常疯狂。使用语音听写来……
一位开发者展示了如何使用OpenAI Codex的“Build macOS App”插件配合语音听写快速构建一个macOS应用,在不到7分钟(加上6分钟调整)内完成。
NoMac.app
NoMac.app 是一个为AI代理设计的无头iOS应用发布管道。
打造出一款能创建高度个性化 macOS 应用的 macOS 应用,支持 Gemma 4 E2B 等小模型
Ironsmith 是一款开源 macOS 应用,只需一个提示即可生成本地 macOS 应用,使用 Gemma 4 等本地 AI 模型,能在 8GB MacBook Air 等低端硬件上运行。
@chyni: Codex现在开发iOS app 真是太香了。 Product Design + 内置浏览器做原型 iOS App Build 做开发,结合内置的模拟器预览功能 现在都不需要打开xcode了
Codex 现在可以直接用于开发 iOS 应用,整合了产品设计、内置浏览器原型和模拟器预览功能,无需打开 Xcode。