Gradle 插件——整合渠道资源

前言

公司现有渠道 SDK 接入需要使用两个工程,首先在粘合工程编写粘合代码测试完毕后需要手动将粘合代码打成一个 jar 包,然后移动该 jar 包以及相关资源到 SDK 工程,接着再手动打成 zip 包上传 oss,同时同步信息到 oms 后台,过程非常繁琐。于是我就开发了该插件用于解决该问题。

需求分析

目标为将两个工程合并成一个,执行一个 Task 就会自动执行以下操作:

  1. 抽取粘合代码并打成 jar 包,并拷贝到 build/pack 目录下。
  2. 迁移相关资源文件夹 res、libs、assets 及 sdk_script 文件到 build/pack 目录下。
  3. 根据 AndroidManifest.xml 自动生成 SDKManifest.xml。
  4. 根据 Gradle 配置信息生成 config.xml。
  5. 将上述资源打包成 zip 包。
  6. 上传 zip 包到 oss 后台,更新 oms 后台 sdk 版本及下载链接。
  7. 自动更新 versionCode,确保每次打包使用最新资源。

代码实现

首先根据上篇文章,自定义插件类 PackPlugin,先不急于实现逻辑,分析下是否需要配置信息,很明显需要,于是创建类 PackExtension,代码如下:

open class PackExtension {
var sdkName: String? = null
var proxyApp: String? = null
var userClass: String? = null
var payClass: String? = null
var versionName: String? = null
var versionCode: Int = 0
var channelId: Int = 0
var username: String? = null
var password: String? = null
var environment: PackEnv = PackEnv.DEV
}

定义好了配置,那么就来实现功能,首先是第一步。自定义 Task 类 PackJarTask ,代码如下:

open class PackJarTask: Jar() {
init {
project.afterEvaluate {
it.delete(TEMP_DIR, OUTPUT_DIR)
from(SOURCE_DIR)
into("")
exclude("BuildConfig.class", "R.class")
exclude { element ->
element.name.startsWith("R$")
}
destinationDir = project.file(TEMP_DIR)
baseName = it.extension.sdkName
}
}
}

注意父类 Jar 类会实现打包,所有只需要在 init 代码块中配置好输入输出即可,第一步完成后那么来实现第二步。自定义 Task 类 PackMoveTask ,代码如下:

open class PackMoveTask : DefaultTask() {
@TaskAction
fun runTask() {
with(project) {
copy {
it.from("libs")
it.into("$TEMP_DIR/libs")
}
copy {
it.from("src/main/res")
it.into("$TEMP_DIR/res")
}
copy {
it.from("src/main/assets")
it.into("$TEMP_DIR/assets")
}
copy {
it.from("extraFiles")
it.into("$TEMP_DIR/extraFiles")
}
copy {
it.from("sdk_script.py")
it.into("$TEMP_DIR/")
}
}
}
}

这一步就是简单的拷贝资源,第二步完成后那么来实现第三步。自定义 Task 类 PackManifest ,代码如下:

open class PackManifestTask : DefaultTask() {
@TaskAction
fun runTask() {
val proxyApp = project.extension.proxyApp
val manifest = project.file("src/main/AndroidManifest.xml")
val rootNode = XmlParser(false, false).parse(manifest)
val permissions = rootNode.get("uses-permission") as NodeList
val applications = rootNode.get("application") as NodeList
if (applications.isNullOrEmpty() || applications.size > 1) {
throw RuntimeException("application config error, please fix it first.")
}
val application = applications[0] as Node
val xml = xml("manifest", "UTF-8", XmlVersion.V10) {
attribute("xmlns:android", "http://schemas.android.com/apk/res/android")
"permissionConfig" {
permissions.forEach {
if (it is Node) {
"uses-permission" {
attribute("android:name", it.attribute("android:name"))
}
}
}
}
"applicationConfig" {
if (!proxyApp.isNullOrEmpty()) {
attribute("proxyApplication", proxyApp)
}
application.children().forEach { node ->
if (node is Node) {
handleComponent(this, node, node.name().toString())
}
}
}
}
project.file("$TEMP_DIR/SDKManifest.xml").writeText(xml.toString())
}
/**
* 1. 创建当前节点
* 2. 处理当前节点属性
* 3. 处理子节点(递归)
*/
private fun handleComponent(parentNode: KNode, sourceNode: Node, name: String) {
with(parentNode) {
name {
handleAttributes(this, sourceNode)
sourceNode.children().forEach { subNode ->
if (subNode is Node) {
handleComponent(this, subNode, subNode.name().toString())
}
}
}
}
}
/**
* 处理节点属性
*/
private fun handleAttributes(parentNode: KNode, sourceNode: Node) {
sourceNode.attributes().entries.forEach { entry ->
if (entry is MutableMap.MutableEntry<*, *>) {
parentNode.attribute(entry.key.toString(), entry.value.toString())
}
}
}

}

这一步稍稍复杂,首先借助了 XmlParser 解析 AndroidManifest.xml ,然后借助代码库 kotlin-xml-builder 以 DSL 的形式构建 SDKManifest.xml 。第三步完成了那么来实现第四步。自定义 Task 类 PackConfigTask ,代码如下:

open class PackConfigTask : DefaultTask() {
@TaskAction
fun runTask() {
val extension = project.extension
val sdkName = extension.sdkName.orEmpty()
val versionName = extension.versionName.orEmpty()
val versionCode = extension.versionCode
val xml = xml("config", "UTF-8", XmlVersion.V10) {
"operations" {
"operation" {
attribute("step", 1)
attribute("type", "mergeManifest")
attribute("from", "SDKManifest.xml")
attribute("to", "AndroidManifest.xml")
}
"operation" {
attribute("step", 2)
attribute("type", "copyRes")
attribute("from", "assets")
attribute("to", "assets")
}
"operation" {
attribute("step", 3)
attribute("type", "copyRes")
attribute("from", "libs")
attribute("to", "libs")
}
"operation" {
attribute("step", 4)
attribute("type", "copyRes")
attribute("from", "res")
attribute("to", "res")
}
}
"plugins" {
"plugin" {
attribute("name", project.extension.userClass.orEmpty())
attribute("type", "1")
attribute("desc", "用户登录接口")
}
"plugin" {
attribute("name", project.extension.payClass.orEmpty())
attribute("type", "2")
attribute("desc", "用户支付接口")
}
}
"version" {
"name" {
"name" {
text(sdkName)
}
"versionName" {
text(versionName)
}
"versionCode" {
text(versionCode.toString())
}
}
}
}
project.file("$TEMP_DIR/config.xml").writeText(xml.toString())
}
}

这一步就是读取 PackExtension 中定义的配置信息,将其写入 config.xml 中,第四步完成了那么实现第五步,自定义 Task 类 PackZipTask ,代码如下:

open class PackZipTask : Zip() {
init {
project.afterEvaluate {
val sdkName = project.extension.sdkName
requireNotNull(sdkName) {
"sdk name is empty!"
}
from("$TEMP_DIR/")
into(sdkName)
include("**/*")
archiveName = "$sdkName.zip"
destinationDir = project.file(OUTPUT_DIR)
}
}
}

这一步和第一步类似,父类 Zip 类会完成打包操作,只需在 init 代码块中配置好输入输出即可。第五步完成了那么实现第六步上传 oss 后台,并更新 oms 信息。自定义 Task 类 PackUploadTask。

open class PackUploadTask : DefaultTask() {
@TaskAction
fun runTask() {
val sdkFile = getSdkFile()
require(sdkFile.exists()) {
SDK_NOT_EXIST
}
uploadSDKFile(NetworkManager.getSdkInfo(project))
}
private fun getSdkFile(): File {
return project.file("$OUTPUT_DIR/${project.extension.sdkName}.zip")
}
private fun uploadSDKFile(sdkInfo: SdkInfo) {
val sdkFile = getSdkFile()
val sdkUrl = "$OSS_URL${getUploadKey(sdkFile)}"
val ossClient = OSSClientBuilder()
.build(END_POINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY)
val uploadReq = PutObjectRequest(BUCKET_NAME, getUploadKey(sdkFile), sdkFile)
uploadReq.progressListener = object : ProgressListener {
private var total = 0L
private var bytes = 0L
override fun progressChanged(event: ProgressEvent) {
when (event.eventType) {
ProgressEventType.TRANSFER_STARTED_EVENT -> {
println("start upload please wait...")
}
ProgressEventType.REQUEST_CONTENT_LENGTH_EVENT -> {
total = event.bytes
println("upload total size is: $total")
}
ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT -> {
bytes += event.bytes
println("upload progress is: ${bytes.toFloat() / total}")
}
ProgressEventType.TRANSFER_COMPLETED_EVENT -> {
NetworkManager.modifySdkInfo(project, sdkUrl, sdkInfo)
}
ProgressEventType.TRANSFER_FAILED_EVENT -> {
throw RuntimeException("upload to oss failed")
}
else -> { }
}
}

}
ossClient.putObject(uploadReq)
ossClient.shutdown()
println("upload success. The url is $sdkUrl")
}
private fun getUploadKey(sdkFile: File): String {
val environment = project.extension.environment
return "sdk/${environment.name}/${sdkFile.name}"
}
}

这一步首先使用 PackExtension 配置的用户名和密码登录 oms 后台,并获取到 token,然后将上个 Task 生成的 zip 包上传到了 oss 后台,然后调用调用接口修复 oms 后台 sdk 信息(带上 token)。上传完成后就只剩下最后一步了更新版本号,自定义 Task 类 PackTask ,代码如下:

open class PackTask : DefaultTask() {
@TaskAction
fun runTask() {
val versionFile = project.file(VERSION_FILE_NAME)
if (versionFile.exists()) {
Properties().apply {
load(versionFile.inputStream())
val nextVersion = (getProperty(VERSION_CODE).toInt() + 1).toString()
setProperty(VERSION_CODE, nextVersion)
store(versionFile.outputStream(), "auto increment")
println("Next version is $nextVersion.")
}
}
println("The replacement is successful.")
}
}

这一步也就是读取 properties 文件,然后将其 versionCode 自增 1 写入。至此逻辑全部完成了,现在可以编写 PackPlugin 类了,代码如下:

class PackPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create(EXTENSION_NAME, PackExtension::class.java)
val packJarTask = project.tasks.create(TASK_JAR_NAME, PackJarTask::class.java)
val packMoveTask = project.tasks.create(TASK_MOVE_NAME, PackMoveTask::class.java)
val packManiTask = project.tasks.create(TASK_MANIFEST_NAME, PackManifestTask::class.java)
val packConfigTask = project.tasks.create(TASK_CONFIG_NAME, PackConfigTask::class.java)
val packZipTask = project.tasks.create(TASK_ZIP_NAME, PackZipTask::class.java)
val packUploadTask = project.tasks.create(TASK_UPLOAD_NAME, PackUploadTask::class.java)
val packTask = project.tasks.create(TASK_NAME, PackTask::class.java)
val packCompileTask = project.tasks.getByName(TASK_BUILD_NAME)
packJarTask.mustRunAfter(packCompileTask)
packMoveTask.mustRunAfter(packJarTask)
packManiTask.mustRunAfter(packMoveTask)
packConfigTask.mustRunAfter(packManiTask)
packZipTask.mustRunAfter(packConfigTask)
packUploadTask.mustRunAfter(packZipTask)
packTask.group = TASK_GROUP
packTask.dependsOn(
listOf(
packCompileTask,
packJarTask,
packMoveTask,
packManiTask,
packConfigTask,
packZipTask,
packUploadTask
)
)
configurePlugin(project, extension)
}
private fun configurePlugin(project: Project, extension: PackExtension) {
val versionFile = project.file(VERSION_FILE_NAME)
if (versionFile.exists()) {
Properties().apply {
load(versionFile.inputStream())
val code = getProperty(VERSION_CODE)
val name = getProperty(VERSION_NAME)
if (code.isNullOrEmpty()) {
throw RuntimeException("version code is empty")
}
if (name.isNullOrEmpty()) {
throw RuntimeException("version name is empty")
}
extension.versionName = name
extension.versionCode = code.toInt()
}
}
}
}

插件首先添加了 PackExtension 扩展,以及 PackJarTask、PackMoveTask、PackManiTask、PackConfigTask、PackZipTask、PackUploadTask、PackTask 这 7 个 Task,并使用 mustRunAfter 约束执行顺序,同时在打 jar 包时前需要执行下编译,因此使 PackJarTask 依赖 BuildTask。至此该插件就开发完成了,直接执行下 PackTask 就可以自动完成上述操作。

上传 Maven 仓库

在 build.gradle 中添加应用 maven 插件,然后配置以下信息:

uploadArchives {
repositories {
mavenDeployer {
pom.groupId = 'com.yushi.android'
pom.artifactId = "pack"
pom.version = '1.0.1'
repository(url: URL) {
authentication(userName: USERNAME, password: PASSWORD)
}
}
}
}

执行该 Task 就会把该插件上传到公司内部 Maven 仓库。

0%