团队在依赖升级后顺利归档,却在发布前才发现新组件没有进入开源许可清单。问题通常不在编译,而在依赖来源分散:SwiftPM 有锁文件,CocoaPods 有另一套目录,仓库里还可能直接复制源码或加入二进制包。把检查放到云端 Mac CI,可以让同一份工程在固定工作区内生成清单、比较策略并保存证据,而不是靠发布负责人临时翻目录。
先定义门禁要回答的问题
一条可执行的门禁至少要回答四件事:本次构建实际解析了哪些组件、版本或提交是什么、许可证全文在哪里、该条件是否已经批准。不要只搜索文件名后直接放行,因为 LICENSE 存在不代表内容没有变化,也不代表组件真的被当前目标链接。
建议把结果拆成机器清单和人工报告。机器清单使用 JSON,记录组件名、来源、锁定版本、许可证类型、文本哈希与发现路径;人工报告使用 Markdown,展示新增、删除和变化项。两者都作为流水线产物保留。
门禁检查的是依赖事实与既定策略是否一致。发现未知项时应停止发布并请求复核,而不是根据许可证名称自动猜测处理方式。
从构建结果盘点真实依赖
先让 Xcode 完成依赖解析,再读取工作区实际使用的目录。这样可以避免扫描到开发者机器上的旧缓存。每个任务使用独立的 DerivedData,任务结束后再清理。
set -euo pipefail
ROOT="$(pwd)"
OUT="$ROOT/build/license-audit"
DD="$ROOT/build/DerivedData"
mkdir -p "$OUT"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-resolvePackageDependencies \
-derivedDataPath "$DD"
find "$DD/SourcePackages/checkouts" \
-maxdepth 3 \
-type f \
\( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
-print0 | sort -z > "$OUT/swiftpm-license-files.bin"
find "$ROOT/Pods" \
-maxdepth 4 \
-type f \
\( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
-print0 | sort -z > "$OUT/pods-license-files.bin"
二进制依赖不能只看文件扩展名。应同时登记下载地址对应的内部组件标识、校验和、版本和随包提供的声明。仓库内复制的源码则维护一份 compliance/manual-components.json,明确目录所有者,避免每次扫描都依赖模糊匹配。
锁文件必须同时入账
把 Package.resolved、Podfile.lock 和人工组件清单复制到报告目录。门禁要验证清单中的每个组件都能在某个锁定来源中找到,反向也要验证锁文件里的直接依赖没有从报告消失。若工程包含多个工作区,应逐个解析,不能只检查主应用。
规范化许可证并计算指纹
许可证文本经常只有换行符或尾部空格不同。可以先统一为 UTF-8、LF 换行并移除行尾空白,再计算 SHA-256。不要删除正文段落、版权行或年份,否则真实变化会被掩盖。
python3 - <<'PY'
from pathlib import Path
import hashlib
import json
root = Path("build/license-audit/licenses")
rows = []
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="strict")
normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip() + "\n"
digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
rows.append({"path": str(path), "sha256": digest})
Path("build/license-audit/license-hashes.json").write_text(
json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8"
)
PY
许可证类型识别与文本指纹要分开保存。类型用于匹配策略,指纹用于发现上游悄然修改。无法可靠识别时标记为 UNKNOWN,不得因为文件名看起来熟悉就自动归类。
用策略文件控制变化
策略文件应进入版本控制,并由明确的代码审查流程修改。可以按许可证标识设置 allow、review 和 deny,同时为特定组件记录已批准的文本哈希。检查结果建议采用以下判定:
| 情况 | CI 结果 | 后续动作 |
|---|---|---|
| 已知组件,版本与文本未变 | 通过 | 保存报告 |
| 新版本,许可证指纹未变 | 复核 | 确认组件变更 |
| 新组件或未知许可证 | 阻断 | 补充来源与审批 |
| 已有组件的许可证文本变化 | 阻断 | 比较全文差异 |
| 锁文件有组件但报告缺失 | 阻断 | 修复发现规则 |
允许列表不应只写宽泛类型。组件可能附带额外 NOTICE、双重许可或单独的资源授权,因此报告要保留所有相关文件。对于二进制包,还要验证当前下载物的校验和与批准记录一致。
把报告接入合并与发布流程
日常合并请求可以执行依赖解析、差异检查和报告生成;发布任务再增加归档内容核对,确认最终产物对应同一份锁文件。推荐把基线放在 compliance/license-baseline.json,CI 只比较结构化字段,不比较终端彩色输出。
门禁失败时,日志应直接列出组件、旧值、新值和证据路径,避免只返回“许可检查失败”。报告目录至少包含锁文件副本、规范化文本、哈希清单、策略版本和差异摘要。ArmCLI 上的并发任务应各自使用独立工作目录,避免一个任务更新依赖后污染另一个任务的扫描结果。
上线前检查项
发布前确认依赖解析没有使用未记录的本地路径;所有直接与传递依赖都有版本来源;未知许可证数量为零;人工组件清单中的目录仍然存在;报告与归档使用相同提交;CI 产物中不包含访问令牌、私有下载地址或未脱敏环境变量。
这套流程的价值不只是生成一页声明,而是把依赖变化变成可审阅的工程事件。只要锁文件、许可证指纹和策略基线保持同步,团队就能在依赖进入发布分支时发现问题,而不是在归档完成后重新追查组件来源。
常见问题
只检查 Package.resolved 是否足够?
不够。Package.resolved 能确认 SwiftPM 依赖版本,但不能证明许可证文本已被发现,也不会覆盖 CocoaPods、仓库内复制的源码或二进制依赖。
哪些变化应该阻断发布构建?
新增依赖没有许可证文件、许可证类型不在允许列表、锁文件与扫描结果不一致,或同一组件的许可证文本发生变化时,都应先阻断并要求人工复核。
为下一次构建准备独享物理 Mac
按任务选择机型、机房与租期。节点全年正常运行,实际可用状态以控制台实时返回为准。