<# .SYNOPSIS Unity CLI Installer for Windows. .DESCRIPTION Downloads and installs the Unity CLI binary for Windows. Verifies the download with SHA-256 before installing. .EXAMPLE irm https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.ps1 | iex .PARAMETER Target Version to install. Defaults to "latest" (recommended). .LINK https://unity.com #> param( [string]$Target = "latest", [ValidateSet("", "alpha", "beta")] [string]$Channel = $env:UNITY_CLI_CHANNEL ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" # Faster downloads $CdnBase = "https://public-cdn.cloud.unity3d.com/hub/prod/cli/" $InstallDir = Join-Path $env:LOCALAPPDATA "Unity\bin" # Channel selects the version manifest: latest.json, latest-beta.json, latest-alpha.json $ManifestFile = switch ($Channel) { "alpha" { "latest-alpha.json" } "beta" { "latest-beta.json" } default { "latest.json" } } # ─── Platform check ─────────────────────────────────────────────────────────── if ([System.IntPtr]::Size -ne 8) { Write-Host "`n error Windows 32-bit is not supported. Unity CLI requires a 64-bit system.`n" -ForegroundColor Red exit 1 } $arch = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { "arm64" } else { "x64" } $platformKey = "win32-$arch" # ─── Output helpers ─────────────────────────────────────────────────────────── function Write-Info($msg) { Write-Host " " -NoNewline Write-Host "info" -ForegroundColor Cyan -NoNewline Write-Host " $msg" } function Write-Warn($msg) { Write-Host " " -NoNewline Write-Host "warn" -ForegroundColor Yellow -NoNewline Write-Host " $msg" } function Fail($msg) { Write-Host "`n " -NoNewline Write-Host "error" -ForegroundColor Red -NoNewline Write-Host " $msg`n" exit 1 } # ─── Fetch manifest ─────────────────────────────────────────────────────────── function Get-JsonManifest($url, $failMsg) { try { $iwrParams = @{ Uri = $url; ErrorAction = "Stop" } if ($PSVersionTable.PSVersion.Major -lt 6) { $iwrParams["UseBasicParsing"] = $true } $response = Invoke-WebRequest @iwrParams return $response.Content | ConvertFrom-Json } catch { if (-not $failMsg) { $failMsg = "Failed to fetch version manifest from $url" } Fail "$failMsg`n $_" } } function Get-Manifest { Write-Info "Checking for latest version..." return Get-JsonManifest "${CdnBase}${ManifestFile}" } # ─── Download binary ────────────────────────────────────────────────────────── function Get-Binary($url, $dest) { Write-Info "Downloading binary..." try { $wc = New-Object System.Net.WebClient $wc.DownloadFile($url, $dest) } catch { Fail "Failed to download binary from $url`n $_" } } # ─── SHA-256 verification ───────────────────────────────────────────────────── function Confirm-Sha256($file, $expected) { Write-Info "Verifying checksum..." $actual = (Get-FileHash -Path $file -Algorithm SHA256).Hash.ToLower() if ($actual -ne $expected.ToLower()) { Remove-Item $file -Force -ErrorAction SilentlyContinue Fail "SHA-256 verification failed.`n Expected: $expected`n Actual: $actual" } } # ─── PATH management ────────────────────────────────────────────────────────── function Add-ToUserPath($dir) { $current = [System.Environment]::GetEnvironmentVariable("PATH", "User") $dirs = @($current -split ";" | Where-Object { $_ -ne "" }) # Windows PATH matching is case-insensitive and a trailing slash is # cosmetic, so "C:\Foo", "c:\foo\" and "C:\FOO" are the same entry. Trim # trailing separators and compare with an ordinal case-insensitive comparer # (no per-entry lowercasing allocations) so we don't append a duplicate that # differs only by case or a trailing slash. $target = $dir.TrimEnd('\', '/') foreach ($existing in $dirs) { if ([string]::Equals($existing.TrimEnd('\', '/'), $target, [System.StringComparison]::OrdinalIgnoreCase)) { return $false # already present } } $newPath = ($dirs + $dir) -join ";" [System.Environment]::SetEnvironmentVariable("PATH", $newPath, "User") Publish-EnvironmentChange return $true } # Broadcast WM_SETTINGCHANGE("Environment") so Explorer and anything launched # from it (new Windows Terminal tabs, Start menu) pick up the PATH change # without signing out. Already-open shells still need a restart — they only # read the environment at startup. Best-effort: a failure must not fail the # install. function Publish-EnvironmentChange { try { if (-not ("UnityCli.NativeMethods" -as [type])) { Add-Type -Namespace UnityCli -Name NativeMethods -MemberDefinition @' [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); '@ } $HWND_BROADCAST = [IntPtr]0xffff $WM_SETTINGCHANGE = 0x001A $SMTO_ABORTIFHUNG = 0x0002 $result = [UIntPtr]::Zero [void][UnityCli.NativeMethods]::SendMessageTimeout( $HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 2000, [ref]$result) } catch { # Non-fatal: the PATH is set either way; only propagation is delayed. } } # ─── Main ───────────────────────────────────────────────────────────────────── Write-Host "" Write-Host " Unity CLI Installer" -ForegroundColor White Write-Host "" # Resolve the manifest for the version we're installing. Each release publishes # its own manifest at /latest.json (always that name, regardless of # channel) alongside the channel manifest, which only ever describes the latest # release. So a pinned -Target goes straight to that release's manifest — the # channel manifest would be the wrong version anyway, and skipping it avoids a # needless request and failure point. Without a pin we resolve "latest" from the # channel manifest. Either way filename/sha256 match the binary we download. if ($Target -eq "latest") { $manifest = Get-Manifest $version = $manifest.version } else { $version = $Target Write-Info "Using requested version: $version" $manifest = Get-JsonManifest "${CdnBase}${version}/latest.json" ` "Failed to fetch manifest for version '$version'. Check that it is a published version.`n`n ${CdnBase}${version}/latest.json" if ($manifest.version -ne $version) { Fail "Version manifest mismatch: requested '$version' but manifest reports '$($manifest.version)'." } } $binaryInfo = $manifest.binaries.$platformKey if (-not $binaryInfo) { $available = ($manifest.binaries | Get-Member -MemberType NoteProperty).Name -join ", " Fail "No binary available for platform '$platformKey'. Available: $available" } $filename = $binaryInfo.filename $sha256 = $binaryInfo.sha256 Write-Info "Installing version $version for $platformKey..." # Create install directory if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null } # Download to a temp path $tmpFile = Join-Path $env:TEMP "unity-cli-install-$([System.Guid]::NewGuid().ToString('N')).exe" $downloadUrl = "${CdnBase}${version}/$filename" Get-Binary $downloadUrl $tmpFile try { # Verify checksum Confirm-Sha256 $tmpFile $sha256 # Move to install location $destFile = Join-Path $InstallDir "unity.exe" Move-Item -Path $tmpFile -Destination $destFile -Force # Add to user PATH $pathUpdated = Add-ToUserPath $InstallDir } catch { Remove-Item $tmpFile -Force -ErrorAction SilentlyContinue throw } # ─── Success ────────────────────────────────────────────────────────────────── Write-Host "" Write-Host " " -NoNewline # Parenthesized so PowerShell evaluates the cast; a bare `[char]0x2714` # argument is passed to Write-Host as the literal string "[char]0x2714". Write-Host ([char]0x2714) -ForegroundColor Green -NoNewline Write-Host " Unity CLI $version installed!" -ForegroundColor White Write-Host "" Write-Host " Get started:" -ForegroundColor White Write-Host " unity --help Show available commands" Write-Host " unity editors list List installed Unity editors" Write-Host " unity install Install a Unity editor" Write-Host " unity upgrade Upgrade to the latest CLI version" Write-Host "" if ($pathUpdated) { Write-Host " Note: Restart your terminal to use " -NoNewline Write-Host "unity" -ForegroundColor Cyan -NoNewline Write-Host "." } else { Write-Host " " -NoNewline Write-Host "unity" -ForegroundColor Cyan -NoNewline Write-Host " is ready. Open a new terminal and run " -NoNewline Write-Host "unity --help" -ForegroundColor Cyan -NoNewline Write-Host " to get started." } Write-Host ""