返回 CodeWhale
install.ps1
根目录 / scripts / release / install.ps1
1 # Archive installation only. Runtime authority remains with Codewhale Core.
2 $ErrorActionPreference = 'Stop'
3 $destination = Join-Path $env:USERPROFILE 'bin'
4 $names = @('codewhale.exe', 'codew.exe', 'codewhale.bat')
5 $optional = $env:CODEWHALE_INSTALL_COMPILED_HOST -eq '1'
6 if ($optional) {
7 $names += @('codewhale-extension-host.exe', 'codewhale-extension-host.LICENSES.txt', 'codewhale-extension-host.relink-source.tar.gz', 'codewhale-extension-host.release.json')
8 }
9 function File-Hash([string]$file) {
10 if (!(Test-Path -LiteralPath $file)) { return $null }
11 $item = Get-Item -LiteralPath $file -Force
12 if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw "Refusing nonregular installation file $file" }
13 return (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant()
14 }
15 function Host-Entry([string]$receipt) {
16 if ((Get-Item -LiteralPath $receipt).Length -gt 65536) { throw 'Compiled-host receipt exceeds 64 KiB' }
17 $catalog = Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json
18 $architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
19 if ($architecture -eq 'x64') { $target = 'windows-x64' }
20 elseif ($architecture -eq 'arm64') { $target = 'windows-arm64' }
21 else { throw "No qualified compiled host for Windows/$architecture; use Node" }
22 $rows = @($catalog.hosts | Where-Object { $_.target -eq $target })
23 if ($catalog.schema -ne 1 -or $rows.Count -ne 1) { throw 'No unique qualified compiled-host receipt' }
24 $hostEntry = $rows[0]
25 if ($hostEntry.license_closure -ne 'complete' -or $hostEntry.relink_source -ne 'complete' -or $hostEntry.native_platform -ne 'win32' -or $hostEntry.native_arch -ne $architecture -or $hostEntry.passed -ne 5 -or $hostEntry.failed -ne 0 -or $hostEntry.skipped -ne 0 -or $hostEntry.native_passed -lt 9 -or $hostEntry.native_failed -ne 0 -or $hostEntry.native_skipped -ne 0) { throw 'Compiled host has no complete matching-native qualification' }
26 return $hostEntry
27 }
28 $stage = $null
29 $published = @()
30 $retain = $false
31 try {
32 # Check every source before the first destination mutation.
33 foreach ($name in $names) {
34 if (!(File-Hash (Join-Path $PSScriptRoot $name))) { throw "Missing archive payload $name; extract the complete archive" }
35 }
36 if ((File-Hash (Join-Path $PSScriptRoot 'codewhale.exe')) -ne (File-Hash (Join-Path $PSScriptRoot 'codew.exe'))) { throw 'Command alias differs from the canonical Codewhale program' }
37 if ($optional) {
38 $entry = Host-Entry (Join-Path $PSScriptRoot 'codewhale-extension-host.release.json')
39 $hashes = @($entry.sha256, $entry.notices_sha256, $entry.source_sha256)
40 for ($i = 0; $i -lt 3; $i++) {
41 if ($hashes[$i] -notmatch '^[a-f0-9]{64}$' -or (File-Hash (Join-Path $PSScriptRoot $names[$i + 3])) -ne $hashes[$i]) { throw 'Compiled host archive payload hash mismatch' }
42 }
43 }
44 if (Test-Path -LiteralPath $destination) {
45 $directory = Get-Item -LiteralPath $destination -Force
46 if (!$directory.PSIsContainer -or ($directory.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Refusing a nonregular or redirected installation directory' }
47 }
48 [IO.Directory]::CreateDirectory($destination) | Out-Null
49 $stage = Join-Path $destination ('.codewhale-install-' + [Guid]::NewGuid().ToString('N'))
50 [IO.Directory]::CreateDirectory($stage) | Out-Null
51 $oldHost = $null
52 if ($optional -and (File-Hash (Join-Path $destination 'codewhale-extension-host.release.json'))) { $oldHost = Host-Entry (Join-Path $destination 'codewhale-extension-host.release.json') }
53 $snapshots = @()
54 foreach ($name in $names) {
55 $target = Join-Path $destination $name
56 $oldHash = File-Hash $target
57 if ($optional -and $name -like 'codewhale-extension-host*' -and $name -ne 'codewhale-extension-host.release.json' -and $oldHash) {
58 if (!$oldHost) { throw "Refusing unclaimed compiled-host destination $target" }
59 $expected = switch ($name) { 'codewhale-extension-host.exe' { $oldHost.sha256 }; 'codewhale-extension-host.LICENSES.txt' { $oldHost.notices_sha256 }; 'codewhale-extension-host.relink-source.tar.gz' { $oldHost.source_sha256 } }
60 if ($oldHash -ne $expected) { throw "Refusing modified compiled-host destination $target" }
61 }
62 $staged = Join-Path $stage $name
63 Copy-Item -LiteralPath (Join-Path $PSScriptRoot $name) -Destination $staged
64 $newHash = File-Hash $staged
65 if ($newHash -ne (File-Hash (Join-Path $PSScriptRoot $name))) { throw "Source changed during staging: $name" }
66 $backup = Join-Path $stage ($name + '.previous')
67 if ($oldHash) {
68 Copy-Item -LiteralPath $target -Destination $backup
69 if ((File-Hash $backup) -ne $oldHash) { throw "Destination changed during backup: $target" }
70 }
71 $snapshots += [PSCustomObject]@{ Name=$name; Target=$target; Staged=$staged; OldHash=$oldHash; NewHash=$newHash; Backup=$backup }
72 }
73 foreach ($row in $snapshots) { if ((File-Hash $row.Target) -ne $row.OldHash) { throw "Destination changed before publication: $($row.Target)" } }
74 foreach ($row in $snapshots) {
75 if ((File-Hash $row.Target) -ne $row.OldHash) { throw "Destination changed during publication: $($row.Target)" }
76 Move-Item -LiteralPath $row.Staged -Destination $row.Target -Force:([bool]$row.OldHash)
77 $published += $row
78 }
79 Write-Host "Installed canonical Codewhale commands to $destination. Add this directory to your user PATH. Node remains the default host."
80 } catch {
81 $original = $_
82 [array]::Reverse($published)
83 foreach ($row in $published) {
84 try {
85 if ((File-Hash $row.Target) -ne $row.NewHash) { throw "Published file changed; preserved $($row.Target)" }
86 if ($row.OldHash) { Move-Item -LiteralPath $row.Backup -Destination $row.Target -Force }
87 else { Remove-Item -LiteralPath $row.Target }
88 } catch { $retain = $true; Write-Warning $_ }
89 }
90 if ($retain) { Write-Warning "Rollback incomplete; recovery files retained at $stage" }
91 Write-Error $original -ErrorAction Continue
92 exit 1
93 } finally {
94 if ($stage -and !$retain) { Remove-Item -LiteralPath $stage -Recurse -Force }
95 }
96
96 lines Plain Text