Android 创建 Gradle Task 自动打包并上传至蒲公英

news2024/11/16 0:25:07

前言

  • Android 项目日常开发过程中,经常需要打包给到非开发人员验收或调试,例如测试阶段,就要经常基于测试服务器地址,打包安装包,给到组内测试人员进行测试,并且 BUG 修复完成之后也需要再次打包给到测试人员回测。
  • 为了减免机械性的重复步骤,为项目配置不同的 渠道(Product Flavors),不同渠道对应不同的服务器地址,并且为每一个渠道创建一个 Gradle Task 执行打包并上传至蒲公英的操作,同时在蒲公英中配置 Webhook ,最终可实现:执行对应 **渠道(Product Flavors)**的 Gradle Task,即可自动打包并上传至蒲公英,并将包更新信息同步至企业微信、钉钉、飞书等工作群组,使得包更新流程可视化,并简化了开发和测试联调流程。

实现步骤

1.创建 pgyer-upload.gradle 文件

每个渠道的 Task 执行内容一致:打包并记录更新信息后上传至蒲公英 ,所以抽取公共内容(方法)创建如下 pgyer-upload.gradle 文件

import groovy.json.JsonSlurper

import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.text.SimpleDateFormat

ext.uploadApk = this.&uploadApk

/**
 * Sept 1 创建执行任务
 */

/**
 * 蒲公英 ApiKey
 * https://www.pgyer.com/account/api
 * TODO 替换成自己的蒲公英 APIkey
 */
static String getApiKey() {
    return "myApiKey"
}

/**
 * 上传 apk 到蒲公英
 * @apk 安装包文件
 * @flavorName 渠道名
 */
def uploadApk(File apk, String flavorName) {
    if (apk == null) {
        throw new RuntimeException("apk file not exists!")
    }
    def apkName = apk.name
    println "*************** Upload Init ***************"
    //渠道信息
    String buildFlavorName = flavorName
    if (flavorName == "DemoRelease") {
        buildFlavorName = "Test"
    } else if (flavorName == "DevRelease") {
        buildFlavorName = "Dev"
    } else if (flavorName == "ProduceRelease") {
        buildFlavorName = "Produce"
    } else if (flavorName == "BetaRelease") {
        buildFlavorName = "Beta"
    }
    println "flavorName = ${buildFlavorName}  apkName = ${apkName}  apkSize = ${apk.size()}"
    // Git 提交信息
    String commitLogStr = getGitCommitLogByCount(5)
//    String commitLogStr = getGitCommitLogByToDay()
    def appModule = project.rootProject.project(':app')
    def appVersionName = appModule.android.defaultConfig.versionName
    def appVersionCode = appModule.android.defaultConfig.versionCode
    //更新信息
    String updateDescription = "\n[${getBranchName()}]:${buildFlavorName}-${appVersionName}-${appVersionCode}" +
            "\n${commitLogStr}"
    println "updateDescription : ${updateDescription}"

    println "*************** Upload Get Token ***************"
    //组装cosToken需要的的参数,见https://www.pgyer.com/doc/view/api#fastUploadApp
    List<KeyValue> cosTokenParams = new ArrayList<>()
    //API KEY
    cosTokenParams.add(new KeyValue("_api_key", apiKey))
    //属于android平台
    cosTokenParams.add(new KeyValue("buildType", "android"))
    //更新描述
    cosTokenParams.add(new KeyValue("buildUpdateDescription", updateDescription))
    // 获取上传的 token ,见 https://www.pgyer.com/doc/view/api#fastUploadApp
    HttpResponse<String> response = postFormData("https://www.pgyer.com/apiv2/app/getCOSToken", cosTokenParams)
    def resp = new JsonSlurper().parseText(response.body())
    println ">>>> Get Token Response :\n${response.body()}"

    println "*************** Uploading Apk File ***************"
    // 上传文件到第上一步获取的 URL,参数从上一步获取,这里需要解析参数
    String paramsString = String.valueOf(resp.data.params)
    String[] params = paramsString.substring(1, paramsString.length() - 1).split(',')
    List<KeyValue> list = new ArrayList<>()
    if (params != null) {
        for (i in 0..<params.length) {
            String rawParam = params[i].trim()
            String parsedKey = rawParam.substring(0, rawParam.indexOf("="))
            String parsedValue = rawParam.substring(rawParam.indexOf("=") + 1, rawParam.length())
            //添加参数
            list.add(new KeyValue(parsedKey, parsedValue))
        }
    }
    // 添加apk文件
    list.add(new KeyValue("file", apk.getPath(), true))
    HttpResponse<String> uploadResponse = postFormData(resp.data.endpoint, list)
    if (uploadResponse.statusCode() == 204) {
        println(">>>> Upload Success ")
    } else {
        println(">>>> Upload Fail :" + uploadResponse.body())
    }
    println "*************** Upload Completed ***************"
}

static HttpResponse<String> postFormData(String url, List<KeyValue> list) {
    long requestStartTime = System.nanoTime()
    String boundary = "*********"
    // Result request body
    List<byte[]> byteArrays = new ArrayList<>()
    // Separator with boundary
    byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=").getBytes(StandardCharsets.UTF_8)
    // Iterating over data parts
    for (i in 0..<list.size()) {
        // Opening boundary
        byteArrays.add(separator)
        def entry = list[i]
        // If value is type of Path (file) append content type with file name and file binaries, otherwise simply append key=value
        if (entry.isFile) {
            java.nio.file.Path path = new File(entry.getValue()).toPath()
            String mimeType = Files.probeContentType(path)
            byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName()
                    + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8))
            byteArrays.add(Files.readAllBytes(path))
            byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8))
        } else {
            byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n")
                    .getBytes(StandardCharsets.UTF_8))
        }
    }
    byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8))
    def publisher = HttpRequest.BodyPublishers.ofByteArrays(byteArrays)
    HttpRequest httpRequest = HttpRequest
            .newBuilder(URI.create(url))
            .version(HttpClient.Version.HTTP_1_1)
            .header("Content-Type", "multipart/form-data;boundary=" + boundary)
            .POST(publisher)
            .build()
    return HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString())
}

/**
 * 获取当天提交日志
 * @return
 */
static String getGitCommitLogByToDay() {
    //获取 git 提交日志
    Calendar calendar = Calendar.getInstance()
    String endTime = new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())
    calendar.add(Calendar.DATE, -1)
    String startTime = new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())
    //git 命令
    String gitCommand = "git log --pretty=\"%s\" --since=\"${startTime}\" --before=\"${endTime}\""
    //println "getUpdateDescription() --> gitCommand = ${gitCommand}"
    String description = gitCommand.execute().text.trim()
    return description
}

/**
 * 获取最近 n 条提交日志
 */
static String getGitCommitLogByCount(int count) {
    //git 命令
    String gitCommand = "git log -${count} --pretty=format:\"%s\""
    //println "getUpdateDescription() --> gitCommand = ${gitCommand}"
    String description = gitCommand.execute().text.trim()
    return description
}

/**
 * 获取分支名
 */
static String getBranchName() {
    String gitCommand = "git rev-parse --abbrev-ref HEAD"
    return gitCommand.execute().text.trim()
}

class KeyValue {
    String key
    String value
    boolean isFile

    KeyValue(String key, String value) {
        this(key, value, false)
    }

    KeyValue(String key, String value, boolean isFile) {
        this.key = key
        this.value = value
        this.isFile = isFile
    }

    @Override
    String toString() {
        return "{key:" + key + ", value:" + value + ", isFile:$isFile}"
    }
}

该代码主要执行 APK 上传至蒲公英的操作,并上传指定更新内容:

  • [ Git 分支名]:渠道名-APP版本名-APP版本号
  • 5 条 Git Commit 信息

2.引用自定义 Gradle 文件

在项目的 build.gradle 文件最外层执行

apply from: "pgyer-upload.gradle"

3.配置渠道(非必须)

app modulebuild.gradle 文件 android 层内执行

    /**
     * Sept 3 配置渠道 (非必须)
     */
    flavorDimensions "channel"
    productFlavors {
        // 生产环境
        produce {
            buildConfigField "String", "HOST", "\"${HOST}\""
        }
        // 预生产环境
        beta {
            buildConfigField "String", "HOST", "\"${HOST_BETA}\""
        }
        // 开发环境
        dev {
            buildConfigField "String", "HOST", "\"${HOST_DEV}\""
        }
        // 测试
        demo {
            buildConfigField "String", "HOST", "\"${HOST_TEST}\""
        }
        productFlavors.all {
                // 遍历 productFlavors 多渠道,设置渠道号
            flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
        }
    }

此处代码中的 produce、beta 、dev、demo 均为自定义的渠道名,可根据自身业务需求进行增删修改,其中 HOST 为自定义的变量名,不同渠道引用值不一样(示例中的值来源配置在项目的 gradle.properties 文件中,如下所示),项目编译后,在代码中即可通过 BuildConfig.FLAVORBuildConfig.HOST 获取当前编译环境的 渠道名自定义变量 HOST 的值

# gradle.properties 文件内
# Host
HOST=https://hao123.com
HOST_BETA=https://hao123.com:1008
HOST_TEST=https://hao123.com:1010
HOST_DEV=https://hao123.com:1024

Android Studio 中,可通过左下方 Build Variants 手动切换编译渠道
切换编译渠道

4.创建打包 Task

同样在 app modulebuild.gradle 文件 android 层内执行

    /**
     * Sept 4 创建打包 Task
     * 遍历所有可执行的 variants 创建对应的打包 Task
     * 生成后的路径及名称:Tasks/build/pushApk[productFlavorsName][Release/Debug]
     * eg:Tasks/build/pushApkDevRelease
     */
    android.applicationVariants.all { variant ->
        String taskSuffix = variant.name.capitalize()
        if (taskSuffix.contains("Release") || taskSuffix.contains("Debug")) {
            task("pushApk${taskSuffix}") {
                dependsOn ":app:assemble${taskSuffix}"
                group 'build'
                description 'Custom task for gradle'
                doLast {
                    variant.outputs.all { output ->
                        // 执行脚本任务
                        uploadApk(output.outputFile, taskSuffix)
                    }
                }
            }
        }
    }

5.执行 Gradle Task

按上述步骤操作之后,先执行 Sync Project with Gradle Files 生成不同渠道对应的打包 Task
然后可通过以下两种方式进行执行打包并上传的脚本任务

  • 直接执行 Task : Tasks/build/pushApkDevRelease
    直接执行 Task 步骤 1
    直接执行 Task 步骤 2

  • 通过 Gradle 命令执行 Task : gradle pgyerUploadDevRelease
    通过 Gradle 命令执行 Task

5.1执行效果

Gradle Task 执行完毕
企业微信群更新信息示例

6.补充说明

6.1 关于变种(variants)

在 Android 应用程序构建过程中,变种variants)是指基于不同构建配置或渠道进行构建的应用程序版本。
Android Gradle 插件使用变种来生成不同版本的应用程序,以满足不同的需求,如不同的构建类型、不同的渠道或不同的产品变体等。
每个变种具有自己的构建配置和特定的属性设置,例如包名、应用图标、应用名称等。通过创建不同的变种,可以实现以下目标:

1.构建类型(Build Types):构建类型定义了不同的构建环境和配置,例如调试版(Debug)和发布版(Release)。每个构建类型可以具有自己的代码、资源、签名证书、编译标志等。
2.渠道(Product Flavors):渠道是为了满足不同目标市场或用户群体的需求而定义的版本变体。通过渠道,可以为不同的渠道定制应用程序的内容,如应用程序图标、名称、启动画面、配置文件等。
3.变体(Build Variants):变体是构建类型和渠道的组合,表示一个具体的应用程序版本。每个变体都有其自己的构建输出,如 APK 文件或可安装的应用程序包。

通过定义和配置不同的变种,开发人员可以轻松地构建适用于不同需求的不同版本的应用程序,以便进行测试、发布和分发。

6.2 上传 APK 文件失败

上传文件至蒲公英失败
该问题产生的原因是当前项目 JDK 版本 > 11 ,而当前 Gradle Task 内执行的文件 POST 操作是自行封装的请求参数,该封装在 JDK 版本 > 11 的情况下,无法被服务器正常识别,猜测原因是自定义包装请求参数过程中出现了偏差导致。
最后的解决方案是:

1.降低项目 JDK 版本为 11 或以下即可解决该问题(大部分情况下需要同步修改项目的 Gradle 和 Gradle Plugin 版本,改动较大,不建议
2.改用自定义 Gradle 插件形式创建 Task ,即可在 Task 中引用第三方的网络请求库如 OKHttp ,使用第三方的网络请求库封装 form-data,解决该问题(无需改动项目 JDK、Gradle 及 Gradle Plugin 版本,推荐使用该方案

方案 2 的具体实施,将在下一篇文章中进行演示
PS:如果你们可以在自定义 Gradle 文件中引用到第三方的网络请求库或者是正确编写携带文件的 form-data 网络请求,则可以自行更改部分代码后修复该问题

6.3 Gradle 控制台中文显示异常

studio64.exe.vmoptions 文件中输入

# 解决 gradle 控制台中文乱码问题
-Dfile.encoding=utf-8

打开 studio64.exe.vmoptions 文件
编辑 studio64.exe.vmoptions 文件

6.4 Android Studio 右侧 Gradle 栏内无法看到 Tasks 列表

在 Android studio 的 Setting 中找到最底部 Experimental ,取消如下图中的勾选并应用
显示 Gradle Tasks 列表

6.5 蒲公英 Webhook 配置

参考 https://seed.pgyer.com/WGNQkEpP

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/818200.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

极验4代滑块验证码破解(补环境直接强暴式拿下)

目录 前言一、分析二、验证总结借鉴 前言 极验第四代好像简单了特别多&#xff0c;没有什么技巧&#xff0c;环境党直接5分钟拿下。 网址: aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vYWRhcHRpdmUtY2FwdGNoYS1kZW1v 一、分析 直接去它官网&#xff0c;滑动滑块打开控制台瞅瞅 可以看…

Flask学习笔记_异步论坛(四)

Flask学习笔记_异步论坛&#xff08;四&#xff09; 1.配置和数据库链接1.exts.py里面实例化sqlalchemy数据库2.config.py配置app和数据库信息3.app.py导入exts和config并初始化到app上 2.创建用户模型并映射到数据库1.models/auth.py创建用户模型2.app.py导入模型并用flask-mi…

解决Debian10乱码以及远程连接ssh的问题

文章目录 解决Debian10乱码Debian10配置ssh 解决Debian10乱码 下载locales apt-get install locales配置语言 dpkg-reconfigure locales输入上述命令后会进入到以下页面【空格为选中&#xff0c;回车下一个页面】 在这个页面里我们按空格选中如图的选项&#xff0c;然后回…

安科瑞智慧空开微型断路器在银行的应用-安科瑞黄安南

应用场景 智能微型断路器与智能网关组合应用于末端回路 功能 1.计量功能&#xff1a;实时上报电压、电流、功率、电能、漏电、温度、频率等电参量&#xff1b; 2.报警功能&#xff1a;过压报警、欠压报警、过流报警、过载报警、漏电报警、超温报警、三相电缺相报警&#xff…

Jetson Docker 编译 FFmpeg 支持硬解nvmpi和cuvid

0 设备和docker信息 设备为NVIDIA Jetson Xavier NX&#xff0c;jetpack版本为 5.1.1 [L4T 35.3.1] 使用的docker镜像为nvcr.io/nvidia/l4t-ml:r35.2.1-py3,详见https://catalog.ngc.nvidia.com/orgs/nvidia/containers/l4t-ml 使用下列命令拉取镜像: sudo docker pull nvcr…

windows查看 jar包进程号指令

1 打开cmd 2 : 9898 jar包对应的端口号 netstat -aon|findstr 9898 3 &#xff1a;打开任务管理器 根据搜索出的23700 找到对应进程

【C++】STL——vector的模拟实现、常用构造函数、迭代器、运算符重载、扩容函数、增删查改

文章目录 1.模拟实现vector1.1构造函数1.2迭代器1.3运算符重载1.4扩容函数1.5增删查改 1.模拟实现vector vector使用文章 1.1构造函数 析构函数 在C中&#xff0c;vector是一个动态数组容器&#xff0c;可以根据需要自动调整大小。vector类提供了几个不同的构造函数来创建和初…

gradle项目上传项目依赖到远程仓库

gradle项目上传项目依赖到远程仓库 第一步&#xff1a;在需要上传的项目的bulid.gradle下添加maven插件&#xff0c;并配置连接远程仓库的信息以及项目的三要素信息&#xff0c;如下所示 dependencies {implementation org.mapstruct:mapstruct:1.4.2.Final } apply plugin: …

Linux - make/Makefifile

0.背景 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备完成大型工程的能力 一个工程中的源文件不计数&#xff0c;其按类型、功能、模块分别放在若干个目录中&#xff0c;makefile定义了一系列的规则来指定&#xff0c;哪些文件需要先编译&#xff0c;哪些文件需…

【Spring】Spring之循环依赖底层源码解析

什么是循环依赖 A依赖了B&#xff0c;B依赖了A。 示例&#xff1a; // A依赖了B class A{public B b; }// B依赖了A class B{public A a; }其实&#xff0c;循环依赖并不是问题&#xff0c;因为对象之间相互依赖是很正常的事情。示例&#xff1a; A a new A(); B b new B…

5分钟快手入门laravel邮件通知

第一步&#xff1a; 生成一个邮件发送对象 php artisan make:mail TestMail 第二步&#xff1a; 编辑.env 添加/修改&#xff08;没有的key则添加&#xff09; MAIL_DRIVERsmtp MAIL_HOSTsmtp.163.com &#xff08;这里用163邮箱&#xff09; MAIL_PORT25 &#xff08;163邮箱…

Bug记录: CUDA error_ device-side assert triggered

Bug记录&#xff1a; CUDA error: device-side assert triggered 在接触AIGC算法的过程中偶尔会遇到这样的bug&#xff1a;RuntimeError: CUDA error: device-side assert triggered return torch._C._cuda_synchronize() RuntimeError: CUDA error: device-side assert trig…

Qt实现引导界面UITour

介绍 最近做了一款键鼠自动化&#xff0c;想第一次安装打开后搞一个引导界面&#xff0c;找了好多资料没啥参考&#xff0c;偶然发现qt有引导界面如下图。 Qt整挺好&#xff0c;但是未找到源码&#xff0c;真的不想手撸&#xff0c;无奈实在找不到&#xff0c;下图是仿照qt实现…

在Vue中使用深度选择器定制Element Plus组件样式

介绍&#xff1a; 在Vue.js开发中&#xff0c;我们经常使用Element Plus作为UI组件库&#xff0c;它提供了丰富的组件供我们使用。然而&#xff0c;有时候我们希望对Element Plus的组件样式进行一些定制&#xff0c;比如调整字体大小、改变颜色等。在这篇博客中&#xff0c;我…

【GitOps系列】如何实施金丝雀发布?

文章目录 前言金丝雀发布概述金丝雀实战创建生产环境 部署金丝雀环境配置金丝雀策略金丝雀发布自动化创建 Rollout 对象创建 Service 和 Ingress 对象访问生产环境金丝雀发布自动化 访问 Argo Rollout Dashboard自动化原理结语 前言 蓝绿发布是一种通过资源冗余来换取回滚效率的…

关于jar文件反编译

最近在搞tck测试&#xff0c;想要将其日志转换成apdu脚本&#xff0c;结果出现默认输出最大长度不足&#xff0c;输出被省略现象。 软件log出现的错误信息 ... Output overflow: JavaTest Harness has limited the test output to the text to that at the beginning and the…

stable-diffusion-webui 启动服务,卡在浏览器loading中, 重定向解决

最新的code&#xff0c;按步骤安装&#xff0c;趟完pip和github的坑&#xff0c;终于启动服务 然后悲催的卡在浏览器这一步&#xff0c;一直在loading&#xff0c;折腾一下午&#xff0c;尝试可能有效的步骤&#xff0c;也许最后一步才有用&#xff1a; 1. 启动IIS服务 2. 配…

Java课题笔记~Maven基础

2、Maven 基础 2.1 Maven安装与配置 下载安装 配置&#xff1a;修改安装目录/conf/settings.xml 本地仓库&#xff1a;存放的是下载的jar包 中央仓库&#xff1a;要从哪个网站去下载jar包 - 阿里云的仓库 2.2 创建Maven项目

爬虫006_python中的运算符_算术运算符_赋值运算符_复合赋值运算符_比较运算符_逻辑运算符_逻辑运算符性能提升---python工作笔记024

首先看加减乘除 然后看这里的 // 是取整数部分,不是四舍五入 然后%这个是取余数 然后**是,几次方那种 指数

游游的排列构造

示例1 输入 5 2 输出 3 1 5 2 4 示例2 输入 5 3 输出 2 1 4 3 5 #include<bits/stdc.h> using namespace std; typedef long long ll; const int N1e55; int n,k; int main(){scanf("%d%d",&n,&k);int xn-k1;int yn-k;int f1;for(int i1;i&l…