Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .agents/skills/uloop-compile-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
name: uloop-compile-check
description: "Compile C# with the Unity-bundled Roslyn compiler without launching or contacting the Unity Editor; reports errors and warnings only."
---

# uloop compile-check

Compile the project's C# offline, without starting Unity or sending anything to a running Editor.
It replays the response files Unity wrote during its last build with
the C# compiler bundled in the Editor install; the output lands in `Library/uloop/compile-check/`
and is never loaded by Unity.

## When to use

- You want to know whether the code compiles, without starting the Editor or waiting for an import
and a domain reload, and the result need not be live in the Editor yet.

## When not to use

- You want the change reflected in the Editor (play mode, tests, a tool call): run `uloop compile`.
- You just added or removed an `.asmdef`, added a reference to one, or changed scripting defines:
run `uloop compile` once first; the response files describe the previous build only, so the run
is refused with `COMPILE_CHECK_UNITY_BUILD_REQUIRED`.
- The project has no `Library` yet (a fresh clone, or an Editor that has never opened it): there is
no build to replay, so the run is refused with the same code. Let Unity build once, with
`uloop launch` or headless (`<Unity executable> -batchmode -nographics -quit -projectPath <dir>`).
After that one build, compile-check works without the Editor until an `.asmdef` or build-setting
change makes it ask for another (see Limitations).

## Usage

```bash
uloop compile-check
uloop compile-check --all --project-path /path/to/project
```

By default it compiles the assemblies whose sources changed since the last Unity build, plus every
assembly that references one of them. Independent assemblies compile at the same time (`--jobs`
caps how many; `--jobs 1` is sequential). An assembly whose inputs are exactly as the previous run
read them is not compiled again: its recorded diagnostics are reported, errors included. `--all`
compiles every assembly from scratch, the way out if a reused result ever looks wrong.

Once compile-check has compiled a large assembly in full without errors, an edit that keeps every
declaration is checked on its changed files only, in a few seconds, and the assembly is listed in
`FastCheckedAssemblies`. Compiled in full instead (`FastPathFallbacks` says why when the check was
tried): the first run after a Unity build; a run that adds, removes or renames a `.cs` file, or
changes more than 30 files; an edit that changes a declaration (a `using` directive counts); the
first run after a full compile that had errors; and an assembly compiled only because one it
references changed, which records no baseline. A change to the public surface also compiles the
dependents that can see it: those that write a changed name, or all of them when no name carries the
change; `SurfaceChecks` says which and why. Details and times: `references/full-compiles.md`.

Full compiles run the project's analyzers with the response file's flags, `-warnaserror` included.
The changed-files check skips the assembly-wide rules in `FastChecks[].UncheckedCodes`, so run once
with `--full` before finishing: it compiles in full what differs from its last full compile and
reuses the rest; right after a run that compiled in full it takes about a second. When warnings are
errors, such a rule (CS0414, for example) can fail that run only. A run that stops on declaration
errors (an unknown type, say) reports neither body errors nor analyzer diagnostics: fix them and run
again.

## Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `--all` | flag | Compile every assembly instead of only the changed ones and their dependents |
| `--full` | flag | Compile changed assemblies in full instead of checking only their changed files |
| `--editor-version <version>` | string | Use this Unity Editor version's compiler instead of ProjectVersion.txt |
| `--jobs <N>` | number | Compile up to N assemblies at once (default: half the CPUs, at least 1; `1` compiles sequentially) |
| `--max-depth <N>` | number | Search depth when locating the project (default: 3, -1 for unlimited) |
| `--project-path <path>` | string | Target another Unity project instead of the current directory |

## Output

A JSON payload; an empty list means the case did not occur in this run:

- `Success`: whether the compile produced no errors
- `ErrorCount`, `WarningCount`: totals across every compiled and reused assembly
- `Errors`, `Warnings`: each with `Message`, `Code`, `File`, `Line`, `Column`, `Assembly`
- `CompiledAssemblies`: compiled by this run, in dependency order
- `ReusedAssemblies`: reported from the previous run's result, without compiling
- `BlockedAssemblies`: not compiled because an assembly they reference has errors, as in Unity's
build; fix those and run again
- `FastCheckedAssemblies`: checked on their changed files only
- `FastChecks`: per fast-checked assembly, `ChangedFiles`, `UncheckedCodes` (rules only `--full` runs),
`GeneratorsNotRerun`, and `Reused` (the same edit's previous check, reported again)
- `FastPathFallbacks`: `Assembly` and `Reason` for each assembly compiled in full instead
- `FastPathBaselineNotes`: `Assembly` and `Reason` for each successful full compile that left no
baseline for the next check
- `SurfaceChecks`: per assembly others reference that this run compiled, replayed or fast-checked
without errors: `Kept`, or the `ChangedNames` a dependent must write to be affected (`Unnamed`
says why no name tells the change), and the compiled `Dependents` it reached, each with the
`Name` that selected it
- `SkippedAssemblies`: how many were left out because nothing they can see changed
- `ResponseFileSet`, `ProjectRoot`: the Bee build the run replayed and the resolved project root
- `Message`: one-line summary
- `NextActions`: the next step (fix and rerun, `--full` for the skipped rules, or `uloop compile` to
make the result live); empty when nothing changed

The process exits 1 when `ErrorCount` is greater than zero, and when the command itself fails, such
as with `COMPILE_CHECK_UNITY_BUILD_REQUIRED`; then no payload is printed and the error, with its
`ErrorCode`, goes to stderr. One compile-check runs per project at a time; a second waits up to
five minutes, then fails with `COMPILE_CHECK_PROJECT_BUSY`. Run it once more rather than retrying in
a loop.

## Limitations

- It replays the last Unity build's response files. A new or deleted `.asmdef`, a changed `.asmdef`
reference, an `.asmdef` that stopped building for the Editor, or a newly required precompiled
reference stops the run and asks for `uloop compile`.
- Build settings are compared against a record the first run after a Unity build writes: a
removed scripting define, a changed `csc.rsp`, `defineConstraints`, `versionDefines`,
`packages-lock.json` or `manifest.json` stops the run, and a scripting define the last build lacks
stops it when `ProjectSettings.asset` was saved after that build. A change made before that first
run (other than an added define) is not seen, so run `uloop compile` after one. Under a Unity 6
build profile that overrides Player Settings or carries its own defines, only the global
ProjectSettings defines are compared.
- A reference removed from an `.asmdef` stops the run too; the four references Unity adds on its own
are allowed, and a reference that matches no `.asmdef` in the project is not judged.
- A new `.cs` file outside every `.asmdef` goes to `Assembly-CSharp` or a sibling by Unity's path
rules (`Editor` folders, `Assets/Plugins`); if the last build did not produce that assembly, the run
stops and asks for `uloop compile`. A new script in a package folder without an `.asmdef` is not seen.
- A generator that reads a referenced assembly is not rerun for a change in that assembly; run
`uloop compile` after one.
85 changes: 85 additions & 0 deletions .agents/skills/uloop-compile-check/references/full-compiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# When compile-check compiles in full

Read this when a run took longer than you expected, when an assembly shows up in
`FastPathFallbacks`, or when you want to know what the next edit will cost.

The changed-files check applies to assemblies with at least 300 sources, once compile-check has
compiled that assembly in full without errors and kept a baseline, and at most 30 of its files
changed. Smaller assemblies are always compiled in full.

## What an edit costs

| Edit | What runs | Typical time |
|------|-----------|--------------|
| A body-only edit of up to 30 files in a large assembly with a baseline | Changed-files check | A few seconds |
| A body-only edit of more than 30 files | Full compile (`FastPathFallbacks`: too many changed files) | As for a full compile |
| A new, deleted or renamed `.cs` file | Full compile of the assembly | About Unity's compile of it, without the import and the domain reload |
| A declaration change (a member, a signature, a `using` directive) | Full compile, plus the dependents that can see the change | The assembly, then its dependents |
| The first run after a Unity build | Full compile of every changed assembly | As for a full compile |
| The first run after a full compile that had errors | Full compile (no baseline was recorded) | As for a full compile |
| An assembly compiled only because a reference changed | Full compile now, and again on its next body-only edit (it records no baseline) | As for a full compile |
| `--full` | In full: whatever differs from its last full compile - the assemblies fast-checked since, the dependents their rebuilt reference assemblies reach, and anything edited since the last run; the rest reused | As for a full compile of those; about a second right after a run that compiled in full |
| `--all` | Every assembly | The whole build |

## Dependents

When an assembly compiles in full, its public surface (its reference assembly) is compared with the
one from the last Unity build, not with the previous compile-check run; a surface that differs from
Unity's keeps compiling its dependents until Unity builds again. Only the dependents that write a
name the change touches are compiled; adding a member counts the type's name among the changed
names. A change a dependent can feel without writing any name is unnamed and compiles every
dependent: a new base type or interface, an operator, an added abstract member, a member C# binds by
pattern (`GetEnumerator`, `Deconstruct` and the like), among others. The output's `SurfaceChecks`
lists, per assembly that others reference, the verdict, the changed names or the unnamed reason, and
the compiled dependents the change reached, with the name that selected each.

An assembly starts once every assembly it references that this run also compiles has finished. An
assembly selected for its own sources, or a dependent already known to need compiling, starts
sooner: as soon as every assembly it references that was selected for its own sources has finished.
The dependents it references that are still compiling are assumed to keep Unity's public surface;
when one of them turns out to have changed it, the assembly is compiled again after they finish, at
most twice per run. A dependent whose recorded result may still apply once those finish waits for
them instead.

## Baselines and `no baseline` reasons

A full compile that succeeds records the baseline the next changed-files check is made against. When
there is none, `FastPathFallbacks[].Reason` says why:

- `no baseline: no full compile of this assembly is on record`: no full compile has run yet, or an
input changed while it ran (`FastPathBaselineNotes` of that run says which).
- `no baseline: the last full compile (at <time>) had errors; the first full compile without errors
records one`: a failed full compile drops the baseline an earlier one left.
- `no baseline: the last full compile (at <time>) did not record one`: `FastPathBaselineNotes` of
that run says why, or the assembly was compiled only because a reference changed.
- `no baseline: it was recorded by another version of compile-check`: the baseline was written by a
different compile-check build.

`<time>` is when that full compile finished, in UTC; it can be from an earlier session or from
before a Unity build in between.

In every case the next full compile that succeeds records one, and body-only edits after it are
fast-checked.

A run with `--all` records baselines too. Under `--all`, a reference whose rebuilt API differs from
Unity's build is read from this check's output; when a later run leaves that reference out and so
reads Unity's build again, the first body edit of an assembly referencing it is compiled in full,
with `inputs other than source contents changed: references` as the reason.

## Runs that stop on declaration errors

A run whose assembly has declaration errors, such as an unknown type or an ambiguous name, reports
those errors only: the errors inside method bodies, the warnings of that stage (which `-warnaserror`
would turn into errors) and the analyzer diagnostics are not reported until the declaration errors
are fixed. Such a run is also faster than a full compile. Fix the declaration errors and run again.

## Analyzers

A full compile uses the response file as Unity wrote it, its `-analyzer:` entries and flags such as
`-warnaserror` included; compile-check adds only the output folder for generated files. The
changed-files check skips the rules listed in `FastChecks[].UncheckedCodes`: those of CS0067,
CS0169, CS0414 and CS0649 that the project does not suppress, and the rules of analyzers that look
at the whole compilation. Run once with `--full` before finishing to cover them. That run compiles
in full whatever differs from its last full compile - the assemblies fast-checked since, the
dependents their rebuilt reference assemblies reach, and anything edited since the last run - and
reuses the rest; right after a run that compiled in full it takes about a second.
Loading
Loading