Summary
PostSharp''s compiler honors the MSBuild Deterministic property (PostSharp.properties maps $(Deterministic) to PostSharpDeterministic, passed to the PostSharp task), and Tests/Core/TestDeterministicBuild verifies that two builds with Deterministic=True produce byte-identical PE and PDB — it passes, including the .NET Framework / Windows-PDB case.
However, that test only covers the plain weaver. When Patterns weavers run (Common / Model / Threading), the output is not reproducible: two rebuilds with identical inputs and Deterministic=true produce different PE and PDB files.
Repro (PostSharp 2024.0, branch topic/2024.0/test-reorganization)
Build UserInterface\PostSharp.Settings.UI\PostSharp.Settings.UI.csproj twice and compare outputs:
pwsh Build/Scripts/msbuild.ps1 -VisualStudioVersion <installed> UserInterface\PostSharp.Settings.UI\PostSharp.Settings.UI.csproj /t:Rebuild /p:Configuration=vsix
# copy bin\vsix\PostSharp.Settings.UI.exe + .pdb aside, rebuild, compare hashes
- The project is woven by the Patterns weavers (build emits COM014 / THR027 warnings).
- Verified via binlog that both
csc /deterministic+ and the PostSharp task parameter Deterministic=True were in effect in both builds.
PostSharp.Settings.exe and PostSharp.VisualStudio.Debugging.Server.dll from the same solution (not aspect-woven, plain csc) are byte-identical across rebuilds — the difference is specific to the woven assemblies (PostSharp.Settings.UI.exe, PostSharp.VisualStudio.Package.VS16.0/VS17.0.dll).
Observed difference
The PE differs only in hash-derived/build-id fields — everything else is byte-identical:
- COFF
TimeDateStamp (offset 0x88 in the repro binary)
- PE checksum region
- debug directory data (~128 bytes: CodeView GUID/age and the
PdbChecksum entry)
- MVID
- the Windows (full) PDB differs as well
The pattern suggests the deterministic output-hashing path is bypassed or salted somewhere in the Patterns weaving pipeline — plausibly the Windows PDB writing in that path produces a fresh signature, which then cascades into the PE''s derived fields via PdbChecksum.
Impact
This blocks strict content comparison of the rebuilt UserInterface assemblies in the signed/unsigned distribution equivalence gate (Build/Distribution/Compare-SignedDistribution.ps1): the UserInterface solution is rebuilt in every distribution build (TouchArtifacts forces it), so the woven assemblies + their PDBs + the VSIX catalog.json/manifest.json (which embed their hashes) are currently exempted to presence-only comparison (-RebuiltComponentNames; see Documentation/Distribution.md, section "Rebuilt components"). More generally, any customer relying on deterministic builds does not get reproducible output for aspect-woven projects.
Suggested fix path
- Find where determinism is lost in the weave pipeline when Patterns weavers / Windows PDB writing are involved.
- Extend
Tests/Core/TestDeterministicBuild with a Patterns-aspect test case (it currently only covers the plain weaver, which is why this regressed silently).
Root cause (resolved)
Found and fixed in commit 1dadc2f95c on topic/2024.0/test-reorganization. The Patterns weavers turned out to be incidental - the defect is in the compiler's Windows-PDB writing path and reproduces with a plain MethodInterceptionAspect as well.
MetaDataEmitImpl.GetTypeDefProps / GetMethodProps (the IMetaDataImport shim that the unmanaged symbol writer, diasymreader, queries for type/method names) reported name lengths in bytes instead of wide characters. For woven types whose fully qualified name exceeds 127 characters, diasymreader then falls back to naming the PDB module after a heap address, which is different on every build. The nondeterministic PDB content cascades exactly as suspected in the summary: PDB content hash (pdbId) feeds the CodeView GUID and debug-directory timestamp, which feed the PE image hash, which produces the MVID, COFF TimeDateStamp, PE checksum, and strong-name signature.
The trigger in PostSharp.Settings.UI is the binding type generated for the [Dispatched] explicit interface implementation IProgressTaskObserver.SetStatusText in ProgressPage: <PostSharp.Settings.Wizard.Progress.IProgressTaskObserver.SetStatusText>z__Binding, whose fully qualified name is 133 characters. A second cliff exists at diasymreader's own internal buffer (empirically between 251 and 271 characters), where it takes the same fallback regardless of the reported length - names handed to the symbol writer are therefore now deterministically clamped to 250 characters.
Verification: PostSharp.Settings.UI rebuilds are byte-identical (PE and PDB); Tests/Core/TestDeterministicBuild was extended with a long-name explicit-interface-implementation scenario (plain aspect, no Patterns dependency) that fails on the previous compiler and passes now. The signed/unsigned gate's presence-only exemption (-RebuiltComponentNames) stays in place until a signed/unsigned chain run on a fixed compiler confirms the rebuilt UI assemblies are byte-identical end-to-end.
The same defect also corrupts PDB module names in non-deterministic Windows-PDB builds (cosmetic, no debugging impact) - tracked separately as #36.
Summary
PostSharp''s compiler honors the MSBuild
Deterministicproperty (PostSharp.propertiesmaps$(Deterministic)toPostSharpDeterministic, passed to the PostSharp task), andTests/Core/TestDeterministicBuildverifies that two builds withDeterministic=Trueproduce byte-identical PE and PDB — it passes, including the .NET Framework / Windows-PDB case.However, that test only covers the plain weaver. When Patterns weavers run (Common / Model / Threading), the output is not reproducible: two rebuilds with identical inputs and
Deterministic=trueproduce different PE and PDB files.Repro (PostSharp 2024.0, branch
topic/2024.0/test-reorganization)Build
UserInterface\PostSharp.Settings.UI\PostSharp.Settings.UI.csprojtwice and compare outputs:csc /deterministic+and the PostSharp task parameterDeterministic=Truewere in effect in both builds.PostSharp.Settings.exeandPostSharp.VisualStudio.Debugging.Server.dllfrom the same solution (not aspect-woven, plain csc) are byte-identical across rebuilds — the difference is specific to the woven assemblies (PostSharp.Settings.UI.exe,PostSharp.VisualStudio.Package.VS16.0/VS17.0.dll).Observed difference
The PE differs only in hash-derived/build-id fields — everything else is byte-identical:
TimeDateStamp(offset 0x88 in the repro binary)PdbChecksumentry)The pattern suggests the deterministic output-hashing path is bypassed or salted somewhere in the Patterns weaving pipeline — plausibly the Windows PDB writing in that path produces a fresh signature, which then cascades into the PE''s derived fields via
PdbChecksum.Impact
This blocks strict content comparison of the rebuilt UserInterface assemblies in the signed/unsigned distribution equivalence gate (
Build/Distribution/Compare-SignedDistribution.ps1): the UserInterface solution is rebuilt in every distribution build (TouchArtifactsforces it), so the woven assemblies + their PDBs + the VSIXcatalog.json/manifest.json(which embed their hashes) are currently exempted to presence-only comparison (-RebuiltComponentNames; seeDocumentation/Distribution.md, section "Rebuilt components"). More generally, any customer relying on deterministic builds does not get reproducible output for aspect-woven projects.Suggested fix path
Tests/Core/TestDeterministicBuildwith a Patterns-aspect test case (it currently only covers the plain weaver, which is why this regressed silently).Root cause (resolved)
Found and fixed in commit
1dadc2f95contopic/2024.0/test-reorganization. The Patterns weavers turned out to be incidental - the defect is in the compiler's Windows-PDB writing path and reproduces with a plainMethodInterceptionAspectas well.MetaDataEmitImpl.GetTypeDefProps/GetMethodProps(theIMetaDataImportshim that the unmanaged symbol writer, diasymreader, queries for type/method names) reported name lengths in bytes instead of wide characters. For woven types whose fully qualified name exceeds 127 characters, diasymreader then falls back to naming the PDB module after a heap address, which is different on every build. The nondeterministic PDB content cascades exactly as suspected in the summary: PDB content hash (pdbId) feeds the CodeView GUID and debug-directory timestamp, which feed the PE image hash, which produces the MVID, COFFTimeDateStamp, PE checksum, and strong-name signature.The trigger in
PostSharp.Settings.UIis the binding type generated for the[Dispatched]explicit interface implementationIProgressTaskObserver.SetStatusTextinProgressPage:<PostSharp.Settings.Wizard.Progress.IProgressTaskObserver.SetStatusText>z__Binding, whose fully qualified name is 133 characters. A second cliff exists at diasymreader's own internal buffer (empirically between 251 and 271 characters), where it takes the same fallback regardless of the reported length - names handed to the symbol writer are therefore now deterministically clamped to 250 characters.Verification:
PostSharp.Settings.UIrebuilds are byte-identical (PE and PDB);Tests/Core/TestDeterministicBuildwas extended with a long-name explicit-interface-implementation scenario (plain aspect, no Patterns dependency) that fails on the previous compiler and passes now. The signed/unsigned gate's presence-only exemption (-RebuiltComponentNames) stays in place until a signed/unsigned chain run on a fixed compiler confirms the rebuilt UI assemblies are byte-identical end-to-end.The same defect also corrupts PDB module names in non-deterministic Windows-PDB builds (cosmetic, no debugging impact) - tracked separately as #36.