Bitcoin Core macOS 部署与确定性打包:从 make deploy 到 Apple SDK 提取与分离签名
本篇指南基于 Bitcoin Core 仓库的 contrib/macdeploy/README.md 展开,讲解 macOS 版应用的完整部署链路:如何通过 make deploy 目标调用 macdeployqtplus 脚本生成可分发的 Bitcoin-Core.zip、如何从 Xcode.app 中提取受许可限制的 Apple SDK 并生成可自由再分发的确定性 tarball,以及官方构建流程中“无签名构建 + 分离签名(detached signature)”如何在不泄露 Apple 签名密钥的前提下保持构建的可复现性。读完后你能独立完成 macOS 部署构建、SDK 归档制备,并理解 release 签名流程中每一方(构建者、Apple 密钥持有者)的职责边界。
一、make deploy:macOS 应用打包的官方入口
contrib/macdeploy/README.md 明确说明:macdeployqtplus 脚本不应手动运行,正确的用法是在正常构建完成后执行:
make deploy
完成时会生成 Bitcoin-Core.zip。这一流程背后的自动化逻辑可以在构建系统中找到对应实现。
deploy 目标在 CMake 中的定义
在 cmake/module/Maintenance.cmake 的 add_macos_deploy_target 函数中(第 42–108 行),deploy 目标的构建链条是:
- 填充 App bundle 骨架:从 share/qt/Info.plist.in 生成
Bitcoin-Qt.app/Contents/Info.plist,写入PkgInfo、空empty.lproj、应用图标bitcoin.icns和InfoPlist.strings; - 安装主二进制:通过
cmake --install ... --component bitcoin-qt --prefix ${macos_app}/Contents/MacOS --strip将bitcoin-qt二进制安装进 bundle 并重命名为Bitcoin-Qt,随后清理临时的bin/share目录; - 区分宿主平台:
- 在 macOS 上构建(
CMAKE_HOST_APPLE):deploy直接调用python3 contrib/macdeploy/macdeployqtplus Bitcoin-Qt.app -translations-dir=... -zip=bitcoin-macos-app,产出签名前可用的 zip; - 在 Linux 上交叉构建:
macdeployqtplus通过OBJDUMP环境变量指向交叉 objdump,输出解包到dist/目录;随后 cmake/script/macos_zip.sh 用zip打包为dist/bitcoin-macos-app.zip(若系统未安装zip命令,deploy目标会直接报错提示)。
- 在 macOS 上构建(
此外,deploydir 目标只负责产出部署后的 app(不打包 zip),deploy 依赖 deploydir,而二者又依赖 bitcoin-qt 目标,保证了资源生成、bundle 填充、依赖收集、压缩打包的完整依赖顺序。
macdeployqtplus 做什么
contrib/macdeploy/macdeployqtplus 是社区对 Qt 官方 macdeployqt 的增强移植版本,其核心职责是:
- 用
objdump --macho --dylibs-used递归解析Bitcoin-Qt二进制及其依赖的动态库(framework / dylib),把 Qt 等第三方库复制进Contents/Frameworks并重写 install name 为@executable_path/../Frameworks/...; - 跳过系统库:以
/System/Library/、@executable_path、/usr/lib/开头的依赖不会被打包(见FrameworkInfo.fromLibraryLine,第 86 行附近),避免把系统自带的 framework 冗余塞进 bundle; - 部署 Qt 插件到
Contents/PlugIns、将基础语言翻译文件加入Contents/Resources。
其命令行参数(第 385–397 行)为:
| 参数 | 说明 |
|---|---|
app-bundle |
要部署的应用 bundle(此处为 Bitcoin-Qt.app) |
-verbose |
输出额外调试信息 |
-no-plugins |
跳过插件部署(默认部署) |
-no-strip |
不对二进制执行 strip(默认会 strip) |
-translations-dir path |
Qt 翻译文件路径,基础翻译会自动加入 bundle 资源 |
-zip name |
将 app bundle 打包为同名 zip |
注意脚本会在每次运行时清空 dist 目录后再部署,保证产物干净。因此手动运行该脚本既容易破坏状态又没有必要——make deploy 才是受构建系统管控的正确入口。
二、Apple SDK 提取:两阶段流程
由于所有构建都必须针对 Apple SDK,而 SDK 虽可免费下载但不可再分发,release 流程需要一套标准化的“从 Xcode 提取 SDK”步骤,保证各构建者拿到的是同一份输入。README 将过程分为两步。
步骤 1:获取 Xcode.app
- 需要一个免费的 Apple 开发者账户;
- 从 Apple 开发者下载页获取
Xcode_26.1.1_Apple_silicon.xip(也可登录后在 Downloads → More 中搜索Xcode 26.1.1); - 下载需要 Apple ID 且浏览器需允许该域名保存 cookies;
- 下载的 XIP 归档 sha256 应为
f4c65b01e2807372b61553c71036dbfef492d7c79d4c380a5afb61aa1018e555。
在 Linux 上解压 .xip(需要 cpio 和 bitcoin-core 维护的 apple-sdk-tools 仓库中的提取脚本):
# 安装/克隆提取 Xcode.app 所需的工具
apt install cpio
git clone https://github.com/bitcoin-core/apple-sdk-tools.git
# 解开 .xip,并将得到的 Xcode.app 放到当前工作目录
python3 apple-sdk-tools/extract_xcode.py -f Xcode_26.1.1_Apple_silicon.xip | cpio -d -i
在 macOS 上则简单得多:
xip -x Xcode_26.1.1_Apple_silicon.xip
步骤 2:从 Xcode.app 生成 SDK tarball
使用 contrib/macdeploy/gen-sdk.py,把上一阶段解出的 Xcode.app 路径作为第一个参数:
./contrib/macdeploy/gen-sdk.py '/path/to/Xcode.app'
生成的归档应命名为 Xcode-26.1.1-17B100-extracted-SDK-with-libcxx-headers.tar,其 sha256 应为 9600fa93644df674ee916b5e2c8a6ba8dacf631996a65dc922d003b98b5ea3b1——两个校验和是核对所有构建者输入一致性的关键。
gen-sdk.py 的源码细节
阅读 contrib/macdeploy/gen-sdk.py 可以看到这个脚本是如何保证产物确定性的:
- 版本信息自动读取:脚本解析
Xcode.app/Contents/version.plist获取CFBundleShortVersionString和ProductBuildVersion,再解析MacOSX.sdk/System/Library/CoreServices/SystemVersion.plist获取 SDK 的ProductVersion与ProductBuildVersion,并据此拼出输出文件名Xcode-{xcode_version}-{xcode_build_id}-extracted-SDK-with-libcxx-headers.tar(可通过-o参数覆盖输出路径); - 只打包必要子树:仅添加 SDK 下的
usr/include、usr/lib和System/Library/Frameworks三个目录,而不是整个 SDK; - 过滤 Swift 模块文件:
change_tarinfo_base过滤器会丢弃.swiftmodule与.modulemap文件(第 65–66 行)——C++ 交叉编译用不到它们,剔除后 tarball 体积大幅减小; - 元数据归一化:对每个 tar 条目强制
mtime = 0、uid/gid 归零、用户名组名置空、权限统一为0o0755(可执行位)或0o0644(第 71–76 行),这正是“interim tarballs 完全确定且可自由再分发”这一承诺的实现基础; - PAX 格式打包:使用
tarfile.PAX_FORMAT写出,配合上述归一化保证跨机器字节级一致。
三、确定性 macOS 构建的签名难题与“分离签名”方案
deterministic macOS App Notes 一节描述了整个 release 流程中最微妙的问题。背景约束有三点:
- macOS 应用在 Linux 上用较新的 LLVM 交叉编译产生;
- 构建必须针对 Apple SDK,但该 SDK 不可再分发(上一节的提取流程即为此服务);
- 使用 Apple 认可的密钥对二进制签名是产出可分发 macOS 二进制的硬性要求,而该私钥无法共享。
Guix 构建流程被设计为不把 SDK 文件包含进其输出;所有中间 tarball 均完全确定、可自由再分发。签名问题则通过三方协作解决:
- 构建者(builders):用 Guix 产出未签名 release——一个用户可选择自行祝福、自签名并运行的未签名 ZIP,外加一个未签名 app 结构的 tarball;
- Apple 密钥持有者(Apple keyholder):用未签名 app 通过仓库内置脚本生成分离签名(detached signature),这些签名公开存放于 bitcoin-core 维护的 bitcoin-detached-sigs 仓库;
- 构建者:把未签名 app + 分离签名喂回 Guix,将二者组合成一个确定性的已签名 ZIP。
分离签名脚本的工作原理
contrib/macdeploy/detached-sig-create.sh 就是密钥持有者使用的“included script”,它依赖 signapple(README 与 release 流程文档均要求安装并更新到最新 master)。脚本用法(见 doc/release-process.md 的 Codesigning 一节):
tar xf bitcoin-${VERSION}-${ARCH}-apple-darwin-codesigning.tar.gz
./detached-sig-create.sh /path/to/codesign.p12 /path/to/AuthKey_foo.p8 <apple developer team uuid>
三个参数分别是:Apple 代码签名证书(.p12)、App Store Connect API key(.p8)、Apple 开发者团队 UUID。脚本会两次提示输入密钥口令(输入时不回显)。
从脚本源码(第 12–62 行)可以看到它的具体动作:
- 定位未签名 bundle
dist/Bitcoin-Qt.app及其主二进制Contents/MacOS/Bitcoin-Qt,用signapple info检测架构(arm64/x86_64),输出目录为sign.temp/osx/${ARCH}-apple-darwin; - 签名 + 公证 app bundle:
signapple sign -f --hardened-runtime --detach ...生成分离的 bundle 签名,signapple apply将其套回 bundle 验证,signapple notarize --detach ...生成分离的公证票据; - 逐个签名子二进制:遍历
*/bin/*与*/libexec/*下的每个可执行文件(对应 bitcoind、bitcoin-cli、bitcoin-tx、bitcoin-wallet、bitcoin-util 等命令行工具),逐个 detach 签名并生成<name>.<arch>sign文件; - 公证二进制:对
bin目录做一次整体 notarize(脚本注释说明:二进制无法使用 stapled 公证,因此这一步不产生实际输出,仅用于验证公证能通过); - 最后将
sign.temp/osx/${ARCH}-apple-darwin打包为signature-osx-${ARCH}.tar.gz。
这个签名产物(detached signature)提交到 bitcoin-detached-sigs 仓库后,任意构建者都可以执行 guix-codesign 流程将其与未签名 app 组合出最终签名版本——由于签名本身是确定性的输入之一,最终 ZIP 依然可被多方独立复现并互相校验。doc/release-process.md 还建议:在正式打 tag 之前先测试签名能否正确附加;但若测试后又在 detached-sigs 仓库提交了正式签名,guix-codesign 步骤需要重跑一次以保证 attestation 与非签名构建保持一致。
四、小结与可复现性要点
围绕 contrib/macdeploy/README.md 的三条主线可以归纳为:
| 环节 | 命令/脚本 | 确定性保障 |
|---|---|---|
| 应用打包 | make deploy → macdeployqtplus |
由 CMake 目标驱动,dist 目录每次清空重建 |
| SDK 提取 | 解压 Xcode.xip → gen-sdk.py | mtime/uid/gid/权限归一化,XIP 与 tarball 双重 sha256 校验 |
| 代码签名 | detached-sig-create.sh + signapple | 分离签名公开可复用,私钥永不离开密钥持有者 |
适用前提与限制:SDK 提取流程绑定在 Xcode 26.1.1 (17B100) 这一具体版本上(sha256 值即针对该版本),换用其他 Xcode 版本时 tarball 文件名与校验和都会变化;make deploy 的 zip 打包路径在 Linux 交叉构建环境下要求系统存在 zip 命令;而签名相关脚本仅在持有 Apple 密钥的官方发布流程中可完整执行,普通使用者拿到的是可直接运行的已签名发行版。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00