需求

一个Android工程常常会有很多依靠,或许是组件化项目关于其他事务及中间件的依靠,也有或许是关于第三方sdk的依靠。这儿咱们别离看下关于这两种场景为什么需求主动化的版别依靠剖析东西:

  • 二方组件依靠:
    每次集成发版时分或许会有许多事务组件进行发布,有了主动剖析的东西能够监测到有开发期的事务组件进行了集成,有效的对风险进行躲避。
  • 三方sdk依靠:
    一个正常的项目关于三方sdk的晋级肯定是非常慎重的,但是现在sdk的依靠层级非常深,包含androidx,常常一拖多,不知道什么时分就晋级了一个咱们不想晋级的sdk版别,这时主动化剖析就非常重要。

通过上述剖析,完成的东西大概是这样的:

  1. 需求在一些关键的构建场景(如集成,上线)阶段进行版别依靠的主动化剖析及陈述,陈述能够直接通过机器人或邮件进行提醒。
  2. 提醒的信息不需求太杂乱,能直观的看出相关于前次构建那些库进行了晋级即可,后续详细的剖析还是交给人工,运用gradle dependencies进行二次排查。

接下来剖析下怎样快速的完成一个主动化版别依靠剖析东西, 先看一下详细的效果展现:

集成分支(xxx)相关于前次构建(xxx)依靠上的变化:
版别号发生了变化 com.squareup.okhttp3:okhttp 3.11.0===>3.12.6

采集依靠

想要对依靠进行剖析,首要需求将工程的所有依靠先收集起来,这儿咱们不需求像dependencies进行杂乱的树形剖析,直接将所有依靠铺平(flatten) 到一个文件中即可,文件每一行类似:

com.squareup.okhttp3:okhttp:3.11.0

首要界说依靠对应的数据bean:

class Dependency{
    String group = ""
    String name = ""
    String version = ""
    @Override
    String toString() {
        return "${group}:${name}:${version}"
    }
}

选用网络上的通用做法,通过gradle api的方式获取依靠:

project.afterEvaluate {
    project.android.applicationVariants.all { variant ->
        tasks.create(name: "showDependencies${variant.name.capitalize()}",
                description: "展现所有依靠") {
            File file = new File("${getProjectDir()}","Dependencies${variant.name.capitalize()}")
            if (file.exists()){
                file.delete()
            }
            List<Dependency> list = new ArrayList<>()
            HashSet<String> set = new HashSet<>()
            Configuration configuration
            try {
                //gradle 3.x
                configuration = project.configurations."${variant.name}CompileClasspath"
            } catch (Exception e) {
                //gradle 2.x
                configuration = project.configurations."_${variant.name}Compile"
            }
            configuration.resolvedConfiguration.lenientConfiguration.allModuleDependencies.each {
                list.add(collectDependency(it))
            }
            file.withWriter { writer->
                list.forEach{ data->
                    addDependency(writer,data,0,set)
                }
                set.forEach{ item->
                    writer.writeLine item
                }
            }
        }
    }
}
Dependency collectDependency(ResolvedDependency dependency){
    def identifier = dependency.module.id
    def item = new Dependency()
    item.group = identifier.group
    item.name = identifier.name
    item.version = identifier.version
    return item
}
def addDependency(BufferedWriter writer, Dependency dependency, int index, HashSet<String>set){
    set.add(dependency.toString())
}

到这儿停止,获取到了抱负格局的依靠内容,并去重后存入“Dependencies变体”文件中。
脚本履行的机遇是每次构建工程,在gradle configuration的完成阶段(afterEvaluate)。能够通过一些变量,将脚本履行约束在CI/CD的集成阶段,在开发期不太需求每次都跑这个,这儿不再赘述。

依靠改变生成

生成了依靠相关的文件,就能够进行真实的依靠比对,这儿将场景简化为:两次集成依靠改变剖析后,只需求比对上一次和本次的依靠剖析文件即可。
咱们将上一次的依靠剖析文件界说为 DA,本次的依靠剖析文件界说为 DB。

  1. 新增:DA中不存在,DB中存在。
  2. 修正:DA,DB中均存在,但两次version不一样。
  3. 删除:DA中存在,DB中不存在。

本次依靠剖析完成后,将文件归档保存下来,就变成了上一次的依靠剖析文件。
这儿需求留意的是依靠剖析文件前次的位置是在当前CI的工程workspace下,比如咱们运用jenkins,就在slave对应集成job的作业空间下,归档指的是将其迁移到一个安稳的存储磁盘中,因为jenkins的作业空间每次都是新的。

有了两次的依靠剖析文件,看下详细的比对细节:

workspace = os.getenv("WORKSPACE")
work_space_name = workspace.split("/")[len(workspace.split("/")) - 1]
REMOTE_SAVE_DIR = "xxxx"
domain = "xxxx"
for roots, dirs, files in os.walk(workspace + "/app", topdown=True):
    for name in files:
        if name.find("Dependencies") != -1:
            file_name = os.path.join(roots, name)
            record_map = {}
            init_version_map(open(file_name), record_map)
            find_last_commit_version_diff(name, record_map)
            write_file(record_map, name)

本次的依靠文件从当前job作业空间获取即可,数据反序列化后写入record_map中,详细逻辑在init_version_map办法。
前次的依靠文件需求从归档的存储中获取,例如依据domain和remote_save_dir拼出详细的拉取地址进行拉取,拉取后写入last_record_map中,详细比对逻辑在find_last_commit_version_diff办法。

init_version_map
def init_version_map(file, version_map):
    for line in file.readlines():
        if len(line.strip()) != 0:
            item_list = line.split(":")
            if item_list[0].strip().find(work_space_name) != -1:
                item_list[0] = item_list[0].strip().replace(work_space_name, "local")
            if len(item_list) == 3:
                version_map[item_list[0].strip() + ":" + item_list[1].strip()] = item_list[2].strip()
            else:
                key = ""
                for item in item_list:
                    key += item
                version_map[key] = "NO_VERSION"

由于存入依靠剖析文件中的格局为:

 "${group}:${name}:${version}"

所以map以${group:name}为key,version为value。

find_last_commit_version_diff
def find_cloud_dependencies(target_name):
    try:
        # 树立一个sshclient目标
        ssh = paramiko.SSHClient()
        # 答应将信赖的主机主动加入到host_allow 列表,此办法有必要放在connect办法的前面
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        transport = paramiko.Transport((domain, port))
        transport.connect(username='xxx', password='xxx')
        sftp = paramiko.SFTPClient.from_transport(transport)
        local = "/home/dev/workspace/RecordDependencies/cloud" + target_name
        sftp.get(src, local)
        ssh.close()
        return os.path.exists(local)
    except BaseException as error:
        return False

这一步将上一次的依靠剖析文件从远端拉回,并命名为“cloudDependencies变体”文件。

def find_last_commit_version_diff(name, is_doctor, current_record_map):
    if find_cloud_dependencies(name, is_doctor):
        last_record_map = {}
        last_path = "/home/dev/workspace/RecordDependencies/cloud" + name
        init_version_map(open(last_path), last_record_map)
        add_version_map = {}
        remove_version_map = {}
        changed_version_map = {}
        for lib, version in current_record_map.items():
            if lib not in last_record_map:
                add_version_map[lib] = version
            elif last_record_map[lib] != version:
                changed_version_map[lib] = last_record_map[lib] + "===>" + version
        for lib, version in last_record_map.items():
            if lib not in current_record_map:
                remove_version_map[lib] = version
        version_diff_file_path = "/home/dev/workspace/RecordDependencies/lastVersionDiff" + name
        if os.path.exists(version_diff_file_path):
            os.remove(version_diff_file_path)
        diff_file = open(version_diff_file_path, 'w')
        if add_version_map or remove_version_map or changed_version_map:
            for lib, version in add_version_map.items():
                diff_file.write("新增了  " + lib + "  版别号为:" + version + "  n  ")
            if add_version_map:
                diff_file.write("**————————**  n  ")
            for lib, version in remove_version_map.items():
                diff_file.write("移除了  " + lib + "  版别号为:" + version + "  n  ")
            if remove_version_map:
                diff_file.write("**————————**  n  ")
            for lib, version in changed_version_map.items():
                diff_file.write("版别号发生了变化  " + lib + "  " + version + "  n  ")
        else:
            diff_file.write("相关于上一次打包没有变化  n  ")
        diff_file.close()
        upload_file(version_diff_file_path, "lastVersionDiff" + name)

上一次的依靠剖析map为last_record_map,本次的依靠剖析map为current_record_map,依据之前剖析的规矩进行比对,并生成对应的文本阐明。

终究生成的比对文本写入文件“lastVersionDiff变体”中,供后续运用,能够将文件的内容发送到机器人或邮件,便是最初完成展现的效果了。