MangoVM 工程指南

用 xcconfig 管理 iOS 多环境配置并检查归档泄露

用 xcconfig 管理 iOS 多环境配置并检查归档泄露

同一套 iOS 工程同时连接开发、预发布和生产服务时,最危险的错误通常不是编译失败,而是成功归档了错误环境。开发地址、日志开关或内部功能标记一旦进入发布包,流水线仍可能显示绿色。更稳妥的做法是把公开配置分层管理,把构建入口固定下来,并把归档检查当成发布步骤,而不是依赖某位开发者在 Xcode 里选对菜单。

先划清配置与秘密的边界

xcconfig 适合保存可以随客户端公开的构建参数,例如 API 基础地址、Bundle ID 后缀、日志级别和功能开关默认值。它不是秘密保险箱。凡是写进客户端构建过程的值,都可能出现在 Info.plist、资源文件、编译参数或可执行文件中。

判断标准很简单:如果用户拿到安装包后不应该知道这个值,就不要把它交给 Xcode 编进 App。

服务端管理密钥、签名服务凭据和具有写权限的令牌应留在服务端或受控的 CI 凭据存储中。客户端只接收短期、低权限且可撤销的授权结果。即使 CI 通过环境变量注入秘密,也不能改变“最终产物可被分析”这一事实。

先列一张配置清单,并为每项标明公开性、来源和归档后的预期位置。团队评审的是这张清单,而不是散落在 Build Settings 中的几十个值。

建立可审查的 xcconfig 分层

建议将公共设置、环境差异和本地覆盖拆开:

Config/
  Base.xcconfig
  Development.xcconfig
  Staging.xcconfig
  Production.xcconfig
  LocalOverrides.xcconfig.example

Base.xcconfig 只放所有环境共享的值。各环境文件先包含它,再覆盖少量差异:

#include "Base.xcconfig"

APP_ENVIRONMENT = staging
API_BASE_URL = https:/$()/staging-api.invalid
ENABLE_VERBOSE_LOGGING = YES
PRODUCT_BUNDLE_IDENTIFIER = com.example.product.staging

这里的 $() 用于避免 URL 中的双斜线被解析成注释。示例域名只是不可解析的文档值,实际项目应替换为自己的服务地址。

本地覆盖文件不要提交仓库,可用可选包含降低首次检出门槛:

#include? "LocalOverrides.xcconfig"

但本地覆盖只能用于开发便利,不能成为正式归档的隐式输入。生产配置应在没有该文件时仍能完整解析。把 DevelopmentStagingRelease 三个 Configuration 分别绑定到对应文件,并确认用于 CI 的 Scheme 已设置为 Shared。

固定命令行构建入口

云端 Mac 上的归档任务不应继承某次图形界面操作留下的状态。workspace、scheme、configuration 和输出路径都要显式传入:

set -euo pipefail

WORKSPACE="App.xcworkspace"
SCHEME="App"
CONFIGURATION="Release"
ARCHIVE_PATH="$PWD/build/App.xcarchive"

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -showBuildSettings > build-settings.txt

xcodebuild \
  -workspace "$WORKSPACE" \
  -scheme "$SCHEME" \
  -configuration "$CONFIGURATION" \
  -archivePath "$ARCHIVE_PATH" \
  clean archive

在归档前校验解析结果

不要只检查源文件,要检查 Xcode 最终解析出的设置。可以从 build-settings.txt 提取关键项,并对生产环境设置硬断言:

grep -E "APP_ENVIRONMENT|API_BASE_URL|PRODUCT_BUNDLE_IDENTIFIER|ENABLE_VERBOSE_LOGGING" \
  build-settings.txt

grep -q "APP_ENVIRONMENT = production" build-settings.txt
grep -q "ENABLE_VERBOSE_LOGGING = NO" build-settings.txt

若项目使用多个 Target,应为每个 Target 分别检查,因为扩展、测试包和主 App 可能继承不同的 Base Configuration。脚本还应打印当前 Git 提交、Xcode 版本与所用 Scheme,方便出现差异时还原现场。

扫描真正交付的归档

Build Settings 正确不代表产物一定正确。Run Script、生成代码或复制资源步骤仍可能把测试配置带入归档。归档完成后,至少检查主 App 的属性列表、嵌入资源和可执行文件字符串。

APP_PATH="$(find build/App.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"

plutil -p "$APP_PATH/Info.plist"

if grep -RInaE "staging-api|debug-token|INTERNAL_ONLY" "$APP_PATH"; then
  echo "forbidden marker found in archive" >&2
  exit 1
fi

EXECUTABLE_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleExecutable" "$APP_PATH/Info.plist")
if strings "$APP_PATH/$EXECUTABLE_NAME" | grep -E "staging-api|debug-token|INTERNAL_ONLY"; then
  echo "forbidden marker found in executable" >&2
  exit 1
fi

扫描词应使用团队定义的标记,不要把真实秘密直接写进脚本或日志。建议维护“允许出现”和“禁止出现”两份短清单,避免普通单词造成误报。扫描结果只保留文件路径和规则名,不回显命中的完整敏感内容。

检查对象 应核对内容 失败处理
最终 Build Settings 环境名、Bundle ID、日志开关 立即停止归档
Info.plist 服务地址、URL Scheme、环境标签 阻止导出
App 资源目录 调试配置、测试数据、临时文件 删除来源并重建
可执行文件 内部地址特征、令牌标记 定位生成步骤并轮换相关凭据

处理常见漂移与误区

最常见的漂移来自“Scheme 在本机存在但未共享”。仓库中缺少对应的 xcshareddata 后,CI 可能无法找到 Scheme,或工程师临时换用了另一个入口。第二类问题是把环境名写进脚本分支,却没有把 Configuration 作为唯一来源,久而久之会形成两套判断逻辑。

另一个误区是认为变量被混淆、拆分或写入二进制后就安全。客户端中的长期秘密没有可靠的隐藏方式。发现它进入历史归档时,应先撤销或轮换凭据,再修复构建配置;只删除当前分支里的字符串并不能消除已经分发的风险。

在 MangoVM 的固定物理节点上执行这套流程时,还应让 Runner 使用干净的工作目录,不复用上一次任务生成的 DerivedData、临时配置和导出目录。清理目标要限定在本次工作区,避免并发任务互相删除文件。

把检查变成合并与发布门禁

最终可将流程拆成两个阶段:合并请求阶段解析设置并检查配置文件结构,发布阶段执行完整归档和产物扫描。前者反馈快,后者以真正交付物为准。任何环境断言失败都应返回非零状态,不能只在日志里提示。

提交前可以用以下清单收尾:

当配置来源、解析结果和最终产物形成三层证据,环境选择就不再依赖操作习惯。发布失败会发生在归档离开流水线之前,而不是由用户替团队发现。

常见问题

可以把 API 密钥直接写进 xcconfig 吗?

不可以把需要保密的服务端密钥写入 xcconfig。构建设置可能进入 Info.plist、编译参数或可执行文件,最终能从归档中提取。App 只应包含可公开的客户端配置,真正的密钥应保留在服务端。

为什么本地归档正常,CI 却使用了错误环境?

常见原因是 Scheme 未共享、Configuration 映射不一致,或脚本依赖交互式 Shell 中才存在的变量。应显式传入 scheme、configuration 和 workspace,并用 xcodebuild -showBuildSettings 保存最终设置作为证据。

独享物理节点

为开发流水线配置固定的云端 Mac

MangoVM 提供 M4 与 M4 Pro 两档 Apple Silicon 物理节点。选择租期、区域和存储附加项后,即可核对完整订单明细。

选择配置并订购