背景
公司的Android项目是在google play发布,由于项目组的历史原因,测试负责jenkins的打包操作(无论是测试阶段还是打release包),因为每次发版的负责人可能不一样,并且项目采用的是指定flavors去打不同国家(渠道)包,这就很难避免因为人为因素导致的打包出错(debug还是release,菲律宾还是越南版本等等)。所以这个插件的出发点是做到无论是谁打的包,是谁负责推到gp发布,都可以直接拿到发布apk的打包信息,通过打包信息去判断是否为最终发布版本。结合项目主要考虑插入以下信息来保证唯一性:
- 国家版本,即我们熟知的flavors渠道号
- buildType,即debug还是relase等等
- 包名,在项目中不是必选项,因为上传gp如果包名不一致会上传失败
- 版本号
- git提交信息,包括打包分支和最近的提交commit id,格式 branch :commitId
- 自定义额外信息,做扩展用
以上信息都可以自定义是否写入,默认写入。
流程
梳理了需要插入的打包信息后,接下来就是需要确定怎么插入。一开始想用最简单的方式,直接在zip的comment字段插入信息,直接读取就好了,但是发现项目已经用了v2签名,这种方法不适用;沿着这种思路可以考虑用美团的walle,但考虑到上传gp的稳定性和没有生成文件的直观方便,最终还是选择在META-INF插入自定义文件,同时拿到生成包后写一个python脚本去读。
grale插件
Talk is cheap,show me the code.这里介绍的是本地插件,没有push到maven。
1.定义常量
class InsertMetaInfConstants {
//定义gradle扩展名
static final String EXTENSION_NAME = "insertmetainf"
//指定生成文件路径
static final String GENERATE_INSERT_PATH = 'generated/insertmetainf/'
}
2.gralde扩展参数配置
class InsertMetaInfExtension {
/**
* 设置meta-inf文件名字
*/
String metaInfName = null
/**
* 是否写入flavor,默认写入
*/
boolean insertFlavor= true
/**
* 是否写入build type,默认写入
*/
boolean insertBuildType= true
/**
* 是否写入applicaitionId,默认写入
*/
boolean insertApplicaitionId = true
/**
* 是否写入versionName,默认写入
*/
boolean insertVersionName = true
/**
* 是否写入versionCode,默认写入
*/
boolean insertVersionCode = true
/**
* 是否写入该分支最后git的commit信息,默认写入
*/
boolean insertGitCommit = true
/**
* 额外信息
*/
String[] insertExtraInfo = null
}
3.自定义gralde plugin
import com.android.build.gradle.*
import org.gradle.api.Plugin
import org.gradle.api.Project
class InsertMetaInfPlugin implements Plugin {
@Override
void apply(Project project) {
project.extensions.create(InsertMetaInfConstants.EXTENSION_NAME, InsertMetaInfExtension)
InsertMetaInfTask insertMetaInfTask = project.task("insertMetaInfTask", type: InsertMetaInfTask)
def resDir = new File(project.buildDir, InsertMetaInfConstants.GENERATE_INSERT_PATH)
def destDir = new File(resDir, 'META-INF/')
boolean isLibrary = project.plugins.hasPlugin(LibraryPlugin)
if (isLibrary) {
project.extensions.getByType(LibraryExtension).sourceSets({
main.resources.srcDirs += resDir
})
} else if (project.plugins.hasPlugin(TestPlugin)) {
project.extensions.getByType(TestExtension).sourceSets({
main.resources.srcDirs += resDir
})
} else if (project.plugins.hasPlugin(AppPlugin)) {
project.extensions.getByType(AppExtension).sourceSets({
main.resources.srcDirs += resDir
})
}
insertMetaInfTask.setDestDir(destDir)
project.afterEvaluate {
project.tasks.findAll { task ->
task.name.startsWith('generate') && task.name.endsWith('Resources')
}.each { t ->
t.dependsOn insertMetaInfTask
}
}
}
}
4.自定义gralde task
import com.android.build.gradle.internal.tasks.DefaultAndroidTask
import org.gradle.api.invocation.Gradle
import org.gradle.api.tasks.TaskAction
import java.util.regex.Matcher
import java.util.regex.Pattern
class InsertMetaInfTask extends DefaultAndroidTask {
def insertApplicaitionId = null
def insertVersionName = null
def insertVersionCode = null
def insertFlavor = null
def insertBuildType = null
InsertMetaInfTask() {
setVariantName("InsertMetaInfTask")
}
private File destDir
void setDestDir(File destDir) {
this.destDir = destDir
}
@TaskAction
def doExecute() {
println("+-----------------------------------------------------------------------------+")
println("| Insert Meta Inf Plugin START |")
println("+-----------------------------------------------------------------------------+")
InsertMetaInfExtension insertMetaInfExtension = project[InsertMetaInfConstants.EXTENSION_NAME]
destDir.mkdirs()
String metaInfName = insertMetaInfExtension.metaInfName
if (null == metaInfName || metaInfName.length() <= 0) {
throw new RuntimeException("metaInfName can not be empty!\n")
}
def vfile = new File(destDir, metaInfName)
StringBuilder sb = new StringBuilder()
//初始化默认的打包信息
initCurrentPackageInfo()
insertTask(sb, insertMetaInfExtension)
def metaInfStr = sb.toString()
vfile.text = metaInfStr
println(
"\nMETA-INF properties generated in ${destDir.getAbsolutePath()}${File.separator}$metaInfName: \n\n$metaInfStr")
println("+-----------------------------------------------------------------------------+")
println("| Insert Meta Inf Plugin END |")
println("+-----------------------------------------------------------------------------+")
}
/**
* 开始写入操作
*/
def insertTask(StringBuilder sb, InsertMetaInfExtension extension) {
//写入基本信息
writeBaseInfo(sb, extension)
//写入额外信息,有的话
writeExtraInfo(sb, extension)
}
def writeExtraInfo(StringBuilder sb, InsertMetaInfExtension extension) {
String[] extraInfo = extension.insertExtraInfo;
if(extraInfo != null && extraInfo.length > 0) {
for(String info : extraInfo) {
sb.append(info).append('\n')
}
}
}
def writeBaseInfo(StringBuilder sb, InsertMetaInfExtension insertMetaInfExtension) {
getJenkinsBuildCode()
if (insertMetaInfExtension.insertFlavor) {
sb.append("flavor=").append(insertFlavor).append('\n')
}
if(insertMetaInfExtension.insertBuildType) {
sb.append("buildType=").append(insertBuildType).append('\n')
}
if (insertMetaInfExtension.insertApplicaitionId) {
sb.append("packageName=").append(insertApplicaitionId).append('\n')
}
if (insertMetaInfExtension.insertVersionName) {
sb.append("versionName=").append(insertVersionName).append('\n')
}
if (insertMetaInfExtension.insertVersionCode) {
sb.append("versionCode=").append(insertVersionCode).append('\n')
}
if (insertMetaInfExtension.insertGitCommit) {
sb.append("gitCommit=").append(getGitBranch() + " : " + getLastCommitId()).append('\n')
}
}
/**
* 获取打包分支
* @return
*/
def getGitBranch() {
return 'git symbolic-ref --short -q HEAD'.execute().text.trim()
}
/**
* 获取最近的git commit id
* @return
*/
def getLastCommitId() {
return 'git rev-parse --short HEAD'.execute().text.trim()
}
def initCurrentPackageInfo() {
def currFlavor = getCurrentFlavor()
def test = project.extensions.findByName("android")
test.productFlavors.all { flavor ->
if (flavor.name.equalsIgnoreCase(currFlavor)) {
insertApplicaitionId = flavor.getApplicationId()
insertVersionCode = flavor.getVersionCode()
insertVersionName = flavor.getVersionName()
}
}
}
def getCurrentFlavor() {
Gradle gradle = project.getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern
if (tskReqStr.contains("assemble")) {
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
} else {
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
}
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find()) {
insertFlavor = matcher.group(1).toLowerCase()
insertBuildType = matcher.group(2).toLowerCase()
return insertFlavor
} else {
println "NO MATCH FOUND"
return ""
}
}
/**
* 获取Jenkins Build 号
* @return
*/
def getJenkinsBuildCode() {
ext.env = System.getenv()
ext.buildNumber = env.BUILD_NUMBER?.toInteger()
def test = "$buildNumber"
println("test buildNumber: " + test)
return
}
}
5.使用插件
//导入插件路径
apply plugin: 'com.***.insertmetainf'
insertmetainf {
metaInfName 'packageInfo.properties'
insertVersionCode false
insertExtraInfo "test1=gg", "test2=pp"
}
6.生成文件信息例子
flavor=ph
buildType=debug
packageName=com.***
versionName=1.1.5
gitCommit=master : d113b9ab
test1=gg
test2=pp
pyhton读取脚本
读取就比较简单了,直接用zip去获取apk里面的META-INF/packageInfo.properties文件:
#coding:utf-8
'''
Auth:bottleTan
Date:2019-08-12
This is a tool for read apk meta-inf/packageInfo.properties file
for more details, please contact [email protected]
'''
import sys
import os
import zipfile
target_file = 'META-INF/packageInfo.properties'
def read(apk_path):
z = zipfile.ZipFile(apk_path, 'r')
try:
print("========================================================")
print(z.read(target_file))
print("========================================================")
except Exception:
raise RuntimeError(target_file + ' is not exists or other exception.')
if __name__ == '__main__':
command = sys.argv
if len(command) < 2:
raise ValueError("command line params must have 2 parameters at least.")
apkPath = command[1]
if not os.path.exists(apkPath):
raise IOError(apkPath + " is not exists,please check.")
print(apkPath)
read(apkPath)
至此,整个流程还是比较简单的,代码也是相对清晰。源码基本如上,就不放到github上了。