[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$InstallArgs
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if ($PSVersionTable.PSEdition -eq 'Desktop') {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
$staticBaseUrl = if ($env:BOT_FORGE_BASE_URL) {
$env:BOT_FORGE_BASE_URL.TrimEnd('/')
} else {
$null
}
$releaseTag = if ($env:BOT_FORGE_RELEASE_TAG) {
$env:BOT_FORGE_RELEASE_TAG
} else {
'v1.0.2'
}
$releaseAttachmentsBaseUrl = "https://api.gitcode.com/api/v5/repos/xuanwu/bot-forge/releases/$releaseTag/attach_files"
$architecture = try {
[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
} catch {
if ($env:PROCESSOR_ARCHITEW6432) {
$env:PROCESSOR_ARCHITEW6432
} else {
$env:PROCESSOR_ARCHITECTURE
}
}
$platform = switch ($architecture) {
{ $_ -in 'X64', 'AMD64' } { 'windows-x86_64'; break }
{ $_ -in 'Arm64', 'ARM64' } { 'windows-aarch64'; break }
default { throw "Unsupported Windows architecture: $architecture" }
}
$localAppData = if ($env:LOCALAPPDATA) {
$env:LOCALAPPDATA
} else {
[Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
}
$roamingAppData = if ($env:APPDATA) {
$env:APPDATA
} else {
[Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData)
}
if (-not $localAppData -or -not $roamingAppData) {
throw 'Unable to determine Windows application data directories; set BOT_FORGE_HOME and BOT_FORGE_CONFIG_DIR.'
}
$dataDir = if ($env:BOT_FORGE_HOME) {
$env:BOT_FORGE_HOME
} else {
Join-Path $localAppData 'bot-forge'
}
$binDir = if ($env:BOT_FORGE_BIN_DIR) {
$env:BOT_FORGE_BIN_DIR
} else {
Join-Path $dataDir 'bin'
}
$configDir = if ($env:BOT_FORGE_CONFIG_DIR) {
$env:BOT_FORGE_CONFIG_DIR
} else {
Join-Path $roamingAppData 'bot-forge'
}
$binaryRelativePath = "$platform-bot-forge.exe"
$configRelativePath = 'bot-forge.toml'
$checksumsRelativePath = 'SHA256SUMS'
$checksumsSha256 = $env:BOT_FORGE_CHECKSUMS_SHA256
$binaryPath = Join-Path $binDir 'bot-forge.exe'
$configPath = Join-Path $configDir 'bot-forge.toml'
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "bot-forge-$PID-$([guid]::NewGuid().ToString('N'))"
$exitCode = 1
function Download-File([string]$RelativePath, [string]$Destination) {
$uri = if ($staticBaseUrl) {
"$staticBaseUrl/$RelativePath"
} else {
"$releaseAttachmentsBaseUrl/$RelativePath/download"
}
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $Destination
}
function Get-ExpectedChecksum([string]$RelativePath, [string]$ChecksumsPath) {
foreach ($line in Get-Content -LiteralPath $ChecksumsPath) {
$match = [regex]::Match($line, '^\s*([A-Fa-f0-9]{64})\s+\*?(.+?)\s*$')
if ($match.Success -and $match.Groups[2].Value -eq $RelativePath) {
return $match.Groups[1].Value.ToLowerInvariant()
}
}
throw "SHA256SUMS does not contain a checksum for $RelativePath."
}
function Test-DownloadedFile([string]$RelativePath, [string]$Path, [string]$ChecksumsPath) {
$expected = Get-ExpectedChecksum $RelativePath $ChecksumsPath
$actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "Checksum verification failed: $RelativePath"
}
}
try {
New-Item -ItemType Directory -Force -Path $dataDir, $binDir, $configDir, $tempDir | Out-Null
$checksumsPath = Join-Path $tempDir 'SHA256SUMS'
$downloadedBinary = Join-Path $tempDir 'bot-forge.exe'
$downloadedConfig = Join-Path $tempDir 'bot-forge.toml'
Write-Host "Downloading bot-forge ($platform)..."
Download-File $checksumsRelativePath $checksumsPath
if (-not $checksumsSha256 -or $checksumsSha256 -notmatch '^[A-Fa-f0-9]{64}$') {
throw 'BOT_FORGE_CHECKSUMS_SHA256 must provide the trusted SHA256SUMS digest.'
}
$actualChecksumsHash = (Get-FileHash -LiteralPath $checksumsPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualChecksumsHash -ne $checksumsSha256.ToLowerInvariant()) {
throw 'SHA256SUMS release digest verification failed.'
}
Download-File $binaryRelativePath $downloadedBinary
Download-File $configRelativePath $downloadedConfig
Test-DownloadedFile $binaryRelativePath $downloadedBinary $checksumsPath
Test-DownloadedFile $configRelativePath $downloadedConfig $checksumsPath
$stagedBinary = Join-Path $binDir ".bot-forge.new-$([guid]::NewGuid().ToString('N')).exe"
$binaryReplacementBackup = Join-Path $binDir ".bot-forge.old-$([guid]::NewGuid().ToString('N')).exe"
Copy-Item -LiteralPath $downloadedBinary -Destination $stagedBinary
try {
if (Test-Path -LiteralPath $binaryPath) {
[System.IO.File]::Replace($stagedBinary, $binaryPath, $binaryReplacementBackup)
} else {
[System.IO.File]::Move($stagedBinary, $binaryPath)
}
} finally {
Remove-Item -LiteralPath $stagedBinary -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $binaryReplacementBackup -Force -ErrorAction SilentlyContinue
}
if (-not (Test-Path -LiteralPath $configPath)) {
$stagedConfig = Join-Path $configDir ".bot-forge.toml.new-$([guid]::NewGuid().ToString('N'))"
Copy-Item -LiteralPath $downloadedConfig -Destination $stagedConfig
try {
[System.IO.File]::Move($stagedConfig, $configPath)
} catch [System.IO.IOException] {
Remove-Item -LiteralPath $stagedConfig -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $configPath) {
Write-Host "Keeping configuration created concurrently: $configPath"
} else {
throw
}
}
} else {
Write-Host "Keeping existing configuration: $configPath"
}
Write-Host 'Download and verification complete; starting bot-forge.'
& $binaryPath install --config $configPath @InstallArgs
$exitCode = $LASTEXITCODE
} finally {
if (Test-Path -LiteralPath $tempDir) {
Remove-Item -LiteralPath $tempDir -Recurse -Force
}
}
exit $exitCode