900字范文,内容丰富有趣,生活中的好帮手!
900字范文 > Gradle自动化之自动打包并上传到fir测试网站

Gradle自动化之自动打包并上传到fir测试网站

时间:2021-12-11 16:56:47

相关推荐

Gradle自动化之自动打包并上传到fir测试网站

前言

每个项目都需要测试,没有测试的项目是无法发布到线上的

而由于安卓的碎片化,公司里测试需要测几种不同版本的系统和不同厂商(型号)的手机,所以我平时发的测试包必须放到某个服务器或网站上,通过二维码的方式给测试,这样才能让测试流程更方便

之前的流程都是,先打包,然后将包上传到fir测试网站上,然后将二维码发给测试们,感觉很麻烦,还是写一个自动化的插件比较好,而Android开发的管理工具Gradle正好也是支持自动化的,所以就用Gradle来做一个自动打包上传测试的功能

正文

首先我们创建一个Task任务,这个Task任务就是自动打包上传测试的任务,可以在app或者跟目录的build.gradle(.kts)文件中加入task

Groovy语言:

task firDebug(type: Task) {}

Kotlin(kts)语言:现在Gradle也支持用Kotlin脚本(kts)来写了,我不太会Groovy语法,所以就直接用的kts写的2333

tasks.register<Task>("firDebug") {}

然后在根项目下创建buildSrc目录,来用Kotlin编写脚本代码,参考:https://mp./s/mVqShijGTExtQ_nLslchpQ 和https://mp./s/xs164Y1Oi4rEZfhKCoUnGA(感谢Benny Huo老师的文章)

目录长这个样子

在build.gradle.kts中加入如下代码:

plugins {`kotlin-dsl`//可以使用kotlin写Gradle}repositories {maven("/nexus/repository/maven-public/")}dependencies {implementation("com.google.code.gson:gson:2.8.6")//这几个是一会需要用到的库implementation("net.dongliu:apk-parser:2.6.9")implementation("dom4j:dom4j:1.6.1")implementation("com.squareup.okio:okio:2.10.0")implementation("javax.activation:activation:1.1.1")//ps:jdk11后需要手动引用}

然后直接在kotlin目录下写相应的代码,先写一个入口代码,文件:Fir.kt

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/fun Task.uploadToFir(module: String, channel: String, type: String) {}

然后修改task任务:

Groovy

task firDebug(type: Task) {FirKt.uploadToFir(it,"app",你的渠道,"debug")//用java的方式调用kt的扩展函数}

Kotlin(kts)

tasks.register<Task>("firDebug") {//这里的this就是task,所以下面不需要显式声明receiveruploadToFir("app", 你的渠道, "debug")}

然后填充uploadToFir方法,上面写的有注释:

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/fun Task.uploadToFir(module: String, channel: String, type: String) {// TODO by lt 修改fir的api_tokenval firApiToken = ""val path =":$module:assemble${channel[0].toUpperCase()}${channel.substring(1)}${type[0].toUpperCase()}${type.substring(1)}"//拼一下需要执行的打包的代码,类似这样子:app:assembleGoogleDebug"fir.uploadToFir: start assemble. path=$path".println()//执行打包dependsOn(path)doLast {//在该任务其他代码执行完毕后,在执行该lambda中的代码//获取apk包val inputFile = getApkFile(module, channel, type)//去指定目录下找到刚才打出来的apk包"fir.uploadToFir: get apk file success. file=${inputFile.absolutePath}".println()val apkInfo =parseApkFile(inputFile.absolutePath) ?: throw FileNotFoundException("找不到打出来的apk包")//获取到apk的信息"fir.uploadToFir: get apk info=${apkInfo.toJson()}".println()//下面的两个网络请求是fir网站上提供的apival tokenResult = post("/apps", mapOf("type" to "android","bundle_id" to apkInfo.packageName!!,"api_token" to firApiToken), null, "application/octet-stream")//post请求的方法(从网上找的一个java原生网络请求)val tokenBean = tokenResult.jsonToAny<FirTokenBean>()"fir.uploadToFir: get token success. result=$tokenResult".println()val binary =tokenBean?.cert?.binary ?: throw RuntimeException("网络数据有误:${tokenBean.toJson()}")post(binary.upload_url!!,mapOf("key" to binary.key!!,"token" to binary.token!!,"x:name" to apkInfo.appName!!,"x:version" to apkInfo.versionName!!,"x:build" to apkInfo.versionCode.toString()),mapOf("file" to inputFile.absolutePath),"application/octet-stream").println()}}

ps:最后会放出完整代码

其实很简单,就是执行一下打包命令,然后找到打出来的apk包,并获取apk包的信息,最后通过接口将apk上传到fir网站上,然后就ok了

我这里省略了将二维码或者网址发给测试的功能,如果需要的话可以建一个钉钉群,将测试拉进去,然后使用钉钉的机器人功能直接每次打完包将二维码发到群里就好了

完整代码如下:

/*** 打包并上传到测试平台* [module]表示那个module文件夹,比如app* [channel]表示打哪个渠道,比如google* [type]表示打什么版本的包(正式,debug),比如debug*/fun Task.uploadToFir(module: String, channel: String, type: String) {// TODO by lt 修改fir的api_tokenval firApiToken = ""val path =":$module:assemble${channel[0].toUpperCase()}${channel.substring(1)}${type[0].toUpperCase()}${type.substring(1)}"//拼一下需要执行的打包的代码,类似这样子:app:assembleGoogleDebug"fir.uploadToFir: start assemble. path=$path".println()//执行打包dependsOn(path)doLast {//在该任务其他代码执行完毕后,在执行该lambda中的代码//获取apk包val inputFile = getApkFile(module, channel, type)//去指定目录下找到刚才打出来的apk包"fir.uploadToFir: get apk file success. file=${inputFile.absolutePath}".println()val apkInfo =parseApkFile(inputFile.absolutePath) ?: throw FileNotFoundException("找不到打出来的apk包")//获取到apk的信息"fir.uploadToFir: get apk info=${apkInfo.toJson()}".println()//下面的两个网络请求是fir网站上提供的apival tokenResult = post("/apps", mapOf("type" to "android","bundle_id" to apkInfo.packageName!!,"api_token" to firApiToken), null, "application/octet-stream")//post请求的方法(从网上找的一个java原生网络请求)val tokenBean = tokenResult.jsonToAny<FirTokenBean>()"fir.uploadToFir: get token success. result=$tokenResult".println()val binary =tokenBean?.cert?.binary ?: throw RuntimeException("网络数据有误:${tokenBean.toJson()}")post(binary.upload_url!!,mapOf("key" to binary.key!!,"token" to binary.token!!,"x:name" to apkInfo.appName!!,"x:version" to apkInfo.versionName!!,"x:build" to apkInfo.versionCode.toString()),mapOf("file" to inputFile.absolutePath),"application/octet-stream").println()}}/*** 获取apk文件中的一些数据*/fun parseApkFile(path: String): UpdateInfo? {try {ApkFile(path).use { file ->val updateInfo = UpdateInfo()val meta = file.apkMetaupdateInfo.androidManifest = file.manifestXmlupdateInfo.versionName = meta.versionNameupdateInfo.versionCode = meta.versionCodeupdateInfo.packageName = meta.packageNameupdateInfo.appName = meta.nameupdateInfo.channel = getChannelName(file.manifestXml)updateInfo.path = pathreturn updateInfo}} catch (e: Exception) {return null}}/*** 根据渠道,type,module取到包*/fun getApkFile(module: String, channel: String, type: String): File =File("$module/build/outputs/apk/$channel/$type").listFiles()!!.toList().filter { it.name.endsWith(".apk") }.sortedWith { s1, s2 ->if (checkVersion(s1.name, s2.name)) 1 else -1}.last()data class UpdateInfo(var androidManifest: String? = null,var versionName: String? = null,var versionCode: Long = 0,var channel: String? = null,var packageName: String? = null,var appName: String? = null,var path: String? = null)private fun getManifestMetaData(xml: String): List<Pair<String?, String?>> {val datas: MutableList<Pair<String?, String?>> = ArrayList()val reader = SAXReader()try {val document = reader.read(ByteArrayInputStream(xml.toByteArray(StandardCharsets.UTF_8)))val rootElement = document.rootElementval iterator = rootElement.elementIterator("application")while (iterator.hasNext()) {val next = iterator.next() as Elementval temp = next.elementIterator("meta-data")while (temp.hasNext()) {val meta = temp.next() as Elementval metaName = meta.attributeValue("name")val metaValue = meta.attributeValue("value")datas.add(Pair<String?, String?>(metaName, metaValue))}}} catch (e: DocumentException) {e.printStackTrace()}return datas}private fun getChannelName(xml: String): String? {val metaData = getManifestMetaData(xml)val umeng_channels: List<Pair<String?, String?>> =metaData.stream().filter { (first) -> first == "UMENG_CHANNEL" }.collect(Collectors.toList())return if (!umeng_channels.isEmpty()) {umeng_channels[0].second} else null}/*** 判断两个版本号哪个大* @return true 表示前面大或相等*/private fun checkVersion(version1: String?, version2: String?): Boolean {try {if (version1 == version2)return trueversion1 ?: return trueversion2 ?: return trueval split = version1.split(".")val split2 = version2.split(".")if (split.size > split2.size)return trueelse if (split.size < split2.size)return falsefor (i in split.indices) {if (split[i].toIntOrNull() ?: 0 > split2[i].toIntOrNull() ?: 0)return trueelse if (split[i].toIntOrNull() ?: 0 < split2[i].toIntOrNull() ?: 0)return false}return true} catch (e: Exception) {return true}}fun Any?.println() = println(this)fun Any?.toJson(): String? = Gson().toJson(this)inline fun <reified T> String?.jsonToAny(): T? = Gson().fromJson(this, T::class.java)data class FirTokenBean(val app_user_id: String? = null,val cert: Cert? = null,val download_domain: String? = null,val download_domain_https_ready: Boolean = false,val form_method: String? = null,val id: String? = null,val short: String? = null,val storage: String? = null,val type: String? = null,val user_system_default_download_domain: String? = null) {data class Cert(val binary: Binary? = null,val icon: Icon? = null,val mqc: Mqc? = null,val prefix: String? = null,val support: String? = null)data class Binary(val custom_headers: CustomHeaders? = null,val key: String? = null,val token: String? = null,val upload_url: String? = null)data class Icon(val custom_callback_data: CustomCallbackData? = null,val custom_headers: CustomHeadersX? = null,val key: String? = null,val token: String? = null,val upload_url: String? = null)data class Mqc(val is_mqc_availabled: Boolean = false,val total: Int = 0,val used: Int = 0)class CustomHeaders()data class CustomCallbackData(val original_key: String? = null)class CustomHeadersX()}/*** 上传图片* @param urlStr* @param textMap* @param fileMap* @param contentType 没有传入文件类型默认采用application/octet-stream* contentType非空采用filename匹配默认的图片类型* @return 返回response数据*/fun post(urlStr: String, textMap: Map<String, String>?,fileMap: Map<String, String>?, contentType: String?): String {var contentType = contentTypevar res = ""var conn: HttpURLConnection? = null// boundary就是request头和上传文件内容的分隔符val BOUNDARY = "---------------------------123821742118716"try {val url = URL(urlStr)conn = url.openConnection() as HttpURLConnectionconn.setConnectTimeout(5000)conn.setReadTimeout(30000)conn.setDoOutput(true)conn.setDoInput(true)conn.setUseCaches(false)conn.setRequestMethod("POST")conn.setRequestProperty("Connection", "Keep-Alive")// conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=$BOUNDARY")val out: OutputStream = DataOutputStream(conn.getOutputStream())// textif (textMap != null) {val strBuf = StringBuffer()val iter: Iterator<*> = textMap.entries.iterator()while (iter.hasNext()) {val entry = iter.next() as Map.Entry<*, *>val inputName = entry.key as Stringval inputValue = entry.value as String? ?: continuestrBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n")strBuf.append("Content-Disposition: form-data; name=\"$inputName\"\r\n\r\n")strBuf.append(inputValue)}out.write(strBuf.toString().toByteArray())}// fileif (fileMap != null) {val iter: Iterator<*> = fileMap.entries.iterator()while (iter.hasNext()) {val entry = iter.next() as Map.Entry<*, *>val inputName = entry.key as Stringval inputValue = entry.value as String? ?: continueval file = File(inputValue)val filename: String = file.getName()//没有传入文件类型,同时根据文件获取不到类型,默认采用application/octet-streamcontentType = MimetypesFileTypeMap().getContentType(file)//contentType非空采用filename匹配默认的图片类型if ("" != contentType) {if (filename.endsWith(".png")) {contentType = "image/png"} else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".jpe")) {contentType = "image/jpeg"} else if (filename.endsWith(".gif")) {contentType = "image/gif"} else if (filename.endsWith(".ico")) {contentType = "image/image/x-icon"}}if (contentType == null || "" == contentType) {contentType = "application/octet-stream"}val strBuf = StringBuffer()strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n")strBuf.append("Content-Disposition: form-data; name=\"$inputName\"; filename=\"$filename\"\r\n")strBuf.append("Content-Type:$contentType\r\n\r\n")out.write(strBuf.toString().toByteArray())val `in` = DataInputStream(FileInputStream(file))var bytes = 0val bufferOut = ByteArray(1024)while (`in`.read(bufferOut).also { bytes = it } != -1) {out.write(bufferOut, 0, bytes)}`in`.close()}}val endData = "\r\n--$BOUNDARY--\r\n".toByteArray()out.write(endData)out.flush()out.close()// 读取返回数据val strBuf = StringBuffer()val reader = BufferedReader(InputStreamReader(conn.getInputStream()))var line: String? = nullwhile (reader.readLine().also { line = it } != null) {strBuf.append(line).append("\n")}res = strBuf.toString()reader.close()} catch (e: Exception) {println("发送POST请求出错。$urlStr")e.printStackTrace()} finally {conn?.disconnect()}return res}

end

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。