maven打包依赖构建工具

缘起:公司每次迭代都会统一拉起分支,每个领域有各自的微服务,微服务内部按功能和使用方等维度划分多个不同的子模块,例如对外的spi接口层,spi实现层,SDK层,对内的api接口层、实现层,业务实现的bootstarp层,web层,service层等等;在编译时就会出现以下问题:

  1. 循环依赖 2:依赖项命名规则不规范,导致无法不知道应该编译那个项目的那个模块 3:依赖模块超多,纯手工编译需要根据依赖报错信息来指定编译依赖的模块,耗时太大

基于此,我实现了一个自动依赖识别和构建的程序,它可以分析目录下所有的项目,解析出它们对应的模块,当在编译时依赖模块找不到时,自动切换到指定项目中进行相应 模块的编译

功能:

  • 自动遍历目录下所有的工程,检查是否是maven工程,解析maven是否支持profile的配置,生成项目模块的映射关系
  • 指定编写某个工程或某个工程的子模块,会自动发现maven仓库中没有的依赖模块,然后进行依赖模块的编译和deploy
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import json
import os
import subprocess
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import ParseError

ns = {'ns': 'http://maven.apache.org/POM/4.0.0'}


print("hello world")

force_no_cache=False
offline = False
param_profile = 'test'
param_version = '0.0.1-SNAPSHOT'
# param_setting = None
param_setting = "/root/.m2/settings.xml"
param_exclusions = {
    'com.tianjiaguo.site:test-parent' : '!com.tianjiaguo.site:testsdk'
}

def check_pom_plugin(pom_path, plugin_artifact):
    global ns
    try:
        tree = ET.parse(pom_path)
        root = tree.getroot()
        extensions = root.find('.//ns:build/ns:extensions', ns)
        if extensions:
            for ext in extensions.findall('ns:extension', ns):
                artifact_id = ext.find('ns:artifactId', ns)
                if artifact_id is not None and artifact_id.text == plugin_artifact:
                    return True
        plugins = root.find('.//ns:build/ns:plugins', ns)
        if plugins:
            for plugin in plugins.findall('ns:plugin', ns):
                artifact_id = plugin.find('ns:artifactId', ns)
                if artifact_id is not None and artifact_id.text == plugin_artifact:
                    return True
        return False
    except Exception as e:
        print(f"解析错误:{str(e)}")
        return False

def check_pom_profile(pom_path, profile_id):
    global ns
    try:
        tree = ET.parse(pom_path)
        root = tree.getroot()
        profiles = root.find('.//ns:profiles', ns)
        if profiles:
            for profile in profiles.findall('ns:profile', ns):
                pid = profile.find('ns:id', ns)
                if pid is not None and pid.text == profile_id:
                    return True
        return False
    except Exception as e:
        print(f"解析错误:{str(e)}")
        return False 

def extract_artifacts(error_line: str):
    if not error_line.startswith('[ERROR] Failed to execute goal on project '):
       return None
    # 找到关键部分
    start = error_line.find("The following artifacts could not be resolved: ")
    if start != -1:
        end = -1
        end_strs = [
            # ': Cannot access ',
            # ': Failure to find ',
            # ': Could not find ',
        ]
        for end_str in end_strs:
            end = error_line.find(end_str)
            if end != -1:
                break
        if end == -1:
            error_line_toend = error_line[start + len("The following artifacts could not be resolved: "):]
            if error_line_toend.find(": ") == -1:
                return None
            artifacts_str = error_line_toend.split(": ")[0]
            artifacts_list = [artifact.strip() for artifact in artifacts_str.split(", ")]
            return artifacts_list
        
        if start != -1 and end != -1:
            # 提取并分割字符串
            artifacts_str = error_line[start + len("The following artifacts could not be resolved: "):end]
            artifacts_list = [artifact.strip() for artifact in artifacts_str.split(", ")]
            return artifacts_list
    else:
        m_pattern = [
            r"Could not find artifact ([^ ]+)", 
            r"Failure to find ([^ ]+)", 
        ]
        for pattern in m_pattern:
            matches = re.findall(pattern, error_line)
            if len(matches) > 0:
                # 提取依赖包信息
                artifacts = []
                for match in matches:
                    artifact = match
                    artifacts.append(artifact)
                return artifacts if len(artifacts) > 0 else None
        print(f'依赖提取失败,文本行是: {error_line}')
        return None
        
def get_depend_module_name(artifact:str):
    f_d_info = artifact.split(":")
    failed_dep = f"{f_d_info[0]}:{f_d_info[1]}"
    return failed_dep


import re

def find_all_dependency(base_dir, item):
    global ns
    global param_profile
    global param_setting
    
    work_dir = os.path.join(base_dir, item)
    if os.path.isfile(os.path.join(work_dir, 'g_no_compile__')):
        print(f"目录不获取依赖信息 {work_dir}")
        return {}
    if not os.path.isfile(os.path.join(work_dir, 'pom.xml')):
        print(f"目录不是Maven工程 {work_dir}")
        return {}
    os.chdir(work_dir)
    
    if item == 'maventest':
        print(f'检查对外三包测试工程 {item}')
    
    if item == 'fiverify':
        print(f'检查对外三包测试工程 {item}')
    
    project_validate = check_pom_plugin(os.path.join(work_dir, "pom.xml"), "fi-common-effective-profile")
    has_profile = check_pom_profile(os.path.join(work_dir, 'pom.xml'), param_profile)
    if project_validate:
        c1 = f" -f effective-profile-pom.xml "
    else:
        c1 = ""
    if has_profile:
        c2 = f" -P{param_profile} "
    else:
        c2 = ""
    if param_setting:
        s1 = f" -s {param_setting} "
    else:
        s1 = ""
        
    try:
        command = f"set -e && set -x && cd {work_dir} && mvn help:effective-pom -Doutput=effective-pom.xml {c2} {s1}"
        subprocess.run(command, shell=True, check=True, cwd=work_dir)
        tree = ET.parse(os.path.join(work_dir, 'effective-pom.xml'))        
        root = tree.getroot()
        
        project_infos = {} # cur_module_info ++
        project_roots = root.findall('.//ns:project', ns)
        module_step = -1 # 判断是否根pom,这个在某些项目上有问题
        if len(project_roots) == 0:
            project_roots.append(root)
        for prj_root in project_roots:
            module_step += 1
            group_id = prj_root.find('ns:groupId', ns).text
            artifact_id = prj_root.find('ns:artifactId', ns).text
            version = prj_root.find('ns:version', ns).text
            if version != param_version:
                print(f"请检查模块的版本号 {item} {group_id}:{artifact_id}:{version}")
            module_name = f'{group_id}:{artifact_id}'
            if has_profile:
                cur_module_info = { module_name: { "dir": item, "profile": param_profile, "validate": project_validate, "step": module_step, } }
            else:
                cur_module_info = { module_name: { "dir": item, "profile": None, "validate": project_validate, "step": module_step, } }
            project_infos.update(cur_module_info)
        return project_infos
        # for module in root.findall('.//ns:modules/ns:module', ns):
        #     module_name = module.text.strip()
        #     module_pom = os.path.join(work_dir, module_name, 'pom.xml')
        #     if os.path.exists(module_pom):
        #         module_tree = ET.parse(module_pom)
        #         module_root = module_tree.getroot()
                
        #         group_id = module_root.find('ns:groupId', ns)
        #         if group_id is None:
        #             group_id = root.find('ns:groupId', ns)
        #         artifact_id = module_root.find('ns:artifactId', ns)
        #         if group_id is not None and artifact_id is not None:
        #             # module.append(f'{group_id.text}:{artifact_id.text}')
        #             # "com.tianjiaguo.third-c:test": { "dir": "maventest", "profile": None|test, "validate": None|True|False, }
        #             module_name = f'{group_id.text}:{artifact_id.text}'
        #             cur_module_info = { module_name: { "dir": item, "profile": param_profile, "validate": project_validate, } }
        #             project_infos.update(cur_module_info)
        # return project_infos
    except (ParseError, subprocess.CalledProcessError) as e:
        print(f"获取子模块 {item} 信息失败:{str(e)}")
        return {}
    
    # command = f"mvn {c2} validate {s1} && mvn {c1} {c2} dependency:tree {s1} |tee dependency-v1.log"  # 替换为你要执行的命令
    # process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    # project_infos = {} # cur_module_info ++
    # while True:
    #     output = process.stdout.readline()
    #     if output == '' and process.poll() is not None:
    #         break
    #     if output:
    #         out_line = output.strip()
    #         print(out_line)
    #         match = re.search(r'<([^>]+)>', out_line)
    #         if match:
    #             # 获取到模块编译的开始
    #             module_name = match.group(1).strip()
    #             print(f'开始处理模块 {module_name} 的依赖信息')
    #             # "com.tianjiaguo.third-package:test": { "dir": "maventest", "profile": None|test, "validate": None|True|False, }
    #             cur_module_info = { module_name: { "dir": item, "profile": param_profile, "validate": project_validate, } }
    #             project_infos.update(cur_module_info)
    # return project_infos

# def save_dict_to_json(dictionary, file_path):
#     """将字典序列化为JSON并保存到本地文件"""
#     with open(file_path, 'w', encoding='utf-8') as file:
#         json.dump(dictionary, file, ensure_ascii=False, indent=4)

# def load_dict_from_json(file_path):
#     """从本地JSON文件中加载并还原为字典"""
#     with open(file_path, 'r', encoding='utf-8') as file:
#         return json.load(file)

def find_all_dependency_with_cache(base_dir, item):
    work_dir = os.path.join(base_dir, item)
    dependency_info_file = os.path.join(work_dir, 'dependency_info.json')
    if not force_no_cache and os.path.isfile(dependency_info_file):
        with open(dependency_info_file, 'r', encoding='utf-8') as file:
            return json.load(file)
    else:
        dependencies = find_all_dependency(base_dir=base_dir, item=item)
        if dependencies and len(dependencies) > 0:
            with open(dependency_info_file, 'w', encoding='utf-8') as file:
                json.dump(dependencies, file, ensure_ascii=False, indent=4 )
        return dependencies
        
            

def find_all_dependencys(work_dir: str):
    project_infos = {} # "com.tianjiaguo.site:test": { "dir": "maventest", "profile": None, "validate": None, }
    os.chdir(work_dir)
    
    for item in os.listdir(work_dir):
        item_path = os.path.join(work_dir, item)
        if os.path.isdir(item_path):
            os.chdir(item_path)
            ds = find_all_dependency_with_cache(work_dir, item)
            project_infos.update(ds)
    return project_infos
            

def find_all_dependencys_with_cache(work_dir: str):
    project_infos = {} # "com.tianjiaguo.site:test": { "dir": "maventest", "profile": None, "validate": None, }
    os.chdir(work_dir)
    
    dependency_info_file = os.path.join(work_dir, 'dependency_info.json')
    if not force_no_cache and os.path.isfile(dependency_info_file):
        with open(dependency_info_file, 'r', encoding='utf-8') as file:
            return json.load(file)
    else:
        dependencies = find_all_dependencys(work_dir=work_dir)
        if dependencies and len(dependencies) > 0:
            with open(dependency_info_file, 'w', encoding='utf-8') as file:
                json.dump(dependencies, file, ensure_ascii=False, indent=4 )
        return dependencies
    

if __name__ == "__main__":
    # work_dir="/mnt/g/workspace/test"
    work_dir="/mnt/g/workspace/0.0.1-SNAPSHOT"
    os.chdir(work_dir)
    project_infos = find_all_dependencys_with_cache(work_dir=work_dir)
    
    # root_project_infos = []
    # for project_info_key, project_info in project_infos.items():
    #     project_step = project_info['step']
    #     if project_step == 0:
    #         root_project_infos.append({
    #             'project_artifact' : project_info_key,
    #             'project_info' : project_info,
    #         })
    #         print(f'failed_dep_infos.append("{project_info_key}:pom:{param_version}" ) # {project_info["dir"]}')
    
    failed_dep_infos = []
    success_deps = {
        "com.tianjiaguo.site:test-models":True,
    }
    for dep_key, dep_info  in project_infos.items():
        if dep_key.endswith(('-sdk')):
            failed_dep_infos.append(f'{dep_key}:pom:{param_version}')
    for dep_key, dep_info  in project_infos.items():
        if dep_key.endswith(('-api')):
            failed_dep_infos.append(f'{dep_key}:pom:{param_version}')
    ###################################################################
    # ...
    # failed_dep_infos.append("com.tianjiaguo.site:test:pom:0.0.1-SNAPSHOT" ) # maventest
    # failed_dep_infos.append("com.tianjiaguo.site:test-parent:pom:0.0.1-SNAPSHOT" ) # fi-pom ---------------
    # failed_dep_infos.append("com.tianjiaguo.site:test-pom-parent:pom:0.0.1-SNAPSHOT" ) # fi-pom ---------------
    
    while len(failed_dep_infos) > 0 :
        failed_dep_info = failed_dep_infos.pop()
        failed_dep = get_depend_module_name(failed_dep_info)
        if failed_dep in success_deps:
            print(f"构建 {failed_dep} 成功")
            continue
        else:
            print(f"构建 {failed_dep}")
        
        if failed_dep not in project_infos:
            print(f"依赖 {failed_dep} 找不到,请检查本地工程是否齐全")
            exit(1)
        
        project_info = project_infos[failed_dep]
        project_dir = project_info['dir']
        project_profile = project_info['profile']
        project_validate = project_info['validate']
        project_step = project_info['step']
        
        project_path = f"{work_dir}/{project_dir}"
        os.chdir(project_path)
        # pom.xml
        if project_validate:
            cmd_pom_file = f" -f effective-profile-pom.xml "
        else:
            cmd_pom_file = ""
        # profile
        if project_profile:
            cmd_profile = f" -P{project_profile} "
        else:
            cmd_profile = ""
        # pl
        if failed_dep in param_exclusions:
            exclude_pl = param_exclusions[failed_dep]
        else:
            exclude_pl = None
        if project_step == 0:
            cmd_pl = ""
            if exclude_pl:
                cmd_pl = f" -pl '{exclude_pl}' "
        else:
            if exclude_pl:
                cmd_pl = f"  -pl '{failed_dep},{exclude_pl}' "
            else:
                cmd_pl = f"  -pl '{failed_dep}' "
        # offline
        if offline:
            cmd_offline = " -o "
        else:
            cmd_offline = ""
        # setting
        if param_setting:
            cmd_setting = f" -s {param_setting} "
        else:
            cmd_setting = ""
        # ###################################################################
        cmd_validate = f" && mvn {cmd_profile} validate {cmd_setting}"
        cmd_clean_install = f" && mvn {cmd_pom_file} {cmd_profile} clean install -Dmaven.test.skip=true {cmd_pl} {cmd_setting} {cmd_offline} "
        if 'com.tianjiaguo.site:test' == failed_dep:
            cmd_dploy_pl_pom = ""
            cmd_deploy_root_pom = ""
        else:
            cmd_dploy_pl_pom = f" && mvn {cmd_pom_file} {cmd_profile} deploy -Dmaven.test.skip=true {cmd_pl} {cmd_setting} {cmd_offline} "
            cmd_deploy_root_pom = f" && mvn {cmd_pom_file} {cmd_profile} deploy -Dmaven.test.skip=true -N {cmd_setting} {cmd_offline} "
        
        command = f"set -e && set -x && cd {project_path} {cmd_validate} {cmd_clean_install} {cmd_dploy_pl_pom} {cmd_deploy_root_pom} "  # 替换为你要执行的命令
        process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        is_dep_err = False
        while True:
            output = process.stdout.readline()
            if output == '' and process.poll() is not None:
                break
            if output:
                out_line = output.strip()
                print(out_line)
                artifacts_list = extract_artifacts(out_line)
                if artifacts_list and len(artifacts_list) > 0:
                    is_dep_err = True
                    failed_dep_infos.append(failed_dep_info)
                    for artifact in artifacts_list:
                        print(f"获取依赖 {artifact} 失败")
                        failed_dep_infos.append(artifact)
        if process.returncode != 0:# {os.path.join({work_dir},{project_dir})} 
            print(f"执行 {failed_dep} 编译程序失败,执行依赖构建脚本,脚本:{command}")
            # subprocess.Popen("ls .", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            if not is_dep_err:
                print(f'执行 {failed_dep} 失败,需要处理编译问题')
                exit(0)
        else:
            print(f"执行 {failed_dep} 编译程序成功")
            success_deps[failed_dep] = True
        if len(failed_dep_infos) > 0:
            print(f'继续编译...')
            
    
    
    
This entry was posted in 未分类. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.