2 setup-windows-current.ps1 - is this step's program already current?
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:
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.
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.
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.
25 [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM')] [string] $Step,
26 # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86.
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'
35 function Say([string] $Msg) { Write-Host "[setup-windows] $Msg" }
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)
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() }
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 }
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.
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 }
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."
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."
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."
85 Say "ClaudeCode: $have is the latest release, $mach build; not reinstalling."
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 }
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."
104 if ($have -ne $latest) {
105 Say "NASM: $have installed per-user, $latest available; updating."
108 Say "NASM: $have (per-user, which winget cannot see) is the version winget offers; not reinstalling."
113 $current = switch ($Step) {
114 'ClaudeCode' { Test-ClaudeCodeCurrent }
115 'NASM' { Test-NasmCurrent }
118 Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual."
121 exit $(if ($current -eq $true) { 0 } else { 1 })