[CmdletBinding()] param( [string]$InstallRoot = (Join-Path $env:LOCALAPPDATA 'SILORA-Terminal'), [switch]$NoLaunch, [switch]$NoPath, [ValidateSet('zh-CN','en-US','zh','en')][string]$Language = 'zh-CN' ) # Published over HTTPS. Version, package hash and publisher key are pinned here. & { param($TargetRoot, $SkipLaunch, $SkipPath, $SelectedLanguage) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' $PackageUrl = 'https://get.silora.network/silora/releases/v0.29.8/SILORA-Terminal-v0.29.8-runtime.zip' $PackageFallbackUrl = 'https://arzrs.xyz/silora/releases/v0.29.8/SILORA-Terminal-v0.29.8-runtime.zip' $PackageSha256 = 'fcda9b69e0b7fb87b0027b0b1e737613e961f47185fa0ce8602498dcbd2cfd05' $ArchiveRoot = 'SILORA-Terminal-Release' $MaximumPackageBytes = 67108864L $DownloadOverallDeadlineSeconds = 900 $DownloadReadIdleTimeoutMilliseconds = 30000 $DownloadResponseTimeoutMilliseconds = 30000 $English = $SelectedLanguage -eq 'en' -or $SelectedLanguage -eq 'en-US' function Say([string]$Chinese, [string]$EnglishText) { if ($English) { return $EnglishText } return $Chinese } # The trusted public key is compiled into the native host and repeated in the # installer and CLI bootstrap. This file never invokes a release executable. $script:SiloraReleasePublicKeyXml = '5j/gz3vRksOwO0TxOTDdiUbHMP1LovklplW7Ote56iUokAJeQp4MWaJETHuPAd7TRLF8plfG8VtWtML3ChzltdXXvrHaM0VkUJ32lLM3H/sx0cF92x0e4RJfFx7o6Zha4vDje6ArrHee1mGSWRYxqeLxKyDusxBN+40Liy3WBejnfPKouHgpl9QRgn3TUy5nUedbHhsMqGnTEx8FStoj9mAa1H0pTjWyGo/o9g8kZJTLuhAaoghlqks6K80PTGnu4hJ9mHn/J0iLjFQmW8jCedqKkmOwLpUhQ+PRYJWrQOsx/tXwa6hTXdD+U84iDrxIuRjjkIEXCXj+j301G794eYIgACyyOR7qaszb9pFDcSoL2km98e9+lhvWobYHibV9K0NtJjnWCGBwDY2r8y1mv4xl0pHYeLmreT595oQc6eOAR6taL6Mg/8GlnrTR2jLimvBxHyvMI9QHTRTy4zWxO1tj9cX9raNfCZjPsteDdMwKrmqVlUMIMfvF0Pu53JjBAQAB' $script:SiloraReleaseKeyId = '4f745e9c17bd3cf510b8343b3c1a41a4407fcf4e3f1ff4f99ac0d336cfb96636' $script:SiloraReleaseVersion = '0.29.8' function Get-SiloraReleaseText($Object, [string]$Key) { if ($null -ne $Object -and $Object.ContainsKey($Key) -and $Object[$Key] -is [string]) { return [string]$Object[$Key] } return $null } function Assert-SiloraReleaseNoReparse([string]$Path) { if (([IO.File]::GetAttributes($Path) -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'SILORA release verification failed: linked paths are not accepted' } } function Open-SiloraReleaseLockedFile([string]$Path, $Streams) { Assert-SiloraReleaseNoReparse $Path $stream = New-Object IO.FileStream($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $Streams.Add($stream) return $stream } function Read-SiloraReleaseMetadata([IO.FileStream]$Stream, [int]$Maximum) { if ($Stream.Length -lt 1 -or $Stream.Length -gt $Maximum) { throw 'SILORA release verification failed: invalid signed metadata size' } $bytes = New-Object byte[] ([int]$Stream.Length) $offset = 0 while ($offset -lt $bytes.Length) { $count = $Stream.Read($bytes, $offset, $bytes.Length - $offset) if ($count -eq 0) { throw 'SILORA release verification failed: incomplete metadata' } $offset += $count } return ,$bytes } function Get-SiloraReleaseSafePath([string]$Value) { if ([string]::IsNullOrEmpty($Value) -or $Value.Length -gt 180 -or $Value -cnotmatch '^[A-Za-z0-9_./-]+\z' -or $Value.StartsWith('/', [StringComparison]::Ordinal)) { throw 'SILORA release verification failed: invalid manifest path' } foreach ($part in $Value.Split('/')) { if ($part.Length -eq 0 -or $part -ceq '.' -or $part -ceq '..' -or $part.EndsWith('.', [StringComparison]::Ordinal) -or $part -match '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|\z)') { throw 'SILORA release verification failed: unsafe manifest path' } } if ($Value -ceq 'release-manifest.json' -or $Value -ceq 'release-manifest.sig' -or $Value -ceq '.silora-install.json') { throw 'SILORA release verification failed: reserved manifest path' } return $Value } function Assert-SiloraReleaseInventory([string]$Root, [string]$Folder, $Expected, [int]$Depth = 0) { if ($Depth -gt 16) { throw 'SILORA release verification failed: directory nesting exceeded' } Assert-SiloraReleaseNoReparse $Folder foreach ($path in [IO.Directory]::GetFileSystemEntries($Folder)) { Assert-SiloraReleaseNoReparse $path if ([IO.Directory]::Exists($path)) { Assert-SiloraReleaseInventory $Root $path $Expected ($Depth + 1) } else { $relative = $path.Substring($Root.Length + 1).Replace('\', '/') if ($relative -ceq '.silora-install.json') { continue } if (-not $Expected.Contains($relative)) { throw ('SILORA release verification failed: unexpected file: ' + $relative) } } } } function Release-SiloraReleaseLease($Lease) { if ($null -ne $Lease) { foreach ($stream in $Lease.Streams) { $stream.Dispose() } $Lease.Streams.Clear() } } function Test-SiloraSignedRelease([string]$Root) { $locks = New-Object 'System.Collections.Generic.List[System.IO.FileStream]' try { $absolute = [IO.Path]::GetFullPath($Root).TrimEnd([char[]]'\/') $ancestor = New-Object IO.DirectoryInfo($absolute) while ($null -ne $ancestor) { Assert-SiloraReleaseNoReparse $ancestor.FullName; $ancestor = $ancestor.Parent } $manifestStream = Open-SiloraReleaseLockedFile (Join-Path $absolute 'release-manifest.json') $locks $signatureStream = Open-SiloraReleaseLockedFile (Join-Path $absolute 'release-manifest.sig') $locks [byte[]]$manifest = Read-SiloraReleaseMetadata $manifestStream 262144 [byte[]]$signature = Read-SiloraReleaseMetadata $signatureStream 1024 $parameters = New-Object Security.Cryptography.CspParameters $parameters.ProviderType = 24 $rsa = New-Object Security.Cryptography.RSACryptoServiceProvider($parameters) try { $rsa.PersistKeyInCsp = $false $rsa.FromXmlString($script:SiloraReleasePublicKeyXml) if ($signature.Length -ne $rsa.KeySize / 8 -or -not $rsa.VerifyData($manifest, [Security.Cryptography.CryptoConfig]::MapNameToOID('SHA256'), $signature)) { throw 'SILORA release verification failed: signature does not match the trusted publisher key' } } finally { $rsa.Dispose() } Add-Type -AssemblyName System.Web.Extensions $json = New-Object Web.Script.Serialization.JavaScriptSerializer $json.MaxJsonLength = 262144 $json.RecursionLimit = 12 $utf8 = New-Object Text.UTF8Encoding($false, $true) $payload = $json.DeserializeObject($utf8.GetString($manifest)) if ($payload -isnot [System.Collections.Generic.Dictionary[string,object]] -or (Get-SiloraReleaseText $payload 'schema') -cne 'SILORA_SIGNED_RELEASE_V1' -or (Get-SiloraReleaseText $payload 'product') -cne 'SILORA-Terminal' -or (Get-SiloraReleaseText $payload 'version') -cne $script:SiloraReleaseVersion -or (Get-SiloraReleaseText $payload 'signerKeyId') -cne $script:SiloraReleaseKeyId) { throw 'SILORA release verification failed: release identity does not match this client' } $flavor = Get-SiloraReleaseText $payload 'flavor' if ($flavor -cne 'runtime' -and $flavor -cne 'development') { throw 'SILORA release verification failed: unknown release flavor' } $files = $null if ($payload.ContainsKey('files')) { $files = $payload['files'] } if ($files -isnot [object[]] -or $files.Length -lt 5 -or $files.Length -gt 256) { throw 'SILORA release verification failed: invalid file inventory' } $expected = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $null = $expected.Add('release-manifest.json') $null = $expected.Add('release-manifest.sig') [long]$total = 0 foreach ($entry in $files) { if ($entry -isnot [System.Collections.Generic.Dictionary[string,object]]) { throw 'SILORA release verification failed: invalid file entry' } $relative = Get-SiloraReleaseSafePath (Get-SiloraReleaseText $entry 'path') $digest = Get-SiloraReleaseText $entry 'sha256' if (-not $expected.Add($relative) -or $null -eq $digest -or $digest -cnotmatch '^[0-9a-f]{64}\z') { throw 'SILORA release verification failed: duplicate file or invalid digest' } if (-not $entry.ContainsKey('size')) { throw 'SILORA release verification failed: invalid file length' } $lengthObject = $entry['size'] if (($lengthObject -isnot [int] -and $lengthObject -isnot [long]) -or $lengthObject -lt 0 -or $lengthObject -gt $(if ($relative -ceq 'app/wallet/node.exe') { 134217728 } else { 67108864 })) { throw 'SILORA release verification failed: invalid file length' } [long]$length = $lengthObject $total += $length if ($total -gt 536870912) { throw 'SILORA release verification failed: release size exceeded' } $path = [IO.Path]::GetFullPath([IO.Path]::Combine($absolute, $relative.Replace('/', [IO.Path]::DirectorySeparatorChar))) if (-not $path.StartsWith($absolute + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'SILORA release verification failed: path escaped release root' } $parent = [IO.Directory]::GetParent($path) while ($null -ne $parent -and $parent.FullName.Length -gt $absolute.Length) { Assert-SiloraReleaseNoReparse $parent.FullName; $parent = $parent.Parent } $file = Open-SiloraReleaseLockedFile $path $locks if ($file.Length -ne $length) { throw ('SILORA release verification failed: file length changed: ' + $relative) } $hash = [Security.Cryptography.SHA256]::Create() try { $actual = ([BitConverter]::ToString($hash.ComputeHash($file))).Replace('-', '').ToLowerInvariant() } finally { $hash.Dispose() } if ($actual -cne $digest) { throw ('SILORA release verification failed: file content changed: ' + $relative) } } foreach ($required in @('app/SILORA.Terminal.exe', 'preview.html', 'app/chain/protocol-config.json', 'app/compute/workload-policy.json', 'install.ps1', 'silora.cmd')) { if (-not $expected.Contains($required)) { throw 'SILORA release verification failed: required file is missing from signed inventory' } } Assert-SiloraReleaseInventory $absolute $absolute $expected return [pscustomobject]@{ Streams = $locks Root = $absolute Files = $files Verification = [pscustomobject]@{ Ok = $true; Product = 'SILORA-Terminal'; Version = $script:SiloraReleaseVersion; Flavor = $flavor; KeyId = $script:SiloraReleaseKeyId; FileCount = $files.Length } } } catch { foreach ($stream in $locks) { $stream.Dispose() } $locks.Clear() throw } } function Assert-BootstrapAncestors([string]$Path) { $current = [IO.Path]::GetFullPath($Path) while ($current) { if ([IO.File]::Exists($current) -or [IO.Directory]::Exists($current)) { Assert-SiloraReleaseNoReparse $current } $parent = [IO.Directory]::GetParent($current) if ($null -eq $parent) { break } $current = $parent.FullName } } function Get-BootstrapRelativePath([string]$Value) { if ([string]::IsNullOrEmpty($Value) -or $Value.Length -gt 220 -or $Value -cnotmatch '^[A-Za-z0-9_./-]+\z' -or $Value.StartsWith('/')) { throw 'Unsafe package archive path.' } $clean = $Value.TrimEnd('/') foreach ($part in $clean.Split('/')) { if ($part.Length -eq 0 -or $part -ceq '.' -or $part -ceq '..' -or $part.EndsWith('.') -or $part -match '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|\z)') { throw 'Unsafe package archive path component.' } } if ($clean -cne $ArchiveRoot -and -not $clean.StartsWith($ArchiveRoot + '/', [StringComparison]::Ordinal)) { throw 'Unexpected package archive root.' } if ($clean -ceq $ArchiveRoot -and -not $Value.EndsWith('/')) { throw 'Archive root must be a directory.' } return $clean } function Expand-BootstrapArchive([IO.FileStream]$Source, [string]$Destination) { Add-Type -AssemblyName System.IO.Compression $Source.Position = 0 $archive = New-Object IO.Compression.ZipArchive($Source, [IO.Compression.ZipArchiveMode]::Read, $true) try { if ($archive.Entries.Count -lt 1 -or $archive.Entries.Count -gt 512) { throw 'Invalid package archive entry count.' } $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $entries = New-Object 'System.Collections.Generic.List[object]' [long]$total = 0 foreach ($entry in $archive.Entries) { $relative = Get-BootstrapRelativePath $entry.FullName if (-not $seen.Add($relative)) { throw 'Duplicate package archive path.' } $kind = ($entry.ExternalAttributes -shr 16) -band 61440 if ($kind -notin @(0,16384,32768) -or ($entry.ExternalAttributes -band 1024) -ne 0) { throw 'Package links and special files are not allowed.' } $directory = $entry.FullName.EndsWith('/') if (($directory -and $entry.Length -ne 0) -or $entry.Length -lt 0 -or $entry.Length -gt $(if ($relative -ceq ($ArchiveRoot + '/app/wallet/node.exe')) { 134217728 } else { 67108864 })) { throw 'Invalid package archive file size.' } $total += $entry.Length if ($total -gt 536870912) { throw 'Package archive expanded size exceeded.' } $target = [IO.Path]::GetFullPath([IO.Path]::Combine($Destination, $relative.Replace('/', '\'))) if (-not $target.StartsWith($Destination + '\', [StringComparison]::OrdinalIgnoreCase)) { throw 'Package archive path escaped staging directory.' } $entries.Add([pscustomobject]@{ Entry=$entry; Path=$target; Directory=$directory }) } # Validate every entry before writing any of them. foreach ($item in $entries) { Assert-BootstrapAncestors $item.Path if ($item.Directory) { [IO.Directory]::CreateDirectory($item.Path) | Out-Null; continue } [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($item.Path)) | Out-Null Assert-BootstrapAncestors $item.Path $inputStream = $null $outputStream = $null try { $inputStream = $item.Entry.Open() $outputStream = New-Object IO.FileStream($item.Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) $buffer = New-Object byte[] 65536 [long]$written = 0 while (($count = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { $written += $count if ($written -gt $item.Entry.Length) { throw 'Package entry exceeds declared size.' } $outputStream.Write($buffer, 0, $count) } if ($written -ne $item.Entry.Length) { throw 'Package entry is incomplete.' } } finally { if ($null -ne $inputStream) { $inputStream.Dispose() } if ($null -ne $outputStream) { $outputStream.Dispose() } } } } finally { $archive.Dispose() } } function Get-BootstrapRemainingMilliseconds($Watch, [int]$OverallDeadlineSeconds) { [double]$remaining = ($OverallDeadlineSeconds * 1000.0) - $Watch.Elapsed.TotalMilliseconds if ($remaining -le 0) { return 0 } return [int][Math]::Min([double][int]::MaxValue, [Math]::Ceiling($remaining)) } function Download-BootstrapPackage( [string]$Destination, [int]$OverallDeadlineSeconds = $DownloadOverallDeadlineSeconds, [int]$ReadIdleTimeoutMilliseconds = $DownloadReadIdleTimeoutMilliseconds ) { if ($OverallDeadlineSeconds -lt 1 -or $OverallDeadlineSeconds -gt 900) { throw 'Invalid package download deadline.' } if ($ReadIdleTimeoutMilliseconds -lt 50 -or $ReadIdleTimeoutMilliseconds -gt 30000) { throw 'Invalid package read timeout.' } $urls = @($PackageUrl, $PackageFallbackUrl) $lastError = $null $overallWatch = [Diagnostics.Stopwatch]::StartNew() try { for ($attempt = 0; $attempt -lt $urls.Count; $attempt++) { $remainingMs = Get-BootstrapRemainingMilliseconds $overallWatch $OverallDeadlineSeconds if ($remainingMs -le 0) { $lastError = 'Package download exceeded the time limit.' break } if ([IO.File]::Exists($Destination)) { Assert-SiloraReleaseNoReparse $Destination [IO.File]::Delete($Destination) } $uri = New-Object Uri($urls[$attempt]) if ($uri.Scheme -cne 'https' -or $uri.UserInfo -or $uri.Port -ne 443 -or @('get.silora.network','arzrs.xyz') -cnotcontains $uri.Host) { throw 'Invalid official package URL.' } $request = [Net.HttpWebRequest]::Create($uri) $request.AllowAutoRedirect = $false $request.Timeout = [Math]::Max(1, [Math]::Min($DownloadResponseTimeoutMilliseconds, $remainingMs)) $request.ReadWriteTimeout = $request.Timeout $request.UserAgent = 'SILORA-Installer/' + $script:SiloraReleaseVersion $response = $null; $source = $null; $target = $null; $readTask = $null try { $response = $request.GetResponse() if ([int]$response.StatusCode -ne 200) { throw ('HTTP ' + [int]$response.StatusCode) } if ($response.ContentLength -lt -1 -or $response.ContentLength -gt $MaximumPackageBytes) { throw 'Package download size exceeded.' } $source = $response.GetResponseStream() $target = New-Object IO.FileStream($Destination, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None) $buffer = New-Object byte[] 65536 [long]$received = 0 while ($true) { $remainingMs = Get-BootstrapRemainingMilliseconds $overallWatch $OverallDeadlineSeconds if ($remainingMs -le 0) { $request.Abort() throw 'Package download exceeded the time limit.' } $waitMs = [Math]::Max(1, [Math]::Min($ReadIdleTimeoutMilliseconds, $remainingMs)) $readTask = $source.ReadAsync($buffer, 0, $buffer.Length) $completed = $false try { $completed = $readTask.Wait($waitMs) } catch { $request.Abort() $failure = $_.Exception if ($failure -is [System.AggregateException] -and $failure.InnerExceptions.Count -gt 0) { $failure = $failure.Flatten().InnerExceptions[0] } throw $failure } if (-not $completed) { $request.Abort() if ((Get-BootstrapRemainingMilliseconds $overallWatch $OverallDeadlineSeconds) -le 0) { throw 'Package download exceeded the time limit.' } throw 'Package download stalled.' } $count = $readTask.GetAwaiter().GetResult() $readTask = $null if ($count -le 0) { break } $received += $count if ($received -gt $MaximumPackageBytes) { throw 'Package download size exceeded.' } $target.Write($buffer, 0, $count) $percent = -1 if ($response.ContentLength -gt 0) { $percent = [Math]::Min(100, [int](100.0 * $received / $response.ContentLength)) } Write-Progress -Id 7 -Activity 'SILORA' -Status (Say '正在下载安装包' 'Downloading package') -PercentComplete $percent } if ($received -lt 1 -or ($response.ContentLength -ge 0 -and $received -ne $response.ContentLength)) { throw 'Package download is incomplete.' } return } catch { $lastError = $_.Exception.Message } finally { if ($null -ne $readTask -and -not $readTask.IsCompleted) { $request.Abort() } if ($null -ne $target) { $target.Dispose() } if ($null -ne $source) { $source.Dispose() } if ($null -ne $response) { $response.Dispose() } $request.Abort() } if ([IO.File]::Exists($Destination)) { Assert-SiloraReleaseNoReparse $Destination [IO.File]::Delete($Destination) } if ($attempt + 1 -lt $urls.Count -and (Get-BootstrapRemainingMilliseconds $overallWatch $OverallDeadlineSeconds) -gt 0) { Write-Host (Say ' 下载中断,正在切换备用下载地址。' ' Download interrupted. Retrying with the backup address.') -ForegroundColor DarkGray } } if ([IO.File]::Exists($Destination)) { Assert-SiloraReleaseNoReparse $Destination [IO.File]::Delete($Destination) } throw ((Say '下载安装包失败,请检查网络后重试:' 'Package download failed; check your connection and retry: ') + $lastError) } finally { $overallWatch.Stop() } } function Remove-BootstrapStage([string]$Root, [string]$ExpectedParent, [string]$ExpectedLeaf) { $absolute = [IO.Path]::GetFullPath($Root).TrimEnd('\') if (-not [IO.Path]::GetDirectoryName($absolute).Equals($ExpectedParent, [StringComparison]::OrdinalIgnoreCase) -or [IO.Path]::GetFileName($absolute) -cne $ExpectedLeaf -or $ExpectedLeaf -cnotmatch '^silora-download-[a-f0-9]{32}\z') { throw 'Refusing to clean an unverified staging path.' } if (-not [IO.Directory]::Exists($absolute)) { return } Assert-BootstrapAncestors $absolute $pending = New-Object 'System.Collections.Generic.Stack[string]' $pending.Push($absolute) while ($pending.Count -gt 0) { foreach ($path in [IO.Directory]::GetFileSystemEntries($pending.Pop())) { Assert-SiloraReleaseNoReparse $path if ([IO.Directory]::Exists($path)) { $pending.Push($path) } } } Remove-Item -LiteralPath $absolute -Recurse -Force } $stage = $null; $package = $null; $lease = $null; $installedLease = $null $previousProtocol = [Net.ServicePointManager]::SecurityProtocol try { if ($PSVersionTable.PSVersion -lt [Version]'5.1' -or $PSVersionTable.PSEdition -ne 'Desktop' -or $env:OS -ne 'Windows_NT') { throw (Say '请使用 Windows PowerShell 5.1(powershell.exe)运行安装指令。' 'Run this installer in Windows PowerShell 5.1 (powershell.exe).') } [Net.ServicePointManager]::SecurityProtocol = $previousProtocol -bor [Net.SecurityProtocolType]::Tls12 $target = [IO.Path]::GetFullPath($TargetRoot).TrimEnd('\') Assert-BootstrapAncestors $target $tempParent = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') Assert-BootstrapAncestors $tempParent $stageLeaf = 'silora-download-' + [Guid]::NewGuid().ToString('N') $stage = [IO.Path]::Combine($tempParent, $stageLeaf) if ([IO.Directory]::Exists($stage) -or [IO.File]::Exists($stage)) { throw 'Staging path already exists.' } [IO.Directory]::CreateDirectory($stage) | Out-Null Assert-BootstrapAncestors $stage Write-Host (' SILORA ' + $script:SiloraReleaseVersion) -ForegroundColor Cyan Write-Host ' Born from Compute' -ForegroundColor DarkGray $packagePath = [IO.Path]::Combine($stage, 'package.zip') Download-BootstrapPackage $packagePath $package = New-Object IO.FileStream($packagePath, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read) $sha = [Security.Cryptography.SHA256]::Create() try { $actualHash = ([BitConverter]::ToString($sha.ComputeHash($package))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } if ($actualHash -cne $PackageSha256) { throw (Say '安装包 SHA-256 校验失败,已停止安装。' 'Package SHA-256 verification failed; installation stopped.') } Write-Progress -Id 7 -Activity 'SILORA' -Status (Say '正在验证发布者签名和全部文件' 'Verifying publisher signature and all files') -PercentComplete 100 Expand-BootstrapArchive $package $stage $releaseRoot = [IO.Path]::Combine($stage, $ArchiveRoot) if (Test-Path -LiteralPath (Join-Path $releaseRoot '.silora-install.json')) { throw 'A release archive must not include a local installation marker.' } $lease = Test-SiloraSignedRelease $releaseRoot if ($lease.Verification.Flavor -cne 'runtime') { throw 'Only a signed runtime release can be installed.' } # Keep verified payloads locked until the child installer has finished. $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' $arguments = @('-NoLogo','-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File', (Join-Path $releaseRoot 'install.ps1'),'-InstallRoot',$target,'-Language',$SelectedLanguage,'-NoLaunch') if ($SkipPath) { $arguments += '-NoPath' } Write-Progress -Id 7 -Activity 'SILORA' -Completed & $powershell @arguments $installerExitCode = $LASTEXITCODE if ($installerExitCode -ne 0) { throw ((Say '安装程序失败,退出码:' 'Installer failed with exit code: ') + $installerExitCode) } $installedLease = Test-SiloraSignedRelease $target $launcher = Join-Path $target 'silora.cmd' if (-not $SkipPath) { $pathEntries = @($env:Path -split ';' | Where-Object { $_ }) if ($pathEntries -notcontains $target) { $env:Path = $target + ';' + $env:Path } Write-Host (Say ' 当前 PowerShell 已启用 silora 命令。' ' The silora command is ready in this PowerShell window.') -ForegroundColor Green } Write-Host ((Say ' 启动命令:& "' ' Launch command: & "') + $launcher + '"') -ForegroundColor Cyan if (-not $SkipLaunch -and [Environment]::UserInteractive) { try { $application = Start-Process -FilePath (Join-Path $target 'app\SILORA.Terminal.exe') -ArgumentList @('-Language',$SelectedLanguage) -WorkingDirectory $target -PassThru $application.Dispose() Write-Host (Say ' 已打开 SILORA,请在程序中开始计算。' ' SILORA opened. Start computation from the application.') -ForegroundColor Green } catch { throw ((Say '安装已完成,但启动失败;请使用上方启动命令:' 'Installed, but launch failed. Use the launch command above: ') + $_.Exception.Message) } } } catch { throw ((Say 'SILORA 安装未完成:' 'SILORA installation did not complete: ') + $_.Exception.Message) } finally { Write-Progress -Id 7 -Activity 'SILORA' -Completed Release-SiloraReleaseLease $installedLease Release-SiloraReleaseLease $lease if ($null -ne $package) { $package.Dispose() } [Net.ServicePointManager]::SecurityProtocol = $previousProtocol if ($null -ne $stage) { try { Remove-BootstrapStage $stage $tempParent $stageLeaf } catch { Write-Warning ((Say '临时文件清理失败:' 'Staging cleanup failed: ') + $_.Exception.Message) } } } } $InstallRoot $NoLaunch $NoPath $Language