基于ResScannerUE的资源检查自动化实践

Automated Resource Scanning and Inspection with ResScannerUE

对于项目中的资源检查需求,需要能够实现简单且方便地配置、自动化地执行,并且能够及时地定位相关人员。基于这个需求,我开源了一个资源合规扫描工具ResScannerUE,配置文档:UE 资源合规检查工具 ResScannerUE
本篇文章将介绍如何通过该工具,实现资源扫描的自动化,并提供了与Git结合的方式进行增量检测支持,使用Commandlet在CI平台上实现Content内容变更的自动触发并执行检测规则,并能够定位到出问题资源最近提交人,实现精确定位,实时发送扫描报告至企业微信,提醒通知相关人员进行处理。
另外,我还提供了基于Git的Pre-Commit Hook实现,可以在提交之前检测本次提交是否具有不合规资源,并禁止提交,避免问题资源污染远程仓库。整体方案经过了精心设计和大量体验优化、增强自动化支持,可以非常方便地配置、接入,能够实现各种资源扫描的需求。

在插件中能够配置针对每个规则的路径、资源列表,同时也提供了针对所有规则使用的全局的资源路径、资源列表,并且能支持屏蔽每个规则中的配置。

控制好全局资源,并且屏蔽单个规则中的资源配置,能够精确地控制对哪些资源进行所有规则的检查,这也是要实现自动化资源扫描的基础。

对于实现自动化资源扫描,根据需求不同,可以拆分成两个部分:

  1. 全量扫描
  2. 增量扫描

全量扫描自不必多说,只需要在插件中指定全局资源/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版本比对检测

通常,工程仓库会分为两部分:

  1. 代码仓库:基础工程结构、代码、配置等
  2. 资源仓库: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
// set CPF_Transient to AssetPackageNames or AssetsCommiter
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);
}
}
}
// clear 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机制,可以十分方便地接入企业微信提醒,在提交资源后,自动对提交资源的进行检测,并发送群提醒:

整个流程只需要:

  1. python拉起ResScannerUE的Commandlet
  2. 通过commandlet的返回值检测是否有命中资源(return -1)
  3. 读取Saved/ResScanner/*_result.json文件
  4. 使用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 os
import sys
import chardet
import argparse
import codecs
import subprocess
from notice_robot_py import robot

commandlets_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 # os.system(final_cmdlet)
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的机制结合实现:

  1. 在Commit时执行Hook脚本
  2. Hook脚本拉起ResScannerUE的Commandlet,进行待提交资源的合规扫描(GitChecker.bUncommitFiles = true
  3. 获取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
#coding=utf-8
import os
import sys
import subprocess

# handle encoding convert
def conv_encoding(content_str):
# return content_str.decode('utf-8').encode('gbk')
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实现资源扫描自动化的工程实践,可以方便地将资源扫描接入开发管线的各个流程中。

全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:基于ResScannerUE的资源检查自动化实践
文章作者:查利鹏
发布时间:2021年10月25日 15时49分
本文字数:本文一共有3.1k字
原始链接:https://imzlp.com/posts/20376/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!