无需打开 Xcode 即可构建并发布 Mac 和 iOS 应用

Hacker News Top 工具

摘要

一份全面指南,介绍如何完全通过命令行构建并发布 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 身份)。

相似文章

NoMac.app

Product Hunt

NoMac.app 是一个为AI代理设计的无头iOS应用发布管道。