在開(kāi)發(fā)中經(jīng)常需要打測(cè)試包,然后上傳至蒲公英等三方平臺(tái),這其中需要經(jīng)歷的操作為:
- 拉取代碼
- 設(shè)置項(xiàng)目的打包環(huán)境
- 利用xcode進(jìn)行打包
- 上傳至蒲公英等三方平臺(tái)
每一次打包上面的過(guò)程必不可少,而且都是手工的,本篇文章我們采用CD(Continuous Delivery)持續(xù)交付和CI(Continuous Integration)持續(xù)集成來(lái)進(jìn)行自動(dòng)化打包一鍵操作,解放雙手,拒絕手動(dòng)的重復(fù)低效率勞動(dòng)。
CD(Continuous Delivery)
工廠里的裝配線以快速、自動(dòng)化、可重復(fù)的方式從原材料生產(chǎn)出消費(fèi)品。同樣,軟件交付管道以快速、自動(dòng)化和可重復(fù)的方式從源代碼生成發(fā)布版本,如何完成這項(xiàng)工作的總體設(shè)計(jì)稱為“持續(xù)交付”(CD),啟動(dòng)裝配線的過(guò)程稱為“持續(xù)集成”(CI)。持續(xù)交付是將應(yīng)用程序推送到交付環(huán)境的自動(dòng)化,大多數(shù)開(kāi)發(fā)團(tuán)隊(duì)通常具有一個(gè)或多個(gè)開(kāi)發(fā)和測(cè)試環(huán)境,在該環(huán)境中會(huì)進(jìn)行應(yīng)用程序更改以進(jìn)行測(cè)試和審查,本文介紹的Jenkins就可以完成上面打包過(guò)程必經(jīng)步驟的后三步。
CI(Continuous Integration)
CD往往和CI是一起配合著使用的,CI的概念很廣(可以自己百度)在本文主要用來(lái)完成上面打包過(guò)程必經(jīng)步驟的第一步,并執(zhí)行CD的動(dòng)作, 這樣就完成了上面所說(shuō)的四個(gè)步驟,同時(shí)在指定時(shí)間內(nèi)執(zhí)行CI動(dòng)作,本文使用jenkins來(lái)完成CI動(dòng)作。
使用CI與CD后開(kāi)發(fā)的具體流程將會(huì)是如下圖所示的:

fastlane和jenkins的安裝
fastlane的安裝
首先附上官網(wǎng)地址https://docs.fastlane.tools/里面會(huì)有詳細(xì)的使用教程。
-
安裝Xcode Command Line Tools
由于fastlane需要使用Xcode Command Line Tools所以使用此命令進(jìn)行安裝xcode-select --install,安裝完成后需要在Xcode->Preferences->Locations中選擇剛剛安裝的Command Line Tools。
安裝bundle
由于fastlane需要使用bundle和gem來(lái)管理fastlane所使用的一些依賴,使用gem install bundler命名來(lái)進(jìn)行安裝。-
安裝xcbeautify
由于打包需要使用使用xcodebuild來(lái)進(jìn)行打包,使用brew install xcbeautify進(jìn)行安裝,不然會(huì)報(bào)如下錯(cuò)誤:
安裝fastlane
使用brew install fastlane命令進(jìn)行安裝,當(dāng)然先要安裝Homebrew。
jenkins的安裝
直接使用brew install jenkins命令進(jìn)行安裝,安裝完成后使用brew services start jenkins來(lái)啟動(dòng)jenkins(同樣brew services stop jenkins 是關(guān)閉jenkins)在瀏覽器中輸入http://localhost:8080/進(jìn)入到jenkins的GUI管理頁(yè)面。接下來(lái)打開(kāi)Jenkins后會(huì)讓去一個(gè)填寫(xiě)password的頁(yè)面如下圖,存儲(chǔ)password的地方就是圖片上那行紅色字體目錄下,使用終端cat +紅色字體路徑就看到了。

然后安裝推薦的插件:

進(jìn)入后首先第一件事就是安裝
Xcode integration插件,選擇系統(tǒng)管理’ -- ‘插件管理’‘,然后搜索進(jìn)行安裝。
至此上面的二個(gè)算是初步安裝配置完成。
項(xiàng)目的創(chuàng)建
為了更好的演示整個(gè)流程,我們采用新建一個(gè)項(xiàng)目進(jìn)行講解,同時(shí)利用.xcconfig文件來(lái)進(jìn)行環(huán)境的管理,具體build configuration文件的使用可見(jiàn)我另外一篇文章iOS開(kāi)發(fā)中xconfig和script腳本的使用,項(xiàng)目新建了一個(gè)stage環(huán)境:

同時(shí)利用
Custom Flags來(lái)切換環(huán)境:

stage.xcconfig中定了APP_NAME = AutoBuildStage,debug.xcconfig中定了APP_NAME = AutoBuildDebug,release.xcconfig中定了APP_NAME = AutoBuildRelease,同時(shí)并在info.plist增加Bundle display name這個(gè)key并設(shè)置為$(APP_NAME):

ViewController中的代碼如下:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var envLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
#if Debug
envLabel.text = "環(huán)境是Debug"
#elseif STG
envLabel.text = "環(huán)境是Stage"
#elseif Release
envLabel.text = "環(huán)境是Release"
#endif
}
}
這樣配置后在Edit Scheme中選擇不同的build configuration則會(huì)envLabel顯示不用的環(huán)境信息,同時(shí)安裝的APP名字也會(huì)是.xcconfig文件中所設(shè)置的值。

利用fastlane初始化項(xiàng)目
cd到項(xiàng)目所在的目錄利用fastlane init swift進(jìn)行初始化,會(huì)讓選擇構(gòu)建的方式,這里輸入4采用自定義的方式進(jìn)行構(gòu)建。
fastlane現(xiàn)在是支持使用swift的,這里我們采用熟悉的swift來(lái)編寫(xiě)腳本,當(dāng)然swift寫(xiě)的腳本也是可以裝換成Ruby格式的

構(gòu)建完成會(huì)自動(dòng)生成如下文件:

項(xiàng)目設(shè)置手動(dòng)簽名
在寫(xiě)打包代碼前,首先設(shè)置項(xiàng)目的簽名采用手動(dòng)簽名,項(xiàng)目較大團(tuán)隊(duì)較多時(shí)一定要采用手動(dòng)簽名的方式,不要使用Xcode的自動(dòng)簽名方式,因?yàn)橐坏┠硞€(gè)人的簽名出問(wèn)題就會(huì)導(dǎo)致其他人拉完代碼后也出問(wèn)題,所以本項(xiàng)目采用手動(dòng)簽名的方式,在此先導(dǎo)出自己的.p12證書(shū)和描述文件以備后面使用。

編寫(xiě)自動(dòng)打包的代碼
打開(kāi)fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj所在的工程文件,并打開(kāi)Fastfile.swift文件里面代碼如下所示:
import Foundation
class Fastfile: LaneFile {
func customLane() {
desc("Description of what the lane does")
// add actions here: https://docs.fastlane.tools/actions
}
}
在
Fastfile類中函數(shù)以lane結(jié)尾的稱之為一個(gè)lane,在終端中通過(guò)fastlane <laneName>命令來(lái)執(zhí)行這個(gè)lane,其實(shí)就等于執(zhí)行這個(gè)函數(shù)。
-
首先新建一個(gè)Configuration協(xié)議
因?yàn)轫?xiàng)目配置了Debug,Release和stage三個(gè)Configuration,所以利用協(xié)議能更好的改變Configuration。
protocol Configuration {
/// file name of the certificate
var certificate: String { get }
/// file name of the provisioning profile
var provisioningProfile: String { get }
/// configuration name in xcode project
var buildConfiguration: String { get }
/// the app id for this configuration
var appIdentifier: String { get }
/// export methods, such as "ad-doc" or "appstore"
var exportMethod: String { get }
}
-
新建ProjectSetting枚舉
新建一個(gè)ProjectSetting枚舉來(lái)保存一些項(xiàng)目打包過(guò)程中需要的常用配置信息。
enum ProjectSetting {
static var workspace = " iOSAutoBuild.xcworkspace"
static var project = "iOSAutoBuild.xcodeproj"
static var scheme = "iOSAutoBuild"
static var target = "iOSAutoBuild"
static var productName = "iOSAutoBuild"
static let devices: [String] = ["iPhone 8", "iPad Air"]
static let codeSigningPath = "certs"
// 采用環(huán)境變量的方式更加安全
static let keyChainDefaultPath = environmentVariable(get: "KEYCHAIN_DEFAULT_PATH").replacingOccurrences(of: "\"", with: "")
static let certificatePassword = ""
static let sdk = "iphoneos15.2"
// 蒲公英平臺(tái)的信息
static let pgyerApiKey = "xxxxxxxxxxxx";// 填入自己在平臺(tái)申請(qǐng)的
static let pgyerUserKey = "xxxxxxxxxxxxx";// 填入自己在平臺(tái)申請(qǐng)的
}
對(duì)于
keyChainDefaultPath變量采用了environmentVariable(get:)的方法來(lái)獲取系統(tǒng)的環(huán)境變量,系統(tǒng)的環(huán)境變量在終端中設(shè)置的命令為:export keyChainDefaultPath =”YOUR_ keyChainDefaultPath”,采用系統(tǒng)變量的好處是更加安全,不用在代碼中體現(xiàn)敏感信息。
- 定義一個(gè)Configuration
struct Staging: Configuration {
var certificate = "AppleDis"
var provisioningProfile = "AdHocMLife"
var buildConfiguration = "stage"
var appIdentifier = "com.mamba.iOSAutoBuild"
var exportMethod = "ad-hoc"
}
這樣想切換環(huán)境時(shí)只需要更改
buildConfiguration變量即可。
- 打包時(shí)的簽名操作
class Fastfile: LaneFile {
var stubKeyChainPassword: String = environmentVariable(get: "KEYCHAIN_PASSWORD")
var keyChainName: String {
return "\(ProjectSetting.productName).keychain"
}
var keyChainDefaultFilePath: String {
return "\(ProjectSetting.keyChainDefaultPath)/\(keyChainName)-db"
}
func package(config: Configuration) {
if FileManager.default.fileExists(atPath: keyChainDefaultFilePath) {
deleteKeychain(name: "\(keyChainName)")
}
// 新建一個(gè)以項(xiàng)目名命名的鑰匙串
createKeychain(
name: "\(keyChainName)",
password: stubKeyChainPassword,
defaultKeychain: false,
unlock: true,
timeout: 3600,
lockWhenSleeps: true
)
// 導(dǎo)入證書(shū)到自定義的鑰匙串
importCertificate(
certificatePath: "\(ProjectSetting.codeSigningPath)/\(config.certificate).p12",
certificatePassword: "\(ProjectSetting.certificatePassword)",
keychainName: keyChainName,
keychainPassword: "\(stubKeyChainPassword)"
)
// 更新項(xiàng)目的簽名設(shè)置
updateProjectProvisioning(
xcodeproj: "\(ProjectSetting.project)",
profile: "\(ProjectSetting.codeSigningPath)/\(config.provisioningProfile).mobileprovision",
targetFilter: "^\(ProjectSetting.target)$",
buildConfiguration: "\(config.buildConfiguration)",
certificate: "\(config.certificate).p12"
)
}
}
特別注意:
updateProjectProvisioning是會(huì)更改項(xiàng)目的設(shè)置的,所以最好是打包的機(jī)器是額外的一臺(tái)機(jī)器(熟稱打包機(jī)),當(dāng)然CD是不會(huì)提交代碼到遠(yuǎn)端倉(cāng)庫(kù)的,上面的一些action可以查看fastlane的官網(wǎng),里面有詳細(xì)介紹。
- 打包構(gòu)建buildApp
buildApp(
scheme: "\(ProjectSetting.scheme)",
clean: true,
outputDirectory: "./打包目錄",
outputName: "\(ProjectSetting.productName)_\(dateStr).ipa",
configuration: "\(config.buildConfiguration)",
silent: true,
exportMethod: "\(config.exportMethod)",
exportOptions: optionsArr,
sdk: "\(ProjectSetting.sdk)"
)
dateStr是當(dāng)前時(shí)間變量,代碼中沒(méi)有體現(xiàn)。
- 最后是打包
func developerStrageLane() {
desc("Mamba Create a developer stage")
package(config: Staging())
}
至此我們就可以利用fastlane進(jìn)行打包了,在終端中輸入bundle exec fastlane developerStrage即可,最后會(huì)有打包完成的成功信息大致如下;

聯(lián)動(dòng)jenkins
上面的部分只是完成了打包,現(xiàn)在繼續(xù)利用jenkins來(lái)完成定時(shí)的遠(yuǎn)端代碼拉取,并打包同時(shí)長(zhǎng)傳蒲公英。fastlane現(xiàn)在支持swift,同時(shí)也是支持第三方插件的,首先對(duì)上面的fastlane過(guò)程中加入cocoapods和蒲公英的插件。
加入cocoapods
由于前面說(shuō)了fastlane是利用gem和bundle來(lái)管理三方依賴的,所以打開(kāi)Gemfile文件,增加gem "cocoapods"保存,然后當(dāng)我們?cè)诮K端執(zhí)行bundle install — path vendor/bundler時(shí)會(huì)安裝Gemfile文件中的依賴,當(dāng)然一般我們的打包機(jī)是自己安裝了cocoapods的。
- 執(zhí)行
cocoapods
在上面的package方法上面增加如下代碼:
func beforeAll() {
cocoapods()
}
加入蒲公英插件
-
pgyer插件的安裝
插件安裝前修改gemfile文件,增加如下信息:
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
按照官網(wǎng)的說(shuō)法是需要鏈接gemfile和后面生成的Pluginfile文件,cd到項(xiàng)目所在的目錄執(zhí)行sudo fastlane add_plugin pgyer,會(huì)發(fā)現(xiàn)增加了一個(gè)Pluginfile文件,具體的改變直接放官網(wǎng)說(shuō)的吧:

-
執(zhí)行pgyer的action
直接使用下面代碼上傳至蒲公英
pgyer(apiKey:"\(ProjectSetting.pgyerApiKey)", userKey: "\(ProjectSetting.pgyerUserKey)")
新建jenkins任務(wù)
進(jìn)入jenkins主頁(yè),點(diǎn)擊新建任務(wù),輸入任務(wù)名后選擇構(gòu)建自由風(fēng)格的軟件項(xiàng)目。

在源碼管理處填入遠(yuǎn)端倉(cāng)庫(kù)地址。

在構(gòu)建觸發(fā)器中輸入定時(shí)構(gòu)建的時(shí)間。

具體的時(shí)間語(yǔ)法百度即可,這里是每天每隔半小時(shí)自動(dòng)執(zhí)行任務(wù)。
在構(gòu)建處選擇執(zhí)行shell,并填入需要執(zhí)行的命令。

注意:這里可以填入上文提到的系統(tǒng)變量,例如在
cd語(yǔ)句的后面加上export CERTIFICATE_PASSWORD=”xxx”或者bundle install — path vendor/bundler。
最后點(diǎn)擊保存即可,可在構(gòu)建歷史中查看歷史構(gòu)建記錄。
構(gòu)建任務(wù)的過(guò)程中可能會(huì)報(bào)
bundle :fastlane command not find的錯(cuò)誤,解決辦法是在jenkins的系統(tǒng)管理->系統(tǒng)設(shè)置->全局屬性->環(huán)境變量 增加鍵PATH值:終端輸出值:(echo $PATH)
總結(jié):
本文主要介紹了CD和CI在軟件開(kāi)發(fā)中的運(yùn)用,利用fastlane和jenkins,并配合swift來(lái)實(shí)現(xiàn)iOS項(xiàng)目的自動(dòng)化打包,并進(jìn)行了實(shí)際的Demo演示,實(shí)際操作可能因環(huán)境不同報(bào)錯(cuò),只需要仔細(xì)閱讀終端錯(cuò)誤提示并進(jìn)行相應(yīng)修復(fù)即可,fastlane的錯(cuò)誤提示機(jī)制還是相對(duì)友善的。

