<# setup-windows-current.ps1 - is this step's program already current? setup-windows.bat asks this before the installs that would otherwise run in full on every re-run, because neither the installer nor winget can tell that what is on the box is already the latest: ClaudeCode Anthropic's installer script always downloads the release (a couple of hundred MB) and reinstalls it; it has no "already current" check of its own. NASM its installer falls back to a per-user directory when it cannot write to Program Files, and registers nothing there - so winget cannot see it, and reruns the installer every time. AndroidStudio an install that did not come from winget is one winget cannot match to its package, so it would install over it. And Android Studio updates itself; winget never upgrades it. Exit code 0 means already current: skip the install. Anything else means go ahead as before - and that includes this script failing (no network, a version string it does not recognise). A check that cannot decide must never be the reason something is left missing or out of date, so its fallback is the old behaviour, not a skip. Every other install in the setup was already safe to re-run; the README's "Re-running" note lists how each one decides. #> param( [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM', 'AndroidStudio')] [string] $Step, # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86. [string] $HostArch ) # Continue, not Stop: this runs native tools that may write to stderr, and every # decision below is made explicitly. Anything that does throw lands in the catch # at the bottom and becomes "go ahead and install". $ErrorActionPreference = 'Continue' function Say([string] $Msg) { Write-Host "[setup-windows] $Msg" } function Get-PEMachine([string] $Path) { # The machine type in the COFF header, as Get-PEMachine in # setup-windows-no-uac.ps1 reads it: what the file will actually run as. $fs = [IO.File]::OpenRead($Path) $br = New-Object IO.BinaryReader($fs) try { $fs.Seek(0x3c, 'Begin') | Out-Null $fs.Seek($br.ReadInt32() + 4, 'Begin') | Out-Null switch ($br.ReadUInt16()) { 0x8664 { 'x64' } 0xAA64 { 'ARM64' } 0x014c { 'x86' } default { 'unknown' } } } finally { $br.Close(); $fs.Close() } } function Get-Version([string] $Text) { # The first dotted version number in some output, or $null. $m = [regex]::Match("$Text", '\d+(\.\d+)+') if ($m.Success) { $m.Value } else { $null } } function Test-ClaudeCodeCurrent { # Current means: the native install's claude.exe is the latest release AND # the build for this host. The second half matters on ARM64, where an install # run from an emulated shell got the x64 build - the same version, but the # wrong binary, and worth replacing. # # The launcher in .local\bin is the full binary, not a shim, so its PE header # is the build that runs. A copy from the old winget route is deliberately # NOT consulted: the batch file reports that one separately. $exe = Join-Path $env:USERPROFILE '.local\bin\claude.exe' if (-not (Test-Path $exe)) { Say 'ClaudeCode: no native install yet.'; return $false } $have = Get-Version (& $exe --version 2>$null | Out-String) # The same endpoint the installer asks, so "latest" means the same thing. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $latest = Get-Version (Invoke-RestMethod -Uri 'https://downloads.claude.ai/claude-code-releases/latest' -ErrorAction Stop) if (-not $have -or -not $latest) { Say "ClaudeCode: could not compare versions (installed '$have', latest '$latest'); running the installer." return $false } $want = if ($HostArch -eq 'ARM64') { 'ARM64' } else { 'x64' } $mach = Get-PEMachine $exe if ($have -ne $latest) { Say "ClaudeCode: $have installed, $latest available; updating." return $false } if ($mach -ne $want) { Say "ClaudeCode: $have is current, but it is the $mach build on an $HostArch host; reinstalling for this host." return $false } Say "ClaudeCode: $have is the latest release, $mach build; not reinstalling." $true } function Test-NasmCurrent { # Only the per-user fallback install needs this. A machine-wide NASM has an # Add/Remove Programs entry, so winget sees it and upgrades it only when # there is something newer - that case is left to winget. $exe = Join-Path $env:LOCALAPPDATA 'bin\NASM\nasm.exe' if (-not (Test-Path $exe)) { return $false } $have = Get-Version (& $exe -v 2>$null | Out-String) $show = winget show --id NASM.NASM --exact --accept-source-agreements --disable-interactivity 2>$null | Out-String $m = [regex]::Match($show, '(?m)^\s*Version:\s*(\S+)') $latest = if ($m.Success) { $m.Groups[1].Value } else { $null } if (-not $have -or -not $latest) { Say "NASM: could not compare versions (installed '$have', winget '$latest'); leaving it to winget." return $false } if ($have -ne $latest) { Say "NASM: $have installed per-user, $latest available; updating." return $false } Say "NASM: $have (per-user, which winget cannot see) is the version winget offers; not reinstalling." $true } function Test-AndroidStudioCurrent { # Installed at all counts as current, because Android Studio keeps itself # current: it updates in the app (Help > Check for Updates), and the winget # package is published with UpgradeBehavior: deny and RequireExplicitUpgrade, # so winget would not upgrade it anyway. The check matters for an install # that did not come from winget - measured on the x64 laptop, where winget # lists Studio only as "ARP\Machine\X64\Android Studio", unmatched to # Google.AndroidStudio - since `winget install` would then run the installer # over it. Looked for both ways: the Add/Remove Programs entry its installer # writes, and studio64.exe where that installer puts it. $arp = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq 'Android Studio' } $exe = @((Join-Path $env:ProgramFiles 'Android\Android Studio\bin\studio64.exe'), (Join-Path $env:LOCALAPPDATA 'Programs\Android Studio\bin\studio64.exe')) | Where-Object { Test-Path $_ } if (-not $arp -and -not $exe) { Say 'AndroidStudio: not installed yet.'; return $false } $ver = @($arp)[0].DisplayVersion Say "AndroidStudio: $(if ($ver) { $ver } else { 'an existing install' }) is already here, and it updates itself (Help > Check for Updates); not reinstalling." $true } try { $current = switch ($Step) { 'ClaudeCode' { Test-ClaudeCodeCurrent } 'NASM' { Test-NasmCurrent } 'AndroidStudio' { Test-AndroidStudioCurrent } } } catch { Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual." $current = $false } exit $(if ($current -eq $true) { 0 } else { 1 })