Fastlane 实践(一):自动化打包和发布
遇到问题, 多了一个扩展打包失败,顺便看到了这个文章觉得不错,转载记录一下,
xcode 12 自动打包问题,dev 有arm64 兼容问题,切换ad-hoc 自动打包上传解决
From: http://chaosky.tech/2020/05/04/fastlane-in-action-1/
fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.
fastlane 是自动化Beta部署和发布iOS和Android应用程序最简单方法。它可以处理所有繁琐的任务,例如生成屏幕截图,处理代码签名以及发布应用程序。
Fastlane 安装
安装 Xcode command line tools
$ xcode-select --install
安装 Homebrew
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装 RVM
$ curl -sSL https://get.rvm.io | bash -s stable --auto-dotfiles
$ source ~/.rvm/scripts/rvm
修改 RVM 的 Ruby 安装源到 Ruby China 的 Ruby 镜像服务器,这样能提高安装速度。
$ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > ~/.rvm/user/db
安装Ruby 2.6.5
$ rvm install 2.6.5
$ rvm use 2.6.5 --default
更新 RubyGems 镜像
$ gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
$ gem sources -l
https://gems.ruby-china.org
# 确保只有 gems.ruby-china.org
bundle config mirror.https://rubygems.org https://gems.ruby-china.org
安装 CocoaPods 和 Fastlane
$ gem install cocoapods
$ gem install fastlane -NV
$ gem install bundle
快速开始
进入 iOS App 的目录并运行:
1
fastlane init
fastlane 会自动自动识别你的项目,并询问任何缺失的信息。
Fastlane 进阶用法
随着公司项目的增多,每次都运行重复的Fastlane 命令进行配置会低效很多,所以急需一套可以满足所有App需求的配置。
Fastlane 是由Ruby开发,所以也支持 dotenv 的功能。
最终Fastlane生成目录结构如下:
├── .env
├── Appfile
├── Deliverfile
├── Fastfile
├── Matchfile
├── Pluginfile
├── README.md
├── Scanfile
├── metadata
│ ├── app_icon.jpg
│ ├── copyright.txt
│ ├── primary_category.txt
│ ├── primary_first_sub_category.txt
│ ├── primary_second_sub_category.txt
│ ├── review_information
│ │ ├── demo_password.txt
│ │ ├── demo_user.txt
│ │ ├── email_address.txt
│ │ ├── first_name.txt
│ │ ├── last_name.txt
│ │ ├── notes.txt
│ │ └── phone_number.txt
│ ├── secondary_category.txt
│ ├── secondary_first_sub_category.txt
│ ├── secondary_second_sub_category.txt
│ ├── trade_representative_contact_information
│ │ ├── address_line1.txt
│ │ ├── address_line2.txt
│ │ ├── address_line3.txt
│ │ ├── city_name.txt
│ │ ├── country.txt
│ │ ├── email_address.txt
│ │ ├── first_name.txt
│ │ ├── is_displayed_on_app_store.txt
│ │ ├── last_name.txt
│ │ ├── phone_number.txt
│ │ ├── postal_code.txt
│ │ ├── state.txt
│ │ └── trade_name.txt
│ └── zh-Hans
│ ├── apple_tv_privacy_policy.txt
│ ├── description.txt
│ ├── keywords.txt
│ ├── marketing_url.txt
│ ├── name.txt
│ ├── privacy_url.txt
│ ├── promotional_text.txt
│ ├── release_notes.txt
│ ├── subtitle.txt
│ └── support_url.txt
└── pem
├── development_xxx.xxx.xxx.p12
├── development_xxx.xxx.xxx.pem
├── development_xxx.xxx.xxx.pkey
├── production_xxx.xxx.xxx.p12
├── production_xxx.xxx.xxx.pem
├── production_xxx.xxx.xxx.pkey
.env
这个文件中放入的是需要引用的环境变量。
FASTLANE_SKIP_UPDATE_CHECK=true
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
APPLE_ID="xxxx"
TEAM_ID="xxxx"
FASTLANE_PASSWORD="xxx"
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="qwwe-tdpp-hdpc-fgzy"
ITC_TEAM_ID="xxxx"
APP_IDENTIFIER="xxx.xxx.xxx"
SCHEME_NAME="XXX"
WORKSPACE_NAME="XXX.xcworkspace"
XCODEPROJ_NAME="XXX.xcodeproj"
DEV_APP_IDENTIFIER="xxx.xxx.dev.xxx"
DEV_APP_NAME="XXX测试版"
PROD_APP_IDENTIFIER="xxx.xxx.xxx"
PROD_APP_NAME="XXX"
MATCH_GIT_BRANCH="XXX"
DELIVER_METADATA_PATH="./fastlane/metadata"
DOWNLOAD_METADATA_PATH="./metadata"
Appfile
app_identifier "#{ENV["APP_IDENTIFIER"]}"
apple_id "#{ENV["APPLE_ID"]}"
team_id "#{ENV["TEAM_ID"]}"
itc_team_id "#{ENV["ITC_TEAM_ID"]}"
Deliverfile
app_identifier "#{ENV["APP_IDENTIFIER"]}"
username "#{ENV["APPLE_ID"]}"
Fastfile
fastlane_require "spaceship"
fastlane_version "2.89.0"
default_platform :ios
platform :ios do
base_path = Pathname::new(File::dirname(__FILE__)).realpath.parent
before_all do
end
desc "生成 adhoc 测试版本,提交到蒲公英,参数 => type:'adhoc/development',默认adhoc"
lane :pgyer_beta do |options|
type = String(options[:type] || "adhoc")
if type == "adhoc"
export_method = "ad-hoc"
match_type = "adhoc"
match_type_name = "AdHoc"
else
export_method = "development"
match_type = "development"
match_type_name = "Development"
end
git_reversion = sh("git log -1 --pretty=format:'%h'")
version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")
build_number = number_of_commits(all: false)
git_log = sh("git log --no-merges -1 --pretty=format:'# %ai%n# %B by %an'")
build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")
output_dir = "#{base_path}/Output/adhoc/#{build_time}"
output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"
add_badge(shield: "#{version_number}-#{build_number}-orange")
increment_build_number(build_number: build_number)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}"
)
update_info_plist(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
block: proc do |plist|
plist["CFBundleDisplayName"] = "#{ENV["DEV_APP_NAME"]}"
plist["CFBundleName"] = "#{ENV["DEV_APP_NAME"]}"
plist["GIT_REVISION"] = git_reversion
plist["BUILD_TIME"] = build_time
plist["APP_CHANNEL"] = "pgyer"
urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}
urlScheme[:CFBundleURLSchemes] = ["#{ENV["DEV_WEIXIN_APPID"]}"]
end
)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["DEV_NOTIFICATION_SERVICE"]}"
)
match(
type: "#{match_type}",
app_identifier: ["#{ENV["DEV_APP_IDENTIFIER"]}", "#{ENV["DEV_NOTIFICATION_SERVICE"]}"],
readonly: true
)
gym(
export_method: "#{export_method}",
include_bitcode: false,
scheme: "#{ENV["SCHEME_NAME"]}",
configuration: "AdHoc",
export_options: {
compileBitcode: false,
uploadBitcode: false,
provisioningProfiles: {
"#{ENV["DEV_APP_IDENTIFIER"]}" => "match #{match_type_name} #{ENV["DEV_APP_IDENTIFIER"]}",
"#{ENV["DEV_NOTIFICATION_SERVICE"]}" => "match #{match_type_name} #{ENV["DEV_NOTIFICATION_SERVICE"]}"
}
},
output_directory: output_dir,
output_name: output_name
)
upload_ipa(type: 'gxm', log: git_log)
bugly(app_id: "#{ENV["DEV_BUGLY_APPID"]}",
app_key:"#{ENV["DEV_BUGLY_APPKEY"]}",
symbol_type: 2,
bundle_id: "#{ENV["DEV_APP_IDENTIFIER"]}",
product_version: "#{version_number}(#{build_number})",
channel: 'pgyer'
)
copy_dsym(tpye: 'adhoc')
end
desc "生成 adhoc 预发版本,提交到蒲公英"
lane :pgyer_release do
git_reversion = sh("git log -1 --pretty=format:'%h'")
build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")
version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")
build_number = number_of_commits(all: false)
git_log = sh("git log --no-merges -1 --pretty=format:'# %ai%n# %B by %an'")
output_dir = "#{base_path}/Output/release/#{build_time}"
output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"
add_badge(shield: "#{version_number}-#{build_number}-orange", alpha: true)
increment_build_number(build_number: build_number)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}"
)
update_info_plist(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
block: proc do |plist|
plist["CFBundleDisplayName"] = "#{ENV["PROD_APP_NAME"]}"
plist["CFBundleName"] = "#{ENV["PROD_APP_NAME"]}"
plist["GIT_REVISION"] = git_reversion
plist["BUILD_TIME"] = build_time
plist["APP_CHANNEL"] = "pgyer"
urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}
urlScheme[:CFBundleURLSchemes] = ["#{ENV["PROD_WEIXIN_APPID"]}"]
end
)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["PROD_NOTIFICATION_SERVICE"]}"
)
match(
type: "adhoc",
app_identifier: ["#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"],
readonly: true
)
update_project_provisioning(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
target_filter: "#{ENV["SCHEME_NAME"]}",
profile:ENV["sigh_#{ENV["PROD_APP_IDENTIFIER"]}_adhoc_profile-path"],
build_configuration: "Release"
)
update_project_provisioning(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
target_filter: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}",
profile:ENV["sigh_#{ENV["PROD_NOTIFICATION_SERVICE"]}_adhoc_profile-path"],
build_configuration: "Release"
)
gym(
export_method: "ad-hoc",
scheme: "#{ENV["SCHEME_NAME"]}",
configuration: "Release",
export_options: {
compileBitcode: false,
uploadBitcode: false,
provisioningProfiles: {
"#{ENV["PROD_APP_IDENTIFIER"]}" => "match AdHoc #{ENV["PROD_APP_IDENTIFIER"]}",
"#{ENV["PROD_NOTIFICATION_SERVICE"]}" => "match AdHoc #{ENV["PROD_NOTIFICATION_SERVICE"]}"
}
},
output_directory: output_dir,
output_name: output_name
)
upload_ipa(type: 'gxm', log: "App Store 包上传:#{version_number}(#{build_number})")
bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",
app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",
symbol_type: 2,
bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",
product_version: "#{version_number}(#{build_number})",
channel: 'pgyer'
)
copy_dsym(tpye: 'release')
end
desc "生成 appstore 版本,发布到 App Store"
lane :appstore_release do
git_reversion = sh("git log -1 --pretty=format:'%h'")
build_time = Time.new.strftime("%Y-%m-%d_%H.%M.%S")
version_number = get_info_plist_value(path: "#{ENV["SCHEME_NAME"]}/Info.plist", key: "CFBundleShortVersionString")
build_number = number_of_commits(all: false)
output_dir = "#{base_path}/Output/appstore/#{build_time}"
output_name = "#{ENV["SCHEME_NAME"]}_v#{version_number}(#{build_number}).ipa"
clear_derived_data
increment_build_number(build_number: build_number)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}"
)
update_info_plist(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["SCHEME_NAME"]}/Info.plist",
block: proc do |plist|
plist["CFBundleDisplayName"] = "#{ENV["PROD_APP_NAME"]}"
plist["CFBundleName"] = "#{ENV["PROD_APP_NAME"]}"
plist["GIT_REVISION"] = git_reversion
plist["BUILD_TIME"] = build_time
plist["APP_CHANNEL"] = "appstore"
urlScheme = plist["CFBundleURLTypes"].find{|scheme| scheme["CFBundleURLName"] == "weixin"}
urlScheme[:CFBundleURLSchemes] = ["#{ENV["PROD_WEIXIN_APPID"]}"]
end
)
update_app_identifier(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
plist_path: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}/Info.plist",
app_identifier: "#{ENV["PROD_NOTIFICATION_SERVICE"]}"
)
match(
type: "appstore",
app_identifier: ["#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"],
readonly: true
)
update_project_provisioning(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
target_filter: "#{ENV["SCHEME_NAME"]}",
profile:ENV["sigh_#{ENV["PROD_APP_IDENTIFIER"]}_appstore_profile-path"],
build_configuration: "AppStore"
)
update_project_provisioning(
xcodeproj: "#{ENV["XCODEPROJ_NAME"]}",
target_filter: "#{ENV["NOTIFICATIONSERVICE_SCHEME_NAME"]}",
profile:ENV["sigh_#{ENV["PROD_NOTIFICATION_SERVICE"]}_appstore_profile-path"],
build_configuration: "AppStore"
)
gym(
export_method: "app-store",
scheme: "#{ENV["SCHEME_NAME"]}",
configuration: "AppStore",
export_options: {
provisioningProfiles: {
"#{ENV["PROD_APP_IDENTIFIER"]}" => "match AppStore #{ENV["PROD_APP_IDENTIFIER"]}",
"#{ENV["PROD_NOTIFICATION_SERVICE"]}" => "match AppStore #{ENV["PROD_NOTIFICATION_SERVICE"]}"
}
},
output_directory: output_dir,
output_name: output_name
)
bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",
app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",
symbol_type: 2,
bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",
product_version: "#{version_number}(#{build_number})",
channel: 'appstore'
)
upload_ipa(type: 'gxm', log: "App Store 包上传:#{version_number}(#{build_number})")
copy_dsym(type: 'appstore')
deliver(
metadata_path: "#{ENV["DELIVER_METADATA_PATH"]}",
force: true
)
end
desc "上传 AppStore DSYM 文件到 Bugly,参数 => version:[latest]"
lane :upload_appstore_dsyms do |options|
version = String(options[:version] || "latest")
download_dsyms(version: version)
dsym_paths = lane_context[SharedValues::DSYM_PATHS]
for dsym_path in dsym_paths
split_strs = dsym_path.split(/\//).last.split(/-/)
version_number = split_strs[1]
build_number = split_strs[2].split(/\./)[0]
bugly(app_id: "#{ENV["PROD_BUGLY_APPID"]}",
app_key:"#{ENV["PROD_BUGLY_APPKEY"]}",
symbol_type: 2,
bundle_id: "#{ENV["PROD_APP_IDENTIFIER"]}",
product_version: "#{version_number}(#{build_number})",
channel: 'appstore',
dsym: dsym_path
)
end
clean_build_artifacts
end
desc "手动批量添加设备到profile"
lane :add_devices_manual do
UI.header "Add Device"
device_hash = {}
device_sum = UI.input("Device Sum: ").to_i
if device_sum == 0
next
end
index = 0
while index < device_sum do
device_name = UI.input("Device Name: ")
device_udid = UI.input("Device UDID: ")
device_hash[device_name] = device_udid
index += 1
end
register_devices(
devices: device_hash
)
refresh_profiles
end
desc "文件批量添加设备到profile"
lane :add_devices_file do
register_devices(
devices_file: "fastlane/devices.txt"
)
refresh_profiles
end
desc "批量导出设备"
lane :export_devices do
password = UI.password("输入 #{ENV["APPLE_ID"]} 账号密码: ")
Spaceship::Portal.login("#{ENV["APPLE_ID"]}", password)
Spaceship::Portal.select_team(team_id: "#{ENV["TEAM_ID"]}")
devices = Spaceship.device.all
File.open("#{base_path}/fastlane/devices.txt", "wb") do |f|
f.puts "Device ID\tDevice Name"
devices.each do |device|
f.puts "#{device.udid}\t#{device.name}"
end
end
end
desc "更新 provisioning profiles"
lane :refresh_profiles do
match(
type: "development",
force: true,
force_for_new_devices: true
)
match(
type: "adhoc",
force: true,
force_for_new_devices: true
)
match(
type: "appstore",
force: true,
force_for_new_devices: true
)
end
desc "同步 certificates 和 provisioning profiles"
lane :sync_cert_profiles do
match(
type: "development",
readonly: true
)
match(
type: "adhoc",
readonly: true
)
match(
type: "appstore",
readonly: true
)
end
desc "移除本地描述文件"
lane :remove_local_profiles do
app_identifiers = ["#{ENV["DEV_APP_IDENTIFIER"]}", "#{ENV["DEV_NOTIFICATION_SERVICE"]}", "#{ENV["PROD_APP_IDENTIFIER"]}", "#{ENV["PROD_NOTIFICATION_SERVICE"]}"]
types = ["development", "adhoc", "appstore"]
app_identifiers.each do |app_identifier|
types.each do |type|
remove_provisioning_profile(app_identifier: app_identifier, type: type)
end
end
end
desc "revoke 证书和描述文件"
private_lane :revoke_cert_profiles do
ENV["MATCH_SKIP_CONFIRMATION"] = "1"
sh("fastlane match nuke development")
sh("fastlane match nuke distribution")
end
desc "生成APNs证书"
lane :generate_apns_cert do
pem(
development: true,
force: true,
app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}",
p12_password: "GXM", output_path: "fastlane/pem"
)
pem(
development: false,
force: true,
app_identifier: "#{ENV["DEV_APP_IDENTIFIER"]}",
p12_password: "GXM", output_path: "fastlane/pem"
)
pem(
development: true,
force: true,
app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}",
p12_password: "GXM", output_path: "fastlane/pem"
)
pem(
development: false,
force: true,
app_identifier: "#{ENV["PROD_APP_IDENTIFIER"]}",
p12_password: "GXM", output_path: "fastlane/pem"
)
end
desc "同步 metadata"
lane :sync_metadata do
ENV["DELIVER_FORCE_OVERWRITE"] = "1"
sh("fastlane deliver download_metadata --metadata_path #{ENV["DOWNLOAD_METADATA_PATH"]}")
end
desc "拷贝 dSYM"
private_lane :copy_dsym do |options|
type = String(options[:type] || "adhoc")
dsym_path = lane_context[SharedValues::DSYM_OUTPUT_PATH]
share_dir = File.join(ENV['HOME'],'/Public/iOS', "#{ENV["SCHEME_NAME"]}", "#{type}")
FileUtils.mkdir_p(share_dir)
FileUtils.cp_r(File.join(dsym_path), share_dir)
end
desc "上传 ipa,type: [pgyer,gxm], log: desc"
private_lane :upload_ipa do |options|
type = options[:type] || 'pgyer'
log = options[:log] || ''
log = String
if type == "pgyer"
pgyer(
api_key: '0098b94391ff417d86837343597789a9',
user_key: '4ca1278171177f624ba3f3cc39eb2d73',
update_description: log
)
else
sh("curl -X 'POST' 'https://fabu.guoxiaomei.com/api/apps/5dca5121f3920d001f71e42d/upload' -H 'Content-Type: multipart/form-data' -H 'accept: application/json' -H 'apikey: 07a0840834294e7b89c41ab9c302c852' -F 'file=@#{lane_context[SharedValues::IPA_OUTPUT_PATH]}'")
end
end
after_all do |lane|
end
error do |lane, exception|
end
end
以上 fastlane 满足基本的功能需求。
Fastlane 实践(一):自动化打包和发布
http://chenzhao.date/2020/10/24/Fastlane 实践(一):自动化打包和发布.html
install_url
to use ShareThis. Please set it in _config.yml
.