feat: compile-check skips dependents when a rebuilt assembly keeps the same API in different bytes
Summary
- compile-check no longer recompiles the assemblies that depend on a rebuilt assembly when its reference assembly changed in bytes but shows them the same API. Before, only a byte-identical reference assembly let them be skipped.
User Impact
- In a nullable-enabled assembly, adding a private reference-type field, a private generic method with an
unmanagedconstraint, or a private member before a lambda, an async method or an iterator changes the reference assembly's bytes but not its API. Each such edit used to recompile every dependent (10 to 20 seconds for an assembly 67 others reference directly, in a project with 561 assemblies). Those dependents are now skipped and counted inSkippedAssemblies. - In a project where the changed-files check has never built its worker (every assembly under 300 sources, for example), the first comparison builds it once, which takes a few seconds; later runs reuse it until the worker's key changes.
- Comparing takes about 60 ms for a small assembly, up to about 0.5 s for a large one. The answer is recorded, so later runs on the same pair answer from the record in about 0 ms until Unity builds again.
- The CLI VibeLog's
cli_compile_check_completeentry lists every comparison undersurface_checks. - Not fixed yet: with Unity 2022.3, an assembly whose source file names differ only in letter case at the first differing character still compares as changed. compile-check orders its sources differently from Unity's response file there, so the types come out in another order. A follow-up pull request fixes the source order.
Changes
- When a rebuilt reference assembly differs from Unity's in bytes, the run asks the changed-files check worker's
compare-surfacecommand (added in the previous pull request).samekeeps the surface;different, a worker that cannot be built, and a failed or timed-out comparison count as changed. Identical bytes still settle it without the worker, and so does a run with no worker. The worker reads temporary copies of the exact bytes the run hashed, so a Unity build that rewrites its reference assembly mid-comparison cannot get an answer recorded under the wrong pair. Copies that cannot be written count as changed. - Each answer is written to
<Assembly>.surface.json, keyed by the sha256 of both reference assemblies and the worker key, through the result store. A record that does not match, cannot be read, or cannot be written is ignored and never fails the run. It is not removed with the assembly's outputs, because its key is the content itself. UnitResultcarries the verdict and its reason (same bytes,same API,changed,bytes differ,could not compare: ...,could not read: ...). The JSON response is unchanged.- Docs: step 3 of
docs/compile-check.mdexplains the comparison, what it leaves out, why that is safe, and the one difference a dependent can see: one that already fails because it names a private nested type may report the error under another ID. Step 6 and the--allnotes now talk about the same API instead of identical bytes, anddocs/vibe-logs.mdlistssurface_checks. - Two existing small-assembly tests now keep Alpha's reference assembly identical to Unity's. Their assertion that the worker is never built is unchanged; the worker would otherwise be built for the comparison.
Verification
-
New tests at the verdict level, one per row of the decision table:
- the worker is asked whatever the assemblies contain;
sameanddifferentanswers are recorded;- a matching record answers either way without the worker;
- a record is ignored when the format version, the produced sha256, the Unity sha256 or the worker key differs (one test each);
- an unavailable worker and a failed comparison count as changed, and a failed comparison records nothing;
- a record that cannot be written still keeps the surface;
- the worker gets the rebuilt bytes and Unity's in that order, and the copies are removed afterwards;
- when Unity rewrites its reference assembly mid-comparison, the answer and its record cover the pair the run read;
- copies that cannot be written count as changed.
-
Scheduler-level tests: a dependent is skipped and counted when the worker says
same, and a failed unit is never compared. Result store, client and VibeLog tests cover the record, the request and response, andsurface_checks. -
Mutations, each restored afterwards:
Mutation Fails Ask the worker only when either assembly contains EmbeddedAttribute13 tests, including the any-content case Ignore the record's format version / produced sha256 / Unity sha256 / worker key the matching one of the four record tests Never use the record both matching-record tests Record a failed comparison the failed-comparison test Compare units that nothing in the plan references the plan-reads test and the two small-assembly tests Hand the worker the rebuilt bytes as Unity's / swap the two the handed-bytes test (and the mid-comparison test) Hand the worker the original paths instead of the copies the mid-comparison test Leave the copies behind the handed-bytes test -
In
cli/dispatcher:golangci-lint fmt --diffis empty,go vet,golangci-lint run(0 issues) and the complexity config (0 issues) pass,go test ./...passes with coverage 94.27 (baseline 94.2), andgo test -racepasses oninternal/compilecheck/...andinternal/dispatcher.scripts/check-file-length.sh,scripts/sync-tool-docs.sh --checkandcheck-skill-sizepass. -
Checked in the Unity 2022.3 development project with a temporary pair of assemblies, one depending on the other, both since removed:
- Two private
stringfields in a nullable-enabled class:surface_checksshowssame APIin 59 ms, and the dependent is skipped (SkippedAssemblies77 to 76). - Running again unchanged: the verdict comes from the record in 0 ms.
- A public
string?field: the verdict ischangedwith the differing line, and the dependent is compiled.
- Two private
-
Repository CI does not run on pull requests into this branch; the checks above were run locally.