---
title: "Compilation & assembly membership"
slug: /workflows/compile-assemblies/
markdown: /workflows/compile-assemblies.md
section: verify-and-test
tier: 2
summary: "Why code will not compile, or was never compiled: asmdefs, defines, build profiles."
sources: ['unity/compile-assemblies.md']
canonical: https://unityworkflowlibrary.dev/workflows/compile-assemblies/
read_first: /read-first.md
---

> Read first, on every prompt: [non-destructive rules](/read-first.md).

# Compilation & assembly membership

> Why code will not compile, or was never compiled: asmdefs, defines, build profiles.

"Why won't my C# compile — or why was it never compiled?" Assembly membership, obsolete-as-error APIs, package shims, per-platform defines. A different question from "why won't my APK build" (the platform build page).

- **Unity 6000.5 + `com.unity.mathematics` CS0729:** builtin `mathematics@1.4` can ship an empty
  `Forwarders.cs` shim; TypeForwardedTo + module types in the same assembly → **CS0729** on `float3`,
  `math`, `float3x4`, … and ScriptAssemblies stall. Local recovery: clear the body of
  `Library/PackageCache/com.unity.mathematics@*/Unity.Mathematics/Forwarders.cs` (module still provides
  types). **Re-apply after Library wipe / package reimport.** Prefer not embedding a fake empty
  `Packages/com.unity.mathematics` (can hide module types entirely). (Seen in a mobile strategy game: on Entities 6.5
  / Unity 6000.5.0f1.)

- **PHANTOM-SCRIPT-NOT-IN-ASSEMBLY — a .cs file can sit on disk with a valid .meta and a
  resolvable GUID while Unity's compilation pipeline never adds it to ANY assembly, and the
  only symptom is CS0103 "the name X does not exist" reported from OTHER files.** The file
  itself produces no error, no warning, and no missing-script marker — it is simply absent from
  the compile, so every type it declares is unresolvable to its neighbours even though the
  Inspector shows a normal MonoScript and `AssetDatabase.LoadAssetAtPath<MonoScript>` returns
  non-null. Diagnosis is one call, and it is definitive:
  `CompilationPipeline.GetAssemblies(AssembliesType.Editor)` → find the owning assembly →
  check whether `sourceFiles` contains the path. If the file is missing there, stop debugging
  the code: the code is fine and was never compiled. **What does NOT fix it:**
  `AssetDatabase.Refresh` (even `ForceSynchronousImport | ForceUpdate`), `ImportAsset` on the
  file, `RequestScriptCompilation`, or even
  `RequestScriptCompilation(RequestScriptCompilationOptions.CleanBuildCache)` — all of these
  succeed and change nothing, which is what makes the trap eat time. **The fix is to give the
  file a new asset identity:** delete the `.cs` AND its `.meta`, `AssetDatabase.Refresh`, then
  recreate the file at a DIFFERENT path. It then imports normally and `sourceFiles` count goes
  up by one. Root cause is a corrupted per-path AssetDatabase entry, usually from writing the
  file externally while the editor was mid-import/mid-compile — which is exactly what an agent
  does when it writes a script and immediately requests a compile, and is far likelier when a
  second session is editing the same project concurrently. Prefer verifying `sourceFiles`
  membership (not just "the .meta exists") before concluding a new script's compile error is
  about its contents. (Seen in a mobile strategy game: adding a fog-of-war visibility system: four
  compile cycles and a full clean build blamed on namespaces and asmdef refs; the file had
  simply never entered Game.DOTS's 169-file source list.)
  **Amended v246 (a tower-defense game, second occurrence).** Four corrections:
  (a) **The orphan's OWN assembly stays GREEN.** The error surfaces only where a type is named in
  *code*; a `<see cref="..."/>` doc comment is not a reference. a tower-defense game.Sim compiled clean at
  122 of its 123 files because the orphan's only same-assembly mention was in a doc comment — the
  break appeared solely in the consuming Assembly-CSharp-Editor. So "assembly X reported 0 errors"
  never proves X's file list is complete, and the error's assembly is not the broken file's assembly.
  (b) **A cross-assembly reference DETECTS the orphan; it does not enroll it.** Adding a consumer
  cannot "keep a file compiled" — it only converts silence into CS0246/CS0103. Never write that
  claim into a comment as a safeguard; it is not one.
  (c) **Sweep for orphans project-wide in one call** instead of testing the file you suspect: build
  a `HashSet` of the union of every assembly's `sourceFiles`, then set-subtract it from
  `AssetDatabase.FindAssets("t:MonoScript", <script root>)`. The remainder is every file compiled by
  no assembly. It is one line of output and it is exhaustive.
  (d) **An editor restart also clears it, and preserves the GUID.** Choose by whether the GUID is
  already serialized: grep it in `*.unity`/`*.prefab`/`*.asset` first. Never serialized (always true
  of a `MonoBehaviour` that has never compiled — no instance of it can exist) → the path change is
  free. Already serialized → restart instead, or the move silently detaches every component. When
  moving, the class name must stay the file name, so change the FOLDER, not the filename.
  `ImportAsset(ForceUpdate)` + `RequestScriptCompilation` were re-measured here and again changed
  nothing (10238 → 10238 total `sourceFiles`); the "does NOT fix it" list above is confirmed, not
  folklore. Grep this glossary BEFORE diagnosing a missing type — the second occurrence cost a
  domain reload that the rule already predicted would be wasted.

- **FIRST-PARTY-CODE-LIVES-UNDER-PACKAGES-TOO — a search scoped to `Assets/` returns false negatives in any
  project with `file:` / junctioned packages.** a tower-defense game: a lane grepped `Assets/` for the
  interfaces its new Control Panel tab implements, got zero hits, and was one step from reporting "the tab cannot
  compile" — the types live in `Packages/com.game.foundation/` (junctioned, `manifest.json` `file:`). Before
  reporting a negative about a symbol: search `Assets/` AND `Packages/` (and `Library/PackageCache/` for
  registry packages), or ask the editor (`CompilationPipeline.GetAssemblies`, a type load). Cross-check a
  negative against a file that already uses the symbol and compiles. A zero-hit grep is a scoping claim, not a
  finding. (Lane B worker, a tower-defense game.)
- **ORPHAN-SWEEP-MUST-FILTER-DEFINE-CONSTRAINED-ASSEMBLIES**: a project-wide "which .cs is in no assembly"
  sweep (`CompilationPipeline.GetAssemblies` sourceFiles vs every `.cs`) always lists the files of an asmdef whose
  `defineConstraints` are unsatisfied — those assemblies are switched off by design and look identical to real
  phantoms by count. one shipped project: 20 "orphans" were `Latios.FlowFieldNavigation` behind
  `LATIOS_ADDON_FLOWFIELD`, and the lane then dismissed its own true phantom as the same noise. Filter out
  asmdefs with unsatisfied constraints (or scope to your script root) so a non-zero result always means
  something. A sweep that cries wolf every run trains you to discard the true result. (Lane B.)
- **SCRIPTING-DEFINES-ARE-PER-PLATFORM — a define set on Standalone/Android is NOT set on iOS,
  and the failure is silent by construction.** `PlayerSettings.SetScriptingDefineSymbols` is
  per `NamedBuildTarget`, so a define added during desktop development simply does not exist on
  a mobile target unless someone added it there too. Nothing warns: the code inside `#if MY_DEFINE`
  is preprocessed away, the build compiles clean, and the feature is absent at runtime. Framework
  mode-switches are the dangerous case — a define that selects a transform/backend mode (Latios
  `LATIOS_TRANSFORMS_UNITY`, gating 89 framework files) makes the missing platform build a
  *different framework configuration*, not merely a disabled feature. Audit by printing the define
  string for every `NamedBuildTarget` side by side and diffing them; treat any platform-unique
  define as a defect until justified. Cheap guard: an `IPreprocessBuildWithReport` that compares
  the active target's defines against a required set and throws. (Seen in a mobile strategy game: iOS carried only `dUI_MANAGER` while every other platform carried four defines including the
  one gating the whole animation bootstrap — an iOS build would have shipped every unit in bind
  pose with zero errors.)

## Unity 6.5 turned the InstanceID family into compile ERRORS (not warnings)

`6000.5` promoted the `InstanceID` -> `EntityId` migration from deprecation warning to
`[Obsolete(..., error: true)]`, so these are **`CS0619` hard errors**, not `CS0618` warnings:

| Erroring API | Replacement |
| --- | --- |
| `Object.GetInstanceID` | `Object.GetEntityId` |
| `EditorUtility.InstanceIDToObject(int)` | `EditorUtility.EntityIdToObject(EntityId)` |
| `AssetDatabase.GetAssetPath(int)` | `AssetDatabase.GetAssetPath(Object)` *(or the `EntityId` overload)* |
| `AssetPreview.IsLoadingAssetPreview(int)` | `...IsLoadingAssetPreview(EntityId)` |
| `SerializedProperty.objectReferenceInstanceIDValue` | `...objectReferenceEntityIdValue` |

This breaks **third-party Asset Store code you did not write** (and editor tooling — see
the [Live play testing via Unity MCP](/workflows/live-play-testing-unity-mcp/) page). Upgrading an existing project to 6.5 will surface errors in plugins that
have shipped unchanged for years.

**Prefer the overload that was never deprecated over the new one.** For the AssetDatabase case,
`GetAssetPath(Object)` has always existed and is not obsolete — it fixes the error without pinning
the file to 6.5+, which matters in vendor code you may re-import later. Reach for `GetEntityId`
only where no such overload exists.

**A failed assembly hides every error downstream of it.** Assemblies that reference a broken one
are never compiled, so their errors are simply absent from the console — a two-error console can
be concealing a hundred. After fixing the first wave, always recompile and re-read before claiming
the project is clean, and sweep the whole tree for the offending API rather than fixing only what
the console listed.

## DEDICATED-SERVER-IS-A-DEFINE-NOT-A-PLATFORM - you cannot excludePlatforms your way off the server

There is no `"Server"` entry in an asmdef's `includePlatforms` / `excludePlatforms`. Dedicated Server is
a build SUBTARGET of Standalone, not a platform, so the only platform token that would match it is
`WindowsStandalone64` / `LinuxStandalone64` - excluding that takes the normal client build with it.
Measured vocabulary actually in use across ~170 package asmdef platform entries in a Unity 6 project:
Editor (164), iOS (3), macOSStandalone (2), WindowsStandalone64 (2), WebGL (2), LinuxStandalone64 (2),
WindowsStandalone32 (1). No Server, anywhere. Verify with
`grep -rl '"Server"' Library/PackageCache --include=*.asmdef` before believing any plan built on one.

The code lever is `"defineConstraints": ["!UNITY_SERVER"]` on the asmdef: UNITY_SERVER is defined for the
Server subtarget, so the assembly is simply not compiled into that build.

Two things that look like they solve this and do not:
- **Multiplayer Roles content selection** (`com.unity.dedicated-server`) strips render / UI / audio
  COMPONENTS out of scenes and prefabs. It never removes code. It is complementary to the asmdef fence,
  never a substitute, and `ContentSelectionSettings` is `internal` to the package - script it through the
  public `EditorMultiplayerRolesManager.SetMultiplayerRoleForBuildProfile`, and read the strip flags off
  `ProjectSettings/Packages/com.unity.dedicated-server/ContentSelectionSettings.asset`.
  **`[RequireComponent]` makes a component permanently unstrippable** (a space sandbox game). Unity refuses
  to remove a component another component declares a dependency on, so `m_StripUIComponents: 1` silently
  fails on every object whose behaviour declares `[RequireComponent(typeof(UIDocument))]` - measured, 8
  presenters declaring it, 4 "Can't remove UIDocument (Script) because X (Script) depends on it" errors,
  and 2 UIDocuments left alive and ENABLED in a server-role play session while cameras stripped to 0.
  Strip the whole GameObject by role instead of pulling a component out from under a dependent. And note
  what the strip FLAG is worth: reading `m_StripUIComponents: 1` back out of the settings asset tells you
  the setting is on, never that anything was stripped - only counting live objects in a running server
  distinguishes "render=True ui=True" from "cameras 0, UIDocuments 2". A validator that reads its own
  configuration is not a measurement.
- **Managed stripping** removes unreferenced IL, which is not the same as a guaranteed exclusion and
  cannot be asserted in a validator.

The real precondition nobody costs in: a folder with NO asmdef compiles into Assembly-CSharp, which
takes no define constraint and is built for every platform and subtarget. So any folder still in
Assembly-CSharp that touches a vendor or render-pipeline type pins that dependency onto the server no
matter what you do to the vendor asmdefs - and worse, the moment you DO constrain the vendor assembly,
Assembly-CSharp stops compiling on the server. A server-clean build therefore starts by giving every
runtime folder an asmdef, and that ordering is the whole schedule: fence the leaves last, not first.
Find the real reference surface with BOTH `grep -rln "using <Vendor>"` AND `grep -rn "<Root>\.<Namespace>\."` -
a fully-qualified reference has no using line and is invisible to the first grep alone.
(a space sandbox game, developer decision D-026 "headless server runs only its own code": the plan was written
as an excludePlatforms change; no such token exists. Three of 35 Flow files carried the whole HDRP/vendor
dependency, and a fully-qualified `MyGame.Presentation.NavigationService` call in a fourth
was missed by a using-only grep and would have failed the dedicated build with an error pointing nowhere
near its cause.)

**Amendment (a space sandbox game, v243).** A PREDEFINED assembly cannot reference a define-constrained
one. The moment two runtime assemblies took `!UNITY_SERVER`, `Assembly-CSharp-Editor` could no longer see
them and the whole editor tree collapsed into 30 errors, every one cascading from a single
`CS0234: 'Presentation' does not exist in the namespace 'a space sandbox game'`. Giving the editor folder its own
asmdef with explicit references fixed all 30 in one pass. So "give every folder an asmdef" includes the
Editor folder, and it is a PREREQUISITE of the fence, not tidy-up afterwards.
Do not reflexively stamp the same constraint on the editor assembly to make the error go away: that makes
the tooling VANISH under a server profile instead of failing loudly, and it takes your measurement tools
down at exactly the moment you want to measure a server. Unconstrained-and-loud beats constrained-and-silent.

## ACTIVE-BUILD-PROFILE-REDEFINES-EDITOR-COMPILE - a server profile silently deletes your editor tooling

An ACTIVE Build Profile re-applies its subtarget on every domain reload. Select a Dedicated Server
profile once and `UNITY_SERVER` is defined for editor compilation from then on, so every assembly
carrying `defineConstraints: ["!UNITY_SERVER"]` is dropped from the compile - and setting
`EditorUserBuildSettings.standaloneBuildSubtarget` back does NOT hold, because the active profile
re-imposes it on the next reload. Clear or switch the PROFILE, not the subtarget.

What makes this dangerous is the shape of the failure: **the compile reports ZERO ERRORS**, because the
dropped code is not compiled rather than compiled wrong. Measured in one real occurrence: 41 editor source
files silently not built, the whole Control Panel down from 21 tabs to 2, and `unity_get_compilation_errors`
returning `count: 0` throughout. An agent polling the error count reports green and moves on.

- **Verify by `CompilationPipeline.GetAssemblies` or a type/tab count, never by the error count.** An
  assembly that is absent produces no errors. This is the same family as MUTEX-DIR-NEEDS-OWNER-AND-EXIT-TRAP
 : a failure that presents as its own success.
- **BUILD-UNDER-ITS-OWN-ACTIVE-PROFILE : never switch profile inside the build call.**
  `SetActiveBuildProfile` changes the defines, but the domain reloads only after the call returns. The
  build then writes the player's assets with Editor types compiled under the OLD profile's defines. It
  reports Succeeded, and the player dies at launch: "sharedassets1.assets is corrupted! [Position out of
  bounds!]". Measured: a client built while the server profile was active (its define list lacked two
  vendor defines the client profile carries) crashed on 2/2 launches. The rebuild, with the client
  profile activated and reloaded first, launched clean. So a build REFUSES unless its profile is
  already active. The developer's button switches the profile and asks for a second press after the reload.
- The loaded domain also lies for a while after an asmdef change: `TypeCache` and
  `AppDomain.GetAssemblies` can still report the OLD layout while the pipeline already has the new one.
  Trust the pipeline, and re-check after a real reload.
(a space sandbox game: a cancelled dedicated-server build left `Windows Server` as the active profile; the
next reload deleted Presentation, UI and the editor assembly from the compile with 0 errors reported.)


## EDITOR-PLATFORM-ASSEMBLY-HAS-NO-PLAYMODE-TESTS

- **A test `.asmdef` with `"includePlatforms": ["Editor"]` can only host EditMode tests.** The Test
  Runner will not load it for a PlayMode run, so `-runTests -testPlatform PlayMode` reports
  `total="0"` and exits 0. A green pipeline and a folder named `Tests/PlayMode` then both assert
  play-mode coverage that does not exist.
- Measured: 62 green "UI" tests, every one of them EditMode, none of which had ever run the scene.
  The first four real play-mode tests (load the shipping scene, let frames run, assert) immediately
  found a shipping defect the whole EditMode suite could not see.
- **`total="0"` is a failure, not a pass.** Assert a minimum test count per platform in the runner,
  or the suite silently evaporates when an assembly's platform list changes.
- A real play-mode assembly (`"includePlatforms": []`) cannot reference `UnityEditor`, so it cannot
  use `AssetDatabase`. That constraint is the point: it forces the test to load the shipping scene
  and inspect what actually booted, instead of building its own subject out of editor-only handles.

## UNUSED-ASMDEF-REFERENCE-SHIPS-THE-LIBRARY (HARD)

- **An unused `using` line does not pull a library into the player.** The C# compiler emits no
  assembly reference for it. What ships is: (1) every `references` entry on a runtime `.asmdef`,
  (2) every `autoReferenced` assembly, (3) every package in `Packages/manifest.json`.
- **An asmdef `references` row is a hard dependency.** Unity includes that assembly in the player
  even when no type from it is named. A leftover `Unity.Entities.Hybrid` or a Foundation assembly
  on a world-gen asmdef that never mentions those types still cooks into the client.
- **A leftover render-pipeline package is the same defect at the next layer up.** It is not inert:
  vendor megashaders keep the foreign SubShader alive and `PrepareStageVariantsForSinglePlatform`
  enumerates it (SHADER-VARIANT-PREP-RUNS-BEFORE-STRIPPING).
- **Sweep before every player cook.** For each runtime asmdef (`includePlatforms` empty or
  Standalone), grep that assembly's scripts for the reference name and its namespace. Zero hits →
  drop the reference in the same slice, then cook. `tools/asmdef-unused-refs.sh` reports
  candidates; confirm with a type grep before deleting (Burst can appear as `[BurstCompile]`
  without `using Unity.Burst`).
- Do not "keep it for later". A sentinel-only runtime assembly belongs on `"includePlatforms":
  ["Editor"]`, not in the player.

## OFFLINE-COMPILE-ONLY-COVERS-LISTED-SOURCES (HARD)

An out-of-editor compile that reuses the build system-s PINNED response file compiles exactly the
sources that response lists, and nothing else. A file you just created is not in it. So a NEW file
- a new type, and most dangerously a new TEST - passes the compile by never being compiled at all,
and the run reports clean.

- **It fails in both directions.** A new file that the response omits but existing code REFERENCES
  produces phantom -type or namespace not found- errors on a tree that is actually healthy; the
  same omission on a file nothing references yet reports a false PASS. Measured: a guard test
  compiled clean on its first run without existing, and separately, two missing-type errors were
  briefly read as a broken integration branch when the type was present and merely unlisted.
- **Grep the GENERATED response file for each changed file-s FULL PATH** before quoting a pass.
  It beats watching the source count, which only says *a* file moved, and it names the assembly
  that picked the file up. The full path, never the basename: the pinned response is generated
  against the shared checkout, so a branch file overrides it only if the tool-s resolver found it,
  and a basename match is satisfied by the WRONG TREE-s copy - you then compile the integration
  branch-s version of a file you are certain you just edited. A count moving does not catch that
  either.
- **Have the response include a whole folder or glob for the kinds of file that get added often**
  (tests especially), rather than naming files or matching one lane-s pattern.
- **A lane-s glob list silently defines what -pass- means, and nothing signals the boundary.**
  When lanes each compile their own folder set, one lane adding a file outside a peer-s globs gives
  that peer phantom errors with no hint of the cause. The per-file path check is what closes it;
  neither lane is told.
- **Never report an integration branch broken on a pinned-response compile alone.** Check whether
  the missing symbol exists in the tree and is simply absent from the response, before telling
  anyone the branch is red.
- **An assembly the tool does not rebuild is linked from the Editor's last build, and that is stale too** (amended). A lane compiling only its own assemblies linked a prebuilt dll older than a peer's merged members, and the
  peer's source failed against it; it was reported as the peer's defect on a healthy tree. Rebuild every assembly
  that the compiled sources reference, and keep ONE list of files merged into the integration branch that the pinned
  response lacks, compiled by every lane and pruned once the Editor recompiles, instead of per-lane copies.

## OFFLINE-PLAYER-PASS-OVERWRITES-THE-EDITOR-OUTPUT

An offline compile with a player (or server) define set wrote to the same output folder as the Editor-define pass. A
recipe compiled against that folder next reported editor-only members (`#if UNITY_EDITOR` statics) as missing, on a
tree that was healthy. Run the Editor pass LAST, or give each define set its own output folder, before compiling or
running anything that uses editor-only members.

## PURE-LOGIC-RUNS-WITHOUT-THE-EDITOR (0.3.99)

An EditMode test needs the Editor, and the Editor is a single seat: when another
lane holds it, "compiles clean" is the only evidence left and the assertions in a
new test file have never once been executed. Anything that does not touch engine
objects can be run anyway, in about five minutes, against the assembly the offline
compile already produced.

Point `csc` at the built DLL plus the packages it needs and the Editor's own
.NET runtime, and run the result as a console program. The pieces:

- reference `<Editor>/Data/NetCoreRuntime/shared/Microsoft.NETCore.App/<ver>/` for
  `System.Private.CoreLib`, `System.Runtime`, `System.Console` **and `netstandard`** -
  without the last one, a struct from a package fails with "the type 'ValueType' is
  defined in an assembly that is not referenced";
- a package type may be **type-forwarded**: `Unity.Mathematics.float3` resolves through
  `UnityEngine.MathematicsModule.dll`, so reference both it and the package assembly;
- the compiled package DLL lives under `Library/ScriptAssemblies/`, never the
  `.ref.dll` the response file cites - a reference assembly has no method bodies;
- ship a `<name>.runtimeconfig.json` naming the framework, or the runtime refuses to
  start the program.

Worth the five minutes for more than a green tick: printing the function across its
real input range catches a modelling error that passing tests cannot. A gather-aim
cone measured a target by the **corner** of its mesh bounds, which passed every
assertion while handing a flat, wide object half again the slack its silhouette
deserved; the table of resulting angles at realistic distances is what exposed it.
Do not claim the engine-side behaviour from this - it is the arithmetic, not the game.

**Getting it to LOAD is the whole cost, and the failures arrive one at a time.** The compile reference
set is not the run set: a type that merely IMPLEMENTS an ECS interface drags the whole entities
assembly and the engine core module in at runtime, even when the code under test touches neither and
only does arithmetic. Each missing assembly surfaces only when the line that needs it executes, so a
probe that checks numbers first and a string last prints eleven passes and then dies on the twelfth.
Copy assemblies next to the program and re-run until it completes - the passes printed before a load
failure are real results, not a broken run.

- **One dependency is not where the others are.** Package assemblies come from
  `Library/ScriptAssemblies/`, but a package may ship a PREBUILT support assembly inside its own
  package folder that never appears there. Fixed-string EQUALITY routes through one such assembly, so
  a probe that only compares numbers never needs it and a probe that compares a name fails at exactly
  that line. When a load failure names an assembly missing from `ScriptAssemblies`, search the package
  cache for the DLL before concluding the technique does not apply.
- Add the engine core module as soon as anything package-typed is involved; it is the first failure in
  practice, not an edge case.
