本文章主要围绕本人在Github上的一个开源脚本:
这个脚本是一个用于在Jenkins中构建Unity项目的辅助工具。它的主要功能是构建HybridCLR热更新,并将生成的DLL文件和AOT元数据DLL文件复制到指定的目录,并将它们添加到Unity的Addressable Assets系统中。
如果您还不知道Jenkins和HybridCLR的话,建议先阅读一下往期博客和相关官方文档。
【Unity实战】HybridCLR热更快速集成-CSDN博客
脚本讲解
首先,我们来看一下这个脚本的结构。它是一个继承自MonoBehaviour的类,并且包含了一些静态方法用于构建热更新(也必须得是静态,否则的话后续Jenkins通过命令行调起Unity会比较麻烦,找不到这个函数)。
// JenkinsBuild
// Shepherd Zhu
// Jenkins Build Helper
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using HybridCLR.Editor;
using HybridCLR.Editor.Commands;
using HybridCLR.Editor.Settings;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
public class JenkinsBuild : MonoBehaviour
{
// 重要提醒:建议先在工作电脑上配好Groups和Labels,本脚本虽说遇到新文件可以添加到Addressables,但是不太可靠。
[MenuItem("Shepherd0619/Build Hot Update")]
/// <summary>
/// 开始执行HybridCLR热更打包,默认打当前平台
/// </summary>
public static void BuildHotUpdate()
{
BuildHotUpdate(EditorUserBuildSettings.activeBuildTarget);
}
/// <summary>
/// 开始执行HybridCLR热更打包
/// </summary>
/// <param name="target">目标平台</param>
public static void BuildHotUpdate(BuildTarget target)
{
}
public static void BuildHotUpdateForWindows64()
{
BuildHotUpdate(BuildTarget.StandaloneWindows64);
}
public static void BuildHotUpdateForiOS()
{
BuildHotUpdate(BuildTarget.iOS);
}
public static void BuildHotUpdateForLinux64()
{
BuildHotUpdate(BuildTarget.StandaloneLinux64);
}
public static void BuildHotUpdateForAndroid()
{
BuildHotUpdate(BuildTarget.Android);
}
/// <summary>
/// 将热更DLL加入到Addressables
/// </summary>
/// <param name="dllPath">DLL完整路径</param>
private static void SetHotUpdateDllLabel(string dllPath)
{
}
/// <summary>
/// 将AOT元数据DLL加入到Addressables
/// </summary>
/// <param name="dllPath">DLL完整路径</param>
private static void SetAOTMetadataDllLabel(string dllPath)
{
}
private static bool buildAddressableContent()
{
}
}
整个脚本的核心是`BuildHotUpdate`方法。这个方法接受一个`BuildTarget`参数,用于指定构建的目标平台。在方法中,首先打印出正在构建的目标平台,然后依次执行一系列构建热更新所需的命令。
/// <summary>
/// 开始执行HybridCLR热更打包
/// </summary>
/// <param name="target">目标平台</param>
public static void BuildHotUpdate(BuildTarget target)
{
Console.WriteLine(
$"[JenkinsBuild] Start building hot update for {Enum.GetName(typeof(BuildTarget), target)}"
);
try
{
CompileDllCommand.CompileDll(target);
Il2CppDefGeneratorCommand.GenerateIl2CppDef();
// 这几个生成依赖HotUpdateDlls
LinkGeneratorCommand.GenerateLinkXml(target);
// 生成裁剪后的aot dll
StripAOTDllCommand.GenerateStripedAOTDlls(target);
// 桥接函数生成依赖于AOT dll,必须保证已经build过,生成AOT dll
MethodBridgeGeneratorCommand.GenerateMethodBridge(target);
ReversePInvokeWrapperGeneratorCommand.GenerateReversePInvokeWrapper(target);
AOTReferenceGeneratorCommand.GenerateAOTGenericReference(target);
}
catch (Exception e)
{
Console.WriteLine(
$"[JenkinsBuild] ERROR while building hot update! Message:\n{e.ToString()}"
);
return;
}
// 复制打出来的DLL并进行替换
string sourcePath = Path.Combine(
Application.dataPath,
$"../{SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target)}"
);
string destinationPath = Path.Combine(Application.dataPath, "HotUpdateDLLs");
if (!Directory.Exists(sourcePath))
{
Console.WriteLine(
"[JenkinsBuild] Source directory does not exist! Possibly HybridCLR build failed!"
);
return;
}
if (!Directory.Exists(destinationPath))
{
Console.WriteLine(
"[JenkinsBuild] Destination directory does not exist! Abort the build!"
);
return;
}
// string[] dllFiles = Directory.GetFiles(sourcePath, "*.dll");
// foreach (string dllFile in dllFiles)
// {
// string fileName = Path.GetFileName(dllFile);
// string destinationFile = Path.Combine(destinationPath, fileName + ".bytes");
// Console.WriteLine($"[JenkinsBuild] Copy: {dllFile}");
// File.Copy(dllFile, destinationFile, true);
// }
List<string> hotUpdateAssemblyNames = SettingsUtil.HotUpdateAssemblyNamesExcludePreserved;
for (int i = 0; i < hotUpdateAssemblyNames.Count; i++)
{
Console.WriteLine($"[JenkinsBuild] Copy: {hotUpdateAssemblyNames[i] + ".dll"}");
File.Copy(sourcePath + "/" + hotUpdateAssemblyNames[i] + ".dll", Path.Combine(destinationPath, hotUpdateAssemblyNames[i] + ".dll.bytes"), true);
}
Console.WriteLine("[JenkinsBuild] Hot Update DLLs copied successfully!");
// 复制打出来的AOT元数据DLL并进行替换
Console.WriteLine("[JenkinsBuild] Start copying AOT Metadata DLLs!");
sourcePath = Path.Combine(
Application.dataPath,
$"../{SettingsUtil.GetAssembliesPostIl2CppStripDir(target)}"
);
destinationPath = Path.Combine(Application.dataPath, "HotUpdateDLLs/AOTMetadata");
if (!Directory.Exists(sourcePath))
{
Console.WriteLine(
"[JenkinsBuild] Source directory does not exist! Possibly HybridCLR build failed!"
);
return;
}
if (!Directory.Exists(destinationPath))
{
Console.WriteLine(
"[JenkinsBuild] Destination directory does not exist! Abort the build!"
);
return;
}
// 获取AOTGenericReferences.cs文件的路径
string aotReferencesFilePath = Path.Combine(
Application.dataPath,
SettingsUtil.HybridCLRSettings.outputAOTGenericReferenceFile
);
if (!File.Exists(aotReferencesFilePath))
{
Console.WriteLine(
"[JenkinsBuild] AOTGenericReferences.cs file does not exist! Abort the build!"
);
return;
}
// 读取AOTGenericReferences.cs文件内容
string[] aotReferencesFileContent = File.ReadAllLines(aotReferencesFilePath);
// 查找PatchedAOTAssemblyList列表
List<string> patchedAOTAssemblyList = new List<string>();
for (int i = 0; i < aotReferencesFileContent.Length; i++)
{
if (aotReferencesFileContent[i].Contains("PatchedAOTAssemblyList"))
{
while (!aotReferencesFileContent[i].Contains("};"))
{
if (aotReferencesFileContent[i].Contains("\""))
{
int startIndex = aotReferencesFileContent[i].IndexOf("\"") + 1;
int endIndex = aotReferencesFileContent[i].LastIndexOf("\"");
string dllName = aotReferencesFileContent[i].Substring(
startIndex,
endIndex - startIndex
);
patchedAOTAssemblyList.Add(dllName);
}
i++;
}
break;
}
}
// 复制DLL文件到目标文件夹,并添加后缀名".bytes"
foreach (string dllName in patchedAOTAssemblyList)
{
string sourceFile = Path.Combine(sourcePath, dllName);
string destinationFile = Path.Combine(
destinationPath,
Path.GetFileName(dllName) + ".bytes"
);
if (File.Exists(sourceFile))
{
Console.WriteLine($"[JenkinsBuild] Copy: {sourceFile}");
File.Copy(sourceFile, destinationFile, true);
//SetAOTMetadataDllLabel("Assets/HotUpdateDLLs/" + Path.GetFileName(dllName) + ".bytes");
}
else
{
Console.WriteLine("[JenkinsBuild] AOTMetadata DLL file not found: " + dllName);
}
}
AssetDatabase.SaveAssets();
Console.WriteLine("[JenkinsBuild] BuildHotUpdate complete!");
AssetDatabase.Refresh();
// 刷新后开始给DLL加标签
//SetHotUpdateDllLabel("Assets/HotUpdateDLLs/Assembly-CSharp.dll.bytes");
for (int i = 0; i < hotUpdateAssemblyNames.Count; i++)
{
SetHotUpdateDllLabel("Assets/HotUpdateDLLs/" + hotUpdateAssemblyNames[i] + ".dll.bytes");
}
foreach(string dllName in patchedAOTAssemblyList)
{
SetAOTMetadataDllLabel("Assets/HotUpdateDLLs/AOTMetadata/" + Path.GetFileName(dllName) + ".bytes");
}
Console.WriteLine("[JenkinsBuild] Start building Addressables!");
buildAddressableContent();
}
在`BuildHotUpdate`方法中,首先调用`CompileDllCommand.CompileDll`方法来编译DLL文件。然后调用`Il2CppDefGeneratorCommand.GenerateIl2CppDef`方法来生成Il2Cpp的定义文件。接下来,调用`LinkGeneratorCommand.GenerateLinkXml`方法来生成链接文件。然后,调用`StripAOTDllCommand.GenerateStripedAOTDlls`方法来生成裁剪后的AOT DLL文件。接着,调用`MethodBridgeGeneratorCommand.GenerateMethodBridge`方法和`ReversePInvokeWrapperGeneratorCommand.GenerateReversePInvokeWrapper`方法来生成桥接函数和反向PInvoke包装器。最后,调用`AOTReferenceGeneratorCommand.GenerateAOTGenericReference`方法来生成AOT泛型引用。
在执行完所有的构建命令后,脚本会将生成的DLL文件和AOT元数据DLL文件复制到指定的目录。这里使用了`File.Copy`方法来实现复制。复制完成后,脚本会打印出复制成功的消息。
接下来,脚本会将生成的DLL文件和AOT元数据DLL文件添加到Unity的Addressable Assets系统中。这里使用了`AddressableAssetSettings`类来实现。首先通过`AddressableAssetSettingsDefaultObject.Settings`属性获取到Addressable Assets的设置对象,然后通过`FindGroup`方法找到指定的Group,接着使用`CreateOrMoveEntry`方法创建或移动Asset Entry,并将其添加到指定的Group中。最后,通过设置Entry的标签和地址来完成添加操作。
当然这一通操作完,得必须SetDirty以通知Unity这块有改动。
/// <summary>
/// 将热更DLL加入到Addressables
/// </summary>
/// <param name="dllPath">DLL完整路径</param>
private static void SetHotUpdateDllLabel(string dllPath)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
AddressableAssetGroup group = settings.FindGroup("DLLs");
var guid = AssetDatabase.AssetPathToGUID(dllPath);
if (settings.FindAssetEntry(guid) != null)
{
Console.WriteLine(
$"[JenkinsBuild.SetHotUpdateDLLLabel] {dllPath} already exist in Addressables. Abort!"
);
return;
}
var entry = settings.CreateOrMoveEntry(guid, group);
entry.labels.Add("default");
entry.labels.Add("HotUpdateDLL");
entry.address = Path.GetFileName(dllPath);
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true);
}
/// <summary>
/// 将AOT元数据DLL加入到Addressables
/// </summary>
/// <param name="dllPath">DLL完整路径</param>
private static void SetAOTMetadataDllLabel(string dllPath)
{
var settings = AddressableAssetSettingsDefaultObject.Settings;
AddressableAssetGroup group = settings.FindGroup("DLLs");
var guid = AssetDatabase.AssetPathToGUID(dllPath);
if (settings.FindAssetEntry(guid) != null)
{
Console.WriteLine(
$"[JenkinsBuild.SetAOTMetadataDLLLabel] {dllPath} already exist in Addressables. Abort!"
);
return;
}
var entry = settings.CreateOrMoveEntry(guid, group);
entry.labels.Add("default");
entry.labels.Add("AOTMetadataDLL");
entry.address = Path.GetFileName(dllPath);
settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true);
}
最后,脚本会调用`buildAddressableContent`方法来构建Addressable Assets的内容。这里使用了`AddressableAssetSettings.BuildPlayerContent`方法来实现构建。构建完成后,脚本会打印出构建结果,并返回构建是否成功的标志。
private static bool buildAddressableContent()
{
string path = Path.Combine(Application.dataPath, "../ServerData/"+Enum.GetName(typeof(BuildTarget), EditorUserBuildSettings.activeBuildTarget));
if(Directory.Exists(path)){
Directory.Delete(path, true);
}
AddressableAssetSettings.BuildPlayerContent(out AddressablesPlayerBuildResult result);
bool success = string.IsNullOrEmpty(result.Error);
if (!success)
{
Console.WriteLine("[JenkinsBuild.buildAddressableContent] Addressables build error encountered: " + result.Error);
}
return success;
}
以上就是这个脚本的主要功能和实现逻辑。通过这个脚本,我们可以方便地在Jenkins中构建Unity项目的热更新,并将生成的DLL文件和AOT元数据DLL文件添加到Unity的Addressable Assets系统中。
命令行参数样例
Unity.exe -nographics -batchmode -quit -executeMethod JenkinsBuild.BuildHotUpdateForWindows64
这个会只打热更新,不会打完整客户端。
更多Unity编辑器命令行参数,请查阅官方文档
Unity - Manual: Unity Editor command line arguments
希望这篇博客能给你带来一些思路。
(理论上我应该出个图文讲解视频,但是Hmm,我实在是在视频剪辑这块拉夸的一批)