Skip to content

fix(core): resolve Obfuz player-build failure when HotUpdate.Code is referenced#635

Merged
JasonXuDeveloper merged 2 commits intomasterfrom
fix/obfuz-hotupdate-search-path
Apr 23, 2026
Merged

fix(core): resolve Obfuz player-build failure when HotUpdate.Code is referenced#635
JasonXuDeveloper merged 2 commits intomasterfrom
fix/obfuz-hotupdate-search-path

Conversation

@JasonXuDeveloper
Copy link
Copy Markdown
Owner

Summary

  • Fixes FileNotFoundException: Assembly HotUpdate.Code not found from Obfuz during Unity player build for users who have a compile-time reference to HotUpdate.Code anywhere in Assembly-CSharp.
  • Two-part fix: flip autoReferenced to false on the template's HotUpdate.Code.asmdef (removes the trigger by construction), plus an IPreprocessBuildWithReport / IPostprocessBuildWithReport that injects HybridCLRData/HotUpdateDlls/<target>/ into ObfuzSettings.additionalAssemblySearchPaths only during the build (defensive catch-all for users who deviate from the template).
  • Does NOT modify Obfuz.asset on disk; mutation is in-memory and reverted post-build.

Why not just disable Obfuz

The workaround circulating in reports ("set Obfuz off") silently disables the entire player-DLL obfuscation stage - shipping Assembly-CSharp, JEngine.Core, and Obfuz.Generated unobfuscated. That trades a real security feature for a clean build log. This PR keeps obfuscation on.

Root cause

ObfuscationProcess.OnPostBuildPlayerScriptDLLs (com.code-philosophy.obfuz/Editor/Unity/ObfuscationProcess.cs, callbackOrder 10000) obfuscates the player's managed DLLs. It builds an Obfuscator whose AssemblyCache.LoadModule (Editor/Utils/AssemblyCache.cs:75-97) recursively resolves every GetAssemblyRefs() entry through a flat PathAssemblyResolver.

  • HybridCLR's FilterHotFixAssemblies (IFilterBuildAssemblies) correctly strips HotUpdate.Code from the player staging area so it doesn't ship baked in.
  • But HotUpdate.Code.dll still exists in HybridCLRData/HotUpdateDlls/<target>/ from the prior code-build step - and Obfuz's resolver doesn't look there.
  • Combined with "autoReferenced": true on HotUpdate.Code.asmdef, any Assembly-CSharp source that uses a HotUpdate.Code.* type embeds a real assembly ref into Assembly-CSharp. Obfuz then tries to load it during recursion and throws.

The assembly: HotUpdate.Code not found! ignore. line is a red herring - it's TryLoadModule returning null on the first pass. The actual throw comes from the subsequent strict LoadModule call in AssemblyCache.LoadModule's foreach (var refAsm in mod.GetAssemblyRefs()) recursion.

Changes

  1. UnityProject/Assets/HotUpdate/Code/HotUpdate.Code.asmdef - "autoReferenced": true -> false. Hot-update assemblies should not be compile-referenced by Unity's predefined assemblies; main code loads them at runtime. Safe for the template: PromptInitializer.cs references only JEngine.Core/JEngine.UI, and HybridCLR's own AOTGenericReferences.cs emits all HotUpdate.Code.* entries as // comments (GenericReferenceWriter.cs:72-127).
  2. UnityProject/Packages/com.jasonxudeveloper.jengine.core/Editor/CustomEditor/ObfuzHotUpdateSearchPathInjector.cs (new) - single class implementing both build-report hooks. Snapshots ObfuzSettings.additionalAssemblySearchPaths pre-build, appends SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target) if present (warning log if missing, directing users to run the JEngine code-build step first), restores the original list post-build.

Rebase-merge friendly: each commit is self-contained.

Test plan

  • Reproduce on current master - revert the asmdef flip, add Assets/Scripts/Ref.cs doing public static System.Type t = typeof(HotUpdate.Code.EntryPoint);, delete the injector, switch to Android, run Build Main Package (code), then File -> Build Settings -> Build. Expect FileNotFoundException: Assembly HotUpdate.Code not found and Obfuscation failed.
  • Injector-only - keep autoReferenced: true and the throwaway ref file, restore the injector. Android build succeeds; console shows [JEngine] Injected HybridCLR hot-update dir into Obfuz search paths for this build: HybridCLRData/HotUpdateDlls/Android. Library/Obfuz/Android/ObfuscatedAssemblies/Assembly-CSharp.dll exists and is larger than the staging copy.
  • Asmdef-only - flip to autoReferenced: false, remove the throwaway ref file, remove the injector. Android build succeeds with no injection log (Assembly-CSharp no longer references HotUpdate.Code).
  • Both together (this PR) - default setup, Android build succeeds. No injection log on a clean template because there's no ref; if a user has one, the injector catches it.
  • Missing hot-update dir - delete HybridCLRData/HotUpdateDlls/Android/ and build. Injector logs a clear warning naming the path and advising to run Build Main Package (code) first.
  • Other target - repeat on StandaloneOSX, confirm the per-target path is used.
  • Obfuz disabled - set ObfuzSettings.buildPipelineSettings.enable = 0, build succeeds (injector runs harmlessly, Obfuz exits early).
  • Disk state untouched - confirm ProjectSettings/Obfuz.asset additionalAssemblySearchPaths is unchanged on disk after any build.
  • Built player does not ship HotUpdate.Code.dll - inspect the final APK: HotUpdate.Code.dll.bytes present under streaming assets, no raw HotUpdate.Code.dll assembly.

No unit tests: core package convention is no editor tests (consistent with existing BuildManager.cs), and the hook's inputs (BuildReport, ObfuzSettings.Instance, SettingsUtil static) aren't meaningfully mockable without a refactor that would add more surface area than the 10 lines of dedup-and-mutate logic being tested.

Hot-update assemblies are loaded at runtime by Bootstrap; they should
not be compile-referenced by Unity's predefined assemblies
(Assembly-CSharp, Assembly-CSharp-firstpass). With autoReferenced=true,
any Assembly-CSharp source that uses a HotUpdate.Code.* type embeds a
real assembly reference into Assembly-CSharp's metadata.

During the Unity player build, HybridCLR's FilterHotFixAssemblies
(IFilterBuildAssemblies) correctly strips HotUpdate.Code from the
player staging area. Obfuz's ObfuscationProcess
(IPostBuildPlayerScriptDLLs) then tries to obfuscate Assembly-CSharp
and recursively resolves its assembly refs via AssemblyCache.LoadModule
(Obfuz Editor/Utils/AssemblyCache.cs:75-97). The HotUpdate.Code ref
can't be resolved from the player search paths, so obfuscation throws
FileNotFoundException: Assembly HotUpdate.Code not found.

Setting autoReferenced=false removes the trigger for template users:
Assembly-CSharp no longer auto-references HotUpdate.Code, so nothing
forces the metadata ref, and Obfuz's recursive resolver never asks for
HotUpdate.Code. Verified safe for the current template: PromptInitializer
references only JEngine.Core / JEngine.UI, and AOTGenericReferences.cs
emits HotUpdate.Code.* entries as comments only
(GenericReferenceWriter.cs:72-127).

Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

Unity Test Results

EditMode: All tests passed
PlayMode: All tests passed

Unity Version: 2022.3.55f1
Project Path: UnityProject

✅ All tests passed! The PR is ready for review.

View workflow run

Click here to view the full workflow run

When Assembly-CSharp has a compile-time reference to HotUpdate.Code
(e.g. because a user re-enables autoReferenced on the hot-update asmdef
or their own code uses a HotUpdate.Code type), Unity's player build
fails during Obfuz's ObfuscationProcess.OnPostBuildPlayerScriptDLLs
with:

    FileNotFoundException: Assembly HotUpdate.Code not found
        at Obfuz.Utils.AssemblyCache.LoadModule (System.String moduleName)

Root cause: HybridCLR's FilterHotFixAssemblies correctly strips
HotUpdate.Code from the player staging area so it never ships baked in.
But Obfuz then obfuscates Assembly-CSharp and recursively resolves
every GetAssemblyRefs() entry via a flat PathAssemblyResolver. The
resolver only searches the player staging dir plus
ObfuzSettings.additionalAssemblySearchPaths, and HotUpdate.Code lives
in HybridCLRData/HotUpdateDlls/<target>/ - not in either.

Add ObfuzHotUpdateSearchPathInjector, an IPreprocessBuildWithReport /
IPostprocessBuildWithReport that snapshots
ObfuzSettings.additionalAssemblySearchPaths at the start of each
player build, appends HybridCLRData/HotUpdateDlls/<target>/ when
present, and restores the original list afterwards. In-memory only -
Obfuz.asset on disk is never modified.

The asmdef fix in the previous commit removes the trigger for the
template; this processor is the defensive safety net for users who
deviate from the template.

Signed-off-by: JasonXuDeveloper - 傑 <jason@xgamedev.net>
@JasonXuDeveloper JasonXuDeveloper force-pushed the fix/obfuz-hotupdate-search-path branch from 9166633 to b624d05 Compare April 23, 2026 10:47
@github-actions github-actions Bot added the tests label Apr 23, 2026
@JasonXuDeveloper JasonXuDeveloper enabled auto-merge (rebase) April 23, 2026 10:47
@JasonXuDeveloper JasonXuDeveloper merged commit 177acdd into master Apr 23, 2026
17 of 18 checks passed
@JasonXuDeveloper JasonXuDeveloper deleted the fix/obfuz-hotupdate-search-path branch April 23, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant