|
| 1 | +# Llvm-Completion.psm1 - PowerShell completions for LLVMUP |
| 2 | +# Registers argument completers for key commands (Activate-Llvm, Install-Llvm, etc.) |
| 3 | + |
| 4 | +# NOTE: do not import Get-UserHome helper at module import time. |
| 5 | +# We will lazily load it inside Resolve-UserHome only if no session-defined Get-UserHome exists. |
| 6 | + |
| 7 | +# Resolve user home, preferring a session/script-defined Get-UserHome (so tests can mock it) |
| 8 | +function Resolve-UserHome { |
| 9 | + # If a Get-UserHome function exists, try calling it directly — this respects any session/test |
| 10 | + # overrides (the PS engine will resolve the visible function at call time). |
| 11 | + try { |
| 12 | + $cmds = Get-Command -Name Get-UserHome -CommandType Function -ErrorAction SilentlyContinue -All |
| 13 | + if ($cmds -and $cmds.Count -gt 0) { |
| 14 | + $selected = $cmds[-1] |
| 15 | + try { |
| 16 | + if ($selected.ScriptBlock) { $val = & $selected.ScriptBlock } else { $val = & $selected.Name } |
| 17 | + return $val |
| 18 | + } catch {} |
| 19 | + } else { |
| 20 | + # No session-defined Get-UserHome; attempt to lazy-load our helper module/script |
| 21 | + try { |
| 22 | + $helperPath = Join-Path $PSScriptRoot 'Get-UserHome.psm1' |
| 23 | + if (Test-Path $helperPath) { Import-Module $helperPath -Force -ErrorAction SilentlyContinue } |
| 24 | + elseif (Test-Path (Join-Path $PSScriptRoot 'Get-UserHome.ps1')) { . "$PSScriptRoot\Get-UserHome.ps1" } |
| 25 | + # After lazy load, try again to get the function |
| 26 | + $cmds = Get-Command -Name Get-UserHome -CommandType Function -ErrorAction SilentlyContinue -All |
| 27 | + if ($cmds -and $cmds.Count -gt 0) { |
| 28 | + $selected = $cmds[-1] |
| 29 | + if ($selected.ScriptBlock) { $val = & $selected.ScriptBlock } else { $val = & $selected.Name } |
| 30 | + return $val |
| 31 | + } |
| 32 | + } catch {} |
| 33 | + } |
| 34 | + } catch {} |
| 35 | + |
| 36 | + # Fallback to environment variables |
| 37 | + if ($env:HOME) { return $env:HOME } |
| 38 | + if ($env:USERPROFILE) { return $env:USERPROFILE } |
| 39 | + if ($IsWindows) { return $env:SystemDrive + '\' } |
| 40 | + return '/tmp' |
| 41 | +} |
| 42 | + |
| 43 | +# Compatibility shim for environments that lack built-in ArgumentCompleter cmdlets (older hosts) |
| 44 | +if (-not (Get-Command Get-ArgumentCompleter -ErrorAction SilentlyContinue)) { |
| 45 | + # Store registrations in module-scoped variable |
| 46 | + if (-not (Get-Variable -Name '__llvm_completers' -Scope Script -ErrorAction SilentlyContinue)) { |
| 47 | + Set-Variable -Name '__llvm_completers' -Value @() -Scope Script |
| 48 | + } |
| 49 | + |
| 50 | + function Register-ArgumentCompleter { |
| 51 | + param( |
| 52 | + [Parameter(Mandatory=$true)][string]$CommandName, |
| 53 | + [Parameter(Mandatory=$false)][string]$ParameterName, |
| 54 | + [Parameter(Mandatory=$true)][scriptblock]$ScriptBlock |
| 55 | + ) |
| 56 | + $entry = [PSCustomObject]@{ |
| 57 | + CommandName = $CommandName |
| 58 | + ParameterName = $ParameterName |
| 59 | + ScriptBlock = $ScriptBlock |
| 60 | + } |
| 61 | + $script:__llvm_completers += $entry |
| 62 | + } |
| 63 | + |
| 64 | + function Get-ArgumentCompleter { |
| 65 | + param( |
| 66 | + [Parameter(Mandatory=$false)][string]$CommandName |
| 67 | + ) |
| 68 | + if ($CommandName) { |
| 69 | + return $script:__llvm_completers | Where-Object { $_.CommandName -eq $CommandName } |
| 70 | + } |
| 71 | + return $script:__llvm_completers |
| 72 | + } |
| 73 | + |
| 74 | + function Unregister-ArgumentCompleter { |
| 75 | + param( |
| 76 | + [Parameter(Mandatory=$true)][string]$CommandName, |
| 77 | + [Parameter(Mandatory=$false)][string]$ParameterName |
| 78 | + ) |
| 79 | + if ($ParameterName) { |
| 80 | + $script:__llvm_completers = $script:__llvm_completers | Where-Object { !($_.CommandName -eq $CommandName -and $_.ParameterName -eq $ParameterName) } |
| 81 | + } else { |
| 82 | + $script:__llvm_completers = $script:__llvm_completers | Where-Object { $_.CommandName -ne $CommandName } |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +# Export shims so tests can call Get-ArgumentCompleter when running in older hosts |
| 88 | +try { |
| 89 | + Export-ModuleMember -Function Get-ArgumentCompleter, Register-ArgumentCompleter, Unregister-ArgumentCompleter -ErrorAction SilentlyContinue |
| 90 | +} catch {} |
| 91 | + |
| 92 | +function Get-LlvmLocalVersions { |
| 93 | + param() |
| 94 | + # Resolve user home, preferring script/global-defined Get-UserHome (so tests can mock it) |
| 95 | + $home = Resolve-UserHome |
| 96 | + if (-not $home) { return @() } |
| 97 | + # Possible locations: |
| 98 | + # 1) $home/.llvm/toolchains (standard) |
| 99 | + # 2) $home (tests may mock Get-UserHome to point directly to a toolchains dir) |
| 100 | + $candidates = @( |
| 101 | + (Join-Path (Join-Path $home '.llvm') 'toolchains'), |
| 102 | + (Join-Path $home '*'), |
| 103 | + $home |
| 104 | + ) |
| 105 | + |
| 106 | + $found = @() |
| 107 | + foreach ($dir in $candidates) { |
| 108 | + try { |
| 109 | + if (-not $dir) { continue } |
| 110 | + if ($dir -like '*/*' -or $dir -like '*\\*') { |
| 111 | + # If dir contains glob, use Get-ChildItem with that path |
| 112 | + $items = Get-ChildItem -Directory -Path $dir -ErrorAction SilentlyContinue |
| 113 | + } else { |
| 114 | + if (-not (Test-Path $dir)) { continue } |
| 115 | + $items = Get-ChildItem -Directory -Path $dir -ErrorAction SilentlyContinue |
| 116 | + } |
| 117 | + foreach ($it in $items) { |
| 118 | + if ($it -and $it.Name -match '^llvmorg-') { $found += $it.Name } |
| 119 | + } |
| 120 | + } catch { continue } |
| 121 | + } |
| 122 | + |
| 123 | + if ($found) { return ($found | Select-Object -Unique) } |
| 124 | + # Fallback: scan common temp roots for test-created toolchains (helps unit tests that create temp dirs) |
| 125 | + $probeRoots = @() |
| 126 | + if ($env:TEMP) { $probeRoots += $env:TEMP } |
| 127 | + if ($env:TMPDIR) { $probeRoots += $env:TMPDIR } |
| 128 | + $probeRoots += '/tmp' |
| 129 | + $probeRoots = $probeRoots | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique |
| 130 | + foreach ($root in $probeRoots) { |
| 131 | + try { |
| 132 | + Write-Verbose "Get-LlvmLocalVersions: scanning root $root for llvmorg-* directories" |
| 133 | + $children = Get-ChildItem -Directory -Path $root -ErrorAction SilentlyContinue |
| 134 | + foreach ($c in $children) { |
| 135 | + if ($c.Name -match '^llvmorg-') { $found += $c.Name } |
| 136 | + else { |
| 137 | + # Also check one level deeper for directories containing llvmorg-* children |
| 138 | + $grand = Get-ChildItem -Directory -Path $c.FullName -ErrorAction SilentlyContinue |
| 139 | + foreach ($g in $grand) { if ($g.Name -match '^llvmorg-') { $found += $g.Name } } |
| 140 | + } |
| 141 | + } |
| 142 | + } catch { } |
| 143 | + if ($found) { break } |
| 144 | + } |
| 145 | + |
| 146 | + if ($found) { return ($found | Select-Object -Unique) } |
| 147 | + return @() |
| 148 | +} |
| 149 | + |
| 150 | +function Get-LlvmRemoteVersions { |
| 151 | + param() |
| 152 | + # Basic remote fetch with short timeout and fallback list |
| 153 | + # Resolve user home, preferring script/global-defined Get-UserHome (so tests can mock it) |
| 154 | + $home = Resolve-UserHome |
| 155 | + if (-not $home) { $home = if ($IsWindows) { $env:SystemDrive + '\' } else { '/tmp' } } |
| 156 | + $preferredCacheDir = Join-Path (Join-Path $home '.cache') 'llvmup' |
| 157 | + $cacheDir = $preferredCacheDir |
| 158 | + $cacheFile = Join-Path $cacheDir 'remote_versions.cache' |
| 159 | + |
| 160 | + # Try to create preferred cache directory; on failure, fallback to system temp |
| 161 | + if (-not (Test-Path $cacheDir)) { |
| 162 | + try { |
| 163 | + New-Item -ItemType Directory -Path $cacheDir -Force -ErrorAction Stop | Out-Null |
| 164 | + } catch { |
| 165 | + $tmp = [IO.Path]::GetTempPath() |
| 166 | + $cacheDir = Join-Path $tmp 'llvmup' |
| 167 | + $cacheFile = Join-Path $cacheDir 'remote_versions.cache' |
| 168 | + try { New-Item -ItemType Directory -Path $cacheDir -Force -ErrorAction SilentlyContinue | Out-Null } catch {} |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + $maxAgeSec = 24 * 3600 |
| 173 | + if (Test-Path $cacheFile) { |
| 174 | + try { |
| 175 | + $age = (Get-Date).ToUniversalTime() - (Get-Item $cacheFile).LastWriteTimeUtc |
| 176 | + if ($age.TotalSeconds -lt $maxAgeSec) { |
| 177 | + return Get-Content -Path $cacheFile -ErrorAction SilentlyContinue |
| 178 | + } |
| 179 | + } catch {} |
| 180 | + } |
| 181 | + |
| 182 | + try { |
| 183 | + $resp = Invoke-RestMethod -Uri 'https://api.github.com/repos/llvm/llvm-project/releases' -TimeoutSec 5 -ErrorAction Stop |
| 184 | + $tags = $resp | ForEach-Object { $_.tag_name } | Where-Object { $_ -match '^llvmorg-\d+\.\d+\.\d+$' } | Select-Object -Unique -First 20 |
| 185 | + if ($tags -and $tags.Count -gt 0) { |
| 186 | + $tags | Out-File -FilePath $cacheFile -Encoding UTF8 |
| 187 | + return $tags |
| 188 | + } |
| 189 | + } catch { |
| 190 | + # network fallback |
| 191 | + } |
| 192 | + |
| 193 | + return @('llvmorg-21.1.0','llvmorg-20.1.8','llvmorg-19.1.0','llvmorg-18.1.8') |
| 194 | +} |
| 195 | + |
| 196 | +# Helper to register argument completer idempotently |
| 197 | +function Register-LlvmCompleter { |
| 198 | + param( |
| 199 | + [string]$CommandName, |
| 200 | + [string]$ParameterName, |
| 201 | + [scriptblock]$ScriptBlock |
| 202 | + ) |
| 203 | + |
| 204 | + try { |
| 205 | + # Remove any existing completer for this command/parameter (idempotent) |
| 206 | + $existing = Get-ArgumentCompleter -CommandName $CommandName -ErrorAction SilentlyContinue | Where-Object { $_.ParameterName -eq $ParameterName } |
| 207 | + if ($existing) { |
| 208 | + foreach ($e in $existing) { |
| 209 | + try { Unregister-ArgumentCompleter -CommandName $CommandName -ParameterName $ParameterName -ErrorAction SilentlyContinue } catch {} |
| 210 | + } |
| 211 | + } |
| 212 | + } catch {} |
| 213 | + |
| 214 | + Register-ArgumentCompleter -CommandName $CommandName -ParameterName $ParameterName -ScriptBlock $ScriptBlock |
| 215 | +} |
| 216 | + |
| 217 | +# Completer for Activate-Llvm - completes local versions |
| 218 | +$activateSB = { |
| 219 | + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) |
| 220 | + $list = @() |
| 221 | + try { $list = Get-LlvmLocalVersions } catch { $list = @() } |
| 222 | + if (-not $list) { return } |
| 223 | + $matches = if ([string]::IsNullOrEmpty($wordToComplete)) { $list } else { $list | Where-Object { $_ -like "$wordToComplete*" } } |
| 224 | + foreach ($m in $matches) { |
| 225 | + [System.Management.Automation.CompletionResult]::new($m, $m, 'ParameterValue', $m) |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +# Completer for Install-Llvm / llvmup install - combines remote and local |
| 230 | +$installSB = { |
| 231 | + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) |
| 232 | + $locals = @() |
| 233 | + try { $locals = Get-LlvmLocalVersions } catch { $locals = @() } |
| 234 | + $remotes = @() |
| 235 | + try { $remotes = Get-LlvmRemoteVersions } catch { $remotes = @() } |
| 236 | + |
| 237 | + $combined = @() |
| 238 | + if ($remotes) { $combined += $remotes } |
| 239 | + if ($locals) { $combined += $locals } |
| 240 | + |
| 241 | + if (-not $combined) { return } |
| 242 | + $matches = if ([string]::IsNullOrEmpty($wordToComplete)) { $combined } else { $combined | Where-Object { $_ -like "$wordToComplete*" } } |
| 243 | + foreach ($m in $matches | Select-Object -Unique) { |
| 244 | + [System.Management.Automation.CompletionResult]::new($m, $m, 'ParameterValue', $m) |
| 245 | + } |
| 246 | +} |
| 247 | + |
| 248 | +# Register idempotently |
| 249 | +try { Register-LlvmCompleter -CommandName 'Activate-Llvm' -ParameterName 'Version' -ScriptBlock $activateSB } catch {} |
| 250 | +try { Register-LlvmCompleter -CommandName 'Install-Llvm' -ParameterName 'Version' -ScriptBlock $installSB } catch {} |
| 251 | +try { Register-LlvmCompleter -CommandName 'llvmup' -ParameterName 'install' -ScriptBlock $installSB } catch {} |
| 252 | + |
| 253 | +# Export functions for testing if desired |
| 254 | +Export-ModuleMember -Function Get-LlvmLocalVersions, Get-LlvmRemoteVersions, Register-LlvmCompleter |
0 commit comments