freecode-sh 0.1.0-alpha.42 → 0.1.0-rc.4
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/README.md +4 -34
- package/check-platform.mjs +2 -20
- package/freecode.mjs +102 -182
- package/install-lock.mjs +153 -0
- package/package.json +19 -30
- package/release-trust.mjs +24 -0
- package/release-version.cjs +17 -0
- package/release.mjs +64 -0
- package/terminal-release.json +33 -0
- package/terminal-release.json.sig +0 -0
- package/install.ps1 +0 -683
- package/install.sh +0 -687
- package/npm-bootstrap-environment.mjs +0 -7
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
|
-
}
|