Jenkins是一款開源CI$CD軟件,用于自動化各種任務(wù),包括構(gòu)建、測試和部署軟件
優(yōu)點(diǎn):
持續(xù)的軟件版本發(fā)布、測試項(xiàng)目
監(jiān)控外部調(diào)用執(zhí)行的工作
對于移動端開發(fā)來說,使用Jenkins持續(xù)化集成,可以幫助開發(fā)人員縮短開發(fā)周期,開發(fā)人員只需要關(guān)注開發(fā)任務(wù),像給產(chǎn)品、測試人員打包時,這些任務(wù)就可以交給Jenkins來做,測試人員可只需要掃描一下二維碼安裝即可。
先來一張效果圖

安裝
這里我們通過homebrew安裝,如果未安裝Homebrew,先安裝Homebrew,詳見Homebrew安裝和使用
Homebrew安裝完成后,執(zhí)行以下命令安裝Jenkins
brew install jenkins
安裝完成后,執(zhí)行war包
java -jar /usr/local/Cellar/jenkins/2.183/libexec/jenkins.war
這里Jenkins版本號可根據(jù)自己的Jenkins版本進(jìn)行更換
另附啟動和關(guān)閉Jenkins命令:
啟動
jenkins -h
關(guān)閉
control + c 快捷鍵關(guān)閉
啟動后,先不要急著打開Jenkins的web容器,先去/Library/LaunchDaemons目錄下新建一個org.jenkins-ci.plist文件,文件內(nèi)容如下(可直接拷貝修改JENKINS_HOME值為你自己的路徑)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StandardOutPath</key>
<string>/var/log/jenkins/jenkins.log</string>
<key>StandardErrorPath</key>
<string>/var/log/jenkins/jenkins.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>JENKINS_HOME</key>
<string>/Users/aladin/Documents/Jenkins/Home</string>
</dict>
<key>GroupName</key>
<string>daemon</string>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>org.jenkins-ci</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Library/Application Support/Jenkins/jenkins-runner.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>jenkins</string>
<key>SessionCreate</key>
<true/>
</dict>
</plist>
啟動Jenkins
為避免權(quán)限問題,先執(zhí)行下面的命令:
sudo chown root /usr/local/Cellar/jenkins/2.122/homebrew.mxcl.jenkins.plist
啟動完成后,打開瀏覽器,輸入http://localhost:8080會出現(xiàn)如下頁面:

稍等幾分鐘,會出現(xiàn)如下頁面

進(jìn)入到Jenkins提示的目錄(我這里是/Users/Shared/Jenkins/Home/secrets/initialAdminPassword)獲取管理員密碼,在該路徑中,非Jenkins用戶secrets目錄和initialAdminPassword文件時沒有讀寫權(quán)限的,將該權(quán)限改成只讀或讀和寫:


獲取到密碼后,記得備份下,后期可能會用到
配置
1、安裝插件
可以按照推薦的插件安裝,也可以自己選擇

這里我們選擇安裝推薦的插件

安裝完成后,會提示我們創(chuàng)建用戶

創(chuàng)建完成后,會提示我們配置Jenkins URL,這個可以根據(jù)自身情況進(jìn)行修改,這里我們先用默認(rèn)設(shè)置http://localhost:8080/
Jenkins插件安裝
因?yàn)楣卷?xiàng)目托管在gitlab上,所以這里需要安裝gitlab插件,同時安裝Git Parameter插件和Build Name and Description Setter(用于參數(shù)化構(gòu)建)


安裝Dynamic Parameter插件
由于Dynamic Parameter插件有漏洞,在Jenkins中搜索不到,這里給出下載地址,經(jīng)測試目前只有0.1.1版本能用,該插件下載完成后需要在插件管理-高級-上傳插件中進(jìn)行安裝
插件安裝完成后,我們就來進(jìn)行基本環(huán)境的配置
2、環(huán)境配置
2.1系統(tǒng)配置:

增加環(huán)境變量
進(jìn)入Manage Jenkins -> Configure System -> 全局屬性,勾選Environment variables,增加一對鍵值

其中PATH的值為本機(jī)的環(huán)境變量,可以在終端執(zhí)行以下命令查看,為一堆路徑:
echo $PATH
Android sdk配置:

SDK配置中的鍵必須是ANDROID_HOME,值為你本機(jī)的Android SDK目錄,這里要注意SDK目錄的權(quán)限問題,沒有權(quán)限的話,可能會導(dǎo)致后期構(gòu)建的時候提示找不到SDK路徑。
JDK、Git、Gradle配置:


2.2Gradle配置
接下來看下build.gradle中的部分配置
apply plugin: 'com.android.application'
def fileArray = []
def getBuildTime() {
return new Date().format("yyyy-MM-dd-HH-mm")
}
//是否Jenkins打包
def isJenkins() {
return "true".equals(IS_JENKINS)
}
//是否是Google渠道
def isGpChannel() {
return "gp".equals(JENKINS_CHANNEL)
}
//獲取Channel
def getJenkinsChannel() {
// def channels = System.getenv("JENKINS_CHANNEL")
def channels = JENKINS_CHANNEL
println("多渠道:" + channels)
// String channels = "yingyongbao"
channels.toString().tokenize(',').each { channelItem ->
android.productFlavors.create(channelItem, {
manifestPlaceholders = [
APP_NAME : APP_NAME,
CHANNEL_VALUE: channelItem,
API_DOMAIN : API_DOMAIN
]
println("當(dāng)前渠道:" + channelItem)
})
// android.sourceSets.main.manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
android {
compileSdkVersion 28
buildToolsVersion "29.0.0"
defaultConfig {
String packageName = "com.ywd.jenkinsbuildtest"
if (isJenkins()) {
packageName = PACKAGE_NAME
}
applicationId packageName
minSdkVersion 19
targetSdkVersion 28
versionCode 100
versionName "1.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//確保所有的flavors都屬于同一維度
flavorDimensions "default"
// sourceSets.main {
// jni.srcDirs = []
// //LOCAL_LDFLAGS += -fuse-ld=bfd
// //jni.srcDirs 'src/main/jni'
// jniLibs.srcDir 'src/main/libs'
// }
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
preview {
minifyEnabled false
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
getJenkinsChannel()
// productFlavors {
// def channel = "gp"
// println("productFlavors_isJenkins":isJenkins())
// if(isJenkins()){
// channel = CHANNEL_VALUE
// }
// println(channel)
// app {
// manifestPlaceholders = [CHANNEL_VALUE: channel]
// }
// }
applicationVariants.all { variant ->
variant.outputs.all { output ->
def appVersion = variant.versionName //版本號
def buildType = "" //構(gòu)建類型
def buildTime = getBuildTime() //構(gòu)建時間
println("IS_JENKINS_${IS_JENKINS}")
if ("true".equals(IS_JENKINS)) {
appVersion = APP_VERSION
buildTime = BUILD_TIME
}
//構(gòu)建類型
if ("debug".equals(variant.buildType.name)) {
buildType = "Debug"
} else if ("preview".equals(variant.buildType.name)) {
buildType = "Preview"
} else {
buildType = "Release"
}
def fileName = "${appVersion}_${variant.productFlavors[0].name}_${buildTime}_${buildType}.apk"
def outFile = output.outputFile
if (outFile != null && outFile.name.endsWith('.apk')) {
outputFileName = fileName
}
fileArray.add(outFile.parentFile.absolutePath + File.separator + fileName)
}
}
//根據(jù)不同場景配置不同的AndroidManifest.xml文件
sourceSets {
println("==============sourceSets==============")
main {
if (isGpChannel()) {
manifest.srcFile 'src/main/gp/AndroidManifest.xml'
println("使用Google配置")
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
println("使用默認(rèn)配置")
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
afterEvaluate {
//只有Jenkins打包才復(fù)制,此處復(fù)制到文件下載路徑
if (isJenkins()) {
assembleDebug.doLast {
forEachFile(fileArray)
}
assemblePreview.doLast {
forEachFile(fileArray)
}
assembleRelease.doLast {
forEachFile(fileArray)
}
}
}
def forEachFile(fileArray) {
fileArray.forEach { file ->
renameAndMoveoutApk(file)
}
}
def renameAndMoveoutApk(orignalFile) {
//此處路徑根據(jù)實(shí)際情況設(shè)置
def intoFile = rootDir.parentFile.parentFile.parentFile.parentFile.getAbsolutePath() + File.separator + "Shared/apache-tomcat-9.0.21/webapps/Jenkins_apk"
copy {
println("開始復(fù)制:目標(biāo)路徑:${intoFile}")
from orignalFile
into intoFile
// rename("${android.defaultConfig.versionName}_${android.defaultConfig.versionCode}_","")
}
}
gradle.properties
IS_JENKINS = false
BUILD_TIME = 2019-09-04
APP_NAME = JenkinsBuildTest
APP_VERSION = 1.0.0
PACKAGE_NAME = com.ywd.jenkinsbuildtest1
# 渠道
JENKINS_CHANNEL = app
# 接口域名
API_DOMAIN = ""
2.3Jenkins項(xiàng)目配置
新建項(xiàng)目

填寫項(xiàng)目名稱,這里我們選擇構(gòu)建一個自由風(fēng)格的軟件項(xiàng)目

創(chuàng)建好后,進(jìn)入項(xiàng)目配置

選擇參數(shù)化構(gòu)建,添加參數(shù)GitParameter

這里變量名隨意,參數(shù)類型選擇Branch or Tag

添加參數(shù),選擇Choice Parameter,這里參數(shù)名為IS_JENKINS,注意這里參數(shù)名要和gradle.properties中定義的相同

創(chuàng)建參數(shù)BUILD_TYPE,這里名字可以隨意,參數(shù)根據(jù)自己項(xiàng)目中定義

添加Dynamic Parameter

創(chuàng)建參數(shù)BUILD_TIME,注意這里參數(shù)名要和gradle.properties中定義的相同
并且Dynamic Parameter使用的是Groovy Script

創(chuàng)建
源碼管理
這里我們使用的是Git

輸入倉庫地址后,點(diǎn)擊添加,添加認(rèn)證

創(chuàng)建完成后,選擇剛剛創(chuàng)建的用戶憑據(jù),并填寫上面參數(shù)化構(gòu)建填好的分支變量名,注意變量名前要加$

選擇構(gòu)建插件
Android使用的是Gradle構(gòu)建,這里我們選擇之前配置好的Gradle版本,并輸入以下命令
clean assemble${BUILD_TYPE} --stacktrace
然后勾選Pass all job parameters as Project properties,舊版本是勾選Pass job parameters as Gradle properties

配置完成后,點(diǎn)擊保存,回到項(xiàng)目首頁

可以看到,原先的立即構(gòu)建已經(jīng)變成了Build with Parameters

配置完成后,點(diǎn)擊開始構(gòu)建,這里我們可以查看控制臺輸出

在控制臺我們可以看到和Android Studio打包同樣的輸出結(jié)果,最后顯示構(gòu)建成功。

構(gòu)建完成后,我們可以在項(xiàng)目目錄找到打好的包

構(gòu)建名稱
原本的構(gòu)建名稱只是一個編號,對于使用人員來講,沒有辨識度,我們可以在項(xiàng)目的構(gòu)建環(huán)境中進(jìn)行配置,更改名稱,具體操作如下,在構(gòu)建環(huán)境中勾選Set Build Name,并填入上文配置的參數(shù)名稱

保存后,我們再次構(gòu)建查看下結(jié)果,構(gòu)建名稱已經(jīng)改變了

經(jīng)過如上配置,我們的Jenkins打包就可以正常工作了,但是構(gòu)建完成的包,測試人員該怎么安裝呢,不能每次打完包還要我們?nèi)ロ?xiàng)目目錄下找到發(fā)給他們吧,這是不可能的,讓測試區(qū)工作區(qū)自己找?可不太可能。。。接下來我們卡一下如何將打完的包生成二維碼并展示
3、生成二維碼并展示
3.1 Tomcat安裝及配置
安裝并配置Tomcat,詳見Mac安裝Tomcat
修改配置文件conf/web.xml
<init-param>
<param-name>listings</param-name>
<param-value>true</param-value>
</init-param>
把原來的false改為true,此時在webapps下新建個目錄,如download,就可以通過瀏覽器訪問里面的內(nèi)容

3.2 Python安裝及配置
安裝Python和pip詳見Mac安裝Python和pip
安裝Pillow
輸入命令sudo pip install Pillow,出現(xiàn)如下提示說明已經(jīng)安裝完成

3.3 qrcode安裝及配置
輸入以下命令
pip3 install myqr

以上配置完成后,打開Jenkins,進(jìn)入Manage Jenkins -> 全局屬性,然后新增屬性,添加Python全局變量

3.4 生成二維碼
進(jìn)入項(xiàng)目 -> 配置 -> 構(gòu)建,增加構(gòu)建步驟

填寫如下命令
myqr http://172.20.41.235:8888/Jenkins_apk/${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.apk -n ${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.png -v 1 -l L -d /Users/Shared/apache-tomcat-9.0.21/webapps/Jenkins_apk
其中路徑和${}里面的參數(shù)根據(jù)自己的實(shí)際情況進(jìn)行配置
關(guān)于qrcode的詳細(xì)使用,詳見Github
3.5 展示二維碼
通過myqr命令會在Tomcat下載目錄生成一張二維碼圖片,接下來我們要把二維碼圖片顯示在Jenkins上:
安裝插件description setter plugin,安裝好后,進(jìn)入項(xiàng)目->配置->構(gòu)建后操作,增加構(gòu)建后操作步驟,選擇Set build description

我這里已經(jīng)設(shè)置過了build description,所以是灰色的
選擇完成后,描述可以添加HTML標(biāo)簽,所以我們可以將<img src='' />標(biāo)簽加入到描述中,不過這里有個問題,加了img標(biāo)簽后,Jenkins并不會顯示二維碼圖片,這是因?yàn)镴enkins出于安全考慮,所有描述信息的Markup Formatter默認(rèn)是采用Plain text模式,這種模式不會對描述信息中的HTML編碼進(jìn)行解析。
我們可以在Manage Jenkins -> Configure Global Security,將Markup Formatter的設(shè)置改為Safe HTML即可。
Description中的描述
<img src='http://172.20.41.235:8888/Jenkins_apk/${APP_VERSION}_${JENKINS_CHANNEL}_${BUILD_TIME}_${BUILD_TYPE}.png' height="200" width="200" /><br><a >下載連接</a>
其中地址和參數(shù)可根據(jù)自身實(shí)際情況進(jìn)行配置。

4、常見問題
1、
Caused by: java.lang.RuntimeException: The SDK directory '/Users/aladin/Library/Android/sdk' does not exist.
這個路徑是SDK默認(rèn)的路徑,剛開始是以為jenkins沒有把local.properties文件拉下來,結(jié)果這個文件拉下來之后,還是報(bào)這個錯,后來估計(jì)是權(quán)限問題,然后將everyone只讀權(quán)限放在Library目錄下就沒問題了
參考文章:
Android-解放雙手告別測試-使用Jenkins自動化打包
Android使用Jenkins持續(xù)集成
Jenkins本地搭建遇到的問題 for Mac