From: Max Vilimpoc Date: Tue, 25 Aug 2026 10:45:40 +0000 (+0200) Subject: dotfiles: sync the Windows provisioning scripts X-Git-Url: https://vilimpoc.org/repos/dotfiles/commitdiff_plain/452f525c182a835867d0f229c821c41118d15808?ds=inline dotfiles: sync the Windows provisioning scripts Three changes made in the other copy of these scripts, ported back so the two do not drift. The scripts are now byte-identical apart from a few naming lines and the one divergence that is deliberate: this copy keeps the PLACEHOLDER git identity, which the README tells you to edit before running. OpenSSH Server. Installed from the Windows on-demand capability (10/1809 and later), set Automatic, started, and reachable on all firewall profiles. That last part is the one worth having: the capability ships its own inbound rule, but it is Private-only on some images, and a VM's host-only or bridged adapter gets classified Public more often than not -- which presents as a service that is plainly running and plainly unreachable. That rule is adopted rather than duplicated. OpenSSH-Server-In-TCP is the name the capability itself uses, so a second rule beside it under another name would leave the narrow one in place and merely work around it, while one under the same name would collide. Widen it to all profiles if it exists, create it if it does not. One rule either way, under the name the platform expects. rsync. Windows ships the SSH transport and nothing to run over it, so `rsync host:path` has no remote end. The nuket/rsync-windows build is downloaded to C:\Tools\rsync and added to the machine PATH. Not "Program Files", because the fallback when PATH lookup fails is --rsync-path and a path with spaces is painful to quote through two shells. Machine rather than user PATH, because the remote end runs as `rsync --server ...` in a non-interactive session with no login shell: Win32-OpenSSH composes that environment from the registry, so a machine entry resolves there and does so for every account on the box. sshd is restarted after the write, since the running service holds the environment it started with. BinSkim now checks before it fetches. The .nupkg is a self-contained .NET build -- 141 MB at 4.4.9.11 -- and the old code downloaded it every run before working out it had nothing to do. The flat-container index is a few KB of JSON; take the newest non-prerelease and compare against nupkg-version.txt beside the installed tool. The download URL now interpolates the version we checked, rather than the v2 /package/ endpoint that redirects to whatever is newest right now. The PATH append moved out of the download branch so a lost PATH entry no longer costs 141 MB to repair. Both new sections warn rather than throw: a box that cannot run sshd should still finish provisioning the toolchain it came for. README picks up the remote-access notes, including the authorized_keys ACL requirement and the separate file that accounts in the Administrators group need. Co-Authored-By: Claude Opus 5 --- diff --git a/README.md b/README.md index 5fd8878..e1e0c87 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ Windows development-box provisioning scripts. development environment: editors and shells, Python, the Visual Studio 2022 toolchain (including the Clang and Windows XP targeting toolsets), the Windows Driver Kit, and a handful of analysis tools (Sysinternals, OpenCppCoverage, -BinSkim, the Windows Performance Toolkit). +BinSkim, the Windows Performance Toolkit). It also sets the box up to be driven +remotely: OpenSSH Server plus an rsync build for Windows, which is what makes a +throwaway VM reachable from a Linux host. ## Files | File | Purpose | | --- | --- | | `setup-windows.bat` | Entry point. Runs the non-elevated, per-user half (winget installs, user `PATH` edits, global git config), then launches the elevated half and prints its log. | -| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit. Can also be run directly from an Administrator prompt. | +| `setup-windows-with-uac.ps1` | The elevated half, started via UAC by the batch file. Enables `ssh-agent`, installs and starts OpenSSH Server, installs `rsync.exe` to the machine `PATH`, then installs Visual Studio 2022 Community with the required components, the WDK, and the Windows Performance Toolkit. Can also be run directly from an Administrator prompt. | ## Usage @@ -46,6 +48,22 @@ BinSkim, the Windows Performance Toolkit). script; the batch file prints it when the elevated window closes. The log is gitignored, as it contains local paths. - Both halves are idempotent — re-running skips anything already installed. + BinSkim in particular checks NuGet for the newest stable version *before* + downloading: the package is a self-contained .NET build well over 100 MB, and + re-provisioning an up-to-date box should not pay for it. The installed version + is tracked in `nupkg-version.txt` beside the tool. +- **Remote access.** OpenSSH Server is installed from the Windows on-demand + capability (10/1809+), set to start automatically, and given an inbound TCP 22 + firewall rule on *all* profiles — a VM's host-only or bridged adapter is + routinely classified Public, which is the usual reason a running `sshd` is + unreachable. Windows ships no `rsync`, so a build of it + ([nuket/rsync-windows](https://github.com/nuket/rsync-windows)) is installed to + `C:\Tools\rsync` and added to the **machine** `PATH`. That last detail matters: + the remote end of an `rsync` runs non-interactively, with no login shell, and + Win32-OpenSSH builds that environment from the registry `PATH` rather than from + a profile. Key auth needs `~/.ssh/authorized_keys` ACL'd to just you and + `SYSTEM`; accounts in the Administrators group use + `C:\ProgramData\ssh\administrators_authorized_keys` instead. - Visual Studio is installed in three labelled passes (base workload, Clang/LLVM, XP toolset) so a failure identifies which component group is responsible. - The scripts were extracted from a native Windows project, so the component diff --git a/setup-windows-with-uac.ps1 b/setup-windows-with-uac.ps1 index 254db14..8da5c55 100644 --- a/setup-windows-with-uac.ps1 +++ b/setup-windows-with-uac.ps1 @@ -1,11 +1,13 @@ #Requires -RunAsAdministrator <# setup-windows-with-uac.ps1 - Elevated portion of BlockBox Windows provisioning. Invoked by setup-windows.bat + Elevated portion of the Windows provisioning. Invoked by setup-windows.bat via Start-Process -Verb RunAs, or run manually from an Administrator prompt. What this installs / configures: - ssh-agent set to automatic + started + - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22 + - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine PATH - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX, Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset) - Windows Driver Kit 10.0.26100 @@ -110,6 +112,114 @@ Write-Step 'Enabling ssh-agent' Set-Service -Name ssh-agent -StartupType Automatic if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent } +# --------------------------------------------------------------------------- +# OpenSSH Server (sshd) +# +# Used to reach the test VMs (VirtualBox) from the host: remote shell plus the +# transport rsync rides on when seeding test data in. Ships with Windows 10 +# 1809+ / Windows 11 as an on-demand capability, so no third-party install. +# +# The capability normally adds the "OpenSSH Server (sshd)" inbound firewall +# rule; we verify and create it if missing (it is absent on some images). +# +# Non-fatal: a box that can't run sshd should still finish provisioning. +# --------------------------------------------------------------------------- +Write-Step 'OpenSSH Server (sshd)' +try { + $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' | + Select-Object -First 1 + if (-not $sshd) { + Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.' + } else { + if ($sshd.State -ne 'Installed') { + Write-Host " Installing $($sshd.Name) ..." + $r = Add-WindowsCapability -Online -Name $sshd.Name + if ($r.RestartNeeded) { Write-Host ' [reboot required after OpenSSH Server]' -ForegroundColor Yellow } + } else { + Write-Host " OK: $($sshd.Name) already installed" + } + + Set-Service -Name sshd -StartupType Automatic + if ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd } + Write-Host ' sshd: Automatic + running' + + # Firewall: allow inbound 22 on all profiles. VirtualBox host-only and + # bridged adapters are frequently classified Public, and the capability's + # own rule is Private-only on some images, which is what leaves a plainly + # running sshd plainly unreachable. + # + # OpenSSH-Server-In-TCP is the name the capability itself uses, so this + # WIDENS that rule rather than adding a second one next to it. Creating + # our own under a different name would leave the narrow rule in place and + # the box still unreachable on a Public-classified adapter; creating one + # under the same name would collide. Adopt it if present, create it if not. + $ruleName = 'OpenSSH-Server-In-TCP' + if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) { + Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any + Write-Host " Widened firewall rule $ruleName to all profiles" + } else { + New-NetFirewallRule -Name $ruleName -DisplayName 'OpenSSH SSH Server (sshd)' ` + -Enabled True -Direction Inbound -Protocol TCP -Action Allow ` + -LocalPort 22 -Profile Any | Out-Null + Write-Host " Added firewall rule $ruleName (TCP 22, all profiles)" + } + } +} catch { + Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)" +} + +# --------------------------------------------------------------------------- +# rsync for Windows (github.com/nuket/rsync-windows) +# +# Windows' OpenSSH ships the transport only - no rsync - so pushing test data +# from a Linux box needs an rsync.exe on the Windows side. +# +# Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is +# invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes +# the client-side --rsync-path escape hatch painful to quote. Added to the +# MACHINE PATH so it resolves for every account, including the non-interactive +# sshd session, which builds its environment from the machine + user registry +# PATH rather than from a login shell. +# +# Non-fatal: a download failure only warns. +# --------------------------------------------------------------------------- +Write-Step 'rsync for Windows' +$RsyncUrl = 'https://github.com/nuket/rsync-windows/releases/download/v3.5.0-g521ad8ad/rsync.exe' +$RsyncDir = 'C:\Tools\rsync' +try { + New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null + $RsyncExe = Join-Path $RsyncDir 'rsync.exe' + # Download to a temp name first so an interrupted transfer can't leave a + # truncated rsync.exe sitting on the PATH. + $tmpExe = "$RsyncExe.download" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpExe -UseBasicParsing + Move-Item -Path $tmpExe -Destination $RsyncExe -Force + Write-Host " Downloaded rsync.exe to $RsyncExe" + + # Machine PATH (HKLM environment). Idempotent: only appends if absent. + $m = [Environment]::GetEnvironmentVariable('Path', 'Machine') + if (-not $m) { $m = '' } + if (($m -split ';') -notcontains $RsyncDir) { + $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $RsyncDir } else { $RsyncDir } + [Environment]::SetEnvironmentVariable('Path', $new, 'Machine') + Write-Host " Added $RsyncDir to the machine PATH (restart shells / sshd to pick it up)." + # sshd caches the environment it was started with, so an already-running + # service would not see the new PATH until restarted. + if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') { + Restart-Service sshd + Write-Host ' Restarted sshd so it inherits the updated machine PATH.' + } + } else { + Write-Host " OK: $RsyncDir already in the machine PATH" + } + + & $RsyncExe --version | Select-Object -First 1 +} catch { + Write-Warning "rsync install failed: $($_.Exception.Message)" + Write-Warning "Download manually from $RsyncUrl and drop it in $RsyncDir." +} + # --------------------------------------------------------------------------- # Visual Studio 2022 Community # --------------------------------------------------------------------------- diff --git a/setup-windows.bat b/setup-windows.bat index b94922d..ff77716 100644 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -1,7 +1,7 @@ @echo off @rem --------------------------------------------------------------------------- -@rem setup-windows.bat - provision a fresh Windows box for BlockBox development +@rem setup-windows.bat - provision a fresh Windows box for native development @rem --------------------------------------------------------------------------- @rem --- Non-admin (per-user) installs + git config --- @@ -13,7 +13,7 @@ winget install WinMerge.WinMerge winget install WiXToolset.WiXCLI @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 (BlockBox + the sandbox DLLs build with PDBs, +@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. winget install OpenCppCoverage.OpenCppCoverage @@ -28,9 +28,22 @@ powershell -NoProfile -Command "$c = @((Join-Path $env:ProgramFiles 'WinMerge'), @rem Microsoft.CodeAnalysis.BinSkim NuGet package ships a self-contained win-x64 @rem build, so this needs no .NET SDK/runtime: download the .nupkg (a zip), extract @rem the win-x64 tool folder to %LOCALAPPDATA%\Programs\BinSkim, and add it to the -@rem user PATH. After restarting the shell: binskim analyze path\to\BlockBox.exe +@rem user PATH. After restarting the shell: binskim analyze path\to\your.exe +@rem +@rem VERSION CHECK FIRST. The .nupkg is a large download (self-contained .NET), so +@rem we ask NuGet what the newest stable version is BEFORE fetching anything, and +@rem skip the download entirely when the installed copy already matches. The +@rem installed version is recorded in nupkg-version.txt next to the tool; for a +@rem copy installed before that marker existed we fall back to BinSkim.exe's own +@rem ProductVersion, which costs at most one more download and then self-heals. +@rem The flat-container URL pins the exact version we checked, unlike the v2 +@rem /package/ endpoint, which just redirects to whatever is newest at the +@rem moment of the request. +@rem +@rem The user PATH is refreshed on every run, including the skip path, so a lost +@rem PATH entry is repaired without re-downloading the tool to do it. @rem A failure here only warns (exit 0) so it never aborts the rest of provisioning. -powershell -NoProfile -Command "try { $ErrorActionPreference='Stop'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $dest=Join-Path $env:LOCALAPPDATA 'Programs\BinSkim'; $tmp=Join-Path $env:TEMP ('binskim_'+[guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $tmp | Out-Null; $zip=Join-Path $tmp 'binskim.zip'; Invoke-WebRequest -Uri 'https://www.nuget.org/api/v2/package/Microsoft.CodeAnalysis.BinSkim' -OutFile $zip; Expand-Archive -Path $zip -DestinationPath $tmp -Force; $exe=Get-ChildItem -Path $tmp -Recurse -Filter 'BinSkim.exe' | Where-Object { $_.FullName -match 'win-x64' } | Sort-Object FullName | Select-Object -Last 1; if (-not $exe) { throw 'BinSkim.exe (win-x64) not found in package.' }; if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }; New-Item -ItemType Directory -Force -Path $dest | Out-Null; Copy-Item -Path (Join-Path $exe.Directory.FullName '*') -Destination $dest -Recurse -Force; Remove-Item -Recurse -Force $tmp; $u=[Environment]::GetEnvironmentVariable('Path','User'); if (-not $u) { $u='' }; if (($u -split ';') -notcontains $dest) { $new = if ($u.Trim()) { $u.TrimEnd(';')+';'+$dest } else { $dest }; [Environment]::SetEnvironmentVariable('Path',$new,'User'); Write-Host ('Added '+$dest+' to user PATH (restart your shell to pick it up).') } else { Write-Host ($dest+' already in user PATH.') }; Write-Host ('BinSkim installed to '+$dest) } catch { Write-Warning ('BinSkim install failed: '+$_.Exception.Message); exit 0 }" +powershell -NoProfile -Command "try { $ErrorActionPreference='Stop'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $dest=Join-Path $env:LOCALAPPDATA 'Programs\BinSkim'; $exe=Join-Path $dest 'BinSkim.exe'; $mark=Join-Path $dest 'nupkg-version.txt'; $have=$null; if (Test-Path $exe) { if (Test-Path $mark) { $have=(Get-Content $mark -Raw).Trim() } else { $pv=(Get-Item $exe).VersionInfo.ProductVersion; if ($pv) { $have=$pv.Split('+')[0].Trim() } } }; $latest=$null; try { $idx=Invoke-RestMethod -Uri 'https://api.nuget.org/v3-flatcontainer/microsoft.codeanalysis.binskim/index.json' -UseBasicParsing; $latest=$idx.versions | Where-Object { $_ -notmatch '-' } | Select-Object -Last 1 } catch { Write-Warning ('BinSkim version check failed: '+$_.Exception.Message) }; if (-not $latest) { if ($have) { Write-Host ('BinSkim '+$have+' kept (could not reach NuGet to check for a newer one).') } else { Write-Warning 'BinSkim not installed and NuGet unreachable; skipping.'; exit 0 } } elseif ($have -and ($have -eq $latest -or $have -eq ($latest+'.0'))) { Write-Host ('BinSkim '+$have+' is already the newest stable release; skipping download.') } else { if ($have) { Write-Host ('BinSkim '+$have+' -> '+$latest+'; downloading.') } else { Write-Host ('BinSkim '+$latest+'; downloading.') }; $tmp=Join-Path $env:TEMP ('binskim_'+[guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $tmp | Out-Null; $zip=Join-Path $tmp 'binskim.zip'; Invoke-WebRequest -Uri ('https://api.nuget.org/v3-flatcontainer/microsoft.codeanalysis.binskim/'+$latest+'/microsoft.codeanalysis.binskim.'+$latest+'.nupkg') -OutFile $zip -UseBasicParsing; Expand-Archive -Path $zip -DestinationPath $tmp -Force; $src=Get-ChildItem -Path $tmp -Recurse -Filter 'BinSkim.exe' | Where-Object { $_.FullName -match 'win-x64' } | Sort-Object FullName | Select-Object -Last 1; if (-not $src) { throw 'BinSkim.exe (win-x64) not found in package.' }; if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }; New-Item -ItemType Directory -Force -Path $dest | Out-Null; Copy-Item -Path (Join-Path $src.Directory.FullName '*') -Destination $dest -Recurse -Force; Remove-Item -Recurse -Force $tmp; Set-Content -Path $mark -Value $latest -Encoding ascii; Write-Host ('BinSkim '+$latest+' installed to '+$dest) }; $u=[Environment]::GetEnvironmentVariable('Path','User'); if (-not $u) { $u='' }; if (($u -split ';') -notcontains $dest) { $new = if ($u.Trim()) { $u.TrimEnd(';')+';'+$dest } else { $dest }; [Environment]::SetEnvironmentVariable('Path',$new,'User'); Write-Host ('Added '+$dest+' to user PATH (restart your shell to pick it up).') } else { Write-Host ($dest+' already in user PATH.') } } catch { Write-Warning ('BinSkim install failed: '+$_.Exception.Message); exit 0 }" @rem --- Global git identity: EDIT THESE BEFORE RUNNING --- @rem Replace the placeholders with your own name and email, or comment the two