freecode-sh 0.1.0-alpha.42 → 0.1.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.ps1 DELETED
@@ -1,683 +0,0 @@
1
- #Requires -Version 5.1
2
-
3
- <#
4
- .SYNOPSIS
5
- Installs the Free Code native Windows alpha without administrator access.
6
-
7
- .EXAMPLE
8
- irm https://freecode.sh/install.ps1 | iex
9
-
10
- .EXAMPLE
11
- .\install.ps1 -Version 0.1.0-alpha.1 -NoModifyPath
12
- #>
13
-
14
- [CmdletBinding()]
15
- param(
16
- [string]$Version,
17
- [string]$Channel,
18
- [string]$BaseUrl,
19
- [string]$ChannelBaseUrl,
20
- [string]$ArchivePath,
21
- [string]$ChecksumPath,
22
- [string]$ManifestPath,
23
- [string]$ExpectedCurrentVersion,
24
- [string]$ExpectedArchiveSha256,
25
- [long]$ExpectedArchiveSize,
26
- [Alias('InstallRoot')]
27
- [string]$InstallDir,
28
- [switch]$AllowUnsigned,
29
- [switch]$NoModifyPath,
30
- [switch]$Rollback,
31
- [switch]$Uninstall
32
- )
33
-
34
- Set-StrictMode -Version 2.0
35
- $ErrorActionPreference = 'Stop'
36
- $ProgressPreference = 'SilentlyContinue'
37
-
38
- $Product = 'freecode-sh'
39
- $Target = 'windows-x64'
40
- $AssetName = 'freecode-windows-x64.zip'
41
- $LauncherRelativePath = 'bin/freecode.exe'
42
- $InterfaceRelativePath = 'libexec/freecode/freecode-tui.exe'
43
- $EngineRelativePath = 'libexec/freecode/freecode-engine.exe'
44
- $DefaultBaseUrl = 'https://releases.freecode.sh/releases'
45
- $DefaultChannelBaseUrl = 'https://releases.freecode.sh/channels'
46
- $ReleaseSigningModulusBase64 = '8so4sWArOV0GXihYvoSoxDLI1yMoILnVfCjYQwe4CKQ9EC10JV30cntMJZWXvXZS6STqQGIjyWisa6Op4nIdOuXKDjaEcQIeftgZBzzyGrMmAMC3W57VAi3d+w/F2XnkXBaH3NOphfftF6Fxt3Q02CxN3PjuuFaOGytKsYF5uDLCvEb+MpzJlD3HZoBzhhMCAoY6wmCdlgaDxD20UFdyEigF5wwpcUWpYmbYMFp+6SLe/Fi8IrYirwiCv5eLur6PzFFcwFrJgUeZAdxmGuM8eJzZlc7mK0+gzi8KpC2FZ9eOx1SafPE4An4+u8rZ7WA1Qk5eCAhNU9lm+JAeeKcCXzhQpPUafybJg75dIbIayOC3tf5mn+X+Vl5+uY19nT2qcJsjgUeIcqbMs9FGXiIA2G292N5WppyiDcx8yAIG6rOrAUGzHMYCrhvxQLAx2KP5QfC53VZ3La7yZOATY5icF78qSSBrKDrT42vinbosbX++Wr34YB6pXhiZF6jHPHrx'
47
- $ReleaseSigningExponentBase64 = 'AQAB'
48
-
49
- function Fail([string]$Message) {
50
- throw "freecode installer: $Message"
51
- }
52
-
53
- function Get-EnvironmentValue([string]$Name) {
54
- return [Environment]::GetEnvironmentVariable($Name, 'Process')
55
- }
56
-
57
- if (-not $Version) { $Version = Get-EnvironmentValue 'FREECODE_INSTALL_VERSION' }
58
- if (-not $Channel) {
59
- $Channel = Get-EnvironmentValue 'FREECODE_INSTALL_CHANNEL'
60
- if (-not $Channel) { $Channel = 'alpha' }
61
- }
62
- if (-not $BaseUrl) {
63
- $BaseUrl = Get-EnvironmentValue 'FREECODE_INSTALL_BASE_URL'
64
- if (-not $BaseUrl) { $BaseUrl = $DefaultBaseUrl }
65
- }
66
- if (-not $ChannelBaseUrl) {
67
- $ChannelBaseUrl = Get-EnvironmentValue 'FREECODE_CHANNEL_BASE_URL'
68
- if (-not $ChannelBaseUrl) { $ChannelBaseUrl = $DefaultChannelBaseUrl }
69
- }
70
-
71
- $testReleaseModulus = Get-EnvironmentValue 'FREECODE_TEST_RELEASE_PUBLIC_KEY_MODULUS'
72
- $testReleaseExponent = Get-EnvironmentValue 'FREECODE_TEST_RELEASE_PUBLIC_KEY_EXPONENT'
73
- if ($testReleaseModulus -or $testReleaseExponent) {
74
- $testBaseUri = $null
75
- if (
76
- -not $testReleaseModulus -or
77
- -not $testReleaseExponent -or
78
- -not [Uri]::TryCreate($BaseUrl, [UriKind]::Absolute, [ref]$testBaseUri) -or
79
- $testBaseUri.Scheme -ne 'http' -or
80
- -not $testBaseUri.IsLoopback
81
- ) {
82
- Fail 'the test release key override is permitted only with a loopback HTTP release URL'
83
- }
84
- $ReleaseSigningModulusBase64 = $testReleaseModulus
85
- $ReleaseSigningExponentBase64 = $testReleaseExponent
86
- }
87
- if (-not $InstallDir) {
88
- $InstallDir = Get-EnvironmentValue 'FREECODE_INSTALL_ROOT'
89
- if (-not $InstallDir) {
90
- if (-not $env:LOCALAPPDATA) { Fail 'LOCALAPPDATA is required' }
91
- $InstallDir = Join-Path $env:LOCALAPPDATA 'Freecode'
92
- }
93
- }
94
- if ((Get-EnvironmentValue 'FREECODE_ALLOW_UNSIGNED_INSTALL') -eq 'true') {
95
- $AllowUnsigned = $true
96
- }
97
- if ((Get-EnvironmentValue 'FREECODE_INSTALL_EMBEDDED') -eq 'true') {
98
- $NoModifyPath = $true
99
- }
100
-
101
- $InstallDir = [IO.Path]::GetFullPath($InstallDir)
102
- $VersionsDir = Join-Path $InstallDir 'versions'
103
- $BinDir = Join-Path $InstallDir 'bin'
104
- $CurrentVersionPath = Join-Path $InstallDir 'current-version'
105
- $PreviousVersionPath = Join-Path $InstallDir 'previous-version'
106
- $MarkerPath = Join-Path $InstallDir '.freecode-install.json'
107
- $ShimPath = Join-Path $BinDir 'freecode.cmd'
108
-
109
- function Assert-SafeInstallRoot {
110
- if ($InstallDir.IndexOfAny([char[]]@('"', '%', '!', "`r", "`n", ';')) -ge 0) {
111
- Fail 'the install directory contains characters that cannot be represented safely in the command shim'
112
- }
113
- $comparison = [StringComparison]::OrdinalIgnoreCase
114
- $driveRoot = [IO.Path]::GetPathRoot($InstallDir)
115
- $unsafeRoots = @($driveRoot, $env:USERPROFILE, $env:LOCALAPPDATA) | Where-Object { $_ }
116
- foreach ($unsafe in $unsafeRoots) {
117
- if ([string]::Equals($InstallDir.TrimEnd('\'), ([IO.Path]::GetFullPath($unsafe)).TrimEnd('\'), $comparison)) {
118
- Fail "refusing to use a broad install directory: $InstallDir"
119
- }
120
- }
121
- }
122
-
123
- function Assert-Version([string]$Value) {
124
- if ($Value -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$') {
125
- Fail "invalid release version: $Value"
126
- }
127
- }
128
-
129
- function Assert-Channel([string]$Value) {
130
- if ($Value -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]*$') {
131
- Fail "invalid release channel: $Value"
132
- }
133
- }
134
-
135
- function Receive-File([string]$Uri, [string]$Destination) {
136
- try {
137
- Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Destination
138
- } catch {
139
- Fail "could not download $Uri ($($_.Exception.Message))"
140
- }
141
- }
142
-
143
- function Read-Json([string]$Path, [string]$Description) {
144
- try {
145
- return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
146
- } catch {
147
- Fail "$Description is not valid JSON"
148
- }
149
- }
150
-
151
- function Get-Sha256Hex([string]$Path) {
152
- $stream = [IO.File]::OpenRead($Path)
153
- $sha256 = [Security.Cryptography.SHA256]::Create()
154
- try {
155
- return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace('-', '').ToLowerInvariant()
156
- } finally {
157
- $sha256.Dispose()
158
- $stream.Dispose()
159
- }
160
- }
161
-
162
- function Enter-InstallLock {
163
- $normalizedInstallDir = $InstallDir.TrimEnd('\').ToUpperInvariant()
164
- $sha256 = [Security.Cryptography.SHA256]::Create()
165
- try {
166
- $digest = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($normalizedInstallDir))
167
- } finally {
168
- $sha256.Dispose()
169
- }
170
- $mutexName = "Local\FreeCodeInstall-$(([BitConverter]::ToString($digest)).Replace('-', ''))"
171
- $mutex = [Threading.Mutex]::new($false, $mutexName)
172
- $acquired = $false
173
- try {
174
- try {
175
- $acquired = $mutex.WaitOne(0)
176
- if (-not $acquired) {
177
- Write-Host 'Waiting for another Free Code setup to finish...'
178
- $acquired = $mutex.WaitOne([TimeSpan]::FromMinutes(5))
179
- }
180
- } catch [Threading.AbandonedMutexException] {
181
- $acquired = $true
182
- }
183
- if (-not $acquired) {
184
- Fail 'timed out waiting for another Free Code setup to finish'
185
- }
186
- return $mutex
187
- } catch {
188
- $mutex.Dispose()
189
- throw
190
- }
191
- }
192
-
193
- function Exit-InstallLock([Threading.Mutex]$Mutex) {
194
- try {
195
- $Mutex.ReleaseMutex()
196
- } finally {
197
- $Mutex.Dispose()
198
- }
199
- }
200
-
201
- function Assert-ReleaseManifestSignature([string]$ManifestPath, [string]$SignaturePath, [string]$Description) {
202
- if (-not (Test-Path -LiteralPath $SignaturePath -PathType Leaf)) {
203
- Fail "$Description does not have a valid Free Code release signature"
204
- }
205
- $rsa = [Security.Cryptography.RSA]::Create()
206
- try {
207
- $parameters = New-Object Security.Cryptography.RSAParameters
208
- $parameters.Modulus = [Convert]::FromBase64String($ReleaseSigningModulusBase64)
209
- $parameters.Exponent = [Convert]::FromBase64String($ReleaseSigningExponentBase64)
210
- $rsa.ImportParameters($parameters)
211
- $valid = $rsa.VerifyData(
212
- [IO.File]::ReadAllBytes($ManifestPath),
213
- [IO.File]::ReadAllBytes($SignaturePath),
214
- [Security.Cryptography.HashAlgorithmName]::SHA256,
215
- [Security.Cryptography.RSASignaturePadding]::Pkcs1
216
- )
217
- } catch {
218
- $valid = $false
219
- } finally {
220
- $rsa.Dispose()
221
- }
222
- if (-not $valid) {
223
- Fail "$Description does not have a valid Free Code release signature"
224
- }
225
- }
226
-
227
- function Get-Asset($Manifest, [string]$Description) {
228
- if (-not $Manifest.version) { Fail "$Description does not contain a version" }
229
- if (-not $Manifest.assets) { Fail "$Description does not contain release assets" }
230
- $property = $Manifest.assets.PSObject.Properties[$Target]
231
- if (-not $property) { Fail "$Description does not contain a $Target asset" }
232
- return $property.Value
233
- }
234
-
235
- function Assert-PayloadContract($Manifest, [string]$Description) {
236
- $payload = $null
237
- $payloads = Get-OptionalProperty $Manifest 'payloads'
238
- if ($payloads) {
239
- $targetPayload = $payloads.PSObject.Properties[$Target]
240
- if ($targetPayload) { $payload = $targetPayload.Value }
241
- }
242
- if (-not $payload) { $payload = Get-OptionalProperty $Manifest 'payload' }
243
- if (
244
- -not $payload -or
245
- [string](Get-OptionalProperty $payload 'launcher') -ne $LauncherRelativePath -or
246
- [string](Get-OptionalProperty $payload 'interface') -ne $InterfaceRelativePath -or
247
- [string](Get-OptionalProperty $payload 'engine') -ne $EngineRelativePath
248
- ) {
249
- Fail "$Description does not declare the expected $Target payload paths"
250
- }
251
- }
252
-
253
- function Get-OptionalProperty($Object, [string]$Name) {
254
- if (-not $Object) { return $null }
255
- $property = $Object.PSObject.Properties[$Name]
256
- if (-not $property) { return $null }
257
- return $property.Value
258
- }
259
-
260
- function Test-UnsignedAsset($Security) {
261
- return (Get-OptionalProperty $Security 'testUnsigned') -eq $true
262
- }
263
-
264
- function Assert-WindowsAssetSecurity($Asset, [string]$Description) {
265
- $security = Get-OptionalProperty $Asset 'security'
266
- if ($security -and (Test-UnsignedAsset $security)) {
267
- if (-not $AllowUnsigned) {
268
- Fail "$Description is an unsigned test artifact; rerun with -AllowUnsigned only on a trusted test PC"
269
- }
270
- return
271
- }
272
- if (
273
- -not $security -or
274
- $security.windowsAuthenticodeSigned -ne $true -or
275
- $security.timestamped -ne $true -or
276
- -not $security.publisher
277
- ) {
278
- Fail "$Description is not marked as Authenticode signed and timestamped"
279
- }
280
- }
281
-
282
- function Assert-ZipPaths([string]$Archive, [string]$Destination) {
283
- Add-Type -AssemblyName System.IO.Compression.FileSystem
284
- $root = [IO.Path]::GetFullPath($Destination).TrimEnd('\') + '\'
285
- $zip = [IO.Compression.ZipFile]::OpenRead($Archive)
286
- try {
287
- foreach ($entry in $zip.Entries) {
288
- $name = $entry.FullName.Replace('\', '/')
289
- if (
290
- [string]::IsNullOrWhiteSpace($name) -or
291
- $name.StartsWith('/') -or
292
- $name -match '^[A-Za-z]:' -or
293
- ($name.Split('/') -contains '..')
294
- ) {
295
- Fail "release archive contains an unsafe path: $name"
296
- }
297
- $destinationPath = [IO.Path]::GetFullPath((Join-Path $Destination $name))
298
- if (-not $destinationPath.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) {
299
- Fail "release archive escapes the staging directory: $name"
300
- }
301
- }
302
- } finally {
303
- $zip.Dispose()
304
- }
305
- }
306
-
307
- function Get-RequiredExecutables([string]$Root) {
308
- return @(
309
- (Join-Path $Root $LauncherRelativePath),
310
- (Join-Path $Root $InterfaceRelativePath),
311
- (Join-Path $Root $EngineRelativePath)
312
- )
313
- }
314
-
315
- function Assert-CompleteInstall([string]$Root) {
316
- foreach ($executable in Get-RequiredExecutables $Root) {
317
- if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
318
- Fail "release is missing required executable: $executable"
319
- }
320
- }
321
- }
322
-
323
- function Assert-BundleIntegrity([string]$Root, [string]$CandidateVersion) {
324
- $bundlePath = Join-Path $Root 'libexec\freecode\bundle-manifest.json'
325
- $bundle = Read-Json $bundlePath 'bundle manifest'
326
- if ([string]$bundle.version -ne $CandidateVersion) {
327
- Fail "bundle manifest version $($bundle.version) does not match $CandidateVersion"
328
- }
329
- if (-not $bundle.payload) { Fail 'bundle manifest does not contain payload checksums' }
330
- $rootPrefix = [IO.Path]::GetFullPath($Root).TrimEnd('\') + '\'
331
- foreach ($property in $bundle.payload.PSObject.Properties) {
332
- $name = $property.Name.Replace('\', '/')
333
- if ($name.StartsWith('/') -or $name -match '^[A-Za-z]:' -or ($name.Split('/') -contains '..')) {
334
- Fail "bundle manifest contains an unsafe path: $name"
335
- }
336
- $path = [IO.Path]::GetFullPath((Join-Path $Root $name))
337
- if (-not $path.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
338
- Fail "bundle manifest path escapes the install root: $name"
339
- }
340
- if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail "bundle payload is missing $name" }
341
- $actual = Get-Sha256Hex $path
342
- if ($actual -ne [string]$property.Value) { Fail "bundle payload checksum failed for $name" }
343
- }
344
- }
345
-
346
- function Assert-Signatures([string]$Root, $Security) {
347
- if ((Test-UnsignedAsset $Security) -and $AllowUnsigned) {
348
- Write-Warning 'Installing an unsigned draft Windows artifact for testing. Do not redistribute this build.'
349
- return
350
- }
351
- foreach ($executable in Get-RequiredExecutables $Root) {
352
- $signature = Get-AuthenticodeSignature -LiteralPath $executable
353
- if ([string]$signature.Status -ne 'Valid') {
354
- Fail "Authenticode verification failed for $executable ($($signature.Status))"
355
- }
356
- if (-not $signature.SignerCertificate -or $signature.SignerCertificate.Subject -ne $Security.publisher) {
357
- $actual = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { 'missing' }
358
- Fail "unexpected Authenticode publisher for $executable (expected $($Security.publisher), received $actual)"
359
- }
360
- if (-not $signature.TimeStamperCertificate) {
361
- Fail "trusted timestamp is missing from $executable"
362
- }
363
- }
364
- }
365
-
366
- function Write-TextAtomically([string]$Path, [string]$Text) {
367
- $temporary = "$Path.$([Guid]::NewGuid().ToString('N')).tmp"
368
- [IO.File]::WriteAllText($temporary, $Text, [Text.UTF8Encoding]::new($false))
369
- if (Test-Path -LiteralPath $Path) {
370
- $backup = "$Path.$([Guid]::NewGuid().ToString('N')).bak"
371
- try {
372
- [IO.File]::Replace($temporary, $Path, $backup, $true)
373
- } finally {
374
- Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
375
- Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
376
- }
377
- } else {
378
- [IO.File]::Move($temporary, $Path)
379
- }
380
- }
381
-
382
- function Write-CommandShim {
383
- New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
384
- if ((Test-Path -LiteralPath $ShimPath) -and (Get-Item -LiteralPath $ShimPath).PSIsContainer) {
385
- Fail "refusing to replace directory at $ShimPath"
386
- }
387
- $shim = @"
388
- @echo off
389
- setlocal
390
- set "FREECODE_VERSION="
391
- set /p FREECODE_VERSION=<"$CurrentVersionPath"
392
- if not defined FREECODE_VERSION (
393
- echo Free Code is not activated. Re-run install.ps1. 1>&2
394
- exit /b 1
395
- )
396
- "$VersionsDir\%FREECODE_VERSION%\bin\freecode.exe" %*
397
- "@
398
- Write-TextAtomically $ShimPath ($shim -replace "`n", "`r`n")
399
- }
400
-
401
- function Move-StagedVersion([string]$Source, [string]$Destination) {
402
- # Windows scanners can briefly hold a newly extracted executable open. Retry
403
- # only the rename; never copy a partial version or relax payload verification.
404
- for ($attempt = 0; $attempt -lt 21; $attempt++) {
405
- try {
406
- [IO.Directory]::Move($Source, $Destination)
407
- return
408
- } catch [IO.IOException], [UnauthorizedAccessException] {
409
- if ($attempt -eq 20 -or (Test-Path -LiteralPath $Destination)) { throw }
410
- if ($attempt -eq 0) { Write-Host 'Waiting for Windows to release the installation files...' }
411
- Start-Sleep -Milliseconds 250
412
- }
413
- }
414
- }
415
-
416
- function Set-UserPath([bool]$Add) {
417
- $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
418
- $entries = @($userPath -split ';' | Where-Object { $_ })
419
- $comparison = [StringComparer]::OrdinalIgnoreCase
420
- $matches = @($entries | Where-Object { $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) })
421
- if ($Add) {
422
- if ($matches.Count -eq 0) {
423
- $updated = (@($entries) + $BinDir) -join ';'
424
- [Environment]::SetEnvironmentVariable('Path', $updated, 'User')
425
- Write-Host "Added $BinDir to your user PATH."
426
- }
427
- if (-not (@($env:Path -split ';') | Where-Object { $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) })) {
428
- $env:Path = "$BinDir;$env:Path"
429
- }
430
- } elseif ($matches.Count -gt 0) {
431
- $updated = @($entries | Where-Object { -not $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) }) -join ';'
432
- [Environment]::SetEnvironmentVariable('Path', $updated, 'User')
433
- }
434
- }
435
-
436
- function Test-VersionHealth([string]$CandidateVersion) {
437
- $launcher = Join-Path (Join-Path $VersionsDir $CandidateVersion) $LauncherRelativePath
438
- $versionOutput = @(& $launcher --version 2>&1)
439
- $versionExit = $LASTEXITCODE
440
- $versionOutput | ForEach-Object { Write-Host $_ }
441
- if ($versionExit -ne 0 -or $versionOutput.Count -eq 0 -or [string]$versionOutput[0] -ne "freecode-sh $CandidateVersion") {
442
- return $false
443
- }
444
- $doctorOutput = @(& $launcher doctor 2>&1)
445
- $doctorExit = $LASTEXITCODE
446
- $doctorOutput | ForEach-Object { Write-Host $_ }
447
- return $doctorExit -eq 0
448
- }
449
-
450
- function Invoke-Uninstall {
451
- if (-not (Test-Path -LiteralPath $MarkerPath -PathType Leaf)) {
452
- Fail "refusing to remove an unrecognized directory without $MarkerPath"
453
- }
454
- $marker = Read-Json $MarkerPath 'install marker'
455
- if ($marker.product -ne $Product -or $marker.installDir -ne $InstallDir) {
456
- Fail 'install marker does not match this Free Code installation'
457
- }
458
- if (-not $NoModifyPath) { Set-UserPath $false }
459
- Remove-Item -LiteralPath $InstallDir -Recurse -Force
460
- Write-Host "Free Code was removed from $InstallDir."
461
- }
462
-
463
- function Invoke-Rollback {
464
- if (-not (Test-Path -LiteralPath $PreviousVersionPath -PathType Leaf)) {
465
- Fail 'there is no previous Free Code version to restore'
466
- }
467
- $previous = (Get-Content -LiteralPath $PreviousVersionPath -Raw).Trim()
468
- Assert-Version $previous
469
- $previousRoot = Join-Path $VersionsDir $previous
470
- Assert-CompleteInstall $previousRoot
471
- Assert-BundleIntegrity $previousRoot $previous
472
- $metadataPath = Join-Path $previousRoot 'release-metadata.json'
473
- $metadata = Read-Json $metadataPath 'stored release metadata'
474
- Assert-WindowsAssetSecurity $metadata.asset 'stored release metadata'
475
- Assert-Signatures $previousRoot $metadata.asset.security
476
- $current = if (Test-Path -LiteralPath $CurrentVersionPath) { (Get-Content -LiteralPath $CurrentVersionPath -Raw).Trim() } else { '' }
477
- Write-TextAtomically $CurrentVersionPath "$previous`n"
478
- if (-not (Test-VersionHealth $previous)) {
479
- if ($current) { Write-TextAtomically $CurrentVersionPath "$current`n" }
480
- Fail 'the previous version failed its health check; activation was restored'
481
- }
482
- if ($current) { Write-TextAtomically $PreviousVersionPath "$current`n" }
483
- Write-CommandShim
484
- Write-Host "Rolled back Free Code to $previous."
485
- }
486
-
487
- Assert-SafeInstallRoot
488
- Assert-Channel $Channel
489
-
490
- $localInputs = @(@($ArchivePath, $ChecksumPath, $ManifestPath) | Where-Object { $_ })
491
- if ($localInputs.Count -ne 0 -and $localInputs.Count -ne 3) {
492
- Fail '-ArchivePath, -ChecksumPath, and -ManifestPath must be provided together'
493
- }
494
- if ($localInputs.Count -eq 3 -and -not $Version) {
495
- Fail '-Version is required when installing local candidate files'
496
- }
497
-
498
- if ($Uninstall -and $Rollback) { Fail '-Uninstall and -Rollback cannot be used together' }
499
- if ($Uninstall) {
500
- $operationMutex = Enter-InstallLock
501
- try {
502
- Invoke-Uninstall
503
- } finally {
504
- Exit-InstallLock $operationMutex
505
- }
506
- exit 0
507
- }
508
- if ($Rollback) {
509
- $operationMutex = Enter-InstallLock
510
- try {
511
- Invoke-Rollback
512
- } finally {
513
- Exit-InstallLock $operationMutex
514
- }
515
- exit 0
516
- }
517
-
518
- $architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
519
- if ($architecture -ne 'AMD64') {
520
- Fail "this beta supports only Windows x64 (detected $architecture)"
521
- }
522
-
523
- # Windows PowerShell 5.1 may otherwise negotiate an obsolete TLS version.
524
- if ([Net.ServicePointManager]::SecurityProtocol -band [Net.SecurityProtocolType]::Tls12) {
525
- [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
526
- } else {
527
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
528
- }
529
-
530
- $BaseUrl = $BaseUrl.TrimEnd('/')
531
- $ChannelBaseUrl = $ChannelBaseUrl.TrimEnd('/')
532
- $TemporaryDir = Join-Path ([IO.Path]::GetTempPath()) "freecode-install-$([Guid]::NewGuid().ToString('N'))"
533
- $StageDir = $null
534
- $InstallMutex = $null
535
- New-Item -ItemType Directory -Path $TemporaryDir -Force | Out-Null
536
-
537
- try {
538
- if (-not $Version) {
539
- $channelPath = Join-Path $TemporaryDir 'channel.json'
540
- $channelSignaturePath = Join-Path $TemporaryDir 'channel.json.sig'
541
- Write-Host "Resolving Free Code channel $Channel..."
542
- Receive-File "$ChannelBaseUrl/$Channel.json" $channelPath
543
- Receive-File "$ChannelBaseUrl/$Channel.json.sig" $channelSignaturePath
544
- Assert-ReleaseManifestSignature $channelPath $channelSignaturePath "channel $Channel"
545
- $channelManifest = Read-Json $channelPath "channel $Channel"
546
- if ($channelManifest.channel -ne $Channel) { Fail "channel manifest identifies $($channelManifest.channel), expected $Channel" }
547
- Assert-PayloadContract $channelManifest "channel $Channel"
548
- $channelAsset = Get-Asset $channelManifest "channel $Channel"
549
- Assert-WindowsAssetSecurity $channelAsset "channel $Channel"
550
- $Version = [string]$channelManifest.version
551
- }
552
- Assert-Version $Version
553
-
554
- $downloadedArchivePath = Join-Path $TemporaryDir $AssetName
555
- $downloadedChecksumPath = "$downloadedArchivePath.sha256"
556
- $downloadedManifestPath = Join-Path $TemporaryDir 'release-manifest.json'
557
- $downloadedSignaturePath = "$downloadedManifestPath.sig"
558
- if ($localInputs.Count -eq 3) {
559
- Write-Host "Loading local Free Code $Version candidate files..."
560
- Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ArchivePath)) -Destination $downloadedArchivePath
561
- Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ChecksumPath)) -Destination $downloadedChecksumPath
562
- Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ManifestPath)) -Destination $downloadedManifestPath
563
- $localSignaturePath = "$([IO.Path]::GetFullPath($ManifestPath)).sig"
564
- if (Test-Path -LiteralPath $localSignaturePath -PathType Leaf) {
565
- Copy-Item -LiteralPath $localSignaturePath -Destination $downloadedSignaturePath
566
- }
567
- } else {
568
- $releaseRoot = "$BaseUrl/v$Version"
569
- Write-Host "Downloading Free Code $Version for Windows x64..."
570
- Receive-File "$releaseRoot/$AssetName" $downloadedArchivePath
571
- Receive-File "$releaseRoot/$AssetName.sha256" $downloadedChecksumPath
572
- Receive-File "$releaseRoot/release-manifest.json" $downloadedManifestPath
573
- Receive-File "$releaseRoot/release-manifest.json.sig" $downloadedSignaturePath
574
- }
575
-
576
- $manifest = Read-Json $downloadedManifestPath "release $Version"
577
- $asset = Get-Asset $manifest "release $Version"
578
- $localUnsignedOverride = $localInputs.Count -eq 3 -and $AllowUnsigned -and (Test-UnsignedAsset $asset.security)
579
- if (-not $localUnsignedOverride) {
580
- Assert-ReleaseManifestSignature $downloadedManifestPath $downloadedSignaturePath "release $Version"
581
- }
582
- if ([string]$manifest.version -ne $Version) {
583
- Fail "release manifest version $($manifest.version) does not match requested version $Version"
584
- }
585
- if ($manifest.channel -ne $Channel) { Fail "release manifest channel $($manifest.channel) does not match $Channel" }
586
- Assert-PayloadContract $manifest "release $Version"
587
- Assert-WindowsAssetSecurity $asset "release $Version"
588
- if ($asset.archive -ne $AssetName) { Fail "release manifest names unexpected asset $($asset.archive)" }
589
- if ($ExpectedArchiveSha256 -and [string]$asset.sha256 -ne $ExpectedArchiveSha256) {
590
- Fail 'release archive checksum does not match the signed update channel'
591
- }
592
- if ($ExpectedArchiveSize -and ([long]$asset.size -ne $ExpectedArchiveSize -or (Get-Item -LiteralPath $downloadedArchivePath).Length -ne $ExpectedArchiveSize)) {
593
- Fail 'release archive size does not match the signed update channel'
594
- }
595
-
596
- $checksumLine = (Get-Content -LiteralPath $downloadedChecksumPath | Select-Object -First 1)
597
- if ($checksumLine -notmatch '^([a-fA-F0-9]{64})\s{1,2}(\*?)([^\s]+)$') {
598
- Fail 'release checksum file is malformed'
599
- }
600
- $expectedChecksum = $Matches[1].ToLowerInvariant()
601
- if ($Matches[3] -ne $AssetName) { Fail "checksum file names unexpected asset $($Matches[3])" }
602
- if ([string]$asset.sha256 -ne $expectedChecksum) { Fail 'release manifest and checksum file disagree' }
603
- $actualChecksum = Get-Sha256Hex $downloadedArchivePath
604
- if ($actualChecksum -ne $expectedChecksum) { Fail "checksum verification failed for $AssetName" }
605
-
606
- $InstallMutex = Enter-InstallLock
607
- if ($ExpectedCurrentVersion) {
608
- $activeVersion = if (Test-Path -LiteralPath $CurrentVersionPath) { (Get-Content -LiteralPath $CurrentVersionPath -Raw).Trim() } else { '' }
609
- if ($activeVersion -ne $ExpectedCurrentVersion -and $activeVersion -ne $Version) {
610
- Fail 'the active installation changed during the update; retry with the current version'
611
- }
612
- }
613
- New-Item -ItemType Directory -Path $VersionsDir -Force | Out-Null
614
- $versionRoot = Join-Path $VersionsDir $Version
615
- $newInstall = -not (Test-Path -LiteralPath $versionRoot)
616
- if ($newInstall) {
617
- $StageDir = Join-Path $InstallDir ".install-$Version-$([Guid]::NewGuid().ToString('N'))"
618
- New-Item -ItemType Directory -Path $StageDir -Force | Out-Null
619
- Assert-ZipPaths $downloadedArchivePath $StageDir
620
- [IO.Compression.ZipFile]::ExtractToDirectory($downloadedArchivePath, $StageDir)
621
- Assert-CompleteInstall $StageDir
622
- Assert-BundleIntegrity $StageDir $Version
623
- Assert-Signatures $StageDir $asset.security
624
- @{
625
- schemaVersion = 1
626
- product = $Product
627
- version = $Version
628
- asset = $asset
629
- } | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $StageDir 'release-metadata.json') -Encoding UTF8
630
- Move-StagedVersion $StageDir $versionRoot
631
- $StageDir = $null
632
- } else {
633
- Assert-CompleteInstall $versionRoot
634
- Assert-BundleIntegrity $versionRoot $Version
635
- $storedMetadata = Read-Json (Join-Path $versionRoot 'release-metadata.json') 'stored release metadata'
636
- if (
637
- [string]$storedMetadata.version -ne $Version -or
638
- [string]$storedMetadata.asset.sha256 -ne [string]$asset.sha256 -or
639
- [string](Get-OptionalProperty $storedMetadata.asset.security 'publisher') -ne [string](Get-OptionalProperty $asset.security 'publisher') -or
640
- [bool](Test-UnsignedAsset $storedMetadata.asset.security) -ne [bool](Test-UnsignedAsset $asset.security)
641
- ) {
642
- Fail "existing version metadata does not match the verified release manifest for $Version"
643
- }
644
- Assert-Signatures $versionRoot $asset.security
645
- }
646
-
647
- New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
648
- @{
649
- schemaVersion = 1
650
- product = $Product
651
- installDir = $InstallDir
652
- binDir = $BinDir
653
- } | ConvertTo-Json | Set-Content -LiteralPath $MarkerPath -Encoding UTF8
654
-
655
- $previous = if (Test-Path -LiteralPath $CurrentVersionPath) { (Get-Content -LiteralPath $CurrentVersionPath -Raw).Trim() } else { '' }
656
- if ($previous -and $previous -ne $Version) { Write-TextAtomically $PreviousVersionPath "$previous`n" }
657
- Write-TextAtomically $CurrentVersionPath "$Version`n"
658
- Write-CommandShim
659
-
660
- if (-not (Test-VersionHealth $Version)) {
661
- if ($previous) {
662
- Write-TextAtomically $CurrentVersionPath "$previous`n"
663
- } else {
664
- Remove-Item -LiteralPath $CurrentVersionPath -Force -ErrorAction SilentlyContinue
665
- }
666
- if ($newInstall) { Remove-Item -LiteralPath $versionRoot -Recurse -Force -ErrorAction SilentlyContinue }
667
- Fail 'Free Code health checks failed; restored the previous activation'
668
- }
669
-
670
- if (-not $NoModifyPath) { Set-UserPath $true }
671
- Write-Host ''
672
- Write-Host "Free Code $Version installed successfully."
673
- Write-Host "Command: $ShimPath"
674
- Write-Host 'Open a new terminal, then run: freecode doctor'
675
- } finally {
676
- if ($StageDir -and (Test-Path -LiteralPath $StageDir)) {
677
- Remove-Item -LiteralPath $StageDir -Recurse -Force -ErrorAction SilentlyContinue
678
- }
679
- if ($InstallMutex) {
680
- Exit-InstallLock $InstallMutex
681
- }
682
- Remove-Item -LiteralPath $TemporaryDir -Recurse -Force -ErrorAction SilentlyContinue
683
- }