[CmdletBinding()] param( [string]$SourceDirectory, [string]$Prefix = [IO.Path]::Combine( [Environment]::GetFolderPath('LocalApplicationData'), 'Programs', 'SkilakMesh'), [string]$BinDir = [IO.Path]::Combine( [Environment]::GetFolderPath('LocalApplicationData'), 'Programs', 'SkilakMesh', 'bin'), [switch]$ActivateOnly, [switch]$AddToPath, [switch]$NoPathUpdate, [switch]$SkipServiceRegistration, [ValidateSet('powershell', 'setup-exe')] [string]$InstallRoute = 'powershell' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $Version = '0.0.12' $PublicSetupSha256 = '' $ExpectedPublisherIdentity = '' $DistributionProfile = 'cli-scripts-v1' $CliSourceCommit = '4e505da39f9d8d0d75b15d3514d4cc012286e0f3' $CliArchiveSha256 = '1448ac6b432d768ffadabf7a2dfb3d4c6d84eb8f044f9c49892903ecdc3d0d8f' $CliArchiveSize = [long]39650818 $CliRecordSha256 = '38e3de9bbcbb473396035679627686446f2f859d4520c130fa4ae3a00c66740a' $CliRecordSize = [long]823 $CliUninstallerSha256 = 'af5d511f4dc18ab45af83cc6c3245792f3f1a17f0f5d0ce9bbe6b7946ea97cad' $CliUninstallerSize = [long]19033 $CliCoreRevision = 'c9e0e1e80ad5adaa0a511069c020f213fbb6e9c9989d7b6491ce751adf561736' $CliDependencyRevision = 'ac1520573c3dc52b0c10d258c5ca191eae75e4ffcff504e46e8daa08152fd3e7' $CliHarnessRevision = '74ba607bd557203fe0ffb2c71817def8649673dc9e3020786a0d5a2a79c7d3be' $CliExpandedSizeLimit = [long]78041916 $CliEntryCountLimit = [int]285 $ReleaseOrigin = "https://downloads.mesh.skilak.ai/releases/v$Version/native/windows-x86_64" if ($Version -notmatch '^\d+\.\d+\.\d+$') { throw "install.ps1: invalid rendered version: $Version" } if ($SkipServiceRegistration -and [string]::IsNullOrWhiteSpace($SourceDirectory)) { throw ('install.ps1: -SkipServiceRegistration requires -SourceDirectory ' + 'and is only supported for offline packaging verification') } if ([Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ne 'X64') { throw 'install.ps1: this artifact requires Windows x86_64' } if ($AddToPath -and $NoPathUpdate) { throw 'install.ps1: choose either -AddToPath or -NoPathUpdate' } $canonicalPrefix = [IO.Path]::GetFullPath([IO.Path]::Combine( [Environment]::GetFolderPath('LocalApplicationData'), 'Programs', 'SkilakMesh')) $canonicalBinDir = [IO.Path]::GetFullPath((Join-Path $canonicalPrefix 'bin')) $Prefix = [IO.Path]::GetFullPath($Prefix) $BinDir = [IO.Path]::GetFullPath($BinDir) if (-not [string]::Equals( $Prefix, $canonicalPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "install.ps1: -Prefix must be the canonical per-user path: $canonicalPrefix" } if (-not [string]::Equals( $BinDir, $canonicalBinDir, [StringComparison]::OrdinalIgnoreCase)) { throw "install.ps1: -BinDir must be the canonical launcher path: $canonicalBinDir" } if ($Prefix -eq [IO.Path]::GetPathRoot($Prefix)) { throw "install.ps1: refusing unsafe installation prefix: $Prefix" } if ($BinDir -eq [IO.Path]::GetPathRoot($BinDir)) { throw "install.ps1: refusing unsafe launcher directory: $BinDir" } $versionsRoot = Join-Path $Prefix 'versions' $versionRoot = Join-Path $versionsRoot $Version $dispatcher = Join-Path $versionRoot 'smesh\smesh.exe' $launcher = Join-Path $BinDir 'smesh.cmd' $receiptPath = Join-Path $Prefix 'install-receipt.json' $installedUninstaller = Join-Path $Prefix 'uninstall.ps1' $uninstallRegistryPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\SkilakMesh' $unsignedStem = "skilakmesh-$Version-windows-x86_64-unsigned" $signedStem = "skilakmesh-$Version-windows-x86_64-signed" $unsignedNames = @( "$unsignedStem.zip", "$unsignedStem.build.json", 'SHA256SUMS.windows', 'uninstall.ps1') $embeddedSignedNames = @( "$signedStem.zip", "$signedStem.build.json", 'SHA256SUMS.windows.signed', 'install.ps1', 'uninstall.ps1') $publicSignedNames = @( "$signedStem.zip", "$signedStem.build.json", 'SHA256SUMS.windows.public', 'install.ps1', 'uninstall.ps1') $windowsManifestNames = @( 'SHA256SUMS.windows.public', 'SHA256SUMS.windows.signed', 'SHA256SUMS.windows') if (@('legacy', 'cli-scripts-v1') -cnotcontains $DistributionProfile) { throw 'install.ps1: unknown Windows distribution profile' } if ($DistributionProfile -eq 'cli-scripts-v1') { if ($InstallRoute -ne 'powershell' -or $SkipServiceRegistration -or $PublicSetupSha256 -or $ExpectedPublisherIdentity -or $CliSourceCommit -notmatch '^[0-9a-f]{40}$' -or $CliArchiveSha256 -notmatch '^[0-9a-f]{64}$' -or $CliRecordSha256 -notmatch '^[0-9a-f]{64}$' -or $CliUninstallerSha256 -notmatch '^[0-9a-f]{64}$' -or $CliCoreRevision -notmatch '^[0-9a-f]{64}$' -or $CliDependencyRevision -notmatch '^[0-9a-f]{64}$' -or $CliHarnessRevision -notmatch '^[0-9a-f]{64}$' -or $CliArchiveSize -isnot [long] -or $CliRecordSize -isnot [long] -or $CliUninstallerSize -isnot [long] -or $CliExpandedSizeLimit -isnot [long] -or $CliEntryCountLimit -isnot [int] -or $CliArchiveSize -lt 1 -or $CliRecordSize -lt 1 -or $CliUninstallerSize -lt 1 -or $CliExpandedSizeLimit -lt 1 -or $CliEntryCountLimit -lt 1) { throw 'install.ps1: CLI profile identity is incomplete or mixed' } } elseif ($CliSourceCommit -or $CliArchiveSha256 -or $CliRecordSha256 -or $CliUninstallerSha256 -or $CliCoreRevision -or $CliDependencyRevision -or $CliHarnessRevision -or $CliArchiveSize -or $CliRecordSize -or $CliUninstallerSize -or $CliExpandedSizeLimit -or $CliEntryCountLimit) { throw 'install.ps1: legacy profile contains mixed CLI identity' } function Test-RegularFile([string]$Path, [string]$Label) { if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "install.ps1: $Label is missing: $Path" } $item = Get-Item -LiteralPath $Path -Force if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "install.ps1: $Label is a reparse point: $Path" } } function Copy-PinnedStream( [IO.Stream]$InputStream, [IO.Stream]$OutputStream, [long]$Size) { $buffer = [byte[]]::new(65536) $count = [long]0 $deadline = [Diagnostics.Stopwatch]::StartNew() while (($read = $InputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $count += $read if ($count -gt $Size -or $deadline.Elapsed.TotalMinutes -gt 10) { throw 'install.ps1: CLI asset transfer exceeded its bound' } $OutputStream.Write($buffer, 0, $read) } if ($count -ne $Size) { throw 'install.ps1: CLI asset size differs from pinned identity' } } function Get-DownloadUri([string]$Uri) { if ($env:SKILAK_INTERNAL -eq '1') { $separator = if ($Uri.Contains('?')) { '&' } else { '?' } return "$Uri${separator}internal=1" } return $Uri } function Receive-PinnedAsset( [string]$Uri, [string]$Destination, [string]$Digest, [long]$Size) { if (-not $Uri.StartsWith('https://', [StringComparison]::Ordinal) -or $Size -lt 1) { throw 'install.ps1: invalid CLI asset URL or size' } $request = [Net.HttpWebRequest]::Create((Get-DownloadUri $Uri)) $request.Timeout = 30000 $request.ReadWriteTimeout = 30000 $request.AllowAutoRedirect = $false $response = $null $inputStream = $null $outputStream = $null try { $response = $request.GetResponse() if ($response.StatusCode -ne [Net.HttpStatusCode]::OK -or $response.ResponseUri.Scheme -ne 'https' -or ($response.ContentLength -ge 0 -and $response.ContentLength -ne $Size)) { throw 'install.ps1: CLI asset transfer identity differs' } $inputStream = $response.GetResponseStream() $outputStream = [IO.File]::Create($Destination) Copy-PinnedStream $inputStream $outputStream $Size } finally { if ($null -ne $outputStream) { $outputStream.Dispose() } if ($null -ne $inputStream) { $inputStream.Dispose() } if ($null -ne $response) { $response.Dispose() } } $actual = (Get-FileHash -LiteralPath $Destination ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $Digest) { throw 'install.ps1: CLI asset SHA-256 differs from pinned identity' } } function Copy-PinnedLocalAsset( [string]$Source, [string]$Destination, [string]$Digest, [long]$Size) { Test-RegularFile $Source 'offline CLI asset' if ((Get-Item -LiteralPath $Source).Length -ne $Size) { throw 'install.ps1: offline CLI asset size differs from pinned identity' } $inputStream = [IO.File]::OpenRead($Source) $outputStream = $null try { $outputStream = [IO.File]::Create($Destination) Copy-PinnedStream $inputStream $outputStream $Size } finally { if ($null -ne $outputStream) { $outputStream.Dispose() } $inputStream.Dispose() } if ((Get-FileHash -LiteralPath $Destination -Algorithm SHA256).Hash.ToLowerInvariant() -cne $Digest) { throw 'install.ps1: offline CLI asset SHA-256 differs from pinned identity' } } function Assert-CliSourceDirectory( [string]$Directory, [string[]]$ExpectedNames) { $source = [IO.Path]::GetFullPath($Directory) Assert-NoReparseAncestor $source 'offline CLI source' if (-not (Test-Path -LiteralPath $source -PathType Container)) { throw 'install.ps1: offline CLI source directory is missing' } $actualNames = @(Get-ChildItem -LiteralPath $source -Force | ForEach-Object { $_.Name }) if (@(Compare-Object -ReferenceObject $ExpectedNames ` -DifferenceObject $actualNames -CaseSensitive).Count -ne 0) { throw 'install.ps1: offline CLI asset set is incomplete or mixed' } foreach ($name in $ExpectedNames) { Test-RegularFile (Join-Path $source $name) 'offline CLI asset' } return $source } function Assert-CliArchiveName([string]$Name) { if ($Name.Contains('\') -or $Name.Contains(':') -or $Name.StartsWith('/') -or $Name -notmatch '^smesh/') { throw 'install.ps1: CLI archive contains an unsafe Windows name' } foreach ($part in $Name.TrimEnd('/').Split('/')) { if ([string]::IsNullOrEmpty($part) -or $part -eq '.' -or $part -eq '..' -or $part -match '[\x00-\x1f<>:"|?*]' -or $part -match '[. ]$' -or $part -match '^(?i:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)') { throw 'install.ps1: CLI archive contains an unsafe Windows name' } } } function Assert-CliArchivePrefixes([string[]]$Names, [string[]]$Files) { $fileSet = [Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase) foreach ($file in $Files) { [void]$fileSet.Add($file) } foreach ($name in $Names) { $parts = $name.Split('/') for ($i = 1; $i -lt $parts.Length; $i++) { if ($fileSet.Contains([string]::Join('/', $parts, 0, $i))) { throw 'install.ps1: CLI archive has a file/directory collision' } } } } function Expand-CliArchive( [string]$ArchivePath, [string]$Destination, [long]$ExpandedSizeLimit) { $zip = [IO.Compression.ZipFile]::OpenRead($ArchivePath) try { $actualExpanded = [long]0 foreach ($entry in $zip.Entries) { $relative = $entry.FullName.Replace('/', [IO.Path]::DirectorySeparatorChar) $target = Join-Path $Destination $relative if ($entry.FullName.EndsWith('/')) { [IO.Directory]::CreateDirectory($target) | Out-Null continue } if ($entry.Length -lt 0 -or $entry.Length -gt ($ExpandedSizeLimit - $actualExpanded)) { throw 'install.ps1: CLI archive expansion exceeds pinned bounds' } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($target)) | Out-Null $inputStream = $entry.Open() $outputStream = $null try { $outputStream = [IO.File]::Open( $target, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write) $buffer = [byte[]]::new(65536) $entryBytes = [long]0 while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $entryBytes += $read $actualExpanded += $read if ($entryBytes -gt $entry.Length -or $actualExpanded -gt $ExpandedSizeLimit) { throw 'install.ps1: CLI archive expansion exceeds pinned bounds' } $outputStream.Write($buffer, 0, $read) } if ($entryBytes -ne $entry.Length) { throw 'install.ps1: CLI archive entry length differs' } } finally { if ($null -ne $outputStream) { $outputStream.Dispose() } $inputStream.Dispose() } } } finally { $zip.Dispose() } } function Assert-CliOuterIdentity($Record, [string]$ArchiveName) { foreach ($field in @('artifact_class', 'release_channel', 'distribution_profile', 'platform', 'architecture', 'source_tag', 'source_commit', 'core_revision', 'dependency_revision', 'harness_revision', 'native_build_record_sha256', 'signing_status')) { if ($Record.$field -isnot [string]) { throw 'install.ps1: CLI build identity differs from pinned release' } } if ($null -eq $Record.archive -or $Record.archive.name -isnot [string] -or $Record.archive.format -isnot [string] -or $Record.archive.sha256 -isnot [string] -or ($Record.schema -isnot [int] -and $Record.schema -isnot [long]) -or $Record.schema -ne 2 -or $Record.artifact_class -ne 'public-unsigned' -or $Record.release_channel -ne 'public' -or $Record.distribution_profile -ne 'cli-scripts-v1' -or $Record.platform -ne 'Windows' -or $Record.architecture -ne 'x86_64' -or $null -ne $Record.expected_team_id -or $Record.source_tag -ne "v$Version" -or $Record.source_commit -cne $CliSourceCommit -or $Record.core_revision -cne $CliCoreRevision -or $Record.dependency_revision -cne $CliDependencyRevision -or $Record.harness_revision -cne $CliHarnessRevision -or $Record.archive.name -cne $ArchiveName -or $Record.archive.format -cne 'zip' -or $Record.archive.sha256 -cne $CliArchiveSha256 -or ($Record.archive.size -isnot [int] -and $Record.archive.size -isnot [long]) -or $Record.archive.size -ne $CliArchiveSize -or $Record.native_build_record_sha256 -notmatch '^[0-9a-f]{64}$' -or $Record.signing_status -ne 'unsigned') { throw 'install.ps1: CLI build identity differs from pinned release' } } function Assert-CliInnerIdentity($Inner, $Record, [string]$InnerDigest) { foreach ($field in @('artifact_class', 'release_channel', 'distribution_profile', 'signing_status', 'platform', 'architecture', 'source_tag', 'source_commit', 'core_revision', 'dependency_revision', 'harness_revision')) { if ($Inner.$field -isnot [string]) { throw 'install.ps1: embedded CLI identity differs from pinned build record' } } if (($Inner.schema -isnot [int] -and $Inner.schema -isnot [long]) -or $InnerDigest -cne $Record.native_build_record_sha256 -or $Inner.schema -ne 2 -or $Inner.artifact_class -ne 'public-unsigned' -or $Inner.release_channel -ne 'public' -or $Inner.distribution_profile -ne 'cli-scripts-v1' -or $Inner.signing_status -ne 'unsigned' -or $Inner.platform -ne 'Windows' -or $Inner.architecture -ne 'x86_64' -or $null -ne $Inner.expected_team_id -or $Inner.source_tag -ne "v$Version" -or $Inner.source_commit -cne $CliSourceCommit -or $Inner.core_revision -cne $CliCoreRevision -or $Inner.dependency_revision -cne $CliDependencyRevision -or $Inner.harness_revision -cne $CliHarnessRevision) { throw 'install.ps1: embedded CLI identity differs from pinned build record' } } function Assert-InstalledUninstaller([string]$ExpectedSha256) { if ($ExpectedSha256 -notmatch '^[0-9a-f]{64}$') { throw 'install.ps1: Windows uninstaller digest is invalid' } Test-RegularFile $installedUninstaller 'installed Windows uninstaller' $actual = (Get-FileHash -LiteralPath $installedUninstaller ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $ExpectedSha256) { throw ('install.ps1: installed uninstall.ps1 differs from its verified ' + 'digest; program files were preserved') } } function Assert-NoReparseAncestor([string]$Path, [string]$Label) { $full = [IO.Path]::GetFullPath($Path) $root = [IO.Path]::GetPathRoot($full) $current = $root foreach ($part in $full.Substring($root.Length).Split( [IO.Path]::DirectorySeparatorChar)) { if ([string]::IsNullOrEmpty($part)) { continue } $current = Join-Path $current $part if (Test-Path -LiteralPath $current) { $item = Get-Item -LiteralPath $current -Force if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "install.ps1: $Label contains a reparse-point ancestor: $current" } } } } function Assert-FreshRecoveryRoot { if (-not (Test-Path -LiteralPath $Prefix)) { return } if (-not (Test-Path -LiteralPath $Prefix -PathType Container)) { throw ('install.ps1: canonical program root exists without a verified ' + 'receipt; foreign contents were preserved') } $rootItem = Get-Item -LiteralPath $Prefix -Force if (($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw ('install.ps1: canonical program root is a reparse point without ' + 'a verified receipt; it was preserved') } $entries = @(Get-ChildItem -LiteralPath $Prefix -Force) if ($entries.Count -eq 0) { return } if ($entries.Count -eq 1 -and $entries[0].Name -eq 'versions' -and $entries[0].PSIsContainer -and ($entries[0].Attributes -band ` [IO.FileAttributes]::ReparsePoint) -eq 0 -and @(Get-ChildItem -LiteralPath $entries[0].FullName -Force).Count -eq 0) { return } throw ('install.ps1: canonical program root is nonempty without a verified ' + 'receipt; foreign contents were preserved') } function Get-FileSet([string]$Directory, [string[]]$Names) { $present = 0 foreach ($name in $Names) { if (Test-Path -LiteralPath (Join-Path $Directory $name) -PathType Leaf) { $present += 1 } } return $present } function Get-CertificateSha256( [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate) { $hasher = [Security.Cryptography.SHA256]::Create() try { $digest = $hasher.ComputeHash($Certificate.RawData) } finally { $hasher.Dispose() } return ([BitConverter]::ToString($digest)).Replace('-', '').ToLowerInvariant() } function Test-UserPathEntry([string]$Directory) { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ([string]::IsNullOrEmpty($userPath)) { return $false } foreach ($entry in $userPath.Split([char]';')) { if ([string]::Equals( $entry.Trim(), $Directory, [StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function Get-PriorInstallState { if (-not (Test-Path -LiteralPath $receiptPath)) { return [pscustomobject]@{ Receipt = $null PathOwned = $false } } Test-RegularFile $receiptPath 'install receipt' if ((Get-Item -LiteralPath $receiptPath).Length -gt 8192) { throw 'install.ps1: existing install receipt is too large' } try { $receipt = Get-Content -LiteralPath $receiptPath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { throw 'install.ps1: existing install receipt is invalid JSON' } $expected = @( 'schema', 'product', 'route', 'version', 'platform', 'architecture', 'program_root', 'launcher', 'service', 'path_entry_added', 'uninstaller_sha256') $actual = @($receipt.PSObject.Properties.Name) if (@(Compare-Object -ReferenceObject $expected -DifferenceObject $actual).Count ` -ne 0 -or $receipt.schema -ne 1 -or $receipt.product -ne 'SkilakMesh' -or @('powershell', 'setup-exe') -notcontains $receipt.route -or $receipt.version -notmatch '^\d+\.\d+\.\d+$' -or $receipt.platform -ne 'Windows' -or $receipt.architecture -ne 'x86_64' -or $receipt.service -ne '\SkilakMesh-Gateway' -or -not ($receipt.path_entry_added -is [bool]) -or $receipt.uninstaller_sha256 -notmatch '^[0-9a-f]{64}$' -or -not [string]::Equals( [IO.Path]::GetFullPath([string]$receipt.program_root), $Prefix, [StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals( [IO.Path]::GetFullPath([string]$receipt.launcher), $launcher, [StringComparison]::OrdinalIgnoreCase)) { throw 'install.ps1: existing install receipt has an invalid contract' } return [pscustomobject]@{ Receipt = $receipt PathOwned = ($receipt.path_entry_added -and (Test-UserPathEntry $BinDir)) } } function Get-UserPathPlan([string]$Directory, [bool]$PriorOwned) { if (Test-UserPathEntry $Directory) { return [pscustomobject]@{ Add = $false Owned = $PriorOwned } } $approved = $AddToPath if (-not $AddToPath -and -not $NoPathUpdate) { Write-Host "The launcher directory is not on your user PATH: $Directory" try { $answer = Read-Host 'Add this exact directory to your user PATH? [y/N]' $approved = $answer -match '^(?i:y|yes)$' } catch { $approved = $false } } if (-not $approved) { Write-Host ('PATH was not changed. Rerun with -ActivateOnly -AddToPath, ' + "or add this user PATH entry yourself: $Directory") Write-Host 'Open a new terminal after changing PATH.' return [pscustomobject]@{ Add = $false Owned = $false } } return [pscustomobject]@{ Add = $true Owned = $true } } function Apply-UserPathPlan([string]$Directory, [object]$Plan) { if (-not $Plan.Add) { if ((Test-UserPathEntry $Directory) -and -not (($env:Path.Split(';')) | Where-Object { [string]::Equals( $_.Trim(), $Directory, [StringComparison]::OrdinalIgnoreCase) })) { $env:Path = "$Directory;$env:Path" } return [bool]$Plan.Owned } if (Test-UserPathEntry $Directory) { Write-Warning ('PATH changed during installation. The existing launcher ' + 'directory was preserved as user-owned.') return $false } $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $updated = if ([string]::IsNullOrEmpty($userPath)) { $Directory } elseif ($userPath.EndsWith(';')) { "$userPath$Directory" } else { "$userPath;$Directory" } [Environment]::SetEnvironmentVariable('Path', $updated, 'User') $env:Path = "$Directory;$env:Path" Write-Host "Added this user PATH entry: $Directory" Write-Host 'Open a new terminal before running smesh.' return $true } function Get-LauncherText([string]$Target) { return (@( '@echo off', '@setlocal DisableDelayedExpansion', "`"$Target`" %*" ) -join "`r`n") } function Assert-OwnedExistingLauncher([object]$PriorReceipt) { Test-RegularFile $launcher 'existing launcher' $actual = Get-Content -LiteralPath $launcher -Raw -Encoding ASCII $targets = @($dispatcher) if ($null -ne $PriorReceipt) { $targets += Join-Path $Prefix ( 'versions\{0}\smesh\smesh.exe' -f $PriorReceipt.version) } foreach ($target in $targets) { if ([string]::Equals( $actual, (Get-LauncherText $target), [StringComparison]::Ordinal)) { return } } throw 'install.ps1: existing smesh.cmd is not owned by this installation' } function Write-Launcher { Assert-NoReparseAncestor $Prefix 'installation prefix' Assert-NoReparseAncestor $versionRoot 'installed version' Assert-NoReparseAncestor $BinDir 'launcher directory' Test-RegularFile $dispatcher 'installed Windows dispatcher' if ($dispatcher.Contains('%')) { throw 'install.ps1: launcher target contains an unsafe percent sign' } [IO.Directory]::CreateDirectory($BinDir) | Out-Null Assert-NoReparseAncestor $BinDir 'launcher directory' $pending = Join-Path $BinDir (".smesh-$Version-" + [Guid]::NewGuid() + '.cmd') try { Set-Content -LiteralPath $pending -Value (Get-LauncherText $dispatcher) ` -Encoding Ascii -NoNewline Move-Item -LiteralPath $pending -Destination $launcher -Force } finally { if (Test-Path -LiteralPath $pending) { Remove-Item -LiteralPath $pending -Force } } Write-Verbose "Skilak Mesh native $Version active at $launcher" } function Install-OwnedService { $output = @(& $dispatcher --skilak-install-service) $output | Out-Host if ($LASTEXITCODE -ne 0) { throw 'install.ps1: background-service registration failed' } $script:ServiceOutcome = 'registered' if ($output -contains 'Skilak Mesh Windows scheduled task upgraded and restarted successfully.') { $script:ServiceOutcome = 'restarted' } elseif ($output -contains ('Skilak Mesh Windows scheduled task is already ' + 'installed; the running gateway was left unchanged.')) { $script:ServiceOutcome = 'running' } elseif ($output -contains ('Skilak Mesh Windows scheduled task installed; ' + 'the gateway was not started.')) { $script:ServiceOutcome = 'stopped' } } function Test-ExistingConfiguration([string]$ConfigRoot) { try { Assert-NoReparseAncestor $ConfigRoot 'configuration directory' $config = Join-Path $ConfigRoot 'config.json' if (-not (Test-Path -LiteralPath $config -PathType Leaf)) { return $false } return ((Get-Item -LiteralPath $config -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0 } catch { return $false } } function Write-CompletionNotice( [bool]$Configured, [string]$PreviousVersion, [string]$ServiceOutcome) { if (-not $Configured) { Write-Host "Skilak Mesh $Version is installed." Write-Host '' Write-Host 'Next, set it up:' Write-Host ' smesh init' return } if ($PreviousVersion -eq $Version) { Write-Host "Skilak Mesh $Version is already installed and verified." } elseif ($PreviousVersion) { Write-Host "Skilak Mesh upgraded to $Version (from $PreviousVersion)." } else { Write-Host "Skilak Mesh $Version is installed." } switch ($ServiceOutcome) { 'restarted' { Write-Host 'Your settings and connected clients were kept, and Mesh restarted.' } 'running' { Write-Host 'Your settings and connected clients were kept, and Mesh is still running.' } 'stopped' { Write-Host 'Your settings and connected clients were kept.' Write-Host 'Mesh is stopped; run smesh start when you are ready.' } 'skipped' { Write-Host 'Your settings and connected clients were kept.' Write-Host 'Background-service registration was skipped.' } default { Write-Host 'Your settings and connected clients were kept.' Write-Host 'Run smesh status to check protection.' } } } function Activate-Version( [object]$PriorReceipt, [object]$PathPlan, [string]$UninstallerSha256, [string]$RecoveryVersion, [string]$RecoveryRoute, [string]$RecoveryReceiptBackup, [string]$BackupDirectory) { $hadLauncher = Test-Path -LiteralPath $launcher $launcherBackup = Join-Path $BackupDirectory 'previous-smesh.cmd' if ($hadLauncher) { Assert-OwnedExistingLauncher $PriorReceipt Copy-Item -LiteralPath $launcher -Destination $launcherBackup } try { Write-Launcher Write-InstallReceipt ` $PathPlan.Owned $UninstallerSha256 $Version $InstallRoute Register-Uninstaller $Version } catch { if ($hadLauncher) { Move-Item -LiteralPath $launcherBackup -Destination $launcher -Force } elseif (Test-Path -LiteralPath $launcher) { Remove-Item -LiteralPath $launcher -Force } Copy-Item -LiteralPath $RecoveryReceiptBackup ` -Destination $receiptPath -Force Set-ReceiptAcl $receiptPath try { Register-Uninstaller $RecoveryVersion } catch { Write-Warning ('Could not restore the recovery Installed Apps entry; ' + "run the verified helper at $installedUninstaller.") } throw } $pathEntryAdded = Apply-UserPathPlan $BinDir $PathPlan Write-InstallReceipt ` $pathEntryAdded $UninstallerSha256 $Version $InstallRoute if ($SkipServiceRegistration) { $script:ServiceOutcome = 'skipped' Write-Warning ('Background-service registration was explicitly skipped. ' + 'Headless or service-account runtime is unsupported; this ' + 'invocation did not register or update a background task.') return $pathEntryAdded } try { Install-OwnedService } catch { Write-Warning ('Service setup failed. Verified program files, launcher, ' + 'recovery receipt, and Installed Apps entry remain; run ' + "'smesh uninstall' or uninstall.ps1.") throw } return $pathEntryAdded } function Install-Uninstaller( [string]$SourcePath, [string]$ExpectedSha256, [object]$PriorReceipt) { Test-RegularFile $SourcePath 'Windows uninstaller' [IO.Directory]::CreateDirectory($Prefix) | Out-Null if (Test-Path -LiteralPath $installedUninstaller) { $ownedDigest = if ($null -ne $PriorReceipt) { $PriorReceipt.uninstaller_sha256 } else { $ExpectedSha256 } Assert-InstalledUninstaller $ownedDigest } $pending = Join-Path $Prefix ('.uninstall-' + [Guid]::NewGuid() + '.ps1') try { Copy-Item -LiteralPath $SourcePath -Destination $pending Move-Item -LiteralPath $pending -Destination $installedUninstaller -Force } finally { if (Test-Path -LiteralPath $pending) { Remove-Item -LiteralPath $pending -Force } } Assert-InstalledUninstaller $ExpectedSha256 } function Set-ReceiptAcl([string]$Path) { try { $acl = [Security.AccessControl.FileSecurity]::new() $acl.SetAccessRuleProtection($true, $false) $rights = [Security.AccessControl.FileSystemRights]::FullControl $allow = [Security.AccessControl.AccessControlType]::Allow $user = [Security.Principal.WindowsIdentity]::GetCurrent().User $system = [Security.Principal.SecurityIdentifier]::new('S-1-5-18') $acl.AddAccessRule( [Security.AccessControl.FileSystemAccessRule]::new($user, $rights, $allow)) $acl.AddAccessRule( [Security.AccessControl.FileSystemAccessRule]::new($system, $rights, $allow)) Set-Acl -LiteralPath $Path -AclObject $acl } catch { Write-Warning "Could not restrict the install receipt ACL: $($_.Exception.Message)" } } function Write-InstallReceipt( [bool]$PathEntryAdded, [string]$UninstallerSha256, [string]$ReceiptVersion, [string]$ReceiptRoute) { Assert-InstalledUninstaller $UninstallerSha256 if ($ReceiptVersion -notmatch '^\d+\.\d+\.\d+$' -or @('powershell', 'setup-exe') -notcontains $ReceiptRoute) { throw 'install.ps1: recovery receipt identity is invalid' } $receipt = [ordered]@{ schema = 1 product = 'SkilakMesh' route = $ReceiptRoute version = $ReceiptVersion platform = 'Windows' architecture = 'x86_64' program_root = $Prefix launcher = $launcher service = '\SkilakMesh-Gateway' path_entry_added = $PathEntryAdded uninstaller_sha256 = $UninstallerSha256 } $json = $receipt | ConvertTo-Json -Compress if ([Text.Encoding]::UTF8.GetByteCount($json) -gt 4096) { throw 'install.ps1: install receipt is unexpectedly large' } $pending = Join-Path $Prefix ('.install-receipt-' + [Guid]::NewGuid() + '.json') try { [IO.File]::WriteAllText( $pending, "$json`n", [Text.UTF8Encoding]::new($false)) Set-ReceiptAcl $pending Move-Item -LiteralPath $pending -Destination $receiptPath -Force } finally { if (Test-Path -LiteralPath $pending) { Remove-Item -LiteralPath $pending -Force } } } function Get-NativeTreeFingerprint([string]$Root) { Assert-NoReparseAncestor $Root 'native version tree' if (-not (Test-Path -LiteralPath $Root -PathType Container)) { throw "install.ps1: native version tree is missing: $Root" } $fullRoot = [IO.Path]::GetFullPath($Root).TrimEnd( [IO.Path]::DirectorySeparatorChar) $prefix = "$fullRoot$([IO.Path]::DirectorySeparatorChar)" $entries = [Collections.Generic.List[string]]::new() $directories = [Collections.Generic.Stack[string]]::new() $directories.Push($fullRoot) while ($directories.Count -ne 0) { $directory = $directories.Pop() foreach ($item in Get-ChildItem -LiteralPath $directory -Force) { if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'install.ps1: native version tree contains a reparse point' } if (-not $item.FullName.StartsWith( $prefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'install.ps1: native version tree escaped its root' } $relative = $item.FullName.Substring($prefix.Length).Replace('\', '/') if ($item.PSIsContainer) { $entries.Add("D:$relative") $directories.Push($item.FullName) } else { $digest = (Get-FileHash -LiteralPath $item.FullName ` -Algorithm SHA256).Hash.ToLowerInvariant() $entries.Add("F:$relative`:$($item.Length):$digest") } } } return @($entries | Sort-Object) } function Assert-MatchingNativeTree([string]$Candidate, [string]$Installed) { $candidateEntries = @(Get-NativeTreeFingerprint $Candidate) $installedEntries = @(Get-NativeTreeFingerprint $Installed) if (@(Compare-Object -ReferenceObject $candidateEntries ` -DifferenceObject $installedEntries -CaseSensitive).Count -ne 0) { throw ('install.ps1: existing version differs from the verified archive; ' + 'program files were preserved') } } function Register-Uninstaller([string]$ReceiptVersion) { if ($ReceiptVersion -notmatch '^\d+\.\d+\.\d+$') { throw 'install.ps1: uninstall entry version is invalid' } $windows = [Environment]::GetFolderPath('Windows') $powershell = Join-Path $windows ` 'System32\WindowsPowerShell\v1.0\powershell.exe' Test-RegularFile $powershell 'Windows PowerShell' $command = ('"{0}" -NoLogo -NoProfile -ExecutionPolicy Bypass -File "{1}"' ` -f $powershell, $installedUninstaller) $quiet = (('"{0}" -NoLogo -NoProfile -NonInteractive ' + '-ExecutionPolicy Bypass -File "{1}"') -f $powershell, $installedUninstaller) $registeredDispatcher = Join-Path $Prefix ( 'versions\{0}\smesh\smesh.exe' -f $ReceiptVersion) Test-RegularFile $registeredDispatcher 'registered Windows dispatcher' New-Item -Path $uninstallRegistryPath -Force | Out-Null $values = [ordered]@{ DisplayName = 'Skilak Mesh' DisplayVersion = $ReceiptVersion Publisher = 'Skilak LLC' InstallLocation = $Prefix DisplayIcon = $registeredDispatcher UninstallString = $command QuietUninstallString = $quiet } foreach ($entry in $values.GetEnumerator()) { New-ItemProperty -Path $uninstallRegistryPath -Name $entry.Key ` -Value $entry.Value -PropertyType String -Force | Out-Null } foreach ($name in @('NoModify', 'NoRepair')) { New-ItemProperty -Path $uninstallRegistryPath -Name $name -Value 1 ` -PropertyType DWord -Force | Out-Null } } $workDirectory = Join-Path ([IO.Path]::GetTempPath()) ` ("skilakmesh-install-" + [Guid]::NewGuid()) [IO.Directory]::CreateDirectory($workDirectory) | Out-Null $installLock = $null $lockAcquired = $false $stagedRoot = $null $publishedNewVersion = $false $ServiceOutcome = 'registered' try { Assert-NoReparseAncestor $Prefix 'installation prefix' Assert-NoReparseAncestor $BinDir 'launcher directory' Assert-NoReparseAncestor $versionsRoot 'versions directory' $prior = Get-PriorInstallState $configRoot = Join-Path ([Environment]::GetFolderPath('ApplicationData')) 'SkilakMesh' $configuredInstall = Test-ExistingConfiguration $configRoot $previousVersion = if ($null -ne $prior.Receipt) { [string]$prior.Receipt.version } else { '' } if ($null -eq $prior.Receipt) { Assert-FreshRecoveryRoot } if ($ActivateOnly) { if ($null -eq $prior.Receipt -or $prior.Receipt.version -ne $Version) { throw ('install.ps1: -ActivateOnly requires this version''s verified ' + 'install receipt; rerun with the complete installer assets to repair') } Assert-InstalledUninstaller $prior.Receipt.uninstaller_sha256 $pathPlan = Get-UserPathPlan $BinDir $prior.PathOwned Write-InstallReceipt ` $pathPlan.Owned $prior.Receipt.uninstaller_sha256 ` $Version $prior.Receipt.route Register-Uninstaller $Version $recoveryReceiptBackup = Join-Path ` $workDirectory 'recovery-install-receipt.json' Copy-Item -LiteralPath $receiptPath -Destination $recoveryReceiptBackup $pathEntryAdded = Activate-Version ` $prior.Receipt $pathPlan $prior.Receipt.uninstaller_sha256 ` $Version $prior.Receipt.route $recoveryReceiptBackup $workDirectory exit 0 } $selectedNames = $null $signed = $false if ($DistributionProfile -eq 'legacy') { if (-not $SourceDirectory) { if ($PublicSetupSha256 -notmatch '^[0-9a-f]{64}$' -or [string]::IsNullOrWhiteSpace($ExpectedPublisherIdentity)) { throw 'install.ps1: this public installer was not finalized with release identity' } $setupName = "skilakmesh-$Version-windows-x86_64-setup.exe" $setupPath = Join-Path $workDirectory $setupName $setupUri = "$ReleaseOrigin/$setupName" Invoke-WebRequest -UseBasicParsing -Uri (Get-DownloadUri $setupUri) -OutFile $setupPath Test-RegularFile $setupPath 'downloaded Windows setup executable' $setupDigest = (Get-FileHash -LiteralPath $setupPath ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($setupDigest -ne $PublicSetupSha256) { throw 'install.ps1: downloaded setup SHA-256 verification failed' } $setupSignature = Get-AuthenticodeSignature -LiteralPath $setupPath if ($setupSignature.Status.ToString() -ne 'Valid' -or $null -eq $setupSignature.SignerCertificate -or -not [string]::Equals( $setupSignature.SignerCertificate.Subject, $ExpectedPublisherIdentity, [StringComparison]::OrdinalIgnoreCase)) { throw 'install.ps1: downloaded setup Authenticode publisher identity differs' } if ($null -eq $setupSignature.TimeStamperCertificate) { throw 'install.ps1: downloaded setup Authenticode timestamp is missing' } $setupArguments = @() if ($AddToPath) { $setupArguments += '/AddToPath' } elseif ($NoPathUpdate) { $setupArguments += '/NoPath' } & $setupPath @setupArguments if ($LASTEXITCODE -ne 0) { throw "install.ps1: signed setup exited with code $LASTEXITCODE" } exit 0 } } if ($DistributionProfile -eq 'cli-scripts-v1') { $cliStem = "skilakmesh-$Version-windows-x86_64-unsigned" $selectedNames = @("$cliStem.zip", "$cliStem.build.json", '', 'uninstall.ps1') $archiveName = $selectedNames[0] $recordName = $selectedNames[1] if ($SourceDirectory) { $source = Assert-CliSourceDirectory $SourceDirectory ` @($archiveName, $recordName, 'uninstall.ps1') Copy-PinnedLocalAsset (Join-Path $source $archiveName) ` (Join-Path $workDirectory $archiveName) $CliArchiveSha256 $CliArchiveSize Copy-PinnedLocalAsset (Join-Path $source $recordName) ` (Join-Path $workDirectory $recordName) $CliRecordSha256 $CliRecordSize Copy-PinnedLocalAsset (Join-Path $source 'uninstall.ps1') ` (Join-Path $workDirectory 'uninstall.ps1') ` $CliUninstallerSha256 $CliUninstallerSize } else { Receive-PinnedAsset "$ReleaseOrigin/$archiveName" ` (Join-Path $workDirectory $archiveName) $CliArchiveSha256 $CliArchiveSize Receive-PinnedAsset "$ReleaseOrigin/$recordName" ` (Join-Path $workDirectory $recordName) $CliRecordSha256 $CliRecordSize $uninstallerUri = "https://downloads.mesh.skilak.ai/releases/v$Version/portable/uninstall.ps1" Receive-PinnedAsset $uninstallerUri ` (Join-Path $workDirectory 'uninstall.ps1') ` $CliUninstallerSha256 $CliUninstallerSize } } else { $source = [IO.Path]::GetFullPath($SourceDirectory) if (-not (Test-Path -LiteralPath $source -PathType Container)) { throw "install.ps1: source directory is missing: $source" } $presentManifests = @($windowsManifestNames | Where-Object { Test-Path -LiteralPath (Join-Path $source $_) }) if ($presentManifests.Count -eq 0) { throw 'install.ps1: no recognized Windows checksum manifest is present' } if ($presentManifests.Count -ne 1) { throw 'install.ps1: Windows release asset lanes are ambiguous or mixed' } $manifestName = $presentManifests[0] if ($manifestName -eq 'SHA256SUMS.windows.public') { if ([string]::IsNullOrWhiteSpace($ExpectedPublisherIdentity)) { throw 'install.ps1: public signed Windows installer was not finalized with release identity' } $selectedNames = $publicSignedNames $foreignNames = @( "$unsignedStem.zip", "$unsignedStem.build.json") $incompleteMessage = 'install.ps1: public signed Windows asset set is incomplete' $signed = $true } elseif ($manifestName -eq 'SHA256SUMS.windows.signed') { $selectedNames = $embeddedSignedNames $foreignNames = @( "$unsignedStem.zip", "$unsignedStem.build.json") $incompleteMessage = 'install.ps1: embedded signed Windows asset set is incomplete' $signed = $true } else { $selectedNames = $unsignedNames $foreignNames = @( "$signedStem.zip", "$signedStem.build.json") $incompleteMessage = 'install.ps1: unsigned Windows asset set is incomplete' } if ((Get-FileSet $source $selectedNames) -ne $selectedNames.Count) { throw $incompleteMessage } foreach ($name in $foreignNames) { if (Test-Path -LiteralPath (Join-Path $source $name)) { throw 'install.ps1: Windows release asset lanes are ambiguous or mixed' } } foreach ($name in $selectedNames) { $sourcePath = Join-Path $source $name Test-RegularFile $sourcePath 'offline release asset' Copy-Item -LiteralPath $sourcePath -Destination $workDirectory } } $archiveName = $selectedNames[0] $recordName = $selectedNames[1] $checksumsName = $selectedNames[2] $archivePath = Join-Path $workDirectory $archiveName $recordPath = Join-Path $workDirectory $recordName $checksumsPath = Join-Path $workDirectory $checksumsName Test-RegularFile $archivePath 'Windows archive' Test-RegularFile $recordPath 'Windows build record' if ($DistributionProfile -eq 'cli-scripts-v1') { $cliRecord = Get-Content -LiteralPath $recordPath -Raw -Encoding UTF8 | ConvertFrom-Json Assert-CliOuterIdentity $cliRecord $archiveName } if ($DistributionProfile -eq 'legacy') { Test-RegularFile $checksumsPath 'Windows checksums' $checksumEntries = @{} foreach ($line in Get-Content -LiteralPath $checksumsPath -Encoding UTF8) { if ($line -notmatch '^(?[0-9a-f]{64}) (?[^\\/:\s]+)$') { throw "install.ps1: invalid entry in $checksumsName" } if ($checksumEntries.ContainsKey($Matches.name)) { throw "install.ps1: duplicate entry in $checksumsName" } $checksumEntries[$Matches.name] = $Matches.hash } if ($checksumEntries.Count -ne 4 -or -not $checksumEntries.ContainsKey($archiveName) -or -not $checksumEntries.ContainsKey($recordName) -or -not $checksumEntries.ContainsKey('install.ps1') -or -not $checksumEntries.ContainsKey('uninstall.ps1')) { throw "install.ps1: $checksumsName has the wrong file set" } foreach ($path in @($archivePath, $recordPath)) { $name = [IO.Path]::GetFileName($path) $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() if ($actual -ne $checksumEntries[$name]) { throw "install.ps1: checksum verification failed for $name" } } Test-RegularFile $PSCommandPath 'Windows installer' $installerHash = (Get-FileHash -LiteralPath $PSCommandPath ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($installerHash -ne $checksumEntries['install.ps1']) { throw 'install.ps1: checksum verification failed for install.ps1' } $sourceUninstaller = Join-Path $workDirectory 'uninstall.ps1' Test-RegularFile $sourceUninstaller 'Windows uninstaller' $uninstallerHash = (Get-FileHash -LiteralPath $sourceUninstaller ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($uninstallerHash -ne $checksumEntries['uninstall.ps1']) { throw 'install.ps1: checksum verification failed for uninstall.ps1' } } else { $sourceUninstaller = Join-Path $workDirectory 'uninstall.ps1' $uninstallerHash = $CliUninstallerSha256 } Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($archivePath) try { if ($zip.Entries.Count -eq 0) { throw 'install.ps1: native archive is empty' } $names = [System.Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase) $fileNames = [System.Collections.Generic.HashSet[string]]::new( [StringComparer]::OrdinalIgnoreCase) $catalogCount = 0 $expandedSize = [long]0 foreach ($entry in $zip.Entries) { $name = $entry.FullName if ($DistributionProfile -eq 'cli-scripts-v1') { Assert-CliArchiveName $name if ($zip.Entries.Count -gt $CliEntryCountLimit -or $entry.Length -lt 0 -or $entry.Length -gt ($CliExpandedSizeLimit - $expandedSize)) { throw 'install.ps1: CLI archive expansion exceeds pinned bounds' } $expandedSize += $entry.Length } if ($name -eq 'SkilakMesh.cat') { $catalogCount += 1 } else { $parts = @($name.TrimEnd('/').Split('/')) if ($parts.Count -lt 1 -or $parts[0] -ne 'smesh') { throw 'install.ps1: native archive contains an unsafe path' } foreach ($part in $parts) { if ([string]::IsNullOrEmpty($part) -or $part -eq '.' -or $part -eq '..') { throw 'install.ps1: native archive contains an unsafe path' } } } if ($name.Contains('\') -or $name.Contains(':') -or $name.StartsWith('/') -or -not $names.Add($name.TrimEnd('/'))) { throw 'install.ps1: native archive contains an unsafe or duplicate path' } if (-not $name.EndsWith('/')) { [void]$fileNames.Add($name) } $unixType = ($entry.ExternalAttributes -shr 16) -band 0xF000 $dosAttributes = $entry.ExternalAttributes -band 0xFFFF if (($unixType -ne 0 -and $unixType -ne 0x4000 -and $unixType -ne 0x8000) -or ($dosAttributes -band 0x400)) { throw 'install.ps1: native archive contains a link or reparse point' } } if (($signed -and $catalogCount -ne 1) -or (-not $signed -and $catalogCount -ne 0)) { throw 'install.ps1: Windows file catalog set differs from the asset lane' } if ($DistributionProfile -eq 'cli-scripts-v1') { Assert-CliArchivePrefixes @($names) @($fileNames) } } finally { $zip.Dispose() } [IO.Directory]::CreateDirectory($versionsRoot) | Out-Null Assert-NoReparseAncestor $Prefix 'installation prefix' Assert-NoReparseAncestor $versionsRoot 'versions directory' $installLock = Join-Path $versionsRoot ".$Version.install.lock" try { New-Item -ItemType Directory -Path $installLock -ErrorAction Stop | Out-Null $lockAcquired = $true } catch { throw "install.ps1: another $Version installation is active at $installLock" } $stagedRoot = Join-Path $versionsRoot ` (".$Version.installing." + [Guid]::NewGuid()) [IO.Directory]::CreateDirectory($stagedRoot) | Out-Null if ($DistributionProfile -eq 'cli-scripts-v1') { Expand-CliArchive $archivePath $stagedRoot $CliExpandedSizeLimit } else { [IO.Compression.ZipFile]::ExtractToDirectory($archivePath, $stagedRoot) } foreach ($item in Get-ChildItem -LiteralPath $stagedRoot -Force -Recurse) { if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'install.ps1: extracted native tree contains a reparse point' } } $candidate = Join-Path $stagedRoot 'smesh' $candidateDispatcher = Join-Path $candidate 'smesh.exe' Test-RegularFile $candidateDispatcher 'native dispatcher' if (-not (Test-Path -LiteralPath (Join-Path $candidate '_internal') ` -PathType Container)) { throw 'install.ps1: native archive has no PyInstaller runtime tree' } $innerRecord = Join-Path $candidate 'native-build.json' Test-RegularFile $innerRecord 'embedded native build record' $archiveDigest = (Get-FileHash -LiteralPath $archivePath ` -Algorithm SHA256).Hash.ToLowerInvariant() $record = Get-Content -LiteralPath $recordPath -Raw -Encoding UTF8 | ConvertFrom-Json if ($DistributionProfile -eq 'cli-scripts-v1') { $innerDigest = (Get-FileHash -LiteralPath $innerRecord ` -Algorithm SHA256).Hash.ToLowerInvariant() $inner = Get-Content -LiteralPath $innerRecord -Raw -Encoding UTF8 | ConvertFrom-Json Assert-CliInnerIdentity $inner $cliRecord $innerDigest Write-Warning 'This CLI archive is unsigned; verify the trusted installer source.' } elseif ($signed) { $inner = Get-Content -LiteralPath $innerRecord -Raw -Encoding UTF8 | ConvertFrom-Json $innerDigest = (Get-FileHash -LiteralPath $innerRecord ` -Algorithm SHA256).Hash.ToLowerInvariant() $dispatcherDigest = (Get-FileHash -LiteralPath $candidateDispatcher ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($record.release_channel -eq 'public') { $expectedSignedClass = 'public-signed-dispatcher-catalog' $expectedUnsignedClass = 'public-unsigned' } elseif ($record.release_channel -eq 'private-beta') { $expectedSignedClass = 'private-beta-signed-dispatcher-catalog' $expectedUnsignedClass = 'private-beta-unsigned' } else { throw 'install.ps1: signed Windows release channel is invalid' } if ($record.schema -ne 1 -or $record.artifact_class -ne $expectedSignedClass -or $record.platform -ne 'Windows' -or $record.architecture -ne 'x86_64' -or $record.archive.name -ne $archiveName -or $record.archive.sha256 -ne $archiveDigest -or $record.unsigned_build_record_sha256 -ne $innerDigest -or $record.signed_dispatcher_sha256 -ne $dispatcherDigest -or $inner.schema -ne 2 -or $inner.release_channel -ne $record.release_channel -or $inner.artifact_class -ne $expectedUnsignedClass -or $inner.platform -ne 'Windows' -or $inner.architecture -ne 'x86_64' -or $null -ne $inner.expected_team_id -or $inner.source_tag -ne "v$Version" -or $inner.source_commit -notmatch '^[0-9a-f]{40}$') { throw 'install.ps1: signed Windows build record differs from the archive' } foreach ($field in @('source_commit', 'source_tag', 'source_date_epoch', 'core_revision', 'dependency_revision', 'harness_revision')) { if ($record.$field -ne $inner.$field) { throw "install.ps1: signed Windows $field differs from embedded identity" } } foreach ($field in @('core_revision', 'dependency_revision', 'harness_revision')) { if ($record.$field -notmatch '^[0-9a-f]{64}$') { throw "install.ps1: signed Windows $field is invalid" } } if ($record.authenticode.signed_paths.Count -ne 2 -or $record.authenticode.signed_paths[0] -ne 'SkilakMesh.cat' -or $record.authenticode.signed_paths[1] -ne 'smesh/smesh.exe' -or $record.authenticode.dispatcher_status -ne 'Valid' -or $record.authenticode.dispatcher_timestamped -ne $true -or $record.authenticode.nested_pe_status -ne 'catalog-sealed-not-individually-signed' -or $record.file_catalog.status -ne 'Valid' -or $record.file_catalog.timestamped -ne $true) { throw 'install.ps1: signed Windows verification record is invalid' } $catalogPath = Join-Path $stagedRoot 'SkilakMesh.cat' Test-RegularFile $catalogPath 'signed Windows file catalog' $catalogDigest = (Get-FileHash -LiteralPath $catalogPath ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($record.file_catalog.archive_path -ne 'SkilakMesh.cat' -or $record.file_catalog.sha256 -ne $catalogDigest -or $record.file_catalog.scope -ne 'smesh') { throw 'install.ps1: signed Windows catalog record differs' } $catalogCheck = Test-FileCatalog ` -Path $candidate -CatalogFilePath $catalogPath -Detailed $catalogSignature = Get-AuthenticodeSignature -LiteralPath $catalogPath $dispatcherSignature = Get-AuthenticodeSignature ` -LiteralPath $candidateDispatcher if ($catalogCheck.Status.ToString() -ne 'Valid' -or $catalogSignature.Status.ToString() -ne 'Valid' -or $dispatcherSignature.Status.ToString() -ne 'Valid' -or $null -eq $catalogSignature.SignerCertificate -or $null -eq $dispatcherSignature.SignerCertificate -or $null -eq $catalogSignature.TimeStamperCertificate -or $null -eq $dispatcherSignature.TimeStamperCertificate) { throw 'install.ps1: signed Windows catalog or dispatcher is not valid' } $catalogSigner = Get-CertificateSha256 $catalogSignature.SignerCertificate $dispatcherSigner = Get-CertificateSha256 $dispatcherSignature.SignerCertificate if ($catalogSigner -ne $dispatcherSigner -or $catalogSigner -ne $record.file_catalog.signer_sha256) { throw 'install.ps1: signed Windows publisher identity differs' } if ($manifestName -eq 'SHA256SUMS.windows.public' -and ( -not [string]::Equals( $catalogSignature.SignerCertificate.Subject, $ExpectedPublisherIdentity, [StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals( $dispatcherSignature.SignerCertificate.Subject, $ExpectedPublisherIdentity, [StringComparison]::OrdinalIgnoreCase) -or -not [string]::Equals( [string]$record.file_catalog.signer_subject, $ExpectedPublisherIdentity, [StringComparison]::OrdinalIgnoreCase))) { throw 'install.ps1: public signed Windows publisher identity differs from the finalized installer' } } else { $outerDigest = (Get-FileHash -LiteralPath $recordPath ` -Algorithm SHA256).Hash.ToLowerInvariant() $innerDigest = (Get-FileHash -LiteralPath $innerRecord ` -Algorithm SHA256).Hash.ToLowerInvariant() if ($outerDigest -ne $innerDigest) { throw 'install.ps1: unsigned build record differs from the archive' } Write-Warning ('This beta is unsigned. Windows SmartScreen may show ' + '"Windows protected your PC" on first run; choose More info, then Run anyway.') } & $candidateDispatcher --skilak-native-self-check | Out-Null if ($LASTEXITCODE -ne 0) { throw 'install.ps1: native artifact self-check failed' } & $candidateDispatcher --skilak-native-containment-self-check | Out-Null if ($LASTEXITCODE -ne 0) { throw 'install.ps1: native containment self-check failed' } $pathPlan = Get-UserPathPlan $BinDir $prior.PathOwned $recoveryVersion = if ($null -ne $prior.Receipt) { [string]$prior.Receipt.version } else { $Version } $recoveryRoute = if ($null -ne $prior.Receipt) { [string]$prior.Receipt.route } else { $InstallRoute } $hadInstalledUninstaller = Test-Path -LiteralPath $installedUninstaller $hadInstallReceipt = Test-Path -LiteralPath $receiptPath $previousUninstaller = Join-Path $workDirectory 'previous-uninstall.ps1' $previousReceipt = Join-Path $workDirectory 'previous-install-receipt.json' if ($hadInstalledUninstaller) { Copy-Item -LiteralPath $installedUninstaller -Destination $previousUninstaller } if ($hadInstallReceipt) { Copy-Item -LiteralPath $receiptPath -Destination $previousReceipt } if (Test-Path -LiteralPath $versionRoot) { Assert-MatchingNativeTree $stagedRoot $versionRoot Remove-Item -LiteralPath $stagedRoot -Recurse -Force $stagedRoot = $null Write-Host "Verified the existing $Version files; repairing installation state." } else { Move-Item -LiteralPath $stagedRoot -Destination $versionRoot $stagedRoot = $null $publishedNewVersion = $true } try { Install-Uninstaller $sourceUninstaller $uninstallerHash $prior.Receipt Write-InstallReceipt ` $pathPlan.Owned $uninstallerHash $recoveryVersion $recoveryRoute } catch { if ($hadInstalledUninstaller) { Copy-Item -LiteralPath $previousUninstaller ` -Destination $installedUninstaller -Force } elseif (Test-Path -LiteralPath $installedUninstaller) { Remove-Item -LiteralPath $installedUninstaller -Force } if ($hadInstallReceipt) { Copy-Item -LiteralPath $previousReceipt ` -Destination $receiptPath -Force Set-ReceiptAcl $receiptPath } elseif (Test-Path -LiteralPath $receiptPath) { Remove-Item -LiteralPath $receiptPath -Force } if ($publishedNewVersion -and (Test-Path -LiteralPath $versionRoot)) { Remove-Item -LiteralPath $versionRoot -Recurse -Force $publishedNewVersion = $false } throw } Register-Uninstaller $recoveryVersion $recoveryReceiptBackup = Join-Path ` $workDirectory 'recovery-install-receipt.json' Copy-Item -LiteralPath $receiptPath -Destination $recoveryReceiptBackup $pathEntryAdded = Activate-Version ` $prior.Receipt $pathPlan $uninstallerHash ` $recoveryVersion $recoveryRoute $recoveryReceiptBackup $workDirectory if ($SkipServiceRegistration) { Write-Verbose ('Verified program files and recovery uninstall; ' + 'service setup was not attempted.') } Write-CompletionNotice $configuredInstall $previousVersion $ServiceOutcome } finally { if (Test-Path -LiteralPath $workDirectory) { Remove-Item -LiteralPath $workDirectory -Recurse -Force } if ($null -ne $stagedRoot -and (Test-Path -LiteralPath $stagedRoot)) { Remove-Item -LiteralPath $stagedRoot -Recurse -Force } if ($lockAcquired -and $null -ne $installLock -and (Test-Path -LiteralPath $installLock)) { Remove-Item -LiteralPath $installLock -Force } }