Skip to content

Build Pipeline

TL;DR: On every CCK avatar/spawnable/world build, WasmBuildProcessor gathers every WasmBehaviour + every script in CCKWasmProjectDescriptor.includedScripts, generates a WasmModule.csproj, and runs dotnet publish -c Release. Output is written to Assets/WasmModule.wasm and imported as a WasmModuleAsset reference on the root’s WasmVMAnchor. Toolchain components are downloaded on demand to %LOCALAPPDATA%/ChilloutVR/CVRBuildTools/.

Source: CVR.CCK.Wasm/CCK/Editor/WasmBuildUtility.cs

private const string DotnetVersion = "10.0.100-preview.5.25277.114";
private const string WasiVersion = "25.0";
private const string CCKWasmModuleVersion = "0.0.65";
  • .NET SDK 10.0.100-preview — NativeAOT compiler (ILC) targets WASI/WASM.
  • WASI SDK 25.0clang/wasm-ld toolchain.
  • CCKWasmModule 0.0.65 — private repo providing WasmModule.props and the Unity-shim assemblies the guest compiles against.

%LOCALAPPDATA%/ChilloutVR/CVRBuildTools/:

CVRBuildTools/
├── local-dotnet/ # dotnet.exe + SDK 10 preview
├── wasi-sdk/ # clang + sysroot, version pinned in .version
└── CCKWasmModule/ # WasmModule.props + shim assemblies

EnsureDotnetInstalled, EnsureWasiInstalled, EnsureCCKWasmModuleInstalled in WasmBuildUtility.cs each check a version file, download the expected version if missing, and extract via tar (Linux/Win 10+). The .NET installer uses the official dotnet-install.ps1/.sh; the WASI SDK is pulled from github.com/WebAssembly/wasi-sdk; the CCK WasmModule currently pulls from a private git.chilloutvr.dev repo.

For the standard flow (no externalProjectPath), WasmBuildUtility.GenerateCsproj writes:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<WasmOutputPath>{Unity Application.dataPath}/</WasmOutputPath>
<DefineConstants>CVR_SCRIPTING_CONTEXT_{AVATAR|PROP|WORLD};{custom defines}</DefineConstants>
<!-- Added only if enableReflection == false on the descriptor: -->
<IlcDisableReflection>true</IlcDisableReflection>
</PropertyGroup>
<ItemGroup>
<Compile Include="{absolute path 1}" />
<Compile Include="{absolute path 2}" />
<!-- ... one line per script path collected ... -->
</ItemGroup>
<Import Project="{CVRBuildTools/CCKWasmModule[-dev]}/WasmModule.props" />
</Project>

The WasmModule.props import is what injects:

  • the WASI/WASM target framework + RID
  • the shim assemblies (Unity types, CVR API surface)
  • ILC options
  • output path = {WasmOutputPath}WasmModule.wasm

WasmBuildProcessor.BuildScripts (runs on OnPreProcess{Avatar|Spawnable|World}):

  1. Iterate every WasmBehaviour component in the current hierarchy — add its source MonoScript path to the compile set; in-place replace the component with WasmRuntimeBehaviour via CCKCommonTools.ReplaceComponentInPlace.
  2. Iterate CCKWasmProjectDescriptor.includedScripts — add each MonoScript’s file path. Skip abstract types and non-WasmBehaviour descendants.
  3. Reflect over every resulting type:
    • ScanForUnityEvents — for each UnityEvents enum flag, look for a matching method name and set the bit.
    • ScanForGameEvents — same for GameEvents.
    • Store result in WasmBehaviourInfo { behaviourId, definedEvents, definedGameEvents }.
  4. Assign each behaviour a numeric behaviourId by hash-sort order (WasmBuildUtility.GetHash — FNV-1a of the class name).
  5. Generate UnityEventRegister.cs from the UnityEventSerializationContext and drop it alongside the csproj. This registers all persistent event listeners found in the scene.
  6. Run dotnet publish -c Release (or -c Debug when debugging/playmode). Output is read line-by-line and categorized into Debug.LogWarning, Debug.LogError, or plain Debug.Log based on error CS/warning CS/error NETSDK markers.
  7. Import the resulting Assets/WasmModule.wasm and assign it to WasmVMAnchor.moduleAsset + populate behaviourInfos and the union of all definedGameEvents.

If CCKWasmProjectDescriptor.externalProjectPath is set, the pipeline skips csproj generation entirely and runs dotnet publish on the user-managed project. Use this to:

  • consume NuGet packages (add <PackageReference> lines yourself),
  • organize code into your own folder structure,
  • share the WASM module project with non-Unity tooling.
  • Release (enableDebugging == false, not Play Mode): dotnet publish -c Release. Smaller, faster .wasm. No debug info.
  • Debug (enableDebugging == true or Play Mode): dotnet publish -c Debug. Larger .wasm with DWARF sections; Wasmtime’s WithDebugInfo(true) in WasmManager config makes stack traces usable.

CCKEditorPrefs.WasmVerboseBuild adds -bl to the publish command (binary log) and logs all output lines via Debug.Log.

By default the csproj sets <IlcDisableReflection>true</IlcDisableReflection>, which ILC uses to strip the reflection metadata tables. This keeps the module small but breaks any code path that reflects over types.

Toggle CCKWasmProjectDescriptor.enableReflection to remove that property and publish a full-reflection module. Expect a significant size increase.

During BuildScripts, the processor scans every SerializedProperty whose type is PersistentCall (Unity’s serialized event handler data) and rewrites any that target a WasmBehaviour instance to instead target the replacement WasmRuntimeBehaviour + a string method name. Detail: Unity Events Rewiring.

  • Assets/WasmModule.wasm — the module binary. Do not commit or hand-edit — it is regenerated every build.
  • WasmVMAnchor on the root GameObject — runtime pointer to the module + behaviour metadata.
  • {projectDirectory}/UnityEventRegister.cs — regenerated every build, used for event registration at runtime.
  • {projectDirectory}/WasmModule.csproj — regenerated every build.
  • Build hangs at 50 % — usually the first-time install of .NET SDK or WASI SDK. Check the progress bar label; if it says “Installing .NET SDK” a download is in progress.
  • error CS0103 inside WasmModule classes — the csproj uses Microsoft.NET.Sdk, not Unity’s built-in compiler. Types like UnityEngine.Object are supplied by the WasmModule.props shim assemblies. Namespace mismatches against what the shim exposes will fail to compile here even when Unity’s editor compile was happy.
  • NotImplementedException from bound function — the binding may be stubbed out. Check Not Exposed.
  • Authoring — the author-facing workflow.
  • Runtime Lifecycle — what the module does after it’s produced.
  • Limits — what happens if the resulting module takes too long per call.