install.ps1 copies bash.exe to %LOCALAPPDATA%\Programs\BashForWindows, adds it to the user PATH, and injects a "Bash for Windows" profile into Windows Terminal's settings.json so the shell appears in the + dropdown. install.bat is a double-click wrapper that bypasses the PS execution policy. build.sh --release produces a release/ folder ready to zip and distribute. Supports -Uninstall flag to cleanly remove everything. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
217 lines
8.3 KiB
PowerShell
217 lines
8.3 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Installs bash-for-windows and registers it as a Windows Terminal profile.
|
|
.DESCRIPTION
|
|
Copies bash.exe to a permanent location, adds it to the user PATH,
|
|
and adds a "Bash for Windows" entry to Windows Terminal so it appears
|
|
in the new-tab dropdown.
|
|
.EXAMPLE
|
|
.\install.ps1
|
|
.\install.ps1 -InstallDir "$env:APPDATA\BashForWindows"
|
|
.\install.ps1 -NoPath -NoTerminal
|
|
.\install.ps1 -Uninstall
|
|
#>
|
|
|
|
param(
|
|
[string]$InstallDir = "$env:LOCALAPPDATA\Programs\BashForWindows",
|
|
[switch]$NoPath,
|
|
[switch]$NoTerminal,
|
|
[switch]$Uninstall
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$ExeName = "bash.exe"
|
|
$ProfileGuid = "{7a86baf2-f5b9-4b78-b3f8-5e1e4f3d9b2a}"
|
|
$ProfileName = "Bash for Windows"
|
|
|
|
# ── Pretty output helpers ─────────────────────────────────────────────────────
|
|
|
|
function Write-Header {
|
|
Write-Host ""
|
|
Write-Host " bash-for-windows" -ForegroundColor White -NoNewline
|
|
Write-Host " installer" -ForegroundColor DarkGray
|
|
Write-Host " ─────────────────────────────────────────" -ForegroundColor DarkGray
|
|
}
|
|
|
|
function Write-Step { param([string]$s) Write-Host "`n ▸ $s" -ForegroundColor Cyan }
|
|
function Write-Ok { param([string]$s) Write-Host " ✓ $s" -ForegroundColor Green }
|
|
function Write-Warn { param([string]$s) Write-Host " ⚠ $s" -ForegroundColor Yellow }
|
|
function Write-Fatal { param([string]$s) Write-Host " ✗ $s" -ForegroundColor Red; exit 1 }
|
|
|
|
# ── Locate source bash.exe (must live next to this script) ────────────────────
|
|
|
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
$SourceExe = Join-Path $ScriptDir $ExeName
|
|
|
|
if (-not (Test-Path $SourceExe)) {
|
|
Write-Header
|
|
Write-Fatal "'$ExeName' not found next to this script. Expected: $SourceExe"
|
|
}
|
|
|
|
# ── Windows Terminal settings.json locations ──────────────────────────────────
|
|
|
|
function Get-WTSettingsPath {
|
|
@(
|
|
"$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json",
|
|
"$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json",
|
|
"$env:LOCALAPPDATA\Microsoft\Windows Terminal\settings.json"
|
|
) | Where-Object { Test-Path $_ } | Select-Object -First 1
|
|
}
|
|
|
|
# ── PATH helpers ──────────────────────────────────────────────────────────────
|
|
|
|
function Add-ToUserPath {
|
|
param([string]$Dir)
|
|
$current = [Environment]::GetEnvironmentVariable("PATH", "User") -split ";" |
|
|
Where-Object { $_ -ne "" }
|
|
if ($Dir -in $current) {
|
|
Write-Warn "Already in user PATH: $Dir"
|
|
return
|
|
}
|
|
[Environment]::SetEnvironmentVariable("PATH", (($current + $Dir) -join ";"), "User")
|
|
Write-Ok "Added to user PATH: $Dir"
|
|
Write-Warn "Re-open your terminal for the PATH change to take effect."
|
|
}
|
|
|
|
function Remove-FromUserPath {
|
|
param([string]$Dir)
|
|
$current = [Environment]::GetEnvironmentVariable("PATH", "User") -split ";" |
|
|
Where-Object { $_ -ne "" -and $_ -ne $Dir }
|
|
[Environment]::SetEnvironmentVariable("PATH", ($current -join ";"), "User")
|
|
Write-Ok "Removed from user PATH: $Dir"
|
|
}
|
|
|
|
# ── Windows Terminal profile helpers ──────────────────────────────────────────
|
|
|
|
function Register-WTProfile {
|
|
param([string]$ExePath)
|
|
|
|
$settingsPath = Get-WTSettingsPath
|
|
if (-not $settingsPath) {
|
|
Write-Warn "Windows Terminal not found — skipping profile registration."
|
|
Write-Warn "Install Windows Terminal from the Microsoft Store and re-run the installer."
|
|
return
|
|
}
|
|
|
|
try {
|
|
$raw = Get-Content $settingsPath -Raw -Encoding UTF8
|
|
$json = $raw | ConvertFrom-Json
|
|
} catch {
|
|
Write-Warn "Could not parse settings.json: $_"
|
|
return
|
|
}
|
|
|
|
# Build the profile object
|
|
$newProfile = [PSCustomObject]@{
|
|
guid = $ProfileGuid
|
|
name = $ProfileName
|
|
commandline = $ExePath
|
|
startingDirectory = "%USERPROFILE%"
|
|
hidden = $false
|
|
}
|
|
|
|
# Ensure profiles.list exists
|
|
if (-not $json.PSObject.Properties["profiles"]) {
|
|
Add-Member -InputObject $json -MemberType NoteProperty `
|
|
-Name "profiles" -Value ([PSCustomObject]@{ list = @() })
|
|
}
|
|
if (-not $json.profiles.PSObject.Properties["list"]) {
|
|
Add-Member -InputObject $json.profiles -MemberType NoteProperty `
|
|
-Name "list" -Value @() -Force
|
|
}
|
|
|
|
$list = @($json.profiles.list)
|
|
|
|
# Find existing entry by GUID
|
|
$existingIdx = -1
|
|
for ($i = 0; $i -lt $list.Count; $i++) {
|
|
if ($list[$i].guid -eq $ProfileGuid) { $existingIdx = $i; break }
|
|
}
|
|
|
|
if ($existingIdx -ge 0) {
|
|
$list[$existingIdx] = $newProfile
|
|
Write-Ok "Updated existing '$ProfileName' profile."
|
|
} else {
|
|
# Prepend so it appears near the top of the dropdown
|
|
$list = @($newProfile) + $list
|
|
Write-Ok "Added '$ProfileName' profile."
|
|
}
|
|
|
|
$json.profiles.list = $list
|
|
|
|
# Write back — high depth so nested keybindings etc. aren't truncated
|
|
$updated = $json | ConvertTo-Json -Depth 50
|
|
[System.IO.File]::WriteAllText($settingsPath, $updated, [System.Text.UTF8Encoding]::new($false))
|
|
Write-Ok "Saved settings: $settingsPath"
|
|
Write-Ok "Open Windows Terminal, click '+', and select '$ProfileName'."
|
|
}
|
|
|
|
function Remove-WTProfile {
|
|
$settingsPath = Get-WTSettingsPath
|
|
if (-not $settingsPath) { return }
|
|
|
|
try {
|
|
$json = Get-Content $settingsPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
} catch { return }
|
|
|
|
if (-not $json.profiles -or -not $json.profiles.list) { return }
|
|
|
|
$list = @($json.profiles.list) | Where-Object { $_.guid -ne $ProfileGuid }
|
|
$json.profiles.list = $list
|
|
$updated = $json | ConvertTo-Json -Depth 50
|
|
[System.IO.File]::WriteAllText($settingsPath, $updated, [System.Text.UTF8Encoding]::new($false))
|
|
Write-Ok "Removed '$ProfileName' from Windows Terminal."
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# UNINSTALL
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
if ($Uninstall) {
|
|
Write-Header
|
|
Write-Step "Removing files"
|
|
if (Test-Path $InstallDir) {
|
|
Remove-Item $InstallDir -Recurse -Force
|
|
Write-Ok "Deleted: $InstallDir"
|
|
} else {
|
|
Write-Warn "Not found (already removed?): $InstallDir"
|
|
}
|
|
|
|
Write-Step "Removing from PATH"
|
|
Remove-FromUserPath $InstallDir
|
|
|
|
Write-Step "Removing Windows Terminal profile"
|
|
Remove-WTProfile
|
|
|
|
Write-Host "`n Uninstall complete." -ForegroundColor Green
|
|
Write-Host ""
|
|
exit 0
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# INSTALL
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
Write-Header
|
|
|
|
Write-Step "Installing binary"
|
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
$DestExe = Join-Path $InstallDir $ExeName
|
|
Copy-Item -Path $SourceExe -Destination $DestExe -Force
|
|
Write-Ok "Copied to: $DestExe"
|
|
|
|
if (-not $NoPath) {
|
|
Write-Step "Updating user PATH"
|
|
Add-ToUserPath $InstallDir
|
|
}
|
|
|
|
if (-not $NoTerminal) {
|
|
Write-Step "Registering Windows Terminal profile"
|
|
Register-WTProfile $DestExe
|
|
}
|
|
|
|
Write-Host "`n Install complete." -ForegroundColor Green
|
|
Write-Host ""
|