Tooling Recipes

Things that are hard to work out from scratch and needed more than once. Paths are macOS and assume the Unity version this project pins (6000.5.4f1).

Decompile the engine

The most reliable way to settle a question about UI Toolkit, URP or any other engine assembly is to read it. Decompiling the whole assembly once and grepping it beats fishing for type names:

dotnet tool install -g ilspycmd      # once; lands in ~/.dotnet/tools, not on PATH
cd "/Applications/Unity/Hub/Editor/6000.5.4f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine"
~/.dotnet/tools/ilspycmd -o /tmp/uitk -p UnityEngine.UIElementsModule.dll   # whole assembly, ~1070 files
~/.dotnet/tools/ilspycmd -t UnityEngine.UIElements.MaterialDefinition UnityEngine.UIElementsModule.dll

Type and member names found this way are authoritative; line numbers are not — they shift with the decompiler version, so cite the type and member, never a line.

The shader libraries are plain text and need no decompiling:

  • UI Toolkit’s own shader library: /Applications/Unity/Hub/Editor/6000.5.4f1/Unity.app/Contents/Resources/CGIncludes/Internal/UnityUIE.cginc

  • Shader Graph’s UITK target, useful as a worked example of the vertex contract: …​/BuiltInPackages/com.unity.shadergraph/Editor/Generation/Targets/UITK/

Compile C# while Unity holds the project lock

Bee leaves a full response file per assembly — every define, every reference, every source file — from the last successful build, so Roslyn can be run on it directly without waiting for the editor. Nothing is written into the project: point -out at a scratch path.

cd <project>
csc=/Applications/Unity/Hub/Editor/6000.5.4f1/Unity.app/Contents/Resources/Scripting/DotNetSdk/sdk/8.0.318/Roslyn/bincore/csc.dll
# ...EDbg.dag = editor defines (UNITY_EDITOR), ...P.dag = player defines. Check both.
sed 's#-out:.*#-out:"/tmp/csc/Promethist.LiquidGlass.dll"#; s#-refout:.*#-refout:"/tmp/csc/Promethist.LiquidGlass.ref.dll"#' \
    Library/Bee/artifacts/2000b0aEDbg.dag/Promethist.LiquidGlass.rsp > /tmp/lg.rsp
dotnet "$csc" @/tmp/lg.rsp        # silence means it compiled

Two ways this bites:

  • The source list is from the last build. A file added since will not be in it and has to be appended ("$PWD/<path>", one per line); a deleted file has to be grep -v’d out. Diff the `.rsp against find <dir> -name '*.cs' rather than assuming.

  • Give each define set its own output directory. The player build of an assembly has no UNITY_EDITOR, so it contains none of the assembly’s editor-only internal members. Building both configs to one path leaves the player output in place, and the matching *.Editor assembly then fails against it — with exactly the CS1061 wall described below, from a compile that was fine.

  • Keep the assembly’s own filename and redirect only the directory. Roslyn takes the assembly identity from the output filename, so -out:/tmp/out.dll builds an assembly called out, and [assembly: InternalsVisibleTo("Promethist.Something.Editor")] then does not match — every editor-only internal member reads as missing, surfacing as a wall of plausible-looking CS1061 errors that look exactly like a real break in the code just changed. For the same reason, point an editor assembly’s -r: at the runtime assembly just built rather than at the stale one in Library/Bee/artifacts.

This does not cover shaders, which only the editor compiles.

File encoding

.gitattributes forces LF and .editorconfig sets insert_final_newline = false, so source files have LF endings and no trailing newline. The BOM is per file, not per extension — check before writing one:

head -c 3 "$f" | od -An -tx1     # efbbbf => keep a BOM, anything else => none