Diagnosing an elusive build problem

Diagnosing an elusive build problem

After a day of pair programming, we ended up with a lot of new code and blueprints in our project. We submitted all code to our VCS, and waited for the inevitable “OK” from the build automation. Sadly, it never came; instead, we went from 0 errors (and a few warnings–more on that later) to 127 errors! Seeing 127 errors, we first suspected a bug in our automation driver, 127 is a really suspicious number. Sadly, even running the automation scripts on our development machines produced the same error.

C:\'Program Files\Epic Games\UE_5.0\Engine\Build\BatchFiles'\RunUAT.bat 
    BuildCookRun 
    -installed 
    -nop4 
    -build 
    -project="...\Talisker.uproject" 
    -cook
    -CookAll 
    -ForceMonolithic
    -stage
    -archive
    -pak
    -archivedirectory="..." 
    -package
    -map=/Game/Maps/L_World+/Game/Maps/L_MainMenu
    -ddc=InstalledDerivedDataBackendGraph
    -nullrhi 
    -prereqs 
    -targetplatform=Win64 
    -target=Talisker
    -configuration=Shipping 
    -serverconfig=Shipping       
    -clientconfig=Shipping         
    -utf8output

Produced an error? But what about the remaining 126 errors? Well, it turns out that it was all just one error, but with lots and lots of lines which all counted towards the scary number.

...
Warning/Error Summary (Unique only)
-----------------------------------
Error: begin: stack for UAT
Error: === Handled ensure: ===
Error:
Error: Ensure condition failed: !GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native) [...\UObjectGlobals.cpp] [Line: 2446]
Error: Stack:
Error: [Callstack] 0x00007ffb595740bc UnrealEditor-CoreUObject.dll!StaticAllocateObject() [...\UObjectGlobals.cpp:2446]
Error: [Callstack] 0x00007ffb592ac2f8 UnrealEditor-CoreUObject.dll!UClass::CreateDefaultObject() [...\Class.cpp:3882]
...
Error: [Callstack] 0x00007ffb5955eed0 UnrealEditor-CoreUObject.dll!FObjectInitializer::PostConstructInit() [...\UObjectGlobals.cpp:3012]
Error: [Callstack] 0x00007ffb5952d79a UnrealEditor-CoreUObject.dll!FObjectInitializer::~FObjectInitializer() [...\UObjectGlobals.cpp:2885]
...
LogInit: Display: NOTE: Only first 50 errors displayed.

Unpicking !GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native)

LogInit: Display: NOTE: Only first 50 errors displayed. sic! Searching the web revealed a post on Blueprint circular dependencies, which looked like it could be describing our problem. We deduced that it was caused by a blueprint, but which one?! It was a very productive day, and we had lots of new blueprints added; we verified that the previous revision builds successfully, so it had to be one of the newly added blueprints. We opened the blueprints in the editor to look for dependencies between them–even though we were certain we did not create any dependency loops. And surely, we did not discover anything suspicious.

Unfortunately, the error in the build does not provide any hints. So, we turned to Unreal Engine’s source code. Our first attempt was to modify the StaticAllocateObject in UObjectGlobals.cpp:2446 to log more details about what was causing the problem.

UObject* StaticAllocateObject
(
    const UClass*    InClass,
    UObject*        InOuter,
    FName            InName,
    EObjectFlags    InFlags,
    EInternalObjectFlags InternalSetFlags,
    bool bCanRecycleSubobjects,
    bool* bOutRecycledSubobject,
    UPackage* ExternalPackage
)
{
    ...

    if (bCreatingCDO)
    {
        check(InClass->GetClass());

        // Debugging help vvv
        checkf(!GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native), 
            TEXT("Possible Blueprint compilation cycle detected in %s."), 
            *GetNameSafe(InClass));
        // Debugging help ^^^

        ensure(!GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native));
        InName = InClass->GetDefaultObjectName();
        // never call PostLoad on class default objects
        InFlags &= ~(RF_NeedPostLoad|RF_NeedPostLoadSubobjects);
    }

    ...
}

We built the engine, and ran our automation command locally again. We got the same error (good!–at least we knew that the error was consistently there), but the name of the class was in our case a BP_TextStyle_Generic_C. That was a little strange: while BP_TextStyle_Generic_C is indeed one of our blueprints, but it was not modified in the failing revision, and it is a subclass of UCommonTextStyle. And it naturally has no circular dependencies. So, this debugging addition did not help. The next method that looked like a good debugging point was FBlueprintCompileReinstancer::MoveCDOToNewClass. We added a simple warning log message

UClass* FBlueprintCompileReinstancer::MoveCDOToNewClass(UClass* OwnerClass, const TMap<UClass*, UClass*>& OldToNewMap, bool bAvoidCDODuplication)
{
    GIsDuplicatingClassForReinstancing = true;
    OwnerClass->ClassFlags |= CLASS_NewerVersionExists;

    UE_LOG(LogBlueprint, Warning, TEXT("MoveCDOToNewClass: %s."), *GetNameSafe(OwnerClass));

    ...
}

This finally produced something more reasonable:

Warning: MoveCDOToNewClass: WBP_TransitionWidget_C
Warning:

Script Stack (0 frames):

Error: begin: stack for UAT
Error: === Critical error: ===
Error:
Error: Assertion failed: !GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native) [...\UObjectGlobals.cpp] [Line: 2450]
Error: [Callstack] 0x00007ffa3b7e5f6f UnrealEditor-CoreUObject.dll!StaticAllocateObject() [...\UObjectGlobals.cpp:2448]
...

The MoveCDOToNewClass: WBP_TransitionWidget_C was immediately followed by the !GIsDuplicatingClassForReinstancing || InClass->HasAnyClassFlags(CLASS_Native) hard error. The WBP_TransitionWidget extends UTransitionWidget (if you excuse the Blueprint and C++ notation) was indeed one of the added blueprints. Deleting it resolved the error. We were able to delete it because it was only referenced by a C++ class, and the reference was a UPROPERTY(Config) TSubclassOf<UTransitionWidget> field. It was only a very small blueprint with some trivial animation, so re-creating it won’t be a problem. We recreated it in another blueprint and built the project. Success! Then we remembered to also change the configuration property, and the error was back!

The cause

It turned out that saying that the error was in the blueprint was not accurate. We ultimately traced it down to the UPROPERTY(Config) field in the UAsyncAction_Transition class. Removing the configuration and loading the class explictly in UAsyncAction_Transition::Activate() resolved the build problem.

UCLASS(Config=Game)
class TALISKER_API UAsyncAction_Transition : public UBlueprintAsyncActionBase {
    ...
    UPROPERTY(Config) // <-- this was the culprit
    TSubclassOf<UTransitionWidget> DefaultTransitionWidgetClass;
}

Summary

The golden bullet that helped us find the problem was the additional log message in FBlueprintCompileReinstancer::MoveCDOToNewClass in Unreal Engine source code Source\Editor\UnrealEd\Kismet2\KismetReinstanceUtilities.cpp:1698. (Line numbers are valid for Unreal Engine 5.0.3.) From that, we knew where to focus, and resolving the problem then only took a few tens of minutes.

Leave a Reply

Your email address will not be published. Required fields are marked *