├── HotUpdater ├── HotUpdater.asmdef └── HotUpdater.cs ├── JenkinsBuild.cs ├── LICENSE └── README.md /HotUpdater/HotUpdater.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HotUpdater", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:9e24947de15b9834991c9d8411ea37cf", 6 | "GUID:84651a3751eca9349aac36a66bba901b", 7 | "GUID:13ba8ce62aa80c74598530029cb2d649" 8 | ], 9 | "includePlatforms": [], 10 | "excludePlatforms": [], 11 | "allowUnsafeCode": false, 12 | "overrideReferences": false, 13 | "precompiledReferences": [], 14 | "autoReferenced": true, 15 | "defineConstraints": [], 16 | "versionDefines": [], 17 | "noEngineReferences": false 18 | } -------------------------------------------------------------------------------- /HotUpdater/HotUpdater.cs: -------------------------------------------------------------------------------- 1 | // HotUpdater 2 | // Shepherd Zhu 3 | // Fetch the update and load dlls before enter the game. 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using UnityEngine; 9 | using UnityEngine.AddressableAssets; 10 | using UnityEngine.ResourceManagement.AsyncOperations; 11 | using System.Reflection; 12 | using HybridCLR; 13 | 14 | public class HotUpdater : MonoBehaviour 15 | { 16 | public string versionNumber; 17 | private string progressText; 18 | 19 | private void OnGUI() 20 | { 21 | GUIStyle style = new GUIStyle(GUI.skin.label); 22 | style.alignment = TextAnchor.LowerRight; 23 | style.normal.textColor = Color.white; 24 | 25 | GUI.Label(new Rect(Screen.width - 100, Screen.height - 30, 100, 30), "Version: " + versionNumber, style); 26 | GUI.Label(new Rect(Screen.width - 100, Screen.height - 60, 100, 30), "ResVer: " + PlayerPrefs.GetString("ResVersion"), style); 27 | 28 | GUIStyle progressStyle = new GUIStyle(GUI.skin.label); 29 | style.alignment = TextAnchor.MiddleCenter; 30 | style.normal.textColor = Color.white; 31 | GUI.Label(new Rect(Screen.width * 0.5f, Screen.height * 0.5f,300,30), progressText, progressStyle); 32 | } 33 | 34 | private void Start() 35 | { 36 | progressText = "Hot Updater Initializing..."; 37 | StartCoroutine(StartCheckForResDownload()); 38 | } 39 | 40 | IEnumerator StartCheckForResDownload() 41 | { 42 | Debug.Log("[HotUpdater] Start looking for local catalog cache."); 43 | string _catalogPath = Application.persistentDataPath + "/com.unity.addressables"; 44 | if (Directory.Exists(_catalogPath)) 45 | { 46 | try 47 | { 48 | Directory.Delete(_catalogPath, true); 49 | Debug.Log("[HotUpdater] Successfully delete catalog cache!"); 50 | } 51 | catch (Exception e) 52 | { 53 | Debug.LogWarning($"[HotUpdater] Unable to delete catalog cache. \n{e.ToString()}"); 54 | } 55 | } 56 | Debug.Log("[HotUpdater] Looking for local catalog cache is complete!"); 57 | var init = Addressables.InitializeAsync(false); 58 | yield return init; 59 | if (init.Status == AsyncOperationStatus.Succeeded) 60 | { 61 | var checkUpdateCatlogHandle = Addressables.CheckForCatalogUpdates(false); 62 | yield return checkUpdateCatlogHandle; 63 | if (checkUpdateCatlogHandle.Status == AsyncOperationStatus.Failed) 64 | { 65 | progressText = "Failed on downloading updates!"; 66 | Debug.LogError("[HotUpdater] Failed on Addressables.CheckForCatalogUpdates!"); 67 | yield break; 68 | } 69 | else if (checkUpdateCatlogHandle.Result.Count > 0) 70 | { 71 | var updateCatlogHandle = Addressables.UpdateCatalogs(checkUpdateCatlogHandle.Result, false); 72 | yield return updateCatlogHandle; 73 | if(updateCatlogHandle.Status == AsyncOperationStatus.Failed) 74 | { 75 | progressText = "Failed on downloading updates!"; 76 | Debug.LogError("[HotUpdater] Failed on Addressables.UpdateCatalogs!"); 77 | yield break; 78 | } 79 | else if(updateCatlogHandle.Status == AsyncOperationStatus.Succeeded) 80 | { 81 | Debug.Log("[HotUpdater] GetDownloadSizeAsync Started!"); 82 | var downloadSize = Addressables.GetDownloadSizeAsync("default"); 83 | yield return downloadSize; 84 | float size = (float)downloadSize.Result / 1024 / 1024; 85 | Debug.Log($"[HotUpdater] DownloadSize is {size}"); 86 | if (updateCatlogHandle.Status == AsyncOperationStatus.Succeeded && downloadSize.Result > 0) 87 | { 88 | Debug.Log("[HotUpdater] Start downloading addressables."); 89 | yield return StartCoroutine(DownloadAddressables()); 90 | yield break; 91 | } 92 | else if(updateCatlogHandle.Status == AsyncOperationStatus.Failed) 93 | { 94 | progressText = "Failed on downloading updates!"; 95 | Debug.LogError("[HotUpdater] Failed on Addressables.GetDownloadSizeAsync!"); 96 | yield break; 97 | } 98 | } 99 | Addressables.Release(updateCatlogHandle); 100 | } 101 | Addressables.Release(checkUpdateCatlogHandle); 102 | } 103 | else if(init.Status == AsyncOperationStatus.Failed) 104 | { 105 | progressText = "Failed on Addressables.InitializeAsync!"; 106 | Debug.LogError("[HotUpdater] Failed on Addressables.InitializeAsync!"); 107 | yield break; 108 | } 109 | Addressables.Release(init); 110 | 111 | StartCoroutine(EnterGame()); 112 | } 113 | 114 | IEnumerator DownloadAddressables() 115 | { 116 | var downloadDependenciesHandle = Addressables.DownloadDependenciesAsync("default", false); 117 | while (downloadDependenciesHandle.Status == AsyncOperationStatus.None) 118 | { 119 | var progress = downloadDependenciesHandle.GetDownloadStatus().Percent; 120 | progressText = $"Downloading...{progress}"; 121 | yield return null; 122 | } 123 | yield return downloadDependenciesHandle; 124 | if (downloadDependenciesHandle.Status == AsyncOperationStatus.Succeeded) 125 | { 126 | Addressables.Release(downloadDependenciesHandle); 127 | string time = string.Format("{0:yyyyMMddHHMMss}", DateTime.Now); 128 | PlayerPrefs.SetString("ResVersion", time); 129 | progressText = "Patching..."; 130 | StartCoroutine(EnterGame()); 131 | } 132 | else 133 | { 134 | progressText = "Failed on downloading updates!"; 135 | Debug.LogError("[HotUpdater] Failed on Addressables.DownloadDependenciesAsync!"); 136 | yield break; 137 | } 138 | } 139 | 140 | /// 141 | /// Load hot update dlls. 142 | /// 143 | IEnumerator LoadHotfixDLLs() 144 | { 145 | // Can't load dlls in Editor because it will duplicate the reference to scripts and cause issues. 146 | #if !UNITY_EDITOR 147 | var hotFixDllLabelHandle = Addressables.LoadAssetsAsync("HotUpdateDLL", null); 148 | yield return hotFixDllLabelHandle; 149 | var hotFixDlls = hotFixDllLabelHandle.Result; 150 | Addressables.Release(hotFixDllLabelHandle); 151 | foreach (var hotFixDll in hotFixDlls) { 152 | Assembly.Load(hotFixDll.bytes); 153 | } 154 | Debug.Log("[HotUpdater] LoadHotfixDLLs complete!"); 155 | #endif 156 | yield return null; 157 | } 158 | /// 159 | /// Load AOT metadata dlls. 160 | /// 161 | /// 162 | IEnumerator LoadMetadataForAOTDLLs() 163 | { 164 | // Can't load dlls in Editor because it will duplicate the reference to scripts and cause issues. 165 | #if !UNITY_EDITOR 166 | HomologousImageMode mode = HomologousImageMode.SuperSet; 167 | var aotMetadateDllHandle = Addressables.LoadAssetsAsync("AOTMetadataDLL", null); 168 | yield return aotMetadateDllHandle; 169 | var AOTMetadataDlls = aotMetadateDllHandle.Result; 170 | foreach (var AOTMetadataDll in AOTMetadataDlls) 171 | { 172 | LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(AOTMetadataDll.bytes, mode); 173 | Debug.Log($"[HotUpdater] LoadMetadataForAOTAssembly:{AOTMetadataDll.name}. mode:{mode} ret:{err}"); 174 | } 175 | Addressables.Release(aotMetadateDllHandle); 176 | Debug.Log("[HotUpdater] LoadMetadataForAOTDLLs complete!"); 177 | #endif 178 | yield return null; 179 | } 180 | 181 | /// 182 | /// Load hot update dlls and AOT metadata dlls here and enter the main menu scene or somewhere 183 | /// 184 | /// 185 | IEnumerator EnterGame() 186 | { 187 | yield return LoadHotfixDLLs(); 188 | yield return LoadMetadataForAOTDLLs(); 189 | 190 | Addressables.LoadSceneAsync("MainMenu").Completed += (obj) => 191 | { 192 | if (obj.Status == AsyncOperationStatus.Succeeded) 193 | { 194 | Debug.Log("[HotUpdater] Successfully load into MainMenu."); 195 | } 196 | else 197 | { 198 | Debug.LogError("[HotUpdater] Failed to load MainMenu scene!"); 199 | } 200 | }; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /JenkinsBuild.cs: -------------------------------------------------------------------------------- 1 | // JenkinsBuild 2 | // Shepherd Zhu 3 | // Jenkins Build Helper 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using HybridCLR.Editor; 11 | using HybridCLR.Editor.Commands; 12 | using HybridCLR.Editor.Settings; 13 | using UnityEditor; 14 | using UnityEditor.AddressableAssets; 15 | using UnityEditor.AddressableAssets.Settings; 16 | using UnityEngine; 17 | 18 | public static class JenkinsBuild 19 | { 20 | // 重要提醒:建议先在工作电脑上配好Groups和Labels,本脚本虽说遇到新文件可以添加到Addressable,但是不太可靠。 21 | 22 | /// 23 | /// 开始执行HybridCLR热更打包,默认打当前平台 24 | /// 25 | [MenuItem("Shepherd0619/Build Hot Update")] 26 | public static void BuildHotUpdate() 27 | { 28 | if (EditorUtility.DisplayDialog("Warning", 29 | $"You are attempting to build hot update for {EditorUserBuildSettings.activeBuildTarget}.", 30 | "Proceed", "Quit")) 31 | BuildHotUpdate(EditorUserBuildSettings.selectedBuildTargetGroup, EditorUserBuildSettings.activeBuildTarget); 32 | } 33 | 34 | [MenuItem("Shepherd0619/Build Playground Win Server")] 35 | public static void BuildPlaygroundWinServer() 36 | { 37 | var currentTarget = EditorUserBuildSettings.activeBuildTarget; 38 | 39 | if (!EditorUtility.DisplayDialog("Warning", 40 | "You are attempting to build a Windows Server.", 41 | "Proceed", "Quit")) 42 | return; 43 | 44 | var options = new BuildPlayerOptions(); 45 | options.target = BuildTarget.StandaloneWindows64; 46 | options.subtarget = (int)StandaloneBuildSubtarget.Server; 47 | options.scenes = new[] { "Assets/_Project/Scenes/Playground.unity" }; 48 | options.locationPathName = 49 | Path.Combine(Application.dataPath, 50 | $"../Build/WindowsServer/{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds()}/PSXGame.exe"); 51 | 52 | // 关闭热更新 53 | HybridCLRSettings.Instance.enable = false; 54 | HybridCLRSettings.Save(); 55 | 56 | BuildPipeline.BuildPlayer(options); 57 | 58 | EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, currentTarget); 59 | HybridCLRSettings.Instance.enable = true; 60 | HybridCLRSettings.Save(); 61 | } 62 | 63 | /// 64 | /// 开始执行HybridCLR热更打包 65 | /// 66 | /// 目标平台 67 | public static void BuildHotUpdate(BuildTargetGroup group, BuildTarget target) 68 | { 69 | Console.WriteLine( 70 | $"[JenkinsBuild] Start building hot update for {Enum.GetName(typeof(BuildTarget), target)}" 71 | ); 72 | 73 | EditorUserBuildSettings.SwitchActiveBuildTarget(group, target); 74 | 75 | // 打开热更新 76 | HybridCLRSettings.Instance.enable = true; 77 | HybridCLRSettings.Save(); 78 | 79 | try 80 | { 81 | CompileDllCommand.CompileDll(target); 82 | Il2CppDefGeneratorCommand.GenerateIl2CppDef(); 83 | 84 | // 这几个生成依赖HotUpdateDlls 85 | LinkGeneratorCommand.GenerateLinkXml(target); 86 | 87 | // 生成裁剪后的aot dll 88 | StripAOTDllCommand.GenerateStripedAOTDlls(target); 89 | 90 | // 桥接函数生成依赖于AOT dll,必须保证已经build过,生成AOT dll 91 | MethodBridgeGeneratorCommand.GenerateMethodBridgeAndReversePInvokeWrapper(target); 92 | AOTReferenceGeneratorCommand.GenerateAOTGenericReference(target); 93 | } 94 | catch (Exception e) 95 | { 96 | Console.WriteLine( 97 | $"[JenkinsBuild] ERROR while building hot update! Message:\n{e}" 98 | ); 99 | return; 100 | } 101 | 102 | // 复制打出来的DLL并进行替换 103 | var sourcePath = Path.Combine( 104 | Application.dataPath, 105 | $"../{SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target)}" 106 | ); 107 | var destinationPath = Path.Combine(Application.dataPath, "HotUpdateDLLs"); 108 | 109 | if (!Directory.Exists(sourcePath)) 110 | { 111 | Console.WriteLine( 112 | "[JenkinsBuild] Source directory does not exist! Possibly HybridCLR build failed!" 113 | ); 114 | return; 115 | } 116 | 117 | if (!Directory.Exists(destinationPath)) 118 | { 119 | Console.WriteLine( 120 | "[JenkinsBuild] Destination directory does not exist!" 121 | ); 122 | Directory.CreateDirectory(destinationPath); 123 | } 124 | 125 | // string[] dllFiles = Directory.GetFiles(sourcePath, "*.dll"); 126 | 127 | // foreach (string dllFile in dllFiles) 128 | // { 129 | // string fileName = Path.GetFileName(dllFile); 130 | // string destinationFile = Path.Combine(destinationPath, fileName + ".bytes"); 131 | // Console.WriteLine($"[JenkinsBuild] Copy: {dllFile}"); 132 | // File.Copy(dllFile, destinationFile, true); 133 | // } 134 | 135 | var hotUpdateAssemblyNames = SettingsUtil.HotUpdateAssemblyNamesExcludePreserved; 136 | for (var i = 0; i < hotUpdateAssemblyNames.Count; i++) 137 | { 138 | Console.WriteLine($"[JenkinsBuild] Copy: {hotUpdateAssemblyNames[i] + ".dll"}"); 139 | File.Copy(sourcePath + "/" + hotUpdateAssemblyNames[i] + ".dll", 140 | Path.Combine(destinationPath, hotUpdateAssemblyNames[i] + ".dll.bytes"), true); 141 | } 142 | 143 | Console.WriteLine("[JenkinsBuild] Hot Update DLLs copied successfully!"); 144 | 145 | // 复制打出来的AOT元数据DLL并进行替换 146 | Console.WriteLine("[JenkinsBuild] Start copying AOT Metadata DLLs!"); 147 | sourcePath = Path.Combine( 148 | Application.dataPath, 149 | $"../{SettingsUtil.GetAssembliesPostIl2CppStripDir(target)}" 150 | ); 151 | destinationPath = Path.Combine(Application.dataPath, "HotUpdateDLLs/AOTMetadata"); 152 | 153 | if (!Directory.Exists(sourcePath)) 154 | { 155 | Console.WriteLine( 156 | "[JenkinsBuild] Source directory does not exist! Possibly HybridCLR build failed!" 157 | ); 158 | return; 159 | } 160 | 161 | if (!Directory.Exists(destinationPath)) 162 | { 163 | Console.WriteLine( 164 | "[JenkinsBuild] Destination directory does not exist!" 165 | ); 166 | Directory.CreateDirectory(destinationPath); 167 | } 168 | 169 | // 获取AOTGenericReferences.cs文件的路径 170 | var aotReferencesFilePath = Path.Combine( 171 | Application.dataPath, 172 | SettingsUtil.HybridCLRSettings.outputAOTGenericReferenceFile 173 | ); 174 | 175 | if (!File.Exists(aotReferencesFilePath)) 176 | { 177 | Console.WriteLine( 178 | "[JenkinsBuild] AOTGenericReferences.cs file does not exist! Abort the build!" 179 | ); 180 | return; 181 | } 182 | 183 | // 读取AOTGenericReferences.cs文件内容 184 | var aotReferencesFileContent = File.ReadAllLines(aotReferencesFilePath); 185 | 186 | // 查找PatchedAOTAssemblyList列表 187 | var patchedAOTAssemblyList = new List(); 188 | 189 | for (var i = 0; i < aotReferencesFileContent.Length; i++) 190 | if (aotReferencesFileContent[i].Contains("PatchedAOTAssemblyList")) 191 | { 192 | while (!aotReferencesFileContent[i].Contains("};")) 193 | { 194 | if (aotReferencesFileContent[i].Contains("\"")) 195 | { 196 | var startIndex = aotReferencesFileContent[i].IndexOf("\"") + 1; 197 | var endIndex = aotReferencesFileContent[i].LastIndexOf("\""); 198 | var dllName = aotReferencesFileContent[i].Substring( 199 | startIndex, 200 | endIndex - startIndex 201 | ); 202 | patchedAOTAssemblyList.Add(dllName); 203 | } 204 | 205 | i++; 206 | } 207 | 208 | break; 209 | } 210 | 211 | // 复制DLL文件到目标文件夹,并添加后缀名".bytes" 212 | foreach (var dllName in patchedAOTAssemblyList) 213 | { 214 | var sourceFile = Path.Combine(sourcePath, dllName); 215 | var destinationFile = Path.Combine( 216 | destinationPath, 217 | Path.GetFileName(dllName) + ".bytes" 218 | ); 219 | 220 | if (File.Exists(sourceFile)) 221 | { 222 | Console.WriteLine($"[JenkinsBuild] Copy: {sourceFile}"); 223 | File.Copy(sourceFile, destinationFile, true); 224 | //SetAOTMetadataDllLabel("Assets/HotUpdateDLLs/" + Path.GetFileName(dllName) + ".bytes"); 225 | } 226 | else 227 | { 228 | Console.WriteLine("[JenkinsBuild] AOTMetadata DLL file not found: " + dllName); 229 | } 230 | } 231 | 232 | AssetDatabase.SaveAssets(); 233 | 234 | Console.WriteLine("[JenkinsBuild] BuildHotUpdate complete!"); 235 | 236 | AssetDatabase.Refresh(); 237 | 238 | // 刷新后开始给DLL加标签 239 | //SetHotUpdateDllLabel("Assets/HotUpdateDLLs/Assembly-CSharp.dll.bytes"); 240 | for (var i = 0; i < hotUpdateAssemblyNames.Count; i++) 241 | SetHotUpdateDllLabel("Assets/HotUpdateDLLs/" + hotUpdateAssemblyNames[i] + ".dll.bytes"); 242 | 243 | foreach (var dllName in patchedAOTAssemblyList) 244 | SetAOTMetadataDllLabel("Assets/HotUpdateDLLs/AOTMetadata/" + Path.GetFileName(dllName) + ".bytes"); 245 | 246 | Console.WriteLine("[JenkinsBuild] Start building Addressables!"); 247 | BuildAddressableContent(); 248 | } 249 | 250 | public static void BuildHotUpdateForWindows64() 251 | { 252 | BuildHotUpdate(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows64); 253 | } 254 | 255 | public static void BuildHotUpdateForiOS() 256 | { 257 | BuildHotUpdate(BuildTargetGroup.iOS, BuildTarget.iOS); 258 | } 259 | 260 | public static void BuildHotUpdateForLinux64() 261 | { 262 | BuildHotUpdate(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64); 263 | } 264 | 265 | public static void BuildHotUpdateForAndroid() 266 | { 267 | BuildHotUpdate(BuildTargetGroup.Android, BuildTarget.Android); 268 | } 269 | 270 | public static void BuildWindowsServer() 271 | { 272 | // 获取命令行参数 273 | var args = Environment.GetCommandLineArgs(); 274 | 275 | // 获取scenes参数 276 | var scenes = GetArgument(args, "scenes"); 277 | if (scenes == null || scenes.Length <= 0) return; 278 | 279 | // 获取targetPath参数 280 | var targetPath = GetArgument(args, "targetPath"); 281 | if (targetPath == null || targetPath.Length <= 0) return; 282 | 283 | var options = new BuildPlayerOptions(); 284 | options.target = BuildTarget.StandaloneWindows64; 285 | options.subtarget = (int)StandaloneBuildSubtarget.Server; 286 | options.scenes = scenes; 287 | options.locationPathName = targetPath[0]; 288 | 289 | // 关闭热更新 290 | HybridCLRSettings.Instance.enable = false; 291 | HybridCLRSettings.Save(); 292 | 293 | var sb = new StringBuilder(); 294 | for (var i = 0; i < scenes.Length; i++) sb.Append(scenes[i] + "; "); 295 | Console.WriteLine($"[JenkinsBuild] Start building WindowsServer! Scenes: {sb}, TargetPath: {targetPath[0]}"); 296 | BuildPipeline.BuildPlayer(options); 297 | } 298 | 299 | public static void BuildLinuxServer() 300 | { 301 | // 获取命令行参数 302 | var args = Environment.GetCommandLineArgs(); 303 | 304 | // 获取scenes参数 305 | var scenes = GetArgument(args, "scenes"); 306 | if (scenes == null || scenes.Length <= 0) return; 307 | 308 | // 获取targetPath参数 309 | var targetPath = GetArgument(args, "targetPath"); 310 | if (targetPath == null || targetPath.Length <= 0) return; 311 | 312 | var options = new BuildPlayerOptions(); 313 | options.target = BuildTarget.StandaloneLinux64; 314 | options.subtarget = (int)StandaloneBuildSubtarget.Server; 315 | options.scenes = scenes; 316 | options.locationPathName = targetPath[0]; 317 | 318 | // 关闭热更新 319 | HybridCLRSettings.Instance.enable = false; 320 | HybridCLRSettings.Save(); 321 | 322 | var sb = new StringBuilder(); 323 | for (var i = 0; i < scenes.Length; i++) sb.Append(scenes[i] + "\n"); 324 | Console.WriteLine($"[JenkinsBuild] Start building LinuxServer! Scenes: {sb}, TargetPath: {targetPath[0]}"); 325 | BuildPipeline.BuildPlayer(options); 326 | } 327 | 328 | /// 329 | /// 获取某个参数 330 | /// 331 | /// 全部的命令行参数 332 | /// 要获取的参数名称 333 | /// 所求参数 334 | private static string[] GetArgument(string[] args, string name) 335 | { 336 | var start = Array.FindIndex(args, arg => arg == $"-{name}"); 337 | if (start < 0) 338 | { 339 | Console.WriteLine($"[JenkinsBuild.GetArgument] Can not find argument: {name}"); 340 | return null; 341 | } 342 | 343 | start++; 344 | var end = Array.FindIndex(args, start, arg => arg[0] == '-'); 345 | if (end < 0) end = args.Length; 346 | var count = end - start; 347 | if (count <= 0) 348 | { 349 | Console.WriteLine( 350 | $"[JenkinsBuild.GetArgument] Can not find argument value: {name}, Count: {count}, Start: {start}, End: {end}"); 351 | return null; 352 | } 353 | 354 | var result = args.Skip(start).Take(count).ToArray(); 355 | return result; 356 | } 357 | 358 | /// 359 | /// 将热更DLL加入到Addressable 360 | /// 361 | /// DLL完整路径 362 | private static void SetHotUpdateDllLabel(string dllPath) 363 | { 364 | var settings = AddressableAssetSettingsDefaultObject.Settings; 365 | var group = settings.FindGroup("DLLs"); 366 | var guid = AssetDatabase.AssetPathToGUID(dllPath); 367 | if (settings.FindAssetEntry(guid) != null) 368 | { 369 | Console.WriteLine( 370 | $"[JenkinsBuild.SetHotUpdateDLLLabel] {dllPath} already exist in Addressables. Abort!" 371 | ); 372 | return; 373 | } 374 | 375 | var entry = settings.CreateOrMoveEntry(guid, group); 376 | entry.labels.Add("default"); 377 | entry.labels.Add("HotUpdateDLL"); 378 | entry.address = Path.GetFileName(dllPath); 379 | settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true); 380 | } 381 | 382 | /// 383 | /// 将AOT元数据DLL加入到Addressable 384 | /// 385 | /// DLL完整路径 386 | private static void SetAOTMetadataDllLabel(string dllPath) 387 | { 388 | var settings = AddressableAssetSettingsDefaultObject.Settings; 389 | var group = settings.FindGroup("DLLs"); 390 | var guid = AssetDatabase.AssetPathToGUID(dllPath); 391 | if (settings.FindAssetEntry(guid) != null) 392 | { 393 | Console.WriteLine( 394 | $"[JenkinsBuild.SetAOTMetadataDLLLabel] {dllPath} already exist in Addressables. Abort!" 395 | ); 396 | return; 397 | } 398 | 399 | var entry = settings.CreateOrMoveEntry(guid, group); 400 | entry.labels.Add("default"); 401 | entry.labels.Add("AOTMetadataDLL"); 402 | entry.address = Path.GetFileName(dllPath); 403 | settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entry, true); 404 | } 405 | 406 | /// 407 | /// 打当前平台的打包Addressable 408 | /// 409 | /// 410 | private static bool BuildAddressableContent() 411 | { 412 | var path = Path.Combine(Application.dataPath, 413 | "../ServerData/" + Enum.GetName(typeof(BuildTarget), EditorUserBuildSettings.activeBuildTarget)); 414 | if (Directory.Exists(path)) Directory.Delete(path, true); 415 | 416 | AddressableAssetSettings.BuildPlayerContent(out var result); 417 | var success = string.IsNullOrEmpty(result.Error); 418 | 419 | if (!success) 420 | Console.WriteLine("[JenkinsBuild.BuildAddressableContent] Addressable build error encountered: " + 421 | result.Error); 422 | return success; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Shepherd 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JenkinsBuildUnity 2 | A little script that connect Unity (with HybridCLR hot update) and Jenkins together. 3 | 4 | # Goal 5 | To make CI/CD more easier to be integrated into any Unity project has hot update feature. 6 | 7 | # What is CI/CD? 8 | In software engineering, CI/CD or CICD is the combined practices of continuous integration (CI) and continuous delivery (CD) or, less often, continuous deployment. They are sometimes referred to collectively as continuous development or continuous software development. -- Wikipedia 9 | 10 | Alright, in humans word, a computer that keeps building the app till death and you don't have to pay the salary to do so. 11 | 12 | # What is Hot Update? 13 | Hotfixes can also solve many of the same issues as a patch, but it is applied to a “hot” system—a live system—to fix an issue: 14 | 15 | 1. Immediately 16 | 2. Without creating system downtimes or outages. 17 | 18 | Hotfixes are also known as QFE updates, short for quick-fix engineering updates, a name that illustrates the urgency. 19 | 20 | Normally, you’ll create a hotfix quickly, as an urgent measure against issues that need to be fixed immediately and outside of your normal development flow. Unlike patches, hotfixes address very specific issues like adding a new feature, bug, or security fix 21 | 22 | -- Chrissy Kidd from bmc.com 23 | 24 | In humans word, a solution that only build the modified part and deliver it to players/testers/your boss/... as fast as possible. 25 | 26 | # Why do I need CI/CD? 27 | 1. You will no longer have to pause your current work because of building on your PC. 28 | 2. You will no longer have to perform the complicated building process again and again. 29 | 3. You will no longer have to check your collaborator's commit manually. 30 | 4. Testers/boss can build the client by themselves, instead of a phone call asking you to put down what you are doing and go help them. 31 | 5. ...... 32 | 33 | # Why do I need Hot Update? 34 | 1. Push fixes to players as quickly as possible without waiting for App Store/Google Play's review. 35 | 2. No need to compile the full client over and over again which drive many of you crazy. 36 | 3. ...... 37 | 38 | # How to use? 39 | In simple, it should be like the following: 40 | 1. Install [Jenkins](https://www.jenkins.io/) and unity3d plugin on your server or another PC. 41 | 2. Import [HybridCLR](https://github.com/focus-creative-games/hybridclr_unity) and Addressables into your Unity project and do some configurations. 42 | 3. Place "JenkinsBuild.cs" into "Assets/Editor". 43 | 4. Add a Addressables group called "DLLs" and labels "HotUpdateDLL" and "AOTMetadataDLL". 44 | 5. Start HybridCLR build. 45 | 6. ~~Copy and paste the dlls under "HybridCLRData/HotUpdateDlls" and "HybridCLRData/AssembliesPostIl2CppStrip" the project need. (**No need to copy all of them.** )~~ 46 | 7. ~~Add ".bytes" to the end of the name of dll you copied.~~ 47 | ~~Like "Assembly-CSharp.dll.bytes".~~ 48 | 8. ~~Add those dlls into "DLLs" group and add the corresponding label to them.~~ 49 | 50 | No need to do 6, 7, 8 anymore since the script can do them automatically. 51 | **Make sure your HybridCLRSettings and Addressables Settings are correct since the script do these jobs depends on them.** 52 | 53 | 9. Create a new Assembly Definition and a script to download & apply updates before loading into main scene. 54 | (**Example scripts included!** ) 55 | 10. Set DisableCatalogUpdateOnStart to true which can be found in Addressable Settings since the script will take over the catalog update. 56 | 11. Start Jenkins job and enjoy! 57 | For unity editor command line, please type like this: 58 | ```-nographics -batchmode -quit -executeMethod JenkinsBuild.BuildHotUpdateForWindows64``` 59 | 60 | For details, you can visit the following docs and tutorials created by others to understand the content above better: 61 | 1. https://www.youtube.com/watch?v=WdIG0af7S0g 62 | 2. https://hybridclr.doc.code-philosophy.com/en/docs/basic 63 | 3. https://docs.unity3d.com/Packages/com.unity.addressables@1.20/manual/index.html 64 | 65 | Still feel confused? ~~You may wait for my step by step tutorial on [YouTube](https://www.youtube.com/channel/UCRQdc3lSimZvrvIAkt3bTuw) about it (**should be coming soon with an example project**).~~ 66 | 67 | **I'm not very good at video editing, but don't worry, I will use Markdown documents and sample projects to do the tutorial instead.** 68 | 69 | For Chinese, there are already tutorials on my CSDN blog! 70 | 71 | 来自中国的朋友!你们可以访问我的CSDN博客来获取相关教程! 72 | 73 | 1. https://blog.csdn.net/u012587406/article/details/135260464 74 | 2. https://blog.csdn.net/u012587406/article/details/135441946 75 | 76 | # References 77 | 1. [0oSTrAngeRo0/AutoBuild](https://github.com/0oSTrAngeRo0/AutoBuild) for command line args. 78 | --------------------------------------------------------------------------------