多环境支持¶
多环境通过不同配置复用同一套代码,例如切换服务地址、功能开关和应用名称。TestFlight 是测试分发渠道,上传到 TestFlight 不会自动切换应用使用的服务环境。
Xcode 构建配置¶
| 概念 | 用途 |
|---|---|
| Project | 组织源代码、资源、构建设置和多个 Target |
| Target | 定义要生成的产品,例如 App、扩展或测试包,以及构建所需的文件和步骤 |
| Build Settings | 编译、链接、签名等具体设置 |
| Build Configuration | 一组命名的构建设置,例如 Debug、Release,也可增加环境配置 |
| Scheme | 指定 Run、Test、Profile、Archive 等操作使用的 Target 和 Build Configuration |
环境差异主要是参数和资源时,可在同一 Target 下使用不同的 Configuration 与 Scheme。需要测试版和正式版同时安装时,使用不同的 Bundle ID,并分别配置签名和 App Store Connect 应用记录。Flutter 多环境需按 iOS flavors 文档 配置 Scheme 和对应的 Configuration。
用 xcconfig 管理构建设置¶
- 创建 Configuration Settings File(
.xcconfig),填写需要管理的构建设置,不将它加入 Target 的资源。 - 在 Project → Info → Configurations 中,为相应 Project/Target 的 Configuration 选择基础配置文件;每处选择一个文件,可通过
#include "Common.xcconfig"复用配置。 - 同层级 Build Settings 中显式设置的值会覆盖 xcconfig。需要交给文件管理的设置,应移除对应显式覆盖。
- 需要保留继承值并追加列表项时,使用
$(inherited),例如OTHER_SWIFT_FLAGS = $(inherited) -D STAGING。
参考:Apple 构建系统、Scheme 配置、xcconfig 配置。
fastlane:打包与上传 TestFlight¶
fastlane 使用内置 action 组织任务:build_app 负责原生项目归档和导出,upload_to_testflight 负责上传及等待处理。一个 lane 是一组命名的操作,可通过命令执行。
1. 准备项目和工具¶
先检查当前终端使用的 Xcode 和 iOS SDK:
xcode-select -p
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version
xcodebuild -checkFirstLaunchStatus
xcode-select -p 应指向完整 Xcode 的 Contents/Developer。如果指向独立 Command Line Tools 或错误版本,在 Xcode → Settings → Locations → Command Line Tools 中选择目标 Xcode;首次运行所需组件和许可按 Xcode 提示完成,然后重新检查。
- 在 Signing & Capabilities 中配置 Team、签名证书和描述文件,在 Manage Schemes 中勾选目标 Scheme 的 Shared。首次接入先通过 Product → Archive 归档,在 Organizer 中使用 Validate App 验证。
- 确认 Scheme 的 Archive 使用预期环境配置。发布配置的 Debug Information Format 应为
DWARF with dSYM File,保留每次发布的.xcarchive和符号文件。Apple 调试符号说明
安装 Ruby 与 Bundler(一次性)。 使用 rbenv 管理 Ruby;以下以 rbenv 1.3.2、Ruby 3.4.11 为本次核验版本,后续按 Ruby 维护状态及项目兼容性升级。fastlane 当前支持 Ruby 3.2 及以上,建议使用 3.3 及以上的受支持版本。已有 rbenv 的电脑复用原安装,不重复克隆。
git clone --branch v1.3.2 --depth 1 https://github.com/rbenv/rbenv.git "$HOME/.rbenv"
git clone --depth 1 https://github.com/rbenv/ruby-build.git "$HOME/.rbenv/plugins/ruby-build"
用 open -e "$HOME/.zshrc" 打开交互式 shell 配置,在所有已有 PATH 修改之后加入以下内容;已有等效配置时调整位置,不重复添加。登录 shell 也需使用此环境时,在 ~/.zprofile 末尾加入同样的内容:
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - zsh)"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
新开终端标签页后执行:
rbenv install -s 3.4.11
rbenv global 3.4.11
ruby --version
command -v ruby
gem install bundler
bundle --version
command -v ruby 应指向 ~/.rbenv/shims/ruby。rbenv global 设置没有项目版本约束时的默认 Ruby;项目中的 .ruby-version 优先。Ruby 编译需要可用的 Xcode 编译工具和依赖库,遇到编译失败按 ruby-build 构建要求 检查,不向系统 Ruby 安装 gems。参考:rbenv 安装、fastlane 安装。
配置每个项目。 下文“执行目录”指 fastlane/ 的上级目录:原生项目通常为 .xcodeproj/.xcworkspace 所在目录,Flutter 项目为 ios/。先进入该目录。
新项目可执行 rbenv local 3.4.11 生成 .ruby-version;已有项目先检查自身或上级目录的 .ruby-version、Gemfile 和锁文件要求,安装并使用其兼容版本,不直接覆盖原配置。
项目及上级目录没有 Gemfile 时创建如下内容;已有文件时复用并合并 fastlane,保留现有依赖和版本约束:
source "https://rubygems.org"
gem "fastlane"
bundle install
bundle exec fastlane --version
mkdir -p fastlane
bundle install 按已有 Gemfile.lock 安装,首次运行会生成锁文件。将 .ruby-version、Gemfile、Gemfile.lock 和 fastlane/Fastfile 纳入版本管理;后续通过 bundle exec fastlane 执行。需要升级 fastlane 时才执行 bundle update fastlane,验证后保留新锁文件。切换到其他 Ruby 版本后,如缺少 Bundler,需在该版本下安装;已有锁文件的项目遵循其 BUNDLED WITH 版本。
使用 CocoaPods 的项目还需先安装 Pod 依赖。若已有 Gemfile 管理 CocoaPods,运行 bundle exec pod install;否则先用 pod --version 确认可用,再按项目原流程运行 pod install。希望将它也纳入 Bundler 时,在 Gemfile 中加入符合项目要求的 gem "cocoapods",重新 bundle install。不使用 CocoaPods 的项目可跳过。
2. 配置 App Store Connect API Key¶
首次上传前,确认对应 Apple Developer 团队具备发布权限,并在 App Store Connect 中创建与 Bundle ID 匹配的 App 记录。本例使用团队 API Key 认证,无需 Apple ID 密码或 App 专用密码:
- 登录 App Store Connect → Users and Access → Integrations → App Store Connect API。尚未开通 API 时,由 Account Holder 申请;开通后由 Account Holder 或 Admin 创建 Team Key。
- 仅上传构建可使用 Developer;后续要维护测试说明、通知设置或测试人员时,使用 App Manager 或 Admin。Team Key 按所选角色作用于团队全部 App,不能限制为单个 App。
- 保存 Key ID、Issuer ID,下载只能下载一次的
.p8私钥,放到仓库外。例如先创建目录,再将下载文件移入其中:
mkdir -p "$HOME/.config/appstoreconnect"
chmod 700 "$HOME/.config/appstoreconnect"
将 AuthKey_你的Key_ID.p8 替换为实际文件名后设置权限:
chmod 600 "$HOME/.config/appstoreconnect/AuthKey_你的Key_ID.p8"
在执行目录创建 fastlane/.env,替换以下占位符;私钥使用绝对路径:
ASC_KEY_ID=你的Key_ID
ASC_ISSUER_ID=你的Issuer_ID
ASC_KEY_PATH="/Users/你的用户名/.config/appstoreconnect/AuthKey_你的Key_ID.p8"
APP_IDENTIFIER=com.example.myapp
保存后执行 chmod 600 fastlane/.env。这些变量由下方 Fastfile 读取,APP_IDENTIFIER 必须与实际 IPA 一致。API Key 用于访问 App Store Connect;归档和导出仍需要项目签名证书、对应私钥及描述文件。
在项目 .gitignore 中合并以下规则:
**/fastlane/.env*
!**/fastlane/.env.example
*.p8
*.ipa
*.xcarchive/
*.dSYM/
*.dSYM.zip
**/fastlane/report.xml
.env.example 仅保存占位符。忽略规则不会取消已被 Git 跟踪的文件,提交前仍需检查范围。自动构建环境通过密钥管理功能注入变量和私钥文件。
参考:Apple 上传要求、Apple API Key、fastlane API Key 认证。
3. 创建 Fastfile¶
在执行目录创建 fastlane/Fastfile;已有 Fastfile 时合并所需入口。native_beta 构建原生项目后上传,upload_ipa 上传已有 IPA,Flutter 使用后者。路径参数支持绝对路径和相对于执行目录的路径。
# Fastfile 的 Ruby 代码在 fastlane/ 中运行,统一以其上级目录解析路径。
execution_directory = File.expand_path("..", Dir.pwd)
def required_value(value, name)
UI.user_error!("缺少参数或配置:#{name}") if value.to_s.strip.empty?
value.to_s
end
def required_env(name)
required_value(ENV[name], name)
end
def required_file(value, name, root)
path = File.expand_path(required_value(value, name), root)
UI.user_error!("#{name} 文件不存在或不可读:#{path}") unless File.file?(path) && File.readable?(path)
path
end
def connect_api_key(root)
app_store_connect_api_key(
key_id: required_env("ASC_KEY_ID"),
issuer_id: required_env("ASC_ISSUER_ID"),
key_filepath: required_file(ENV["ASC_KEY_PATH"], "ASC_KEY_PATH", root),
in_house: false
)
end
default_platform(:ios)
platform :ios do
desc "上传已有 IPA,并等待 Apple 处理完成"
lane :upload_ipa do |options|
ipa = required_file(options[:ipa], "ipa", execution_directory)
UI.user_error!("ipa 必须指向 .ipa 文件:#{ipa}") unless File.extname(ipa).downcase == ".ipa"
upload_to_testflight(
api_key: connect_api_key(execution_directory),
app_identifier: required_env("APP_IDENTIFIER"),
ipa: ipa,
skip_waiting_for_build_processing: false,
skip_submission: true
)
end
desc "构建原生 iOS 项目并上传 TestFlight"
lane :native_beta do |options|
project = File.expand_path(required_value(options[:project], "project"), execution_directory)
UI.user_error!("project 不是有效的 .xcodeproj 目录:#{project}") unless File.directory?(project) && File.extname(project) == ".xcodeproj"
scheme = required_value(options[:scheme], "scheme")
build_number = required_value(options[:build_number], "build_number")
build_options = {
scheme: scheme,
export_method: "app-store",
output_directory: File.join(execution_directory, "build", "ios")
}
if options[:workspace].to_s.strip.empty?
build_options[:project] = project
else
workspace = File.expand_path(options[:workspace], execution_directory)
UI.user_error!("workspace 不是有效的 .xcworkspace 目录:#{workspace}") unless File.directory?(workspace) && File.extname(workspace) == ".xcworkspace"
build_options[:workspace] = workspace
end
# 先检查上传配置,避免完成归档后才发现缺少凭据。
%w[ASC_KEY_ID ASC_ISSUER_ID APP_IDENTIFIER].each { |name| required_env(name) }
required_file(ENV["ASC_KEY_PATH"], "ASC_KEY_PATH", execution_directory)
increment_build_number(xcodeproj: project, build_number: build_number)
ipa = build_app(**build_options)
upload_ipa(ipa: ipa)
end
end
写好后先进行本地检查,以下命令只解析并列出入口,不执行构建或上传:
ruby -c fastlane/Fastfile
bundle exec fastlane lanes
- fastlane 的 Fastfile 代码与 action 工作目录不同,示例先把输入转换为绝对路径,避免文件检查和 action 指向不同位置。目录规则
increment_build_number内部使用agvtool,会修改工程及相关 Info.plist,多个 Target 的构建号也可能一起更新。显式传build_number时不强制启用 Apple Generic Versioning;仍需确认工程的CURRENT_PROJECT_VERSION、最终CFBundleVersion能被正确更新。若版本来自 xcconfig 或自定义脚本,先适配项目已有机制,再使用此入口;自动加一模式则需配置 Apple Generic Versioning。fastlane 构建号、Apple agvtoolbuild_app按 Scheme 的 Archive 配置构建并返回 IPA 路径。fastlane 参数使用export_method: "app-store";新 Xcode 的底层名称为app-store-connect,仍兼容旧名,不能据此直接替换 fastlane 的参数值。以已锁定的 fastlane 和xcodebuild -help为准。build_app 参数skip_waiting_for_build_processing: false等待处理,skip_submission: true跳过 fastlane 后续分发。本例不传测试元数据;以后加入 changelog 等参数时,skip_submission并不会跳过此前的元数据更新,需要相应权限。上传参数与权限- App Store Connect 内部组已启用的自动分发仍可能生效,手动分发需关闭该组的 Enable automatic distribution。两个入口均不提交正式 App Store 审核,上传后仍需执行第 5 步。
4. 选择项目类型执行¶
每次上传前,使用当前版本下尚未上传过、递增的构建号。下面的工程名、路径、版本号和 42 都是示例,需要替换为本次发布值。
原生 iOS 项目¶
在执行目录运行,替换为实际工程、工作空间和 Shared Scheme:
bundle exec fastlane ios native_beta \
project:"MyApp.xcodeproj" \
workspace:"MyApp.xcworkspace" \
scheme:"MyApp-Production" \
build_number:42
只有 .xcodeproj 时省略 workspace:,无需修改 Fastfile。CocoaPods 项目应使用安装依赖后生成的 workspace。
多环境可通过 fastlane/.env.staging、fastlane/.env.production 覆盖 APP_IDENTIFIER 等环境差异,文件权限同样设为 600,命令末尾加 --env staging 或 --env production。它们只配置 fastlane,实际 App 环境仍由 Scheme 及其构建配置决定。环境变量加载规则
Flutter 项目¶
先在 Flutter 项目根目录检查环境;使用 FVM 的项目将下列 flutter 替换为 fvm flutter,使用项目要求的 SDK:
flutter doctor -v
flutter pub get
flutter build ipa --release --build-name=1.2.0 --build-number=42
先处理 flutter doctor -v 中与 Xcode、iOS 构建相关的问题。flutter build ipa 完成归档和签名导出,产物位于 build/ios/archive/、build/ios/ipa/。确认生成了本次 IPA 后进入 ios/ 上传;下面路径相对于 ios/,也可使用绝对路径:
cd ios
bundle exec fastlane ios upload_ipa \
ipa:"../build/ios/ipa/实际文件名.ipa"
已配置 flavors 的项目,构建时添加 --flavor production 等实际 Scheme 名;上传时使用对应 .env.production 并添加 --env production。
--build-name 对应用户看到的版本号,--build-number 对应构建号;也可在 pubspec.yaml 中维护 version: 1.2.0+42。此流程不再调用 build_app,也不将清理缓存作为每次发布的固定步骤。Flutter iOS 发布指南、Flutter iOS 环境配置
5. 验证与分发¶
- 在 App Store Connect → 应用 → TestFlight 中核对 Bundle ID、版本号和构建号,确认构建已处理完成;按实际加密用途填写必要的出口合规信息。
- 内部测试:将构建加入内部测试组,并确认测试人员能安装指定构建。若使用自动分发,检查对应组和成员设置。
- 外部测试:填写测试说明及必要审核信息,选择外部测试组并提交 TestFlight 审核;首个构建需要审核,后续构建也可能需要审核,获准后再分发。
- 正式上架:在 App Store Connect 创建或选择正式版本,选用已验证的构建,补齐资料并提交 App Review。无需为了正式提审重新上传相同构建。
上传成功、处理完成、测试者可安装和正式上架是不同状态,应分别确认。保留本次构建产物及日志;第三方崩溃平台需要的符号表应来自同一归档。