构建任务在交互式终端里运行正常,换到无人值守 runner 后却报“无法打开”“操作不被允许”,或者进程刚启动就退出。很多人第一反应是执行 chmod +x,但文件已经具有执行权限时,真正的拦截者往往是 Gatekeeper、代码签名评估或下载文件携带的隔离属性。常见触发场景包括从浏览器下载工具、复制其他会话的产物,以及从保留扩展属性的归档中恢复缓存。
先区分四类执行失败
不要一看到“不能执行”就删除属性。先收集文件类型、权限、架构和扩展属性,四项结果足以排除大部分误判。
TOOL="/opt/build-tools/example-tool"
ls -leO@ "$TOOL"
file "$TOOL"
uname -m
xattr -l "$TOOL"
ls 用于确认所有者和执行位;file 应显示与节点一致的可执行架构;xattr 若出现 com.apple.quarantine,只能说明文件进入过隔离评估流程,不能直接证明文件安全。若错误是 Permission denied,先检查目录是否允许遍历以及挂载点是否带有执行限制。若错误是 Bad CPU type in executable,应更换正确架构的产物,而不是处理隔离属性。
| 现象 | 优先检查 | 不应立即做的事 |
|---|---|---|
| Permission denied | 文件与父目录权限 | 递归清除扩展属性 |
| Bad CPU type | file 与 uname -m |
反复修改执行位 |
| 已损坏或无法验证 | 签名与 Gatekeeper 结果 | 关闭系统安全检查 |
| 仅 runner 失败 | 实际路径、用户与缓存来源 | 假设交互环境完全相同 |
隔离属性是来源线索,不是故障本身。先证明拿到的是预期文件,再决定是否放行。
读取 Gatekeeper 的明确结论
对单个二进制或应用做评估
对命令行工具先检查签名,再请求系统策略给出结论。应用包可把 TOOL 改为对应的 .app 路径。
codesign --display --verbose=4 "$TOOL"
codesign --verify --deep --strict --verbose=2 "$TOOL"
spctl --assess --type execute --verbose=4 "$TOOL"
未签名的内部工具不一定有问题,但它必须来自受控构建流程,并由摘要校验承担完整性证明。第三方预编译工具若宣称带签名,验证失败就应停止使用,重新取得可信产物,不能靠删除隔离属性掩盖签名变化。
查询策略日志
弹窗和 runner 的标准错误经常省略原因。复现后立即读取最近的安全策略记录,可以确认被评估的真实路径,避免检查了缓存 A,实际执行的却是缓存 B。
log show --last 10m \
--predicate 'subsystem == "com.apple.security.syspolicy"' \
--style compact
把日志中的路径与 command -v、runner 配置及脚本变量逐一核对。符号链接尤其容易造成偏差:入口位于工具目录,最终文件却来自旧缓存。
放行前先验证产物身份
最稳妥的流程是让工具发布方或内部制品流程提供 SHA-256 摘要,并把摘要文件与工具版本一起纳入仓库配置。不要从下载结果临时计算一个摘要,再用它验证同一份文件,那只能证明文件读取前后没有变化,不能证明来源正确。
cd /opt/build-tools
shasum -a 256 -c example-tool.sha256
codesign --verify --deep --strict --verbose=2 example-tool
内部自编译工具可以没有外部分发签名,但至少应保留提交版本、构建脚本版本和预期摘要。若工具是压缩包,先验证压缩包,再解压到临时目录,检查最终可执行文件,最后原子替换正式路径。这样 runner 不会在更新中途读到半个文件。
只清除目标的隔离属性
确认摘要、版本和签名符合预期后,只处理被验证的目标。先读取原值并写入作业日志,再删除指定属性。
TOOL="/opt/build-tools/example-tool"
xattr -p com.apple.quarantine "$TOOL" 2>/dev/null || true
if xattr -p com.apple.quarantine "$TOOL" >/dev/null 2>&1; then
xattr -d com.apple.quarantine "$TOOL"
fi
spctl --assess --type execute --verbose=4 "$TOOL"
"$TOOL" --version
不要对工作区执行 xattr -cr .。这会同时清除仓库、依赖、脚本和临时产物的多种扩展属性,既扩大放行范围,也让后续调查失去来源证据。应用包确实可能需要处理包内继承的属性,但仍应把范围限定到已经验证的单个包,而不是 runner 的整个缓存根目录。
把检查固化为安装门禁
稳定的云端 Mac CI 不应在每个构建任务里临时修权限。把工具准备拆成独立安装阶段:下载到临时目录、核对摘要、检查架构、验证签名、读取隔离状态、定点放行、执行版本自检,然后才写入共享工具目录。
缓存键至少包含工具名、版本、CPU 架构和摘要。命中缓存后仍要执行轻量检查,因为缓存可能被人工覆盖。runner 启动时可记录 id -un、uname -m、工具解析路径和版本,但不要输出凭据或完整环境变量。
最后保留三个失败原则:摘要不符立即停止;签名本应存在却验证失败时重新获取;只有隔离评估阻止了已验证产物时才删除目标属性。这样做比全局关闭检查多几条命令,却能让工具来源、放行动作和实际执行版本都可审计。
常见问题
为什么工具具有执行权限,仍然会被 macOS 阻止?
执行权限只决定文件是否可被启动。Gatekeeper 还会检查隔离属性、代码签名、来源与系统安全策略,因此 chmod +x 不能解决所有阻止执行的问题。
可以对整个 CI 工作目录递归执行 xattr -cr 吗?
不建议。递归清除会移除目录内所有文件的扩展属性,掩盖未知产物的来源。应先验证摘要和签名,再只删除目标文件的 com.apple.quarantine 属性。
怎样避免同一工具在每次构建时重复被拦截?
把下载、摘要校验、解包和定点放行放入受控的工具安装阶段,并缓存已经验收的最终文件;缓存键应包含工具版本、架构和摘要。
在独享 Apple Silicon 物理节点上运行构建
按机型、区域和租用周期配置节点,实际可用状态以控制台实时返回为准。