-
Notifications
You must be signed in to change notification settings - Fork 0
Add cross-platform installer scripts #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
95a09ed
Add cross-platform install scripts
cleak f670a29
Tighten installer path and lock handling
cleak 66fcefa
Address installer review feedback
cleak b230769
Avoid PowerShell automatic variable shadowing
cleak 77dc2a3
Harden Windows installer PATH handling
cleak 026fe4e
Harden installer path-safe cargo invocation
cleak 5c6e7fb
Use approved PowerShell path helper verbs
cleak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Installing Tempyr | ||
|
|
||
| Tempyr is a Rust workspace. The repo root `Cargo.toml` is a virtual manifest, so source installs need to target `crates/tempyr-cli` instead of `cargo install --path .`. | ||
|
|
||
| ## Linux | ||
|
|
||
| Run: | ||
|
|
||
| ```bash | ||
| bash install.sh | ||
| ``` | ||
|
|
||
| The script installs Tempyr with: | ||
|
|
||
| ```bash | ||
| cargo install --path crates/tempyr-cli --root "${XDG_DATA_HOME:-$HOME/.local/share}/tempyr" --locked --force --bin tempyr | ||
| ``` | ||
|
|
||
| It then updates `PATH` idempotently in `~/.profile` and in the active shell's rc file when that shell is `bash` or `zsh`. | ||
|
|
||
| If you want `install.sh` to skip shell startup file changes, pass `--no-path-update`: | ||
|
|
||
| ```bash | ||
| bash install.sh --no-path-update | ||
| ``` | ||
|
|
||
| ## Windows | ||
|
|
||
| Run: | ||
|
|
||
| ```powershell | ||
| powershell -ExecutionPolicy Bypass -File .\install.ps1 | ||
| ``` | ||
|
|
||
| The script installs Tempyr with: | ||
|
|
||
| ```powershell | ||
| cargo install --path .\crates\tempyr-cli --root "$Env:LocalAppData\Tempyr" --locked --force --bin tempyr | ||
| ``` | ||
|
|
||
| It then updates the user `PATH` so new shells can find `tempyr.exe`. | ||
|
|
||
| If you want `install.ps1` to skip user `PATH` changes, pass `-NoPathUpdate`: | ||
|
|
||
| ```powershell | ||
| .\install.ps1 -NoPathUpdate | ||
| ``` | ||
|
|
||
| ## Updating safely | ||
|
|
||
| Rerun the installer to update Tempyr. If the currently installed Tempyr binary is locked by a running process, the installer only stops processes whose executable path exactly matches the target installed binary, then retries the install. It does not kill processes based on name alone. | ||
|
|
||
| ## Custom install root | ||
|
|
||
| Both installers accept a custom install root: | ||
|
|
||
| ```bash | ||
| bash install.sh --install-root /some/path | ||
| ``` | ||
|
|
||
| ```powershell | ||
| .\install.ps1 -InstallRoot C:\Some\Path | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,332 @@ | ||
| [CmdletBinding()] | ||
| param( | ||
| [string]$InstallRoot = $(if ($env:TEMPYR_INSTALL_ROOT) { | ||
| $env:TEMPYR_INSTALL_ROOT | ||
| } else { | ||
| Join-Path $env:LOCALAPPDATA "Tempyr" | ||
| }), | ||
| [switch]$NoPathUpdate | ||
| ) | ||
|
|
||
| Set-StrictMode -Version Latest | ||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| function Resolve-CanonicalPath { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Path | ||
| ) | ||
|
|
||
| if (Test-Path -LiteralPath $Path) { | ||
| return (Get-Item -LiteralPath $Path -ErrorAction Stop).FullName.TrimEnd('\') | ||
| } | ||
|
|
||
| return [System.IO.Path]::GetFullPath($Path).TrimEnd('\') | ||
| } | ||
|
|
||
| function Test-FileLocked { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Path | ||
| ) | ||
|
|
||
| if (-not (Test-Path -LiteralPath $Path)) { | ||
| return $false | ||
| } | ||
|
|
||
| try { | ||
| $stream = [System.IO.File]::Open( | ||
| $Path, | ||
| [System.IO.FileMode]::Open, | ||
| [System.IO.FileAccess]::ReadWrite, | ||
| [System.IO.FileShare]::None | ||
| ) | ||
| $stream.Dispose() | ||
| return $false | ||
| } catch [System.UnauthorizedAccessException] { | ||
| return $false | ||
| } catch [System.IO.IOException] { | ||
| return $true | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| function Get-TargetProcessIds { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$BinaryPath | ||
| ) | ||
|
|
||
| $targetPath = Resolve-CanonicalPath -Path $BinaryPath | ||
| $matchingProcessIds = New-Object System.Collections.Generic.List[int] | ||
|
|
||
| foreach ($process in Get-CimInstance Win32_Process -Filter "Name='tempyr.exe'") { | ||
| if (-not $process.ExecutablePath) { | ||
| continue | ||
| } | ||
|
|
||
| try { | ||
| $candidate = Resolve-CanonicalPath -Path $process.ExecutablePath | ||
| } catch { | ||
| continue | ||
| } | ||
|
|
||
| if ([string]::Equals($candidate, $targetPath, [System.StringComparison]::OrdinalIgnoreCase)) { | ||
| $matchingProcessIds.Add([int]$process.ProcessId) | ||
| } | ||
| } | ||
|
|
||
| return $matchingProcessIds.ToArray() | ||
| } | ||
|
|
||
| function Stop-TargetProcesses { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$BinaryPath | ||
| ) | ||
|
|
||
| $processIds = @(Get-TargetProcessIds -BinaryPath $BinaryPath) | ||
| if ($processIds.Count -eq 0) { | ||
| return $false | ||
| } | ||
|
|
||
| Write-Host "Detected a locked Tempyr install at $BinaryPath. Stopping matching processes: $($processIds -join ', ')" | ||
|
|
||
| foreach ($processId in $processIds) { | ||
| Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue | ||
| Wait-Process -Id $processId -Timeout 15 -ErrorAction SilentlyContinue | ||
| } | ||
|
|
||
| return @((Get-TargetProcessIds -BinaryPath $BinaryPath)).Count -eq 0 | ||
| } | ||
|
|
||
| function ConvertTo-CanonicalPathEntry { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Path | ||
| ) | ||
|
|
||
| if ([string]::IsNullOrWhiteSpace($Path)) { | ||
| return $null | ||
| } | ||
|
|
||
| $expandedPath = [Environment]::ExpandEnvironmentVariables($Path.Trim()) | ||
| return (Resolve-CanonicalPath -Path $expandedPath) | ||
| } | ||
|
|
||
| function ConvertTo-CanonicalPathEntrySafe { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Path | ||
| ) | ||
|
|
||
| try { | ||
| return ConvertTo-CanonicalPathEntry -Path $Path | ||
| } catch { | ||
| return $null | ||
| } | ||
| } | ||
|
|
||
| function Output-IndicatesLockError { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Output | ||
| ) | ||
|
|
||
| return $Output -match "being used by another process|cannot access the file|Access is denied" | ||
| } | ||
|
|
||
| function Broadcast-EnvironmentChange { | ||
| if (-not ("Tempyr.Win32.NativeMethods" -as [type])) { | ||
| Add-Type -TypeDefinition @" | ||
| using System; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Tempyr.Win32 { | ||
| public static class NativeMethods { | ||
| [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] | ||
| public static extern IntPtr SendMessageTimeout( | ||
| IntPtr hWnd, | ||
| int Msg, | ||
| IntPtr wParam, | ||
| string lParam, | ||
| int fuFlags, | ||
| int uTimeout, | ||
| out IntPtr lpdwResult); | ||
| } | ||
| } | ||
| "@ | ||
| } | ||
|
|
||
| $result = [IntPtr]::Zero | ||
| [void][Tempyr.Win32.NativeMethods]::SendMessageTimeout( | ||
| [IntPtr]0xffff, | ||
| 0x1A, | ||
| [IntPtr]::Zero, | ||
| "Environment", | ||
| 0x0002, | ||
| 5000, | ||
| [ref]$result | ||
| ) | ||
| } | ||
|
|
||
| function Ensure-UserPathContains { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$PathToAdd | ||
| ) | ||
|
|
||
| $normalizedTarget = ConvertTo-CanonicalPathEntry -Path $PathToAdd | ||
| $userPath = [Environment]::GetEnvironmentVariable("Path", "User") | ||
| $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine") | ||
| $persistedEntries = @() | ||
| foreach ($scopePath in @($userPath, $machinePath)) { | ||
| if (-not [string]::IsNullOrWhiteSpace($scopePath)) { | ||
| $persistedEntries += $scopePath.Split(';', [System.StringSplitOptions]::RemoveEmptyEntries) | ||
| } | ||
| } | ||
|
|
||
| $persistedPathContainsTarget = $false | ||
| foreach ($entry in $persistedEntries) { | ||
| $normalizedEntry = ConvertTo-CanonicalPathEntrySafe -Path $entry | ||
| if ($normalizedEntry -and [string]::Equals($normalizedEntry, $normalizedTarget, [System.StringComparison]::OrdinalIgnoreCase)) { | ||
| $persistedPathContainsTarget = $true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| $currentProcessPathContainsTarget = $false | ||
| foreach ($entry in ($env:Path -split ';')) { | ||
| $normalizedEntry = ConvertTo-CanonicalPathEntrySafe -Path $entry | ||
| if ($normalizedEntry -and [string]::Equals($normalizedEntry, $normalizedTarget, [System.StringComparison]::OrdinalIgnoreCase)) { | ||
| $currentProcessPathContainsTarget = $true | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if ($persistedPathContainsTarget) { | ||
| if (-not $currentProcessPathContainsTarget) { | ||
| $env:Path = "$normalizedTarget;$env:Path" | ||
| } | ||
| return $false | ||
| } | ||
|
|
||
| $updatedPath = if ([string]::IsNullOrWhiteSpace($userPath)) { | ||
| $normalizedTarget | ||
| } else { | ||
| "$($userPath.TrimEnd(';'));$normalizedTarget" | ||
| } | ||
|
|
||
| [Environment]::SetEnvironmentVariable("Path", $updatedPath, "User") | ||
| if (-not $currentProcessPathContainsTarget) { | ||
| $env:Path = "$normalizedTarget;$env:Path" | ||
| } | ||
| Broadcast-EnvironmentChange | ||
| return $true | ||
| } | ||
|
|
||
| function Get-CargoInstallFailureMessage { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [pscustomobject]$InstallResult | ||
| ) | ||
|
|
||
| $message = "cargo install failed with exit code $($InstallResult.ExitCode)." | ||
| if (-not [string]::IsNullOrWhiteSpace($InstallResult.Output)) { | ||
| $output = $InstallResult.Output.TrimEnd() | ||
| if (-not [string]::IsNullOrWhiteSpace($output)) { | ||
| $message = "$message`n$output" | ||
| } | ||
| } | ||
|
|
||
| return $message | ||
| } | ||
|
|
||
| function Invoke-CargoInstall { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$CratePath, | ||
| [Parameter(Mandatory)] | ||
| [string]$InstallRootPath | ||
| ) | ||
|
|
||
| $cargoExe = (Get-Command cargo -ErrorAction Stop).Source | ||
| $cargoArgs = @( | ||
| "install", | ||
| "--path", $CratePath, | ||
| "--root", $InstallRootPath, | ||
| "--locked", | ||
| "--force", | ||
| "--bin", "tempyr" | ||
| ) | ||
| $stdoutFile = New-TemporaryFile | ||
| $stderrFile = New-TemporaryFile | ||
| try { | ||
| & $cargoExe @cargoArgs 1> $stdoutFile.FullName 2> $stderrFile.FullName | ||
| $exitCode = $LASTEXITCODE | ||
|
|
||
| $stdoutLines = @() | ||
| $stderrLines = @() | ||
| if ((Get-Item -LiteralPath $stdoutFile.FullName).Length -gt 0) { | ||
| $stdoutLines = @(Get-Content -LiteralPath $stdoutFile.FullName) | ||
| } | ||
| if ((Get-Item -LiteralPath $stderrFile.FullName).Length -gt 0) { | ||
| $stderrLines = @(Get-Content -LiteralPath $stderrFile.FullName) | ||
| } | ||
|
|
||
| foreach ($line in ($stderrLines + $stdoutLines)) { | ||
| Write-Host $line | ||
| } | ||
|
|
||
| $output = ($stderrLines + $stdoutLines) -join [Environment]::NewLine | ||
| return [pscustomobject]@{ | ||
| ExitCode = $exitCode | ||
| Output = $output | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } finally { | ||
| Remove-Item -LiteralPath $stdoutFile.FullName -Force -ErrorAction SilentlyContinue | ||
| Remove-Item -LiteralPath $stderrFile.FullName -Force -ErrorAction SilentlyContinue | ||
| } | ||
| } | ||
|
|
||
| if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { | ||
| throw "cargo is required but was not found in PATH." | ||
| } | ||
|
|
||
| $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path | ||
| $cratePath = Join-Path $scriptRoot "crates/tempyr-cli" | ||
| $binDir = Join-Path $InstallRoot "bin" | ||
| $targetBinary = Join-Path $binDir "tempyr.exe" | ||
|
|
||
| if (-not (Test-Path -LiteralPath (Join-Path $cratePath "Cargo.toml"))) { | ||
| throw "Could not find crates/tempyr-cli/Cargo.toml relative to $scriptRoot." | ||
| } | ||
|
|
||
| $installResult = Invoke-CargoInstall -CratePath $cratePath -InstallRootPath $InstallRoot | ||
| if ($installResult.ExitCode -ne 0) { | ||
| if ( | ||
| (Test-FileLocked -Path $targetBinary) -and | ||
| (Output-IndicatesLockError -Output $installResult.Output) -and | ||
| (Stop-TargetProcesses -BinaryPath $targetBinary) | ||
| ) { | ||
| Write-Host "Retrying cargo install after stopping matching Tempyr processes..." | ||
| $installResult = Invoke-CargoInstall -CratePath $cratePath -InstallRootPath $InstallRoot | ||
| } | ||
| } | ||
|
|
||
| if ($installResult.ExitCode -ne 0) { | ||
| throw (Get-CargoInstallFailureMessage -InstallResult $installResult) | ||
| } | ||
|
|
||
| if (-not $NoPathUpdate) { | ||
| $addedToPath = Ensure-UserPathContains -PathToAdd $binDir | ||
| } | ||
|
|
||
| Write-Host "" | ||
| Write-Host "Tempyr installed to $targetBinary" | ||
| if (-not $NoPathUpdate) { | ||
| if ($addedToPath) { | ||
| Write-Host "Added $binDir to the user PATH." | ||
| } else { | ||
| Write-Host "$binDir is already present in PATH." | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.