Ctrl K
ComunidadGuías…

install.ps1

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#Requires -Version 5.1

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

$script:Repo = 'sophotechlabs/spinoza'
$script:Releases = "https://github.com/$script:Repo/releases"
$script:EnvironmentKey = 'Environment'

function Get-OSArchitecture {
    try {
        return [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
    }
    catch {
        $null = $_
    }
    if ($env:PROCESSOR_ARCHITEW6432) {
        return $env:PROCESSOR_ARCHITEW6432
    }
    return $env:PROCESSOR_ARCHITECTURE
}

function Resolve-Architecture {
    $known = @{
        'X64'   = 'amd64'
        'AMD64' = 'amd64'
        'Arm64' = 'arm64'
    }
    $reported = Get-OSArchitecture
    if ($known.ContainsKey($reported)) {
        return $known[$reported]
    }
    throw "$reported is not supported; see $script:Releases for the other builds"
}

function Get-RedirectLocation {
    param([Parameter(Mandatory = $true)][string]$Uri)
    $request = [System.Net.HttpWebRequest]::Create($Uri)
    $request.AllowAutoRedirect = $false
    $request.UserAgent = 'spinoza-install'
    $response = $request.GetResponse()
    try {
        return $response.Headers['Location']
    }
    finally {
        $response.Close()
    }
}

function Resolve-LatestVersion {
    $location = Get-RedirectLocation -Uri "$script:Releases/latest"
    if ($location -notmatch '/releases/tag/(.+)$') {
        throw "$script:Repo has no published release yet; set SPINOZA_VERSION to pick one"
    }
    return $Matches[1]
}

function Save-Download {
    param(
        [Parameter(Mandatory = $true)][string]$Uri,
        [Parameter(Mandatory = $true)][string]$Path
    )
    Invoke-WebRequest -Uri $Uri -OutFile $Path -UseBasicParsing -UserAgent 'spinoza-install'
}

function Get-ListedChecksum {
    param(
        [Parameter(Mandatory = $true)][string]$ListPath,
        [Parameter(Mandatory = $true)][string]$Name
    )
    foreach ($line in Get-Content -LiteralPath $ListPath) {
        $fields = $line -split '\s+', 2
        if ($fields.Count -ne 2) {
            continue
        }
        if ($fields[1].Trim() -eq $Name) {
            return $fields[0].Trim()
        }
    }
    return ''
}

function Get-Sha256 {
    param([Parameter(Mandatory = $true)][string]$Path)
    $stream = [System.IO.File]::OpenRead($Path)
    try {
        $sha = [System.Security.Cryptography.SHA256]::Create()
        try {
            return [System.BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-', '').ToLowerInvariant()
        }
        finally {
            $sha.Dispose()
        }
    }
    finally {
        $stream.Dispose()
    }
}

function Test-Checksum {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$Name,
        [Parameter(Mandatory = $true)][string]$ListPath
    )
    $expected = Get-ListedChecksum -ListPath $ListPath -Name $Name
    if ($expected -eq '') {
        throw "$Name is not listed in checksums.txt"
    }
    $actual = Get-Sha256 -Path $Path
    if ($expected -ne $actual) {
        throw "checksum mismatch for $Name"
    }
}

function Get-InstallDirectory {
    if ($env:SPINOZA_INSTALL_DIR) {
        return $env:SPINOZA_INSTALL_DIR
    }
    return (Join-Path $env:LOCALAPPDATA (Join-Path 'Programs' 'spinoza'))
}

function Get-InstalledVersion {
    param([Parameter(Mandatory = $true)][string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        return ''
    }
    try {
        return (& $Path --version 2>$null | Select-Object -First 1)
    }
    catch {
        return ''
    }
}

function Install-Copyright {
    param(
        [Parameter(Mandatory = $true)][string]$Unpacked,
        [Parameter(Mandatory = $true)][string]$Directory
    )
    $source = Join-Path $Unpacked 'LICENSE'
    if (-not (Test-Path -LiteralPath $source)) {
        return
    }
    Copy-Item -LiteralPath $source -Destination (Join-Path $Directory 'LICENSE.txt') -Force
}

function Move-Aside {
    param(
        [Parameter(Mandatory = $true)][string]$Target,
        [Parameter(Mandatory = $true)][string]$Retired,
        [int]$Attempts = 20,
        [int]$WaitMs = 250
    )
    for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
        try {
            Move-Item -LiteralPath $Target -Destination $Retired -Force
            return
        }
        catch {
            if ($attempt -eq $Attempts) {
                throw
            }
            Start-Sleep -Milliseconds $WaitMs
        }
    }
}

function Install-Binary {
    param(
        [Parameter(Mandatory = $true)][string]$Source,
        [Parameter(Mandatory = $true)][string]$Target
    )
    $retired = "$Target.old"
    if (Test-Path -LiteralPath $retired) {
        Remove-Item -LiteralPath $retired -Force -ErrorAction SilentlyContinue
    }
    if (Test-Path -LiteralPath $Target) {
        Move-Aside -Target $Target -Retired $retired
    }
    try {
        Copy-Item -LiteralPath $Source -Destination $Target -Force
    }
    catch {
        if (Test-Path -LiteralPath $retired) {
            Move-Item -LiteralPath $retired -Destination $Target -Force
        }
        throw
    }
    Remove-Item -LiteralPath $retired -Force -ErrorAction SilentlyContinue
}

function Test-OnPath {
    param(
        [Parameter(Mandatory = $true)][string]$Directory,
        [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Path
    )
    $wanted = $Directory.TrimEnd('\')
    foreach ($entry in $Path -split ';') {
        if ($entry.Trim().TrimEnd('\') -eq $wanted) {
            return $true
        }
    }
    return $false
}

function Get-UserPath {
    param([string]$Key = $script:EnvironmentKey)
    $handle = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($Key, $false)
    if ($null -eq $handle) {
        return [pscustomobject]@{
            Value = ''
            Kind  = [Microsoft.Win32.RegistryValueKind]::ExpandString
        }
    }
    try {
        $value = [string]$handle.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
        $kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
        if ($value -ne '') {
            $kind = $handle.GetValueKind('Path')
        }
        return [pscustomobject]@{
            Value = $value
            Kind  = $kind
        }
    }
    finally {
        $handle.Close()
    }
}

function Set-UserPath {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param(
        [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Value,
        [Parameter(Mandatory = $true)]$Kind,
        [string]$Key = $script:EnvironmentKey
    )
    $handle = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($Key)
    try {
        $handle.SetValue('Path', $Value, $Kind)
    }
    finally {
        $handle.Close()
    }
}

function Add-ToPath {
    param([Parameter(Mandatory = $true)][string]$Directory)
    $current = Get-UserPath
    if (Test-OnPath -Directory $Directory -Path $current.Value) {
        return $false
    }
    $updated = $Directory
    if ($current.Value.TrimEnd(';') -ne '') {
        $updated = $current.Value.TrimEnd(';') + ';' + $Directory
    }
    Set-UserPath -Value $updated -Kind $current.Kind
    return $true
}

function Remove-FromPath {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param([Parameter(Mandatory = $true)][string]$Directory)
    $current = Get-UserPath
    $wanted = $Directory.TrimEnd('\')
    $kept = @()
    $removed = $false
    foreach ($entry in $current.Value -split ';') {
        if ($entry.Trim() -eq '') {
            continue
        }
        if ($entry.Trim().TrimEnd('\') -eq $wanted) {
            $removed = $true
            continue
        }
        $kept += $entry
    }
    if (-not $removed) {
        return $false
    }
    Set-UserPath -Value ($kept -join ';') -Kind $current.Kind
    return $true
}

function Test-Attestation {
    param(
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$Name
    )
    if (-not $env:SPINOZA_VERIFY_ATTESTATION) {
        return $false
    }
    if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
        throw 'SPINOZA_VERIFY_ATTESTATION is set but gh is not on PATH'
    }
    & gh attestation verify $Path --repo $script:Repo 2>&1 | Out-Null
    if ($LASTEXITCODE -ne 0) {
        throw "$Name carries no build provenance signed for $script:Repo"
    }
    return $true
}

function Install-App {
    param(
        [Parameter(Mandatory = $true)][string]$Temp,
        [Parameter(Mandatory = $true)][string]$Version,
        [Parameter(Mandatory = $true)][string]$Arch,
        [Parameter(Mandatory = $true)][string]$Directory
    )
    $asset = "spinoza_${Version}_windows_${Arch}_app.zip"
    $listed = Get-ListedChecksum -ListPath (Join-Path $Temp 'checksums.txt') -Name $asset
    if ($listed -eq '') {
        return $false
    }
    Save-Download -Uri "$script:Releases/download/$Version/$asset" -Path (Join-Path $Temp $asset)
    Test-Checksum -Path (Join-Path $Temp $asset) -Name $asset -ListPath (Join-Path $Temp 'checksums.txt')
    Test-Attestation -Path (Join-Path $Temp $asset) -Name $asset | Out-Null
    Expand-Archive -LiteralPath (Join-Path $Temp $asset) -DestinationPath (Join-Path $Temp 'app') -Force
    $bundled = Join-Path (Join-Path $Temp 'app') 'Spinoza.exe'
    if (-not (Test-Path -LiteralPath $bundled)) {
        throw 'the app archive did not contain Spinoza.exe'
    }
    $appDirectory = Join-Path $Directory 'app'
    New-Item -ItemType Directory -Path $appDirectory -Force | Out-Null
    $installed = Join-Path $appDirectory 'Spinoza.exe'
    Install-Binary -Source $bundled -Target $installed
    Add-StartMenuShortcut -Target $installed
    return $true
}

function Get-StartMenuShortcut {
    $menu = Join-Path $env:APPDATA (Join-Path 'Microsoft' (Join-Path 'Windows' (Join-Path 'Start Menu' 'Programs')))
    if (-not (Test-Path -LiteralPath $menu)) {
        return ''
    }
    return (Join-Path $menu 'Spinoza.lnk')
}

function Add-StartMenuShortcut {
    param([Parameter(Mandatory = $true)][string]$Target)
    $menu = Join-Path $env:APPDATA (Join-Path 'Microsoft' (Join-Path 'Windows' (Join-Path 'Start Menu' 'Programs')))
    if (-not (Test-Path -LiteralPath $menu)) {
        return
    }
    $shell = New-Object -ComObject WScript.Shell
    $shortcut = $shell.CreateShortcut((Join-Path $menu 'Spinoza.lnk'))
    $shortcut.TargetPath = $Target
    $shortcut.WorkingDirectory = Split-Path -Parent $Target
    $shortcut.Description = 'Spinoza'
    $shortcut.Save()
}

function Install-Spinoza {
    $arch = Resolve-Architecture
    $version = $env:SPINOZA_VERSION
    if (-not $version) {
        $version = Resolve-LatestVersion
    }
    $asset = "spinoza_${version}_windows_${arch}.zip"
    $directory = Get-InstallDirectory
    $binary = Join-Path $directory 'spinoza.exe'
    $temp = Join-Path ([System.IO.Path]::GetTempPath()) ('spinoza-' + [System.Guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $temp -Force | Out-Null
    try {
        Write-Output "Downloading spinoza $version (windows/$arch)"
        Save-Download -Uri "$script:Releases/download/$version/$asset" -Path (Join-Path $temp $asset)
        Save-Download -Uri "$script:Releases/download/$version/checksums.txt" -Path (Join-Path $temp 'checksums.txt')
        Test-Checksum -Path (Join-Path $temp $asset) -Name $asset -ListPath (Join-Path $temp 'checksums.txt')
        if (Test-Attestation -Path (Join-Path $temp $asset) -Name $asset) {
            Write-Output "Verified the build provenance of $asset"
        }
        Expand-Archive -LiteralPath (Join-Path $temp $asset) -DestinationPath (Join-Path $temp 'unpacked') -Force
        $unpacked = Join-Path (Join-Path $temp 'unpacked') 'spinoza.exe'
        if (-not (Test-Path -LiteralPath $unpacked)) {
            throw 'the archive did not contain a spinoza.exe'
        }
        $previous = Get-InstalledVersion -Path $binary
        New-Item -ItemType Directory -Path $directory -Force | Out-Null
        Install-Binary -Source $unpacked -Target $binary
        Install-Copyright -Unpacked (Join-Path $temp 'unpacked') -Directory $directory
        if ($previous -ne '') {
            Write-Output "Updated spinoza $previous -> $version in $directory"
        }
        else {
            Write-Output "Installed spinoza $version in $directory"
        }
        $app = $false
        if (-not $env:SPINOZA_SKIP_APP) {
            $app = Install-App -Temp $temp -Version $version -Arch $arch -Directory $directory
        }
        if ($app) {
            Write-Output "Installed the Spinoza app in $directory\app, and in the Start menu"
        }
        $added = Add-ToPath -Directory $directory
        if ($added) {
            Write-Output "Added $directory to your PATH; open a new terminal to pick it up"
        }
        $env:Path = "$env:Path;$directory"
        Write-Output "Run it in your browser with 'spinoza --open'"
    }
    finally {
        Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue
    }
}

try {
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
catch {
    $null = $_
}

function Uninstall-Spinoza {
    $directory = Get-InstallDirectory
    $removed = @()
    foreach ($name in @('spinoza.exe', 'spinoza.exe.old', 'LICENSE.txt')) {
        $binary = Join-Path $directory $name
        if (Test-Path -LiteralPath $binary) {
            Remove-Item -LiteralPath $binary -Force
            $removed += $name
        }
    }
    $app = Join-Path $directory 'app'
    if (Test-Path -LiteralPath $app) {
        Remove-Item -LiteralPath $app -Recurse -Force
        $removed += 'the desktop app'
    }
    $shortcut = Get-StartMenuShortcut
    if ($shortcut -ne '' -and (Test-Path -LiteralPath $shortcut)) {
        Remove-Item -LiteralPath $shortcut -Force
        $removed += 'the start menu entry'
    }
    if (Remove-FromPath -Directory $directory) {
        $removed += 'the PATH entry'
    }
    if ((Test-Path -LiteralPath $directory) -and -not (Get-ChildItem -LiteralPath $directory -Force)) {
        Remove-Item -LiteralPath $directory -Force
    }
    if ($removed.Count -eq 0) {
        Write-Output "Nothing to remove: spinoza is not installed in $directory"
        return
    }
    Write-Output "Removed $($removed -join ', ') from $directory"
    Write-Output 'Settings and kubeconfigs were left alone'
}

if ($MyInvocation.InvocationName -ne '.') {
    if ($env:SPINOZA_UNINSTALL) {
        Uninstall-Spinoza
    }
    else {
        Install-Spinoza
    }
}