<# .SYNOPSIS Installs a native Windows ggrun app home from a release bundle. .EXAMPLE powershell -ExecutionPolicy Bypass -File .\install.ps1 .EXAMPLE powershell -ExecutionPolicy Bypass -File .\install.ps1 -Backend cuda .EXAMPLE iwr -useb https://raw.githubusercontent.com/raketenkater/ggrun/main/install.ps1 | iex #> [CmdletBinding()] param( [string]$InstallDir = (Join-Path $env:USERPROFILE 'ggrun'), [string]$Release = 'latest', [ValidateSet('cpu', 'cuda')] [string]$Backend = 'cpu', [string]$ReleaseDir = '', [string]$LlamaCppRepo = 'https://github.com/ggml-org/llama.cpp.git', [string]$LlamaCppRef = 'master', [string]$CudaArchitectures = '', [switch]$NoPath, [switch]$AssumeYes ) $ErrorActionPreference = 'Stop' $Repo = 'raketenkater/ggrun' if (-not $PSBoundParameters.ContainsKey('Backend') -and (Get-Command 'nvidia-smi' -ErrorAction SilentlyContinue)) { try { $gpuList = & nvidia-smi -L 2>$null if ($LASTEXITCODE -eq 0 -and $gpuList) { $Backend = 'cuda' Write-Host "NVIDIA GPU detected - installing the CUDA backend. Pass -Backend cpu to force CPU-only." -ForegroundColor Cyan } } catch {} } $Asset = "ggrun-windows-x86_64-$Backend.zip" $CpuAsset = 'ggrun-windows-x86_64-cpu.zip' $ReleaseInfo = $null function Say($Message) { Write-Host $Message } function Ok($Message) { Write-Host " $([char]0x2713) $Message" -ForegroundColor Green } function Warn($Message) { Write-Host " $([char]0x26A0) $Message" -ForegroundColor Yellow } function Fail($Message) { throw $Message } function Test-Command($Name) { return [bool](Get-Command $Name -ErrorAction SilentlyContinue) } function Require-Command($Name, $Hint) { if (!(Test-Command $Name)) { Fail "$Name was not found. $Hint" } } function Get-OSArchitecture { # The process architecture, on BOTH Windows PowerShell 5.1 and pwsh 7+. # # [RuntimeInformation]::OSArchitecture is a .NET Core API. On .NET Framework - # which is what ships as Windows PowerShell 5.1, the default `powershell`, and # what a user gets from a console one-liner - the property exists but returns # NULL, so .ToString() on it throws "cannot call a method on a null-valued # expression". That was the first-run failure a user hit, reported as # "Es ist nicht moeglich, eine Methode fuer einen Ausdruck aufzurufen, der den # NULL hat" at the very next line after the banner. # # Environment::Is64BitOperatingSystem and ::Is64BitProcess are .NET Framework # 4.0 APIs, so they work everywhere this installer runs. The installer is # x86_64-only, so the real question is whether the PROCESS is 64-bit: a 32-bit # powershell on a 64-bit OS still cannot load a 64-bit backend and must be told # to re-run under the 64-bit host. if (-not [System.Environment]::Is64BitProcess) { if ([System.Environment]::Is64BitOperatingSystem) { return 'x86 (32-bit host)' } return 'x86' } if ([System.Environment]::Is64BitOperatingSystem) { return 'X64' } return 'unknown' } function Test-NonInteractive { # True when the installer cannot ask a question: -AssumeYes, # LLM_INSTALL_NONINTERACTIVE=1, or a session with no usable console stdin. # # The third case is the documented one-liner: # iwr -useb .../install.ps1 | iex # Under `iex` there is no interactive host to answer Read-Host, so a prompt # either throws or hangs forever with no output. setup.sh has detected this # since it was written (`[[ ! -t 0 ]]`); install.ps1 did not, and relied on the # caller passing -AssumeYes or setting the variable - neither of which the # documented one-liner does. if ($AssumeYes -or $env:LLM_INSTALL_NONINTERACTIVE -eq '1') { return $true } try { # IsInputRedirected is true for a piped/redirected stdin, i.e. the iex path. if ([Console]::IsInputRedirected) { return $true } } catch { # No console at all (service host, some CI shells): treat as non-interactive. return $true } return $false } function Confirm-Install([string]$What) { # Consent gate before installing third-party software. Non-interactive when the # session cannot answer a prompt, which includes the documented piped install. if (Test-NonInteractive) { return $true } $reply = Read-Host "Install $What now? [Y/n]" return ($reply -eq '' -or $reply -match '^(y|yes)$') } function Resolve-LatestTagFromWeb { # github.com//releases/latest redirects to the tag page and is not # subject to the unauthenticated REST API rate limit that shared runners and # NAT'd networks hit. try { $request = [System.Net.HttpWebRequest]::Create("https://github.com/$Repo/releases/latest") $request.AllowAutoRedirect = $false $request.UserAgent = 'ggrun-installer' $response = $request.GetResponse() $location = $response.Headers['Location'] $response.Close() if ($location -match '/releases/tag/([^/?#]+)$') { return $Matches[1] } } catch { } return '' } function Get-ReleaseInfo { if ($script:ReleaseInfo) { return $script:ReleaseInfo } try { # CI exercises the github.com fallback below without waiting for a real # API outage. if ($env:LLM_INSTALL_SKIP_RELEASE_API -eq '1') { throw 'release API skipped (LLM_INSTALL_SKIP_RELEASE_API=1)' } if ($Release -eq 'latest') { $script:ReleaseInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" } else { $script:ReleaseInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/tags/$Release" } } catch { $apiError = $_.Exception.Message $tag = $Release if ($Release -eq 'latest') { $tag = Resolve-LatestTagFromWeb } if (!$tag) { Fail "Could not query GitHub release '$Release'. Check internet/proxy access to api.github.com and github.com. Details: $apiError" } # Without the API the asset list is unknown: assets are fetched from the # standard release URLs and SHA256SUMS is still required. Warn "GitHub API unavailable ($apiError); using release $tag from github.com directly." $script:ReleaseInfo = [pscustomobject]@{ tag_name = $tag; assets = @(); fallback = $true } } return $script:ReleaseInfo } function Get-AssetUrl($Info, [string]$Name) { if ($Info.fallback) { return "https://github.com/$Repo/releases/download/$($Info.tag_name)/$Name" } foreach ($item in $Info.assets) { if ($item.name -eq $Name) { return $item.browser_download_url } } return '' } function Save-ReleaseAsset([string]$Name, [bool]$Required) { $archive = Join-Path $tmp $Name if ($ReleaseDir) { $localAsset = Join-Path $ReleaseDir $Name if (!(Test-Path $localAsset)) { if ($Required) { Fail "Release asset not found: $localAsset" } return '' } Copy-Item $localAsset $archive $sumsPath = Join-Path $ReleaseDir 'SHA256SUMS' if ((Test-Path $sumsPath) -and !(Test-Path (Join-Path $tmp 'SHA256SUMS'))) { Copy-Item $sumsPath (Join-Path $tmp 'SHA256SUMS') } } else { $info = Get-ReleaseInfo $url = Get-AssetUrl $info $Name if (!$url) { if ($Required) { Fail "No native Windows release asset found: $Name" } return '' } Say "Downloading: $Name" try { Invoke-WebRequest -Uri $url -OutFile $archive } catch { # Without the API listing, an optional asset may simply not exist. if ($info.fallback -and !$Required) { return '' } Fail "Could not download $Name. Check internet/proxy access to github.com. Details: $($_.Exception.Message)" } $sumsUrl = Get-AssetUrl $info 'SHA256SUMS' if ($sumsUrl -and !(Test-Path (Join-Path $tmp 'SHA256SUMS'))) { try { Invoke-WebRequest -Uri $sumsUrl -OutFile (Join-Path $tmp 'SHA256SUMS') } catch { Fail "Could not download SHA256SUMS; refusing an unverified install. Details: $($_.Exception.Message)" } } } $sums = Join-Path $tmp 'SHA256SUMS' $allowUnverified = ($env:LLM_INSTALL_ALLOW_UNVERIFIED -eq '1') if (Test-Path $sums) { $escapedName = [regex]::Escape($Name) $pattern = '\s' + $escapedName + '$' $line = Get-Content $sums | Where-Object { $_ -match $pattern } | Select-Object -First 1 if ($line) { $expected = ($line -split '\s+')[0].ToLowerInvariant() $actual = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLowerInvariant() if ($actual -ne $expected) { Fail "Checksum mismatch for $Name" } Ok "Verified checksum for $Name" } elseif ($allowUnverified) { Warn "SHA256SUMS did not include $Name; LLM_INSTALL_ALLOW_UNVERIFIED=1 set - continuing" } else { Fail "SHA256SUMS did not include $Name; refusing to install unverified. Set LLM_INSTALL_ALLOW_UNVERIFIED=1 to override." } } elseif ($allowUnverified) { Warn "No SHA256SUMS found; LLM_INSTALL_ALLOW_UNVERIFIED=1 set - installing UNVERIFIED bundle" } else { Fail "No SHA256SUMS found; refusing to install an unverified bundle. Set LLM_INSTALL_ALLOW_UNVERIFIED=1 to override." } return $archive } function Install-ReleaseBundle([string]$Name, [bool]$Required) { $archive = Save-ReleaseAsset $Name $Required if (!$archive) { return $false } $payload = Join-Path $tmp ("payload-" + [IO.Path]::GetFileNameWithoutExtension($Name)) try { Expand-Archive -Path $archive -DestinationPath $payload -Force } catch { Fail "Could not unpack release archive $Name. Delete the downloaded file and retry. Details: $($_.Exception.Message)" } $root = Get-ChildItem -Path $payload -Directory | Select-Object -First 1 if (!$root) { Fail 'Release archive did not contain a payload directory' } Copy-Item -Path (Join-Path $root.FullName 'bin\*') -Destination $bin -Recurse -Force foreach ($file in @('LICENSE', 'README.md', 'CHANGELOG.md')) { $src = Join-Path $root.FullName $file if (Test-Path $src) { Copy-Item $src (Join-Path $InstallDir $file) -Force } } return $true } function Write-CmdWrapper([string]$Path, [string]$ArgsLine) { $content = @" @echo off set "LLM_APP_HOME=%~dp0" set "PATH=%~dp0.bin;%PATH%" "%~dp0.bin\ggrun.exe" $ArgsLine %* "@ Set-Content -Path $Path -Value $content -Encoding ASCII } function Build-CudaBackend { # Reported as issue #27: this checked one tool at a time and hard-failed on # the first miss, so a user with none of git, cmake or nvcc was told only # "cmake was not found" and would have needed three separate runs to learn # the rest. Worse, the CPU bundle had already installed successfully by this # point, so a working ggrun looked like a failed install. # # Report every missing prerequisite at once, and be clear about what does # and does not still work. $missing = @() if (!(Test-Command 'git')) { $missing += 'git (Git for Windows)' } if (!(Test-Command 'cmake')) { $missing += 'cmake (or Visual Studio 2022 with the C++ CMake tools)' } if (!(Test-Command 'nvcc')) { $missing += 'nvcc (NVIDIA CUDA Toolkit, on PATH)' } if ($missing.Count -gt 0) { Warn 'A CUDA backend cannot be built on this machine yet.' Warn ('Missing: ' + ($missing -join '; ')) Warn 'There is no prebuilt Windows CUDA bundle, so CUDA has to be built from source here.' Say 'ggrun itself is installed and works with the CPU backend. Install the tools above and re-run with -Backend cuda to add GPU support.' return } if (!(Test-Command 'nvidia-smi')) { Warn 'nvidia-smi was not found. Install or repair the NVIDIA driver before launching CUDA models.' } $srcRoot = Join-Path $InstallDir '.src' $repoDir = Join-Path $srcRoot 'llama.cpp' $buildDir = Join-Path $repoDir 'build-ggrun-cuda' New-Item -ItemType Directory -Force -Path $srcRoot | Out-Null if (Test-Path (Join-Path $repoDir '.git')) { Say "Updating llama.cpp source: $repoDir" & git -C $repoDir fetch --depth 1 origin $LlamaCppRef if ($LASTEXITCODE -ne 0) { Fail 'git fetch failed for llama.cpp' } & git -C $repoDir checkout FETCH_HEAD if ($LASTEXITCODE -ne 0) { Fail 'git checkout failed for llama.cpp' } } else { Say "Cloning llama.cpp: $LlamaCppRepo ($LlamaCppRef)" & git clone --depth 1 --branch $LlamaCppRef $LlamaCppRepo $repoDir if ($LASTEXITCODE -ne 0) { Fail 'git clone failed for llama.cpp' } } $cmakeArgs = @( '-S', $repoDir, '-B', $buildDir, '-DCMAKE_BUILD_TYPE=Release', '-DGGML_CUDA=ON', '-DGGML_NATIVE=OFF', '-DBUILD_SHARED_LIBS=OFF' ) if ($CudaArchitectures) { $cmakeArgs += "-DCMAKE_CUDA_ARCHITECTURES=$CudaArchitectures" } Say 'Configuring llama.cpp CUDA backend' & cmake @cmakeArgs if ($LASTEXITCODE -ne 0) { Fail 'CMake could not configure the CUDA backend. Install Visual Studio 2022 Build Tools with the Desktop development with C++ workload, CMake tools, and a Windows SDK; then rerun.' } Say 'Building llama-server CUDA backend' & cmake --build $buildDir --config Release --target llama-server --parallel if ($LASTEXITCODE -ne 0) { Fail 'The CUDA backend build failed. Review the CMake output above; verify that the NVIDIA CUDA Toolkit and Visual Studio C++ toolset are compatible.' } $server = Get-ChildItem -Path $buildDir -Recurse -Filter 'llama-server.exe' | Select-Object -First 1 if (!$server) { Fail 'llama-server.exe was not produced by the CUDA build' } Copy-Item $server.FullName (Join-Path $bin 'llama-server.exe') -Force $serverDir = Split-Path -Parent $server.FullName Get-ChildItem -Path $serverDir -Filter '*.dll' -File -ErrorAction SilentlyContinue | ForEach-Object { Copy-Item $_.FullName (Join-Path $bin $_.Name) -Force } Ok "Built native Windows CUDA backend from $($server.FullName)" } function Install-PrebuiltCudaBackend([string]$BinDir) { # Fetch upstream prebuilt llama.cpp CUDA binaries (server + cudart) so a GPU # backend works with no CUDA Toolkit / MSVC / CMake - the from-source build # (Build-CudaBackend) stays as a fallback. Say 'Fetching prebuilt llama.cpp CUDA backend from ggml-org/llama.cpp releases...' # Not releases/latest: since 2026-09 that is a versioned llama.cpp release # with no binaries, while the numbered builds that carry the Windows CUDA # zips are published as prereleases. Asking only for "latest" found nothing # and every NVIDIA install quietly ended up on the CPU backend. try { $releases = Invoke-RestMethod -Uri 'https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=20' -Headers @{ 'User-Agent' = 'ggrun-installer' } } catch { Warn "Could not query llama.cpp releases: $($_.Exception.Message)" return $false } $rel = $releases | Where-Object { $_.assets | Where-Object { $_.name -match 'bin-win-cuda-.*-x64\.zip$' -and $_.name -notmatch '^cudart-' } } | Select-Object -First 1 if (-not $rel) { Warn 'No prebuilt win-cuda asset in recent llama.cpp releases.'; return $false } $assets = $rel.assets # Prefer the cuda-12.4 build for broad driver compatibility, else any win-cuda x64. $server = $assets | Where-Object { $_.name -match 'bin-win-cuda-12\.4-x64\.zip$' -and $_.name -notmatch '^cudart-' } | Select-Object -First 1 if (-not $server) { $server = $assets | Where-Object { $_.name -match 'bin-win-cuda-.*-x64\.zip$' -and $_.name -notmatch '^cudart-' } | Select-Object -First 1 } if (-not $server) { Warn "No prebuilt win-cuda asset in llama.cpp $($rel.tag_name)."; return $false } # Match the cudart redistributable to the server asset's CUDA version. $cudaVer = if ($server.name -match 'cuda-([0-9.]+)-x64') { $Matches[1] } else { '' } $escapedCuda = [regex]::Escape($cudaVer) $pattern = 'cudart-.*win-cuda-' + $escapedCuda + '-x64\.zip$' $cudart = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1 if (-not $cudart) { $cudart = $assets | Where-Object { $_.name -match 'cudart-.*win-cuda-.*-x64\.zip$' } | Select-Object -First 1 } try { foreach ($a in @($server, $cudart)) { if (-not $a) { continue } $zip = Join-Path $tmp $a.name Say " downloading $($a.name)" Invoke-WebRequest -Uri $a.browser_download_url -OutFile $zip Expand-Archive -Path $zip -DestinationPath $BinDir -Force } } catch { Warn "Prebuilt CUDA download/extract failed: $($_.Exception.Message)" return $false } if (!(Test-Path (Join-Path $BinDir 'llama-server.exe'))) { Warn 'Prebuilt archive did not contain llama-server.exe.' return $false } Ok "Installed prebuilt CUDA backend (llama.cpp $($rel.tag_name), CUDA $cudaVer)" return $true } # --- Windows dependency provisioning (issue #18) ------------------------- # On a clean Windows machine the bundled llama-server.exe often fails to start # because the Visual C++ runtime or OpenSSL 3.x DLLs are missing. These helpers # detect and, where possible, auto-provision those dependencies so the install # does not silently break. They are written to stay compatible with the # default Windows PowerShell 5.1 (`powershell`) that the documented install # command uses. function Find-SystemDll([string]$Name) { # Search common locations for a DLL and return the first full path found. $roots = @( (Join-Path $env:ProgramFiles 'OpenSSL-Win64\bin'), $env:ProgramFiles, ${env:ProgramFiles(x86)}, (Join-Path $env:LOCALAPPDATA 'Microsoft\OneDrive'), $env:SystemRoot ) $py = Get-Command python -ErrorAction SilentlyContinue if ($py) { $roots += (Split-Path $py.Path) } $git = Get-Command git -ErrorAction SilentlyContinue if ($git) { $roots += (Split-Path $git.Path) } foreach ($root in $roots) { if (!$root -or !(Test-Path $root)) { continue } $direct = Join-Path $root $Name if (Test-Path $direct) { return $direct } $binDir = Join-Path $root 'bin' if (Test-Path $binDir) { $inBin = Join-Path $binDir $Name if (Test-Path $inBin) { return $inBin } } } return '' } function Test-VCppRedist { # True when the Visual C++ 2015-2022 x64 runtime is installed. $keys = @( 'HKLM:\Software\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'HKLM:\Software\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64' ) foreach ($key in $keys) { try { $props = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue if ($props -and $props.Installed -eq 1) { return $true } } catch { } } return $false } function Ensure-VCppRuntime { # Ensure the Visual C++ 2015-2022 x64 runtime is present. Returns $true if # it is already installed or was installed successfully. if (Test-VCppRedist) { Ok 'Visual C++ runtime already present' return $true } Say 'Visual C++ 2015-2022 runtime is missing; downloading the official redistributable...' $url = 'https://aka.ms/vs/17/release/vc_redist.x64.exe' $dest = Join-Path $tmp 'vc_redist.x64.exe' try { Invoke-WebRequest -Uri $url -OutFile $dest -ErrorAction Stop } catch { Warn "Could not download the Visual C++ redistributable: $($_.Exception.Message)" return $false } Say 'Installing Visual C++ runtime (requires administrator privileges)...' $proc = Start-Process -FilePath $dest -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) { Ok 'Visual C++ runtime installed' return $true } Warn "Visual C++ redistributable installer exited with code $($proc.ExitCode). Run $url manually as administrator." return $false } function Ensure-OpenSSL { # Locate OpenSSL 3.x DLLs and copy them next to llama-server.exe. Falls back # to GGRUN_OPENSSL_ZIP_URL when set. Returns $true if all DLLs are present. $dlls = @('libcrypto-3-x64.dll', 'libssl-3-x64.dll') $allFound = $true foreach ($dll in $dlls) { $existing = Join-Path $bin $dll if (Test-Path $existing) { continue } $src = Find-SystemDll $dll if ($src) { Copy-Item $src $bin -Force Ok "Bundled OpenSSL DLL: $dll (from $src)" } else { $allFound = $false } } if ($allFound) { return $true } if ($env:GGRUN_OPENSSL_ZIP_URL) { Say 'Downloading OpenSSL DLL bundle from GGRUN_OPENSSL_ZIP_URL...' $zip = Join-Path $tmp 'openssl.zip' try { Invoke-WebRequest -Uri $env:GGRUN_OPENSSL_ZIP_URL -OutFile $zip -ErrorAction Stop $out = Join-Path $tmp 'openssl' Expand-Archive -Path $zip -DestinationPath $out -Force foreach ($dll in $dlls) { $hit = Get-ChildItem -Path $out -Filter $dll -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if ($hit) { Copy-Item $hit.FullName $bin -Force Ok "Bundled OpenSSL DLL: $dll" } } return $true } catch { Warn "Could not fetch OpenSSL bundle: $($_.Exception.Message)" } } Warn 'OpenSSL 3.x DLLs (libcrypto-3-x64.dll, libssl-3-x64.dll) were not found on this system.' Warn 'Install OpenSSL 3.x for Windows (Win64 edition) from https://slproweb.com/products/Win32OpenSSL.html and re-run this installer.' return $false } function Find-MissingBackendDll([string]$Server) { # Best-effort: run the backend and collect any reported missing .dll names. $output = & $Server --version 2>&1 $missing = @() foreach ($line in $output) { if ($line -match '([\w\-]+\.dll)') { $missing += $Matches[1] } } return $missing } function Resolve-Python { # Returns @{ Exe; Pre } for a verified Python 3, or $null. Checking the # interpreter avoids mistaking the Microsoft Store execution alias for an # installed Python. `py` needs -3 to select Python 3 explicitly. # # Order MUST match the runtime resolver (go/pkg/python.Command: # python3 -> python -> py) so the deps we pip-install land in the very # interpreter ggrun later runs download_any_gguf.py with. A mismatch silently # installs huggingface_hub into a different interpreter than the one used at # runtime -> ModuleNotFoundError despite a "successful" install. $candidates = @( @{ Exe = 'python3'; Pre = @() }, @{ Exe = 'python'; Pre = @() }, @{ Exe = 'py'; Pre = @('-3') } ) foreach ($candidate in $candidates) { if (!(Test-Command $candidate.Exe)) { continue } try { & $candidate.Exe @($candidate.Pre) -c 'import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)' 2>$null if ($LASTEXITCODE -eq 0) { return $candidate } } catch { } } return $null } function Test-PythonDownloadDeps($Py) { try { & $Py.Exe @($Py.Pre) -c 'import huggingface_hub, tqdm' 2>$null return ($LASTEXITCODE -eq 0) } catch { return $false } } function Ensure-Python { # Every model launch reads GGUF metadata with parse_gguf.py, so ggrun needs # Python 3. Model downloads also need huggingface_hub and tqdm. Installing # Python defaults to yes; a missing Python is reported, not fatal, so the # rest of the app home still installs. $py = Resolve-Python if (!$py -and (Test-Command 'winget') -and (Confirm-Install 'Python 3.12 via winget (needed to launch and download models)')) { Say 'Installing Python 3 via winget...' try { & winget install -e --id Python.Python.3.12 --accept-package-agreements --accept-source-agreements --silent } catch { } # winget updates the persistent PATH, not the current PowerShell # process. Refresh it before looking for the newly installed runtime. $env:Path = @( [Environment]::GetEnvironmentVariable('Path', 'Machine'), [Environment]::GetEnvironmentVariable('Path', 'User'), $env:Path ) -join ';' $py = Resolve-Python } if (!$py) { Warn 'Python 3 not found. ggrun cannot launch or download models without it.' Warn 'Install from https://www.python.org/downloads/ (tick "Add python.exe to PATH"), then run:' Warn ' python -m pip install --user huggingface_hub tqdm' return } if (Test-PythonDownloadDeps $py) { Ok 'Python download dependencies already installed' return } Say 'Installing Python download dependencies (huggingface_hub, tqdm)...' try { & $py.Exe @($py.Pre) -m pip install --user --upgrade huggingface_hub tqdm if ($LASTEXITCODE -eq 0 -and (Test-PythonDownloadDeps $py)) { Ok 'Python download dependencies ready' } else { Warn 'Could not install huggingface_hub/tqdm. Run: python -m pip install --user huggingface_hub tqdm' } } catch { Warn 'Could not install huggingface_hub/tqdm. Run: python -m pip install --user huggingface_hub tqdm' } } function Test-GgufParse($Py, [string]$Parser) { # Every launch runs parse_gguf.py. Check that path on a 24-byte GGUF v3 # header (no tensors, no metadata). $gguf = Join-Path $tmp 'selfcheck.gguf' [IO.File]::WriteAllBytes($gguf, [byte[]](@(0x47, 0x47, 0x55, 0x46, 3) + @(0) * 19)) try { $out = & $Py.Exe @($Py.Pre) $Parser $gguf 2>$null | Out-String return ($LASTEXITCODE -eq 0 -and $out -match 'tensor_accounting_schema') } catch { return $false } } function Collect-Diagnostics([string]$ErrorMessage) { # Sanitized diagnostic bundle for an install-failure GitHub issue. $L = [System.Collections.Generic.List[string]]::new() $L.Add('### Environment') $L.Add("- installer: install.ps1 ($Repo)") $dateStr = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $L.Add("- date: $dateStr") $osVer = [System.Environment]::OSVersion.VersionString $osArch = Get-OSArchitecture $L.Add("- os: $osVer ($osArch)") $psVer = $PSVersionTable.PSVersion $L.Add("- powershell: $psVer") $L.Add("- backend: $Backend | release=$Release") $L.Add('') $L.Add('### Failure') $L.Add("- message: $ErrorMessage") $L.Add('') $L.Add('### Hardware') $gpu = '' if (Get-Command nvidia-smi -ErrorAction SilentlyContinue) { try { $gpu = (& nvidia-smi -L 2>$null) -join '; ' } catch { } } if (-not $gpu) { try { $gpu = ((Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue).Name) -join '; ' } catch {} } $L.Add("- gpu: $gpu") try { $cpuName = (Get-CimInstance Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1).Name; $L.Add("- cpu: $cpuName") } catch {} try { $ramGb = [math]::Round((Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).TotalPhysicalMemory/1GB); $L.Add("- ram: $ramGb GB") } catch {} $L.Add('') $L.Add('### Tools') foreach ($t in 'cmake', 'nvcc', 'git', 'python', 'py', 'go') { $v = 'not found' if (Get-Command $t -ErrorAction SilentlyContinue) { try { if ($t -eq 'go') { $v = (& go version) } else { $v = (& $t --version 2>&1 | Select-Object -First 1) } } catch { $v = 'present (version unknown)' } } $L.Add("- ${t}: $v") } ($L -join "`n") } function Report-InstallFailure($ErrorRecord) { # Mirrors install.sh: gather diagnostics, save a report, and offer a # one-click pre-filled GitHub issue (or gh create). Nothing is sent until # the user acts. Set LLM_INSTALL_NO_REPORT=1 to disable. if ($env:LLM_INSTALL_NO_REPORT -eq '1') { return } $msg = if ($ErrorRecord) { "$ErrorRecord".Trim() } else { 'unknown error' } $diag = Collect-Diagnostics $msg $reportFile = Join-Path $env:USERPROFILE 'ggrun-install-report.txt' try { Set-Content -Path $reportFile -Value $diag -Encoding UTF8 } catch { $reportFile = '' } Warn "Install failed: $msg" if ($reportFile) { Say " Saved diagnostics: $reportFile" } $title = "install failed: $msg" $body = "$diag`n`n" $titleEsc = [uri]::EscapeDataString($title) $bodyEsc = [uri]::EscapeDataString($body) $url = "https://github.com/$Repo/issues/new?labels=install&title=$titleEsc&body=$bodyEsc" $ghAuthed = $false if (Get-Command gh -ErrorAction SilentlyContinue) { gh auth status 2>$null | Out-Null; $ghAuthed = ($LASTEXITCODE -eq 0) } # Same gate as Confirm-Install: a piped `iwr | iex` install has no console # stdin, so it must not reach Read-Host here either. $interactive = -not (Test-NonInteractive) $reply = '' if ($ghAuthed -and $interactive) { # Guard Read-Host: a non-interactive/piped install that failed must still # print the issue URL rather than die at the prompt. try { $reply = Read-Host 'Report this to the maintainers via GitHub now? [y/N]' } catch { $reply = '' } } if ($reply -match '^(y|yes)$') { $body | gh issue create --repo $Repo --title $title --body-file - return } Say " Help us fix it - open a pre-filled issue (nothing is sent until you submit):" Say " $url" } Say '=== ggrun native Windows installer ===' Say "Install dir: $InstallDir" Say "Backend: $Backend" $arch = Get-OSArchitecture if ($arch -ne 'X64') { Fail "This installer currently supports Windows x86_64 only; detected architecture: $arch" } if ($Backend -eq 'cuda') { Say 'CUDA mode: native Windows NVIDIA via llama.cpp GGML_CUDA' } $logsDir = Join-Path $InstallDir '.logs' New-Item -ItemType Directory -Force -Path $logsDir | Out-Null $transcriptPath = Join-Path $logsDir ("setup-" + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log') try { Start-Transcript -Path $transcriptPath -Append | Out-Null } catch { Warn "Could not start transcript: $($_.Exception.Message)" } Say "Log: $transcriptPath" $tmp = Join-Path ([IO.Path]::GetTempPath()) ("ggrun-install-" + [guid]::NewGuid().ToString('N')) New-Item -ItemType Directory -Force -Path $tmp | Out-Null try { $bin = Join-Path $InstallDir '.bin' $models = Join-Path $InstallDir 'models' $config = Join-Path $InstallDir '.config' $cache = Join-Path $InstallDir '.cache' $logs = Join-Path $InstallDir '.logs' New-Item -ItemType Directory -Force -Path $bin, $models, $config, $cache, $logs | Out-Null if ($Backend -eq 'cuda') { $installed = Install-ReleaseBundle $Asset $false if (!$installed) { Say 'Installing the CPU Windows bundle for the launcher, then adding a CUDA backend.' [void](Install-ReleaseBundle $CpuAsset $true) $cudaOk = $false if (Confirm-Install 'prebuilt llama.cpp CUDA backend from ggml-org (no toolchain needed)') { $cudaOk = Install-PrebuiltCudaBackend $bin } if (!$cudaOk) { Warn 'Falling back to building llama.cpp CUDA from source (needs git + cmake + nvcc + MSVC).' Build-CudaBackend } } } else { [void](Install-ReleaseBundle $Asset $true) } $cfgPath = Join-Path $config 'config' $llamaServer = Join-Path $bin 'llama-server.exe' $cfg = @( '# ggrun Go config. Loaded when LLM_APP_HOME points at this app home.', "LLM_APP_HOME=`"$InstallDir`"", "LLM_MODEL_DIR=`"$models`"", "LLM_CACHE_DIR=`"$cache`"", "LLM_LOG_DIR=`"$logs`"", # auto, not a pinned name: a configured name disables the per-model # architecture probe and the build offer for models it cannot load. 'LLM_BACKEND="auto"', "LLAMA_SERVER=`"$llamaServer`"" ) # Reinstalling/upgrading must not reset user configuration. if (!(Test-Path -LiteralPath $cfgPath)) { Set-Content -Path $cfgPath -Value $cfg -Encoding UTF8 } Write-CmdWrapper (Join-Path $InstallDir 'ggrun.cmd') '' if (!$NoPath) { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $parts = @($userPath -split ';' | Where-Object { $_ }) if ($parts -notcontains $InstallDir) { [Environment]::SetEnvironmentVariable('Path', (($parts + $InstallDir) -join ';'), 'User') Warn "Added $InstallDir to the user PATH. Open a new terminal to use ggrun.cmd directly." } } Ensure-Python if (!(Test-Path (Join-Path $bin 'ggrun.exe'))) { Fail 'ggrun.exe was not installed' } if (!(Test-Path $llamaServer)) { Fail 'llama-server.exe was not installed' } $oldAppHome = $env:LLM_APP_HOME try { $env:LLM_APP_HOME = $InstallDir & (Join-Path $bin 'ggrun.exe') version | Out-Null if ($LASTEXITCODE -ne 0) { Fail 'Installed ggrun failed its version check' } & (Join-Path $bin 'ggrun.exe') detect | Out-Null if ($LASTEXITCODE -ne 0) { Fail 'Installed ggrun failed hardware detection' } $py = Resolve-Python $parser = Join-Path $bin 'parse_gguf.py' if ($py -and (Test-Path $parser)) { if (!(Test-GgufParse $py $parser)) { Fail "parse_gguf.py failed under $($py.Exe); model launches would fail." } Ok 'GGUF metadata parser works' } elseif (!$py) { Warn 'Skipped the GGUF parser check: no Python 3. Install it before launching a model.' } & $llamaServer --version | Out-Null if ($LASTEXITCODE -ne 0) { # On a clean Windows machine the usual cause is a missing Visual C++ # runtime or OpenSSL DLL. Try to provision both, then re-test once. $missing = Find-MissingBackendDll $llamaServer $didProvision = $false if (Ensure-VCppRuntime) { $didProvision = $true } if (Ensure-OpenSSL) { $didProvision = $true } if ($didProvision) { & $llamaServer --version | Out-Null } if ($LASTEXITCODE -ne 0) { $msg = 'Installed llama-server could not start.' if ($missing.Count -gt 0) { $msg += ' Missing DLL(s): ' + ($missing -join ', ') + '.' } $msg += ' A required Visual C++ runtime or OpenSSL DLL is missing. Install the Visual C++ 2015-2022 Redistributable (https://aka.ms/vs/17/release/vc_redist.x64.exe) and OpenSSL 3.x for Windows (https://slproweb.com/products/Win32OpenSSL.html), then re-run this installer.' Fail $msg } } } finally { $env:LLM_APP_HOME = $oldAppHome } Ok 'Installed native Windows ggrun' Ok 'CLI, hardware detection, and backend startup checks passed' Say '' Say '+------------------------------------------------------------+' Say '| ggrun installer finished |' Say '+------------------------------------------------------------+' Say "CLI/GUI: $InstallDir\ggrun.cmd (no arguments opens the GUI)" Say "Models: $models" Say "Config: $cfgPath" Say '' Say 'Next:' Say " $InstallDir\ggrun.cmd # interactive GUI" Say " $InstallDir\ggrun.cmd detect" Say " $InstallDir\ggrun.cmd --download" Say " $InstallDir\ggrun.cmd $models\your-model.gguf" } catch { Report-InstallFailure $_ throw } finally { try { Stop-Transcript | Out-Null } catch { } Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }