# Install lvcs on Windows from a GitHub release # (docs/production-readiness.md, R3; decision 252), in Windows PowerShell # 5.1 or PowerShell 7: # # irm https://ps.lvcs.sh | iex # curl.exe -fsSL https://ps.lvcs.sh | powershell -c - # # It downloads lvcs--.zip and SHA256SUMS, refuses on a # checksum mismatch, verifies SHA256SUMS's Sigstore bundle when cosign is on # PATH (saying so when it is not), installs without elevation, and adds the # directory to the user PATH. # # Environment: # LVCS_VERSION the release to install, as 0.1.0 or v0.1.0 (default: # the latest release) # LVCS_INSTALL_DIR where lvcs.exe goes (default: # %LOCALAPPDATA%\Programs\lvcs\bin) # LVCS_BASE_URL a URL serving one release's files in place of GitHub (a # mirror, or the release workflow's install test); needs # LVCS_VERSION # LVCS_NO_SERVICE when 1, skip the last step, `lvcs install`, which runs # the per-user daemon as a Scheduled Task at logon # (decision 272) # # No Windows build is published until the Windows port lands (W1 to W9), so # until then this stops at the download and says so. # # Everything runs inside one script block, so `iex` leaves no variables in # the caller's session and a failure throws instead of closing the shell. & { Set-StrictMode -Version 2 $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Windows PowerShell 5.1 may default to TLS 1.0, which GitHub refuses. [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 $repo = 'gabewillen/lvcs' $signer = "^https://github\.com/$repo/\.github/workflows/release\.yml@refs/tags/v" $issuer = 'https://token.actions.githubusercontent.com' function Say([string]$message) { [Console]::Error.WriteLine("lvcs-install: $message") } # A 32-bit PowerShell on 64-bit Windows reports the real CPU here. $cpu = $env:PROCESSOR_ARCHITEW6432 if (-not $cpu) { $cpu = $env:PROCESSOR_ARCHITECTURE } switch ($cpu) { 'AMD64' { $arch = 'x86_64' } 'ARM64' { $arch = 'aarch64' } default { throw "lvcs-install: no release for architecture '$cpu': releases cover x86_64 (AMD64) and aarch64 (ARM64)" } } $target = "$arch-pc-windows-msvc" $version = "$env:LVCS_VERSION" -replace '^v', '' if ($env:LVCS_BASE_URL) { if (-not $version) { throw 'lvcs-install: LVCS_BASE_URL needs LVCS_VERSION' } $base = "$env:LVCS_BASE_URL".TrimEnd('/') } else { if (-not $version) { $latest = "https://github.com/$repo/releases/latest" try { $response = (Invoke-WebRequest -UseBasicParsing -Method Head -Uri $latest).BaseResponse } catch { throw "lvcs-install: could not reach $latest ($($_.Exception.Message)); set LVCS_VERSION to pick a release" } # 5.1 has HttpWebResponse.ResponseUri; 7 has HttpResponseMessage.RequestMessage. if ($response.PSObject.Properties['ResponseUri']) { $final = "$($response.ResponseUri)" } else { $final = "$($response.RequestMessage.RequestUri)" } if ($final -notmatch '/releases/tag/v([^/]+)$') { throw "lvcs-install: found no latest release at https://github.com/$repo/releases; set LVCS_VERSION to pick one" } $version = $Matches[1] } $base = "https://github.com/$repo/releases/download/v$version" } if ($env:LVCS_INSTALL_DIR) { $dir = $env:LVCS_INSTALL_DIR } elseif ($env:LOCALAPPDATA) { $dir = Join-Path $env:LOCALAPPDATA 'Programs\lvcs\bin' } else { throw 'lvcs-install: LOCALAPPDATA is unset; set LVCS_INSTALL_DIR' } $archive = "lvcs-$version-$target.zip" $tmp = Join-Path ([IO.Path]::GetTempPath()) ("lvcs-install-" + [guid]::NewGuid()) New-Item -ItemType Directory -Path $tmp | Out-Null try { Say "downloading lvcs $version for $target" foreach ($file in @($archive, 'SHA256SUMS')) { try { Invoke-WebRequest -UseBasicParsing -Uri "$base/$file" -OutFile (Join-Path $tmp $file) } catch { throw "lvcs-install: could not download $base/$file ($($_.Exception.Message)); Windows builds are published once the Windows port lands" } } $sums = Join-Path $tmp 'SHA256SUMS' if (Get-Command cosign -ErrorAction SilentlyContinue) { $bundle = Join-Path $tmp 'SHA256SUMS.sigstore.json' Invoke-WebRequest -UseBasicParsing -Uri "$base/SHA256SUMS.sigstore.json" -OutFile $bundle & cosign verify-blob --bundle $bundle --certificate-identity-regexp $signer --certificate-oidc-issuer $issuer $sums | Out-Null if ($LASTEXITCODE -ne 0) { throw 'lvcs-install: refusing to install: the Sigstore signature on SHA256SUMS did not verify' } Say 'verified the Sigstore signature on SHA256SUMS' } else { Say 'cosign is not on PATH, so the Sigstore signature on SHA256SUMS is not checked (the checksum still is)' } $want = $null foreach ($line in Get-Content $sums) { $fields = "$line".Trim() -split '\s+' if ($fields.Count -eq 2 -and $fields[1].TrimStart('*') -eq $archive) { $want = $fields[0]; break } } if (-not $want) { throw "lvcs-install: SHA256SUMS lists no $archive" } $got = (Get-FileHash -Algorithm SHA256 -Path (Join-Path $tmp $archive)).Hash # -ne is case-insensitive; Get-FileHash prints upper case, sha256sum lower. if ($got -ne $want) { throw "lvcs-install: refusing to install: $archive has SHA-256 $got, but SHA256SUMS says $want" } $unpacked = Join-Path $tmp 'x' Expand-Archive -Path (Join-Path $tmp $archive) -DestinationPath $unpacked $new = Join-Path $unpacked 'lvcs.exe' if (-not (Test-Path -PathType Leaf $new)) { throw "lvcs-install: $archive holds no lvcs.exe" } New-Item -ItemType Directory -Force -Path $dir | Out-Null $exe = Join-Path $dir 'lvcs.exe' # Windows cannot overwrite a running .exe but can rename it. $old = "$exe.old" if (Test-Path $old) { Remove-Item -Force $old -ErrorAction SilentlyContinue } if (Test-Path $exe) { Move-Item -Force $exe $old } Copy-Item $new $exe Say "installed $(& $exe --version) to $exe" $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not $userPath) { $userPath = '' } $entries = $userPath -split ';' | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\') } if ($entries -notcontains $dir.TrimEnd('\')) { $joined = (@($userPath.TrimEnd(';'), $dir) | Where-Object { $_ }) -join ';' [Environment]::SetEnvironmentVariable('Path', $joined, 'User') Say "added $dir to your user PATH; open a new terminal to use it there" } if ((($env:Path -split ';') | ForEach-Object { $_.TrimEnd('\') }) -notcontains $dir.TrimEnd('\')) { $env:Path = "$env:Path;$dir" } # The per-user daemon, as a Scheduled Task at logon (decision 272). if ($env:LVCS_NO_SERVICE -eq '1') { Say "LVCS_NO_SERVICE=1: not installing the daemon's service; run lvcs install, or lvcs daemon in the foreground" } else { & $exe install if ($LASTEXITCODE -ne 0) { throw "lvcs-install: lvcs install failed; run it again, or set LVCS_NO_SERVICE=1 and run lvcs daemon yourself" } } } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } }