#!/usr/bin/env python # coding:utf-8 # python3 Tools/build_package/build_package.py -m Debug -p Android -u "/Applications/Unity/Hub/Editor/2020.3.48f1c1/Unity.app/Contents/MacOS/Unity" -a /Volumes/SeanSSD/gitea_ap_art_assets -r /Volumes/SeanSSD/unity_ap_bundle_resource/Bundles -l "test" -d false import datetime import json import os import shutil import sys import time import upload_firebase_storage import psd_convert.parse_psd as parse_psd import config_convert.gen_levels_db as config_gen curr_dir = os.path.split(os.path.abspath(__file__))[0] print("curr_dir = " + curr_dir) sys.path.append(os.path.join(curr_dir, "../")) sys.path.append(os.path.join(curr_dir, "../ipm")) sys.path.append(os.path.join(curr_dir, "../config_convert")) import config import utils as utils from config_convert.game_play_type import MainPlayType from optparse import OptionParser from ipm.wechat_alert import wechat_alert alert = wechat_alert() # input_old_hash = '' # input_new_hash = '' # if len(sys.argv) > 1: # input_old_hash = sys.argv[1] # if input_old_hash == 'none' or input_old_hash == '_': # input_old_hash = '' # if len(sys.argv) > 2: # input_new_hash = sys.argv[2] # if input_new_hash == 'none' or input_new_hash == '_': # input_new_hash = '' # 主机IP IP = "" # Unity执行文件路径 UNITY_PATH = "/Applications/Unity/Hub/Editor/2021.3.45f1/Unity.app/Contents/MacOS/Unity" # 项目路径 PROJECT_PATH = os.path.abspath(os.path.join(curr_dir, "../..")) # resource目录,最终要提交到CDN地文件 RESOURCE_ROOT = os.path.abspath(os.path.join(curr_dir, "../../Bundles")) # cwd = os.getcwd() # print(cwd) if PROJECT_PATH == "": print("未指定 UNITY_PATH 和 PROJECT_PATH") exit() def check_dir(path): if not os.path.exists(path): os.makedirs(path) def isGitCommitFileExist(): return os.path.exists(opts.git_commit_hash_file) def writeGitCommitLog(git_log_hash): # 写入git_commit.txt文件 with open(opts.git_commit_hash_file, 'w') as f: f.write(git_log_hash) def recordNewGitHash(git_log_hash): # 文件末尾新增一行记录新的git hash with open(opts.git_hash_change_record_file, 'a') as f: f.write(git_log_hash + '\n') def isRecordGitHashFileExist(): return os.path.exists(opts.git_hash_change_record_file) def getCurrentGitHash(): current_commit_hash = '' if isGitCommitFileExist(): with open(opts.git_commit_hash_file, 'r') as f: current_commit_hash = f.read() print(f"current hash:{current_commit_hash}") return current_commit_hash def getNewGitHash(): sh1 = f"cd {opts.assets}" sh2 = f"git log -1 --pretty=format:\"%H\"" f = os.popen(f"{sh1} && {sh2}") new_commit_hash = f.read() print(f"new hash:{new_commit_hash}") return new_commit_hash # endregion def getModifyPic(git_dir): pass if opts.old_hash != '' and opts.new_hash != '': current_commit_hash = opts.old_hash new_commit_hash = opts.new_hash else: current_commit_hash = getCurrentGitHash() new_commit_hash = getNewGitHash() if not isGitCommitFileExist(): writeGitCommitLog(new_commit_hash) current_commit_hash = new_commit_hash if not isRecordGitHashFileExist(): recordNewGitHash(new_commit_hash) if current_commit_hash == new_commit_hash: config.notification_helper.append_msg("当前commit hash与新commit hash相同,不需要构建") return None sh1 = f"cd {git_dir}" sh2 = f"git log {new_commit_hash}...{current_commit_hash} --name-status" f = os.popen(f"{sh1} && {sh2}") modify_content_lines = f.readlines() return modify_content_lines def getModifyIdList(modify_content_lines): assetLevelIdList = [] # thumLevelIdList = [] if modify_content_lines is None: return assetLevelIdList#, thumLevelIdList for line in modify_content_lines: levelId = None if line.startswith('D'): continue if '.zip' not in line: continue if '\t' in line and '\n' in line: levelId = line.split('\t')[-1].split('/')[-1].split('.')[0] else: levelId = line.split('/')[-1].split('.')[0] if levelId is None: continue if '.zip' in line and levelId not in assetLevelIdList: assetLevelIdList.append(levelId) # if '.jpg' in line and levelId not in thumLevelIdList: # thumLevelIdList.append(levelId) return assetLevelIdList#, thumLevelIdList def generateLevelIdListByGitLog(): pass global _levelModifiedIdList_ modify_content_lines = getModifyPic(opts.assets) _levelModifiedIdList_ = getModifyIdList(modify_content_lines) if len(opts.special) > 0 and opts.special != '0': manual_modify_level_list = opts.special.split(',') for level_id in manual_modify_level_list: if level_id not in _levelModifiedIdList_: _levelModifiedIdList_.append(level_id) print(f'资源修改列表:{str(_levelModifiedIdList_)}') config.notification_helper.append_msg(f"需要打包的资源:总共{len(_levelModifiedIdList_)}个 所有id: {str(_levelModifiedIdList_)}") def update_new_git_hash(): if opts.old_hash != '' and opts.new_hash != '': current_commit_hash = opts.old_hash new_commit_hash = opts.new_hash else: current_commit_hash = getCurrentGitHash() new_commit_hash = getNewGitHash() if new_commit_hash != '' and new_commit_hash != current_commit_hash: writeGitCommitLog(new_commit_hash) recordNewGitHash(new_commit_hash) config.notification_helper.append_msg( f"[step] update git hash success, old hash:{current_commit_hash}, new hash:{new_commit_hash}") else: config.notification_helper.append_msg("[Error] current_commit_hash is empty") def build_package(opts, modify_files): """ 打资源包 :param opts: 打包参数 :param modify_files: 修改文件 :return: 无 """ time_build_package_start = time.time() params = "AppVersion=" + opts.appversion now = datetime.datetime.now() build_number = "{:2d}{:02d}{:02d}{:02d}{:02d}".format(int(now.year - 2000), int(now.month), int(now.day), int(now.hour), int(now.minute)) params = params + " BuildVersion=" + build_number params = params + " ResVersion=" + opts.resversion params = params + " Platform=" + opts.platform params = params + " BuildType=" + opts.buildtype params = params + " Mode=" + opts.mode params = params + " ModifyFiles=" + json.dumps(modify_files).replace(" ", "") params = params + " ResourceRoot=" + RESOURCE_ROOT print(params) cmd = UNITY_PATH + " -quit " + " -batchmode " + " -projectPath " + PROJECT_PATH + " -executeMethod BuildBundlesHelper.BuildBundles " + params + " -logFile " + PROJECT_PATH + "/Logs/build_log.txt" print(cmd) # 调用Unity打包 os.system(cmd) error = '' is_build_success = False # 热更Bundles文件输出文件夹(Unity输出的源文件) output_dir = f"{RESOURCE_ROOT}/{opts.platform}" bundle_file_path = output_dir + "/{}/" + build_number if (opts.platform == "Android" or opts.platform == "iOS") and os.path.exists(output_dir): for asset_id in modify_files: asset_level_dir = bundle_file_path.format(asset_id) asset_level_thum_dir = bundle_file_path.format(f"{asset_id}_thum") isAppendNewLine = False if not os.path.exists(asset_level_dir): error = error + f"{asset_id}关卡资源打包失败 " isAppendNewLine = True if not os.path.exists(asset_level_thum_dir): error = error + f"{asset_id}缩略图资源打包失败 " isAppendNewLine = True if isAppendNewLine: error = error + "\n" if error == '': is_build_success = True time_build_package_end = time.time() if is_build_success: config.notification_helper.append_msg(f"{opts.platform}平台 YooAsset资源构建成功, cost time:{(time_build_package_end - time_build_package_start):.0f}s") else: config.notification_helper.append_msg(f"{opts.platform}平台 YooAsset资源构建失败, error:{error}, 请前往Jenkins查看具体日志") config.notification_helper.append_at_people(config.at_zhouzhuo) return is_build_success, bundle_file_path def check_params(opts): print(opts) msg = '' build_types = ['BuildIn_All', 'Builtin_Hotupdate', 'BuildBundle', 'BuildPackage'] if opts.buildtype not in build_types: msg = msg + '打包类型设置错误\n' return msg if __name__ == '__main__': # region 创建打包的命令参数 parser = OptionParser() # parser.add_option("-v", "--appversion", dest="appversion", help=('app版本, 显示在app详情内的')) # parser.add_option("-r", "--resversion", dest="resversion", help=('资源版本版本')) # parser.add_option("-b", "--buildtype", dest="buildtype", help=('打包类型, 选项: buildin/buildplayer/buildbundle')) # parser.add_option("-g", "--aab", dest="aab", help=('是否为上传Google Play的aab包')) parser.add_option("-m", "--mode", dest="mode", help=('打包模式, 选项: Release/Debug')) parser.add_option("-p", "--platform", dest="platform", help=('打包平台 选项: Android/iOS')) # parser.add_option("-u", "--unityexe", dest="unityexe", help=('Unity可执行文件')) # parser.add_option("-a", "--assets", dest="assets", help=('美术资源路径')) # parser.add_option("-r", "--resources", dest="resources", help=('导出资源路径')) # parser.add_option("-l", "--log", dest="log", help=('美术资源最新的git log')) # parser.add_option("-d", "--upload", dest="upload", help=('是否上传')) parser.add_option("-s", "--special", dest="special", help=('指定构建的资源')) parser.add_option("-n", "--new-hash", dest="new_hash", help=('最新的hash')) parser.add_option("-o", "--old-hash", dest="old_hash", help=('老的hash')) (opts, args) = parser.parse_args() opts.appversion = "1.0.0" opts.resversion = "1" opts.buildtype = "BuildBundle" opts.unityexe = "/Applications/Unity/Hub/Editor/2021.3.45f1/Unity.app/Contents/MacOS/Unity" opts.upload = "true" opts.log = "log/build.log" opts.assets = "/Volumes/Predator/find-object/find-object-art" opts.resources = "/Volumes/Predator/find-object/find-object-bundle-resource" opts.workspace_path = config.machine_env_config.CD_MAC_STUDIO.value['workspace_path'] opts.git_commit_hash_file = os.path.join(opts.workspace_path, f'find_object_git_commit_hash_{opts.platform}.txt') opts.git_hash_change_record_file = os.path.join(opts.workspace_path, f'find_object_git_hash_change_record_{opts.platform}.txt') if opts.new_hash is None or opts.new_hash == 'none' or opts.new_hash == '_': opts.new_hash = '' if opts.old_hash is None or opts.old_hash == 'none' or opts.old_hash == '_': opts.old_hash = '' # 本地自测适用 # opts.aab = "false" opts.mode = "Debug" # opts.platform = "Android" # opts.assets = "/Users/a0729/gogs.git/find-object-art" # opts.resources = "/Users/a0729/gogs.git/find-object-bundle-resource" # opts.unityexe = "/Applications/Unity/Hub/Editor/2021.3.45f1/Unity.app/Contents/MacOS/Unity" # opts.log = "log/build.log" # opts.upload = "false" opts.special = "main_30point_qxy20250826_half_1" # opts.special = "0" opts.platform = "Android" # opts.old_hash = "d25a33127646e062e4ef1a760fae43e50393b4c5" # opts.new_hash = "fdde3dbb2bcf2cc04a28197afef5c19bed5df237" print(f'test-- {opts}') is_upload = True if opts.upload == "true" else False check_msg = check_params(opts) if check_msg != "": check_msg = check_msg + '请使用 python Tools/BuildPackage/build_package.py -h 查看使用规则' print(check_msg) time_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") title = '客户端构建失败' content = time_str + '\n\n > ' + check_msg print(content) exit() if opts.unityexe != "": UNITY_PATH = opts.unityexe RESOURCE_ROOT = os.path.abspath(os.path.join(curr_dir, "../../Bundles")) # endregion mainPlayType = MainPlayType() # region 清空各玩法软链接资源列表 try: for gameplay in mainPlayType.all_main_play_type_list: for root, dirs, files in os.walk(os.path.join(PROJECT_PATH, "Assets/AssetRaw/UIRaw/Raw/Level/{}".format(gameplay))): for dir in dirs: os.unlink(os.path.join(root, dir)) except Exception as e: print(e) for gameplay in mainPlayType.all_main_play_type_list: for root, dirs, files in os.walk(os.path.join(PROJECT_PATH, "Assets/AssetRaw/UIRaw/Raw/Level/{}".format(gameplay))): for file in files: os.remove(os.path.join(root, file)) try: for gameplay in mainPlayType.all_main_play_type_list: for root, dirs, files in os.walk(os.path.join(PROJECT_PATH, "Assets/AssetRaw/UIRaw/Raw/Thum/{}".format(gameplay))): for file in files: os.unlink(os.path.join(root, file)) except Exception as e: print(e) for gameplay in mainPlayType.all_main_play_type_list: for root, dirs, files in os.walk(os.path.join(PROJECT_PATH, "Assets/AssetRaw/UIRaw/Raw/Thum/{}".format(gameplay))): for file in files: os.remove(os.path.join(root, file)) # endregion # region 检索需要需要打包的关卡 modify_files = [] # if opts.special != "0": # modify_files = opts.special.split(",") # else: #暂时不用这个方案 # 获取最近6小时内的提交记录,用于解析psd # platform = "android" if opts.platform == "Android" else "ios" # with open(os.path.join(opts.assets, f"git_{platform}_commit_change_files.txt"), "r") as f: # lines = f.readlines() # for aline in lines: # aline = aline.replace("\"", "").replace("\n", "").replace("\r", "") # if aline.endswith(".zip"): # base_dir = aline.split("/")[0] # file_name = os.path.basename(aline) # asset_id = file_name.split(".")[0] # if asset_id not in modify_files: # modify_files.append(asset_id) #获取git 提交来得到修改的关卡 generateLevelIdListByGitLog() if len(_levelModifiedIdList_) <= 0: config.notification_helper.append_msg('没有需要构建的资源') else: modify_files = _levelModifiedIdList_ print(f"modify_files = {modify_files}") # alert.alert(f"构建资源列表:{str(modify_files)}") tmstp1 = time.time() # 解析psd => Unity工程 for gameplay in mainPlayType.all_main_play_type_list: gameplay_modify_file_list = [s for s in modify_files if gameplay in s] if len(gameplay_modify_file_list) > 0: gameplay_all_parsed_assets = parse_psd.parse_psd(f"{opts.assets}/Level/{gameplay}", opts.resources, gameplay_modify_file_list, opts.platform) tmstp2 = time.time() print(f"{len(modify_files)}个资源, 解析psd耗时:{tmstp2 - tmstp1}") config.notification_helper.append_msg(f"解析{len(modify_files)}个psd资源耗时:{(tmstp2 - tmstp1):.0f}s") # endregion # region 建立需要打包的资源软连接 metas = [] for asset_id in modify_files: asset_gameplay = None for gameplay in mainPlayType.all_main_play_type_list: if gameplay in asset_id: asset_gameplay = gameplay break if asset_gameplay is not None: src = os.path.join(opts.resources, f"Raw/Level/{asset_gameplay}/{asset_id}") dest = os.path.join(PROJECT_PATH, f"Assets/AssetRaw/UIRaw/Raw/Level/{asset_gameplay}/{asset_id}") utils.mkdirs(os.path.dirname(dest)) if os.path.exists(src): os.symlink(src, dest) src = os.path.join(opts.resources, f"Raw/Level/{asset_gameplay}/{asset_id}.meta") dest = os.path.join(PROJECT_PATH, f"Assets/AssetRaw/UIRaw/Raw/Level/{asset_gameplay}/{asset_id}.meta") if os.path.exists(src): os.symlink(src, dest) else: metas.append(["Level/{}".format(asset_gameplay), dest]) src = os.path.join(opts.resources, f"Raw/Thum/{asset_gameplay}/{asset_id}_thum.png") dest = os.path.join(PROJECT_PATH, f"Assets/AssetRaw/UIRaw/Raw/Thum/{asset_gameplay}/{asset_id}_thum.png") utils.mkdirs(os.path.dirname(dest)) if os.path.exists(src): os.symlink(src, dest) src = os.path.join(opts.resources, f"Raw/Thum/{asset_gameplay}/{asset_id}_thum.png.meta") dest = os.path.join(PROJECT_PATH, f"Assets/AssetRaw/UIRaw/Raw/Thum/{asset_gameplay}/{asset_id}_thum.png.meta") if os.path.exists(src): os.symlink(src, dest) else: metas.append(["Thum/{}".format(asset_gameplay), dest]) print(f"{asset_gameplay}玩法,{asset_id}资源建立软链接") # endregion # region 获取资源不同点个数 asset_find_num_dict = {} for asset_id in modify_files: asset_gameplay = None for gameplay in mainPlayType.all_main_play_type_list: if gameplay in asset_id: asset_gameplay = gameplay break if asset_gameplay is not None: src = os.path.join(opts.resources, f"Raw/Level/{asset_gameplay}/{asset_id}/titem") find_num = 0 if os.path.exists(src): for file in os.listdir(src): if ".meta" in file: continue if f"{asset_id}_titem" in file: find_num = find_num + 1 else: print(f"关卡资源ID asset_gameplay:{asset_gameplay},asset_id={asset_id} 不存在titem目录") asset_find_num_dict[asset_id] = find_num print(f"关卡资源ID asset_id = {asset_id}, 寻找物品个数find_num = {find_num}") # endregion # region 开始打包 以及上传资源 tmBuildPackage1 = time.time() is_build_success, ab_path = build_package(opts, modify_files) tmBuildPackage2 = time.time() print(f"{len(modify_files)}个资源, unity打包耗时:{tmBuildPackage2 - tmBuildPackage1}") if is_build_success and is_upload: print(f"开始上传ab包到firebase") storage_path = "Bundles/{}".format(opts.platform) # 上传ab包到firebase tmUploadPackage1 = time.time() upload_firebase_storage.upload_package_assets(ab_path, modify_files, storage_path, asset_find_num_dict, {config.meta_encryption: "stream", config.meta_spriteatlas: "1"}) tmUploadPackage2 = time.time() print(f"{len(modify_files)}个资源, firebase上传耗时:{tmUploadPackage2 - tmUploadPackage1}") config.notification_helper.append_msg(f"上传{len(modify_files)}个资源到firebase耗时:{(tmUploadPackage2 - tmUploadPackage1):.0f}s") update_new_git_hash() # 遍历ab包,生成所有关卡表 tmUpdateSheet1 = time.time() config_gen.update_all_levels_google_sheet(opts.platform) tmUpdateSheet2 = time.time() config.notification_helper.append_msg(f"{opts.platform}平台所有资源表更新完成!") print(f"{len(modify_files)}个资源, 更新文档耗时:{tmUpdateSheet2 - tmUpdateSheet1}") # endregion # region 打包完成后,将meta文件保存到opts.resources for meta in metas: print(f"meta = {meta}") print(f"copy {meta[1]} to {os.path.join(opts.resources, 'Raw', meta[0])}") shutil.copy(meta[1], os.path.join(opts.resources, 'Raw', meta[0])) # endregion config.notification_helper.append_end_msg() alert.alert(config.notification_helper.get_msg(), config.notification_helper.get_people_list())