feat: compile-check picks up new scripts outside every assembly definition

Merged
HA
4d ago
+528-21

Summary

  • uloop compile-check now compiles a new script created outside every assembly definition, in the predefined assembly Unity would put it in (Assembly-CSharp, Assembly-CSharp-Editor, Assembly-CSharp-firstpass or Assembly-CSharp-Editor-firstpass).

User Impact

  • Before: the predefined assemblies kept the source list of the last Unity build, so a brand-new .cs file outside every .asmdef was not compiled until Unity imported it, and its errors were missed.
  • After: the new script is compiled with the right assembly. If the last build did not produce that assembly - the first editor script of a project that had none - the run stops with COMPILE_CHECK_UNITY_BUILD_REQUIRED (<script> belongs to <assembly>, which the last Unity build did not produce) and uloop compile clears it.

Changes

  • New predefined_sources.go:
    • predefinedAssemblyFor follows Unity's EditorBuildRules.GetPredefinedTargetAssembly: a /editor/ folder anywhere in the path picks an editor assembly, the first-pass folders at the top of Assets pick a first-pass one, and an editor folder under a first-pass folder picks Assembly-CSharp-Editor-firstpass.
    • globPredefinedSources walks Assets once per run with the same glob the assembly definitions use, so it stops where an .asmdef or .asmref starts another assembly and skips names Unity ignores. An .asmdef in Assets itself yields nothing.
    • Scripts under Assets/StreamingAssets, Assets/WebPlayerTemplates, Assets/WebGLTemplates and Assets/Plugins/Android are left out. Unity imports them without compiling them; that list lives in its native script importer, not in the C# build rules.
    • A script whose assembly has no response file is refused.
  • The owner index carries the walk's result; a nil list (Assets never walked) makes RebuildSources refuse a predefined assembly instead of silently rebuilding it without its new scripts, while an empty map means the walk found none.
  • RebuildSources appends the new scripts of a predefined assembly after its recorded ones, in ordinal order, the same order as for assemblies with an .asmdef.
  • The owner index now keeps an .asmref naming an .asmdef the build left out. Unity attaches the folder to that assembly and then drops it, so the folder's scripts belong to no assembly; before, they were globbed into the assembly around it. An .asmref that resolves to no .asmdef still falls through to the enclosing assembly.
  • Packages/ is not walked: where a script in a package folder without an .asmdef goes differs between Unity versions. ADR 0018 already lists that limitation.
  • docs/compile-check.md (How it works, Limitations, a new Troubleshooting entry) and the skill are updated.

Verification

  • cli/dispatcher: gofmt -l . empty, go vet ./..., golangci-lint run ./... 0 issues, go test ./... -count=1 pass.
  • New tests cover the routing (editor segment, first-pass folders), the glob (owned scripts, hidden folders, Assets root .asmdef, package folders, .asmref to an unbuilt assembly / to no assembly / by an unknown name, and the .asmdef glob stopping at the same folder), the rebuild for each predefined assembly, a recorded script kept once, and the refusal through BuildCompilePlan.
  • Mutations (each reverted):
    • (a) predefinedAssemblyFor always returns Assembly-CSharp → the routing and per-assembly rebuild tests fail.
    • (b) the glob ignores assembly boundaries → TestGlobPredefinedSourcesLeavesOutAScriptAnAssemblyDefinitionOwns fails.
    • (c) no refusal for an unbuilt assembly → TestSelectChangedAssembliesRefusesAScriptForAPredefinedAssemblyTheLastBuildLacks fails.
    • (d) no early return for an .asmdef in Assets → TestGlobPredefinedSourcesReturnsNothingWhenAnAssemblyDefinitionSitsInAssets fails.
    • (e) the owner index skips .asmref files of unbuilt assemblies again → both "stops at a reference to an unbuilt assembly" tests fail.
    • (f) name references always resolve → TestGlobPredefinedSourcesDescendsIntoAReferenceByAnUnknownName fails.
    • (g) drop the uncompiled-folder filter → TestGlobPredefinedSourcesLeavesOutTheFoldersUnityNeverCompiles fails.
    • (h) drop the production assignment of the walk's result to the owner index → TestBuildCompilePlanPicksUpANewScriptOutsideEveryAssemblyDefinition fails.
    • (i) refuse a built predefined assembly too (built && false) → the same plan-level test fails.
    • (j) drop the nil check in RebuildSources → TestRebuildSourcesRefusesAPredefinedAssemblyWhenAssetsWasNeverWalked fails.
  • Live run on this repository's Unity project with the Editor closed, using a dist binary built from this branch:
    1. Baseline: exit 0, nothing compiled (no predefined assembly gained a script it should not).
    2. Assets/CompileCheckProbe.cs with int x = "s";: exit 1, CS0029 in that file, CompiledAssemblies = [Assembly-CSharp]. Removed → exit 0.
    3. The same in an Editor folder owned by no .asmdef: exit 1, CompiledAssemblies = [Assembly-CSharp-Editor]. Removed.
    4. git status --porcelain matched the state before the run.
    5. Assets/StreamingAssets/Probe.cs with the same error: Unity's own uloop compile exited 0 with no error and no response file listed it (the build-trace file moved, so Unity did build). Before the filter, compile-check put it into Assembly-CSharp and failed with CS0029. After it, compile-check exits 0. Removed.
    • The refusal for an unbuilt first-pass assembly is covered by unit tests only. The live check would have needed a new Assets/Plugins folder.