上一篇文章介绍了Python 封装 adb 命令,这一篇将介绍 Python 封装 gradle 命令,并生成可视化文档报告。
在日常的 Android 项目开发中,通常使用 gradle 命令来获取项目 tasks、projects、depdencies等信息,或执行自定义的 task。
在之前的一篇文章:使用 Gradle 命令了解项目构建信息中介绍了使用 gradle 命令的基础操作,现在利用 python 对这些命令的输出结果,进行信息过滤或生成可视化的 html 文档报告。
首先定义一个执行 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
-
第二步:保存结果到文件中
- 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
-
第三步:将结果文件用电脑打开
- def open_file(file_path):
- """
- 在电脑上打开截屏文件
- :param file_path: 电脑上的截屏文件地址
- """
- system = platform.system().lower()
- if system == "darwin": # macOS
- subprocess.run(["open", file_path])
- elif system == "linux": # Linux
- subprocess.run(["xdg-open", file_path])
- elif system == "windows": # Windows
- subprocess.run(["start", file_path], shell=True)
- else:
- print("Unsupported operating system.")
-
第四步:将上面步骤组合在一起执行
- 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
-
在之前的一篇文章:使用 Gradle 命令了解项目构建信息介绍了通过监听Task:app:mergeDebugNativeLibs来获取本项目、子项目、三方库的 so 依赖信息,但是在控制台输出的 so 依赖信息不是很直观,如果利用python,可以输出一个 so 依赖信息的 html 文档报告。
native_lib.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"
- ]
- }
- ]
- }
- ]
-
第一步
- 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
-
第二步
- def execute_task():
- """
- 执行 task
- :return: 是否成功,true表示成功,否则失败
- """
- task_name = ':app:mergeDebugNativeLibs'
- result = run_gradle_command(['./gradlew', task_name])
- return result is None
-
第三步
- 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]
-
第四步
- 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
-
第四步
- 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:
假设已经将上面的代码封装成了一个 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
-
在项目 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 命令有:
如果感兴趣,可以自定义更多 gradle 命令的 python 脚本。