*without* one is called out, with a remedy where a native build exists — which is
how the `ninja` problem below was found.
-### You need an administrator for the elevated half
-
-`setup-windows.bat` assumes whoever runs it **can elevate**. If the account is a
-standard user, the UAC prompt is an over-the-shoulder *credential* prompt rather
-than a Yes/No, and declining it leaves the elevated half unrun — no Visual Studio
-components (so no `clang-cl`), no WDK, no `rsync`, no `sshd`. The batch reports
-`ELEVATED SETUP FAILED` and prints no log, because none was written.
-
-The same applies to several packages in the *non-elevated* section, which are
-per-user only in name: the .NET SDK (`exit 5`), CMake and Android GPU Inspector
-(MSI `1603`), and OpenCppCoverage (its installer self-elevates) all fail for a
-standard user. Sign in to an administrator account to provision, or expect that
-subset to be missing.
+### Elevation, and the case where there is no prompt
+
+`setup-windows.bat` works out **once, at the top**, whether this account can
+elevate, and reports it as `Elevation: ALREADY | PROMPT | NOLUA`:
+
+| | |
+| --- | --- |
+| `ALREADY` | Already elevated. The elevated half runs **directly** — no point asking to elevate what already is. |
+| `PROMPT` | Not elevated, UAC on. Elevation is requested: an administrator gets a consent prompt, a standard user gets an over-the-shoulder **credential** prompt. |
+| `NOLUA` | UAC is off machine-wide (`EnableLUA = 0`) **and** this is not an administrator. Elevation is *impossible*, not merely declined. |
+
+`IsInRole(Administrator)` is false for an admin running unelevated under UAC,
+which is what makes `ALREADY` mean *actually elevated* rather than *could be*.
+
+**Why `NOLUA` needs detecting rather than attempting.** `Start-Process -Verb
+RunAs` is not honest about it. With UAC disabled the `RunAs` verb is **silently
+ignored**: no prompt appears, the child starts with the caller's own unelevated
+token, and `Start-Process` reports **exit code 0** as though it had worked. The
+elevated script then refuses itself on its `#Requires -RunAsAdministrator` line,
+exits 1, and never reaches its `Start-Transcript` — so there is no log either.
+Read naively that is indistinguishable from a cancelled UAC prompt, which is
+exactly what this script used to report, sending you to look for a prompt that
+could never have appeared.
+
+Measured on such a box: `Start-Process -Verb RunAs` returned exit 0 while the
+child reported itself as the *unelevated* calling user.
+
+**What is skipped when elevation is impossible.** The elevated half, plus four
+packages that sit in the "per-user" section but are machine-wide installers.
+They used to fail on every run with opaque codes — `exit 5`
+(`ERROR_ACCESS_DENIED`) for the .NET SDK, MSI `1603` for CMake and Android GPU
+Inspector, `exit 1` for OpenCppCoverage, whose installer self-elevates. They are
+now skipped with one clear line instead. None blocks a build: the .NET SDK is
+only there for the WiX MSI tooling, CMake also ships inside Visual Studio, AGI
+profiles Android devices, and OpenCppCoverage cannot instrument ARM64 binaries
+anyway.
+
+A `NOLUA` run is a **success** (exit 0), not a failure — the per-user half did
+its job. To finish the box, run it from an administrator account, then re-run
+`setup-windows-no-uac.ps1` as yourself so the per-user `PATH` and `.gitconfig`
+land in *your* profile rather than the administrator's.
Two consequences worth knowing rather than rediscovering:
if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1"\r
echo [setup-windows] Host architecture: %HOST_ARCH%\r
\r
+@rem ---------------------------------------------------------------------------\r
+@rem Can this account elevate? Worked out ONCE, here, because it gates two very\r
+@rem different things: the elevated half far below, and a handful of the "per-user"\r
+@rem winget installs just after this, which are per-user in name only.\r
+@rem\r
+@rem ALREADY - already elevated. IsInRole(Administrator) is false for an admin\r
+@rem running unelevated under UAC, so this means actually elevated,\r
+@rem not merely capable of it.\r
+@rem PROMPT - not elevated, UAC on, so elevation can be requested.\r
+@rem NOLUA - UAC is off machine-wide (EnableLUA = 0) AND this is not an\r
+@rem administrator. Elevation is impossible, not merely declined:\r
+@rem Windows has no prompt to offer. Note that with UAC off,\r
+@rem `Start-Process -Verb RunAs` does not fail - it is silently\r
+@rem ignored, runs the child with the caller's own token, and reports\r
+@rem success, which is why this needs detecting rather than trying.\r
+@rem ---------------------------------------------------------------------------\r
+set "ELEV=PROMPT"\r
+for /f "usebackq tokens=*" %%A in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$id=[Security.Principal.WindowsIdentity]::GetCurrent(); if (([Security.Principal.WindowsPrincipal]$id).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 'ALREADY' } elseif ((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -ErrorAction SilentlyContinue).EnableLUA -eq 0) { 'NOLUA' } else { 'PROMPT' }"`) do set "ELEV=%%A"\r
+echo [setup-windows] Elevation: %ELEV%\r
+if "%ELEV%"=="NOLUA" (\r
+ echo [setup-windows] This account cannot elevate ^(UAC off, not an administrator^). Packages that\r
+ echo [setup-windows] need a machine-wide install will be SKIPPED rather than left to fail; details\r
+ echo [setup-windows] at each one, and a summary before the elevated half.\r
+)\r
+\r
@rem --- Non-admin (per-user) winget installs ---\r
@rem\r
@rem No --architecture anywhere on purpose. winget already picks the best\r
winget install Anthropic.ClaudeCode\r
winget install Brave.Brave\r
winget install Git.Git\r
-winget install Microsoft.DotNet.SDK.10\r
winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal\r
winget install Python.Python.3.13\r
\r
@rem PATH, behind every machine and user entry. setup-windows-no-uac.ps1 then\r
@rem pins the native ninja's directory to the FRONT of the user PATH so the\r
@rem margin does not depend on two append orders staying as they are.\r
-winget install Kitware.CMake\r
+@rem\r
+@rem Ninja here, CMake in the machine-wide group below: Ninja is a portable zip\r
+@rem winget unpacks into the user profile, CMake is an MSI that installs for the\r
+@rem whole machine.\r
winget install Ninja-build.Ninja\r
\r
@rem --- Second, independent Clang: compiler diversity ---\r
@rem -DCMAKE_CXX_COMPILER with a full path when you care.\r
winget install LLVM.LLVM\r
\r
-@rem x64-only manifests: winget falls back to the x64 installer on ARM64 and these\r
-@rem run under Prism emulation. Harmless for what they do here - iperf3 is a\r
-@rem network benchmark bounded by the link, and AGI is an Android-side GPU\r
-@rem profiler whose work happens on the phone. Neither is CPU-bound on this box.\r
+@rem x64-only manifest: winget falls back to the x64 installer on ARM64 and it runs\r
+@rem under Prism emulation. Harmless for what it does here - iperf3 is a network\r
+@rem benchmark bounded by the link, not by CPU.\r
winget install ar51an.iPerf3\r
-winget install Google.AndroidGPUInspector\r
\r
@rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64\r
@rem build of it. Installed on ARM64 anyway (as emulated x64) because this box\r
winget install Oracle.VirtualBox\r
)\r
\r
+@rem ---------------------------------------------------------------------------\r
+@rem Machine-wide installers, despite sitting in the "per-user" section\r
+@rem\r
+@rem These four are not per-user at all. On an account that cannot elevate they\r
+@rem fail on every run, with installer exit codes that say nothing useful:\r
+@rem Microsoft.DotNet.SDK.10 exit 5 (ERROR_ACCESS_DENIED)\r
+@rem Kitware.CMake exit 1603 (generic MSI failure)\r
+@rem Google.AndroidGPUInspector exit 1603\r
+@rem OpenCppCoverage exit 1 (its installer self-elevates)\r
+@rem\r
+@rem Grouped and skipped outright where elevation is impossible. Attempting a\r
+@rem guaranteed failure four times per run - and paying the download for it -\r
+@rem teaches nobody anything, and the four opaque error codes bury the one line\r
+@rem that matters. On an account that can elevate they run exactly as before.\r
+@rem\r
+@rem NOTE none of these is a build-blocker: the .NET SDK is only here for the WiX\r
+@rem MSI tooling, CMake also ships inside Visual Studio, AGI profiles Android\r
+@rem devices, and OpenCppCoverage cannot instrument ARM64 binaries anyway.\r
+@rem ---------------------------------------------------------------------------\r
+if "%ELEV%"=="NOLUA" (\r
+ echo.\r
+ echo [setup-windows] Skipping the machine-wide installers - this account cannot elevate:\r
+ echo [setup-windows] Microsoft.DotNet.SDK.10, Kitware.CMake, Google.AndroidGPUInspector, OpenCppCoverage.\r
+ echo [setup-windows] None blocks a build. Re-run from an administrator account to get them.\r
+ echo.\r
+ goto :after_admin_pkgs\r
+)\r
+\r
+winget install Microsoft.DotNet.SDK.10\r
+winget install Kitware.CMake\r
+winget install Google.AndroidGPUInspector\r
+\r
@rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
@rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs,\r
@rem which it reads). The installer elevates via UAC.\r
@rem cross-compiles but NOT an ARM64 one. For ARM64 coverage, build the ARM64\r
@rem binaries with /fsanitize-coverage or use the x64 build for the coverage run.\r
winget install OpenCppCoverage.OpenCppCoverage\r
+\r
+:after_admin_pkgs\r
if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only - it cannot instrument ARM64 binaries. Run coverage against the x64 build.\r
\r
@rem --- WiX 5.0.2, pinned on purpose ---\r
set "UAC_LOG=%~dp0setup-windows-uac.log"\r
if exist "%UAC_LOG%" del "%UAC_LOG%"\r
\r
+@rem %ELEV% was worked out at the top of this script - see the comment there for\r
+@rem why `Start-Process -Verb RunAs` cannot be trusted to report this itself.\r
+if "%ELEV%"=="NOLUA" goto :elev_impossible\r
+if "%ELEV%"=="ALREADY" goto :elev_direct\r
+\r
+@rem Not elevated, UAC is on: request it. Expect a prompt.\r
+echo [setup-windows] Requesting elevation ^(expect a UAC prompt^)...\r
powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode"\r
set "UAC_RC=%ERRORLEVEL%"\r
+goto :elev_done\r
+\r
+:elev_direct\r
+echo [setup-windows] Already running elevated; running the elevated half directly.\r
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1"\r
+set "UAC_RC=%ERRORLEVEL%"\r
+goto :elev_done\r
+\r
+:elev_impossible\r
+echo.\r
+echo [setup-windows] SKIPPING the elevated half: this account cannot elevate.\r
+echo [setup-windows] UAC is disabled machine-wide ^(EnableLUA = 0^) and you are not an administrator,\r
+echo [setup-windows] so Windows offers no way to elevate - there is no prompt to accept. With UAC off,\r
+echo [setup-windows] 'Start-Process -Verb RunAs' is silently ignored and reports success, which is why\r
+echo [setup-windows] this used to look like a cancelled prompt.\r
+echo [setup-windows]\r
+echo [setup-windows] Not installed: Visual Studio components ^(including clang-cl^), the WDK,\r
+echo [setup-windows] rsync, and the OpenSSH server. Everything per-user above is unaffected.\r
+echo [setup-windows]\r
+echo [setup-windows] To finish the box, either sign in to an administrator account and re-run this\r
+echo [setup-windows] script, or have an administrator run setup-windows-with-uac.ps1 there. Then\r
+echo [setup-windows] re-run setup-windows-no-uac.ps1 as yourself, so the per-user PATH and .gitconfig\r
+echo [setup-windows] land in YOUR profile rather than the administrator's.\r
+echo.\r
+set "UAC_RC=SKIPPED"\r
+\r
+:elev_done\r
\r
@rem --- Surface the elevated session's output (its window has already closed) ---\r
+@rem\r
+@rem A missing log is NOT self-explanatory, so do not guess at one cause. The\r
+@rem elevated script writes its transcript as almost its first act, so no log\r
+@rem means it never got as far as running: either the prompt was declined, or it\r
+@rem started unelevated and stopped on its own #Requires line. Which of those it\r
+@rem was is already known from %ELEV%, so report that instead of speculating.\r
+if "%UAC_RC%"=="SKIPPED" goto :after_uac_log\r
if exist "%UAC_LOG%" (\r
echo.\r
echo ===== elevated setup log ^(%UAC_LOG%^) =====\r
echo ===== end of elevated setup log =====\r
) else (\r
echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%".\r
- echo [setup-windows] The elevated window may have been cancelled at the UAC prompt.\r
+ if "%ELEV%"=="PROMPT" echo [setup-windows] The elevated window never started - the UAC prompt was most likely declined.\r
+ if "%ELEV%"=="ALREADY" echo [setup-windows] The elevated half exited before writing its transcript; see its output above.\r
)\r
+:after_uac_log\r
\r
@rem --- Non-elevated PowerShell half ---\r
@rem WinMerge on the user PATH, BinSkim, and the global git config (identity +\r
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1"\r
if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing.\r
\r
+@rem Skipping the elevated half is a reported, understood outcome on a box where\r
+@rem elevation is impossible - not a failure to exit non-zero over. The per-user\r
+@rem provisioning above did run, and re-running from an administrator account is\r
+@rem the documented next step.\r
+if "%UAC_RC%"=="SKIPPED" goto :uac_reported\r
if not "%UAC_RC%"=="0" (\r
echo.\r
echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above.\r
)\r
echo.\r
echo [setup-windows] Elevated setup completed successfully.\r
+goto :uac_reported\r
+\r
+:uac_reported\r
+if "%UAC_RC%"=="SKIPPED" echo.\r
+if "%UAC_RC%"=="SKIPPED" echo [setup-windows] Per-user setup complete; the elevated half was skipped ^(see above^).\r
\r
@rem Removed: this doesn't work as well as I hoped, maybe try again later\r
@rem -- Install Headroom ---\r
echo [setup-windows] unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune,\r
echo [setup-windows] and the v141 / Windows XP targeting toolset.\r
)\r
+@rem The list above is what this script PROVIDES on ARM64, not necessarily what\r
+@rem landed on this run - so point at the audit, which reports the actual state.\r
+if "%ELEV%"=="NOLUA" (\r
+ echo [setup-windows] NOT on this box: everything needing elevation was skipped, including\r
+ echo [setup-windows] Visual Studio's clang-cl, the .NET SDK and CMake. The\r
+ echo [setup-windows] architecture audit above lists what is really installed.\r
+)\r