使用Jenkins進行Android自動打包,自定義版本號等信息【轉】


之前App在提交測試和最終部署的過程中App打包一直是由開發人員來完成的,由於項目比較大, 再加上Android打包本身就比較慢,所以每次打包還是很耗時的。並且按照嚴格的研發流程來講,開發人員應該只負責提交代碼,測試和部署過程中的打包都不應該由開發人員來完成,所以我就想着給測試和運維人員搭建一個可以自動打包的環境。后來在網上看到很多網友分享使用Jenkins進行Android自動打包的文章,幾經嘗試終於把環境搭建起來了。

Jenkins安裝

Jenkins作為一個開源的持續集成工具,不僅可以用來進行Android打包,也可以用來進行iOS打包、NodeJs打包、Jave服務打包等。官方地址為:jenkins.io/。Jenkins是使用Java開發的,官方提供一個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

第一次運行的頁面

jenkins_first_load.png

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

jenkins_install_plugin.png

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

jenkins_select_plugin.png

首先把默認選中的插件都取消掉,然后選擇我們要安裝的插件,對於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

如果插件安裝過程中由於依賴關系造成安裝失敗,可以根據錯誤信息先安裝依賴的插件再重新安裝需要的插件。

插件安裝完成之后按照提示創建一個管理員賬號即可使用,登錄之后進行首頁面。

jenkins_main.png

配置環境變量

需要配置的環境變量有Android Home、JDK目錄、Gradle目錄。首先點擊系統管理=>系統設置,選中Environment variables,然后新增Android Home環境變量

jenkins_android_home.png

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

jenkins_gradle.png

JDK和Gradle建議提前下載好放到服務器上,不要使用自動安裝,Jenkins自動下載安裝非常慢

配置打包腳本

Jenkins配置完成之后需要我們來完善我們的gradle腳本讓它能夠滿足我們的打包要求,既能支持在Jenkins中打包,也能支持我們使用Android Studio進行打包。首先我們需要一個變量IS_JENKINS用來標識當前是在Jenkins中打包還是在Android Studio中打包,在不同環境下打包時證書的路徑和APK生成的路徑不同,我們定義一個函數來獲取證書路徑,然后在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
      }
    }
    ....
}

然后配置不同打包環境下apk的生成路徑

   android.applicationVariants.all { variant ->
        variant.outputs.each { output ->
            //新名字
            def newName
            //輸出文件夾
            def outDirectory
            //是否為Jenkins打包,輸出路徑不同
            if ("true".equals(IS_JENKINS)) {
                //BUILD_PATH為服務器輸出路徑
                outDirectory = BUILD_PATH
                newName = "你的應用名稱" + "-" + defaultConfig.versionName + "-" + BUILD_TYPE + ".apk"
            } else {
                outDirectory = output.outputFile.getParent()
                newName = "你的應用名稱" + "-" + 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為服務器輸出路徑
                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內容如下

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要訪問的服務器地址
API_VERSION=api版本號
APP_VERSION=app版本號
IS_JENKINS=false
BUILD_PATH=apk輸出路徑
BUILD_TYPE=Debug

創建Job

經過上面對gradle的配置我們已經做好了准備工作,現在需要在Jenkins上新建一個任務來完成對上面腳本的調用。

在Jenkins中點擊新建,輸入Job名字,由於Jenkins會根據Job名字生成目錄所以建議使用英文不要使用中文,然后選擇構建一個自由風格的軟件項目,然后點擊OK進入配置頁面

jenkins_config_job.png

Job配置一共分為六個部分:General、源碼管理、構建觸發器、構建、構建后操作。

General

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

jenkins_param1.png

BUILD_TYPE表示構建版本是Release版還是Debug版,這樣可以區分App是正式版本還是內容測試版本。JS_JENKINS表示這是從Jenkins打包的,默認值為true

jenkins_param2.png

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

jenkins_param3.png

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

jenkins_param4.png

NODEJS_ADDRESS表示服務器地址,這里可以配置上測試環境、生產環境地址,在打包時選擇要哪個后台服務。

jenkins_param5.png

REMARK用來描述本次打包的版本,比如這次打包使用來驗證哪個問題等等,要不然單憑版本號很難想起當時打包這個版本是用來干什么的。

源碼管理

我們公司使用Gitlab進行代碼管理,這里選擇git,然后輸入倉庫地址,並在Branch Specifier綁定GIT_TAG變量,這樣GIT_TAG會自動讀取倉庫上的分支和TAG列表。

jenkins_scm.png

構建觸發器

構建觸發器用來配置什么時候觸發構建,一般做法有手動觸發、定時觸發、或者提交代碼時觸發。提交代碼觸發需要在gitlab中添加webhook,我們這里使用手動觸發所以這里不做配置

構建環境

通過選中Set Build Name設置構建名稱,我們這里設置名稱為

#${BUILD_NUMBER}_${BUILD_USER}_${APP_VERSION}_${BUILD_TYPE}

在Jenkins中${}表示引用變量,其中BUILD_NUMBER為構建編號,為Jenkins提供的變量;BUILD_USER為構建人,即當前登錄用戶,需要選中Set jenkins user build variables;APP_VERSION為App版本號;BUILD_TYPE為構建類型。一個實際的構建名稱為#14_admin_1.2_Release,表示第14次構建,構建人為admin,構建的App版本為1.2Release版本

jenkin_build_env.png

構建

jenkins_build1.png

選中invoke gradle通過調用gradle腳本進行構建,選擇在系統管理中配置的gradle的版本,這里為gradle4.0

然后在Tasks輸入打包命令

clean assemble${PRODUCT_FLAVORS}${BUILD_TYPE}

首先執行clean,然后執行assemble進行打包。以PRODUCT_FLAVORS選擇yingyongbao,BUILD_TYPE為Release為例,則實際執行的命令為

clean assembleYingyongbaoRelease

然后選中Pass job parameters as Gradle properties這樣才能將我們自定義參數在打包時傳遞到gradle腳本中

這樣我們就能成功打包出apk了

實現二維碼下載

為了能夠更方便的使用,我們還應該提供一個二維碼功能,這樣手機掃描之后就能下載安裝。一般做法有兩個:一是選擇將打包出來的apk上傳到第三方平台;另一個是本地搭建一個服務,實現靜態文件服務器的功能。我們這里選擇在本地服務器搭建一個靜態文件服務,同時將文件地址生成一個二維碼展示出來。

jenkins_build2.png

在Excute Shell中輸入在構建完成之后執行的腳本,根據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 執行/opt/jenkins_node(需要根據自己實際的目錄設置)下的qr.js文件,同時傳遞兩個參數,第一個參數文件apk文件訪問路徑,我在gradle打包腳本中設置apk輸出路徑為/opt/jenkins_node/apk目錄,通過靜態文件服務的訪問地址http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk(10.1.170.154為我們公司內部服務器,需要根據自己情況設置);第二個參數為生成二維碼的保存路徑,同樣為/opt/jenkins_node/apk目錄,這樣靜態文件服務既可以提供apk下載,也可以提供二維碼下載。

然后通過設置build description顯示二維碼功能,通過定義一個html片段,需要在系統管理=>Configure Global Security中將Markup Formatter選擇為Safe HTML

![](http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.png)<br> <a target="_blank" href="http://10.1.170.154:3000/apk/yundiangong-${APP_VERSION}-${BUILD_TYPE}.apk">點擊下載</a><p>${REMARK}</p>

這樣構建成功之后會展示一個二維碼,同時提供一個點擊下載的鏈接,並且還會展示該構建版本的描述信息

我們使用nodeJs實現一個靜態文件服務,通過nodejs啟動一個http服務,然后通過解析請求返回對應的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 //不響應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實現,通過使用qr-image模塊實現生成二維碼功能

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));//保存圖片

代碼完整地址為:github.com/dumingxin/j…,首先需要安裝nodejs,然后在代碼目錄執行npm install,最后執行node web.js啟動靜態文件服務即可。如果想后台運行可以使用pm2啟動web.js

最后打包成功之后的效果

jenkins_final.png


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM