From: Max Vilimpoc Date: Sat, 5 Sep 2026 23:06:42 +0000 (+0200) Subject: dotfiles: detect that elevation is impossible instead of misreporting it X-Git-Url: https://vilimpoc.org/repos/dotfiles/commitdiff_plain/cd774a195092cd0ab4c8e92862d0c5cf7e04f195?ds=inline;hp=fbe899b67d263a1ae5404d3ed25f5f091c3d2c1e dotfiles: detect that elevation is impossible instead of misreporting it 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) Claude-Session: https://claude.ai/code/session_01Ak6VzfGyaocrykX1LhNMwt --- diff --git a/README.md b/README.md index 323fe36..794657d 100644 --- 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: diff --git a/setup-windows.bat b/setup-windows.bat index b9e4752..bd248a9 100644 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -26,6 +26,31 @@ set "IS_ARM64=0" if /i "%HOST_ARCH%"=="ARM64" set "IS_ARM64=1" echo [setup-windows] Host architecture: %HOST_ARCH% +@rem --------------------------------------------------------------------------- +@rem Can this account elevate? Worked out ONCE, here, because it gates two very +@rem different things: the elevated half far below, and a handful of the "per-user" +@rem winget installs just after this, which are per-user in name only. +@rem +@rem ALREADY - already elevated. IsInRole(Administrator) is false for an admin +@rem running unelevated under UAC, so this means actually elevated, +@rem not merely capable of it. +@rem PROMPT - not elevated, UAC on, so elevation can be requested. +@rem NOLUA - UAC is off machine-wide (EnableLUA = 0) AND this is not an +@rem administrator. Elevation is impossible, not merely declined: +@rem Windows has no prompt to offer. Note that with UAC off, +@rem `Start-Process -Verb RunAs` does not fail - it is silently +@rem ignored, runs the child with the caller's own token, and reports +@rem success, which is why this needs detecting rather than trying. +@rem --------------------------------------------------------------------------- +set "ELEV=PROMPT" +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" +echo [setup-windows] Elevation: %ELEV% +if "%ELEV%"=="NOLUA" ( + echo [setup-windows] This account cannot elevate ^(UAC off, not an administrator^). Packages that + echo [setup-windows] need a machine-wide install will be SKIPPED rather than left to fail; details + echo [setup-windows] at each one, and a summary before the elevated half. +) + @rem --- Non-admin (per-user) winget installs --- @rem @rem No --architecture anywhere on purpose. winget already picks the best @@ -44,7 +69,6 @@ echo [setup-windows] Host architecture: %HOST_ARCH% winget install Anthropic.ClaudeCode winget install Brave.Brave winget install Git.Git -winget install Microsoft.DotNet.SDK.10 winget install Microsoft.PowerShell Microsoft.Sysinternals.ProcessExplorer Microsoft.Sysinternals.ProcessMonitor Microsoft.Sysinternals.SDelete Microsoft.VisualStudioCode Microsoft.WindowsTerminal winget install Python.Python.3.13 @@ -87,7 +111,10 @@ if "%IS_ARM64%"=="1" ( @rem PATH, behind every machine and user entry. setup-windows-no-uac.ps1 then @rem pins the native ninja's directory to the FRONT of the user PATH so the @rem margin does not depend on two append orders staying as they are. -winget install Kitware.CMake +@rem +@rem Ninja here, CMake in the machine-wide group below: Ninja is a portable zip +@rem winget unpacks into the user profile, CMake is an MSI that installs for the +@rem whole machine. winget install Ninja-build.Ninja @rem --- Second, independent Clang: compiler diversity --- @@ -109,12 +136,10 @@ winget install Ninja-build.Ninja @rem -DCMAKE_CXX_COMPILER with a full path when you care. winget install LLVM.LLVM -@rem x64-only manifests: winget falls back to the x64 installer on ARM64 and these -@rem run under Prism emulation. Harmless for what they do here - iperf3 is a -@rem network benchmark bounded by the link, and AGI is an Android-side GPU -@rem profiler whose work happens on the phone. Neither is CPU-bound on this box. +@rem x64-only manifest: winget falls back to the x64 installer on ARM64 and it runs +@rem under Prism emulation. Harmless for what it does here - iperf3 is a network +@rem benchmark bounded by the link, not by CPU. winget install ar51an.iPerf3 -winget install Google.AndroidGPUInspector @rem NASM assembles x86/x86-64 only - there is no ARM64 target in it and no ARM64 @rem build of it. Installed on ARM64 anyway (as emulated x64) because this box @@ -138,6 +163,38 @@ if "%IS_ARM64%"=="1" ( winget install Oracle.VirtualBox ) +@rem --------------------------------------------------------------------------- +@rem Machine-wide installers, despite sitting in the "per-user" section +@rem +@rem These four are not per-user at all. On an account that cannot elevate they +@rem fail on every run, with installer exit codes that say nothing useful: +@rem Microsoft.DotNet.SDK.10 exit 5 (ERROR_ACCESS_DENIED) +@rem Kitware.CMake exit 1603 (generic MSI failure) +@rem Google.AndroidGPUInspector exit 1603 +@rem OpenCppCoverage exit 1 (its installer self-elevates) +@rem +@rem Grouped and skipped outright where elevation is impossible. Attempting a +@rem guaranteed failure four times per run - and paying the download for it - +@rem teaches nobody anything, and the four opaque error codes bury the one line +@rem that matters. On an account that can elevate they run exactly as before. +@rem +@rem NOTE none of these is a build-blocker: the .NET SDK is only here for the WiX +@rem MSI tooling, CMake also ships inside Visual Studio, AGI profiles Android +@rem devices, and OpenCppCoverage cannot instrument ARM64 binaries anyway. +@rem --------------------------------------------------------------------------- +if "%ELEV%"=="NOLUA" ( + echo. + echo [setup-windows] Skipping the machine-wide installers - this account cannot elevate: + echo [setup-windows] Microsoft.DotNet.SDK.10, Kitware.CMake, Google.AndroidGPUInspector, OpenCppCoverage. + echo [setup-windows] None blocks a build. Re-run from an administrator account to get them. + echo. + goto :after_admin_pkgs +) + +winget install Microsoft.DotNet.SDK.10 +winget install Kitware.CMake +winget install Google.AndroidGPUInspector + @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the @rem pytest suite under it to produce an HTML report (the binaries under test build with PDBs, @rem which it reads). The installer elevates via UAC. @@ -148,6 +205,8 @@ if "%IS_ARM64%"=="1" ( @rem cross-compiles but NOT an ARM64 one. For ARM64 coverage, build the ARM64 @rem binaries with /fsanitize-coverage or use the x64 build for the coverage run. winget install OpenCppCoverage.OpenCppCoverage + +:after_admin_pkgs if "%IS_ARM64%"=="1" echo [setup-windows] NOTE: OpenCppCoverage is x86/x64-only - it cannot instrument ARM64 binaries. Run coverage against the x64 build. @rem --- WiX 5.0.2, pinned on purpose --- @@ -214,10 +273,51 @@ if not exist "%DOTNET_EXE%" goto :after_wix set "UAC_LOG=%~dp0setup-windows-uac.log" if exist "%UAC_LOG%" del "%UAC_LOG%" +@rem %ELEV% was worked out at the top of this script - see the comment there for +@rem why `Start-Process -Verb RunAs` cannot be trusted to report this itself. +if "%ELEV%"=="NOLUA" goto :elev_impossible +if "%ELEV%"=="ALREADY" goto :elev_direct + +@rem Not elevated, UAC is on: request it. Expect a prompt. +echo [setup-windows] Requesting elevation ^(expect a UAC prompt^)... powershell -NoProfile -Command "$p = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','""%~dp0setup-windows-with-uac.ps1""' -Wait -PassThru; exit $p.ExitCode" set "UAC_RC=%ERRORLEVEL%" +goto :elev_done + +:elev_direct +echo [setup-windows] Already running elevated; running the elevated half directly. +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-with-uac.ps1" +set "UAC_RC=%ERRORLEVEL%" +goto :elev_done + +:elev_impossible +echo. +echo [setup-windows] SKIPPING the elevated half: this account cannot elevate. +echo [setup-windows] UAC is disabled machine-wide ^(EnableLUA = 0^) and you are not an administrator, +echo [setup-windows] so Windows offers no way to elevate - there is no prompt to accept. With UAC off, +echo [setup-windows] 'Start-Process -Verb RunAs' is silently ignored and reports success, which is why +echo [setup-windows] this used to look like a cancelled prompt. +echo [setup-windows] +echo [setup-windows] Not installed: Visual Studio components ^(including clang-cl^), the WDK, +echo [setup-windows] rsync, and the OpenSSH server. Everything per-user above is unaffected. +echo [setup-windows] +echo [setup-windows] To finish the box, either sign in to an administrator account and re-run this +echo [setup-windows] script, or have an administrator run setup-windows-with-uac.ps1 there. Then +echo [setup-windows] re-run setup-windows-no-uac.ps1 as yourself, so the per-user PATH and .gitconfig +echo [setup-windows] land in YOUR profile rather than the administrator's. +echo. +set "UAC_RC=SKIPPED" + +:elev_done @rem --- Surface the elevated session's output (its window has already closed) --- +@rem +@rem A missing log is NOT self-explanatory, so do not guess at one cause. The +@rem elevated script writes its transcript as almost its first act, so no log +@rem means it never got as far as running: either the prompt was declined, or it +@rem started unelevated and stopped on its own #Requires line. Which of those it +@rem was is already known from %ELEV%, so report that instead of speculating. +if "%UAC_RC%"=="SKIPPED" goto :after_uac_log if exist "%UAC_LOG%" ( echo. echo ===== elevated setup log ^(%UAC_LOG%^) ===== @@ -225,8 +325,10 @@ if exist "%UAC_LOG%" ( echo ===== end of elevated setup log ===== ) else ( echo [setup-windows] WARNING: no elevated log found at "%UAC_LOG%". - echo [setup-windows] The elevated window may have been cancelled at the UAC prompt. + if "%ELEV%"=="PROMPT" echo [setup-windows] The elevated window never started - the UAC prompt was most likely declined. + if "%ELEV%"=="ALREADY" echo [setup-windows] The elevated half exited before writing its transcript; see its output above. ) +:after_uac_log @rem --- Non-elevated PowerShell half --- @rem WinMerge on the user PATH, BinSkim, and the global git config (identity + @@ -249,6 +351,11 @@ if exist "%UAC_LOG%" ( powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0setup-windows-no-uac.ps1" if not "%ERRORLEVEL%"=="0" echo [setup-windows] WARNING: setup-windows-no-uac.ps1 reported a failure ^(see above^); continuing. +@rem Skipping the elevated half is a reported, understood outcome on a box where +@rem elevation is impossible - not a failure to exit non-zero over. The per-user +@rem provisioning above did run, and re-running from an administrator account is +@rem the documented next step. +if "%UAC_RC%"=="SKIPPED" goto :uac_reported if not "%UAC_RC%"=="0" ( echo. echo [setup-windows] ELEVATED SETUP FAILED ^(exit code %UAC_RC%^). See log above. @@ -256,6 +363,11 @@ if not "%UAC_RC%"=="0" ( ) echo. echo [setup-windows] Elevated setup completed successfully. +goto :uac_reported + +:uac_reported +if "%UAC_RC%"=="SKIPPED" echo. +if "%UAC_RC%"=="SKIPPED" echo [setup-windows] Per-user setup complete; the elevated half was skipped ^(see above^). @rem Removed: this doesn't work as well as I hoped, maybe try again later @rem -- Install Headroom --- @@ -280,3 +392,10 @@ if "%IS_ARM64%"=="1" ( echo [setup-windows] unavailable : VirtualBox, the Windows 7 x86 test VM, Intel VTune, echo [setup-windows] and the v141 / Windows XP targeting toolset. ) +@rem The list above is what this script PROVIDES on ARM64, not necessarily what +@rem landed on this run - so point at the audit, which reports the actual state. +if "%ELEV%"=="NOLUA" ( + echo [setup-windows] NOT on this box: everything needing elevation was skipped, including + echo [setup-windows] Visual Studio's clang-cl, the .NET SDK and CMake. The + echo [setup-windows] architecture audit above lists what is really installed. +)