]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: detect that elevation is impossible instead of misreporting it
authorMax Vilimpoc <max@vilimpoc.org>
Sat, 5 Sep 2026 23:06:42 +0000 (01:06 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Sat, 5 Sep 2026 23:06:42 +0000 (01:06 +0200)
Running as a standard user on a box with UAC disabled produced no prompt
at all, and the script blamed a cancelled one.

The cause is that Start-Process -Verb RunAs does not report this. With
UAC off machine-wide (EnableLUA = 0) the RunAs verb is SILENTLY IGNORED:
no prompt appears, the child starts with the caller's own unelevated
token, and Start-Process returns exit code 0 as though it had worked.
Measured on the box: exit 0 from Start-Process, while the child reported
itself as the unelevated calling user. The elevated script then refuses
itself on its #Requires -RunAsAdministrator line, exits 1, and never
reaches Start-Transcript - so there is no log either. That is
indistinguishable from a declined prompt unless you know to look, which
is why the old message sent you hunting for a prompt that could never
have appeared.

So decide up front rather than trying and guessing afterwards. One check
at the top of setup-windows.bat classifies the account as ALREADY (run
the elevated half directly - asking to elevate what already is elevated
is pointless, and on a UAC-off box RunAs would be the no-op above),
PROMPT (request it, expect a consent or credential prompt), or NOLUA
(elevation is impossible; say so and skip). IsInRole(Administrator) is
false for an admin running unelevated under UAC, which is what makes
ALREADY mean actually elevated rather than merely capable.

The missing-log message now says which of the two things happened, using
that classification, rather than asserting a cancelled prompt.

Also stop running four installers that cannot succeed. The .NET SDK,
CMake, Android GPU Inspector and OpenCppCoverage sit in the "per-user"
section but are machine-wide installers, and on an account that cannot
elevate they failed on every run with codes that explain nothing - exit 5
(ERROR_ACCESS_DENIED), MSI 1603 twice, and exit 1. Grouped and skipped
with a single clear line when elevation is impossible. None of them
blocks a build: the .NET SDK is only here 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 now a success (exit 0) rather than a reported failure -
the per-user half did its job - and the closing ARM64 summary points at
the architecture audit for what actually landed, since that list
describes what the script provides rather than what this run achieved.

All three paths exercised: NOLUA skips and exits 0, ALREADY runs the
elevated half directly, PROMPT requests elevation and reports a declined
prompt correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt

README.md
setup-windows.bat

index 323fe36e5fafce482f36aa40cd8a8be380e1584b..794657dc784947785ba0e8ae1c38fb5408718b6a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -324,19 +324,47 @@ Anything emulated with a listed reason prints as expected. Anything emulated
 *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:
 
index b9e47529fe28e08d6d4164e51802df397e2749a5..bd248a917a057c349fade212e17ec5ebe26abd0b 100644 (file)
@@ -26,6 +26,31 @@ set "IS_ARM64=0"
 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
@@ -44,7 +69,6 @@ echo [setup-windows] Host architecture: %HOST_ARCH%
 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
@@ -87,7 +111,10 @@ if "%IS_ARM64%"=="1" (
 @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
@@ -109,12 +136,10 @@ winget install Ninja-build.Ninja
 @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
@@ -138,6 +163,38 @@ if "%IS_ARM64%"=="1" (
     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
@@ -148,6 +205,8 @@ if "%IS_ARM64%"=="1" (
 @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
@@ -214,10 +273,51 @@ if not exist "%DOTNET_EXE%" goto :after_wix
 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
@@ -225,8 +325,10 @@ if exist "%UAC_LOG%" (
     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
@@ -249,6 +351,11 @@ if exist "%UAC_LOG%" (
 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
@@ -256,6 +363,11 @@ if not "%UAC_RC%"=="0" (
 )\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
@@ -280,3 +392,10 @@ if "%IS_ARM64%"=="1" (
     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