]> vilimpoc.org git repositories - dotfiles/blob - setup-windows-current.ps1
dotfiles: register UI.Xaml 2.8 for the account before installing Terminal
[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 installs that would otherwise run in
5   full on every re-run, because neither the installer nor winget can tell that
6   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
10                      "already current" check of its own.
11       NASM           its installer falls back to a per-user directory when it
12                      cannot write to Program Files, and registers nothing there -
13                      so winget cannot see it, and reruns the installer every time.
14       AndroidStudio  an install that did not come from winget is one winget
15                      cannot match to its package, so it would install over it.
16                      And Android Studio updates itself; winget never upgrades it.
17
18   Exit code 0 means already current: skip the install. Anything else means go
19   ahead as before - and that includes this script failing (no network, a version
20   string it does not recognise). A check that cannot decide must never be the
21   reason something is left missing or out of date, so its fallback is the old
22   behaviour, not a skip.
23
24   Every other install in the setup was already safe to re-run; the README's
25   "Re-running" note lists how each one decides.
26 #>
27 param(
28     [Parameter(Mandatory)] [ValidateSet('ClaudeCode', 'NASM', 'AndroidStudio')] [string] $Step,
29     # As setup-windows.bat worked it out, from the registry: AMD64, ARM64 or x86.
30     [string] $HostArch
31 )
32
33 # Continue, not Stop: this runs native tools that may write to stderr, and every
34 # decision below is made explicitly. Anything that does throw lands in the catch
35 # at the bottom and becomes "go ahead and install".
36 $ErrorActionPreference = 'Continue'
37
38 function Say([string] $Msg) { Write-Host "[setup-windows] $Msg" }
39
40 function Get-PEMachine([string] $Path) {
41     # The machine type in the COFF header, as Get-PEMachine in
42     # setup-windows-no-uac.ps1 reads it: what the file will actually run as.
43     $fs = [IO.File]::OpenRead($Path)
44     $br = New-Object IO.BinaryReader($fs)
45     try {
46         $fs.Seek(0x3c, 'Begin') | Out-Null
47         $fs.Seek($br.ReadInt32() + 4, 'Begin') | Out-Null
48         switch ($br.ReadUInt16()) { 0x8664 { 'x64' } 0xAA64 { 'ARM64' } 0x014c { 'x86' } default { 'unknown' } }
49     } finally { $br.Close(); $fs.Close() }
50 }
51
52 function Get-Version([string] $Text) {
53     # The first dotted version number in some output, or $null.
54     $m = [regex]::Match("$Text", '\d+(\.\d+)+')
55     if ($m.Success) { $m.Value } else { $null }
56 }
57
58 function Test-ClaudeCodeCurrent {
59     # Current means: the native install's claude.exe is the latest release AND
60     # the build for this host. The second half matters on ARM64, where an install
61     # run from an emulated shell got the x64 build - the same version, but the
62     # wrong binary, and worth replacing.
63     #
64     # The launcher in .local\bin is the full binary, not a shim, so its PE header
65     # is the build that runs. A copy from the old winget route is deliberately
66     # NOT consulted: the batch file reports that one separately.
67     $exe = Join-Path $env:USERPROFILE '.local\bin\claude.exe'
68     if (-not (Test-Path $exe)) { Say 'ClaudeCode: no native install yet.'; return $false }
69
70     $have = Get-Version (& $exe --version 2>$null | Out-String)
71     # The same endpoint the installer asks, so "latest" means the same thing.
72     [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
73     $latest = Get-Version (Invoke-RestMethod -Uri 'https://downloads.claude.ai/claude-code-releases/latest' -ErrorAction Stop)
74     if (-not $have -or -not $latest) {
75         Say "ClaudeCode: could not compare versions (installed '$have', latest '$latest'); running the installer."
76         return $false
77     }
78     $want = if ($HostArch -eq 'ARM64') { 'ARM64' } else { 'x64' }
79     $mach = Get-PEMachine $exe
80     if ($have -ne $latest) {
81         Say "ClaudeCode: $have installed, $latest available; updating."
82         return $false
83     }
84     if ($mach -ne $want) {
85         Say "ClaudeCode: $have is current, but it is the $mach build on an $HostArch host; reinstalling for this host."
86         return $false
87     }
88     Say "ClaudeCode: $have is the latest release, $mach build; not reinstalling."
89     $true
90 }
91
92 function Test-NasmCurrent {
93     # Only the per-user fallback install needs this. A machine-wide NASM has an
94     # Add/Remove Programs entry, so winget sees it and upgrades it only when
95     # there is something newer - that case is left to winget.
96     $exe = Join-Path $env:LOCALAPPDATA 'bin\NASM\nasm.exe'
97     if (-not (Test-Path $exe)) { return $false }
98
99     $have = Get-Version (& $exe -v 2>$null | Out-String)
100     $show = winget show --id NASM.NASM --exact --accept-source-agreements --disable-interactivity 2>$null | Out-String
101     $m = [regex]::Match($show, '(?m)^\s*Version:\s*(\S+)')
102     $latest = if ($m.Success) { $m.Groups[1].Value } else { $null }
103     if (-not $have -or -not $latest) {
104         Say "NASM: could not compare versions (installed '$have', winget '$latest'); leaving it to winget."
105         return $false
106     }
107     if ($have -ne $latest) {
108         Say "NASM: $have installed per-user, $latest available; updating."
109         return $false
110     }
111     Say "NASM: $have (per-user, which winget cannot see) is the version winget offers; not reinstalling."
112     $true
113 }
114
115 function Test-AndroidStudioCurrent {
116     # Installed at all counts as current, because Android Studio keeps itself
117     # current: it updates in the app (Help > Check for Updates), and the winget
118     # package is published with UpgradeBehavior: deny and RequireExplicitUpgrade,
119     # so winget would not upgrade it anyway. The check matters for an install
120     # that did not come from winget - measured on the x64 laptop, where winget
121     # lists Studio only as "ARP\Machine\X64\Android Studio", unmatched to
122     # Google.AndroidStudio - since `winget install` would then run the installer
123     # over it. Looked for both ways: the Add/Remove Programs entry its installer
124     # writes, and studio64.exe where that installer puts it.
125     $arp = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
126                             'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
127                             'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue |
128            Where-Object { $_.DisplayName -eq 'Android Studio' }
129     $exe = @((Join-Path $env:ProgramFiles 'Android\Android Studio\bin\studio64.exe'),
130              (Join-Path $env:LOCALAPPDATA 'Programs\Android Studio\bin\studio64.exe')) | Where-Object { Test-Path $_ }
131     if (-not $arp -and -not $exe) { Say 'AndroidStudio: not installed yet.'; return $false }
132     $ver = @($arp)[0].DisplayVersion
133     Say "AndroidStudio: $(if ($ver) { $ver } else { 'an existing install' }) is already here, and it updates itself (Help > Check for Updates); not reinstalling."
134     $true
135 }
136
137 try {
138     $current = switch ($Step) {
139         'ClaudeCode'    { Test-ClaudeCodeCurrent }
140         'NASM'          { Test-NasmCurrent }
141         'AndroidStudio' { Test-AndroidStudioCurrent }
142     }
143 } catch {
144     Say "${Step}: the version check failed ($($_.Exception.Message)); installing as usual."
145     $current = $false
146 }
147 exit $(if ($current -eq $true) { 0 } else { 1 })