aboutsummaryrefslogtreecommitdiff
path: root/website/install.ps1
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-09-01 14:48:45 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-09-01 14:48:45 +0300
commit8a6d697308162874a236c84aac0b0f8c7ac01f83 (patch)
treeed7dc7e8c6f50d66bde721fae78576aede929f66 /website/install.ps1
parentb54a78aa510b87d1e3b56c619b72eaeb59e4a2a1 (diff)
refactor: separate website presentation from runtime
Diffstat (limited to 'website/install.ps1')
-rw-r--r--website/install.ps1871
1 files changed, 0 insertions, 871 deletions
diff --git a/website/install.ps1 b/website/install.ps1
deleted file mode 100644
index 94a57314..00000000
--- a/website/install.ps1
+++ /dev/null
@@ -1,871 +0,0 @@
-[CmdletBinding()]
-param(
- [string]$Channel = $env:HERDR_CHANNEL,
- [string]$ManifestUrl = $env:HERDR_MANIFEST_URL,
- [string]$InstallDir = $env:HERDR_INSTALL_DIR,
- [string]$ExpectedBuildId = $env:HERDR_EXPECTED_BUILD_ID,
- [int]$Retain = 3,
- [string]$LocalPackagePath,
- [string]$LocalPackageFormat,
- [string]$LocalPackageIdentity,
- [string]$LocalPackageSha256
-)
-
-Set-StrictMode -Version Latest
-$ErrorActionPreference = "Stop"
-$ProgressPreference = "SilentlyContinue"
-
-$channelWasExplicit = -not [string]::IsNullOrWhiteSpace($Channel)
-if ($channelWasExplicit -and $Channel -notin @("stable", "preview")) {
- Write-Error "Invalid Herdr channel '$Channel'. Use 'stable' or 'preview'."
- exit 1
-}
-
-$localPackageValueCount = @(
- $LocalPackagePath,
- $LocalPackageFormat,
- $LocalPackageIdentity,
- $LocalPackageSha256 |
- Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
-).Count
-if ($localPackageValueCount -notin @(0, 4)) {
- throw "Local package mode requires path, format, identity, and SHA-256."
-}
-$useLocalPackage = $localPackageValueCount -eq 4
-if ($useLocalPackage -and $LocalPackageFormat -notin @("zip", "exe")) {
- throw "Local Herdr package has unsupported format '$LocalPackageFormat'."
-}
-
-function Write-Step {
- param([string]$Message)
- Write-Host "==> $Message"
-}
-
-function Write-WarningStep {
- param([string]$Message)
- Write-Warning $Message
-}
-
-function Get-HerdrCommandSource {
- $existing = Get-Command herdr -ErrorAction SilentlyContinue
- if ($null -eq $existing) {
- return $null
- }
-
- return $existing.Source
-}
-
-function Test-PathStartsWith {
- param(
- [string]$Path,
- [string]$Prefix
- )
-
- if ([string]::IsNullOrWhiteSpace($Path) -or [string]::IsNullOrWhiteSpace($Prefix)) {
- return $false
- }
-
- try {
- $normalizedPath = [System.IO.Path]::GetFullPath($Path)
- $normalizedPrefix = [System.IO.Path]::GetFullPath($Prefix).TrimEnd("\") + "\"
- return $normalizedPath.StartsWith($normalizedPrefix, [System.StringComparison]::OrdinalIgnoreCase)
- } catch {
- return $false
- }
-}
-
-function Path-Contains {
- param(
- [string]$PathValue,
- [string]$Entry
- )
-
- if ([string]::IsNullOrWhiteSpace($PathValue)) {
- return $false
- }
-
- $needle = $Entry.TrimEnd("\")
- foreach ($segment in $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)) {
- if ($segment.TrimEnd("\") -ieq $needle) {
- return $true
- }
- }
-
- return $false
-}
-
-function Prepend-PathEntry {
- param(
- [string]$PathValue,
- [string]$Entry
- )
-
- $needle = $Entry.TrimEnd("\")
- $segments = @($Entry)
- if (-not [string]::IsNullOrWhiteSpace($PathValue)) {
- $segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) |
- Where-Object { $_.TrimEnd("\") -ine $needle }
- }
-
- return ($segments -join ";")
-}
-
-function Update-PathRegistryEntry {
- param(
- [Microsoft.Win32.RegistryKey]$EnvironmentKey,
- [string]$Entry
- )
-
- $options = [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames
- $value = $EnvironmentKey.GetValue("Path", $null, $options)
- $kind = if ($null -eq $value) {
- [Microsoft.Win32.RegistryValueKind]::String
- } else {
- $EnvironmentKey.GetValueKind("Path")
- }
- $newValue = Prepend-PathEntry -PathValue $value -Entry $Entry
- if ($newValue -ceq $value) {
- return $false
- }
-
- $EnvironmentKey.SetValue("Path", $newValue, $kind)
- return $true
-}
-
-function Publish-EnvironmentChange {
- if (-not ("HerdrInstaller.EnvironmentNativeMethods" -as [type])) {
- Add-Type -Namespace HerdrInstaller -Name EnvironmentNativeMethods -MemberDefinition @'
-[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
-public static extern System.IntPtr SendMessageTimeout(
- System.IntPtr hWnd,
- uint message,
- System.UIntPtr wParam,
- string lParam,
- uint flags,
- uint timeout,
- out System.UIntPtr result);
-'@
- }
-
- $result = [UIntPtr]::Zero
- [HerdrInstaller.EnvironmentNativeMethods]::SendMessageTimeout(
- [IntPtr]0xffff,
- 0x1a,
- [UIntPtr]::Zero,
- "Environment",
- 0x0002,
- 1000,
- [ref]$result
- ) | Out-Null
-}
-
-function Get-ManifestAsset {
- param(
- [object]$Manifest,
- [string]$Target
- )
-
- $property = $Manifest.assets.PSObject.Properties[$Target]
- if ($null -eq $property) {
- throw "Release manifest does not include a binary for $Target."
- }
-
- $sha256 = $null
- $shaMapProperty = $Manifest.PSObject.Properties["sha256"]
- if ($null -ne $shaMapProperty -and $null -ne $shaMapProperty.Value) {
- $targetShaProperty = $shaMapProperty.Value.PSObject.Properties[$Target]
- if ($null -ne $targetShaProperty -and -not [string]::IsNullOrWhiteSpace([string]$targetShaProperty.Value)) {
- $sha256 = [string]$targetShaProperty.Value
- }
- }
-
- $asset = $property.Value
- if ($asset -is [string]) {
- $url = [string]$asset
- return [PSCustomObject]@{
- Url = $url
- Sha256 = $sha256
- Format = if ($url.EndsWith(".zip", [System.StringComparison]::OrdinalIgnoreCase)) { "zip" } else { "exe" }
- }
- }
-
- $urlProperty = $asset.PSObject.Properties["url"]
- if ($null -eq $urlProperty -or [string]::IsNullOrWhiteSpace([string]$urlProperty.Value)) {
- throw "Release manifest asset $Target is missing a URL."
- }
-
- $url = [string]$urlProperty.Value
- $formatProperty = $asset.PSObject.Properties["format"]
- $format = if ($null -eq $formatProperty -or [string]::IsNullOrWhiteSpace([string]$formatProperty.Value)) {
- if ($url.EndsWith(".zip", [System.StringComparison]::OrdinalIgnoreCase)) { "zip" } else { "exe" }
- } else {
- [string]$formatProperty.Value
- }
- if ($format -notin @("zip", "exe")) {
- throw "Release manifest asset $Target has unsupported format '$format'."
- }
- $shaProperty = $asset.PSObject.Properties["sha256"]
- if ($null -ne $shaProperty -and -not [string]::IsNullOrWhiteSpace([string]$shaProperty.Value)) {
- $sha256 = [string]$shaProperty.Value
- }
-
- return [PSCustomObject]@{
- Url = $url
- Sha256 = $sha256
- Format = $format
- }
-}
-
-function Invoke-CurlDownload {
- param(
- [string]$Uri,
- [string]$Destination
- )
-
- $parsedUri = $null
- if (-not [System.Uri]::TryCreate($Uri, [System.UriKind]::Absolute, [ref]$parsedUri) -or
- $parsedUri.Scheme -notin @("http", "https")) {
- throw "Herdr download URL must use HTTP or HTTPS: $Uri"
- }
-
- $curl = Get-Command curl.exe -CommandType Application -ErrorAction SilentlyContinue |
- Select-Object -First 1
- if ($null -eq $curl) {
- throw "Herdr installation requires curl.exe, which is included with supported Windows versions."
- }
-
- $arguments = @(
- "--fail",
- "--silent",
- "--show-error",
- "--location",
- "--connect-timeout", "30",
- "--speed-limit", "1024",
- "--speed-time", "30"
- )
- if ($parsedUri.Scheme -eq "https") {
- $arguments += @("--proto", "=https", "--tlsv1.2")
- }
- $arguments += @("--output", $Destination, "--", $Uri)
-
- $curlOutput = & $curl.Source @arguments 2>&1
- $curlExitCode = $LASTEXITCODE
- if ($curlExitCode -ne 0) {
- $detail = ($curlOutput | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine
- $message = "Failed to download $Uri (curl exit code $curlExitCode)."
- if (-not [string]::IsNullOrWhiteSpace($detail)) {
- $message += " $detail"
- }
- throw $message
- }
-}
-
-function ConvertTo-ManifestObject {
- param([object]$Manifest)
-
- if ($Manifest -isnot [string]) {
- return $Manifest
- }
-
- $json = $Manifest.TrimStart([char]0xFEFF)
- $utf8BomDecodedAsLatin1 = [string]::Concat([char]0x00EF, [char]0x00BB, [char]0x00BF)
- if ($json.StartsWith($utf8BomDecodedAsLatin1)) {
- $json = $json.Substring(3)
- }
-
- return $json | ConvertFrom-Json
-}
-
-function Get-RemoteManifest {
- param([string]$Uri)
-
- $manifestPath = Join-Path ([System.IO.Path]::GetTempPath()) ("herdr-manifest-" + [System.Guid]::NewGuid().ToString("N") + ".json")
- try {
- Invoke-CurlDownload -Uri $Uri -Destination $manifestPath
- return ConvertTo-ManifestObject -Manifest ([System.IO.File]::ReadAllText($manifestPath))
- } finally {
- Remove-Item -LiteralPath $manifestPath -Force -ErrorAction SilentlyContinue
- }
-}
-
-function Test-FileDigest {
- param(
- [string]$Path,
- [string]$ExpectedDigest
- )
-
- if ([string]::IsNullOrWhiteSpace($ExpectedDigest)) {
- throw "A SHA-256 checksum is required for $Path."
- }
- if ($ExpectedDigest -notmatch '^[0-9a-fA-F]{64}$') {
- throw "Invalid SHA-256 checksum for $Path."
- }
-
- $sha256 = [System.Security.Cryptography.SHA256]::Create()
- try {
- $bytes = [System.IO.File]::ReadAllBytes($Path)
- $actual = [System.BitConverter]::ToString($sha256.ComputeHash($bytes)).Replace("-", "").ToLowerInvariant()
- } finally {
- $sha256.Dispose()
- }
- if ($actual -ne $ExpectedDigest.ToLowerInvariant()) {
- throw "Downloaded Herdr checksum did not match. Expected $ExpectedDigest but got $actual."
- }
-}
-
-function Test-RegularFile {
- param([string]$Path)
-
- if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
- return $false
- }
- $item = Get-Item -LiteralPath $Path -Force
- return -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
-}
-
-function Test-RegularDirectory {
- param([string]$Path)
-
- if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
- return $false
- }
- $item = Get-Item -LiteralPath $Path -Force
- return -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)
-}
-
-function Test-HerdrReleaseComplete {
- param(
- [string]$ReleaseDir,
- [string]$Format
- )
-
- if (-not (Test-RegularDirectory -Path $ReleaseDir)) {
- return $false
- }
- $herdrExe = Join-Path $ReleaseDir "herdr.exe"
- if (-not (Test-RegularFile -Path $herdrExe)) {
- return $false
- }
- if ($Format -eq "exe") {
- return $true
- }
-
- $conptyRoot = Join-Path $ReleaseDir "conpty"
- if (-not (Test-RegularDirectory -Path $conptyRoot) -or
- -not (Test-RegularDirectory -Path (Join-Path $conptyRoot "x64")) -or
- -not (Test-RegularDirectory -Path (Join-Path $conptyRoot "arm64"))) {
- return $false
- }
- $markerPath = Join-Path $conptyRoot "herdr-conpty.json"
- $required = @(
- "conpty/conpty.dll",
- "conpty/x64/OpenConsole.exe",
- "conpty/arm64/OpenConsole.exe",
- "THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-LICENSE.txt",
- "THIRD-PARTY-NOTICES/Microsoft.Windows.Console.ConPTY-NOTICE.md"
- )
- foreach ($relative in $required) {
- if (-not (Test-RegularFile -Path (Join-Path $ReleaseDir ($relative -replace '/', '\')))) {
- return $false
- }
- }
- if (-not (Test-RegularFile -Path $markerPath)) {
- return $false
- }
-
- try {
- $marker = ConvertTo-ManifestObject -Manifest (Get-Content -LiteralPath $markerPath -Raw)
- $schemaProperty = $marker.PSObject.Properties["schema_version"]
- $packageProperty = $marker.PSObject.Properties["package"]
- $versionProperty = $marker.PSObject.Properties["version"]
- $architectureProperty = $marker.PSObject.Properties["architecture"]
- $filesProperty = $marker.PSObject.Properties["files"]
- if ($null -eq $schemaProperty -or [int]$schemaProperty.Value -ne 1 -or
- $null -eq $packageProperty -or [string]$packageProperty.Value -ne "Microsoft.Windows.Console.ConPTY" -or
- $null -eq $versionProperty -or [string]::IsNullOrWhiteSpace([string]$versionProperty.Value) -or
- $null -eq $architectureProperty -or [string]$architectureProperty.Value -ne "x86_64" -or
- $null -eq $filesProperty) {
- return $false
- }
-
- $expectedConptyFiles = @(
- "conpty/conpty.dll",
- "conpty/x64/OpenConsole.exe",
- "conpty/arm64/OpenConsole.exe"
- )
- $markerFileNames = @($filesProperty.Value.PSObject.Properties | ForEach-Object { $_.Name })
- if (@(Compare-Object $expectedConptyFiles $markerFileNames).Count -ne 0) {
- return $false
- }
-
- $bundleEntries = @(Get-ChildItem -LiteralPath $conptyRoot -Force -Recurse)
- if (@($bundleEntries | Where-Object {
- $_.Attributes -band [IO.FileAttributes]::ReparsePoint
- }).Count -ne 0) {
- return $false
- }
- $releaseRoot = [System.IO.Path]::GetFullPath($ReleaseDir).TrimEnd('\')
- $actualBundleFiles = @($bundleEntries | Where-Object { -not $_.PSIsContainer } | ForEach-Object {
- $_.FullName.Substring($releaseRoot.Length + 1).Replace('\', '/')
- })
- $expectedBundleFiles = @($expectedConptyFiles) + "conpty/herdr-conpty.json"
- if (@(Compare-Object $expectedBundleFiles $actualBundleFiles).Count -ne 0) {
- return $false
- }
- foreach ($relative in $expectedConptyFiles) {
- $digestProperty = $filesProperty.Value.PSObject.Properties[$relative]
- if ($null -eq $digestProperty) {
- return $false
- }
- Test-FileDigest -Path (Join-Path $ReleaseDir ($relative -replace '/', '\')) -ExpectedDigest ([string]$digestProperty.Value)
- }
- } catch {
- return $false
- }
- return $true
-}
-
-function Move-DirectoryWithRetry {
- param(
- [string]$Source,
- [string]$Destination,
- [int]$TimeoutMilliseconds = 5000
- )
-
- $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMilliseconds)
- while ($true) {
- try {
- [System.IO.Directory]::Move($Source, $Destination)
- return
- } catch {
- $retryable = $false
- $exception = $_.Exception
- while ($null -ne $exception) {
- if ($exception -is [System.IO.IOException] -or
- $exception -is [System.UnauthorizedAccessException]) {
- $retryable = $true
- break
- }
- $exception = $exception.InnerException
- }
- if (-not $retryable -or
- [DateTime]::UtcNow -ge $deadline -or
- -not (Test-Path -LiteralPath $Source -PathType Container) -or
- (Test-Path -LiteralPath $Destination)) {
- throw
- }
- Start-Sleep -Milliseconds 100
- }
- }
-}
-
-function Remove-DirectoryWithRetry {
- param(
- [string]$Path,
- [int]$TimeoutMilliseconds = 5000
- )
-
- $fullPath = [System.IO.Path]::GetFullPath($Path)
- $extendedPath = if ($fullPath.StartsWith("\\")) {
- "\\?\UNC\" + $fullPath.TrimStart([char]'\')
- } else {
- "\\?\" + $fullPath
- }
- $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMilliseconds)
- while (Test-Path -LiteralPath $Path) {
- try {
- [System.IO.Directory]::Delete($extendedPath, $true)
- return
- } catch {
- if ([DateTime]::UtcNow -ge $deadline) {
- Write-WarningStep "Herdr installed successfully but could not remove a temporary release backup at $Path."
- return
- }
- Start-Sleep -Milliseconds 100
- }
- }
-}
-
-function Invoke-WithInstallLock {
- param(
- [string]$LockPath,
- [scriptblock]$Script
- )
-
- New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LockPath) | Out-Null
- $lock = $null
- while ($null -eq $lock) {
- try {
- $lock = [System.IO.File]::Open(
- $LockPath,
- [System.IO.FileMode]::OpenOrCreate,
- [System.IO.FileAccess]::ReadWrite,
- [System.IO.FileShare]::None
- )
- } catch [System.IO.IOException] {
- Start-Sleep -Milliseconds 250
- }
- }
-
- try {
- & $Script
- } finally {
- $lock.Dispose()
- }
-}
-
-function Test-IsJunction {
- param([string]$Path)
-
- if (-not (Test-Path -LiteralPath $Path)) {
- return $false
- }
-
- $item = Get-Item -LiteralPath $Path -Force
- return ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $item.LinkType -eq "Junction"
-}
-
-function Set-ManagedJunction {
- param(
- [string]$LinkPath,
- [string]$TargetPath,
- [string]$ManagedTargetPrefix,
- [bool]$AllowLegacyHerdrBinMigration = $false
- )
-
- if (Test-Path -LiteralPath $LinkPath) {
- $item = Get-Item -LiteralPath $LinkPath -Force
- if (Test-IsJunction -Path $LinkPath) {
- $existingTarget = [string]$item.Target
- if (-not [string]::IsNullOrWhiteSpace($ManagedTargetPrefix)) {
- $ownedPrefix = $ManagedTargetPrefix.TrimEnd("\")
- if (-not $existingTarget.StartsWith($ownedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
- throw "Refusing to retarget junction at $LinkPath because it is not managed by this installer."
- }
- }
- if ($existingTarget.Equals($TargetPath, [System.StringComparison]::OrdinalIgnoreCase)) {
- return
- }
- Remove-Item -LiteralPath $LinkPath -Recurse -Force
- } elseif ($item.PSIsContainer) {
- if ((Get-ChildItem -LiteralPath $LinkPath -Force | Select-Object -First 1) -ne $null) {
- if (-not (Move-LegacyHerdrBinDirectory -Path $LinkPath -AllowMigration $AllowLegacyHerdrBinMigration)) {
- throw "Refusing to replace non-empty directory at $LinkPath with a junction."
- }
- } else {
- Remove-Item -LiteralPath $LinkPath -Recurse -Force
- }
- } else {
- throw "Refusing to replace file at $LinkPath with a junction."
- }
- }
-
- New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LinkPath) | Out-Null
- New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
-}
-
-function Move-LegacyHerdrBinDirectory {
- param(
- [string]$Path,
- [bool]$AllowMigration
- )
-
- if (-not $AllowMigration) {
- return $false
- }
-
- $entries = @(Get-ChildItem -LiteralPath $Path -Force)
- if (($entries | Where-Object { $_.PSIsContainer } | Select-Object -First 1) -ne $null) {
- return $false
- }
-
- if (($entries | Where-Object { $_.Name -ieq "herdr.exe" } | Select-Object -First 1) -eq $null) {
- return $false
- }
-
- $legacyPath = "$Path.legacy.$([System.Guid]::NewGuid().ToString("N"))"
- Move-Item -LiteralPath $Path -Destination $legacyPath
- Write-Step "Moved legacy Herdr bin directory to $legacyPath."
- return $true
-}
-
-function Remove-StaleInstallArtifacts {
- param([string]$ReleasesDir)
-
- if (-not (Test-Path -LiteralPath $ReleasesDir -PathType Container)) {
- return
- }
-
- Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -Filter ".staging.*" -ErrorAction SilentlyContinue |
- Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
-}
-
-function Remove-OldReleases {
- param(
- [string]$ReleasesDir,
- [string]$CurrentReleaseDir,
- [int]$Keep
- )
-
- if ($Keep -lt 1 -or -not (Test-Path -LiteralPath $ReleasesDir -PathType Container)) {
- return
- }
-
- $currentFullPath = [System.IO.Path]::GetFullPath($CurrentReleaseDir)
- $releaseDirs = Get-ChildItem -LiteralPath $ReleasesDir -Force -Directory -ErrorAction SilentlyContinue |
- Where-Object { -not $_.Name.StartsWith(".staging.") -and -not $_.Name.StartsWith(".backup.") } |
- Sort-Object LastWriteTimeUtc -Descending
- $kept = 0
- foreach ($dir in $releaseDirs) {
- $dirFullPath = [System.IO.Path]::GetFullPath($dir.FullName)
- if ($dirFullPath.Equals($currentFullPath, [System.StringComparison]::OrdinalIgnoreCase)) {
- $kept += 1
- continue
- }
- if ($kept -lt $Keep) {
- $kept += 1
- continue
- }
- Remove-Item -LiteralPath $dir.FullName -Recurse -Force -ErrorAction SilentlyContinue
- }
-}
-
-function Resolve-HerdrVersion {
- param(
- [object]$Manifest,
- [string]$SelectedChannel
- )
-
- if ($SelectedChannel -eq "preview") {
- if ([string]::IsNullOrWhiteSpace([string]$Manifest.base_version) -or [string]::IsNullOrWhiteSpace([string]$Manifest.build_id)) {
- throw "Preview manifest is missing base_version or build_id."
- }
- return "$($Manifest.base_version)-preview.$($Manifest.build_id)"
- }
-
- if ([string]::IsNullOrWhiteSpace([string]$Manifest.version)) {
- throw "Stable manifest is missing version."
- }
- return [string]$Manifest.version
-}
-
-if ($env:OS -ne "Windows_NT") {
- Write-Error "install.ps1 supports Windows only. Use install.sh on Linux or macOS."
- exit 1
-}
-
-if (-not [Environment]::Is64BitOperatingSystem) {
- Write-Error "Herdr requires 64-bit Windows."
- exit 1
-}
-
-$architecture = [System.Runtime.InteropServices.RuntimeInformation,mscorlib]::OSArchitecture.ToString()
-switch ($architecture) {
- "X64" {
- $target = "windows-x86_64"
- $targetTriple = "x86_64-pc-windows-msvc"
- }
- "Arm64" {
- $target = "windows-x86_64"
- $targetTriple = "x86_64-pc-windows-msvc"
- Write-Step "Windows ARM64 detected; installing the x86_64 build under Windows emulation."
- }
- default {
- Write-Error "Unsupported Windows architecture: $architecture"
- exit 1
- }
-}
-
-$herdrHome = if ([string]::IsNullOrWhiteSpace($env:HERDR_HOME)) {
- Join-Path $env:USERPROFILE ".herdr"
-} else {
- $env:HERDR_HOME
-}
-$herdrHome = [System.IO.Path]::GetFullPath($herdrHome)
-$standaloneRoot = Join-Path $herdrHome "packages\standalone"
-$releasesDir = Join-Path $standaloneRoot "releases"
-$currentDir = Join-Path $standaloneRoot "current"
-$lockPath = Join-Path $standaloneRoot "install.lock"
-
-$defaultVisibleBinDir = Join-Path $env:LOCALAPPDATA "Programs\Herdr\bin"
-$visibleBinDir = if ([string]::IsNullOrWhiteSpace($InstallDir)) {
- $defaultVisibleBinDir
-} else {
- $InstallDir
-}
-$allowLegacyVisibleBinMigration = $false
-try {
- $allowLegacyVisibleBinMigration = [System.IO.Path]::GetFullPath($visibleBinDir).TrimEnd("\").Equals(
- [System.IO.Path]::GetFullPath($defaultVisibleBinDir).TrimEnd("\"),
- [System.StringComparison]::OrdinalIgnoreCase
- )
-} catch {
- $allowLegacyVisibleBinMigration = $false
-}
-
-$existingHerdr = Get-HerdrCommandSource
-if (-not [string]::IsNullOrWhiteSpace($existingHerdr) -and -not (Test-PathStartsWith -Path $existingHerdr -Prefix $visibleBinDir)) {
- Write-Step "Detected existing Herdr command at $existingHerdr"
- Write-WarningStep "PATH order decides which Herdr runs. This installer will put $visibleBinDir first for future and current PowerShell sessions."
-}
-
-if ($useLocalPackage) {
- $versionIdentity = $LocalPackageIdentity
- $asset = [PSCustomObject]@{
- Sha256 = $LocalPackageSha256
- Format = $LocalPackageFormat
- }
-} else {
- if (-not $channelWasExplicit) {
- if (-not [string]::IsNullOrWhiteSpace($existingHerdr)) {
- $detectedChannel = [string](& $existingHerdr channel show 2>$null | Select-Object -Last 1)
- $detectedChannel = $detectedChannel.Trim()
- if ($LASTEXITCODE -ne 0 -or $detectedChannel -notin @("stable", "preview")) {
- throw "Could not determine the existing Herdr update channel. Rerun with -Channel stable or -Channel preview."
- }
- $Channel = $detectedChannel
- Write-Step "Preserving existing Herdr $Channel channel"
- } elseif (-not [string]::IsNullOrWhiteSpace($ManifestUrl) -and $ManifestUrl -match "/preview\.json$") {
- $Channel = "preview"
- } else {
- $Channel = "stable"
- }
- }
-
- if ([string]::IsNullOrWhiteSpace($ManifestUrl)) {
- $ManifestUrl = if ($Channel -eq "preview") {
- "https://herdr.dev/preview.json"
- } else {
- "https://herdr.dev/latest.json"
- }
- }
-
- Write-Step "Fetching Herdr $Channel manifest"
- $manifest = Get-RemoteManifest -Uri $ManifestUrl
- $manifestChannelProperty = $manifest.PSObject.Properties["channel"]
- if (-not $channelWasExplicit -and $null -ne $manifestChannelProperty -and [string]$manifestChannelProperty.Value -eq "preview") {
- $Channel = "preview"
- }
- $assetsProperty = $manifest.PSObject.Properties["assets"]
- $assetProperty = if ($null -eq $assetsProperty) {
- $null
- } else {
- $assetsProperty.Value.PSObject.Properties[$target]
- }
- if ($null -eq $assetProperty -and
- -not $channelWasExplicit -and
- $Channel -eq "stable" -and
- $ManifestUrl -match "/latest\.json$") {
- Write-WarningStep "The stable manifest does not include Windows yet; using preview during the stable-channel rollout."
- $Channel = "preview"
- $ManifestUrl = $ManifestUrl.Substring(0, $ManifestUrl.Length - "latest.json".Length) + "preview.json"
- Write-Step "Fetching Herdr preview manifest"
- $manifest = Get-RemoteManifest -Uri $ManifestUrl
- }
- $asset = Get-ManifestAsset -Manifest $manifest -Target $target
- if (-not [string]::IsNullOrWhiteSpace($ExpectedBuildId) -and [string]$manifest.build_id -ne $ExpectedBuildId) {
- throw "Preview manifest changed while updating. Expected build $ExpectedBuildId but found $($manifest.build_id). Run herdr update again."
- }
- $versionIdentity = Resolve-HerdrVersion -Manifest $manifest -SelectedChannel $Channel
-}
-$safeVersionIdentity = $versionIdentity -replace '[^0-9A-Za-z._-]', '-'
-$releaseName = "$safeVersionIdentity-$targetTriple"
-$releaseDir = Join-Path $releasesDir $releaseName
-
-Write-Step "Installing Herdr $versionIdentity for $targetTriple"
-$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("herdr-install-" + [System.Guid]::NewGuid().ToString("N"))
-New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
-
-try {
- Invoke-WithInstallLock -LockPath $lockPath -Script {
- Remove-StaleInstallArtifacts -ReleasesDir $releasesDir
-
- if (-not (Test-HerdrReleaseComplete -ReleaseDir $releaseDir -Format $asset.Format)) {
- $downloadPath = if ($useLocalPackage) {
- $LocalPackagePath
- } else {
- Join-Path $tempDir "herdr-download.$($asset.Format)"
- }
- $stagingDir = Join-Path $releasesDir ".staging.$releaseName.$PID"
- if (-not $useLocalPackage) {
- Write-Step "Downloading Herdr"
- Invoke-CurlDownload -Uri $asset.Url -Destination $downloadPath
- }
- Test-FileDigest -Path $downloadPath -ExpectedDigest $asset.Sha256
-
- if ($asset.Format -eq "zip") {
- Expand-Archive -LiteralPath $downloadPath -DestinationPath $stagingDir
- } else {
- New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
- Copy-Item -LiteralPath $downloadPath -Destination (Join-Path $stagingDir "herdr.exe")
- }
- if (-not (Test-HerdrReleaseComplete -ReleaseDir $stagingDir -Format $asset.Format)) {
- throw "Downloaded Herdr package is incomplete or failed ConPTY verification."
- }
- $stagedHerdr = Join-Path $stagingDir "herdr.exe"
- & $stagedHerdr --version *> $null
- if ($LASTEXITCODE -ne 0) {
- throw "Downloaded Herdr command failed verification: $stagedHerdr --version"
- }
- $backupDir = $null
- if (Test-Path -LiteralPath $releaseDir) {
- $backupDir = Join-Path $releasesDir ".backup.$releaseName.$([System.Guid]::NewGuid().ToString('N'))"
- [System.IO.Directory]::Move($releaseDir, $backupDir)
- }
- try {
- Move-DirectoryWithRetry -Source $stagingDir -Destination $releaseDir
- } catch {
- if ($null -ne $backupDir -and -not (Test-Path -LiteralPath $releaseDir)) {
- [System.IO.Directory]::Move($backupDir, $releaseDir)
- }
- Write-WarningStep "Windows could not activate the downloaded release. Another process may have a package file open, such as antivirus or indexing. No incomplete release was activated. Run herdr update again."
- throw
- }
- }
-
- $releaseHerdr = Join-Path $releaseDir "herdr.exe"
- & $releaseHerdr --version *> $null
- if ($LASTEXITCODE -ne 0) {
- throw "Installed Herdr command failed verification: $releaseHerdr --version"
- }
- Get-ChildItem -LiteralPath $releasesDir -Force -Directory -Filter ".backup.$releaseName.*" -ErrorAction SilentlyContinue |
- ForEach-Object { Remove-DirectoryWithRetry -Path $_.FullName }
-
- Set-ManagedJunction -LinkPath $currentDir -TargetPath $releaseDir -ManagedTargetPrefix $releasesDir
- Set-ManagedJunction -LinkPath $visibleBinDir -TargetPath $releaseDir -ManagedTargetPrefix $standaloneRoot -AllowLegacyHerdrBinMigration $allowLegacyVisibleBinMigration
-
- Remove-OldReleases -ReleasesDir $releasesDir -CurrentReleaseDir $releaseDir -Keep $Retain
- }
-} finally {
- Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue
-}
-
-$userEnvironmentKey = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey("Environment")
-if ($null -eq $userEnvironmentKey) {
- throw "Unable to open the current user's environment registry key."
-}
-try {
- $userPathChanged = Update-PathRegistryEntry -EnvironmentKey $userEnvironmentKey -Entry $visibleBinDir
-} finally {
- $userEnvironmentKey.Dispose()
-}
-if ($userPathChanged) {
- Publish-EnvironmentChange
- Write-Step "PATH updated for future PowerShell sessions."
-} else {
- Write-Step "$visibleBinDir is already first on PATH."
-}
-
-$newProcessPath = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir
-if ($newProcessPath -cne $env:Path) {
- $env:Path = $newProcessPath
-}
-
-$resolvedHerdr = Get-HerdrCommandSource
-if (-not (Test-PathStartsWith -Path $resolvedHerdr -Prefix $visibleBinDir)) {
- Write-WarningStep "PowerShell still resolves herdr to $resolvedHerdr. Open a new PowerShell window or inspect PATH order manually."
-}
-
-Write-Step "Current PowerShell session: herdr"
-Write-Step "Future PowerShell windows: open a new PowerShell window and run: herdr"
-Write-Host "Herdr $versionIdentity installed successfully."