首页
/ Bitcoin Core macOS 部署与确定性打包:从 make deploy 到 Apple SDK 提取与分离签名

Bitcoin Core macOS 部署与确定性打包:从 make deploy 到 Apple SDK 提取与分离签名

2026-09-05 12:16:29作者:蔡丛锟

本篇指南基于 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.cmakeadd_macos_deploy_target 函数中(第 42–108 行),deploy 目标的构建链条是:

  1. 填充 App bundle 骨架:从 share/qt/Info.plist.in 生成 Bitcoin-Qt.app/Contents/Info.plist,写入 PkgInfo、空 empty.lproj、应用图标 bitcoin.icnsInfoPlist.strings
  2. 安装主二进制:通过 cmake --install ... --component bitcoin-qt --prefix ${macos_app}/Contents/MacOS --stripbitcoin-qt 二进制安装进 bundle 并重命名为 Bitcoin-Qt,随后清理临时的 bin/share 目录;
  3. 区分宿主平台
    • 在 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.shzip 打包为 dist/bitcoin-macos-app.zip(若系统未安装 zip 命令,deploy 目标会直接报错提示)。

此外,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 获取 CFBundleShortVersionStringProductBuildVersion,再解析 MacOSX.sdk/System/Library/CoreServices/SystemVersion.plist 获取 SDK 的 ProductVersionProductBuildVersion,并据此拼出输出文件名 Xcode-{xcode_version}-{xcode_build_id}-extracted-SDK-with-libcxx-headers.tar(可通过 -o 参数覆盖输出路径);
  • 只打包必要子树:仅添加 SDK 下的 usr/includeusr/libSystem/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 流程中最微妙的问题。背景约束有三点:

  1. macOS 应用在 Linux 上用较新的 LLVM 交叉编译产生;
  2. 构建必须针对 Apple SDK,但该 SDK 不可再分发(上一节的提取流程即为此服务);
  3. 使用 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 bundlesignapple 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 deploymacdeployqtplus 由 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 密钥的官方发布流程中可完整执行,普通使用者拿到的是可直接运行的已签名发行版。

登录后查看全文
热门项目推荐
相关项目推荐