]> vilimpoc.org git repositories - dotfiles/blob - setup-windows-current.ps1
dotfiles: show the plan, and let it be chosen, before anything runs
[dotfiles] / setup-windows-current.ps1
1 <#
2   setup-windows-current.ps1 - is this step's program already current?
3
4   setup-windows.bat asks this before the two installs that would otherwise run
5   in full on every re-run, because neither the installer nor winget can tell
6   that what is on the box is already the latest:
7
8       ClaudeCode   Anthropic's installer script always downloads the release
9                    (a couple of hundred MB) and reinstalls it; it has no "already
10                    current" check of its own.
11       NASM         its installer falls back to a per-user directory when it cannot
12                    write to Program Files, and registers nothing there - so winget
13                    cannot see it, and reruns the installer every time.
14
15   Exit code 0 means already current: skip the install. Anything else means go
16   ahead as before - and that includes this script failing (no network, a version
17   string it does not recognise). A check that cannot decide must never be the
18   reason something is left missing or out of date, so its fallback is the old
19   behaviour, not a skip.
20
21   Every other install in the setup was already safe to re-run; the README's
22   "Re-running" note lists how each one decides.
23 #>
24 param(
25     [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM')] [string] $Step,
26     # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86.
27     [string] $HostArch
28 )
29
30 # Continue, not Stop: this runs native tools that may write to stderr, and every
31 # decision below is made explicitly. Anything that does throw lands in the catch
32 # at the bottom and becomes "go ahead and install".
33 $ErrorActionPreference = 'Continue'
34
35 function Say([string] $Msg) { Write-Host "[setup-windows] $Msg" }
36
37 function Get-PEMachine([string] $Path) {
38     # The machine type in the COFF header, as Get-PEMachine in
39     # setup-windows-no-uac.ps1 reads it: what the file will actually run as.
40     $fs = [IO.File]::OpenRead($Path)
41     $br = New-Object IO.BinaryReader($fs)
42     try {
43         $fs.Seek(0x3c, 'Begin') | Out-Null
44         $fs.Seek($br.ReadInt32() + 4, 'Begin') | Out-Null
45         switch ($br.ReadUInt16()) { 0x8664 { 'x64' } 0xAA64 { 'ARM64' } 0x014c { 'x86' } default { 'unknown' } }
46     } finally { $br.Close(); $fs.Close() }
47 }
48
49 function Get-Version([string] $Text) {
50     # The first dotted version number in some output, or $null.
51     $m = [regex]::Match("$Text", '\d+(\.\d+)+')
52     if ($m.Success) { $m.Value } else { $null }
53 }
54
55 function Test-ClaudeCodeCurrent {
56     # Current means: the native install's claude.exe is the latest release AND
57     # the build for this host. The second half matters on ARM64, where an install
58     # run from an emulated shell got the x64 build - the same version, but the
59     # wrong binary, and worth replacing.
60     #
61     # The launcher in .local\bin is the full binary, not a shim, so its PE header
62     # is the build that runs. A copy from the old winget route is deliberately
63     # NOT consulted: the batch file reports that one separately.
64     $exe = Join-Path $env:USERPROFILE '.local\bin\claude.exe'
65     if (-not (Test-Path $exe)) { Say 'ClaudeCode: no native install yet.'; return $false }
66
67     $have = Get-Version (& $exe --version 2>$null | Out-String)
68     # The same endpoint the installer asks, so "latest" means the same thing.
69     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
70     $latest = Get-Version (Invoke-RestMethod -Uri 'https://downloads.claude.ai/claude-code-releases/latest' -ErrorAction Stop)
71     if (-not $have -or -not $latest) {
72         Say "ClaudeCode: could not compare versions (installed '$have', latest '$latest'); running the installer."
73         return $false
74     }
75     $want = if ($HostArch -eq 'ARM64') { 'ARM64' } else { 'x64' }
76     $mach = Get-PEMachine $exe
77     if ($have -ne $latest) {
78         Say "ClaudeCode: $have installed, $latest available; updating."
79         return $false
80     }
81     if ($mach -ne $want) {
82         Say "ClaudeCode: $have is current, but it is the $mach build on an $HostArch host; reinstalling for this host."
83         return $false
84     }
85     Say "ClaudeCode: $have is the latest release, $mach build; not reinstalling."
86     $true
87 }
88
89 function Test-NasmCurrent {
90     # Only the per-user fallback install needs this. A machine-wide NASM has an
91     # Add/Remove Programs entry, so winget sees it and upgrades it only when
92     # there is something newer - that case is left to winget.
93     $exe = Join-Path $env:LOCALAPPDATA 'bin\NASM\nasm.exe'
94     if (-not (Test-Path $exe)) { return $false }
95
96     $have = Get-Version (& $exe -v 2>$null | Out-String)
97     $show = winget show --id NASM.NASM --exact --accept-source-agreements --disable-interactivity 2>$null | Out-String
98     $m = [regex]::Match($show, '(?m)^\s*Version:\s*(\S+)')
99     $latest = if ($m.Success) { $m.Groups[1].Value } else { $null }
100     if (-not $have -or -not $latest) {
101         Say "NASM: could not compare versions (installed '$have', winget '$latest'); leaving it to winget."
102         return $false
103     }
104     if ($have -ne $latest) {
105         Say "NASM: $have installed per-user, $latest available; updating."
106         return $false
107     }
108     Say "NASM: $have (per-user, which winget cannot see) is the version winget offers; not reinstalling."
109     $true
110 }
111
112 try {
113     $current = switch ($Step) {
114         'ClaudeCode' { Test-ClaudeCodeCurrent }
115         'NASM'       { Test-NasmCurrent }
116     }
117 } catch {
118     Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual."
119     $current = $false
120 }
121 exit $(if ($current -eq $true) { 0 } else { 1 })