对于项目中的资源检查需求,需要能够实现简单且方便地配置、自动化地执行,并且能够及时地定位相关人员。基于这个需求,我开源了一个资源合规扫描工具ResScannerUE ,配置文档:UE 资源合规检查工具 ResScannerUE 。 本篇文章将介绍如何通过该工具,实现资源扫描的自动化,并提供了与Git结合的方式进行增量检测支持,使用Commandlet在CI平台上实现Content内容变更的自动触发并执行检测规则,并能够定位到出问题资源最近提交人,实现精确定位,实时发送扫描报告至企业微信,提醒通知相关人员进行处理。 另外,我还提供了基于Git的Pre-Commit Hook实现,可以在提交之前检测本次提交是否具有不合规资源,并禁止提交,避免问题资源污染远程仓库。整体方案经过了精心设计和大量体验优化、增强自动化支持,可以非常方便地配置、接入,能够实现各种资源扫描的需求。
在插件中能够配置针对每个规则的路径、资源列表,同时也提供了针对所有规则使用的全局的资源路径、资源列表,并且能支持屏蔽每个规则中的配置。
控制好全局资源 ,并且屏蔽单个规则中的资源配置,能够精确地控制对哪些资源进行所有规则的检查,这也是要实现自动化资源扫描 的基础。
对于实现自动化资源扫描 ,根据需求不同,可以拆分成两个部分:
全量扫描
增量扫描
全量扫描自不必多说,只需要在插件中指定全局资源/Game
等Root目录,屏蔽规则中配置得资源,导出一份配置即可。增量扫描,我在插件中提供了两种方案:基于文件列表的检测、基于Git版本检测。
基于文件列表检测 如果想要动态地指定检测哪些资源而不是在配置中预先指定,我对ResScanner
的commandlet提供了指定文件列表的功能,在执行参数中加入-filecheck
与-filelist
来指定文件列表,通过,
分割文件。
1 UE4Editor.exe Project.uproject -run="ResScanner" -config="ScannerConfig.json" -filecheck -filelist="Asset/A.uasset,Asset/B.uasset"
这种方式需要在外部传递所有要检测的文件。
基于Git版本比对检测 通常,工程仓库会分为两部分:
代码仓库:基础工程结构、代码、配置等
资源仓库:Content
用于隔离美术与程序的工作环境不相互影响。对于大多数资源管理的情况,会使用Git等版本控制工具进行管理。所以需要把Git管理的资源仓库 中每次提交的版本中的文件列表提取出来,传递给ResScanner进行特定资源的检查。
原始的Git进行两个Commit之间的比对可以用以下命令:
1 git diff --name-only HEAD~ HEAD
能够用来获取最近的两次提交之间的文件变动,从而获取到原始的变更文件列表。
通常,这部分逻辑会通过python等脚本来实现,但是这种需求还是具有通用性,所以我直接在ResScannerUE 提供了支持,当我们想要使用Git版本来进行规则的检测时,可以通过插件开启提供的GitChecker
。
Git的版本对比、提交人获取,基于我之前开源的一个工具:GitControllerUE ,是UE中的一个插件,能够获取Git仓库的版本信息。
基于它,ResScannerUE 能够自动化地扫描最近地提交记录变更的文件,检测是否有不合规的提交。默认情况下会检测最近的一次提交,但是,如果想要指定更多次的提交,只需要在配置中修改启示Commit的HASH值即可。
以下配置为例,扫描最近的一次Git提交的文件进行贴图的命名检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 { "configName" : "全量资源扫描" , "bByGlobalScanFilters" : true , "bBlockRuleFilter" : true , "globalScanFilters" : { "filters" : [ ] , "assets" : [ ] } , "globalIgnoreFilters" : [ ] , "gitChecker" : { "bGitCheck" : true , "bRecordCommiter" : true , "bDiffCommit" : true , "repoDir" : { "path" : "[PROJECT_CONTENT_DIR]" } , "beginCommitHash" : "HEAD~" , "endCommitHash" : "HEAD" , "bUncommitFiles" : false } , "bUseRulesTable" : false , "importRulesTable" : "" , "scannerRules" : [ { "ruleName" : "贴图命名规范" , "ruleDescribe" : "贴图需以T_开头" , "bEnableRule" : true , "priority" : "GENERAL" , "scanFilters" : [ ] , "scanAssetType" : "Class'/Script/Engine.Texture2D'" , "recursiveClasses" : true , "nameMatchRules" : { "rules" : [ { "matchMode" : "StartWith" , "matchLogic" : "Necessary" , "rules" : [ { "ruleText" : "T_" , "bReverseCheck" : false } ] } ] , "bReverseCheck" : true } , "pathMatchRules" : { "rules" : [ ] , "bReverseCheck" : false } , "propertyMatchRules" : { "matchRules" : [ ] , "bReverseCheck" : false } , "customRules" : [ ] , "ignoreFilters" : { "filters" : [ ] , "assets" : [ ] } , "bEnablePostProcessor" : false , "postProcessors" : [ ] } ] , "bSaveConfig" : true , "bSaveResult" : true , "savePath" : { "path" : "[PROJECT_SAVED_DIR]/ResScanner" } , "bStandaloneMode" : false , "additionalExecCommand" : "" }
只需要在执行Commandlet时指定该Config即可:
1 UE4Editor.exe Project.uproject -run="ResScanner" -config="Full_Scan_Config.json"
另外,在GitChecker
的配置中,可以开启是否记录文件的提交者,这里我做了一个Trick操作,在扫描结果的存储结构中,是有两个数组的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 USTRUCT (BlueprintType)struct FFileCommiter { GENERATED_USTRUCT_BODY () public : UPROPERTY (EditAnywhere,BlueprintReadWrite) FString File; UPROPERTY (EditAnywhere,BlueprintReadWrite) FString Commiter; } USTRUCT (BlueprintType)struct FRuleMatchedInfo { GENERATED_USTRUCT_BODY () public : UPROPERTY (EditAnywhere,BlueprintReadWrite) TArray<FString> AssetPackageNames; UPROPERTY (EditAnywhere,BlueprintReadWrite) TArray<FFileCommiter> AssetsCommiter; };
扫描的结果会被序列化成json,默认情况下,这两个数组都会被序列化出来,但是这并不是我们的需求,在关闭bRecordCommiter
时只序列化扫描出来资源的AssetPackageNames
,反之则序列化AssetsCommiter
,可以通过控制FProperty
是否具有CPF_Transient
的FLAG来控制,如果包含了该FLAG,则该Property就不会被序列化。
所以,我在每次序列化扫描结果之前会进行一次标记,根据配置来决定给哪个属性添加CPF_Transient
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 static void SetSerializeTransient (bool bCommiter) { FString NotSerializeName = bCommiter ? TEXT ("AssetPackageNames" ) : TEXT ("AssetsCommiter" ); for (TFieldIterator<FProperty> PropertyIter (FRuleMatchedInfo::StaticStruct ());PropertyIter;++PropertyIter) { FProperty* PropertyIns = *PropertyIter; if (NotSerializeName.Equals (*PropertyIns->GetName ())) { PropertyIns->SetPropertyFlags (CPF_Transient); } } } static void ResetTransient () { TArray<FString> NotSerializeNames = {TEXT ("AssetsCommiter" ),TEXT ("AssetPackageNames" )}; for (TFieldIterator<FProperty> PropertyIter (FRuleMatchedInfo::StaticStruct ());PropertyIter;++PropertyIter) { FProperty* PropertyIns = *PropertyIter; if (NotSerializeNames.Contains (*PropertyIns->GetName ()) && PropertyIns->HasAnyPropertyFlags (CPF_Transient)) { PropertyIns->ClearPropertyFlags (CPF_Transient); } } }
在每次序列化之前调用这两个函数即可:
1 2 FRuleMatchedInfo::ResetTransient (); FRuleMatchedInfo::SetSerializeTransient (GetScannerConfig ()->GitChecker.bGitCheck && GetScannerConfig ()->GitChecker.bRecordCommiter);
从而实现按需序列化的效果:
Git待提交文件检测 上一节介绍了已经提交之后的两个Commit版本之间变动资源的检测,如果资源未被Commit,则需要利用另一种机制。
待提交文件,指本地仓库有修改,但未被commit的文件列表:
1 2 3 4 5 6 7 8 $ git status On branch master Your branch is behind 'origin/master' by 49 commits, and can be fast-forwarded. (use "git pull" to update your local branch) Changes to be committed: (use "git restore --staged <file>..." to unstage) modified: Assets/Scene/BaseMaterials/4X4_MRA.uasset
基于待提交的文件检测,可以实现提交之前进行资源扫描,不合规禁止提交。基于pre-commit hook的实现,后文会详细介绍。
插件中在GitChecker
配置中提供了选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 "gitChecker" : { "bGitCheck" : true , "bRecordCommiter" : true , "bDiffCommit" : true , "repoDir" : { "path" : "[PROJECT_CONTENT_DIR]" } , "beginCommitHash" : "HEAD~" , "endCommitHash" : "HEAD" , "bUncommitFiles" : false }
基于Git的检测有两种模式:
bDiffCommit :基于两个Commit HASH的版本比对,对差异资源进行扫描
bUncommitFiles :扫描待提交资源
两种模式可以按需启用,通常,在定时扫描,会启用bDiffCommit
,提交前的检测会使用bUncommitFiles
。
Commandlet参数替换 为了更加方便地通过Commandlet来执行扫描任务,我同时也为ResScannerUE 的commandlet增加了参数替换功能。可以在指定配置文件的同时,通过命令行参数动态替换配置文件中的选项,实现更加强大的自动化能力。
如通过命令行关闭全局资源配置、是否存储扫描结果等:
1 UE4Editor.exe Project.uproject -run="ResScanner" -config="Full_Scan_Config.json" -bByGlobalScanFilters=false -savePath.path="D:\"
以及可以指定Git版本扫描的起始Commit HASH:
1 UE4Editor.exe Project.uproject -run="ResScanner" -config="Full_Scan_Config.json" -gitChecker.bGitCheck=true -gitchecker.beginCommitHash=HEAD~ -gitchecker.endCommitHash=HEAD
插件中的属性都可以通过-ProjectName=Value
的方式来进行指定。这种方式就能实现只需提供一个基础配置,通过命令行来动态控制配置中的参数的行为,在集成至CI系统时,可以暴露更多的可选参数。
企业微信提醒 基于ResScannerUE 的commandlet机制,可以十分方便地接入企业微信提醒,在提交资源后,自动对提交资源的进行检测,并发送群提醒:
整个流程只需要:
python拉起ResScannerUE 的Commandlet
通过commandlet的返回值检测是否有命中资源(return -1
)
读取Saved/ResScanner/*_result.json
文件
使用python发送企业微信消息
我这里提供一个示例:
resscanner_notice.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import osimport sysimport chardetimport argparseimport codecsimport subprocessfrom notice_robot_py import robotcommandlets_args = argparse.ArgumentParser(description="do commandlet" ) commandlets_args.add_argument('--enginedir' ,help ='engine root directory' ) commandlets_args.add_argument('--projectdir' ,help ='project root directory' ) commandlets_args.add_argument('--projectname' ,help ='project name,match projectname.uproject' ) commandlets_args.add_argument('--config' ,help ='config' ) commandlets_args.add_argument('--notice' ,default="false" ,help ='is enable notice to wechat' ) wechat_robot_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" MentionList = ["" ] def notice_to_wechat (Msg,bToWechat ): URlStr = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s" % (wechat_robot_id) MsgType = "text" if bToWechat: robot.Robot.set_robot_config(URlStr, MsgType, MentionList) res = robot.Robot.notice(Msg) if res is not None : print (res) def get_args_by_name (parser_args,ArgName ): args_pairs = parser_args.__dict__ for key,value in args_pairs.items(): if key == ArgName: return value def ue_commandlet (cmd_params ): parser_args = commandlets_args.parse_args() engine_dir = get_args_by_name(parser_args,"enginedir" ) project_dir = get_args_by_name(parser_args,"projectdir" ) project_name = get_args_by_name(parser_args,"projectname" ) engine_cmd_exe_path = "\"%s/Engine/Binaries/Win64/UE4Editor-cmd.exe\"" % (engine_dir) uproject_path = "\"%s/%s.uproject\"" % (project_dir,project_name) final_cmdlet = "%s %s %s" % (engine_cmd_exe_path,uproject_path,cmd_params) print (final_cmdlet) ps = subprocess.Popen(final_cmdlet) ps.wait() exit_code = ps.returncode return exit_code def main (): parser_args = commandlets_args.parse_args() config = get_args_by_name(parser_args,"config" ) project_dir = get_args_by_name(parser_args,"projectdir" ) result_saved_dir = os.path.join(project_dir,"Saved" ,"ResScanner" ,"ResScanner_result.json" ) cmd_params = "-run=ResScanner -config=\"%s\"" % (config) exit_code = ue_commandlet(cmd_params) if exit_code != 0 : if os.path.exists(result_saved_dir): with open (result_saved_dir, encoding='utf-8' , errors='ignore' ) as f: s = f.read() lines = s.splitlines() for line in lines: splited_line = line.rsplit(',' ,1 ) if len (splited_line) == 2 and splited_line[1 ]: match_role = splited_line[1 ] match_role = match_role.strip() if match_role not in MentionList: MentionList.append(match_role) msg = "触发资源扫描,具有不合规资源\n%s" %(s) bIsNotice = str2bool(get_args_by_name(parser_args,"notice" )) notice_to_wechat(msg,bIsNotice) f.close() if __name__ == "__main__" : main()
使用方法:
1 2 3 4 5 6 python3 resscanner_notice.py --enginedir C:\Program Files\Epic Games\UE_4.26 --projectdir D:\UnrealProjects\Client --projectname PROJECT_NAME --config D:\UnrealProjects\Client\ResScannerConfig.json --notice true
传递这几个参数,即可实现拉起ResScanner进行扫描,并可以将结果通知到企业微信。
Pre-Commit Hook 前面的自动化流程,都是基于提交之后触发检测,发送报告的形式。本质还是先出现问题 、再解决问题 的流程。
所以,能否把错误资源对主干的污染消灭在萌芽里,让每个人提交之前就检测资源是否合规,不合规的就禁止提交呢?
答案是肯定的,可以利用ResScannerUE 提供的GitChecker.bUncommitFiles
(详见Git待提交文件检测 )与Git提供的Pre-Commit Hook的机制结合实现:
在Commit时执行Hook脚本
Hook脚本拉起ResScannerUE的Commandlet,进行待提交资源 的合规扫描(GitChecker.bUncommitFiles = true
)
获取Commandlet退出代码,若具有不合规资源,停止commit,提示不合规资源
Git Pre-Commit Hook要执行的脚本如下:
res_rescanner_pre_commit_hook.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import osimport sysimport subprocessdef conv_encoding (content_str ): return content_str def main (): engine_cmd_exe_path = "D:/UnrealEngine/Engine/Engine/Binaries/Win64/UE4Editor-cmd.exe" uproject_path = "D:/UnrealProjects/Client/FGame.uproject" config_path = "D:/UnrealProjects/Client/CommandletConfig/PRE_COMMIT_HOOK.json" cmd_params = "-run=ResScanner -config=\"%s\" -bDiffCommit=false -bUncommitFiles=true" % (config_path) result_saved_dir = "D:/UnrealProjects/Client/Saved/ResScanner/PRE_COMMIT_HOOK_result.json" final_cmdlet = "%s %s %s" % (engine_cmd_exe_path,uproject_path,cmd_params) FNULL = open (os.devnull, 'w' ) ps = subprocess.Popen(conv_encoding(final_cmdlet),stdout=FNULL,stderr=subprocess.STDOUT) ps.wait() exit_code = ps.returncode if exit_code != 0 : if os.path.exists(result_saved_dir): with open (result_saved_dir) as f: s = f.read() lines = s.splitlines() for line in lines: splited_line = line.rsplit(',' ,1 ) if splited_line[1 ]: MentionList.append(splited_line[1 ]) msg = "本次提交具有不合规资源:\n%s" %(s.decode(encoding = "utf-8-sig" ).encode('utf-8' )) print (conv_encoding(msg)) f.close() else : print ("%s file not found!" % (result_saved_dir)) exit(exit_code) if __name__ == "__main__" : main()
将其放在Content
根目录,在Content/.git/hooks
下创建pre-commit
文件,填入以下sh脚本即可:
1 2 3 4 #!/bin/sh python res_rescanner_pre_commit_hook.py exitCode="$?" exit $exitCode
执行效果:
结合一些Git GUI客户端可以实现友好的提示效果:
总结 本篇文章介绍了基于ResScannerUE 实现资源扫描自动化的工程实践,可以方便地将资源扫描接入开发管线的各个流程中。