Files
Radicade/Tools/repair-unity-script-compiler.ps1
T
2026-10-16 23:33:13 -07:00

239 lines
9.8 KiB
PowerShell

<#
.SYNOPSIS
Puts back the Roslyn script compiler that Windows Defender quarantines out of the Unity
package cache, so the editor can compile the project at all.
.DESCRIPTION
Unity 2020.3 compiles C# by launching an *external* compiler, and because this project
installs `com.unity.roslyn` (a transitive dependency of `com.unity.entities`) that compiler
is the one shipped inside the package:
<ProjectRoot>\Library\PackageCache\com.unity.roslyn@<hash>\Compiler~\win64\csc.exe
Defender quarantines unsigned executables out of Unity package folders on this machine - the
recorded casualties are `com.unity.burst\.Runtime\hostwin\llvm-lipo.exe` and `dsymutil.exe`
(heuristic PUA signatures, ThreatID 2147972435) and, for this project, that `csc.exe`. It has
to be deleted rather than absent from the package: the registry tarball does ship it
(`package/Compiler~/win64/csc.exe`).
With it gone, Unity logs the same failure for every assembly it was about to build:
Starting external compile of Unity.Jobs.CodeGen.dll: ...\Compiler~\win64\csc.exe ...
Exception when running external compiler: System.ComponentModel.Win32Exception
(0x80004005): Native error= The system cannot find the file specified.
and then, because nothing compiled at all, reports the misleading
Unable to resolve reference 'Unity.Mathematics' / 'Unity.Collections' / ...
for every `com.recroom.*` assembly, while `Library\ScriptAssemblies` stays empty apart from
`BuiltinAssemblies.stamp`. So: an empty ScriptAssemblies folder plus a wall of unresolvable
references means "the compiler never ran", not "the packages are broken".
This script restores the file from `-FromPath`, or from the Unity editor install's own
`Editor\Data\Tools\Roslyn\csc.exe` (a different build of the same 3.8 apphost, verified to
drive the package's newer `csc.dll`), and then proves it runs by asking it for its version.
The Unity-install copy is 3.8.0-dev.20527.1 as well, so the compiler behaviour is unchanged;
only the ~170 KB launcher differs. Prefer `-FromPath` with the authentic file if you have the
package tarball to hand:
https://cdn.packages.unity.com/tarballs/com.unity.roslyn/com.unity.roslyn-0.2.2-preview/8b5b337db92cbdec37b54235ec8b826c92c6ddef.tgz
(`tar -xzf <tgz> package/Compiler~/win64/csc.exe`, md5 8c4361cc37113f8467feb1d22b4e83da)
This is a repair, not a cure: Defender will do it again on the next full scan, and a Unity
package re-resolve copies the damaged file back out of the shared cache. Add the exclusions
this script prints at the end, and re-run with -IncludeGlobalCache, to close both loops.
.PARAMETER ProjectRoot
Candidate project roots, tried in order; the first one that exists and holds the roslyn
package cache is used.
.PARAMETER FromPath
Use this csc.exe instead of the Unity editor install's copy.
.PARAMETER IncludeGlobalCache
Also repair the shared package cache under %LOCALAPPDATA%\Unity\cache\packages, which is
where a re-resolve would otherwise copy the missing file back from.
.PARAMETER Force
Overwrite csc.exe even when one is already present and working.
.EXAMPLE
pwsh -File tools/repair-unity-script-compiler.ps1
pwsh -File tools/repair-unity-script-compiler.ps1 -FromPath C:\tmp\csc.exe -IncludeGlobalCache -Force
#>
[CmdletBinding()]
param(
[string[]] $ProjectRoot = @(
'C:\RecRoom\Documents\EggFiles\ProjectTemplate',
'C:\RecRoom\OneDrive\Documents\EggFiles\ProjectTemplate'
),
[string] $FromPath,
[switch] $IncludeGlobalCache,
[switch] $Force
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$expectedMd5 = '8c4361cc37113f8467feb1d22b4e83da'
function Write-Status {
param([string] $Level, [string] $Message)
Write-Host ("[{0}] {1}" -f $Level, $Message)
}
function Find-RoslynCompilerDir {
<# The roslyn package folder inside a project, or $null. #>
param([string] $Root)
$pkgRoot = Join-Path $Root 'Library\PackageCache'
if (-not (Test-Path -LiteralPath $pkgRoot)) { return $null }
$dirs = Get-ChildItem -LiteralPath $pkgRoot -Directory -Filter 'com.unity.roslyn@*' -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName 'Compiler~\win64' } |
Where-Object { Test-Path -LiteralPath $_ }
if (-not $dirs) { return $null }
return @($dirs)[0]
}
function Find-UnityInstallCompiler {
<# Tools\Roslyn\csc.exe from the editor version this project is pinned to. #>
param([string] $Root)
$versionFile = Join-Path $Root 'ProjectSettings\ProjectVersion.txt'
$version = $null
if (Test-Path -LiteralPath $versionFile) {
$line = Get-Content -LiteralPath $versionFile | Where-Object { $_ -match '^m_EditorVersion:' } | Select-Object -First 1
if ($line) { $version = ($line -split ':', 2)[1].Trim() }
}
$roots = @()
if ($version) {
$roots += "C:\Program Files\Unity $version"
$roots += "C:\Program Files\Unity\Hub\Editor\$version"
}
$roots += @(Get-ChildItem 'C:\Program Files\Unity*' -Directory -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })
foreach ($r in $roots) {
$candidate = Join-Path $r 'Editor\Data\Tools\Roslyn\csc.exe'
if (Test-Path -LiteralPath $candidate) { return $candidate }
}
return $null
}
function Test-Compiler {
<# Runs the compiler's own version probe: the launcher only works if it can reach its csc.dll. #>
param([string] $Path)
try {
$output = & $Path -version 2>&1
} catch {
return [pscustomobject]@{ Ok = $false; Detail = $_.Exception.Message }
}
$text = ($output | Out-String).Trim()
if ($LASTEXITCODE -ne 0 -or $text -notmatch 'Visual C# Compiler version|^\d+\.\d+') {
$detail = if ($text) { $text } else { "exited with code $LASTEXITCODE and no output" }
return [pscustomobject]@{ Ok = $false; Detail = $detail }
}
return [pscustomobject]@{ Ok = $true; Detail = $text }
}
function Repair-CompilerDir {
param([string] $Dir, [string] $Source)
$target = Join-Path $Dir 'csc.exe'
if ((Test-Path -LiteralPath $target) -and -not $Force) {
$probe = Test-Compiler -Path $target
if ($probe.Ok) {
Write-Status 'PASS' "already present and working: $target ($($probe.Detail))"
return $false
}
Write-Status 'WARN' "present but not usable ($($probe.Detail)) - replacing it"
}
if (-not (Test-Path -LiteralPath $Dir)) {
New-Item -ItemType Directory -Path $Dir -Force | Out-Null
}
Copy-Item -LiteralPath $Source -Destination $target -Force
$probe = Test-Compiler -Path $target
if (-not $probe.Ok) {
throw "Restored $target but it still does not run: $($probe.Detail)"
}
$md5 = (Get-FileHash -LiteralPath $target -Algorithm MD5).Hash.ToLowerInvariant()
if ($md5 -eq $expectedMd5) {
Write-Status 'PASS' "restored the authentic $target ($md5)"
} else {
Write-Status 'PASS' "restored $target (md5 $md5; the Unity install build, not the package one)"
}
return $true
}
# ---- main -------------------------------------------------------------------------
Write-Host ''
$project = $null
foreach ($candidate in $ProjectRoot) {
if (-not (Test-Path -LiteralPath $candidate)) { continue }
if (Find-RoslynCompilerDir -Root $candidate) { $project = $candidate; break }
}
if (-not $project) {
throw "No project with an extracted com.unity.roslyn package found. Tried:`n $($ProjectRoot -join "`n ")"
}
$compilerDir = Find-RoslynCompilerDir -Root $project
Write-Status 'INFO' "project : $project"
Write-Status 'INFO' "compiler : $compilerDir"
$source = $FromPath
if ($source) {
if (-not (Test-Path -LiteralPath $source)) { throw "-FromPath '$source' does not exist." }
Write-Status 'INFO' "source : $source (-FromPath)"
} else {
$source = Find-UnityInstallCompiler -Root $project
if (-not $source) {
throw @"
No csc.exe available to restore from. Either install the editor version named in
ProjectSettings\ProjectVersion.txt, or pass -FromPath pointing at a copy of the package's
win64\csc.exe taken from the registry tarball.
"@
}
Write-Status 'INFO' "source : $source (Unity editor install)"
}
$repaired = Repair-CompilerDir -Dir $compilerDir -Source $source
if (-not $repaired) {
Write-Status 'INFO' 'nothing to do for this project'
}
if ($IncludeGlobalCache) {
$globalRoot = Join-Path $env:LOCALAPPDATA 'Unity\cache\packages\packages.unity.com'
$globalDirs = Get-ChildItem -LiteralPath $globalRoot -Directory -Filter 'com.unity.roslyn@*' -ErrorAction SilentlyContinue |
ForEach-Object { Join-Path $_.FullName 'Compiler~\win64' }
if (-not $globalDirs) {
Write-Status 'WARN' "no com.unity.roslyn under $globalRoot"
}
foreach ($d in $globalDirs) { Repair-CompilerDir -Dir $d -Source $source | Out-Null }
}
# Unity will not pick the compiler up until it recompiles; ScriptAssemblies should then fill in.
$scriptAssemblies = Join-Path $project 'Library\ScriptAssemblies'
if (Test-Path -LiteralPath $scriptAssemblies) {
$built = @(Get-ChildItem -LiteralPath $scriptAssemblies -Filter '*.dll' -ErrorAction SilentlyContinue).Count
if ($built -eq 0) {
Write-Status 'INFO' 'Library\ScriptAssemblies is still empty - reopen the project (Unity recompiles on focus)'
} else {
Write-Status 'INFO' "Library\ScriptAssemblies holds $built assemblies"
}
}
Write-Host ''
Write-Status 'INFO' 'Windows Defender is what removes this file. In an ELEVATED PowerShell:'
Write-Host " Add-MpPreference -ExclusionPath '$project'"
Write-Host " Add-MpPreference -ExclusionPath `"$env:LOCALAPPDATA\Unity\cache`""
Write-Status 'INFO' 'the same rule also emptied com.unity.burst\.Runtime\hostwin (llvm-lipo/dsymutil, needed for Apple targets only)'