云端 Mac 上构建并验收 macOS PKG 安装包

CI/CD 实践 ·约 8 分钟阅读

云端 Mac 上构建并验收 macOS PKG 安装包

团队把命令行工具、后台代理或公司内部应用交付给多台 Mac 时,直接复制文件很快会遇到三个问题:目标路径不一致、升级时旧文件残留、安装结果无法审计。macOS PKG 能把 payload、权限、安装脚本和版本收据放进一个可追踪产物,但前提是不要把开发工作区直接塞给 pkgbuild。下面用一个安装到 /Library/Application Support/AcmeTool 的命令行组件,搭出可放进云端 Mac CI 的构建与验收流程。

先定义安装契约

打包前先写清目标路径、所有者、权限、包标识符和升级规则。包标识符发布后应保持稳定,版本则必须单调递增;否则系统收据无法准确判断覆盖关系。

项目 示例 验收要求
安装根目录 /Library/Application Support/AcmeTool 不写入用户主目录
可执行文件 bin/acmetool root:wheel,权限 0755
配置模板 config/default.json 权限 0644,不含密钥
包标识符 com.example.acmetool.pkg 不随分支变化
版本 1.4.0 与发布标签一致

构建机上的工程目录不是安装契约的一部分。.git、测试报告、下载缓存、临时日志和本机生成的扩展属性都不应进入 PKG。

最可靠的打包输入不是“当前仓库”,而是一次重新创建、内容可枚举的暂存目录。

创建干净的 payload

每次任务开始都删除旧暂存区,显式复制允许交付的文件,再统一权限。不要用宽泛的 cp -R .,因为它会让新增文件在无人注意时进入安装包。

set -euo pipefail

ROOT="$PWD"
STAGE="$ROOT/build/stage"
PKGDIR="$ROOT/build/packages"
INSTALL_DIR="$STAGE/Library/Application Support/AcmeTool"

rm -rf "$STAGE" "$PKGDIR"
mkdir -p "$INSTALL_DIR/bin" "$INSTALL_DIR/config" "$PKGDIR"

install -m 0755 "$ROOT/dist/acmetool" "$INSTALL_DIR/bin/acmetool"
install -m 0644 "$ROOT/packaging/default.json" \
  "$INSTALL_DIR/config/default.json"

xattr -cr "$STAGE"
find "$STAGE" -type d -exec chmod 0755 {} +
find "$STAGE" -type f ! -path '*/bin/acmetool' -exec chmod 0644 {} +

随后生成一份清单,既方便代码审查,也能在构建结束时比较实际输入:

find "$STAGE" -print0 |
  sort -z |
  xargs -0 stat -f '%Sp %Su:%Sg %N' > "$PKGDIR/payload-manifest.txt"

grep -Eq '/(\.git|DerivedData|node_modules)(/|$)' \
  "$PKGDIR/payload-manifest.txt" && exit 1 || true

这里先移除扩展属性,再设定权限,避免下载来源标记或开发机 ACL 被带入产物。配置模板只能放默认值,运行期令牌应在部署时注入。

用 pkgbuild 生成组件包

安装脚本放在独立目录,并确保可执行。脚本应幂等、快速且非交互;不要读取终端输入,也不要假设存在某个登录用户。

mkdir -p packaging/scripts
cat > packaging/scripts/postinstall <<'SH'
#!/bin/sh
set -eu

TARGET="/Library/Application Support/AcmeTool"
test -x "$TARGET/bin/acmetool"
chown -R root:wheel "$TARGET"
chmod 0755 "$TARGET" "$TARGET/bin" "$TARGET/bin/acmetool"
chmod 0644 "$TARGET/config/default.json"
exit 0
SH
chmod 0755 packaging/scripts/postinstall

pkgbuild \
  --root "$STAGE" \
  --identifier "com.example.acmetool.pkg" \
  --version "1.4.0" \
  --install-location "/" \
  --scripts packaging/scripts \
  "$PKGDIR/AcmeTool-component.pkg"

只有一个组件时,组件包已能安装。若需要统一欢迎页、许可文本或组合多个组件,再用 productbuild 生成分发包:

productbuild \
  --package "$PKGDIR/AcmeTool-component.pkg" \
  "$PKGDIR/AcmeTool-1.4.0.pkg"

需要签名时,在受保护的 CI 环境中读取 Installer 签名身份,通过 --sign "$INSTALLER_IDENTITY" 构建正式包。未签名产物仍可用于前置测试,但不能冒充最终交付件。

不安装也要先做静态验收

第一层检查不修改系统。先确认包标识符、版本与 payload,再展开分发包检查脚本和元数据:

PKG="$PKGDIR/AcmeTool-1.4.0.pkg"
EXPANDED="$PKGDIR/expanded"

pkgutil --check-signature "$PKG" || true
pkgutil --payload-files "$PKGDIR/AcmeTool-component.pkg"
rm -rf "$EXPANDED"
pkgutil --expand-full "$PKG" "$EXPANDED"

grep -R "com.example.acmetool.pkg" "$EXPANDED"
grep -R "1.4.0" "$EXPANDED"
find "$EXPANDED" -type f -name postinstall -exec sh -n {} \;

若正式包要求签名,不要对 pkgutil --check-signature 使用容错,应让非零退出码直接阻断流水线。还要拒绝绝对构建路径、私钥片段和工作区用户名:

if grep -R -E "$HOME|BEGIN (RSA |EC )?PRIVATE KEY" "$EXPANDED"; then
  echo "sensitive build data found" >&2
  exit 1
fi

常见误区是只比较包文件大小。体积稳定不代表内容稳定;清单、标识符、版本、脚本语法和签名状态必须分别检查。

隔离安装并核对收据

最终验收要在可回滚的测试环境执行,因为 installer 会写入系统路径和安装收据。测试节点应在任务前恢复到已知状态,且不要与日常开发会话共用。

sudo installer -pkg "$PKG" -target /

test -x "/Library/Application Support/AcmeTool/bin/acmetool"
test "$(stat -f '%Sp' \
  '/Library/Application Support/AcmeTool/bin/acmetool')" = "-rwxr-xr-x"

pkgutil --pkg-info "com.example.acmetool.pkg"
pkgutil --files "com.example.acmetool.pkg" |
  sort > "$PKGDIR/installed-files.txt"

"/Library/Application Support/AcmeTool/bin/acmetool" --version

安装成功只说明脚本返回了零,还应验证可执行文件能启动、配置模板可解析、收据版本正确,并把安装日志与文件清单作为 CI 产物保存。失败时先保留 /var/log/install.log 中对应时间段,再恢复测试环境,不要立即重跑覆盖现场。

收尾检查可以固定为五项:暂存目录无额外文件、权限与所有者符合契约、包版本单调递增、正式产物签名通过、隔离安装后命令与收据均可验证。这样 PKG 就从“能双击的压缩包”变成了可审查、可重复、可回归的工程交付物。

常见问题

pkgbuild 的 root 目录应该直接指向工程目录吗?

不应该。先创建只包含最终安装内容的暂存目录,再把它传给 pkgbuild,避免源码、缓存、日志和本机扩展属性被意外写入安装包。

没有安装签名证书时能否先验证 PKG 流程?

可以。先生成未签名包并完成结构、权限、脚本与隔离安装测试;正式交付前再使用可用的 Installer 签名身份构建,并通过 pkgutil --check-signature 验证。

独享物理节点

为下一次构建或推理任务配置云端 Mac

从三档 Apple Silicon 配置、六个节点和固定租期中选择,实际可用状态以控制台实时返回为准。

配置云端 Mac