]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: put .local\bin on the user PATH, and skip VirtualBox in a VM
authorMax Vilimpoc <max@vilimpoc.org>
Tue, 29 Sep 2026 09:33:45 +0000 (11:33 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Tue, 29 Sep 2026 09:33:45 +0000 (11:33 +0200)
A new LocalBinPath step puts %USERPROFILE%\.local\bin - where Claude
Code's native installer puts claude.exe - on the user PATH, creating it
if need be. The installer itself only prints a reminder.

VirtualBox is now -NotInVM: when this Windows is itself a guest, the
plan marks it [-] and names the hypervisor, read from the firmware's
SMBIOS manufacturer/model rather than HypervisorPresent, which is also
true on physical hosts running Hyper-V, WSL2 or VBS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README.md
setup-windows-no-uac.ps1
setup-windows-plan.ps1
setup-windows-steps.ps1
setup-windows-with-uac.ps1
setup-windows.bat

index e76e01720e62850f84ec00ca568e3110e06a5587..cf8a85b04a838b0aa3b25e82320667cdd9218d6f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -26,10 +26,10 @@ C/C++ compiler. See
 | File | Purpose |
 | --- | --- |
 | `setup-windows.bat` | Entry point. Shows the plan and lets you choose, then runs the winget installs, launches the elevated half and prints its log, then runs the non-elevated script. |
-| `setup-windows-plan.ps1` | The plan. Run by the batch file before anything else: lists every step for this machine, grouped and numbered, and takes your choice. Run it on its own with `-WhatIf` to preview; add `-HostArch ARM64 -Elevation NOLUA` to preview another kind of box. |
+| `setup-windows-plan.ps1` | The plan. Run by the batch file before anything else: lists every step for this machine, grouped and numbered, and takes your choice. Run it on its own with `-WhatIf` to preview; add `-HostArch ARM64 -Elevation NOLUA`, or `-VirtualMachine Hyper-V` / `-VirtualMachine none`, to preview another kind of box. |
 | `setup-windows-steps.ps1` | The catalog the other four read: every step's name, what it does, and where it cannot run and why. Defines functions only; it is dot-sourced, never run. |
 | `setup-windows-current.ps1` | Asked by the batch file before the Claude Code, NASM and Android Studio installs: is what is on the box already current? Those are the installs that would otherwise run in full every time — see [Re-running](#re-running). |
-| `setup-windows-no-uac.ps1` | The non-elevated, per-user half: WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt; it lists its steps and asks first. |
+| `setup-windows-no-uac.ps1` | The non-elevated, per-user half: `%USERPROFILE%\.local\bin` (where `claude.exe` lives), WinMerge, BinSkim and upstream LLVM on the user `PATH` (x64 only), the global git config (identity, plus `core.sshCommand`), the native `ninja` pinned ahead of the one Visual Studio bundles, and an architecture audit of everything provisioned. Can also be run directly from an ordinary prompt; it lists its steps and asks first. |
 | `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs the OpenSSH Client and Server capabilities and starts `sshd`, unpacks the `rsync-windows` release zip for this architecture (`rsync.exe` plus the `ssh.exe` it runs) into `C:\Tools\rsync` on the machine `PATH`, then installs Visual Studio Community with the required components, the WDK, and the Windows Performance Toolkit, and reports whether Intel VTune Profiler is present. Can also be run directly from an Administrator prompt; it lists its steps and asks first. |
 | `setup-windows-7-test-env.bat` | Prepares a **Windows 7 VM** as a test target driven from the host by `VBoxManage guestcontrol`. Copy it into the guest and run it there; it is idempotent, so re-run it after any snapshot restore. The per-user half needs no UAC (crash-dialog suppression, no screen blanking, a staging directory, the shared folder on `Z:`); the machine-wide half is skipped with a notice unless run elevated inside the guest. It then reports what the box can actually test: DWM composition, printers, audio capture devices. |
 
@@ -120,7 +120,8 @@ still adjust it before pressing Enter unless you also pass `-Yes`.
   were asked already. With nothing selected for a half it is not started — so
   no UAC prompt is raised for an elevated half with no steps.
 - Every step is declared once, in `setup-windows-steps.ps1`: its name, what it
-  does, and — with the reason — whether it is x64-only (`-X64Only`) or needs an
+  does, and — with the reason — whether it is x64-only (`-X64Only`), kept off a
+  Windows that is itself a virtual machine (`-NotInVM`), or needs an
   administrator (`-Admin`). The planner, the batch file and both halves read
   that one list and check each other: a step the list does not know is refused
   rather than run, and a selected step that nothing implements fails the run
@@ -339,7 +340,10 @@ missing or out of date.
   `Anthropic.ClaudeCode` are broken through that route, so the batch file runs
   Anthropic's own installer, `irm https://claude.ai/install.ps1 | iex`: it
   checks the download against the release manifest's SHA-256 and puts `claude`
-  in `%USERPROFILE%\.local\bin`, per-user. Two things are fixed up around it.
+  in `%USERPROFILE%\.local\bin`, per-user. The `LocalBinPath` step in
+  `setup-windows-no-uac.ps1` makes sure that directory is on the user `PATH`
+  (creating it if need be); the installer itself only prints a reminder. Two
+  things are fixed up around it.
   The script picks its ARM64 or x64 build from `$env:PROCESSOR_ARCHITECTURE`
   alone, which reads `AMD64` in an emulated x64 shell on ARM64 (see
   [Detecting the architecture](#detecting-the-architecture)), so the batch file
@@ -670,6 +674,13 @@ Two consequences worth knowing rather than rediscovering:
   Windows 7 *x86* guest, which neither VirtualBox nor Hyper-V on ARM64 can run.
   (That script itself is unchanged; it runs inside the guest, so it never sees
   the host architecture.)
+- **VirtualBox is also skipped inside a virtual machine**, on any architecture.
+  The planner reads the firmware's SMBIOS manufacturer and model
+  (`Win32_ComputerSystem`) and recognises VirtualBox, VMware, Hyper-V (and
+  Azure), QEMU/KVM, Parallels, Xen, EC2 and GCE; the plan's `machine:` line
+  names what it found. It does not use `HypervisorPresent`, which is also true
+  on physical hardware running Hyper-V, WSL2 or VBS. The test VMs belong on the
+  physical host.
 - **The `v141` / Windows XP toolset is skipped**, and the verification step says
   `n/a` instead of warning about it. The components are still *listed* in the
   catalog on Arm, so this is a deliberate skip rather than a hard unavailability
index b048ec3770de382e61a87f14e6036b2959521090..0e9c7d48d2bb14bb6ba6db099cc60847fbe44366 100644 (file)
@@ -11,6 +11,8 @@
   elevated would configure the *administrator's* profile instead of yours.
 
   What this installs / configures:
+    - %USERPROFILE%\.local\bin on the user PATH: where Claude Code's native
+      installer puts claude.exe, and nothing else guarantees it is on the PATH
     - WinMerge on the user PATH (x64 only)
     - vswhere.exe on the user PATH: the Visual Studio installer puts it in
       %ProgramFiles(x86)%\Microsoft Visual Studio\Installer, which nothing adds
@@ -167,6 +169,22 @@ function Add-ToUserPath {
     }
 }
 
+function Add-LocalBinToUserPath {
+    # %USERPROFILE%\.local\bin is where Claude Code's native installer puts
+    # claude.exe (see setup-windows.bat). The installer only prints a reminder
+    # when the directory is not on the PATH, and it is the per-user bin
+    # directory other tools reach for too, so put it there regardless and create
+    # it if it is missing - an entry for a directory that is not there yet would
+    # only be dead until something fills it.
+    Write-Step '%USERPROFILE%\.local\bin on the user PATH'
+    $dir = Join-Path $env:USERPROFILE '.local\bin'
+    if (-not (Test-Path $dir)) {
+        New-Item -ItemType Directory -Path $dir | Out-Null
+        Write-Host "    Created $dir."
+    }
+    Add-ToUserPath $dir
+}
+
 function Add-WinMergeToUserPath {
     Write-Step 'WinMerge on the user PATH'
     $candidates = @(
@@ -1119,6 +1137,7 @@ if (([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::
 # ArchAudit last on purpose: it reports on what the steps before it (and the
 # winget installs in setup-windows.bat) actually put on the box.
 $steps = @{
+    LocalBinPath  = { Add-LocalBinToUserPath }
     WinMergePath  = { Add-WinMergeToUserPath }
     VsWhere       = { Add-VsWhereToUserPath }
     BinSkim       = { Install-BinSkim }
@@ -1148,7 +1167,8 @@ if ($unbacked -or $unlisted) {
 # step of the same name; still accepted.
 $plan = @(Resolve-SetupSelection -Steps $catalog `
             -Only $(if ($PSBoundParameters.ContainsKey('Only')) { $Only } else { $null }) -Skip $Skip `
-            -Aliases @{ WinMerge = 'WinMergePath' } -IsArm64 $IsArm64)
+            -Aliases @{ WinMerge = 'WinMergePath' } -IsArm64 $IsArm64 `
+            -VirtualMachine (Get-SetupVirtualMachine))
 Write-SetupPlan $plan 'Plan for setup-windows-no-uac.ps1'
 $runCount = @($plan | Where-Object Selected).Count
 if ($runCount -eq 0) {
index add6305e708760ca8e2b95412c450b66387830c5..41dff110f08ea82b802ce18a5e5e31eb5d0f1b7c 100644 (file)
@@ -18,6 +18,7 @@
   Run it on its own to preview the plan for another kind of box:
 
       powershell -File .\setup-windows-plan.ps1 -WhatIf -HostArch ARM64 -Elevation NOLUA
+      powershell -File .\setup-windows-plan.ps1 -WhatIf -VirtualMachine Hyper-V
 
   The steps, and where each one is unavailable and why, come from
   setup-windows-steps.ps1; this script only presents them and records the
@@ -44,6 +45,9 @@ param(
     # here the same way when this script is run on its own.
     [string]   $HostArch,
     [ValidateSet('ALREADY', 'PROMPT', 'NOLUA')] [string] $Elevation,
+    # The hypervisor this Windows is a guest of, or 'none' for physical
+    # hardware. Detected when not given; setup-windows.bat leaves it to that.
+    [string]   $VirtualMachine,
     [string]   $OutFile
 )
 
@@ -110,6 +114,7 @@ function Show-Plan {
     Write-Host ''
     Write-Host 'Setup plan - nothing has been done yet' -ForegroundColor Cyan
     Write-Host "  host:      $ArchLabel"
+    Write-Host "  machine:   $VmLabel"
     Write-Host "  elevation: $ElevLabel"
     foreach ($g in $groups.Keys) {
         $head = " $g  $($groups[$g])"
@@ -210,6 +215,17 @@ if (-not $Elevation) {
     }
 }
 
+# A guest VM is not somewhere to put a hypervisor of its own: VirtualBox, for
+# one, is for the test VMs, which belong on the physical host. Detected once
+# here; the steps that care are -NotInVM in setup-windows-steps.ps1.
+if (-not $VirtualMachine) { $VirtualMachine = Get-SetupVirtualMachine }
+if ($VirtualMachine -eq 'none') { $VirtualMachine = $null }
+$VmLabel = if ($VirtualMachine) {
+    "virtual machine ($VirtualMachine) - steps meant for the physical host are marked [-]"
+} else {
+    'physical hardware'
+}
+
 $ArchLabel = switch ($HostArch) {
     'ARM64' { 'ARM64 - the lean build box, one C/C++ compiler; x64-only steps are marked [-]' }
     'AMD64' { 'x64 - the full workstation' }
@@ -234,7 +250,7 @@ $onlyGiven = $PSBoundParameters.ContainsKey('Only')
 try {
     $plan = @(Resolve-SetupSelection -Steps (Get-SetupCatalog -IsArm64 $IsArm64) `
                 -Only $(if ($onlyGiven) { $Only } else { $null }) -Skip $Skip `
-                -IsArm64 $IsArm64 -Elevation $Elevation)
+                -IsArm64 $IsArm64 -Elevation $Elevation -VirtualMachine $VirtualMachine)
 } catch {
     # A mistyped -Only / -Skip. Said plainly, and no hand-off file, so the batch
     # file stops before doing anything.
index 4ff5786224f015d303c721417d3e5c99a8d29de4..765bb37ba9538be278215e92179b1c49d4824436 100644 (file)
@@ -15,8 +15,9 @@
     setup-windows-no-uac.ps1     takes the 'user' steps - per-user configuration
 
   THIS IS THE ONE PLACE a step's availability is decided. -X64Only, with its
-  reason, keeps a step off ARM64; -Admin keeps it off an account that cannot
-  elevate. The planner shows both as "[-]" rows with the reason, the batch file
+  reason, keeps a step off ARM64; -NotInVM, with its reason, keeps it off a
+  Windows that is itself a virtual machine; -Admin keeps it off an account that
+  cannot elevate. The planner shows each as a "[-]" row with the reason, the batch file
   prints the same reason in the log where the step would have run, and the two
   halves apply them again when run on their own. This replaces the :x64only
   subroutine that used to sit at the bottom of setup-windows.bat and the
@@ -51,6 +52,9 @@ function New-SetupStep {
         [string]   $Package,
         # Not on ARM64, because <this>. Shown in the plan and in the run log.
         [string]   $X64Only,
+        # Not when this Windows is a guest in a virtual machine, because <this>.
+        # Shown in the plan and in the run log, like -X64Only.
+        [string]   $NotInVM,
         # Needs an account that can elevate: a machine-wide installer, or part
         # of the elevated half.
         [switch]   $Admin,
@@ -66,6 +70,7 @@ function New-SetupStep {
         Text    = $shown
         Package = $Package
         X64Only = $X64Only
+        NotInVM = $NotInVM
         Admin   = [bool]$Admin
         Needs   = $Needs
     }
@@ -115,7 +120,8 @@ function Get-SetupCatalog {
         New-SetupStep NASM                U bat -Package NASM.NASM `
             -X64Only 'x86/x64-only assembler; MSVC armasm64 is the native one'
         New-SetupStep VirtualBox          U bat -Package Oracle.VirtualBox -Text 'for the Windows 7 test VM' `
-            -X64Only 'no ARM64 host build, so no Windows 7 x86 test VM either'
+            -X64Only 'no ARM64 host build, so no Windows 7 x86 test VM either' `
+            -NotInVM 'the test VMs belong on the physical host, not nested in a guest'
 
         # --- setup-windows.bat: machine-wide installers, despite running unelevated ---
         New-SetupStep DotNetSdk           M bat -Admin -Package Microsoft.DotNet.SDK.10 -Text 'for the WiX tooling'
@@ -145,6 +151,8 @@ function Get-SetupCatalog {
         New-SetupStep VTune               E uac -Admin -Text 'Intel VTune Profiler: report whether it is installed (installs nothing)'
 
         # --- setup-windows-no-uac.ps1: per-user configuration ---
+        # First, so the PATH it fixes is in place before anything reports on it.
+        New-SetupStep LocalBinPath        C user -Text '%USERPROFILE%\.local\bin on the user PATH (where Claude Code''s launcher lives)'
         New-SetupStep WinMergePath        C user -Needs WinMerge -Text 'WinMerge on the user PATH' `
             -X64Only 'WinMerge is not installed on ARM64'
         New-SetupStep VsWhere             C user -Needs VisualStudio -Text 'vswhere.exe on the user PATH'
@@ -168,15 +176,54 @@ function Get-SetupCatalog {
 function Get-SetupStepBlocker {
     # Why this step cannot run on this machine at all, or $null if it can.
     # Architecture first: an x64-only step is off ARM64 before elevation is even
-    # consulted, which is the more useful reason to show.
-    param($Step, [bool] $IsArm64, [string] $Elevation)
+    # consulted, which is the more useful reason to show. Then the machine
+    # itself, then the account.
+    param($Step, [bool] $IsArm64, [string] $Elevation, [string] $VirtualMachine)
     if ($IsArm64 -and $Step.X64Only) { return "not on ARM64 - $($Step.X64Only)" }
+    if ($VirtualMachine -and $Step.NotInVM) {
+        return "not in a virtual machine ($VirtualMachine) - $($Step.NotInVM)"
+    }
     if ($Elevation -eq 'NOLUA' -and $Step.Admin) {
         return 'needs an administrator - this account cannot elevate'
     }
     $null
 }
 
+function Get-SetupVirtualMachine {
+    # The hypervisor this Windows is running under as a guest, by name, or $null
+    # on physical hardware.
+    #
+    # Read from the SMBIOS strings the firmware reports - Win32_ComputerSystem's
+    # Manufacturer and Model - which every common hypervisor fills in with its own
+    # name. NOT from HypervisorPresent: that is true on a PHYSICAL box too as soon
+    # as Hyper-V, WSL2, Windows Sandbox or virtualization-based security is on,
+    # which is most Windows 11 machines - the host OS then runs in the root
+    # partition, above a hypervisor, without being anyone's guest.
+    #
+    # A lookup that fails says "physical": this only ever takes steps away, and a
+    # step wrongly offered on a VM is one you can switch off in the plan.
+    $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
+    if (-not $cs) { return $null }
+    $smbios = "$($cs.Manufacturer) | $($cs.Model)"
+    # Hyper-V's pair is matched whole: Microsoft is also the manufacturer of
+    # every Surface, and "Virtual Machine" is the model only in a guest. Azure
+    # guests report the same pair.
+    $known = [ordered]@{
+        'innotek|VirtualBox'                         = 'VirtualBox'
+        'VMware'                                     = 'VMware'
+        '^Microsoft Corporation \| Virtual Machine$' = 'Hyper-V'
+        'QEMU|KVM|Bochs'                             = 'QEMU/KVM'
+        'Parallels'                                  = 'Parallels'
+        '\bXen\b|HVM domU'                           = 'Xen'
+        'Amazon EC2'                                 = 'Amazon EC2'
+        'Google Compute Engine'                      = 'Google Compute Engine'
+    }
+    foreach ($pattern in $known.Keys) {
+        if ($smbios -match $pattern) { return $known[$pattern] }
+    }
+    $null
+}
+
 function Split-SetupNames {
     # `powershell -File` hands a whole "A,B,C" to an array parameter as ONE
     # string, so split here and accept either spelling.
@@ -212,13 +259,16 @@ function Resolve-SetupSelection {
         [string[]]  $Skip = @(),
         [hashtable] $Aliases = @{},
         [bool]      $IsArm64,
-        [string]    $Elevation
+        [string]    $Elevation,
+        # The hypervisor's name when this Windows is a guest (see
+        # Get-SetupVirtualMachine), $null or empty on physical hardware.
+        [string]    $VirtualMachine
     )
     $want = if ($null -eq $Only) { @($Steps.Id) } else { @(Resolve-SetupStepNames $Only $Steps $Aliases) }
     $drop = @(Resolve-SetupStepNames $Skip $Steps $Aliases)
     $named = @(Split-SetupNames $Only | ForEach-Object { if ($Aliases.ContainsKey($_)) { $Aliases[$_] } else { $_ } })
     foreach ($s in $Steps) {
-        $why = Get-SetupStepBlocker $s $IsArm64 $Elevation
+        $why = Get-SetupStepBlocker $s $IsArm64 $Elevation $VirtualMachine
         if ($why -and $named -contains $s.Id) {
             Write-Warning "$($s.Id) was asked for but cannot run here: $why"
         }
index 4e3602f59ad17163906cf4c3ae1aef29ecac56e3..3832ecf3ad9aac03ee1469f0cc3d8fef3acca15f 100644 (file)
@@ -224,7 +224,7 @@ function Invoke-VsModify {
 # Elevation is a given here (#Requires above), so nothing is blocked on it.\r
 $Plan = @(Resolve-SetupSelection -Steps @(Get-SetupCatalog -IsArm64 $IsArm64 | Where-Object Runner -eq 'uac') `\r
             -Only $(if ($PSBoundParameters.ContainsKey('Only')) { $Only } else { $null }) -Skip $Skip `\r
-            -IsArm64 $IsArm64 -Elevation 'ALREADY')\r
+            -IsArm64 $IsArm64 -Elevation 'ALREADY' -VirtualMachine (Get-SetupVirtualMachine))\r
 $StepIds    = @($Plan | ForEach-Object { $_.Id })\r
 $RunSteps   = @($Plan | Where-Object Selected | ForEach-Object { $_.Id })\r
 $AskedSteps = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)\r
index 23f2ff9b812536c1191b69f8ed57134441aab753..a6bc8ea30e7b9562b00af1a30e7ca9c93e1a1258 100644 (file)
@@ -347,6 +347,12 @@ if not "%ERRORLEVEL%"=="0" call :winget NASM NASM.NASM
 @rem This is also what makes setup-windows-7-test-env.bat unreachable from an\r
 @rem ARM64 host: it prepares a Windows 7 *x86* guest, and neither VirtualBox nor\r
 @rem Hyper-V on ARM64 can run one.\r
+@rem\r
+@rem Nor is it installed when this Windows is itself a virtual machine, which the\r
+@rem planner reads from the firmware's SMBIOS strings: the test VMs belong on the\r
+@rem physical host, and nesting VirtualBox inside a guest needs nested\r
+@rem virtualization the outer hypervisor usually does not offer. The plan marks it\r
+@rem -NotInVM, with that reason, and names the hypervisor it found.\r
 call :winget VirtualBox Oracle.VirtualBox\r
 \r
 @rem ---------------------------------------------------------------------------\r