Skip to content

Commit 2d2d589

Browse files
Merge pull request #18 from Palbahngmiyine/worktree-delegated-growing-pearl
feat(install): add rootless Windows installer (PowerShell)
2 parents a6cc6c2 + bca72fe commit 2d2d589

2 files changed

Lines changed: 218 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,49 @@ Linux와 macOS를 우선 지원합니다.
1616

1717
## 설치
1818

19-
### 스크립트 설치 (Linux / macOS)
19+
모든 설치 방법은 관리자 권한 없이 **사용자 영역**에 설치됩니다.
20+
21+
### Linux / macOS
2022

2123
```bash
2224
curl -fsSL https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.sh | bash
2325
```
2426

25-
`~/.local/bin`에 설치됩니다. PATH에 포함되어 있지 않으면 안내 메시지가 출력됩니다.
27+
- 설치 경로: `~/.local/bin/solactl`
28+
- 체크섬(SHA256) 검증 후 압축 해제
29+
- `PATH`에 포함되어 있지 않으면 셸 설정 파일(`~/.zshrc` / `~/.bashrc`) 등록 안내 출력
30+
31+
### Windows (PowerShell)
32+
33+
winget처럼 사용자 영역에만 설치되며 관리자 권한이 필요하지 않습니다. PowerShell 5.1 이상(또는 PowerShell 7+) 에서 실행하세요.
34+
35+
```powershell
36+
irm https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 | iex
37+
```
38+
39+
- 설치 경로: `%LOCALAPPDATA%\Programs\solactl\solactl.exe`
40+
- 체크섬(SHA256) 검증 후 zip 압축 해제
41+
- 사용자 `PATH`(`HKCU\Environment\Path`) 에 설치 디렉터리를 자동 추가 — 새 터미널부터 적용
42+
- 실행 중인 `solactl.exe` 가 잠겨 있으면 기존 파일을 `solactl.exe.old` 로 옮긴 뒤 교체
43+
44+
#### 옵션
45+
46+
특정 버전 고정 / 설치 경로 지정이 필요하면 스크립트를 로컬에 받아 인자로 실행합니다.
47+
48+
```powershell
49+
# 스크립트 다운로드 후 실행
50+
Invoke-WebRequest -UseBasicParsing `
51+
-Uri https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 `
52+
-OutFile $env:TEMP\install.ps1
53+
54+
# 특정 버전 설치
55+
powershell -ExecutionPolicy Bypass -File $env:TEMP\install.ps1 -Version v0.1.6
56+
57+
# 설치 경로 변경
58+
powershell -ExecutionPolicy Bypass -File $env:TEMP\install.ps1 -InstallDir D:\tools\solactl
59+
```
60+
61+
> `irm | iex` 한 줄 설치는 메모리에서 실행되므로 별도의 ExecutionPolicy 설정이 필요하지 않습니다.
2662
2763
### 소스 빌드
2864

@@ -35,10 +71,14 @@ make install # $GOPATH/bin에 설치
3571

3672
### 업그레이드
3773

74+
설치된 `solactl` 자체에서 업그레이드할 수 있습니다. 모든 플랫폼 공통입니다.
75+
3876
```bash
3977
solactl upgrade
4078
```
4179

80+
또는 위의 설치 스크립트를 다시 실행해도 됩니다.
81+
4282
## 사용법
4383

4484
```bash

‎scripts/install.ps1‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#Requires -Version 5.1
2+
<#
3+
.SYNOPSIS
4+
Installs solactl for the current user on Windows (no admin rights required).
5+
6+
.DESCRIPTION
7+
Downloads the latest solactl release for Windows, verifies its SHA256 checksum,
8+
extracts the binary to %LOCALAPPDATA%\Programs\solactl, and ensures that location
9+
is on the user's PATH. Mirrors the behavior of scripts/install.sh for Linux/macOS.
10+
11+
.PARAMETER InstallDir
12+
Override the install directory. Defaults to %LOCALAPPDATA%\Programs\solactl.
13+
14+
.PARAMETER Version
15+
Install a specific tag (e.g. "v0.1.6") instead of the latest release.
16+
17+
.EXAMPLE
18+
irm https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 | iex
19+
20+
.EXAMPLE
21+
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Version v0.1.6
22+
#>
23+
24+
[CmdletBinding()]
25+
param(
26+
[string] $InstallDir = (Join-Path $env:LOCALAPPDATA 'Programs\solactl'),
27+
[string] $Version = ''
28+
)
29+
30+
$ErrorActionPreference = 'Stop'
31+
$ProgressPreference = 'SilentlyContinue'
32+
33+
# GitHub requires TLS 1.2+; older PowerShell defaults to SSL3/TLS1.0.
34+
try {
35+
[Net.ServicePointManager]::SecurityProtocol = `
36+
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
37+
} catch {
38+
# PowerShell Core ignores this; safe to skip.
39+
}
40+
41+
$Repo = 'solapi/solactl'
42+
43+
function Die([string] $Message) {
44+
Write-Host "ERROR: $Message" -ForegroundColor Red
45+
exit 1
46+
}
47+
48+
function Invoke-Download([string] $Uri, [string] $OutFile) {
49+
try {
50+
Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing -MaximumRedirection 5
51+
} catch {
52+
Die "Download failed: $Uri`n$($_.Exception.Message)"
53+
}
54+
}
55+
56+
# --- 1. Detect architecture --------------------------------------------------
57+
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
58+
'AMD64' { 'amd64' }
59+
'ARM64' { 'arm64' }
60+
'x86' { Die 'Unsupported architecture: x86 (32-bit). solactl ships amd64/arm64 only.' }
61+
default { Die "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
62+
}
63+
64+
# --- 2. Resolve target tag ---------------------------------------------------
65+
if ([string]::IsNullOrWhiteSpace($Version)) {
66+
Write-Host 'Checking latest version...'
67+
try {
68+
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -UseBasicParsing
69+
} catch {
70+
Die "Failed to fetch release info: $($_.Exception.Message)"
71+
}
72+
$tag = $release.tag_name
73+
if ([string]::IsNullOrWhiteSpace($tag)) { Die 'Failed to parse release tag.' }
74+
} else {
75+
$tag = $Version
76+
}
77+
Write-Host "Target version: $tag"
78+
79+
$versionNumber = $tag.TrimStart('v')
80+
$archiveName = "solactl_${versionNumber}_windows_${arch}.zip"
81+
$downloadUrl = "https://github.com/$Repo/releases/download/$tag/$archiveName"
82+
$checksumsUrl = "https://github.com/$Repo/releases/download/$tag/checksums.txt"
83+
84+
# --- 3. Download to a temp directory ----------------------------------------
85+
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("solactl-install-" + [System.Guid]::NewGuid().ToString())
86+
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
87+
88+
try {
89+
$archivePath = Join-Path $tmpDir $archiveName
90+
$checksumsPath = Join-Path $tmpDir 'checksums.txt'
91+
92+
Write-Host "Downloading $archiveName..."
93+
Invoke-Download -Uri $downloadUrl -OutFile $archivePath
94+
95+
Write-Host 'Downloading checksums...'
96+
Invoke-Download -Uri $checksumsUrl -OutFile $checksumsPath
97+
98+
# --- 4. Verify SHA256 ----------------------------------------------------
99+
Write-Host 'Verifying checksum...'
100+
$expectedHash = $null
101+
foreach ($line in Get-Content -LiteralPath $checksumsPath) {
102+
$parts = ($line.Trim()) -split '\s+', 2
103+
if ($parts.Length -eq 2 -and $parts[1] -eq $archiveName) {
104+
$expectedHash = $parts[0].ToLowerInvariant()
105+
break
106+
}
107+
}
108+
if (-not $expectedHash) { Die "Checksum not found for $archiveName in checksums.txt" }
109+
110+
$actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
111+
if ($expectedHash -ne $actualHash) {
112+
Die "Checksum mismatch: expected $expectedHash, got $actualHash. File may be tampered."
113+
}
114+
Write-Host 'Checksum verified.'
115+
116+
# --- 5. Extract & install -----------------------------------------------
117+
Write-Host 'Extracting...'
118+
$extractDir = Join-Path $tmpDir 'extract'
119+
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDir -Force
120+
121+
$binary = Join-Path $extractDir 'solactl.exe'
122+
if (-not (Test-Path -LiteralPath $binary)) {
123+
Die 'solactl.exe not found in archive.'
124+
}
125+
126+
if (-not (Test-Path -LiteralPath $InstallDir)) {
127+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
128+
}
129+
130+
$dest = Join-Path $InstallDir 'solactl.exe'
131+
try {
132+
Move-Item -LiteralPath $binary -Destination $dest -Force
133+
} catch {
134+
# Existing binary is likely in use by a running process.
135+
# Park the old one so the install still succeeds.
136+
$stash = "$dest.old"
137+
if (Test-Path -LiteralPath $stash) { Remove-Item -LiteralPath $stash -Force }
138+
Move-Item -LiteralPath $dest -Destination $stash -Force
139+
Move-Item -LiteralPath $binary -Destination $dest -Force
140+
Write-Host "Previous binary was in use; moved aside to $stash"
141+
}
142+
143+
Write-Host ''
144+
Write-Host "Installed solactl $tag"
145+
Write-Host "Location: $dest"
146+
Write-Host ''
147+
148+
# --- 6. Ensure InstallDir is on the user PATH ---------------------------
149+
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
150+
$entries = @()
151+
if ($userPath) { $entries = $userPath -split ';' | Where-Object { $_ -ne '' } }
152+
153+
$alreadyOnPath = $false
154+
foreach ($entry in $entries) {
155+
if ([string]::Equals($entry.TrimEnd('\'), $InstallDir.TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase)) {
156+
$alreadyOnPath = $true
157+
break
158+
}
159+
}
160+
161+
if (-not $alreadyOnPath) {
162+
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
163+
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
164+
# Update current session too so the user can run solactl without reopening the shell.
165+
$env:Path = "$env:Path;$InstallDir"
166+
Write-Host "Added $InstallDir to user PATH."
167+
Write-Host 'Open a new terminal for the PATH change to apply to other shells.'
168+
} else {
169+
Write-Host "$InstallDir is already on user PATH."
170+
}
171+
172+
Write-Host ''
173+
Write-Host 'To upgrade later: solactl upgrade'
174+
} finally {
175+
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
176+
}

0 commit comments

Comments
 (0)