]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: sync the Windows provisioning scripts
authorMax Vilimpoc <max@vilimpoc.org>
Tue, 25 Aug 2026 10:45:40 +0000 (12:45 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Tue, 25 Aug 2026 10:45:40 +0000 (12:45 +0200)
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/<id>
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 <noreply@anthropic.com>
README.md
setup-windows-with-uac.ps1
setup-windows.bat

index 5fd887847a29875a71a1baba5edb5838d9735af8..e1e0c87876e378b26c24ccf192cfce14014d6b6f 100644 (file)
--- 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,
 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. |
 
 ## 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
 
 
 ## 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.
   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
 - 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
index 254db14e59deeb03a19e68a3dcbca0bae28fa4ae..8da5c5517613530e0f244c10da6253f1408b4271 100644 (file)
@@ -1,11 +1,13 @@
 #Requires -RunAsAdministrator\r
 <#\r
   setup-windows-with-uac.ps1\r
 #Requires -RunAsAdministrator\r
 <#\r
   setup-windows-with-uac.ps1\r
-  Elevated portion of BlockBox Windows provisioning. Invoked by setup-windows.bat\r
+  Elevated portion of the Windows provisioning. Invoked by setup-windows.bat\r
   via Start-Process -Verb RunAs, or run manually from an Administrator prompt.\r
 \r
   What this installs / configures:\r
     - ssh-agent set to automatic + started\r
   via Start-Process -Verb RunAs, or run manually from an Administrator prompt.\r
 \r
   What this installs / configures:\r
     - ssh-agent set to automatic + started\r
+    - OpenSSH Server (sshd) capability: automatic + started + inbound TCP 22\r
+    - rsync for Windows (nuket/rsync-windows) in C:\Tools\rsync, on the machine PATH\r
     - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX,\r
       Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset)\r
     - Windows Driver Kit 10.0.26100\r
     - Visual Studio 2022 Community (C++ desktop workload, Spectre libs, WDK VSIX,\r
       Win11 SDK 26100, Clang/LLVM, and the v141 + Windows XP targeting toolset)\r
     - Windows Driver Kit 10.0.26100\r
@@ -110,6 +112,114 @@ Write-Step 'Enabling ssh-agent'
 Set-Service -Name ssh-agent -StartupType Automatic\r
 if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent }\r
 \r
 Set-Service -Name ssh-agent -StartupType Automatic\r
 if ((Get-Service ssh-agent).Status -ne 'Running') { Start-Service ssh-agent }\r
 \r
+# ---------------------------------------------------------------------------\r
+# OpenSSH Server (sshd)\r
+#\r
+# Used to reach the test VMs (VirtualBox) from the host: remote shell plus the\r
+# transport rsync rides on when seeding test data in. Ships with Windows 10\r
+# 1809+ / Windows 11 as an on-demand capability, so no third-party install.\r
+#\r
+# The capability normally adds the "OpenSSH Server (sshd)" inbound firewall\r
+# rule; we verify and create it if missing (it is absent on some images).\r
+#\r
+# Non-fatal: a box that can't run sshd should still finish provisioning.\r
+# ---------------------------------------------------------------------------\r
+Write-Step 'OpenSSH Server (sshd)'\r
+try {\r
+    $sshd = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' |\r
+            Select-Object -First 1\r
+    if (-not $sshd) {\r
+        Write-Warning 'OpenSSH.Server capability not offered by this Windows image - skipping.'\r
+    } else {\r
+        if ($sshd.State -ne 'Installed') {\r
+            Write-Host "    Installing $($sshd.Name) ..."\r
+            $r = Add-WindowsCapability -Online -Name $sshd.Name\r
+            if ($r.RestartNeeded) { Write-Host '    [reboot required after OpenSSH Server]' -ForegroundColor Yellow }\r
+        } else {\r
+            Write-Host "    OK: $($sshd.Name) already installed"\r
+        }\r
+\r
+        Set-Service -Name sshd -StartupType Automatic\r
+        if ((Get-Service sshd).Status -ne 'Running') { Start-Service sshd }\r
+        Write-Host '    sshd: Automatic + running'\r
+\r
+        # Firewall: allow inbound 22 on all profiles. VirtualBox host-only and\r
+        # bridged adapters are frequently classified Public, and the capability's\r
+        # own rule is Private-only on some images, which is what leaves a plainly\r
+        # running sshd plainly unreachable.\r
+        #\r
+        # OpenSSH-Server-In-TCP is the name the capability itself uses, so this\r
+        # WIDENS that rule rather than adding a second one next to it. Creating\r
+        # our own under a different name would leave the narrow rule in place and\r
+        # the box still unreachable on a Public-classified adapter; creating one\r
+        # under the same name would collide. Adopt it if present, create it if not.\r
+        $ruleName = 'OpenSSH-Server-In-TCP'\r
+        if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) {\r
+            Set-NetFirewallRule -Name $ruleName -Enabled True -Profile Any\r
+            Write-Host "    Widened firewall rule $ruleName to all profiles"\r
+        } else {\r
+            New-NetFirewallRule -Name $ruleName -DisplayName 'OpenSSH SSH Server (sshd)' `\r
+                -Enabled True -Direction Inbound -Protocol TCP -Action Allow `\r
+                -LocalPort 22 -Profile Any | Out-Null\r
+            Write-Host "    Added firewall rule $ruleName (TCP 22, all profiles)"\r
+        }\r
+    }\r
+} catch {\r
+    Write-Warning "OpenSSH Server setup failed: $($_.Exception.Message)"\r
+}\r
+\r
+# ---------------------------------------------------------------------------\r
+# rsync for Windows (github.com/nuket/rsync-windows)\r
+#\r
+# Windows' OpenSSH ships the transport only - no rsync - so pushing test data\r
+# from a Linux box needs an rsync.exe on the Windows side.\r
+#\r
+# Installed to C:\Tools\rsync (NOT under "Program Files"): the remote end is\r
+# invoked as `rsync --server ...` through cmd.exe, and a path with spaces makes\r
+# the client-side --rsync-path escape hatch painful to quote. Added to the\r
+# MACHINE PATH so it resolves for every account, including the non-interactive\r
+# sshd session, which builds its environment from the machine + user registry\r
+# PATH rather than from a login shell.\r
+#\r
+# Non-fatal: a download failure only warns.\r
+# ---------------------------------------------------------------------------\r
+Write-Step 'rsync for Windows'\r
+$RsyncUrl = 'https://github.com/nuket/rsync-windows/releases/download/v3.5.0-g521ad8ad/rsync.exe'\r
+$RsyncDir = 'C:\Tools\rsync'\r
+try {\r
+    New-Item -ItemType Directory -Force -Path $RsyncDir | Out-Null\r
+    $RsyncExe = Join-Path $RsyncDir 'rsync.exe'\r
+    # Download to a temp name first so an interrupted transfer can't leave a\r
+    # truncated rsync.exe sitting on the PATH.\r
+    $tmpExe = "$RsyncExe.download"\r
+    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r
+    Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpExe -UseBasicParsing\r
+    Move-Item -Path $tmpExe -Destination $RsyncExe -Force\r
+    Write-Host "    Downloaded rsync.exe to $RsyncExe"\r
+\r
+    # Machine PATH (HKLM environment). Idempotent: only appends if absent.\r
+    $m = [Environment]::GetEnvironmentVariable('Path', 'Machine')\r
+    if (-not $m) { $m = '' }\r
+    if (($m -split ';') -notcontains $RsyncDir) {\r
+        $new = if ($m.Trim()) { $m.TrimEnd(';') + ';' + $RsyncDir } else { $RsyncDir }\r
+        [Environment]::SetEnvironmentVariable('Path', $new, 'Machine')\r
+        Write-Host "    Added $RsyncDir to the machine PATH (restart shells / sshd to pick it up)."\r
+        # sshd caches the environment it was started with, so an already-running\r
+        # service would not see the new PATH until restarted.\r
+        if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') {\r
+            Restart-Service sshd\r
+            Write-Host '    Restarted sshd so it inherits the updated machine PATH.'\r
+        }\r
+    } else {\r
+        Write-Host "    OK: $RsyncDir already in the machine PATH"\r
+    }\r
+\r
+    & $RsyncExe --version | Select-Object -First 1\r
+} catch {\r
+    Write-Warning "rsync install failed: $($_.Exception.Message)"\r
+    Write-Warning "Download manually from $RsyncUrl and drop it in $RsyncDir."\r
+}\r
+\r
 # ---------------------------------------------------------------------------\r
 # Visual Studio 2022 Community\r
 # ---------------------------------------------------------------------------\r
 # ---------------------------------------------------------------------------\r
 # Visual Studio 2022 Community\r
 # ---------------------------------------------------------------------------\r
index b94922d37cd23ba8bbf053bf95d13e497602e8a1..ff77716cb46ac018e404491a95be3ea7418114f1 100644 (file)
@@ -1,7 +1,7 @@
 @echo off\r
 \r
 @rem ---------------------------------------------------------------------------\r
 @echo off\r
 \r
 @rem ---------------------------------------------------------------------------\r
-@rem setup-windows.bat - provision a fresh Windows box for BlockBox development\r
+@rem setup-windows.bat - provision a fresh Windows box for native development\r
 @rem ---------------------------------------------------------------------------\r
 \r
 @rem --- Non-admin (per-user) installs + git config ---\r
 @rem ---------------------------------------------------------------------------\r
 \r
 @rem --- Non-admin (per-user) installs + git config ---\r
@@ -13,7 +13,7 @@ winget install WinMerge.WinMerge
 winget install WiXToolset.WiXCLI\r
 \r
 @rem OpenCppCoverage: native (PE) line coverage for the C++ binaries. run-coverage-occ.py drives the\r
 winget install WiXToolset.WiXCLI\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 (BlockBox + the sandbox DLLs build with PDBs,\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
 winget install OpenCppCoverage.OpenCppCoverage\r
 \r
 @rem which it reads). The installer elevates via UAC.\r
 winget install OpenCppCoverage.OpenCppCoverage\r
 \r
@@ -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\r
 @rem build, so this needs no .NET SDK/runtime: download the .nupkg (a zip), extract\r
 @rem the win-x64 tool folder to %LOCALAPPDATA%\Programs\BinSkim, and add it to the\r
 @rem Microsoft.CodeAnalysis.BinSkim NuGet package ships a self-contained win-x64\r
 @rem build, so this needs no .NET SDK/runtime: download the .nupkg (a zip), extract\r
 @rem the win-x64 tool folder to %LOCALAPPDATA%\Programs\BinSkim, and add it to the\r
-@rem user PATH. After restarting the shell:  binskim analyze path\to\BlockBox.exe\r
+@rem user PATH. After restarting the shell:  binskim analyze path\to\your.exe\r
+@rem\r
+@rem VERSION CHECK FIRST. The .nupkg is a large download (self-contained .NET), so\r
+@rem we ask NuGet what the newest stable version is BEFORE fetching anything, and\r
+@rem skip the download entirely when the installed copy already matches. The\r
+@rem installed version is recorded in nupkg-version.txt next to the tool; for a\r
+@rem copy installed before that marker existed we fall back to BinSkim.exe's own\r
+@rem ProductVersion, which costs at most one more download and then self-heals.\r
+@rem The flat-container URL pins the exact version we checked, unlike the v2\r
+@rem /package/<id> endpoint, which just redirects to whatever is newest at the\r
+@rem moment of the request.\r
+@rem\r
+@rem The user PATH is refreshed on every run, including the skip path, so a lost\r
+@rem PATH entry is repaired without re-downloading the tool to do it.\r
 @rem A failure here only warns (exit 0) so it never aborts the rest of provisioning.\r
 @rem A failure here only warns (exit 0) so it never aborts the rest of provisioning.\r
-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 }"\r
+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 }"\r
 \r
 @rem --- Global git identity: EDIT THESE BEFORE RUNNING ---\r
 @rem Replace the placeholders with your own name and email, or comment the two\r
 \r
 @rem --- Global git identity: EDIT THESE BEFORE RUNNING ---\r
 @rem Replace the placeholders with your own name and email, or comment the two\r