之前App在提交測試和最終部署的過程中App打包一直是由開發(fā)人員來完成的,由于項目比較大, 再加上Android打包本身就比較慢,所以每次打包還是很耗時的。并且按照嚴格的研發(fā)流程來講,開發(fā)人員應(yīng)該只負責(zé)提交代碼,測試和部署過程中的打包都不應(yīng)該由開發(fā)人員來完成,所以我就想著給測試和運維人員搭建一個可以自動打包的環(huán)境。后來在網(wǎng)上看到很多網(wǎng)友分享使用Jenkins進行Android自動打包的文章,幾經(jīng)嘗試終于把環(huán)境搭建起來了。
Jenkins安裝
Jenkins作為一個開源的持續(xù)集成工具,不僅可以用來進行Android打包,也可以用來進行iOS打包、NodeJs打包、Jave服務(wù)打包等。官方地址為:https://jenkins.io/。Jenkins是使用Java開發(fā)的,官方提供一個war包,并且自帶servlet容器,可以獨立運行也可以放在Tomcat中運行。我們這里使用獨立運行的方式。運行命令為:
java -jar jenkins.war
運行成功,打開瀏覽器訪問http://locahost:8080,首次運行會要求輸入管理員密碼,Jenkins在首次運行時生成的,會在控制臺打印出來或者按照頁面提示的文件路徑查看管理員密碼??刂婆_輸出的密碼:
*************************************************************
*************************************************************
*************************************************************
Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:
b7004e63acb940368e62a5dacaa2b246
This may also be found at: /Users/dmx/.jenkins/secrets/initialAdminPassword
第一次運行的頁面

輸入密碼之后點擊continue選擇要安裝的插件

由于Jenkins的插件之間存在依賴關(guān)系,并且Jenkins不會幫我們自動安裝依賴的插件,所以插件安裝過程比較容易出錯,所以我們建議自己選擇要安裝的插件,不選擇Jenkins建議安裝的插件。點擊Select plugins to install進入下一個頁面

首先把默認選中的插件都取消掉,然后選擇我們要安裝的插件,對于Android打包來講一般需要的插件有
- Git plugin
- Gradle Plugin
- Email Extension Plugin
- description setter plugin
- build-name-setter
- user build vars plugin
- Post-Build Script Plug-in
- Branch API Plugin
- SSH plugin
- Scriptler
- Git Parameter Plug-In
- Gitlab plugin
如果插件安裝過程中由于依賴關(guān)系造成安裝失敗,可以根據(jù)錯誤信息先安裝依賴的插件再重新安裝需要的插件。
插件安裝完成之后按照提示創(chuàng)建一個管理員賬號即可使用,登錄之后進行首頁面。

配置環(huán)境變量
需要配置的環(huán)境變量有Android Home、JDK目錄、Gradle目錄。首先點擊系統(tǒng)管理=>系統(tǒng)設(shè)置?,選中Environment variables,然后新增Android Home環(huán)境變量

然后在系統(tǒng)管理=>Global Tool Configuration中配置JDK目錄和Gradle目錄

JDK和Gradle建議提前下載好放到服務(wù)器上,不要使用自動安裝,Jenkins自動下載安裝非常慢
配置打包腳本
Jenkins配置完成之后需要我們來完善我們的gradle腳本讓它能夠滿足我們的打包要求,既能支持在Jenkins中打包,也能支持我們使用Android Studio進行打包。首先我們需要一個變量IS_JENKINS用來標識當(dāng)前是在Jenkins中打包還是在Android Studio中打包,在不同環(huán)境下打包時證書的路徑和APK生成的路徑不同,我們定義一個函數(shù)來獲取證書路徑,然后在gradle中指定打包時使用的證書
def getMyStoreFile(){
if("true".equals(IS_JENKINS)){
return file("使用Jenkins打包時的證書路徑")
}else{
return file("使用Android Studio打包時證書路徑")
}
}
android{
signingConfigs {
release {
keyAlias '*****'
keyPassword '****'
storeFile getMyStoreFile()
storePassword '****'
}
}
buildTypes{
debug{
....
signingConfig signingConfigs.release
}
release{
....
signingConfig signingConfigs.release
}
}
....
}
然后配置不同打包環(huán)境下apk的生成路徑
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
//新名字
def newName
//輸出文件夾
def outDirectory
//是否為Jenkins打包,輸出路徑不同
if ("true".equals(IS_JENKINS)) {
//BUILD_PATH為服務(wù)器輸出路徑
outDirectory = BUILD_PATH
newName = "你的應(yīng)用名稱" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
} else {
outDirectory = output.outputFile.getParent()
newName = "你的應(yīng)用名稱" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
}
output.outputFile = new File(outDirectory, newName)
}
}
最終完成的gradle腳本為
apply plugin: 'com.android.application'
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
....
}
def getMyStoreFile(){
if("true".equals(IS_JENKINS)){
return file("使用Jenkins打包時的證書路徑")
}else{
return file("使用Android Studio打包時證書路徑")
}
}
android {
signingConfigs {
release {
keyAlias '*****'
keyPassword '****'
storeFile getMyStoreFile()
storePassword '****'
}
}
compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION)
buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION
dexOptions {
jumboMode true
}
defaultConfig {
applicationId project.APPLICATION_ID
minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION)
versionName project.APP_VERSION
versionCode Integer.parseInt(project.VERSION_CODE)
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "mips", "mips64", "x86", "x86_64"
}
// Enabling multidex support.
multiDexEnabled true
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
// 移除無用的resource文件
shrinkResources true
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
//新名字
def newName
//輸出文件夾
def outDirectory
//是否為Jenkins打包,輸出路徑不同
if ("true".equals(IS_JENKINS)) {
//BUILD_PATH為服務(wù)器輸出路徑
outDirectory = BUILD_PATH
newName = "你的app名字" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
} else {
outDirectory = output.outputFile.getParent()
newName = "你的app名字" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
}
output.outputFile = new File(outDirectory, newName)
}
}
flavorDimensions("channel")
productFlavors {
yingyongbao { dimension "channel" }
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [CHANNEL_VALUE: name]
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES.txt'
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
exclude 'META-INF/dependencies.txt'
exclude 'META-INF/LGPL2.1'
}
}
gradle腳本中使用了在gradle.properties中定義的變量,gradle.properties內(nèi)容如下
org.gradle.daemon=true
org.gradle.parallel=true
manifestmerger.enabled=true
android.useDeprecatedNdk=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m -XX\:MaxPermSize\=4096m -XX\:+HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
ANDROID_BUILD_MIN_SDK_VERSION=14
ANDROID_BUILD_TOOLS_VERSION=25.0.1
ANDROID_BUILD_TARGET_SDK_VERSION=22
ANDROID_BUILD_SDK_VERSION=24
VERSION_CODE=176
APPLICATION_ID=你的applicationId
#jenkins中用到的變量
NODEJS_ADDRESS=app要訪問的服務(wù)器地址
API_VERSION=api版本號
APP_VERSION=app版本號
IS_JENKINS=false
BUILD_PATH=apk輸出路徑
BUILD_TYPE=Debug
創(chuàng)建Job
經(jīng)過上面對gradle的配置我們已經(jīng)做好了準備工作,現(xiàn)在需要在Jenkins上新建一個任務(wù)?來完成對上面腳本的調(diào)用。
在Jenkins中點擊新建,輸入Job名字,由于Jenkins會根據(jù)Job名字生成目錄所以建議使用英文不要使用中文,然后選擇構(gòu)建一個自由風(fēng)格的軟件項目,然后點擊OK進入配置頁面

Job配置一共分為六個部分:General、源碼管理、構(gòu)建觸發(fā)器、構(gòu)建、構(gòu)建后操作。
General
General中可以配置Job的基本信息,?名字、描述等信息,我們需要?關(guān)注的是關(guān)于構(gòu)建的配置,?如果服務(wù)器資源比較緊張可以選擇丟棄舊的構(gòu)建,然后選中參數(shù)化構(gòu)建過程,這樣就能夠在打包的時候輸入一些必要的參數(shù),比如App版本號、打包類型、服務(wù)器地址、渠道等信息,這些輸入?yún)?shù)會在構(gòu)建過程中替換掉gradle.properties中定義的變量。Jenkins中支持的參數(shù)類型有Boolean、Choice(下拉選擇形式的)、String、Git(需要安裝插件)。網(wǎng)上其他文章中提到的Dynamic Parameter Plug-in由于安全性問題已經(jīng)不再?支持。下面看一下我們需要添加參數(shù):

BUILD_TYPE表示構(gòu)建版本是Release版還是Debug版,這樣可以區(qū)分App是正式版本還是內(nèi)容測試版本。JS_JENKINS表示這是從Jenkins打包的,默認值為true

PRODUCT_FLAVORS表示App的渠道,我們目前只設(shè)置了應(yīng)用寶這個一個渠道,如果渠道包多的話這樣打包效率比較低,需要一個專門進行多渠道打包的工具。APP_VERSION表示APP的版本號,這里添加這個參數(shù)是為了能夠讓運維人員在App發(fā)布時能夠指定發(fā)布的版本號。

GIT_TAG用于在打包時選擇使用倉庫上哪個分支或者TAG,其中Parameter Type可以選擇Tag、Branch、Branch or Tag或者revision,這里我們選擇Branch or Tag

NODEJS_ADDRESS表示服務(wù)器地址,這里可以配置上測試環(huán)境、生產(chǎn)環(huán)境地址,在打包時選擇要哪個后臺服務(wù)。

REMARK用來描述本次打包的版本,比如這次打包使用來驗證哪個問題等等,要不然單憑版本號很難想起當(dāng)時打包這個版本是用來干什么的。
源碼管理
我們公司使用Gitlab進行代碼管理,這里選擇git,然后輸入倉庫地址,并在Branch Specifier綁定GIT_TAG變量,這樣GIT_TAG會自動讀取倉庫上的分支和TAG列表。

構(gòu)建觸發(fā)器
構(gòu)建觸發(fā)器用來配置什么時候觸發(fā)構(gòu)建,一般做法有手動觸發(fā)、定時觸發(fā)、或者提交代碼時觸發(fā)。提交代碼觸發(fā)需要在gitlab中添加webhook,我們這里使用手動觸發(fā)所以這里不做配置
構(gòu)建環(huán)境
通過選中Set Build Name設(shè)置構(gòu)建名稱,我們這里設(shè)置名稱為
#${BUILD_NUMBER}_${BUILD_USER}_${APP_VERSION}_${BUILD_TYPE}
在Jenkins中${}表示引用變量,其中BUILD_NUMBER為構(gòu)建編號,為Jenkins提供的變量;BUILD_USER為構(gòu)建人,即當(dāng)前登錄用戶,需要選中Set jenkins user build variables;APP_VERSION為App版本號;BUILD_TYPE為構(gòu)建類型。一個實際的構(gòu)建名稱為#14_admin_1.2_Release,表示第14次構(gòu)建,構(gòu)建人為admin,構(gòu)建的App版本為1.2Release版本

構(gòu)建

選中invoke gradle通過調(diào)用gradle腳本進行構(gòu)建,選擇在系統(tǒng)管理中配置的gradle的版本,這里為gradle4.0
然后在Tasks輸入打包命令
clean assemble${PRODUCT_FLAVORS}${BUILD_TYPE}
首先執(zhí)行clean,然后執(zhí)行assemble進行打包。以PRODUCT_FLAVORS選擇yingyongbao,BUILD_TYPE為Release為例,則實際執(zhí)行的命令為
clean assembleYingyongbaoRelease
然后選中Pass job parameters as Gradle properties這樣才能將我們自定義參數(shù)在打包時傳遞到gradle腳本中
這樣我們就能成功打包出apk了
實現(xiàn)二維碼下載
為了能夠更方便的使用,我們還應(yīng)該提供一個二維碼功能,這樣手機掃描之后就能下載安裝。一般做法有兩個:一是選擇將打包出來的apk上傳到第三方平臺;另一個是本地搭建一個服務(wù),實現(xiàn)靜態(tài)文件服務(wù)器的功能。我們這里選擇在本地服務(wù)器搭建一個靜態(tài)文件服務(wù),同時將文件地址生成一個二維碼展示出來。

在Excute Shell中輸入在構(gòu)建完成之后執(zhí)行的腳本,根據(jù)apk路徑生成一個二維碼
node /opt/jenkins_node/qr.js http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk /opt/jenkins_node/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.png
即通過node 執(zhí)行/opt/jenkins_node(需要根據(jù)自己實際的目錄設(shè)置)下的qr.js文件,同時傳遞兩個參數(shù),第一個參數(shù)文件apk文件訪問路徑,我在gradle打包腳本中設(shè)置apk輸出路徑為/opt/jenkins_node/apk目錄,通過靜態(tài)文件服務(wù)的訪問地址http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk(10.1.170.154為我們公司內(nèi)部服務(wù)器,需要根據(jù)自己情況設(shè)置);第二個參數(shù)為生成二維碼的保存路徑,同樣為/opt/jenkins_node/apk目錄,這樣靜態(tài)文件服務(wù)既可以提供apk下載,也可以提供二維碼下載。
然后通過設(shè)置build description顯示二維碼功能,通過定義一個html片段,需要在系統(tǒng)管理=>Configure Global Security中將Markup Formatter選擇為Safe HTML
<br> <a target="_blank" >點擊下載</a><p>${REMARK}</p>
這樣構(gòu)建成功之后會展示一個二維碼,同時提供一個點擊下載的鏈接,并且還會展示該構(gòu)建版本的描述信息
我們使用nodeJs實現(xiàn)一個靜態(tài)文件服務(wù),通過nodejs啟動一個http服務(wù),然后通過解析請求返回對應(yīng)的apk文件。代碼如下
const http = require('http')
const path = require('path')
const url = require('url')
const fs = require('fs')
const mime = require('mime')
const port = '3000'
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.end('Hello World')
return
}
if (req.url === '/favicon.ico') return //不響應(yīng)favicon請求
// 獲取url->patnname 即文件名
let pathname = path.join(__dirname, url.parse(req.url).pathname)
pathname = decodeURIComponent(pathname) // url解碼,防止中文路徑出錯
if (fs.existsSync(pathname)) {
if (!fs.statSync(pathname).isDirectory()) {
// 以binary讀取文件
fs.readFile(pathname, 'binary', (err, data) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end(JSON.stringify(err))
return false
}
res.writeHead(200, {
'Content-Type': `${mime.lookup(pathname)};charset:UTF-8`
})
res.write(data, 'binary')
res.end()
})
} else {
res.statusCode = 404;
res.end('Directory Not Support')
}
} else {
res.statusCode = 404;
res.end('File Not Found')
}
});
server.listen(port);
生成二維碼的小程序也是使用nodejs實現(xiàn),通過使用qr-image模塊實現(xiàn)生成二維碼功能
const qr=require('qr-image')
const args = process.argv.splice(2);
const filePath=args[0]//源文件地址
const distPath=args[1]//目標文件地址
const img=qr.image(filePath,{size:5})//生成二維碼圖片
img.pipe(require('fs').createWriteStream(distPath));//保存圖片
代碼完整地址為:https://github.com/dumingxin/jenkinsNode.git,首先需要安裝nodejs,然后在代碼目錄執(zhí)行npm install,最后執(zhí)行node web.js啟動靜態(tài)文件服務(wù)即可。如果想后臺運行可以使用pm2啟動web.js
最后打包成功之后的效果
