From 9326c04f7dfa41cebeebaf32278502ed7e54a0de Mon Sep 17 00:00:00 2001 From: Ryan L'Italien Date: Tue, 25 Aug 2026 15:21:15 -0400 Subject: [PATCH] unreal: distill headless UBT/UHT compile learnings Distillation pass per DISTILLING.md, from a private-repo entry covering a container-based UE 5.7 compile path stood up on a Windows + WSL2 host. Section A: installed engines refuse targets that change shared build settings (and UBT's own suggested fix is impossible there); UHT cannot parse a nested enum used in a UFUNCTION signature; one missing #include masquerades as missing types and broken inheritance; Error_UnknownCookFailure can be plugin drift. Section B: WSL2 df overreporting free space from a sparse vhdx, Windows OpenSSH killing detached children, a dead registry credential reading as a missing tag, and bounded preflight checks with honest exit codes. Skills: unreal-observe gains 3 failure-signature rows, unreal-build gains 2 preflight checks. Sanitized per DISTILLING.md (a): no internal hostnames, machine names, personal names, project codenames, or absolute paths. Compressed 32% against the private source, within the 30-50% target in (b). Co-Authored-By: Claude Opus 5 (1M context) --- unreal/LEARNINGS.md | 53 +++++++++++++++++++++++++++ unreal/skills/unreal-build/SKILL.md | 9 +++++ unreal/skills/unreal-observe/SKILL.md | 3 ++ 3 files changed, 65 insertions(+) diff --git a/unreal/LEARNINGS.md b/unreal/LEARNINGS.md index 8050978..65b45ec 100644 --- a/unreal/LEARNINGS.md +++ b/unreal/LEARNINGS.md @@ -47,6 +47,33 @@ No secrets - reference credential *locations*, never paste them. clean on UE 5.8 (496/503 packages, 0 err/warn) using the same ZenServer/ `FShaderJobCache` grammar - confirming the fix generalizes rather than being tuned to one project's log shape. +- **An installed (binary) engine refuses a target that changes shared build settings, and + UBT's own suggested fix is the trap.** A `.Target.cs` whose `DefaultBuildSettings` / + `IncludeOrderVersion` differ from the installed engine fails in seconds, before compiling + anything: ` modifies the values of properties: [ : A != B ]. This is not + allowed, as has build products in common with UnrealEditor`, with + `CompilationResult=6`. UBT suggests `BuildEnvironment = TargetBuildEnvironment.Unique`, + which means "compile the engine too" and is impossible on an installed or container + engine. Align the target to the engine instead. Now both a signature row and a preflight + check, since it is cheap to spot in `Source/*.Target.cs` before a build starts. +- **One missing `#include` masquerades as missing types *and* as broken inheritance.** + Headers using a shared struct header without including it produced `unknown type name + ''` for a struct that **is** defined, `cannot initialize object parameter of + type ''`, and a `TIsDerivedFrom<...>::IsDerived` static_assert against a class that + **does** derive from that base. Two includes collapsed ~30 of 57 error lines. The durable + lesson is ordering: fix includes and rebuild *before* believing any "unknown type" or + "not derived from" claim. +- **UHT cannot parse a nested enum used in a `UFUNCTION` signature**, and says so + misleadingly: `Unable to find 'class', 'delegate', 'enum', or 'struct' with name 'X'` + paired with `C++ Default parameter not parsed`. The type is usually a few lines above, + declared inside the UCLASS. Hoist it to file scope and tag it `UENUM()`. +- **`Error_UnknownCookFailure` (25) is not always missing content.** A Blueprint-only + template project on a *matched* engine failed with `Could not find a function named "..." + in 'X'` and `In use pin ... no longer exists on node`, plus `Failed to find script package + for import object 'Package /Script/'` - Blueprint nodes outliving a plugin no + longer enabled by default. When the missing import is a `/Script/` package rather + than a `/Game/` asset, check plugin enablement, not an unsynced file. Related preflight: a + project with no `.umap`/`.uasset` has nothing to cook at all - compile it instead. ## B. Operational / rig learnings (standing up a real-engine test rig) @@ -91,3 +118,29 @@ No secrets - reference credential *locations*, never paste them. was roughly a third smaller than the full one, still shipped a working compiler toolchain and editor, and only dropped debug symbols - a good default unless you specifically need those symbols. +9. **A WSL2 `df` reports free space that does not exist.** The WSL ext4 volume is a sparse + virtual disk on the Windows drive: `df` inside WSL showed ~895 GB free while the host + volume had **19 GB**. Disk prechecks on a WSL-hosted rig must read the Windows volume + (`df /mnt/c`), never the WSL root, or a cook sized against the WSL number fills the host + drive. +10. **Windows OpenSSH kills detached children when the SSH session closes** - the + Windows-side counterpart to item 5. A hidden `Start-Process` launched over SSH appears + to start, then silently produces nothing. Launch long jobs as scheduled tasks + (`schtasks /Create ... /SC ONCE /RL HIGHEST`, then `schtasks /Run`), which outlive the + session. +11. **A dead registry credential is indistinguishable from a missing tag, and image + preflights need bounds.** With an expired token, `docker manifest inspect` returns + `denied: denied` for *every* tag, including images already present locally - which reads + as "this tag does not exist" and produced a wrong conclusion about which engine images + are published. Verify auth against a known-present image before calling any tag absent. + `docker login` also writes to the **invoking user's** config, so automation running as + another user keeps its own stale token - point at the good config with `DOCKER_CONFIG=` + rather than copying the credential (see item 3). And bound the check itself: `docker + image inspect` is normally instant but blocks on the content-store lock while a + multi-GB layer commits, and an SSH failure (255) or timeout (124) says **nothing** about + whether the image exists - never report those as "image missing". +12. **Windows sshd ignores the per-user `authorized_keys` for administrators.** With the + default `Match Group administrators` block it reads only + `%ProgramData%\ssh\administrators_authorized_keys`; a per-user key is silently ignored + and fails as a plain `Permission denied (publickey)`. That file also needs inheritance + removed and its ACL limited to SYSTEM + Administrators. diff --git a/unreal/skills/unreal-build/SKILL.md b/unreal/skills/unreal-build/SKILL.md index 8654a7f..2083c83 100644 --- a/unreal/skills/unreal-build/SKILL.md +++ b/unreal/skills/unreal-build/SKILL.md @@ -41,6 +41,15 @@ beats a re-cook; a FAST_COOK tracer (§2) beats a full pipeline (§3). parses cleanly. 4. **Capture the log**: pipe through `tee /cook_output_.log` - the metrics and triage in `unreal-observe` §7 read from it. +5. **Is there anything to cook?** If the project has **no `.umap`/`.uasset`** (a + code-only or scaffold project whose Content is placeholders), a cook has nothing + to do and its failure teaches nothing - use §5 UBT compile instead. Check before + proposing a cook, not after it fails. +6. **On an installed (binary) engine, check target build settings match.** An editor + target that sets `DefaultBuildSettings`/`IncludeOrderVersion` differently from the + installed engine is refused outright by UBT in seconds (`unreal-observe` §8). Read + them out of `Source/*.Target.cs` during the doctor pass, while a mismatch is still + cheap to spot. ## 1. Invocation forms - Windows vs Linux/Mac diff --git a/unreal/skills/unreal-observe/SKILL.md b/unreal/skills/unreal-observe/SKILL.md index 55e09be..90d9e4f 100644 --- a/unreal/skills/unreal-observe/SKILL.md +++ b/unreal/skills/unreal-observe/SKILL.md @@ -272,6 +272,9 @@ don't manufacture findings. Otherwise map these distinctive strings straight to | Cook: `GetLastError=206` · *"The filename or extension is too long"* · SavePackage *"Could not create file ..."* | Windows **MAX_PATH** (260) - see §6 | shorten the project root (only reliable fix); *count the failing path first* - under 260 ⇒ it's something else | | `LogDerivedDataCache: Warning: ... unreachable ... falling back to Local` (usually with a later `Hit Rate: 0.x%`) | shared DDC backend down ⇒ every shader recompiled from source | restore DDC connectivity (DNS/firewall/service) before the next cook; this build's *output* is fine - don't re-run it, and don't kill a cook mid-compile | | `LogUObjectGlobals`/`LogLinker`: *"Could not find file for package"* · *"Failed to load ''"* · a dangling object-property reference, then `Can't cook ` | a referenced content asset is missing from the workspace | on a Perforce team this is usually unsubmitted or unsynced - `p4 have ` (synced?), `p4 files ` (in depot at all?), `p4 opened` (an unsubmitted add?) - *then* hand the fix to the perforce plugin, or clear/repoint the reference via `unreal-editor-scripting` | +| UBT, in seconds, with **no** compilation: ` modifies the values of properties: [ : A != B ]. This is not allowed, as has build products in common with UnrealEditor` (`CompilationResult=6`) | the target's `.Target.cs` sets `DefaultBuildSettings` / `IncludeOrderVersion` differing from the **installed** engine's own build environment; shared build products cannot disagree | align `DefaultBuildSettings` / `IncludeOrderVersion` in `Source/*.Target.cs` to the engine actually in use. Do **NOT** apply UBT's own suggested `BuildEnvironment = TargetBuildEnvironment.Unique` - that's the trap: Unique requires an engine **source** build and is impossible on an installed or container engine | +| UHT: `Unable to find 'class', 'delegate', 'enum', or 'struct' with name 'X'` **paired with** `C++ Default parameter not parsed: 'X::'` | `X` is an `enum class` declared **nested inside** the UCLASS and used in a `UFUNCTION` signature - UHT does not parse nested UENUMs | hoist the enum to **file scope** and tag it `UENUM()`. The "unable to find" wording is misleading - the type is usually a few lines above the error, just not where UHT can see it | +| `unknown type name ''` where the struct **is** defined; and/or `cannot initialize object parameter of type ''`; and/or a `TIsDerivedFrom<..., >::IsDerived` static_assert on a class that **does** derive from `` | a missing `#include` of the header defining those types - one incomplete type cascades into bogus "missing type" and "wrong base class" errors | grep for the definition before believing the error. Add the `#include`, then **rebuild before triaging anything else** - include cascades routinely account for half the error lines in a run | **Exit codes** name *which stage* failed; always pair the code with the first `Error:`/`Fatal:` line, which names *why*: AutomationTool `0` = success, `25` =