Skip to content

PS0264 when the project supersedes a compiler host dependency: exclusion set not computed on the cached deps.json path #76

Description

@gfraiteur

Symptom

Weaving fails with PS0264 when the compiled project references a package at a higher version than the one PostSharp's own compiler host depends on:

error PS0264: Could not load file or assembly
'~/.nuget/packages/system.security.cryptography.protecteddata/10.0.0/lib/net10.0/System.Security.Cryptography.ProtectedData.dll'.
The located assembly's manifest definition does not match the assembly reference. (0x80131040)

Reported by a customer on PostSharp 2026.0.10, .NET 10 host, project referencing System.Security.Cryptography.ProtectedData 10.0.0. Any package shared between the compiled project's graph and the compiler's own graph can trigger it.

The failure is not first-build; it appears on a later build and is then deterministic until obj/ is cleaned. A CI that always starts from a clean obj/ never sees it, which makes it look platform-specific when it is not.

Root cause

ProjectDepsBuilder.Build computes upgradedHostRuntimeDependencies — the set of compiler host runtime assemblies superseded by a higher version from the compiled project's graph. CompilerRuntimeDirectoryBuilder.Build uses that set to avoid copying the compiler's own lower-versioned copy into the project's intermediate directory (Core/PostSharp.MSBuild/NetCore/CompilerRuntimeDirectoryBuilder.cs, lines 64-68).

The set is computed only inside the deps.json regeneration branch (Core/PostSharp.MSBuild/NetCore/ProjectDepsBuilder.cs, lines 346-367). When the cached deps.json is up to date, the method returns early at lines 185-192 with the out parameter left at its initial null (line 141). The exclusion is therefore silently skipped on every build that reuses the cached deps.json.

Sequence:

  1. Build 1 (after restore) regenerates the deps.json, computes the set, correctly skips the file.
  2. Build 2 reuses the cached deps.json, gets null, and copies System.Security.Cryptography.ProtectedData.dll (AssemblyVersion 4.0.2.0, from our NuGet.Packaging -> NuGet.Configuration chain) into the intermediate directory.
  3. Nothing ever removes it — CompilerRuntimeDirectoryBuilder enumerates the tool directory and skips entries; it never sweeps the destination.
  4. The compiler host is executed from that same intermediate directory (BuildClient.cs line 891 sets finalToolPath there), so the stale assembly sits next to the host as an app-directory assembly.
  5. The weaver's later AssemblyLoadContext.Default.LoadFromAssemblyPath for the project's 10.0.0.0 file fails with FUSION_E_REF_DEF_MISMATCH (0x80131040).

Note that the PS0265 log shows "is not yet loaded in the AppDomain" rather than "An assembly with the same name ... is loaded in the application domain, but it's identity doesn't match" (RuntimeAssemblyLocator.cs lines 224-233). The conflicting assembly is therefore never actually loaded — the conflict is at the host resolution level, not between two loaded assemblies.

What is verified vs. inferred

Verified:

  • Steps 1-2 are reproduced by an automated test (see below).
  • System.Security.Cryptography.ProtectedData.dll at AssemblyVersion 4.0.2.0 does ship in the compiler tools directory; confirmed against a local build output.
  • Steps 3-4 are established by code inspection.

Inferred, not directly observed:

  • Step 5 — that the app-directory copy is what wins host assembly resolution over the deps.json entry pointing at the 10.0.0 package path. This is not reproduced here. It is however exactly the rationale recorded in the existing mitigation's own comment (ProjectDepsBuilder.cs lines 346-348: "these should not be included in the compiler runtime directory because .NET 8.0+ runtimes tend to load them even when they should be replaced by higher versions"), so the mechanism is established by prior art in this codebase. Confirming it against a live repro would be worthwhile before closing.

Reproduction

Tests/Core/PostSharp.Compiler.Client.Test/ProjectDepsBuilderTests.cs, two tests over one fixture:

  • UpgradedHostRuntimeDependencies_AreReported_WhenDepsFileIsGeneratedpasses, pinning the intended behaviour on the fresh path.
  • UpgradedHostRuntimeDependencies_AreReported_WhenDepsFileIsCachedfails:
The set of upgraded host runtime dependencies was not computed when the deps.json was taken from the cache.
Expected: not null
But was:  null

The fixture is self-contained (temporary directory, synthetic package folder, synthetic host deps.json and project.assets.json); it needs neither network nor the local NuGet cache.

Proposed fix

  1. Compute upgradedHostRuntimeDependencies before the cache short-circuit. It only needs the host deps.json and the merged dependency set, both cheap to obtain; alternatively persist it alongside the cached deps.json.
  2. Remove a superseded copy already present in the intermediate directory, so machines already in the broken state recover without manual intervention.

Workaround

Delete the stale assembly from the intermediate directory and force one regeneration:

rm obj/<Configuration>/<TargetFramework>/System.Security.Cryptography.ProtectedData.dll
POSTSHARPHOST_DISABLE_CACHE=true dotnet build

The environment variable forces the deps.json to be regenerated, which recomputes the exclusion set. It must be kept set (or the deletion scripted as a pre-build step) until a fixed build is available, because the very next cached build re-plants the file.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions