From bca72fe23aed966e57c1201ad5bfe7744e4d0744 Mon Sep 17 00:00:00 2001 From: Subin Lee Date: Mon, 11 May 2026 13:40:23 +0900 Subject: [PATCH] feat(install): add rootless Windows installer script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/install.ps1 mirroring install.sh for Windows: installs to %LOCALAPPDATA%\Programs\solactl, verifies SHA256, and updates the user PATH via HKCU — no admin rights needed (winget-style). Documents both the irm|iex one-liner and the local-execution form with -Version / -InstallDir options in README. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 44 ++++++++++- scripts/install.ps1 | 176 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 scripts/install.ps1 diff --git a/README.md b/README.md index 85039f8..6c6a18e 100644 --- a/README.md +++ b/README.md @@ -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 설정이 필요하지 않습니다. ### 소스 빌드 @@ -35,10 +71,14 @@ make install # $GOPATH/bin에 설치 ### 업그레이드 +설치된 `solactl` 자체에서 업그레이드할 수 있습니다. 모든 플랫폼 공통입니다. + ```bash solactl upgrade ``` +또는 위의 설치 스크립트를 다시 실행해도 됩니다. + ## 사용법 ```bash diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 0000000..989f6d1 --- /dev/null +++ b/scripts/install.ps1 @@ -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 +} + +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" } +} + +# --- 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 } + 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 } + [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 +}