Summary
UHT produces two different package body hashes for NetCore from identical inputs, because a parallel-parse race decides whether PushModel.h counts toward the hash. Every whole-target UHT re-run can flip NetCore.init.gen.cpp, which makes UBT rebuild UnrealEditor-NetCore.dll and bump the BuildId with no source change.
What Type of Bug are you experiencing?
Foundation (C++ Tools, Profiling, & Pipeline)
Steps to Reproduce
RunUBT.bat -Mode=UnrealHeaderTool .uproject .uhtmanifest -WriteRef -NoOutput [-IncludeDebugOutput]
Run it repeatedly on the same manifest and compare Engine/Programs/UnrealHeaderTool/Saved/ReferenceExports/CodeGen/NetCore.init.gen.cpp:
Mode Result
default (GoWide) 0x7A7616C1 or 0xFC36040C, random
-NoGoWide always 0x7A7616C1
-EnableInputCacheRead always 0xFC36040C
All ten per-header NetCore/*.gen.cpp outputs are byte-identical between the two results. With -IncludeDebugOutput the header list printed in NetCore.init.gen.cpp differs by exactly one line: Runtime/Net/Core/Public/Net/Core/PushModel/PushModel.h.
Root cause
The package body hash combines the BodyHash of every header with ShouldExport == true (UhtPackageCodeGeneratorCppFile.Generate).
PushModel.h has no reflected types (its USTRUCT/UCLASS are inside an #if 0 example, but enough for UBT to hand it to UHT), so ShouldExport depends only on UhtHeaderFileExportFlags.Referenced.
Referenced is set by the same-module compatibility hack in UhtHeaderFile.AddReferencedHeader when Classes/Net/Serialization/FastArraySerializer.h parses include “Net/Core/PushModel/PushModel.h”. That write goes to the other header’s flags.
UhtSession.StepParseHeaders processes headers in parallel; each header first calls ReadFromCache()/Read(), which calls Reset() → HeaderFileExportFlags = None on itself.
If FastArraySerializer.h finishes parsing before PushModel.h starts, PushModel.h’s own Reset() wipes the flag → excluded from the hash. Otherwise it stays → included.
The re-propagation in UhtHeaderFile.Resolve (UhtResolvePhase.InvalidCheck) would repair this, but it only runs when Session.EnableInputCacheRead is true (off by default, bEnableUHTInputCache = false).
-NoGoWide is deterministic because the sequential order always parses Classes/…FastArraySerializer.h before Public/…PushModel.h, so the flag is always wiped.
Suggested fix (either)
Run the Resolve(InvalidCheck) propagation of the same-module Referenced hack unconditionally (drop the Session.EnableInputCacheRead && condition), or
Call Reset() for all headers in a sequential step before the parallel parse, so a header never clears flags that other headers already set on it.
Workaround
BuildConfiguration.xml → true. The hash is then stable (0xFC36040C) across forced UHT re-runs.
Expected Result
identical inputs produce an identical NetCore.init.gen.cpp; no engine module is rebuilt.
Observed Result
Whenever UHT re-runs for the whole target (makefile invalidated: .Build.cs changed, files added/removed), Engine/Intermediate/Build/Win64/UnrealEditor/Inc/NetCore/UHT/NetCore.init.gen.cpp is sometimes rewritten with a different package body hash (0x7A7616C1 ↔ 0xFC36040C; declarations hash 0x12F0F921 unchanged). No engine source changed. UBT then wants to rebuild UnrealEditor-NetCore.dll and bump the BuildId; with -NoEngineChanges the build fails with FailedDueToEngineChange. In our project this happened in 5 of 11 builds that touched project headers.
Affects Versions
5.8
Platform(s)
Windows