Python 封装 gradle 命令

上一篇文章介绍了Python 封装 adb 命令,这一篇将介绍 Python 封装 gradle 命令,并生成可视化文档报告。

执行 gradle 命令

在日常的 Android 项目开发中,通常使用 gradle 命令来获取项目 tasks、projects、depdencies等信息,或执行自定义的 task。

在之前的一篇文章:使用 Gradle 命令了解项目构建信息中介绍了使用 gradle 命令的基础操作,现在利用 python 对这些命令的输出结果,进行信息过滤或生成可视化的 html 文档报告。

首先定义一个执行 gradle 命令的基础方法:

python 复制代码
def run_gradle_command(command):
    """
    :param command: 实际相关命令
    :return: 执行命令结果
    """
    try:
        result = subprocess.run(command, check=True, text=True, capture_output=True, encoding='utf-8')
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error executing command: {e}")
        return None

操作简化-基础信息

如果在 Android 项目控制台中,直接使用 gradle 命令执行 Task :app:dependencyInsight (输出项目中特定依赖项的详细信息),不仅等待时间长(项目越大时间越长),而且控制台还会输出很多冗余信息。利用 python,可以将执行结果信息简化。

第一步:执行 gradle task

python 复制代码
def execute_task_dependency_insight(dependency):
    """
    输出项目中特定依赖项的详细信息
    :param dependency: 依赖项,例如:io.reactivex:rxjava'
    :return: task_name: 任务名称,task_result:执行的任务结果
    """
    task_name = ':app:dependencyInsight'
    task_result = run_gradle_command(
        ['./gradlew', task_name, '--configuration', 'releaseRuntimeClasspath', '--dependency',
         dependency])
    return task_name, task_result

第二步:保存结果到文件中

  • 在 gradle 命令执行 task 的输出结果中,从 > Task :app:dependencyInsight 行开始才是该 task 的关键信息。
python 复制代码
def save_result_to_file(file_dir, file_name, task_name, task_result):
    """
    保存结果到文件
    :param file_dir: 文件目录
    :param file_name: 文件名称
    :param task_name: gradle task 名称
    :param task_result: 内容¬
    :return: 文件地址
    """
    # 从 > Task 行开始截取执行结果
    start_str = "> Task " + task_name
    content = task_result[task_result.rfind(start_str):]
    if len(file_name) > 0:
        des_file_name = file_name + '.txt'
    else:
        des_file_name = task_name[1:].strip().replace(':', '-') + ".txt"
    file_path = file_dir + "/" + des_file_name
    # 如果目录不存在,先创建一个目录
    if not os.path.exists(file_dir):
        os.makedirs(file_dir)
    with open(file_path, 'w') as f:
        f.write(content)
        f.close()
    return file_path

第三步:将结果文件用电脑打开

这一步与上一篇Python 封装 adb 命令中的"在电脑上打开截屏文件"一样:def open_file(file_path)

第四步:将上面步骤组合在一起执行

  • 输出项目中特定依赖项的详细信息:./gradlew :app:dependencyInsight --configuration releaseRuntimeClassPath --dependency io.reactivex:rxjava
python 复制代码
if __name__ == "__main__":
    android_project_path = '/Users/wangjiang/Public/software/android-workplace/Demo'
    # 切换到安卓项目工作目录
    os.chdir(android_project_path)
    # 结果输出文件地址
    report_file_dir = android_project_path + '/build/reports'
    #  输出项目中 io.reactivex:rxjava 依赖项的详细信息
    task_name, task_result = execute_task_dependency_insight('io.reactivex:rxjava')
    # 结果输出问文件名称
    file_name = task_name[1:].replace(':', '-')
    # 结果文件地址
    file_path = _save_result_to_file(report_file_dir, file_name, task_name, task_result)
    open_file(file_path)

执行上面的 python 脚本后,电脑会自动打开 > Task :app:dependencyInsight 的输出结果项目路径/build/reports/app-dependencyInsight.txt

python 复制代码
> Task :app:dependencyInsight
io.reactivex:rxjava:1.3.0
   variant "runtime" [
      org.gradle.status                                               = release (not requested)
      org.gradle.usage                                                = java-runtime
      org.gradle.libraryelements                                      = jar (not requested)
      org.gradle.category                                             = library (not requested)

      Requested attributes not found in the selected variant:
         com.android.build.api.attributes.BuildTypeAttr                  = release
         com.android.build.api.attributes.ProductFlavor:type             = release
         org.gradle.jvm.environment                                      = android
         com.android.build.api.attributes.AgpVersionAttr                 = 7.2.2
         org.jetbrains.kotlin.platform.type                              = androidJvm
   ]
   Selection reasons:
      - By conflict resolution : between versions 1.3.0 and 1.1.6

//.....省略

假设已经将上面的代码封装成了一个 dependency_insight.py,那么执行 python3 dependency_insight.py时,可以将安卓项目路径传递传递进去:

python 复制代码
if __name__ == "__main__":
    args = sys.argv[1:]
    android_project_path = args[0]

python3 dependency_insight.py android_project_path

操作简化- so 依赖信息

在之前的一篇文章:使用 Gradle 命令了解项目构建信息介绍了通过监听Task:app:mergeDebugNativeLibs来获取本项目、子项目、三方库的 so 依赖信息,但是在控制台输出的 so 依赖信息不是很直观,如果利用python,可以输出一个 so 依赖信息的 html 文档报告。

封装 mergeDebugNativeLibs 脚本

  • 将监听 Task:app:mergeDebugNativeLibs 的 groovy 脚本封装到一个单独文件 native_libs.gradle
  • Task:app:mergeDebugNativeLibs 的输出结果用 json 格式保存到文件,文件路径需要在后面的 python 脚本中指定
  • 把文件native_libs.gradle放到 app 的 build.gralde 文件所在同级目录下,并在项目 build.gralde 中添加依赖: apply from: "./native_libs.gradle"
  • 在命令行执行 ./gradlew app:mergeDebugNativeLibs,输出项目 so 依赖信息 json 结果

native_libs.gradle 脚本:

groovy 复制代码
class NativeLibInfo {
    // native lib 名称
    String lib_name
    // so 依赖的项目相对路径信息列表
    List<String> so_relative_path_list

    NativeLibInfo(String lib_name, List<String> so_relative_path_list) {
        this.lib_name = lib_name
        this.so_relative_path_list = so_relative_path_list
    }
}

class ReportInfo {
    //本项目、子项目、三方库
    String name
    //依赖 so 信息列表
    List<NativeLibInfo> info

    ReportInfo(String name, List<NativeLibInfo> info) {
        this.name = name
        this.info = info
    }
}

project.afterEvaluate {
    project.android.applicationVariants.all { variant ->
        //获取构建变体的名称
        logger.lifecycle("${variant.name}")
        def variantName = variant.name
        def name = String.valueOf(variantName.charAt(0)).toUpperCase() + variantName.substring(1)
        def mergeNativeLibsTask = project.tasks.findByName("merge${name}NativeLibs")
        if (mergeNativeLibsTask != null) {
            mergeNativeLibsTask.doLast { task ->
                def result = new ArrayList<ReportInfo>()
                //当前项目相关的 so 文件列表
                def projectNativeResult = new ReportInfo("project native libs", getProjectSoInfo(false, task.projectNativeLibs.getFiles()))
                result.add(projectNativeResult)
                //子项目相关的 so 文件列表
                def subProjectNativeResult = new ReportInfo("sub project native libs", getProjectSoInfo(false, task.subProjectNativeLibs.getFiles()))
                result.add(subProjectNativeResult)
                //三方库相关的 so 文件列表
                def externalProjectNativeResult = new ReportInfo("external project native libs", getProjectSoInfo(true, task.externalLibNativeLibs.getFiles()))
                result.add(externalProjectNativeResult)

                def fileDir = new File(project.buildDir.path + "/reports/so")
                if (!fileDir.exists()) {
                    fileDir.mkdirs()
                }
                saveSoInfoReport(fileDir, result)
            }
        }
    }
}
/**
 * 保存项目依赖的 so 信息列表到 json 文件中
 * @param savePath 保存路径目录
 * @param result 项目依赖的 so 信息列表
 */
def saveSoInfoReport(File saveDir, ArrayList<ReportInfo> result) {
    def reportFile = new File(saveDir, "native_libs.json")
    def jsonBuilder = new groovy.json.JsonBuilder()
    jsonBuilder result.collect { reportInfo ->
        [
                name: reportInfo.name,
                info: reportInfo.info.collect { nativeLibInfo ->
                    [
                            lib_name             : nativeLibInfo.lib_name,
                            so_relative_path_list: nativeLibInfo.so_relative_path_list
                    ]
                }
        ]
    }
    def jsonResult = jsonBuilder.toPrettyString()
    def writer = new BufferedWriter(new FileWriter(reportFile))
    writer.write(jsonResult)
    writer.flush()
    writer.close()
    project.logger.info("Project Native Libs Json Report:" + reportFile.path + '\n')
}

/**
 * 获取项目依赖的 native lib 中的 so 信息列表
 * @param isExternal 是否是三方库
 * @param fileSet so 路径列表
 * @return so 信息列表
 */
def getProjectSoInfo(boolean isExternal, Set<File> fileSet) {
    def projectNames = new HashSet<String>()
    def nativeLibsInfoList = new ArrayList<NativeLibInfo>()
    def rootProjectPath = project.rootProject.projectDir.path
    def buildName = project.rootProject.buildDir.name
    fileSet.forEach { file ->
        def projectName
        if (!isExternal) {
            projectName = file.path.substring(rootProjectPath.length() + 1, file.path.indexOf(buildName) - 1)
        } else {
            projectName = file.parentFile.name
        }
        projectNames.add(projectName)
        def childFiles = file.listFiles().toList()
        def soRelativePathList = new ArrayList<>()
        while (childFiles.size() > 0) {
            def childFile = childFiles.remove(0)
            if (childFile.isDirectory()) {
                childFiles.addAll(childFile.listFiles())
            } else {
                soRelativePathList.add(childFile.path.substring(file.path.length() + 1))
            }
        }
        nativeLibsInfoList.add(new NativeLibInfo(projectName, soRelativePathList))
    }
    return nativeLibsInfoList
}

在命令行执行./gradlew app:mergeDebugNativeLibs,输出结果例如:

json 复制代码
[
    {
        "name": "project native libs",
        "info": [
            
        ]
    },
    {
        "name": "sub project native libs",
        "info": [
            
        ]
    },
    {
        "name": "external project native libs",
        "info": [
            {
                "lib_name": "dynamicview-core-6a43f8cfb73faca6dfc0926d8d08a6d8_44bfd0faea8a1c23a4e56bba23a0f3a191d06c00-debug",
                "so_relative_path_list": [
                    "armeabi-v7a/libSapling.so",
                    "armeabi-v7a/libc++_shared.so",
                    "x86/libSapling.so",
                    "x86/libc++_shared.so",
                    "arm64-v8a/libSapling.so",
                    "arm64-v8a/libc++_shared.so",
                    "x86_64/libSapling.so",
                    "x86_64/libc++_shared.so"
                ]
            },
            {
                "lib_name": "matrix-io-canary-2.0.1",
                "so_relative_path_list": [
                    "armeabi-v7a/libio-canary.so",
                    "x86/libio-canary.so",
                    "arm64-v8a/libio-canary.so",
                    "x86_64/libio-canary.so"
                ]
            },
            {
                "lib_name": "matrix-fd-2.0.2",
                "so_relative_path_list": [
                    "armeabi-v7a/libmatrix-fd.so",
                    "arm64-v8a/libmatrix-fd.so"
                ]
            },
            {
                "lib_name": "matrix-hooks-2.0.2",
                "so_relative_path_list": [
                    "include/ThreadPool.h",
                    "include/ReentrantPrevention.h",
                    "include/SoLoadMonitor.h",
                    "include/Log.h",
                    "include/Macros.h",
                    "include/HookCommon.h",
                    "include/Maps.h",
                    "include/ScopedCleaner.h",
                    "include/JNICommon.h",
                    "include/ProfileRecord.h",
                    "armeabi-v7a/libmatrix-memguard.so",
                    "armeabi-v7a/libmatrix-hookcommon.so",
                    "armeabi-v7a/libmatrix-memoryhook.so",
                    "armeabi-v7a/libmatrix-pthreadhook.so",
                    "armeabi-v7a/libc++_shared.so",
                    "arm64-v8a/libmatrix-memguard.so",
                    "arm64-v8a/libmatrix-hookcommon.so",
                    "arm64-v8a/libmatrix-memoryhook.so",
                    "arm64-v8a/libmatrix-pthreadhook.so",
                    "arm64-v8a/libc++_shared.so",
                    "include/struct/lock_free_array_queue.h",
                    "include/struct/lock_free_queue.h",
                    "include/struct/splay_map.h",
                    "include/struct/buffer_source.h"
                ]
            }
        ]
    }
]

此时,已经可以快速找到某个 so(不管so 是在本项目、子项目,还是三方项目)来自于哪个 native lib

定义 python 脚本,输出 html 文档报告

第一步

  • 定义上面 groovy 脚本中的数据模型,用于 json 反序列化
python 复制代码
class NativeLibInfo:
    def __init__(self, lib_name, so_relative_path_list):
        """
        :param lib_name: native lib 名称
        :param so_relative_path_list: so 依赖的项目相对路径信息列表
        """
        self.lib_name = lib_name
        self.so_relative_path_list = so_relative_path_list


class ReportInfo:
    def __init__(self, name, info):
        """
        :param name: 本项目、子项目、三方库
        :param info: 依赖 so 信息列表
        """
        self.name = name
        self.info = info

第二步

  • 执行 ./gradlew app:mergeDebugNativeLibs
python 复制代码
def execute_task():
    """
    执行 task
    :return: 是否成功,true表示成功,否则失败
    """
    task_name = ':app:mergeDebugNativeLibs'
    result = run_gradle_command(['./gradlew', task_name])
    return result is None

第三步

  • 反序列化 json
python 复制代码
def deserialize_json(json_path):
    """
    :param json_path: json 路径
    :return: ReportInfo List
    """
    if not os.path.exists(json_path):
        print(f"FileNotExists:{json_path}")
        return None
    with (open(json_path, 'r') as file):
        data = json.load(file)
        # json 反序列化
        return [ReportInfo(name=report_data.get('name'), info=[
            NativeLibInfo(**native_lib_info) for native_lib_info in report_data.get('info', [])
        ]) for report_data in data]

第四步

  • 将反序列化 json 后的数据转化为表格数据
python 复制代码
def _custom_group_key(key=''):
    """
    :param key:如 armeabi-v7a/xx.so 或 arm64-v8a/xx.so等等
    :return: armeabi-v7a 或 arm64-v8a
    """
    return key[:key.index('/')]
    
def format_data(data, project_name):
    """
     将data格式化为表格
    :param data: json 结果地址
    :return:表格数据和表格标题
    """
    # html 中表格数据
    table_data = {}
    max_len = 0
    for report_info in data:
        for native_lib_info in report_info.info:
            # 对结果分组
            grouped_so_relative_path_list = [list(group) for key, group in
                                             groupby(native_lib_info.so_relative_path_list, _custom_group_key)]
            new_so_relative_path_list = []
            for item in grouped_so_relative_path_list:
                new_so_relative_path_list.append(', '.join(item))
            print(native_lib_info.lib_name + ": " + str(new_so_relative_path_list))
            native_lib_info.so_relative_path_list = new_so_relative_path_list
            so_relative_path_list_size = len(native_lib_info.so_relative_path_list)
            if so_relative_path_list_size > max_len:
                max_len = so_relative_path_list_size
    # native lib 数量
    num_libs = 0
    for report_info in data:
        num_libs = num_libs + len(report_info.info)
        for native_lib_info in report_info.info:
            while len(native_lib_info.so_relative_path_list) < max_len:
                native_lib_info.so_relative_path_list.append('')
            table_data[native_lib_info.lib_name] = native_lib_info.so_relative_path_list

    title = f"{project_name} native libs: {num_libs}"
    return title, table_data

第四步

  • 利用 pandas 库生成 html 文档报告
python 复制代码
def generate_html(title, table_data, output_path):
    """
    生成 html 文档报告
    :param title: html 文档标题
    :param table_data: native libs info
    :param output_path: html 文档报告文件地址
    """
    table = pd.DataFrame.from_dict(data=table_data).set_index(list(table_data.keys())).transpose()
    styled_html = """
      <style>
        table {
          width: 50%;
          border-collapse: collapse;
          margin-top: 10px;
        }
        th, td {
          border: 1px solid black;
          padding: 8px;
          text-align: left;
        }
      </style>
      """ + table.to_html()
    if os.path.exists(output_path):
        os.remove(output_path)
    html_content = f"<H1>{title}</H1>\n{styled_html}"
    with open(output_path, 'w') as f:
        f.write(html_content)
    print(output_path)
    open_file(output_path)

第五步:将上面步骤组合在一起执行

注意: json_path 的定义,就是执行 ./graldew :app:mergeDebugNativeLibs 完后的 json 结果存储地址

python 复制代码
if __name__ == "__main__":
    android_project_path = '/Users/wangjiang/Public/software/android-workplace/Demo'
    # 切换到安卓项目工作目录
    os.chdir(android_project_path)
    # 结果输出目录
    report_file_dir = android_project_path + '/build/reports/so'
    if not os.path.exists(report_file_dir):
        os.makedirs(report_file_dir)
    # 结果输出地址
    report_path = report_file_dir + '/native_libs.html'
    # 执行 ./graldew mergeDebugNativeLibs 后的 json 结果存储地址,与 native_lib.gradle 中的对应
    json_path = '/Users/wangjiang/Public/software/android-workplace/Demo/build/reports/so/native_libs.json'
    # 项目名称,方便在输出结果html中显示
    project_name = 'My Project'
    isSuccess = execute_task()
    if isSuccess:
        data = deserialize_json(json_path)
        if data is None:
            print("Deserialize json failed: " + json_path)
        else:
            title, table_data = format_data(data, project_name)
            generate_html(title, table_data, report_path)
    else:
        print("Execute task failed")

输出结果例如 file:///Users/wangjiang/Public/software/android-workplace/Demo/build/reports/so/native_libs.html

假设已经将上面的代码封装成了一个 native_libs.py,那么执行 python3 native_libs.py时,可以将安卓项目路径传递传递进去:

python 复制代码
if __name__ == "__main__":
    args = sys.argv[1:]
    android_project_path = args[0]
    json_path = android_project_path+'/build/reports/so/native_libs.json'

python3 native_libs.py android_project_path

将 python 脚本放到 CI/CD 中执行

在项目 CI/CD build 阶段完成后,在 analyze 阶段新增一个 job: so dependency 用于分析项目依赖的 so 信息,例如:

bash 复制代码
so dependency:
  tags:
    - apk
    - android
  stage: analyze
  script:
    - ./gradlew :app:mergeDebugNativeLibs
  after_script:
    - python3.9 native_lib.py
  artifacts:
    name: "$CI_JOB_STAGE}_reports_${CI_PROJECT_NAME}_$CI_COMMIT_REF_SLUG"
    when: on_success
    expire_in: 3 days
    paths:
      - "*/build/reports"
  only:
    - branches
  except:
    - master

在项目每次跑完 pipiline 后,就会生成一个项目 so 依赖信息报告:native_lib.html ,方便每个开发人员下载查看。

小结

使用 python 执行相关 gradle 命令,主要是简化输出信息-直接获取关键信息,或生成可视化报告,或是放在 CI/CD 中执行的 script。可以用 python 封装的常用 gradle 命令还有:

  • 输出项目中特定依赖项的详细信息:./gradlew :app:dependencyInsight
  • 输出项目依赖信息:./gradlew :app:dependencies --configuration releaseRuntimeClasspath
  • 输出项目项目信息:./graldew projects
  • 输出项目 so 信息:./gradlew :app;mergeDebugNativeLibs
  • 其它

如果感兴趣,可以自定义更多 gradle 命令的 python 脚本。


下一篇将介绍 Python 封装 git 命令

后续会将完整代码放到 github。

相关推荐
AI悦创Python辅导6 分钟前
如何挑选适合项目场景的数据分析工具?
前端
用户9272472502199 分钟前
新闻自动采集并通过API发布到博客
前端·后端
清风920012 分钟前
Logback——日志技术(基础)
java·前端·logback
EndingCoder12 分钟前
排序算法与前端交互优化
开发语言·前端·javascript·算法·排序算法·交互
三月的一天37 分钟前
在 React Three Fiber 中实现 3D 模型点击扩散波效果
前端·react.js·前端框架
爱敲代码的小冰38 分钟前
npm 切换 node 版本 和npm的源
前端·npm·node.js
DoraBigHead43 分钟前
🧠【彻底读懂 reduce】acc 是谁?我是谁?我们要干嘛?
前端·javascript·面试
future14121 小时前
项目开发日记
前端·学习·c#·游戏开发
汪子熙1 小时前
CSS 中 td:last-child a 选择器详解
前端·javascript
北北~Simple1 小时前
第一次搭建数据库
服务器·前端·javascript·数据库