<# .SYNOPSIS Moneymoor installer (Windows). .DESCRIPTION Generated by ariza from App::Moneymoor's ariza.toml. Do not edit this file: edit ariza's resources/templates/install-windows.ps1.j2 and re-run `ariza installers --app=`. Downloads the prebuilt Moneymoor bundle for this machine from the m-doughty/App-Moneymoor releases, verifies its sha256, and unpacks it into %LOCALAPPDATA%\Moneymoor\versions\\. A junction named `current` points at the version in use, and %LOCALAPPDATA%\Moneymoor\current\bin goes on your user PATH once. Nothing is compiled, nothing needs administrator rights, and nothing outside %LOCALAPPDATA%\Moneymoor and your user PATH is written. The installed application is then run once, so that whatever a first launch has to do is done here rather than the first time you want the program. .PARAMETER Version Install a specific release tag (e.g. -Version v1.2.3) rather than the latest one. .PARAMETER Url Install this archive instead, bypassing GitHub entirely. A local file path works as well as a URL. Also settable as $env:MONEYMOOR_BUNDLE_URL. .PARAMETER InsecureNoVerify With -Url only: proceed even when the source has no sibling .sha256. .EXAMPLE irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/install.ps1 | iex .EXAMPLE # With arguments, a piped script needs to become a script block first: & ([scriptblock]::Create((irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/install.ps1))) -Version v1.2.3 #> [CmdletBinding()] param( [string] $Version, [string] $Url, [switch] $InsecureNoVerify, [string] $ArizaUpdateCandidate, [string] $ArizaHandoff, [string] $ArizaNonce, [string] $ArizaExpectedCurrent ) $AppDisplay = 'Moneymoor' $AppExec = 'moneymoor' $AppRepo = 'm-doughty/App-Moneymoor' # The platforms App::Moneymoor publishes bundles for. Detection produces one # of these strings or nothing at all; there is deliberately no "closest # match", because a bundle for the wrong architecture does not run. $ArizaSlugs = @('windows-x86_64') $ArizaRoot = Join-Path $env:LOCALAPPDATA $AppDisplay $ArizaVersions = Join-Path $ArizaRoot 'versions' $ArizaBinDir = Join-Path (Join-Path $ArizaRoot 'current') 'bin' $ArizaState = Join-Path $ArizaRoot '.ariza\update-v1' $ArizaPrivate = [bool]($ArizaUpdateCandidate -or $ArizaHandoff -or $ArizaNonce -or $ArizaExpectedCurrent) ############################# common runtime ################################## ############################################################################### # ariza installer runtime (Windows / PowerShell) # # Inlined verbatim into install.ps1 and uninstall.ps1 at render time, so a # generated installer is one self-contained file. # # Adapted from the pre-bundle installers' template/lib/common-windows.ps1: # the registry, the package managers and the shortcut helpers are gone (a # bundle installs none of that), the user-PATH machinery is kept, because # an unguarded PATH write is how a user ends up with the same directory in # it eleven times. # # Callers must have set: $AppDisplay, $AppExec, $ArizaRoot. ############################################################################### $ErrorActionPreference = 'Stop' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 function Ariza-Log { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Blue } function Ariza-Ok { param([string]$Message) Write-Host "ok $Message" -ForegroundColor Green } function Ariza-Warn { param([string]$Message) Write-Host "!! $Message" -ForegroundColor Yellow } function Ariza-Err { param([string]$Message) Write-Host "error $Message" -ForegroundColor Red exit 1 } # ---- downloading ------------------------------------------------------------- function Ariza-Download { param([string]$Url, [string]$Dest) # -UseBasicParsing keeps this working on a Server Core box with no # Internet Explorer engine, which is where Invoke-WebRequest's default # parser falls over. Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing } # ---- checksums --------------------------------------------------------------- function Ariza-Sha256 { param([string]$Path) # Use the .NET implementation directly. Get-FileHash normally ships in # Microsoft.PowerShell.Utility, but minimal/non-standard PowerShell 5.1 # environments can lack that cmdlet while still providing the runtime # APIs the installer needs. $stream = [IO.File]::OpenRead($Path) try { $sha = [Security.Cryptography.SHA256]::Create() try { $hash = $sha.ComputeHash($stream) return ([BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } } finally { $stream.Dispose() } } function Ariza-FirstWord { param([string]$Path) # A ` ` checksum file, as shasum -c reads. $line = (Get-Content -LiteralPath $Path -TotalCount 1) if (-not $line) { return '' } return ($line -split '\s+')[0].ToLower() } function Ariza-VerifySha256 { param([string]$Path, [string]$Expected) $actual = Ariza-Sha256 $Path if ($actual -ne $Expected.ToLower()) { Ariza-Err "checksum mismatch for $(Split-Path -Leaf $Path): expected $Expected, got $actual -- not installing" } } # ---- unpacking --------------------------------------------------------------- function Ariza-Extract { param([string]$Archive, [string]$Into) New-Item -ItemType Directory -Path $Into -Force | Out-Null if ($Archive.ToLower().EndsWith('.zip')) { Expand-Archive -LiteralPath $Archive -DestinationPath $Into -Force } else { # bsdtar has shipped in Windows since 10 1803 and reads .tar.gz, # which is what ariza packages every platform's bundle as. $tar = Get-Command tar.exe -ErrorAction SilentlyContinue if (-not $tar) { Ariza-Err "need tar.exe (Windows 10 1803 or later) to unpack $(Split-Path -Leaf $Archive)" } & $tar.Path -x -z -f $Archive -C $Into if ($LASTEXITCODE -ne 0) { Ariza-Err "tar could not unpack $(Split-Path -Leaf $Archive) (exit $LASTEXITCODE)" } } } # ---- user PATH --------------------------------------------------------------- # # The user environment in the registry (HKCU\Environment), never the # machine one: nothing here needs administrator rights, and nothing here # should be visible to other accounts. function Ariza-PersistPath { param([string]$Dir) $current = [Environment]::GetEnvironmentVariable('PATH', 'User') if (-not $current) { $current = '' } $parts = $current -split ';' | Where-Object { $_ -ne '' } if ($parts -notcontains $Dir) { [Environment]::SetEnvironmentVariable('PATH', ((@($Dir) + $parts) -join ';'), 'User') Ariza-Ok "added $Dir to your PATH" } if (($env:PATH -split ';') -notcontains $Dir) { $env:PATH = "$Dir;$env:PATH" } } function Ariza-UnpersistPath { param([string]$Dir) $current = [Environment]::GetEnvironmentVariable('PATH', 'User') if (-not $current) { return } $parts = $current -split ';' | Where-Object { $_ -ne '' } if ($parts -notcontains $Dir) { return } $kept = $parts | Where-Object { $_ -ne $Dir } [Environment]::SetEnvironmentVariable('PATH', ($kept -join ';'), 'User') Ariza-Ok "removed $Dir from your PATH" } # ---- the `current` junction -------------------------------------------------- # # A junction rather than a symbolic link: creating a symlink on Windows # needs either administrator rights or Developer Mode, and an installer # that demands either for a per-user install is one nobody runs. function Ariza-RemoveLink { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { return } $item = Get-Item -LiteralPath $Path -Force if (-not $item.LinkType) { Ariza-Err "$Path exists and is not a junction -- move it aside and re-run" } # .Delete() removes the junction itself; Remove-Item -Recurse would # walk through it and delete the target's contents. $item.Delete() } function Ariza-PointCurrent { param([string]$Target) $link = Join-Path $ArizaRoot 'current' Ariza-RemoveLink $link New-Item -ItemType Junction -Path $link -Target $Target | Out-Null } ############################################################################### function Ariza-DetectSlug { $arch = $env:PROCESSOR_ARCHITECTURE if (-not $arch) { $arch = '' } switch ($arch.ToUpper()) { 'AMD64' { return 'windows-x86_64' } 'ARM64' { return 'windows-arm64' } 'X86' { # A 32-bit PowerShell on a 64-bit machine reports x86; ask # what the hardware actually is before giving up on it. if ($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64') { return 'windows-x86_64' } if ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { return 'windows-arm64' } return '' } } return '' } function Ariza-VersionFromName { param([string]$Name) # ariza names a bundle --. Stripping a known slug # off the end rather than splitting on dashes is what makes a version # containing one (1.0-rc1) parse correctly. $n = $Name foreach ($ext in @('.tar.gz', '.tgz', '.zip')) { if ($n.ToLower().EndsWith($ext)) { $n = $n.Substring(0, $n.Length - $ext.Length) } } if (-not $n.StartsWith("$AppExec-")) { return '' } $n = $n.Substring($AppExec.Length + 1) foreach ($slug in $ArizaSlugs) { if ($n.EndsWith("-$slug")) { return $n.Substring(0, $n.Length - $slug.Length - 1) } } return '' } function Ariza-LatestTag { # The releases/latest redirect names the tag, so this needs no JSON # parsing and no API token. $url = "https://github.com/$AppRepo/releases/latest" try { $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -MaximumRedirection 5 } catch { return '' } $final = '' if ($resp.BaseResponse.PSObject.Properties.Name -contains 'ResponseUri') { $final = $resp.BaseResponse.ResponseUri.AbsoluteUri # PowerShell 5.1 } elseif ($resp.BaseResponse.PSObject.Properties.Name -contains 'RequestMessage') { $final = $resp.BaseResponse.RequestMessage.RequestUri.AbsoluteUri # PowerShell 7 } if (-not $final) { return '' } return ($final -split '/')[-1] } function Ariza-LinkBin { Ariza-PersistPath $ArizaBinDir } function Ariza-HasEntryPoint { param([string]$Dir) # A bundle launches from bin\.exe where one was staged, and # from bin\.cmd otherwise. Either is a complete install: the # directory goes on PATH, and PATHEXT already prefers the executable # over the batch file where both are there. foreach ($leaf in @("bin\$AppExec.exe", "bin\$AppExec.cmd")) { if (Test-Path -LiteralPath (Join-Path $Dir $leaf)) { return $true } } return $false } function Ariza-FullPath { param([string]$Path) return [IO.Path]::GetFullPath($Path).TrimEnd('\') } function Ariza-ValidatePrivate { if (-not $ArizaPrivate) { return } if (-not $ArizaUpdateCandidate -or -not $ArizaHandoff -or -not $ArizaNonce -or -not $ArizaExpectedCurrent) { Ariza-Err 'private update mode requires candidate, handoff, nonce, and expected-current' } if ($ArizaUpdateCandidate -cnotmatch '^[0-9]+\.[0-9]+\.[0-9]+$') { Ariza-Err 'private update candidate must be a bare ASCII x.y.z version' } if ($ArizaNonce -cnotmatch '^[0-9a-f]{64}$') { Ariza-Err 'private update nonce must be 64 lowercase hexadecimal characters' } if ($Version -or $Url -or $InsecureNoVerify -or $env:MONEYMOOR_BUNDLE_URL) { Ariza-Err 'public version/url/insecure options are forbidden in private update mode' } if (-not (Test-Path -LiteralPath $ArizaExpectedCurrent -PathType Container)) { Ariza-Err 'expected current bundle does not exist' } foreach ($statePath in @((Join-Path $ArizaRoot '.ariza'), $ArizaState)) { if (-not (Test-Path -LiteralPath $statePath)) { continue } $stateItem = Get-Item -LiteralPath $statePath -Force if (($stateItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Ariza-Err 'private update state must not be a reparse point' } } New-Item -ItemType Directory -Path $ArizaState -Force | Out-Null $handoffParent = Ariza-FullPath (Split-Path -Parent $ArizaHandoff) $handoffName = Split-Path -Leaf $ArizaHandoff if ($handoffParent -ceq (Ariza-FullPath $ArizaState)) { if ($handoffName -cne 'handoff' -and -not $handoffName.StartsWith('handoff.')) { Ariza-Err 'private handoff has an invalid name' } } else { $handoffDir = Get-Item -LiteralPath $handoffParent -Force -ErrorAction SilentlyContinue if (-not $handoffDir -or ($handoffDir.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or (Ariza-FullPath (Split-Path -Parent $handoffParent)) -cne (Ariza-FullPath $ArizaState) -or -not $handoffDir.Name.StartsWith('handoff-') -or $handoffName -cne 'result') { Ariza-Err 'private handoff must be inside update-v1 state' } } if (Test-Path -LiteralPath $ArizaHandoff) { Ariza-Err 'private handoff path already exists' } } function Ariza-PhysicalLinkTarget { param([string]$Link) if (-not (Test-Path -LiteralPath $Link)) { return '' } $item = Get-Item -LiteralPath $Link -Force if (-not $item.LinkType) { Ariza-Err "$Link exists and is not a junction" } return Ariza-FullPath $item.Target } function Ariza-VersionForPhysical { param([string]$Physical) if (-not $Physical) { return '' } $parent = Ariza-FullPath (Split-Path -Parent $Physical) if ($parent -cne (Ariza-FullPath $ArizaVersions)) { Ariza-Err 'managed pointer target is outside the versions directory' } return Split-Path -Leaf $Physical } function Ariza-SetJunctionAtomic { param([string]$Name, [string]$Target) $link = Join-Path $ArizaRoot $Name $new = "$link.new-$PID" if (Test-Path -LiteralPath $new) { (Get-Item -LiteralPath $new -Force).Delete() } New-Item -ItemType Junction -Path $new -Target $Target | Out-Null Ariza-RemoveLink $link Move-Item -LiteralPath $new -Destination $link } function Ariza-WriteAtomicText { param([string]$Path, [string]$Text) $tmp = "$Path.tmp-$PID" [IO.File]::WriteAllText($tmp, $Text, (New-Object Text.UTF8Encoding($false))) Move-Item -LiteralPath $tmp -Destination $Path -Force } function Ariza-ValidatePrivateManifest { param([string]$Top) if (-not $ArizaPrivate) { return } $path = Join-Path $Top 'ariza-manifest.json' if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Ariza-Err 'private update bundle has no ariza-manifest.json' } try { $manifest = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json } catch { Ariza-Err 'private update bundle has malformed manifest JSON' } if ($manifest.'ariza-manifest' -ne 1 -or $manifest.app.name -cne 'App::Moneymoor' -or $manifest.app.exec -cne 'moneymoor' -or $manifest.app.version -cne $ArizaUpdateCandidate) { Ariza-Err 'private update bundle identity does not match the candidate' } $updates = $manifest.updates if (-not $updates -or $updates.protocol -ne 1 -or -not $updates.enabled -or $updates.repository -cne 'm-doughty/App-Moneymoor' -or $updates.coordinator -cne 'libexec/ariza/update.raku' -or $updates.installer -cne 'libexec/ariza/install.ps1') { Ariza-Err 'private update bundle has invalid update protocol metadata' } foreach ($relative in @($updates.coordinator, $updates.installer, $updates.'application-target')) { if (-not $relative -or [IO.Path]::IsPathRooted($relative) -or $relative -match '(^|[\\/])\.\.([\\/]|$)') { Ariza-Err 'private update bundle names an unsafe updater path' } if (-not (Test-Path -LiteralPath (Join-Path $Top $relative) -PathType Leaf)) { Ariza-Err "private update bundle is missing $relative" } } } function Ariza-RecordDeferredCleanup { param([string]$Dir) $cleanup = Join-Path $ArizaState 'cleanup' New-Item -ItemType Directory -Path $cleanup -Force | Out-Null $name = Split-Path -Leaf $Dir Ariza-WriteAtomicText (Join-Path $cleanup $name) ((Ariza-FullPath $Dir) + "`n") } function Ariza-RunDeferredCleanup { $cleanup = Join-Path $ArizaState 'cleanup' if (-not (Test-Path -LiteralPath $cleanup -PathType Container)) { return } $current = Ariza-PhysicalLinkTarget (Join-Path $ArizaRoot 'current') $previous = Ariza-PhysicalLinkTarget (Join-Path $ArizaRoot 'previous') foreach ($record in @(Get-ChildItem -LiteralPath $cleanup -File -ErrorAction SilentlyContinue)) { if ($record.Length -gt 4096) { continue } $path = (Get-Content -LiteralPath $record.FullName -TotalCount 1).Trim() if (-not $path -or (Ariza-FullPath (Split-Path -Parent $path)) -cne (Ariza-FullPath $ArizaVersions)) { Remove-Item -LiteralPath $record.FullName -Force; continue } if ($path -ceq $current -or $path -ceq $previous -or ($ArizaPrivate -and $path -ceq (Ariza-FullPath $ArizaExpectedCurrent))) { continue } if (-not (Test-Path -LiteralPath $path)) { Remove-Item -LiteralPath $record.FullName -Force; continue } $item = Get-Item -LiteralPath $path -Force if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { # A directory replaced by a reparse point after recording is not # the object the installer decided was safe to delete. continue } try { Remove-Item -LiteralPath $path -Recurse -Force Remove-Item -LiteralPath $record.FullName -Force Ariza-Log "removed deferred superseded version $($item.Name)" } catch { } } } function Ariza-Prune { param([string]$Keep, [string]$Previous) $dirs = Get-ChildItem -LiteralPath $ArizaVersions -Directory -ErrorAction SilentlyContinue foreach ($dir in $dirs) { if ($dir.Name -ceq $Keep -or $dir.Name -ceq $Previous) { continue } if (($dir.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { continue } try { Remove-Item -LiteralPath $dir.FullName -Recurse -Force Ariza-Log "removed superseded version $($dir.Name)" } catch { try { Ariza-RecordDeferredCleanup $dir.FullName } catch { } Ariza-Warn "could not remove superseded version $($dir.Name); cleanup was deferred" } } } function Ariza-Commit { param([string]$NewVersion) $currentLink = Join-Path $ArizaRoot 'current' $previousLink = Join-Path $ArizaRoot 'previous' $before = Ariza-PhysicalLinkTarget $currentLink if ($ArizaPrivate) { if (-not $before -or (Ariza-FullPath $before) -cne (Ariza-FullPath $ArizaExpectedCurrent)) { Ariza-Err 'managed current changed while the update was being prepared' } } $oldPrevious = Ariza-PhysicalLinkTarget $previousLink $previousVersion = Ariza-VersionForPhysical $before $newTarget = Join-Path $ArizaVersions $NewVersion $journal = Join-Path $ArizaState 'transaction' New-Item -ItemType Directory -Path $ArizaState -Force | Out-Null Ariza-WriteAtomicText $journal ("protocol=1`nold=$before`nprevious=$oldPrevious`nnew=$newTarget`n") try { Ariza-SetJunctionAtomic 'current' $newTarget if ($before) { Ariza-SetJunctionAtomic 'previous' $before } elseif (Test-Path -LiteralPath $previousLink) { Ariza-RemoveLink $previousLink } # Clear the recovery journal before emitting success. The handoff is # then the final fallible commit step; no later error can turn a # natural application exit 75 into a false authenticated relaunch. Remove-Item -LiteralPath $journal -Force if ($ArizaPrivate) { $record = "protocol=1`nnonce=$ArizaNonce`ncandidate=$ArizaUpdateCandidate`n" Ariza-WriteAtomicText $ArizaHandoff $record } } catch { # A handoff-less switch is not authenticatable. Restore both pointers # while every target is still retained. if ($before) { Ariza-SetJunctionAtomic 'current' $before } elseif (Test-Path -LiteralPath $currentLink) { Ariza-RemoveLink $currentLink } if ($oldPrevious) { Ariza-SetJunctionAtomic 'previous' $oldPrevious } elseif (Test-Path -LiteralPath $previousLink) { Ariza-RemoveLink $previousLink } Remove-Item -LiteralPath $journal -Force -ErrorAction SilentlyContinue throw } # Retention failures are caught and deferred, so nothing fallible follows # a private handoff record. Ariza-Prune $NewVersion $previousVersion } function Ariza-RecoverTransaction { $journal = Join-Path $ArizaState 'transaction' if (-not (Test-Path -LiteralPath $journal -PathType Leaf)) { return } $journalItem = Get-Item -LiteralPath $journal -Force if ($journalItem.Length -gt 4096 -or ($journalItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { Ariza-Err 'installer transaction journal is unsafe; refusing to change managed pointers' } $lines = @(Get-Content -LiteralPath $journal) if (@($lines | Where-Object { $_ -ceq 'protocol=1' }).Count -ne 1 -or @($lines | Where-Object { $_.StartsWith('old=') }).Count -ne 1 -or @($lines | Where-Object { $_.StartsWith('previous=') }).Count -ne 1 -or @($lines | Where-Object { $_.StartsWith('new=') }).Count -ne 1) { Ariza-Err 'installer transaction journal is corrupt; refusing to change managed pointers' } $oldLine = $lines | Where-Object { $_.StartsWith('old=') } | Select-Object -First 1 $previousLine = $lines | Where-Object { $_.StartsWith('previous=') } | Select-Object -First 1 if (-not $oldLine -or -not $previousLine) { Ariza-Err 'installer transaction journal is corrupt; refusing to change managed pointers' } $old = $oldLine.Substring(4) $oldPrevious = $previousLine.Substring(9) if ($old -and (Test-Path -LiteralPath $old -PathType Container) -and (Ariza-FullPath (Split-Path -Parent $old)) -ceq (Ariza-FullPath $ArizaVersions)) { Ariza-SetJunctionAtomic 'current' $old if ($oldPrevious -and (Test-Path -LiteralPath $oldPrevious -PathType Container) -and (Ariza-FullPath (Split-Path -Parent $oldPrevious)) -ceq (Ariza-FullPath $ArizaVersions)) { Ariza-SetJunctionAtomic 'previous' $oldPrevious } elseif (Test-Path -LiteralPath (Join-Path $ArizaRoot 'previous')) { Ariza-RemoveLink (Join-Path $ArizaRoot 'previous') } Remove-Item -LiteralPath $journal -Force Ariza-Warn 'recovered an interrupted installer transaction' } else { Ariza-Err 'installer transaction journal names an unsafe old target' } } function Ariza-CheckExisting { param([string]$InstalledVersion) # Returns $true when that version is already installed and usable, in # which case the junction and PATH are repaired on the way past: a # re-run is the obvious thing to try when something has gone missing, # and it should fix it. $dir = Join-Path $ArizaVersions $InstalledVersion if (-not (Test-Path -LiteralPath $dir)) { return $false } if (-not (Ariza-HasEntryPoint $dir)) { Ariza-Warn "$dir is incomplete -- installing it again" Remove-Item -LiteralPath $dir -Recurse -Force return $false } $current = Ariza-PhysicalLinkTarget (Join-Path $ArizaRoot 'current') if (-not $current -or (Ariza-FullPath $current) -cne (Ariza-FullPath $dir)) { Ariza-Commit $InstalledVersion } Ariza-LinkBin Ariza-Ok "$AppDisplay $InstalledVersion is already installed" return $true } function Ariza-Warmup { # Run the app once, now, through the same junction the user's shell # will take. A first launch pages a few hundred megabytes off a cold # disk and builds whatever per-user state the app keeps; doing it # here means it happens while an installer is on screen saying so, # instead of the first time somebody actually wants the program. # # Never fatal. The bundle is installed and its sha256 was checked # before anything was moved into place, so a warm-up that fails on # one machine is far more likely to be that machine -- no console, a # policy, an over-eager scanner -- than a broken release. Refusing to # finish the install over it would take a working program away from # a user who has one. $bin = '' foreach ($leaf in @("$AppExec.exe", "$AppExec.cmd")) { $candidate = Join-Path $ArizaBinDir $leaf if (Test-Path -LiteralPath $candidate) { $bin = $candidate; break } } if (-not $bin) { return } # The arguments come from the app's ariza.toml and are written out # already quoted, once as the argument list and once as the single # string the message names. `@warmArgs` is splatting -- an array # written inline at the call site would be passed as one argument # with spaces in it, which is a different command entirely. $warmArgs = @('--version') $what = '--version' Ariza-Log 'warming up -- the first launch does the work the rest never repeat' $failed = '' try { & $bin @warmArgs *> $null # A native command's exit code is not an error to PowerShell, so # it has to be looked at on purpose. if ($LASTEXITCODE -ne 0) { $failed = "exit code $LASTEXITCODE" } } catch { $failed = $_.Exception.Message } if ($failed) { Ariza-Warn "warm-up failed: $AppExec $what did not complete ($failed)" Ariza-Warn "$AppDisplay is installed and its download was verified -- try running it; the first launch may just take longer" } else { Ariza-Ok 'ready' } } function Ariza-Report { param([string]$InstalledVersion) Write-Host '' Write-Host " run it: $AppExec" Write-Host " installed in: $(Join-Path $ArizaVersions $InstalledVersion)" Write-Host " uninstall: irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/uninstall.ps1 | iex" Write-Host '' Write-Host " Open a new terminal to pick up the PATH change." } function Ariza-Main { if ($ArizaPrivate) { Ariza-ValidatePrivate } else { New-Item -ItemType Directory -Path $ArizaState -Force | Out-Null } Ariza-RecoverTransaction Ariza-RunDeferredCleanup $src = if ($Url) { $Url } elseif ($env:MONEYMOOR_BUNDLE_URL) { $env:MONEYMOOR_BUNDLE_URL } else { '' } $override = [bool]$src $installVer = '' if ($ArizaPrivate) { $slug = Ariza-DetectSlug if (-not $slug -or $ArizaSlugs -notcontains $slug) { Ariza-Err 'no private update bundle for this Windows platform' } $installVer = $ArizaUpdateCandidate $src = "https://github.com/$AppRepo/releases/download/$ArizaUpdateCandidate/$AppExec-$ArizaUpdateCandidate-$slug.tar.gz" $override = $false Ariza-Log "$AppDisplay $installVer update for $slug" } elseif ($override) { # An explicit source bypasses GitHub completely. It is how a # release candidate, an air-gapped copy or an offline test gets # installed, so it accepts a plain file path as readily as a URL. $installVer = Ariza-VersionFromName (Split-Path -Leaf $src) Ariza-Log "$AppDisplay from $src" } else { $slug = Ariza-DetectSlug if (-not $slug -or $ArizaSlugs -notcontains $slug) { Ariza-Err "no prebuilt $AppDisplay bundle for Windows $env:PROCESSOR_ARCHITECTURE yet -- see https://github.com/$AppRepo/releases for what is published" } $tag = if ($Version) { $Version } else { Ariza-LatestTag } if (-not $tag) { Ariza-Err "could not read the latest release tag from https://github.com/$AppRepo/releases/latest -- pass -Version to name one" } $installVer = $tag -replace '^v', '' $src = "https://github.com/$AppRepo/releases/download/$tag/$AppExec-$installVer-$slug.tar.gz" Ariza-Log "$AppDisplay $installVer for $slug" } # Every path that reaches the parting message warms up first, # including the one where nothing was downloaded -- a re-run is what # somebody tries when the last one did not take. if (-not $ArizaPrivate -and $installVer -and (Ariza-CheckExisting $installVer)) { Ariza-Warmup Ariza-Report $installVer return } $tmp = Join-Path ([IO.Path]::GetTempPath()) "$AppExec-install-$PID" New-Item -ItemType Directory -Path $tmp -Force | Out-Null $staging = Join-Path $ArizaRoot ".staging-$PID" try { $archive = Join-Path $tmp (Split-Path -Leaf $src) if (Test-Path -LiteralPath $src) { Copy-Item -LiteralPath $src -Destination $archive -Force } else { Ariza-Log "downloading $src" Ariza-Download $src $archive } $expected = '' $digestSrc = "$src.sha256" if (Test-Path -LiteralPath $digestSrc) { $expected = Ariza-FirstWord $digestSrc } elseif (-not (Test-Path -LiteralPath $src)) { try { Ariza-Download $digestSrc "$archive.sha256" $expected = Ariza-FirstWord "$archive.sha256" } catch { $expected = '' } } if ($expected) { Ariza-VerifySha256 $archive $expected Ariza-Ok 'sha256 verified' } elseif ($override -and $InsecureNoVerify) { # Only reachable for a source the user named themselves, and # only when they asked for it in as many words. A published # release always has a checksum beside it, so its absence # there is a tampered or half-uploaded release and stays fatal. Ariza-Warn "NOT VERIFIED: no $(Split-Path -Leaf $digestSrc) beside the source you gave, and -InsecureNoVerify was passed" } else { Ariza-Err "no checksum published at $digestSrc -- refusing to install a bundle that cannot be verified" } # Unpacked inside the data directory rather than in TEMP, so the # move into place is a rename on the same volume: the new version # appears complete or not at all, and `current` is repointed only # after it is there. New-Item -ItemType Directory -Path $ArizaVersions -Force | Out-Null if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } Ariza-Extract $archive $staging $tops = @(Get-ChildItem -LiteralPath $staging -Directory) if ($tops.Count -ne 1) { Ariza-Err "that archive does not look like a $AppDisplay bundle (expected one directory inside it)" } $top = $tops[0] # What the bundle says it is beats what the archive was called: a # versions\ directory that does not contain would make # every later "already installed" check a lie. $actual = Ariza-VersionFromName $top.Name if (-not $actual) { if (-not $installVer) { Ariza-Err "cannot tell which version $($top.Name) is -- expected $AppExec--" } } else { if ($installVer -and $actual -ne $installVer) { if ($ArizaPrivate) { Ariza-Err "private update archive contains $actual, expected $installVer" } Ariza-Warn "that archive is named $installVer but contains $actual -- installing it as $actual" } $installVer = $actual } Ariza-ValidatePrivateManifest $top.FullName if (-not $ArizaPrivate -and (Ariza-CheckExisting $installVer)) { Ariza-Warmup Ariza-Report $installVer return } $dest = Join-Path $ArizaVersions $installVer if (Test-Path -LiteralPath $dest) { Remove-Item -LiteralPath $dest -Recurse -Force } Move-Item -LiteralPath $top.FullName -Destination $dest # Stamped with the install time, not the build time: the archive # carries the directory's original timestamp, which is the same # for every bundle cut from one source tree, and pruning by age # has to mean "the one I installed before this one". (Get-Item -LiteralPath $dest).LastWriteTime = Get-Date if (-not (Ariza-HasEntryPoint $dest)) { Ariza-Err "the unpacked bundle has neither bin\$AppExec.exe nor bin\$AppExec.cmd -- not touching your existing install" } Ariza-Commit $installVer if ($ArizaPrivate) { Ariza-Ok "$AppDisplay $installVer installed" return } Ariza-LinkBin Ariza-Ok "$AppDisplay $installVer installed" Ariza-Warmup Ariza-Report $installVer } finally { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Recurse -Force } if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } } } Ariza-Main