diff --git a/install.ps1 b/install.ps1 index ad0f614..93aea33 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [string]$InstallDir = (Join-Path $HOME "ToCode"), + [string]$InstallDir, [string]$Repo = "https://github.com/buzzer-re/ToCode.git", [string]$Branch = "main", [switch]$Dev @@ -9,17 +9,63 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +$script:PathWasUpdated = $false + +# When run from an existing ToCode checkout, install from it instead of +# cloning a second copy. $PSScriptRoot is empty when piped through iex. +$scriptDir = $PSScriptRoot +if (-not $InstallDir) { + if ($scriptDir -and (Test-Path (Join-Path $scriptDir ".git")) -and (Test-Path (Join-Path $scriptDir "pyproject.toml"))) { + $InstallDir = $scriptDir + } + else { + $InstallDir = Join-Path $HOME "ToCode" + } +} + function Write-Step { param([string]$Message) Write-Host "==> $Message" } function Fail { - param([string]$Message) - Write-Error "install.ps1: $Message" + param( + [string]$Message, + [string[]]$Hints = @() + ) + Write-Host "" + Write-Host "ToCode was not installed: $Message" -ForegroundColor Red + foreach ($hint in $Hints) { + Write-Host " $hint" -ForegroundColor Yellow + } exit 1 } +function Assert-NativeSuccess { + param( + [string]$Message, + [string[]]$Hints = @() + ) + if ($LASTEXITCODE -ne 0) { + Fail "$Message (exit code $LASTEXITCODE)." $Hints + } +} + +# Runs a native command with stderr suppressed. Under +# $ErrorActionPreference = "Stop", Windows PowerShell turns redirected native +# stderr into a terminating NativeCommandError, so relax the preference first. +function Invoke-Quiet { + param([scriptblock]$Command) + $previous = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & $Command 2>$null + } + finally { + $ErrorActionPreference = $previous + } +} + function Test-Command { param([string]$Name) return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) @@ -38,16 +84,16 @@ function Get-PythonCommand { continue } - $args = @() + $extraArgs = @() if ($candidate.Count -gt 1) { - $args = $candidate[1..($candidate.Count - 1)] + $extraArgs = $candidate[1..($candidate.Count - 1)] } - & $name @args -c "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)" 2>$null + Invoke-Quiet { & $name @extraArgs -c "import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)" } | Out-Null if ($LASTEXITCODE -eq 0) { return @{ Name = $name - Args = $args + Args = $extraArgs } } } @@ -98,30 +144,47 @@ function Add-UserPath { if ($missingFromPersistentPath) { $newUserPath = if ($userPath) { "$userPath;$BinDir" } else { $BinDir } [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + $script:PathWasUpdated = $true Write-Step "Added $BinDir to your user Path" - Write-Host "Open a new PowerShell or cmd session before running tocode outside this installer." } } if (-not (Test-Command "git")) { - Fail "git is required but was not found on PATH" + Fail "Git was not found on PATH." @( + "Install Git for Windows from https://git-scm.com/download/win, then run this installer again." + ) } $gitDir = Join-Path $InstallDir ".git" if (Test-Path $gitDir) { - Write-Step "Updating ToCode at $InstallDir" - git -C $InstallDir fetch origin $Branch - git -C $InstallDir checkout $Branch - git -C $InstallDir pull --ff-only origin $Branch + if ($scriptDir -and ((Resolve-Path $InstallDir).Path.TrimEnd('\') -ieq (Resolve-Path $scriptDir).Path.TrimEnd('\'))) { + Write-Step "Installing ToCode from this checkout at $InstallDir" + } + else { + Write-Step "Updating ToCode at $InstallDir" + git -C $InstallDir fetch origin $Branch + Assert-NativeSuccess "could not fetch branch '$Branch' from origin" + git -C $InstallDir checkout $Branch + Assert-NativeSuccess "could not check out branch '$Branch' in $InstallDir" + git -C $InstallDir pull --ff-only origin $Branch + Assert-NativeSuccess "could not update the checkout at $InstallDir" @( + "The checkout may have local changes. Resolve them, or remove the directory and run this installer again." + ) + } } elseif (Test-Path $InstallDir) { - Fail "$InstallDir already exists and is not a Git checkout" + Fail "$InstallDir already exists and is not a ToCode Git checkout." @( + "Move or delete it, or pick another location with: .\install.ps1 -InstallDir " + ) } else { Write-Step "Cloning ToCode into $InstallDir" git clone --branch $Branch $Repo $InstallDir + Assert-NativeSuccess "could not clone $Repo" } +$binDir = $null + if (Test-Command "uv") { Write-Step "Syncing local project environment with uv" if ($Dev) { @@ -130,21 +193,24 @@ if (Test-Command "uv") { else { uv --directory $InstallDir sync --locked } + Assert-NativeSuccess "uv could not sync the project environment" Write-Step "Installing the tocode command with uv" uv tool install --force --editable $InstallDir + Assert-NativeSuccess "uv could not install the tocode command" - if (-not (Test-Command "tocode")) { - $toolBin = (& uv tool dir --bin 2>$null) - if ($LASTEXITCODE -eq 0 -and $toolBin) { - Add-UserPath -BinDir $toolBin - } + $toolBin = Invoke-Quiet { uv tool dir --bin } + if ($LASTEXITCODE -eq 0 -and $toolBin) { + $binDir = "$toolBin".Trim() } } else { $python = Get-PythonCommand if ($null -eq $python) { - Fail "Python 3.10 or newer is required when uv is not installed" + Fail "neither uv nor Python 3.10+ was found on PATH." @( + "Install uv from https://docs.astral.sh/uv/getting-started/installation/ (recommended),", + "or install Python 3.10 or newer from https://www.python.org/downloads/, then run this installer again." + ) } Write-Step "Installing the tocode command with pip" @@ -153,18 +219,55 @@ else { $package = "$InstallDir[dev]" } & $python.Name @($python.Args) -m pip install --user --editable $package + Assert-NativeSuccess "pip could not install ToCode" + + # The per-user scripts directory is versioned on Windows (for example + # %APPDATA%\Python\Python312\Scripts), so ask Python for the real path + # instead of assuming USER_BASE\Scripts. + $scriptsDir = Invoke-Quiet { & $python.Name @($python.Args) -c "import sysconfig; print(sysconfig.get_path('scripts', sysconfig.get_preferred_scheme('user')))" } + if ($LASTEXITCODE -eq 0 -and $scriptsDir) { + $binDir = "$scriptsDir".Trim() + } +} + +if ($binDir) { + Add-UserPath -BinDir $binDir +} + +if (Test-Command "tocode") { + tocode --help | Out-Null + Assert-NativeSuccess "tocode is installed but 'tocode --help' failed" @( + "Check the output above, or re-run this installer." + ) - $userBase = (& $python.Name @($python.Args) -c "import site; print(site.USER_BASE)") - if ($LASTEXITCODE -eq 0 -and $userBase) { - Add-UserPath -BinDir (Join-Path $userBase 'Scripts') + Write-Host "" + Write-Host "ToCode is installed." -ForegroundColor Green + if ($script:PathWasUpdated) { + Write-Host "Your PATH was updated for future sessions. If 'tocode' is not recognized in an already-open terminal, open a new one." } + Write-Host "Run: tocode -o " + exit 0 } -if (-not (Test-Command "tocode")) { - Fail "tocode was installed, but its bin directory is not on PATH" +$tocodeExe = $null +if ($binDir) { + $tocodeExe = Join-Path $binDir "tocode.exe" } -tocode --help | Out-Null +if ($tocodeExe -and (Test-Path $tocodeExe)) { + Write-Host "" + Write-Host "ToCode is installed." -ForegroundColor Green + Write-Host "The tocode command lives in $binDir, which was added to your PATH, but this terminal does not pick up the change automatically." + Write-Host "Open a new PowerShell window, then run: tocode -o " + exit 0 +} -Write-Step "ToCode is installed" -Write-Host "Run: tocode -o " +$hints = @() +if ($binDir) { + $hints += "Expected it in $binDir - check that directory and add it to your PATH if it is there." +} +else { + $hints += "Could not determine the tool bin directory. Add the directory containing tocode.exe to your PATH manually." +} +$hints += "You can also run ToCode without PATH changes: uv --directory $InstallDir run tocode --help" +Fail "the install finished, but the tocode command could not be located." $hints diff --git a/install.sh b/install.sh index 3369b07..8a4e82f 100644 --- a/install.sh +++ b/install.sh @@ -4,6 +4,7 @@ set -euo pipefail repo_url="${TOCODE_REPO_URL:-https://github.com/buzzer-re/ToCode.git}" branch="${TOCODE_BRANCH:-main}" with_dev=false +tocode_bin_dir="" script_dir="$(cd "$(dirname "$0")" && pwd)" if [ -n "${TOCODE_INSTALL_DIR:-}" ]; then @@ -35,7 +36,11 @@ info() { } die() { - printf 'install.sh: %s\n' "$1" >&2 + printf '\nToCode was not installed: %s\n' "$1" >&2 + shift + for hint in "$@"; do + printf ' %s\n' "$hint" >&2 + done exit 1 } @@ -129,7 +134,8 @@ while [ "$#" -gt 0 ]; do esac done -command -v git >/dev/null 2>&1 || die "git is required but was not found on PATH" +command -v git >/dev/null 2>&1 || die "git was not found on PATH" \ + "Install it with your package manager (for example: apt install git, brew install git), then run this installer again." if [ -d "$install_dir/.git" ]; then if [ "$install_dir" = "$script_dir" ]; then @@ -141,7 +147,8 @@ if [ -d "$install_dir/.git" ]; then git -C "$install_dir" pull --ff-only origin "$branch" fi elif [ -e "$install_dir" ]; then - die "$install_dir already exists and is not a Git checkout" + die "$install_dir already exists and is not a ToCode Git checkout" \ + "Move or delete it, or pick another location with: ./install.sh --dir PATH" else info "Cloning ToCode into $install_dir" git clone --branch "$branch" "$repo_url" "$install_dir" @@ -158,16 +165,20 @@ if command -v uv >/dev/null 2>&1; then info "Installing the tocode command with uv" uv tool install --force --editable "$install_dir" + tocode_bin_dir="$(uv tool dir --bin 2>/dev/null || true)" if ! command -v tocode >/dev/null 2>&1; then - tool_bin="$(uv tool dir --bin 2>/dev/null || true)" - ensure_path "$tool_bin" + ensure_path "$tocode_bin_dir" fi else python_bin="$(find_python || true)" - [ -n "$python_bin" ] || die "Python 3.10 or newer is required when uv is not installed" + [ -n "$python_bin" ] || die "neither uv nor Python 3.10+ was found on PATH" \ + "Install uv from https://docs.astral.sh/uv/getting-started/installation/ (recommended)," \ + "or install Python 3.10 or newer, then run this installer again." "$python_bin" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)' \ - || die "Python 3.10 or newer is required" + || die "$python_bin is older than Python 3.10" \ + "Install uv from https://docs.astral.sh/uv/getting-started/installation/ (recommended)," \ + "or install Python 3.10 or newer, then run this installer again." info "Installing the tocode command with pip" if [ "$with_dev" = true ]; then @@ -176,12 +187,26 @@ else "$python_bin" -m pip install --user --editable "$install_dir" fi - user_bin="$("$python_bin" -c 'import os, site; print(os.path.join(site.USER_BASE, "bin"))')" - ensure_path "$user_bin" + tocode_bin_dir="$("$python_bin" -c 'import sysconfig; print(sysconfig.get_path("scripts", sysconfig.get_preferred_scheme("user")))' 2>/dev/null \ + || "$python_bin" -c 'import os, site; print(os.path.join(site.USER_BASE, "bin"))')" + ensure_path "$tocode_bin_dir" fi -command -v tocode >/dev/null 2>&1 || die "tocode was installed, but its bin directory is not on PATH" -tocode --help >/dev/null - -info "ToCode is installed" -printf 'Run: tocode -o \n' +if command -v tocode >/dev/null 2>&1; then + tocode --help >/dev/null \ + || die "tocode is installed but 'tocode --help' failed" \ + "Check the output above, or run this installer again." + info "ToCode is installed" + printf 'Run: tocode -o \n' +elif [ -n "$tocode_bin_dir" ] && [ -x "$tocode_bin_dir/tocode" ]; then + info "ToCode is installed" + printf 'The tocode command lives in %s, which is not visible in this shell yet.\n' "$tocode_bin_dir" + printf 'Open a new shell session (or run: source %s), then run: tocode -o \n' "$(shell_rc_file)" +else + if [ -n "$tocode_bin_dir" ]; then + die "the install finished, but the tocode command could not be located" \ + "Expected it in $tocode_bin_dir - check that directory and add it to your PATH if it is there." + fi + die "the install finished, but the tocode command could not be located" \ + "Add the directory containing the tocode command to your PATH manually." +fi