Skip to content

fix: Hot reload no longer fails to resolve plugin DLLs that only a referenced assembly uses - #3176

Merged
hatayama merged 4 commits into
feature/hot-reload-large-project-feedbackfrom
fix/hot-reload-transitive-resolver-search-directories
Oct 6, 2026
Merged

hatayama merged 4 commits into
feature/hot-reload-large-project-feedbackfrom
fix/hot-reload-transitive-resolver-search-directories

Conversation

@hatayama

@hatayama hatayama commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Hot reload of a file no longer fails with "Publicizing referenced assemblies failed: Failed to resolve assembly" when the missing assembly is a precompiled plugin DLL that only a referenced assembly lists.
  • Typical case: a test assembly whose asmdef overrides its references, and a referenced game assembly that uses an Asset Store plugin DLL.

User Impact

  • Before: in a project where a game assembly used an Asset Store plugin DLL, uloop hot-reload on a file of an EditMode test assembly referencing that game assembly returned Success=false. The file failed with:

    Publicizing referenced assemblies failed: Failed to resolve assembly: ', Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' Hot reload could not build shim references for this file.

  • After: those files hot-reload normally. Nothing changes for files whose own assembly lists the plugin.

Cause

  • Hot reload compiles a shim against copies of the referenced script assemblies with their members made public. Cecil writes those copies, and while writing it can need another assembly, for example the enum type behind a constant or a default argument.
  • Cecil searched only the directories of the group assembly's own compile references (Assembly.allReferences).
  • Unity's allReferences is not transitive: it is the asmdef references plus the assembly's own precompiled references. A test asmdef with overrideReferences: true does not list the game's auto-referenced plugin DLLs, so the plugin's directory was never searched, even though the referenced game assembly lists it.
  • This repository's own project has the same structure, but the failure does not reproduce with its DLLs:
    • UnityCLILoop.Tests.Editor.HotReload overrides its references.
    • The tool assembly it references (ExecuteDynamicCode) lists the code analysis plugin DLLs in a directory the test assembly's own references do not cover.
    • Those copies happen not to need the plugin while they are written, and a successful copy is cached by MVID. So the Cecil path is pinned with a generated fixture instead (below).

Changes

  • New HotReloadResolverSearchDirectories.Collect(assembly): the search directories are now collected from the assembly and every assembly it references transitively.
    • Breadth-first, so the directories of the assembly's own references always come first. A same-named DLL found in several places keeps resolving to the copy it resolved to before.
    • Reference paths already seen in the walk are skipped before File.Exists, because the engine references (about 230 per assembly) repeat in every assembly. Measured in this project with the same algorithm: 62–145 ms without the skip, 14–16 ms with it, for closures of 20–51 assemblies. Both found 19 directories. The skip cannot change the result: a path seen earlier either already added its directory or did not exist then either.
  • The shim's publicized copies use these directories. The shim's compile references are unchanged: only where Cecil looks for assemblies widens, so Roslyn binds against the same list as before.
  • The internals-exposed copies for an introduced-type artifact use the same directories. Before, they came from the worker's reference list, which holds only the assembly's own references.
  • Copies of introduced-type artifacts, in both the shim and the artifact compile, still receive the directories through a set, so the search order there is the set's enumeration order, as before. Only the shim's copies of the target and project assemblies keep the breadth-first order.
  • docs/hot-reload.md describes the search order.

Verification

New tests

In HotReloadResolverSearchDirectoriesTests:

  • Collect_IncludesDirectoriesOfTransitivelyReferencedPrecompiledAssemblies: a plugin two references away (Tests → Game → Core → plugin) is reached. It also checks that neither the root's own references nor the direct reference's list the plugin, so the graph needs the full walk.
  • Collect_OrdersOwnReferencesBeforeTransitiveOnes: the root's own reference directory comes before the transitive plugin directory.
  • Collect_ForHotReloadTestAssembly_ReachesTheCodeAnalysisPluginDirectoryOnlyTransitively: on this project's real hot-reload test assembly, its own references do not cover the code analysis plugin directory, and the walk reaches it.
  • GetOrCreatePublicizedCopy_ResolvesAnAssemblyThatOnlyATransitiveReferenceLists: a generated image with a constant of an enum from an assembly that only a transitive reference lists.
    • With the old search directories, writing the publicized copy throws AssemblyResolutionException.
    • With the new ones, it succeeds.
    • The failing call runs first because a failed write caches nothing.
  • TryBuildShimReferencePaths_PublicizesTheTargetWithTransitiveSearchDirectories: the shim reference build itself, on the same kind of image and graph, returns references instead of the publicize failure.

In HotReloadIntroducedTypeArtifactReferenceBuilderTests:

  • Build_UsesTheResolverSearchDirectoriesItIsGiven: the artifact reference build writes its exposed copies with the directories it is given. With the directories of the worker references, the same build fails with the exposure message.

The artifact-side call site is only reachable through PrepareAsync and cannot fail with this project's DLLs, so it is covered by the introduced-type E2E tests for execution, not for the failure.

Unity EditMode (local, filtered)

  • Red first: the new tests did not compile without the new class (4 errors, all at its 4 call sites).
  • HotReloadResolverSearchDirectoriesTests|ReferencePublicizerTests|HotReloadShimReferenceBuilderTests|InternalsExposedReferenceTests|HotReloadIntroducedTypeArtifactReferenceBuilderTests|HotReloadIntroducedTypeInitializerE2ETests: 61 tests, 60 passed, 1 skipped. The skipped case needs a platform with two directory separators.
  • After the artifact change: HotReloadIntroducedTypeArtifactReferenceBuilderTests|HotReloadIntroducedTypeReferencePathsTests|HotReloadResolverSearchDirectoriesTests|HotReloadIntroducedTypeInitializerE2ETests|HotReloadIntroducedTypeDynamicCodeCompilationE2ETests|HotReloadIntroducedTypeInternalAccessE2ETests: 52 tests, 51 passed, 1 skipped (the same case).
    • The E2E classes run the shim reference build and the introduced-type artifact compile on the real test assembly.
    • HotReloadIntroducedTypeInternalAccessE2ETests covers the artifact compile with internals exposed.
  • After adding the two call-site tests, both filters together (9 classes): 86 tests, 85 passed, 1 skipped (the same case).
  • Mutation checks, each reverted afterwards:
    • With the walk stopped at the root assembly, the 4 Collect and publicizer tests fail, each for the expected reason (2 missing directories, a missing plugin index, and AssemblyResolutionException).
    • With the old search directories put back at the two production call sites, exactly the 2 call-site tests fail: the shim one with "Publicizing referenced assemblies failed", and the artifact one on the build that is given the directories.

Static checks (local)

  • uloop compile: 0 errors.
  • uloop compile-check: 0 errors. Its 7 warnings are in unchanged test fixtures.
  • asmdef reference policy: no violation.
  • Complexity, failing on findings:
    • Go: 0 issues in each module.
    • C#: no finding above 15. It reports 1011 findings at threshold 5, so the scan was not empty; the changed methods are at 6 or below.
  • File length: no file over 500 SLOC.

…tively referenced assembly

Cecil resolves the assemblies a publicized copy refers to while it writes the copy, and it only
searched the directories of the group assembly's own compile references. Unity's allReferences
is not transitive, so a test assembly whose asmdef overrides its references could not resolve a
precompiled DLL that only a referenced game assembly lists, and the file failed with
"Publicizing referenced assemblies failed".

The search directories now come from a breadth-first walk of the referenced assemblies, with the
assembly's own references first so a same-named DLL keeps resolving to the same copy. Reference
paths seen earlier in the walk are skipped before File.Exists, because the engine references
repeat in every assembly. The shim's compile references are unchanged.
…ctories

The internals-exposed copies for an introduced-type artifact are written by Cecil too, and their
search directories came from the worker's reference list, which holds only the group assembly's
own compile references. The artifact compile now receives the directories of every transitively
referenced assembly, collected from the group's compilation assembly the same way as for the shim
references, so both compiles resolve the same precompiled DLLs.
@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: hatayama/unity-cli-loop/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fdf6b465-d00f-448d-849a-5d9e25af3890
📥 Commits

Reviewing files that changed from the base of the PR and between 882a5a2 and 882a5a2.

⛔ Files ignored due to path filters (2)
  • Assets/Tests/Editor/HotReload/HotReloadResolverSearchDirectoriesTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadResolverSearchDirectories.cs.meta is excluded by none and included by none
📒 Files selected for processing (6)
  • Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactReferenceBuilderTests.cs
  • Assets/Tests/Editor/HotReload/InternalsExposureTestImage.cs
  • Assets/Tests/Editor/HotReload/PublicizerTestSearchDirectories.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadIntroducedTypeArtifactReferenceBuilder.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadShimReferenceBuilder.cs
 _______________________________________________
< Show me the code! And I'll show you the bugs. >
 -----------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: hatayama/unity-cli-loop/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d467b6af-dccd-477d-99e1-e0f35b789534
📥 Commits

Reviewing files that changed from the base of the PR and between 3f5cdea and 882a5a2.

📒 Files selected for processing (2)
  • Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactReferenceBuilderTests.cs
  • Assets/Tests/Editor/HotReload/HotReloadResolverSearchDirectoriesTests.cs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

The change adds breadth-first collection of resolver directories from transitive assembly references. Hot-reload artifact and shim builders use the collected directories. Editor tests cover ordering and transitive resolution, and the documentation records Cecil’s lookup order.

Changes

Hot-reload resolver directory flow

Layer / File(s) Summary
Transitive resolver directory collection
Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadResolverSearchDirectories.cs
Adds breadth-first traversal of the compilation assembly’s reference graph. The collector deduplicates paths and preserves root directories before transitive directories.
Reference builder integration
Packages/src/Editor/FirstPartyTools/HotReload/Patching/*, Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs, Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactReferenceBuilderTests.cs
Passes collected resolver directories to artifact and shim builders. Exposed-copy validation rejects null collections and uses the supplied directories for Cecil resolution.
Resolver flow validation
Assets/Tests/Editor/HotReload/HotReloadResolverSearchDirectoriesTests.cs, Assets/Tests/Editor/HotReload/InternalsExposureTestImage.cs, Assets/Tests/Editor/HotReload/PublicizerTestSearchDirectories.cs, docs/hot-reload.md
Adds tests for transitive directories, ordering, and exposed-copy resolution. Updates fixture cleanup and documents Cecil’s lookup order.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant CompilationAssembly
  participant HotReloadResolverSearchDirectories
  participant HotReloadIntroducedTypeArtifactReferenceBuilder
  participant Cecil
  CompilationAssembly->>HotReloadResolverSearchDirectories: Collect reference directories
  HotReloadResolverSearchDirectories-->>HotReloadIntroducedTypeArtifactReferenceBuilder: Return ordered directories
  HotReloadIntroducedTypeArtifactReferenceBuilder->>Cecil: Resolve exposed-copy references
Loading

Merge Risk: ⚪ Minimal · up to 882a5

No actionable merge-blocking risk is identified; the change is ready for normal merge checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 882a5

Dependency lookup expands within the existing Unity project workflow without adding a public interface or new execution privileges. No introduced security weakness was demonstrated. Correct reuse of generated files when dependency locations change remains unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The visible expansion concerns metadata lookup for generated references in the current Unity project. Influencing newly searched locations requires control of dependency files or compilation-reference metadata. No new caller authority is demonstrated, but directory provenance does not establish an exact allowlist of DLLs Cecil may resolve.

Trust Boundaries and Controls

  • observed — The resolver retains source-directory and ScriptAssemblies lookup before supplied extra directories. Publicizable-home and accepted-source-path checks remain Debug.Assert checks, not newly introduced runtime rejection controls. These mechanisms are unchanged from the full PR base.

Resilience and Maintainability Implications

  • observed — The unchanged writer uses assembly name plus source MVID for cache identity, publishes through a unique temporary file followed by a move, cleans temporary files on ordinary failure, and regenerates zero-byte final files. Nonzero cache hits do not incorporate resolver inputs. Dependency-sensitive equivalence, concurrent publication and process-termination cleanup remain unverified rather than demonstrated PR regressions.

Hardening Proposals

  • proposed — Validate cache equivalence when an unchanged source MVID resolves against different same-name dependency files. If generated metadata depends on the selected dependency, bind reuse to that dependency identity or invalidate the copy when it changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: resolving plugin DLLs that are referenced only by another assembly during hot reload.
Description check ✅ Passed The description explains the failure, its cause, the changes, and the reported tests. It is directly related to the pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The new tests called Collect and the publicizer directly, so putting the old search directories
back at either production call site left every test green. One test now builds shim references
for a target whose metadata needs an assembly only a transitive reference lists, and another
checks that the artifact reference build writes its exposed copies with the directories it is
given rather than ones derived from the worker references.
@hatayama
hatayama changed the base branch from main to feature/hot-reload-large-project-feedback October 6, 2026 06:52
@hatayama
hatayama merged commit 37673bd into feature/hot-reload-large-project-feedback Oct 6, 2026
16 of 17 checks passed
@hatayama
hatayama deleted the fix/hot-reload-transitive-resolver-search-directories branch October 6, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant