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
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,49 @@ Linux와 macOS를 우선 지원합니다.

## 설치

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

### Linux / macOS

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

`~/.local/bin`에 설치됩니다. PATH에 포함되어 있지 않으면 안내 메시지가 출력됩니다.
- 설치 경로: `~/.local/bin/solactl`
- 체크섬(SHA256) 검증 후 압축 해제
- `PATH`에 포함되어 있지 않으면 셸 설정 파일(`~/.zshrc` / `~/.bashrc`) 등록 안내 출력

### Windows (PowerShell)

winget처럼 사용자 영역에만 설치되며 관리자 권한이 필요하지 않습니다. PowerShell 5.1 이상(또는 PowerShell 7+) 에서 실행하세요.

```powershell
irm https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 | iex
```

- 설치 경로: `%LOCALAPPDATA%\Programs\solactl\solactl.exe`
- 체크섬(SHA256) 검증 후 zip 압축 해제
- 사용자 `PATH`(`HKCU\Environment\Path`) 에 설치 디렉터리를 자동 추가 — 새 터미널부터 적용
- 실행 중인 `solactl.exe` 가 잠겨 있으면 기존 파일을 `solactl.exe.old` 로 옮긴 뒤 교체

#### 옵션

특정 버전 고정 / 설치 경로 지정이 필요하면 스크립트를 로컬에 받아 인자로 실행합니다.

```powershell
# 스크립트 다운로드 후 실행
Invoke-WebRequest -UseBasicParsing `
-Uri https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 `
-OutFile $env:TEMP\install.ps1
# 특정 버전 설치
powershell -ExecutionPolicy Bypass -File $env:TEMP\install.ps1 -Version v0.1.6
# 설치 경로 변경
powershell -ExecutionPolicy Bypass -File $env:TEMP\install.ps1 -InstallDir D:\tools\solactl
```

> `irm | iex` 한 줄 설치는 메모리에서 실행되므로 별도의 ExecutionPolicy 설정이 필요하지 않습니다.
### 소스 빌드

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

### 업그레이드

설치된 `solactl` 자체에서 업그레이드할 수 있습니다. 모든 플랫폼 공통입니다.

```bash
solactl upgrade
```

또는 위의 설치 스크립트를 다시 실행해도 됩니다.

## 사용법

```bash
Expand Down
176 changes: 176 additions & 0 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Installs solactl for the current user on Windows (no admin rights required).

.DESCRIPTION
Downloads the latest solactl release for Windows, verifies its SHA256 checksum,
extracts the binary to %LOCALAPPDATA%\Programs\solactl, and ensures that location
is on the user's PATH. Mirrors the behavior of scripts/install.sh for Linux/macOS.

.PARAMETER InstallDir
Override the install directory. Defaults to %LOCALAPPDATA%\Programs\solactl.

.PARAMETER Version
Install a specific tag (e.g. "v0.1.6") instead of the latest release.

.EXAMPLE
irm https://raw.githubusercontent.com/solapi/solactl/main/scripts/install.ps1 | iex

.EXAMPLE
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Version v0.1.6
#>

[CmdletBinding()]
param(
[string] $InstallDir = (Join-Path $env:LOCALAPPDATA 'Programs\solactl'),
[string] $Version = ''
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

# GitHub requires TLS 1.2+; older PowerShell defaults to SSL3/TLS1.0.
try {
[Net.ServicePointManager]::SecurityProtocol = `
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch {
# PowerShell Core ignores this; safe to skip.
}

$Repo = 'solapi/solactl'

function Die([string] $Message) {
Write-Host "ERROR: $Message" -ForegroundColor Red
exit 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using exit 1 in a script intended to be run via iex (Invoke-Expression) will terminate the entire PowerShell session if an error occurs. This is a poor user experience for a one-liner installer. Consider using throw instead, which stops the script execution but keeps the terminal open.

    throw $Message

}

function Invoke-Download([string] $Uri, [string] $OutFile) {
try {
Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing -MaximumRedirection 5
} catch {
Die "Download failed: $Uri`n$($_.Exception.Message)"
}
}

# --- 1. Detect architecture --------------------------------------------------
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
'AMD64' { 'amd64' }
'ARM64' { 'arm64' }
'x86' { Die 'Unsupported architecture: x86 (32-bit). solactl ships amd64/arm64 only.' }
default { Die "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
}
Comment on lines +57 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current architecture detection relies solely on $env:PROCESSOR_ARCHITECTURE, which may return x86 if the user is running a 32-bit PowerShell process on a 64-bit operating system. Checking $env:PROCESSOR_ARCHITEW6432 ensures the script correctly identifies the underlying OS architecture even when running in a 32-bit shell.

$arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'AMD64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'AMD64') { 'amd64' }
elseif ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { 'arm64' }
else { Die "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }


# --- 2. Resolve target tag ---------------------------------------------------
if ([string]::IsNullOrWhiteSpace($Version)) {
Write-Host 'Checking latest version...'
try {
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/$Repo/releases/latest" -UseBasicParsing
} catch {
Die "Failed to fetch release info: $($_.Exception.Message)"
}
$tag = $release.tag_name
if ([string]::IsNullOrWhiteSpace($tag)) { Die 'Failed to parse release tag.' }
} else {
$tag = $Version
}
Write-Host "Target version: $tag"

$versionNumber = $tag.TrimStart('v')
$archiveName = "solactl_${versionNumber}_windows_${arch}.zip"
$downloadUrl = "https://github.com/$Repo/releases/download/$tag/$archiveName"
$checksumsUrl = "https://github.com/$Repo/releases/download/$tag/checksums.txt"

# --- 3. Download to a temp directory ----------------------------------------
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("solactl-install-" + [System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null

try {
$archivePath = Join-Path $tmpDir $archiveName
$checksumsPath = Join-Path $tmpDir 'checksums.txt'

Write-Host "Downloading $archiveName..."
Invoke-Download -Uri $downloadUrl -OutFile $archivePath

Write-Host 'Downloading checksums...'
Invoke-Download -Uri $checksumsUrl -OutFile $checksumsPath

# --- 4. Verify SHA256 ----------------------------------------------------
Write-Host 'Verifying checksum...'
$expectedHash = $null
foreach ($line in Get-Content -LiteralPath $checksumsPath) {
$parts = ($line.Trim()) -split '\s+', 2
if ($parts.Length -eq 2 -and $parts[1] -eq $archiveName) {
$expectedHash = $parts[0].ToLowerInvariant()
break
}
}
if (-not $expectedHash) { Die "Checksum not found for $archiveName in checksums.txt" }

$actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($expectedHash -ne $actualHash) {
Die "Checksum mismatch: expected $expectedHash, got $actualHash. File may be tampered."
}
Write-Host 'Checksum verified.'

# --- 5. Extract & install -----------------------------------------------
Write-Host 'Extracting...'
$extractDir = Join-Path $tmpDir 'extract'
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDir -Force

$binary = Join-Path $extractDir 'solactl.exe'
if (-not (Test-Path -LiteralPath $binary)) {
Die 'solactl.exe not found in archive.'
}

if (-not (Test-Path -LiteralPath $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
}

$dest = Join-Path $InstallDir 'solactl.exe'
try {
Move-Item -LiteralPath $binary -Destination $dest -Force
} catch {
# Existing binary is likely in use by a running process.
# Park the old one so the install still succeeds.
$stash = "$dest.old"
if (Test-Path -LiteralPath $stash) { Remove-Item -LiteralPath $stash -Force }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the .old file is locked (e.g., by a previous failed installation where the process is still running), Remove-Item will throw an error and stop the script because $ErrorActionPreference is set to Stop. Adding -ErrorAction SilentlyContinue prevents a crash in this edge case.

        if (Test-Path -LiteralPath $stash) { Remove-Item -LiteralPath $stash -Force -ErrorAction SilentlyContinue }

Move-Item -LiteralPath $dest -Destination $stash -Force
Move-Item -LiteralPath $binary -Destination $dest -Force
Write-Host "Previous binary was in use; moved aside to $stash"
}

Write-Host ''
Write-Host "Installed solactl $tag"
Write-Host "Location: $dest"
Write-Host ''

# --- 6. Ensure InstallDir is on the user PATH ---------------------------
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$entries = @()
if ($userPath) { $entries = $userPath -split ';' | Where-Object { $_ -ne '' } }

$alreadyOnPath = $false
foreach ($entry in $entries) {
if ([string]::Equals($entry.TrimEnd('\'), $InstallDir.TrimEnd('\'), [System.StringComparison]::OrdinalIgnoreCase)) {
$alreadyOnPath = $true
break
}
}

if (-not $alreadyOnPath) {
$newPath = if ($userPath) { "$userPath;$InstallDir" } else { $InstallDir }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current logic for constructing the new PATH string might result in double semicolons (e.g., path1;;path2) if the existing user PATH already ends with a semicolon. Using the $entries array to join the paths is cleaner and more robust.

        $newPath = ($entries + $InstallDir) -join ';'

[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
# Update current session too so the user can run solactl without reopening the shell.
$env:Path = "$env:Path;$InstallDir"
Write-Host "Added $InstallDir to user PATH."
Write-Host 'Open a new terminal for the PATH change to apply to other shells.'
} else {
Write-Host "$InstallDir is already on user PATH."
}

Write-Host ''
Write-Host 'To upgrade later: solactl upgrade'
} finally {
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}