Merge 79dff4af73 into bfb2b35f74
This commit is contained in:
commit
a3a07f515d
3 changed files with 1229 additions and 0 deletions
247
scripts/install/maixcam/install_picoclaw.py
Normal file
247
scripts/install/maixcam/install_picoclaw.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_ACTION = "install"
|
||||
START_PICOCLAW = True
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Install or uninstall picoclaw.")
|
||||
parser.add_argument(
|
||||
"--action",
|
||||
choices=["install", "uninstall"],
|
||||
default=DEFAULT_ACTION,
|
||||
help=f"Action to perform, defaults to '{DEFAULT_ACTION}'.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def detect_arch() -> str:
|
||||
machine = platform.machine().lower()
|
||||
|
||||
if machine in {"aarch64", "arm64"}:
|
||||
return "arm64"
|
||||
if machine in {"riscv64"}:
|
||||
return "riscv64"
|
||||
|
||||
return f"unknown ({machine})"
|
||||
|
||||
|
||||
def ensure_picoclaw_dir() -> Path:
|
||||
target_dir = Path("/root/picoclaw")
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
return target_dir
|
||||
|
||||
|
||||
def get_download_url(arch: str) -> str:
|
||||
if arch == "riscv64":
|
||||
return "https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/picoclaw_Linux_riscv64.tar.gz"
|
||||
if arch == "arm64":
|
||||
return "https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/picoclaw_aarch64.deb"
|
||||
|
||||
raise ValueError(f"Unsupported architecture: {arch}")
|
||||
|
||||
|
||||
def download_package(url: str, target_dir: Path) -> Path:
|
||||
file_name = Path(url).name
|
||||
target_path = target_dir / file_name
|
||||
urllib.request.urlretrieve(url, target_path)
|
||||
return target_path
|
||||
|
||||
|
||||
def install_arm64(deb_file: Path) -> None:
|
||||
subprocess.run(["dpkg", "-i", str(deb_file)], check=True)
|
||||
|
||||
|
||||
def find_file_recursive(root: Path, file_name: str) -> Path:
|
||||
matches = list(root.rglob(file_name))
|
||||
if not matches:
|
||||
raise FileNotFoundError(f"Required file not found: {file_name}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def install_riscv64(tar_file: Path, work_dir: Path) -> None:
|
||||
install_dir = Path("/opt/picoclaw")
|
||||
if install_dir.exists():
|
||||
shutil.rmtree(install_dir)
|
||||
install_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tarfile.open(tar_file, "r:gz") as tar:
|
||||
tar.extractall(path=install_dir)
|
||||
|
||||
for binary_name in ["picoclaw", "picoclaw-launcher", "picoclaw-launcher-tui"]:
|
||||
installed_binary = find_file_recursive(install_dir, binary_name)
|
||||
installed_binary.chmod(0o755)
|
||||
|
||||
link_path = Path("/usr/bin") / binary_name
|
||||
if link_path.exists() or link_path.is_symlink():
|
||||
link_path.unlink()
|
||||
link_path.symlink_to(installed_binary)
|
||||
|
||||
|
||||
def cleanup_dir(target_dir: Path) -> None:
|
||||
if target_dir.exists():
|
||||
shutil.rmtree(target_dir)
|
||||
|
||||
|
||||
def stop_picoclaw() -> None:
|
||||
process_names = {"picoclaw", "picoclaw-launcher", "picoclaw-launcher-tui"}
|
||||
stopped_any = False
|
||||
self_pid = os.getpid()
|
||||
|
||||
try:
|
||||
ps_result = subprocess.run(["ps", "-eo", "pid=,args="], capture_output=True, text=True, check=False)
|
||||
except FileNotFoundError:
|
||||
print("ps command not found, skip stopping picoclaw processes.")
|
||||
return
|
||||
|
||||
if ps_result.returncode != 0:
|
||||
print("Failed to list processes with ps, skip stopping picoclaw processes.")
|
||||
return
|
||||
|
||||
for line in ps_result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
parts = line.split(maxsplit=1)
|
||||
if not parts:
|
||||
continue
|
||||
|
||||
try:
|
||||
pid = int(parts[0])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if pid == self_pid:
|
||||
continue
|
||||
|
||||
cmdline = parts[1] if len(parts) > 1 else ""
|
||||
executable = os.path.basename(cmdline.split(maxsplit=1)[0]) if cmdline else ""
|
||||
if executable not in process_names and not any(f"/{name}" in cmdline for name in process_names):
|
||||
continue
|
||||
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
stopped_any = True
|
||||
except (ProcessLookupError, PermissionError):
|
||||
continue
|
||||
|
||||
if stopped_any:
|
||||
print("Stopped picoclaw related processes.")
|
||||
else:
|
||||
print("No running picoclaw related process found.")
|
||||
|
||||
|
||||
def uninstall_arm64() -> None:
|
||||
result = subprocess.run(["dpkg", "-l"], capture_output=True, text=True, check=True)
|
||||
package_names = []
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith("ii") and "picoclaw" in line:
|
||||
package_names.append(line.split()[1])
|
||||
|
||||
if not package_names:
|
||||
print("No installed picoclaw package found for arm64.")
|
||||
return
|
||||
|
||||
for package_name in package_names:
|
||||
print(f"Removing package: {package_name}")
|
||||
subprocess.run(["dpkg", "-r", package_name], check=True)
|
||||
|
||||
|
||||
def uninstall_riscv64() -> None:
|
||||
for binary_name in ["picoclaw", "picoclaw-launcher", "picoclaw-launcher-tui"]:
|
||||
link_path = Path("/usr/bin") / binary_name
|
||||
if link_path.exists() or link_path.is_symlink():
|
||||
link_path.unlink()
|
||||
print(f"Removed link: {link_path}")
|
||||
|
||||
install_dir = Path("/opt/picoclaw")
|
||||
if install_dir.exists():
|
||||
shutil.rmtree(install_dir)
|
||||
print(f"Removed directory: {install_dir}")
|
||||
|
||||
|
||||
def remove_picoclaw_user_dir() -> None:
|
||||
user_dir = Path("/root/.picoclaw")
|
||||
if user_dir.exists():
|
||||
shutil.rmtree(user_dir)
|
||||
print(f"Removed directory: {user_dir}")
|
||||
|
||||
|
||||
def start_picoclaw_launcher() -> None:
|
||||
launcher_path = shutil.which("picoclaw-launcher")
|
||||
if not launcher_path:
|
||||
print("picoclaw-launcher not found in PATH, skip start.")
|
||||
return
|
||||
|
||||
env = dict(os.environ)
|
||||
env["HOME"] = "/root"
|
||||
env.setdefault("NO_COLOR", "1")
|
||||
|
||||
subprocess.Popen(
|
||||
[launcher_path, "-no-browser", "-public"],
|
||||
env=env,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
)
|
||||
print("Started picoclaw-launcher in background.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
action = args.action
|
||||
|
||||
arch = detect_arch()
|
||||
print(f"Arch: {arch}")
|
||||
print(f"Action: {action}")
|
||||
|
||||
if action == "install":
|
||||
if arch.startswith("unknown"):
|
||||
print("Unsupported arch, exiting.")
|
||||
raise SystemExit(1)
|
||||
|
||||
picoclaw_dir = ensure_picoclaw_dir()
|
||||
print(f"Ensured directory exists: {picoclaw_dir}")
|
||||
|
||||
download_url = get_download_url(arch)
|
||||
print(f"Downloading from: {download_url}")
|
||||
|
||||
downloaded_file = download_package(download_url, picoclaw_dir)
|
||||
print(f"Downloaded file: {downloaded_file}")
|
||||
|
||||
if arch == "arm64":
|
||||
print("Installing with dpkg...")
|
||||
install_arm64(downloaded_file)
|
||||
else:
|
||||
print("Extracting all files to /opt/picoclaw and creating symlinks...")
|
||||
install_riscv64(downloaded_file, picoclaw_dir)
|
||||
|
||||
if START_PICOCLAW:
|
||||
start_picoclaw_launcher()
|
||||
|
||||
cleanup_dir(picoclaw_dir)
|
||||
print(f"Cleaned up directory: {picoclaw_dir}")
|
||||
else:
|
||||
stop_picoclaw()
|
||||
|
||||
if arch == "arm64":
|
||||
uninstall_arm64()
|
||||
elif arch == "riscv64":
|
||||
uninstall_riscv64()
|
||||
else:
|
||||
print("Unknown architecture, skipping uninstallation.")
|
||||
|
||||
remove_picoclaw_user_dir()
|
||||
529
scripts/install/picoclaw/install-picoclaw.ps1
Normal file
529
scripts/install/picoclaw/install-picoclaw.ps1
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('system', 'user')]
|
||||
[string]$InstallMode = 'user',
|
||||
|
||||
[ValidateSet('github', 'cdn')]
|
||||
[string]$Source = 'github',
|
||||
|
||||
[string]$Arch
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$BaseUrls = @{
|
||||
github = 'https://github.com/sipeed/picoclaw/releases/latest/download/'
|
||||
cdn = 'https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/'
|
||||
}
|
||||
|
||||
$Assets = @{
|
||||
windows = @{
|
||||
x86_64 = @{ file = 'picoclaw_Windows_x86_64.zip' }
|
||||
arm64 = @{ file = 'picoclaw_Windows_arm64.zip' }
|
||||
}
|
||||
linux = @{
|
||||
x86_64 = @{ file = 'picoclaw_Linux_x86_64.tar.gz'; deb = 'picoclaw_x86_64.deb'; rpm = 'picoclaw_x86_64.rpm' }
|
||||
arm64 = @{ file = 'picoclaw_Linux_arm64.tar.gz'; deb = 'picoclaw_aarch64.deb'; rpm = 'picoclaw_aarch64.rpm' }
|
||||
armv7 = @{ file = 'picoclaw_Linux_armv7.tar.gz'; deb = 'picoclaw_armv7.deb'; rpm = 'picoclaw_armv7.rpm' }
|
||||
armv6 = @{ file = 'picoclaw_Linux_armv6.tar.gz'; deb = 'picoclaw_armv6.deb'; rpm = 'picoclaw_armv6.rpm' }
|
||||
riscv64 = @{ file = 'picoclaw_Linux_riscv64.tar.gz'; deb = 'picoclaw_riscv64.deb'; rpm = 'picoclaw_riscv64.rpm' }
|
||||
mipsle = @{ file = 'picoclaw_Linux_mipsle.tar.gz'; deb = 'picoclaw_mipsle.deb'; rpm = 'picoclaw_mipsle.rpm' }
|
||||
s390x = @{ file = 'picoclaw_Linux_s390x.tar.gz'; deb = 'picoclaw_s390x.deb'; rpm = 'picoclaw_s390x.rpm' }
|
||||
}
|
||||
macos = @{
|
||||
x86_64 = @{ file = 'picoclaw_Darwin_x86_64.tar.gz' }
|
||||
arm64 = @{ file = 'picoclaw_Darwin_arm64.tar.gz' }
|
||||
}
|
||||
freebsd = @{
|
||||
x86_64 = @{ file = 'picoclaw_Freebsd_x86_64.tar.gz' }
|
||||
arm64 = @{ file = 'picoclaw_Freebsd_arm64.tar.gz' }
|
||||
armv7 = @{ file = 'picoclaw_Freebsd_armv7.tar.gz' }
|
||||
}
|
||||
netbsd = @{
|
||||
x86_64 = @{ file = 'picoclaw_Netbsd_x86_64.tar.gz' }
|
||||
arm64 = @{ file = 'picoclaw_Netbsd_arm64.tar.gz' }
|
||||
}
|
||||
}
|
||||
|
||||
$ExpectedBins = @('picoclaw', 'picoclaw-launcher', 'picoclaw-launcher-tui')
|
||||
|
||||
function Get-CommandExists {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
return $null -ne (Get-Command -Name $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Get-OsId {
|
||||
if ($IsWindows) {
|
||||
return 'windows'
|
||||
}
|
||||
if ($IsLinux) {
|
||||
return 'linux'
|
||||
}
|
||||
if ($IsMacOS) {
|
||||
return 'macos'
|
||||
}
|
||||
|
||||
$uname = (& uname -s).Trim().ToLowerInvariant()
|
||||
switch ($uname) {
|
||||
'freebsd' { return 'freebsd' }
|
||||
'netbsd' { return 'netbsd' }
|
||||
default {
|
||||
throw "Unsupported operating system: $uname"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-Arch {
|
||||
param([Parameter(Mandatory = $true)][string]$OsId)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Arch)) {
|
||||
return $Arch.ToLowerInvariant()
|
||||
}
|
||||
|
||||
$machine = if ($IsWindows) {
|
||||
[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
||||
}
|
||||
else {
|
||||
(& uname -m).Trim().ToLowerInvariant()
|
||||
}
|
||||
|
||||
switch ($machine) {
|
||||
'x64' { return 'x86_64' }
|
||||
'amd64' { return 'x86_64' }
|
||||
'x86_64' { return 'x86_64' }
|
||||
'arm64' { return 'arm64' }
|
||||
'aarch64' { return 'arm64' }
|
||||
'armv7l' { return 'armv7' }
|
||||
'armv7' { return 'armv7' }
|
||||
'armv6l' { return 'armv6' }
|
||||
'armv6' { return 'armv6' }
|
||||
'riscv64' { return 'riscv64' }
|
||||
'mipsel' { return 'mipsle' }
|
||||
'mipsle' { return 'mipsle' }
|
||||
's390x' { return 's390x' }
|
||||
default {
|
||||
throw "Unsupported architecture '$machine' for OS '$OsId'. Use -Arch to override."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-SystemInstallPrivileges {
|
||||
param([Parameter(Mandatory = $true)][string]$OsId)
|
||||
|
||||
if ($InstallMode -ne 'system') {
|
||||
return
|
||||
}
|
||||
|
||||
if ($OsId -eq 'windows') {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
$isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
throw 'System install on Windows requires Administrator privileges.'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
$uid = (& id -u).Trim()
|
||||
if ($uid -ne '0') {
|
||||
throw 'System install requires root privileges (sudo/root).'
|
||||
}
|
||||
}
|
||||
|
||||
function Get-UnixSystemLayout {
|
||||
if (Test-Path -LiteralPath '/usr/local/share') {
|
||||
return @{
|
||||
installRoot = '/usr/local/share/picoclaw'
|
||||
linkDir = '/usr/local/bin'
|
||||
mode = 'symlink'
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath '/usr/share') {
|
||||
return @{
|
||||
installRoot = '/usr/share/picoclaw'
|
||||
linkDir = '/usr/bin'
|
||||
mode = 'symlink'
|
||||
}
|
||||
}
|
||||
|
||||
$tempInstallRoot = "/tmp/picoclaw-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())"
|
||||
|
||||
try {
|
||||
Get-ChildItem -Path '/tmp' -Filter 'picoclaw-*' -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {
|
||||
if ($_.FullName -ne $tempInstallRoot) {
|
||||
Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Best-effort cleanup; ignore any failures to avoid breaking installation.
|
||||
}
|
||||
|
||||
return @{
|
||||
installRoot = $tempInstallRoot
|
||||
linkDir = '/usr/bin'
|
||||
mode = 'copy-executables'
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InstallLayout {
|
||||
param([Parameter(Mandatory = $true)][string]$OsId)
|
||||
|
||||
if ($InstallMode -eq 'user') {
|
||||
if ($OsId -eq 'windows') {
|
||||
$base = [Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
|
||||
if ([string]::IsNullOrWhiteSpace($base) -or -not (Test-Path -LiteralPath $base)) {
|
||||
throw 'Failed to resolve a valid LocalApplicationData path.'
|
||||
}
|
||||
return @{
|
||||
installRoot = (Join-Path $base 'picoclaw')
|
||||
linkDir = $null
|
||||
mode = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
installRoot = (Join-Path $HOME '.local/share/picoclaw')
|
||||
linkDir = (Join-Path $HOME '.local/bin')
|
||||
mode = 'user'
|
||||
}
|
||||
}
|
||||
|
||||
if ($OsId -eq 'windows') {
|
||||
$programFiles = [Environment]::GetFolderPath([Environment+SpecialFolder]::ProgramFiles)
|
||||
if ([string]::IsNullOrWhiteSpace($programFiles) -or -not (Test-Path -LiteralPath $programFiles)) {
|
||||
throw 'ProgramFiles path from .NET API is invalid or missing. Installation terminated.'
|
||||
}
|
||||
return @{
|
||||
installRoot = (Join-Path $programFiles 'picoclaw')
|
||||
linkDir = $null
|
||||
mode = 'system'
|
||||
}
|
||||
}
|
||||
|
||||
return Get-UnixSystemLayout
|
||||
}
|
||||
|
||||
function New-DirectoryIfMissing {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DownloadUrl {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$FileName,
|
||||
[Parameter(Mandatory = $true)][string]$SourceName
|
||||
)
|
||||
return "$($BaseUrls[$SourceName])$FileName"
|
||||
}
|
||||
|
||||
function Invoke-ArtifactDownload {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Url,
|
||||
[Parameter(Mandatory = $true)][string]$OutFile
|
||||
)
|
||||
Write-Host "Downloading: $Url"
|
||||
Invoke-WebRequest -Uri $Url -OutFile $OutFile
|
||||
}
|
||||
|
||||
function Expand-Artifact {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ArchivePath,
|
||||
[Parameter(Mandatory = $true)][string]$Destination
|
||||
)
|
||||
|
||||
New-DirectoryIfMissing -Path $Destination
|
||||
|
||||
if ($ArchivePath.EndsWith('.zip', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Expand-Archive -LiteralPath $ArchivePath -DestinationPath $Destination -Force
|
||||
return
|
||||
}
|
||||
|
||||
if ($ArchivePath.EndsWith('.tar.gz', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
& tar -xzf $ArchivePath -C $Destination
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to extract tar archive: $ArchivePath"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
throw "Unsupported archive type: $ArchivePath"
|
||||
}
|
||||
|
||||
function Get-BinaryPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Root,
|
||||
[Parameter(Mandatory = $true)][string]$Name
|
||||
)
|
||||
|
||||
$direct = Join-Path $Root $Name
|
||||
if (Test-Path -LiteralPath $direct -PathType Leaf) {
|
||||
return $direct
|
||||
}
|
||||
|
||||
$directExe = Join-Path $Root ("$Name.exe")
|
||||
if (Test-Path -LiteralPath $directExe -PathType Leaf) {
|
||||
return $directExe
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Set-UnixUserPathExport {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$HomeDir,
|
||||
[Parameter(Mandatory = $true)][string]$BinDir
|
||||
)
|
||||
|
||||
$line = 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
$rcFiles = @(
|
||||
(Join-Path $HomeDir '.bashrc'),
|
||||
(Join-Path $HomeDir '.zshrc'),
|
||||
(Join-Path $HomeDir '.profile')
|
||||
)
|
||||
|
||||
$updated = @()
|
||||
foreach ($rcPath in $rcFiles) {
|
||||
if (-not (Test-Path -LiteralPath $rcPath)) {
|
||||
New-Item -ItemType File -Path $rcPath -Force | Out-Null
|
||||
}
|
||||
|
||||
$hasLine = Select-String -Path $rcPath -Pattern [regex]::Escape($line) -SimpleMatch -Quiet -ErrorAction SilentlyContinue
|
||||
if (-not $hasLine) {
|
||||
Add-Content -Path $rcPath -Value "`n$line"
|
||||
$updated += $rcPath
|
||||
}
|
||||
}
|
||||
|
||||
return $updated
|
||||
}
|
||||
|
||||
function Add-PathVariable {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PathToAdd,
|
||||
[Parameter(Mandatory = $true)][ValidateSet('User', 'Machine')][string]$Scope
|
||||
)
|
||||
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', $Scope)
|
||||
$parts = @()
|
||||
if (-not [string]::IsNullOrWhiteSpace($current)) {
|
||||
$parts = $current.Split(';') | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
}
|
||||
|
||||
if ($parts -contains $PathToAdd) {
|
||||
return
|
||||
}
|
||||
|
||||
$newValue = if ($parts.Count -eq 0) { $PathToAdd } else { "$current;$PathToAdd" }
|
||||
[Environment]::SetEnvironmentVariable('Path', $newValue, $Scope)
|
||||
}
|
||||
|
||||
function Install-LinuxPackageFromAsset {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][hashtable]$Asset,
|
||||
[Parameter(Mandatory = $true)][string]$TempDir
|
||||
)
|
||||
|
||||
$debCmd = Get-CommandExists -Name 'dpkg'
|
||||
$rpmCmd = Get-CommandExists -Name 'rpm'
|
||||
|
||||
if (-not $debCmd -and -not $rpmCmd) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($debCmd -and $Asset.ContainsKey('deb')) {
|
||||
$pkg = $Asset.deb
|
||||
$pkgPath = Join-Path $TempDir $pkg
|
||||
Invoke-ArtifactDownload -Url (Get-DownloadUrl -FileName $pkg -SourceName $Source) -OutFile $pkgPath
|
||||
& dpkg -i $pkgPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'dpkg install failed.'
|
||||
}
|
||||
return @{
|
||||
manager = 'dpkg'
|
||||
package = $pkgPath
|
||||
}
|
||||
}
|
||||
|
||||
if ($rpmCmd -and $Asset.ContainsKey('rpm')) {
|
||||
$pkg = $Asset.rpm
|
||||
$pkgPath = Join-Path $TempDir $pkg
|
||||
Invoke-ArtifactDownload -Url (Get-DownloadUrl -FileName $pkg -SourceName $Source) -OutFile $pkgPath
|
||||
& rpm -Uvh --replacepkgs $pkgPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'rpm install failed.'
|
||||
}
|
||||
return @{
|
||||
manager = 'rpm'
|
||||
package = $pkgPath
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Install-LinksOrCopies {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$InstallRoot,
|
||||
[Parameter(Mandatory = $true)][string]$LinkDir,
|
||||
[Parameter(Mandatory = $true)][string]$Mode
|
||||
)
|
||||
|
||||
New-DirectoryIfMissing -Path $LinkDir
|
||||
|
||||
if ($Mode -eq 'copy-executables') {
|
||||
$copied = @()
|
||||
foreach ($name in $ExpectedBins) {
|
||||
$binPath = Get-BinaryPath -Root $InstallRoot -Name $name
|
||||
if ($null -eq $binPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
$dest = Join-Path $LinkDir (Split-Path -Path $binPath -Leaf)
|
||||
Copy-Item -LiteralPath $binPath -Destination $dest -Force
|
||||
$copied += $dest
|
||||
}
|
||||
|
||||
return @{
|
||||
type = 'copied'
|
||||
paths = $copied
|
||||
}
|
||||
}
|
||||
|
||||
$linked = @()
|
||||
foreach ($name in $ExpectedBins) {
|
||||
$binPath = Get-BinaryPath -Root $InstallRoot -Name $name
|
||||
if ($null -eq $binPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
$target = Join-Path $LinkDir $name
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
Remove-Item -LiteralPath $target -Force
|
||||
}
|
||||
|
||||
if ($IsWindows) {
|
||||
Copy-Item -LiteralPath $binPath -Destination $target -Force
|
||||
}
|
||||
else {
|
||||
& ln -s $binPath $target
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to create symlink: $target"
|
||||
}
|
||||
}
|
||||
$linked += $target
|
||||
}
|
||||
|
||||
return @{
|
||||
type = 'linked'
|
||||
paths = $linked
|
||||
}
|
||||
}
|
||||
|
||||
function Write-MissingUserLocalWarning {
|
||||
param([Parameter(Mandatory = $true)][string]$OsId)
|
||||
|
||||
if ($InstallMode -ne 'user' -or $OsId -eq 'windows') {
|
||||
return
|
||||
}
|
||||
|
||||
$localRoot = Join-Path $HOME '.local'
|
||||
if (-not (Test-Path -LiteralPath $localRoot)) {
|
||||
Write-Host "Warning: $localRoot does not exist. The installer will create it automatically." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
$osId = Get-OsId
|
||||
$archId = Resolve-Arch -OsId $osId
|
||||
Assert-SystemInstallPrivileges -OsId $osId
|
||||
Write-MissingUserLocalWarning -OsId $osId
|
||||
|
||||
if (-not $Assets.ContainsKey($osId)) {
|
||||
throw "OS '$osId' is not included in assets map."
|
||||
}
|
||||
|
||||
$osAssets = $Assets[$osId]
|
||||
if (-not $osAssets.ContainsKey($archId)) {
|
||||
$available = ($osAssets.Keys | Sort-Object) -join ', '
|
||||
throw "Architecture '$archId' is not available for '$osId'. Available: $available"
|
||||
}
|
||||
|
||||
$asset = $osAssets[$archId]
|
||||
$layout = Get-InstallLayout -OsId $osId
|
||||
|
||||
$tempDir = Join-Path ([IO.Path]::GetTempPath()) ("picoclaw-install-$([Guid]::NewGuid().ToString('N'))")
|
||||
New-DirectoryIfMissing -Path $tempDir
|
||||
|
||||
try {
|
||||
Write-Host "Install mode: $InstallMode"
|
||||
Write-Host "OS/Arch: $osId/$archId"
|
||||
|
||||
if ($osId -eq 'linux' -and $InstallMode -eq 'system') {
|
||||
$pkgInfo = Install-LinuxPackageFromAsset -Asset $asset -TempDir $tempDir
|
||||
if ($null -ne $pkgInfo) {
|
||||
Write-Host "Installed via package manager: $($pkgInfo.manager)"
|
||||
Write-Host "Package file: $($pkgInfo.package)"
|
||||
Write-Host 'Install location is managed by the package manager.'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$archiveName = $asset.file
|
||||
$archivePath = Join-Path $tempDir $archiveName
|
||||
$downloadUrl = Get-DownloadUrl -FileName $archiveName -SourceName $Source
|
||||
Invoke-ArtifactDownload -Url $downloadUrl -OutFile $archivePath
|
||||
|
||||
New-DirectoryIfMissing -Path $layout.installRoot
|
||||
Expand-Artifact -ArchivePath $archivePath -Destination $layout.installRoot
|
||||
|
||||
if ($osId -eq 'windows') {
|
||||
$pathScope = if ($InstallMode -eq 'system') { 'Machine' } else { 'User' }
|
||||
Add-PathVariable -PathToAdd $layout.installRoot -Scope $pathScope
|
||||
Write-Host "Installed to: $($layout.installRoot)"
|
||||
Write-Host "PATH scope updated: $pathScope"
|
||||
Write-Host 'Please restart your terminal or log out and back in for PATH changes to take effect.'
|
||||
return
|
||||
}
|
||||
|
||||
$linkSummary = Install-LinksOrCopies -InstallRoot $layout.installRoot -LinkDir $layout.linkDir -Mode $layout.mode
|
||||
|
||||
Write-Host "Extracted to: $($layout.installRoot)"
|
||||
if ($linkSummary.paths.Count -gt 0) {
|
||||
if ($linkSummary.type -eq 'copied') {
|
||||
Write-Host "Copied executables to: $($layout.linkDir)"
|
||||
}
|
||||
else {
|
||||
Write-Host "Created links in: $($layout.linkDir)"
|
||||
}
|
||||
$linkSummary.paths | ForEach-Object { Write-Host " - $_" }
|
||||
}
|
||||
else {
|
||||
Write-Host 'No launcher binaries were found for link/copy step.'
|
||||
}
|
||||
|
||||
if ($InstallMode -eq 'user' -and $layout.linkDir) {
|
||||
if ($osId -ne 'windows') {
|
||||
$shellRcUpdated = Set-UnixUserPathExport -HomeDir $HOME -BinDir $layout.linkDir
|
||||
if ($shellRcUpdated.Count -gt 0) {
|
||||
Write-Host 'Updated shell profile files for PATH:'
|
||||
$shellRcUpdated | ForEach-Object { Write-Host " - $_" }
|
||||
}
|
||||
}
|
||||
Write-Host "User-local binary directory: $($layout.linkDir)"
|
||||
Write-Host 'Ensure your shell PATH contains the directory above.' -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $tempDir) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
Write-Host "Warning: failed to remove temp directory $tempDir . Please remove it manually." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
453
scripts/install/picoclaw/install-picoclaw.sh
Normal file
453
scripts/install/picoclaw/install-picoclaw.sh
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "${BASH_VERSION:-}" ]; then
|
||||
printf 'Error: this installer must run with GNU bash (ash/busybox are not supported).\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
|
||||
printf 'Error: GNU bash 4+ is required. Current version: %s\n' "$BASH_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! bash --version 2>/dev/null | head -n 1 | grep -qi 'gnu bash'; then
|
||||
printf 'Error: this installer requires a full GNU bash runtime.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_MODE="user"
|
||||
SOURCE="github"
|
||||
ARCH_OVERRIDE=""
|
||||
|
||||
BASE_GITHUB="https://github.com/sipeed/picoclaw/releases/latest/download/"
|
||||
BASE_CDN="https://picoclaw-downloads.tos-cn-beijing.volces.com/latest/"
|
||||
|
||||
EXPECTED_BINS=("picoclaw" "picoclaw-launcher" "picoclaw-launcher-tui")
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
install-picoclaw.sh [--mode system|user] [--source github|cdn] [--arch ARCH]
|
||||
|
||||
Examples:
|
||||
./install-picoclaw.sh --mode user
|
||||
sudo ./install-picoclaw.sh --mode system --source cdn
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Error: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
create_secure_tmp_dir() {
|
||||
local prefix="${1:-picoclaw}"
|
||||
local d
|
||||
|
||||
if d="$(mktemp -d "/tmp/${prefix}.XXXXXX" 2>/dev/null)"; then
|
||||
printf '%s\n' "$d"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local i candidate
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
candidate="/tmp/${prefix}.$$.${RANDOM}.$(date +%s)"
|
||||
if mkdir -m 700 "$candidate" 2>/dev/null; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_os() {
|
||||
local u
|
||||
u="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
case "$u" in
|
||||
linux) echo "linux" ;;
|
||||
darwin) echo "macos" ;;
|
||||
freebsd) echo "freebsd" ;;
|
||||
netbsd) echo "netbsd" ;;
|
||||
mingw*|msys*|cygwin*)
|
||||
die "Windows shell detected ($u). Please run the PowerShell installer instead: pwsh ./static/scripts/picoclaw/install-picoclaw.ps1 -InstallMode user"
|
||||
;;
|
||||
*) die "Unsupported OS: $u" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_arch() {
|
||||
if [[ -n "$ARCH_OVERRIDE" ]]; then
|
||||
echo "$ARCH_OVERRIDE"
|
||||
return
|
||||
fi
|
||||
|
||||
local m
|
||||
m="$(uname -m | tr '[:upper:]' '[:lower:]')"
|
||||
case "$m" in
|
||||
x86_64|amd64) echo "x86_64" ;;
|
||||
aarch64|arm64) echo "arm64" ;;
|
||||
armv7l|armv7) echo "armv7" ;;
|
||||
armv6l|armv6) echo "armv6" ;;
|
||||
riscv64) echo "riscv64" ;;
|
||||
mipsel|mipsle) echo "mipsle" ;;
|
||||
s390x) echo "s390x" ;;
|
||||
*) die "Unsupported architecture: $m (use --arch to override)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_privileges() {
|
||||
if [[ "$INSTALL_MODE" != "system" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
die "System install requires root privileges (sudo/root)."
|
||||
fi
|
||||
}
|
||||
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local out="$2"
|
||||
|
||||
log "Downloading: $url"
|
||||
if command_exists curl; then
|
||||
curl -fsSL "$url" -o "$out"
|
||||
return
|
||||
fi
|
||||
if command_exists wget; then
|
||||
wget -qO "$out" "$url"
|
||||
return
|
||||
fi
|
||||
die "Neither curl nor wget is available."
|
||||
}
|
||||
|
||||
base_url() {
|
||||
case "$SOURCE" in
|
||||
github) echo "$BASE_GITHUB" ;;
|
||||
cdn) echo "$BASE_CDN" ;;
|
||||
*) die "Invalid source: $SOURCE" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
asset_for() {
|
||||
local os="$1"
|
||||
local arch="$2"
|
||||
|
||||
TAR_FILE=""
|
||||
DEB_FILE=""
|
||||
RPM_FILE=""
|
||||
|
||||
case "$os/$arch" in
|
||||
windows/x86_64) TAR_FILE="picoclaw_Windows_x86_64.zip" ;;
|
||||
windows/arm64) TAR_FILE="picoclaw_Windows_arm64.zip" ;;
|
||||
|
||||
linux/x86_64)
|
||||
TAR_FILE="picoclaw_Linux_x86_64.tar.gz"
|
||||
DEB_FILE="picoclaw_x86_64.deb"
|
||||
RPM_FILE="picoclaw_x86_64.rpm"
|
||||
;;
|
||||
linux/arm64)
|
||||
TAR_FILE="picoclaw_Linux_arm64.tar.gz"
|
||||
DEB_FILE="picoclaw_aarch64.deb"
|
||||
RPM_FILE="picoclaw_aarch64.rpm"
|
||||
;;
|
||||
linux/armv7)
|
||||
TAR_FILE="picoclaw_Linux_armv7.tar.gz"
|
||||
DEB_FILE="picoclaw_armv7.deb"
|
||||
RPM_FILE="picoclaw_armv7.rpm"
|
||||
;;
|
||||
linux/armv6)
|
||||
TAR_FILE="picoclaw_Linux_armv6.tar.gz"
|
||||
DEB_FILE="picoclaw_armv6.deb"
|
||||
RPM_FILE="picoclaw_armv6.rpm"
|
||||
;;
|
||||
linux/riscv64)
|
||||
TAR_FILE="picoclaw_Linux_riscv64.tar.gz"
|
||||
DEB_FILE="picoclaw_riscv64.deb"
|
||||
RPM_FILE="picoclaw_riscv64.rpm"
|
||||
;;
|
||||
linux/mipsle)
|
||||
TAR_FILE="picoclaw_Linux_mipsle.tar.gz"
|
||||
DEB_FILE="picoclaw_mipsle.deb"
|
||||
RPM_FILE="picoclaw_mipsle.rpm"
|
||||
;;
|
||||
linux/s390x)
|
||||
TAR_FILE="picoclaw_Linux_s390x.tar.gz"
|
||||
DEB_FILE="picoclaw_s390x.deb"
|
||||
RPM_FILE="picoclaw_s390x.rpm"
|
||||
;;
|
||||
|
||||
macos/x86_64) TAR_FILE="picoclaw_Darwin_x86_64.tar.gz" ;;
|
||||
macos/arm64) TAR_FILE="picoclaw_Darwin_arm64.tar.gz" ;;
|
||||
|
||||
freebsd/x86_64) TAR_FILE="picoclaw_Freebsd_x86_64.tar.gz" ;;
|
||||
freebsd/arm64) TAR_FILE="picoclaw_Freebsd_arm64.tar.gz" ;;
|
||||
freebsd/armv7) TAR_FILE="picoclaw_Freebsd_armv7.tar.gz" ;;
|
||||
|
||||
netbsd/x86_64) TAR_FILE="picoclaw_Netbsd_x86_64.tar.gz" ;;
|
||||
netbsd/arm64) TAR_FILE="picoclaw_Netbsd_arm64.tar.gz" ;;
|
||||
|
||||
*) die "No asset mapping for $os/$arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_unix_system_layout() {
|
||||
LAYOUT_MODE="symlink"
|
||||
if [[ -d "/usr/local/share" ]]; then
|
||||
INSTALL_ROOT="/usr/local/share/picoclaw"
|
||||
LINK_DIR="/usr/local/bin"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -d "/usr/share" ]]; then
|
||||
INSTALL_ROOT="/usr/share/picoclaw"
|
||||
LINK_DIR="/usr/bin"
|
||||
return
|
||||
fi
|
||||
|
||||
INSTALL_ROOT="$(create_secure_tmp_dir "picoclaw-install" || true)"
|
||||
[[ -n "$INSTALL_ROOT" ]] || die "Failed to create secure temporary install directory under /tmp"
|
||||
LINK_DIR="/usr/bin"
|
||||
LAYOUT_MODE="copy-executables"
|
||||
}
|
||||
|
||||
resolve_layout() {
|
||||
local os="$1"
|
||||
|
||||
if [[ "$INSTALL_MODE" == "user" ]]; then
|
||||
INSTALL_ROOT="$HOME/.local/share/picoclaw"
|
||||
LINK_DIR="$HOME/.local/bin"
|
||||
LAYOUT_MODE="user"
|
||||
return
|
||||
fi
|
||||
|
||||
case "$os" in
|
||||
linux|macos|freebsd|netbsd)
|
||||
resolve_unix_system_layout
|
||||
;;
|
||||
*)
|
||||
die "Bash installer does not support this OS: $os"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_via_package_manager_linux() {
|
||||
local tmp_dir="$1"
|
||||
local base
|
||||
base="$(base_url)"
|
||||
|
||||
if [[ -n "$DEB_FILE" ]] && command_exists dpkg; then
|
||||
local deb_path="$tmp_dir/$DEB_FILE"
|
||||
download_file "${base}${DEB_FILE}" "$deb_path"
|
||||
dpkg -i "$deb_path"
|
||||
PM_RESULT="dpkg:$deb_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$RPM_FILE" ]] && command_exists rpm; then
|
||||
local rpm_path="$tmp_dir/$RPM_FILE"
|
||||
download_file "${base}${RPM_FILE}" "$rpm_path"
|
||||
rpm -Uvh --replacepkgs "$rpm_path"
|
||||
PM_RESULT="rpm:$rpm_path"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PM_RESULT=""
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_archive() {
|
||||
local archive="$1"
|
||||
local dst="$2"
|
||||
mkdir -p "$dst"
|
||||
|
||||
case "$archive" in
|
||||
*.tar.gz)
|
||||
tar -xzf "$archive" -C "$dst"
|
||||
;;
|
||||
*.zip)
|
||||
if command_exists unzip; then
|
||||
unzip -o "$archive" -d "$dst" >/dev/null
|
||||
else
|
||||
die "unzip is required for zip archives"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
die "Unsupported archive: $archive"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_flat_binary_path() {
|
||||
local root="$1"
|
||||
local name="$2"
|
||||
local candidate="$root/$name"
|
||||
|
||||
if [[ -f "$candidate" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
link_expected_bins() {
|
||||
local root="$1"
|
||||
local link_dir="$2"
|
||||
|
||||
mkdir -p "$link_dir"
|
||||
LINKED_PATHS=()
|
||||
|
||||
local name bin target
|
||||
for name in "${EXPECTED_BINS[@]}"; do
|
||||
bin="$(resolve_flat_binary_path "$root" "$name" || true)"
|
||||
if [[ -z "$bin" ]]; then
|
||||
continue
|
||||
fi
|
||||
target="$link_dir/$name"
|
||||
rm -f "$target"
|
||||
ln -s "$bin" "$target"
|
||||
LINKED_PATHS+=("$target")
|
||||
done
|
||||
}
|
||||
|
||||
copy_executables_to_bin() {
|
||||
local root="$1"
|
||||
local bin_dir="$2"
|
||||
|
||||
mkdir -p "$bin_dir"
|
||||
COPIED_PATHS=()
|
||||
|
||||
local name file dest
|
||||
for name in "${EXPECTED_BINS[@]}"; do
|
||||
file="$(resolve_flat_binary_path "$root" "$name" || true)"
|
||||
if [[ -z "$file" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ ! -x "$file" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
dest="$bin_dir/$name"
|
||||
cp -f "$file" "$dest"
|
||||
chmod +x "$dest" || true
|
||||
COPIED_PATHS+=("$dest")
|
||||
done
|
||||
}
|
||||
|
||||
ensure_user_path_export() {
|
||||
local line='export PATH="$HOME/.local/bin:$PATH"'
|
||||
local profile
|
||||
|
||||
if [[ -f "$HOME/.bashrc" ]]; then
|
||||
profile="$HOME/.bashrc"
|
||||
elif [[ -f "$HOME/.bash_profile" ]]; then
|
||||
profile="$HOME/.bash_profile"
|
||||
else
|
||||
profile="$HOME/.profile"
|
||||
fi
|
||||
|
||||
touch "$profile"
|
||||
if ! grep -Fq "$line" "$profile"; then
|
||||
printf '\n%s\n' "$line" >> "$profile"
|
||||
fi
|
||||
USER_PROFILE="$profile"
|
||||
}
|
||||
|
||||
warn_missing_user_local() {
|
||||
if [[ "$INSTALL_MODE" != "user" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ ! -d "$HOME/.local" ]]; then
|
||||
printf '\033[33mWarning: %s/.local does not exist. The installer will create it automatically.\033[0m\n' "$HOME"
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
INSTALL_MODE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--source)
|
||||
SOURCE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--arch)
|
||||
ARCH_OVERRIDE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "$INSTALL_MODE" == "system" || "$INSTALL_MODE" == "user" ]] || die "--mode must be system or user"
|
||||
[[ "$SOURCE" == "github" || "$SOURCE" == "cdn" ]] || die "--source must be github or cdn"
|
||||
|
||||
OS_ID="$(detect_os)"
|
||||
ARCH_ID="$(detect_arch)"
|
||||
assert_privileges
|
||||
warn_missing_user_local
|
||||
|
||||
asset_for "$OS_ID" "$ARCH_ID"
|
||||
resolve_layout "$OS_ID"
|
||||
|
||||
TMP_DIR="$(create_secure_tmp_dir "picoclaw-download")"
|
||||
[[ -n "$TMP_DIR" ]] || die "Failed to create temporary working directory"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
log "Install mode: $INSTALL_MODE"
|
||||
log "OS/Arch: $OS_ID/$ARCH_ID"
|
||||
|
||||
if [[ "$OS_ID" == "linux" && "$INSTALL_MODE" == "system" ]]; then
|
||||
if install_via_package_manager_linux "$TMP_DIR"; then
|
||||
log "Installed via package manager: ${PM_RESULT%%:*}"
|
||||
log "Package file: ${PM_RESULT#*:}"
|
||||
log "Install layout is managed by the package manager."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
ARCHIVE_PATH="$TMP_DIR/$TAR_FILE"
|
||||
download_file "$(base_url)${TAR_FILE}" "$ARCHIVE_PATH"
|
||||
|
||||
mkdir -p "$INSTALL_ROOT"
|
||||
extract_archive "$ARCHIVE_PATH" "$INSTALL_ROOT"
|
||||
|
||||
if [[ "$LAYOUT_MODE" == "copy-executables" ]]; then
|
||||
copy_executables_to_bin "$INSTALL_ROOT" "$LINK_DIR"
|
||||
log "Extracted to: $INSTALL_ROOT"
|
||||
log "Copied executable files to: $LINK_DIR"
|
||||
for p in "${COPIED_PATHS[@]:-}"; do
|
||||
[[ -n "$p" ]] && log " - $p"
|
||||
done
|
||||
elif [[ "$LAYOUT_MODE" == "symlink" || "$LAYOUT_MODE" == "user" ]]; then
|
||||
link_expected_bins "$INSTALL_ROOT" "$LINK_DIR"
|
||||
log "Extracted to: $INSTALL_ROOT"
|
||||
log "Created links in: $LINK_DIR"
|
||||
for p in "${LINKED_PATHS[@]:-}"; do
|
||||
[[ -n "$p" ]] && log " - $p"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_MODE" == "user" ]]; then
|
||||
ensure_user_path_export
|
||||
log "PATH export ensured in: $USER_PROFILE"
|
||||
fi
|
||||
Loading…
Add table
Reference in a new issue