likes
comments
collection
share

Python 封装 gradle 命令

作者站长头像
站长
· 阅读数 6

Github 项目地址:在 Android 开发中使用 Python 脚本

执行 gradle 命令

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

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

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

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 的关键信息。
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

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

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

  • 输出项目中特定依赖项的详细信息:./gradlew :app:dependencyInsight --configuration releaseRuntimeClassPath --dependency io.reactivex:rxjava
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

> 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时,可以将安卓项目路径传递传递进去:

if __name__ == "__main__":
    args = sys.argv[1:]
    android_project_path = args[0]

python3 dependency_insight.py android_project_path

操作简化- so 依赖信息

封装 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 脚本:

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,输出结果例如:

[
    {
        "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 反序列化
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
def execute_task():
    """
    执行 task
    :return: 是否成功,true表示成功,否则失败
    """
    task_name = ':app:mergeDebugNativeLibs'
    result = run_gradle_command(['./gradlew', task_name])
    return result is None

第三步

  • 反序列化 json
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 后的数据转化为表格数据
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 文档报告
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 结果存储地址

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

Python 封装 gradle 命令

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

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 信息,例如:

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 脚本。


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

转载自:https://juejin.cn/post/7324600741376901154
评论
请登录