Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/install.md
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`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
```
332 changes: 332 additions & 0 deletions install.ps1
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
}
Comment thread
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
Comment thread
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."
}
}
Loading