@pablozaiden/installer 0.0.10 → 0.1.0
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 +31 -4
- package/install.ps1 +444 -0
- package/package.json +2 -1
- package/src/contract.ts +17 -4
- package/src/manifest.ts +3 -3
- package/src/update.ts +175 -9
package/README.md
CHANGED
|
@@ -23,6 +23,14 @@ Supported targets are:
|
|
|
23
23
|
| --- | --- |
|
|
24
24
|
| `linux` | `x64`, `arm64` |
|
|
25
25
|
| `darwin` | `x64`, `arm64` |
|
|
26
|
+
| `windows` | `x64`, `arm64` |
|
|
27
|
+
|
|
28
|
+
Windows assets add the executable extension after the target:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
<assetPrefix>-<tag>-windows-<arch>.exe
|
|
32
|
+
<assetPrefix>-<tag>-windows-<arch>.exe.sha256
|
|
33
|
+
```
|
|
26
34
|
|
|
27
35
|
Tags may be provided as `1.2.3` or `v1.2.3`; release assets are always resolved with the `v` tag form published by GitHub releases.
|
|
28
36
|
|
|
@@ -69,9 +77,20 @@ The target repository should publish an installer manifest at either:
|
|
|
69
77
|
- `.github/installer.json`
|
|
70
78
|
- `.installer.json`
|
|
71
79
|
|
|
80
|
+
On Windows, run the PowerShell installer:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/pablozaiden/installer/main/install.ps1))) pablozaiden/link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
It installs `<name>.exe` into `$HOME/.local/bin` by default and adds that
|
|
87
|
+
directory to the user `PATH`. Pass `-NoModifyPath` to print guidance without
|
|
88
|
+
changing `PATH`.
|
|
89
|
+
|
|
72
90
|
The installer:
|
|
73
91
|
|
|
74
|
-
1. Detects Linux/macOS and x64/arm64
|
|
92
|
+
1. Detects Linux/macOS and x64/arm64; the PowerShell entrypoint detects
|
|
93
|
+
Windows x64/arm64.
|
|
75
94
|
2. Loads the target repository's manifest.
|
|
76
95
|
3. Fetches the latest GitHub release.
|
|
77
96
|
4. Downloads each configured binary asset.
|
|
@@ -101,7 +120,8 @@ Single-binary example:
|
|
|
101
120
|
},
|
|
102
121
|
"platforms": {
|
|
103
122
|
"linux": ["x64", "arm64"],
|
|
104
|
-
"darwin": ["x64", "arm64"]
|
|
123
|
+
"darwin": ["x64", "arm64"],
|
|
124
|
+
"windows": ["x64", "arm64"]
|
|
105
125
|
}
|
|
106
126
|
}
|
|
107
127
|
```
|
|
@@ -217,12 +237,18 @@ The updater supports:
|
|
|
217
237
|
- explicit version installs,
|
|
218
238
|
- semver comparison including prereleases,
|
|
219
239
|
- GitHub release metadata validation,
|
|
220
|
-
- Linux/macOS x64/arm64 target resolution,
|
|
240
|
+
- Linux/macOS/Windows x64/arm64 target resolution,
|
|
221
241
|
- checksum verification before replacement,
|
|
222
242
|
- source-mode rejection when running from `bun`,
|
|
223
243
|
- staged temp-file replacement with executable permission preservation,
|
|
224
244
|
- companion binary updates that are committed together with the primary binary and rolled back on replacement failure.
|
|
225
245
|
|
|
246
|
+
Windows cannot replace a running executable. The updater stages and verifies
|
|
247
|
+
the complete update, then starts a detached PowerShell helper. The helper waits
|
|
248
|
+
for the current process to exit, applies all replacements as one rollback-aware
|
|
249
|
+
operation, and writes `<binary>.update-error.log` beside the executable if the
|
|
250
|
+
deferred operation fails.
|
|
251
|
+
|
|
226
252
|
Exported helpers include:
|
|
227
253
|
|
|
228
254
|
```ts
|
|
@@ -310,7 +336,8 @@ jobs:
|
|
|
310
336
|
The workflow:
|
|
311
337
|
|
|
312
338
|
- runs on GitHub release publication,
|
|
313
|
-
- builds `linux-x64`, `linux-arm64`, `darwin-x64`,
|
|
339
|
+
- builds `linux-x64`, `linux-arm64`, `darwin-x64`, `darwin-arm64`,
|
|
340
|
+
`windows-x64`, and `windows-arm64`,
|
|
314
341
|
- exports `TAG`, `VERSION`, `RELEASE_TARGET`, `BUN_TARGET`, `BINARY_NAME`, `ASSET_PREFIX`, and `ASSET_PATH` to each build command,
|
|
315
342
|
- stages release assets using the shared naming convention,
|
|
316
343
|
- generates `.sha256` files by default,
|
package/install.ps1
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Position = 0)]
|
|
4
|
+
[string]$Repository,
|
|
5
|
+
[string]$Ref = "main",
|
|
6
|
+
[string[]]$Binary = @(),
|
|
7
|
+
[string[]]$AssetPrefix = @(),
|
|
8
|
+
[string]$InstallDir,
|
|
9
|
+
[ValidateSet("required", "optional", "none")]
|
|
10
|
+
[string]$Checksum = "required",
|
|
11
|
+
[switch]$NoModifyPath,
|
|
12
|
+
[switch]$Help
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Set-StrictMode -Version Latest
|
|
16
|
+
$ErrorActionPreference = "Stop"
|
|
17
|
+
|
|
18
|
+
$InstallerVersion = "0.0.0-development"
|
|
19
|
+
$DefaultInstallDir = '$HOME/.local/bin'
|
|
20
|
+
$DefaultManifestPaths = @(".github/installer.json", ".installer.json")
|
|
21
|
+
$DefaultChecksumExtension = ".sha256"
|
|
22
|
+
$ChecksumWasSpecified = $PSBoundParameters.ContainsKey("Checksum")
|
|
23
|
+
|
|
24
|
+
function Show-Usage {
|
|
25
|
+
@"
|
|
26
|
+
Usage:
|
|
27
|
+
install.ps1 <owner>/<repo> [options]
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-Ref <ref> Repository ref used to read manifests (default: main)
|
|
31
|
+
-Binary <name[]> Binary names when no manifest is available
|
|
32
|
+
-AssetPrefix <prefix[]> Asset prefixes corresponding to -Binary values
|
|
33
|
+
-InstallDir <dir> Install directory (default: `$HOME/.local/bin)
|
|
34
|
+
-Checksum required|optional|none
|
|
35
|
+
-NoModifyPath Do not add the install directory to the user PATH
|
|
36
|
+
-Help Show this help
|
|
37
|
+
"@
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Fail([string]$Message) {
|
|
41
|
+
[Console]::Error.WriteLine("Error: $Message")
|
|
42
|
+
exit 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Invoke-Download {
|
|
46
|
+
param(
|
|
47
|
+
[Parameter(Mandatory = $true)]
|
|
48
|
+
[string]$Uri,
|
|
49
|
+
[Parameter(Mandatory = $true)]
|
|
50
|
+
[string]$OutFile
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $OutFile
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function Invoke-JsonRequest([string]$Uri) {
|
|
57
|
+
return Invoke-RestMethod -UseBasicParsing -Uri $Uri -Headers @{
|
|
58
|
+
"User-Agent" = "pablozaiden-installer"
|
|
59
|
+
"Accept" = "application/vnd.github+json"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Invoke-RawJsonRequest([string]$Uri) {
|
|
64
|
+
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -Headers @{
|
|
65
|
+
"User-Agent" = "pablozaiden-installer"
|
|
66
|
+
"Accept" = "application/json"
|
|
67
|
+
}
|
|
68
|
+
return ConvertFrom-Json -InputObject ([string]$response.Content)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Test-HttpStatus {
|
|
72
|
+
param(
|
|
73
|
+
[Parameter(Mandatory = $true)]
|
|
74
|
+
[object]$ErrorRecord,
|
|
75
|
+
[Parameter(Mandatory = $true)]
|
|
76
|
+
[int]$StatusCode
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
$responseProperty = $ErrorRecord.Exception.PSObject.Properties["Response"]
|
|
80
|
+
if ($null -eq $responseProperty -or $null -eq $responseProperty.Value) {
|
|
81
|
+
return $false
|
|
82
|
+
}
|
|
83
|
+
$statusProperty = $responseProperty.Value.PSObject.Properties["StatusCode"]
|
|
84
|
+
return $null -ne $statusProperty -and [int]$statusProperty.Value -eq $StatusCode
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Expand-InstallDirectory([string]$Value) {
|
|
88
|
+
if ($Value -eq '$HOME' -or $Value -eq "~") {
|
|
89
|
+
return $HOME
|
|
90
|
+
}
|
|
91
|
+
if ($Value.StartsWith('$HOME/') -or $Value.StartsWith('$HOME\')) {
|
|
92
|
+
return Join-Path $HOME $Value.Substring(6)
|
|
93
|
+
}
|
|
94
|
+
if ($Value.StartsWith("~/") -or $Value.StartsWith("~\")) {
|
|
95
|
+
return Join-Path $HOME $Value.Substring(2)
|
|
96
|
+
}
|
|
97
|
+
return $Value
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Get-ReleaseArchitecture {
|
|
101
|
+
$architecture = $env:PROCESSOR_ARCHITEW6432
|
|
102
|
+
if ([string]::IsNullOrWhiteSpace($architecture)) {
|
|
103
|
+
$architecture = $env:PROCESSOR_ARCHITECTURE
|
|
104
|
+
}
|
|
105
|
+
switch ($architecture.ToUpperInvariant()) {
|
|
106
|
+
"AMD64" { return "x64" }
|
|
107
|
+
"ARM64" { return "arm64" }
|
|
108
|
+
default { Fail "Unsupported architecture: $architecture. Supported architectures are x64 and arm64." }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function Get-Manifest {
|
|
113
|
+
param(
|
|
114
|
+
[Parameter(Mandatory = $true)]
|
|
115
|
+
[string]$TargetRepository,
|
|
116
|
+
[Parameter(Mandatory = $true)]
|
|
117
|
+
[string]$TargetRef
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
$rawBaseUrl = if ($env:RAW_BASE_URL) {
|
|
121
|
+
$env:RAW_BASE_URL.TrimEnd("/")
|
|
122
|
+
} else {
|
|
123
|
+
"https://raw.githubusercontent.com"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
foreach ($path in $DefaultManifestPaths) {
|
|
127
|
+
$uri = "$rawBaseUrl/$TargetRepository/$TargetRef/$path"
|
|
128
|
+
try {
|
|
129
|
+
$manifest = Invoke-RawJsonRequest $uri
|
|
130
|
+
return [PSCustomObject]@{
|
|
131
|
+
Path = $path
|
|
132
|
+
Value = $manifest
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
if (Test-HttpStatus -ErrorRecord $_ -StatusCode 404) {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
throw
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return $null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function Assert-ManifestPlatform {
|
|
145
|
+
param(
|
|
146
|
+
[Parameter(Mandatory = $true)]
|
|
147
|
+
[object]$Manifest,
|
|
148
|
+
[Parameter(Mandatory = $true)]
|
|
149
|
+
[string]$Architecture
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
$platformsProperty = $Manifest.PSObject.Properties["platforms"]
|
|
153
|
+
if ($null -eq $platformsProperty) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
$windowsProperty = $platformsProperty.Value.PSObject.Properties["windows"]
|
|
157
|
+
if ($null -eq $windowsProperty -or @($windowsProperty.Value) -notcontains $Architecture) {
|
|
158
|
+
Fail "Manifest does not support platform windows-$Architecture."
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function Get-ExpectedChecksum {
|
|
163
|
+
param(
|
|
164
|
+
[Parameter(Mandatory = $true)]
|
|
165
|
+
[string]$ChecksumPath,
|
|
166
|
+
[Parameter(Mandatory = $true)]
|
|
167
|
+
[string]$AssetName
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
$plainHashes = @()
|
|
171
|
+
foreach ($line in (Get-Content -LiteralPath $ChecksumPath)) {
|
|
172
|
+
$match = [regex]::Match($line.Trim(), '^([0-9a-fA-F]{64})(?:\s+\*?(.+))?$')
|
|
173
|
+
if (-not $match.Success) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
$hash = $match.Groups[1].Value.ToLowerInvariant()
|
|
177
|
+
$fileName = $match.Groups[2].Value.Trim()
|
|
178
|
+
if ($fileName) {
|
|
179
|
+
if ([IO.Path]::GetFileName($fileName) -eq $AssetName) {
|
|
180
|
+
return $hash
|
|
181
|
+
}
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
$plainHashes += $hash
|
|
185
|
+
}
|
|
186
|
+
if ($plainHashes.Count -eq 1) {
|
|
187
|
+
return $plainHashes[0]
|
|
188
|
+
}
|
|
189
|
+
Fail "Checksum for $AssetName did not contain a valid SHA-256 entry."
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function Get-Sha256([string]$Path) {
|
|
193
|
+
$stream = [IO.File]::OpenRead($Path)
|
|
194
|
+
$algorithm = [Security.Cryptography.SHA256]::Create()
|
|
195
|
+
try {
|
|
196
|
+
return [BitConverter]::ToString($algorithm.ComputeHash($stream)).Replace("-", "").ToLowerInvariant()
|
|
197
|
+
} finally {
|
|
198
|
+
$algorithm.Dispose()
|
|
199
|
+
$stream.Dispose()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function Install-Binary {
|
|
204
|
+
param(
|
|
205
|
+
[Parameter(Mandatory = $true)]
|
|
206
|
+
[string]$TargetRepository,
|
|
207
|
+
[Parameter(Mandatory = $true)]
|
|
208
|
+
[string]$Tag,
|
|
209
|
+
[Parameter(Mandatory = $true)]
|
|
210
|
+
[string]$Architecture,
|
|
211
|
+
[Parameter(Mandatory = $true)]
|
|
212
|
+
[string]$TargetInstallDir,
|
|
213
|
+
[Parameter(Mandatory = $true)]
|
|
214
|
+
[string]$ChecksumPolicy,
|
|
215
|
+
[Parameter(Mandatory = $true)]
|
|
216
|
+
[string]$ChecksumExtension,
|
|
217
|
+
[Parameter(Mandatory = $true)]
|
|
218
|
+
[object]$BinaryDefinition
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
$name = [string]$BinaryDefinition.name
|
|
222
|
+
$prefixProperty = $BinaryDefinition.PSObject.Properties["assetPrefix"]
|
|
223
|
+
$assetPrefix = if ($null -eq $prefixProperty -or [string]::IsNullOrWhiteSpace([string]$prefixProperty.Value)) {
|
|
224
|
+
$name
|
|
225
|
+
} else {
|
|
226
|
+
[string]$prefixProperty.Value
|
|
227
|
+
}
|
|
228
|
+
$requiredProperty = $BinaryDefinition.PSObject.Properties["required"]
|
|
229
|
+
$required = $null -eq $requiredProperty -or [bool]$requiredProperty.Value
|
|
230
|
+
$assetName = "$assetPrefix-$Tag-windows-$Architecture.exe"
|
|
231
|
+
$releaseBaseUrl = if ($env:GITHUB_RELEASE_BASE_URL) {
|
|
232
|
+
$env:GITHUB_RELEASE_BASE_URL.TrimEnd("/")
|
|
233
|
+
} else {
|
|
234
|
+
"https://github.com"
|
|
235
|
+
}
|
|
236
|
+
$downloadUrl = "$releaseBaseUrl/$TargetRepository/releases/download/$Tag/$assetName"
|
|
237
|
+
$tempFile = [IO.Path]::GetTempFileName()
|
|
238
|
+
$checksumFile = [IO.Path]::GetTempFileName()
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
[Console]::WriteLine("Downloading $assetName...")
|
|
242
|
+
try {
|
|
243
|
+
Invoke-Download -Uri $downloadUrl -OutFile $tempFile
|
|
244
|
+
} catch {
|
|
245
|
+
if (-not $required -and (Test-HttpStatus -ErrorRecord $_ -StatusCode 404)) {
|
|
246
|
+
[Console]::WriteLine("Skipping optional binary $name; $assetName was not published.")
|
|
247
|
+
return $false
|
|
248
|
+
}
|
|
249
|
+
throw
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if ($ChecksumPolicy -eq "none") {
|
|
253
|
+
[Console]::WriteLine("Skipping checksum verification for $assetName; checksum policy is none.")
|
|
254
|
+
} else {
|
|
255
|
+
$checksumName = "$assetName$ChecksumExtension"
|
|
256
|
+
$checksumUrl = "$downloadUrl$ChecksumExtension"
|
|
257
|
+
$checksumDownloaded = $true
|
|
258
|
+
try {
|
|
259
|
+
Invoke-Download -Uri $checksumUrl -OutFile $checksumFile
|
|
260
|
+
} catch {
|
|
261
|
+
if ($ChecksumPolicy -eq "required" -or -not (Test-HttpStatus -ErrorRecord $_ -StatusCode 404)) {
|
|
262
|
+
throw
|
|
263
|
+
}
|
|
264
|
+
[Console]::WriteLine("Skipping checksum verification for $assetName; $checksumName was not published.")
|
|
265
|
+
$checksumDownloaded = $false
|
|
266
|
+
}
|
|
267
|
+
if ($checksumDownloaded) {
|
|
268
|
+
[Console]::WriteLine("Verifying $checksumName...")
|
|
269
|
+
$expected = Get-ExpectedChecksum -ChecksumPath $checksumFile -AssetName $assetName
|
|
270
|
+
$actual = Get-Sha256 $tempFile
|
|
271
|
+
if ($expected -ne $actual) {
|
|
272
|
+
Fail "Checksum verification failed for $assetName`: expected $expected, got $actual."
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
$fileName = if ($name.EndsWith(".exe", [StringComparison]::OrdinalIgnoreCase)) {
|
|
278
|
+
$name
|
|
279
|
+
} else {
|
|
280
|
+
"$name.exe"
|
|
281
|
+
}
|
|
282
|
+
$targetPath = Join-Path $TargetInstallDir $fileName
|
|
283
|
+
Move-Item -LiteralPath $tempFile -Destination $targetPath -Force
|
|
284
|
+
[Console]::WriteLine("Installed $name to $targetPath")
|
|
285
|
+
return $true
|
|
286
|
+
} finally {
|
|
287
|
+
Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue
|
|
288
|
+
Remove-Item -LiteralPath $checksumFile -Force -ErrorAction SilentlyContinue
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function Add-UserPath([string]$Directory) {
|
|
293
|
+
$fullDirectory = [IO.Path]::GetFullPath($Directory).TrimEnd("\")
|
|
294
|
+
$currentEntries = @($env:Path -split ";" | Where-Object { $_ })
|
|
295
|
+
$alreadyCurrent = $currentEntries | Where-Object {
|
|
296
|
+
try {
|
|
297
|
+
[IO.Path]::GetFullPath(
|
|
298
|
+
[Environment]::ExpandEnvironmentVariables($_.Trim().Trim('"'))
|
|
299
|
+
).TrimEnd("\").Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
300
|
+
} catch {
|
|
301
|
+
$false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if ($alreadyCurrent) {
|
|
305
|
+
[Console]::WriteLine("")
|
|
306
|
+
[Console]::WriteLine("Installation complete!")
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ($NoModifyPath) {
|
|
311
|
+
[Console]::WriteLine("")
|
|
312
|
+
[Console]::WriteLine("Warning: $fullDirectory is not in PATH.")
|
|
313
|
+
[Console]::WriteLine("Add it to your user PATH before invoking installed binaries by name.")
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
318
|
+
$userEntries = @($userPath -split ";" | Where-Object { $_ })
|
|
319
|
+
$alreadyUser = $userEntries | Where-Object {
|
|
320
|
+
try {
|
|
321
|
+
[IO.Path]::GetFullPath(
|
|
322
|
+
[Environment]::ExpandEnvironmentVariables($_.Trim().Trim('"'))
|
|
323
|
+
).TrimEnd("\").Equals($fullDirectory, [StringComparison]::OrdinalIgnoreCase)
|
|
324
|
+
} catch {
|
|
325
|
+
$false
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (-not $alreadyUser) {
|
|
329
|
+
$nextUserPath = if ([string]::IsNullOrWhiteSpace($userPath)) {
|
|
330
|
+
$fullDirectory
|
|
331
|
+
} else {
|
|
332
|
+
"$fullDirectory;$userPath"
|
|
333
|
+
}
|
|
334
|
+
[Environment]::SetEnvironmentVariable("Path", $nextUserPath, "User")
|
|
335
|
+
}
|
|
336
|
+
$env:Path = "$fullDirectory;$env:Path"
|
|
337
|
+
[Console]::WriteLine("")
|
|
338
|
+
[Console]::WriteLine("Added $fullDirectory to the user PATH.")
|
|
339
|
+
[Console]::WriteLine("Open a new terminal before invoking installed binaries by name.")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if ($Help) {
|
|
343
|
+
Show-Usage
|
|
344
|
+
exit 0
|
|
345
|
+
}
|
|
346
|
+
if ([string]::IsNullOrWhiteSpace($Repository)) {
|
|
347
|
+
Show-Usage
|
|
348
|
+
exit 1
|
|
349
|
+
}
|
|
350
|
+
if ($env:OS -ne "Windows_NT") {
|
|
351
|
+
Fail "install.ps1 supports Windows only. Use install.sh on Linux or macOS."
|
|
352
|
+
}
|
|
353
|
+
if ($Repository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') {
|
|
354
|
+
Fail "Repository must be <owner>/<repo>."
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
$architecture = Get-ReleaseArchitecture
|
|
358
|
+
[Console]::WriteLine("Installer version: $InstallerVersion")
|
|
359
|
+
[Console]::WriteLine("Detected platform: windows-$architecture")
|
|
360
|
+
|
|
361
|
+
$manifestResult = Get-Manifest -TargetRepository $Repository -TargetRef $Ref
|
|
362
|
+
$binaries = @()
|
|
363
|
+
$checksumPolicy = $Checksum
|
|
364
|
+
$checksumExtension = $DefaultChecksumExtension
|
|
365
|
+
$postInstallMessage = $null
|
|
366
|
+
|
|
367
|
+
if ($null -ne $manifestResult) {
|
|
368
|
+
[Console]::WriteLine("Loaded installer manifest: $($manifestResult.Path)")
|
|
369
|
+
$manifest = $manifestResult.Value
|
|
370
|
+
if ($manifest.schemaVersion -ne 1) {
|
|
371
|
+
Fail "Unsupported installer manifest schemaVersion: $($manifest.schemaVersion). Supported schemaVersion is 1."
|
|
372
|
+
}
|
|
373
|
+
Assert-ManifestPlatform -Manifest $manifest -Architecture $architecture
|
|
374
|
+
if ($manifest.PSObject.Properties["repo"]) {
|
|
375
|
+
$Repository = [string]$manifest.repo
|
|
376
|
+
}
|
|
377
|
+
if ($manifest.PSObject.Properties["installDir"]) {
|
|
378
|
+
$InstallDir = [string]$manifest.installDir
|
|
379
|
+
}
|
|
380
|
+
if (-not $ChecksumWasSpecified -and $manifest.PSObject.Properties["checksums"]) {
|
|
381
|
+
$checksumPolicy = if ([bool]$manifest.checksums.required) { "required" } else { "optional" }
|
|
382
|
+
if ($manifest.checksums.PSObject.Properties["extension"]) {
|
|
383
|
+
$checksumExtension = [string]$manifest.checksums.extension
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
$binaries = @($manifest.binaries)
|
|
387
|
+
if ($manifest.PSObject.Properties["postInstallMessage"]) {
|
|
388
|
+
$postInstallMessage = [string]$manifest.postInstallMessage
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
if ($Binary.Count -eq 0) {
|
|
392
|
+
Fail "No installer manifest found. Add .github/installer.json or pass -Binary <name>."
|
|
393
|
+
}
|
|
394
|
+
for ($index = 0; $index -lt $Binary.Count; $index += 1) {
|
|
395
|
+
$prefix = if ($index -lt $AssetPrefix.Count) { $AssetPrefix[$index] } else { $Binary[$index] }
|
|
396
|
+
$binaries += [PSCustomObject]@{
|
|
397
|
+
name = $Binary[$index]
|
|
398
|
+
assetPrefix = $prefix
|
|
399
|
+
required = $true
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if ($binaries.Count -eq 0) {
|
|
405
|
+
Fail "No binaries configured for installation."
|
|
406
|
+
}
|
|
407
|
+
if ([string]::IsNullOrWhiteSpace($InstallDir)) {
|
|
408
|
+
$InstallDir = $DefaultInstallDir
|
|
409
|
+
}
|
|
410
|
+
$InstallDir = Expand-InstallDirectory $InstallDir
|
|
411
|
+
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
|
412
|
+
|
|
413
|
+
$apiBaseUrl = if ($env:GITHUB_API_BASE_URL) {
|
|
414
|
+
$env:GITHUB_API_BASE_URL.TrimEnd("/")
|
|
415
|
+
} else {
|
|
416
|
+
"https://api.github.com"
|
|
417
|
+
}
|
|
418
|
+
[Console]::WriteLine("Fetching latest release...")
|
|
419
|
+
$release = Invoke-JsonRequest "$apiBaseUrl/repos/$Repository/releases/latest"
|
|
420
|
+
$tag = [string]$release.tag_name
|
|
421
|
+
if ([string]::IsNullOrWhiteSpace($tag)) {
|
|
422
|
+
Fail "Could not determine latest release version."
|
|
423
|
+
}
|
|
424
|
+
[Console]::WriteLine("Latest version: $tag")
|
|
425
|
+
|
|
426
|
+
foreach ($binaryDefinition in $binaries) {
|
|
427
|
+
$installed = Install-Binary `
|
|
428
|
+
-TargetRepository $Repository `
|
|
429
|
+
-Tag $tag `
|
|
430
|
+
-Architecture $architecture `
|
|
431
|
+
-TargetInstallDir $InstallDir `
|
|
432
|
+
-ChecksumPolicy $checksumPolicy `
|
|
433
|
+
-ChecksumExtension $checksumExtension `
|
|
434
|
+
-BinaryDefinition $binaryDefinition
|
|
435
|
+
$messageProperty = $binaryDefinition.PSObject.Properties["postInstallMessage"]
|
|
436
|
+
if ($installed -and $null -ne $messageProperty -and $messageProperty.Value) {
|
|
437
|
+
[Console]::WriteLine([string]$messageProperty.Value)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if ($postInstallMessage) {
|
|
442
|
+
[Console]::WriteLine($postInstallMessage)
|
|
443
|
+
}
|
|
444
|
+
Add-UserPath $InstallDir
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pablozaiden/installer",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Reusable GitHub release binary installer and updater utilities",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"index.ts",
|
|
13
13
|
"src/**/*",
|
|
14
14
|
"install.sh",
|
|
15
|
+
"install.ps1",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
package/src/contract.ts
CHANGED
|
@@ -11,9 +11,11 @@ export const SUPPORTED_RELEASE_TARGETS = [
|
|
|
11
11
|
"linux-arm64",
|
|
12
12
|
"darwin-x64",
|
|
13
13
|
"darwin-arm64",
|
|
14
|
+
"windows-x64",
|
|
15
|
+
"windows-arm64",
|
|
14
16
|
] as const;
|
|
15
17
|
|
|
16
|
-
export type ReleaseOs = "linux" | "darwin";
|
|
18
|
+
export type ReleaseOs = "linux" | "darwin" | "windows";
|
|
17
19
|
export type ReleaseArch = "x64" | "arm64";
|
|
18
20
|
export type ReleaseTarget = `${ReleaseOs}-${ReleaseArch}`;
|
|
19
21
|
|
|
@@ -133,7 +135,11 @@ export function parseReleaseTarget(target: string): ReleasePlatform {
|
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
export function resolveReleasePlatform(platform: string, arch: string): ReleasePlatform {
|
|
136
|
-
const os = platform === "
|
|
138
|
+
const os = platform === "win32"
|
|
139
|
+
? "windows"
|
|
140
|
+
: platform === "linux" || platform === "darwin" || platform === "windows"
|
|
141
|
+
? platform
|
|
142
|
+
: undefined;
|
|
137
143
|
const normalizedArch = arch === "x64" || arch === "amd64"
|
|
138
144
|
? "x64"
|
|
139
145
|
: arch === "arm64" || arch === "aarch64"
|
|
@@ -147,11 +153,18 @@ export function resolveReleasePlatform(platform: string, arch: string): ReleaseP
|
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
|
|
150
|
-
throw new Error(`Unsupported platform: ${platform}-${arch}. Supported release targets are Linux and
|
|
156
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}. Supported release targets are Linux, macOS, and Windows on x64 and arm64.`);
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
export function buildReleaseAssetName(assetPrefix: string, tag: string, target: ReleasePlatform): string {
|
|
154
|
-
|
|
160
|
+
const extension = target.os === "windows" ? ".exe" : "";
|
|
161
|
+
return `${assetPrefix}-${normalizeReleaseTag(tag)}-${target.os}-${target.arch}${extension}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function releaseBinaryFileName(binaryName: string, target: ReleasePlatform): string {
|
|
165
|
+
return target.os === "windows" && !binaryName.toLowerCase().endsWith(".exe")
|
|
166
|
+
? `${binaryName}.exe`
|
|
167
|
+
: binaryName;
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
export function assertGitHubRepository(value: string): GitHubRepository {
|
package/src/manifest.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type InstallerManifest = {
|
|
|
26
26
|
installDir?: string;
|
|
27
27
|
binaries: InstallerManifestBinary[];
|
|
28
28
|
checksums?: InstallerChecksumPolicy;
|
|
29
|
-
platforms?: Partial<Record<"linux" | "darwin", Array<"x64" | "arm64">>>;
|
|
29
|
+
platforms?: Partial<Record<"linux" | "darwin" | "windows", Array<"x64" | "arm64">>>;
|
|
30
30
|
postInstallMessage?: string;
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -118,7 +118,7 @@ function parsePlatforms(value: unknown): InstallerManifest["platforms"] {
|
|
|
118
118
|
}
|
|
119
119
|
const raw = expectObject(value, "platforms");
|
|
120
120
|
const platforms: InstallerManifest["platforms"] = {};
|
|
121
|
-
for (const os of ["linux", "darwin"] as const) {
|
|
121
|
+
for (const os of ["linux", "darwin", "windows"] as const) {
|
|
122
122
|
const arches = raw[os];
|
|
123
123
|
if (arches === undefined) {
|
|
124
124
|
continue;
|
|
@@ -168,7 +168,7 @@ function normalizeTargets(platforms: InstallerManifest["platforms"]): ReleaseTar
|
|
|
168
168
|
return [...SUPPORTED_RELEASE_TARGETS];
|
|
169
169
|
}
|
|
170
170
|
const targets: ReleaseTarget[] = [];
|
|
171
|
-
for (const os of ["linux", "darwin"] as const) {
|
|
171
|
+
for (const os of ["linux", "darwin", "windows"] as const) {
|
|
172
172
|
for (const arch of platforms[os] ?? []) {
|
|
173
173
|
targets.push(`${os}-${arch}`);
|
|
174
174
|
}
|
package/src/update.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
githubApiRepositoryUrl,
|
|
11
11
|
normalizeReleaseTag,
|
|
12
12
|
normalizeReleaseVersion,
|
|
13
|
+
releaseBinaryFileName,
|
|
13
14
|
resolveReleasePlatform,
|
|
14
15
|
type GitHubRepository,
|
|
15
16
|
type ReleasePlatform,
|
|
@@ -62,6 +63,8 @@ export type UpdaterDependencies = {
|
|
|
62
63
|
renameFile: (from: string, to: string) => Promise<void>;
|
|
63
64
|
removeFile: (path: string) => Promise<void>;
|
|
64
65
|
statFile: (path: string) => Promise<{ mode: number }>;
|
|
66
|
+
getCurrentProcessId: () => number;
|
|
67
|
+
spawnDetached: (command: string, args: readonly string[]) => Promise<void>;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
export type GitHubReleaseAsset = {
|
|
@@ -98,6 +101,102 @@ type StagedBinaryReplacement = {
|
|
|
98
101
|
backupPath: string;
|
|
99
102
|
};
|
|
100
103
|
|
|
104
|
+
function powerShellString(value: string): string {
|
|
105
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function windowsUpdateHelper(
|
|
109
|
+
stagedReplacements: StagedBinaryReplacement[],
|
|
110
|
+
statusPath: string,
|
|
111
|
+
parentProcessId: number,
|
|
112
|
+
): string {
|
|
113
|
+
const replacements = stagedReplacements.map((staged) => [
|
|
114
|
+
" [PSCustomObject]@{",
|
|
115
|
+
` TargetPath = ${powerShellString(staged.target.targetPath)}`,
|
|
116
|
+
` TempDirectory = ${powerShellString(staged.tempDirectory)}`,
|
|
117
|
+
` TempPath = ${powerShellString(staged.tempPath)}`,
|
|
118
|
+
` BackupPath = ${powerShellString(staged.backupPath)}`,
|
|
119
|
+
" }",
|
|
120
|
+
].join("\n")).join("\n");
|
|
121
|
+
|
|
122
|
+
return String.raw`
|
|
123
|
+
$ErrorActionPreference = "Stop"
|
|
124
|
+
$ParentProcessId = ${String(parentProcessId)}
|
|
125
|
+
$StatusPath = ${powerShellString(statusPath)}
|
|
126
|
+
$replacements = @(
|
|
127
|
+
${replacements}
|
|
128
|
+
)
|
|
129
|
+
$utf8 = [System.Text.UTF8Encoding]::new($false)
|
|
130
|
+
|
|
131
|
+
function Write-UpdateStatus([string]$Message) {
|
|
132
|
+
[System.IO.File]::WriteAllText($StatusPath, $Message, $utf8)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Write-UpdateStatus "Update helper started."
|
|
136
|
+
$deadline = [DateTime]::UtcNow.AddMinutes(5)
|
|
137
|
+
|
|
138
|
+
while ($ParentProcessId -gt 0) {
|
|
139
|
+
try {
|
|
140
|
+
$parentProcess = [System.Diagnostics.Process]::GetProcessById($ParentProcessId)
|
|
141
|
+
if ($parentProcess.HasExited) {
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
} catch [System.ArgumentException] {
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
if ([DateTime]::UtcNow -ge $deadline) {
|
|
148
|
+
Write-UpdateStatus "Timed out waiting for process $ParentProcessId to exit."
|
|
149
|
+
exit 1
|
|
150
|
+
}
|
|
151
|
+
[System.Threading.Thread]::Sleep(100)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
$movedBackups = [System.Collections.Generic.List[object]]::new()
|
|
155
|
+
$installedReplacements = [System.Collections.Generic.List[object]]::new()
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
foreach ($replacement in $replacements) {
|
|
159
|
+
[System.IO.File]::Move($replacement.TargetPath, $replacement.BackupPath)
|
|
160
|
+
$movedBackups.Add($replacement)
|
|
161
|
+
[System.IO.File]::Move($replacement.TempPath, $replacement.TargetPath)
|
|
162
|
+
$installedReplacements.Add($replacement)
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
$failure = $_.Exception.Message
|
|
166
|
+
try {
|
|
167
|
+
for ($index = $installedReplacements.Count - 1; $index -ge 0; $index -= 1) {
|
|
168
|
+
[System.IO.File]::Delete($installedReplacements[$index].TargetPath)
|
|
169
|
+
}
|
|
170
|
+
for ($index = $movedBackups.Count - 1; $index -ge 0; $index -= 1) {
|
|
171
|
+
[System.IO.File]::Move(
|
|
172
|
+
$movedBackups[$index].BackupPath,
|
|
173
|
+
$movedBackups[$index].TargetPath
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
$failure = $failure + " Rollback failed: " + $_.Exception.Message
|
|
178
|
+
}
|
|
179
|
+
Write-UpdateStatus $failure
|
|
180
|
+
exit 1
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
[System.IO.File]::Delete($StatusPath)
|
|
184
|
+
$cleanupDirectories = [System.Collections.Generic.HashSet[string]]::new(
|
|
185
|
+
[System.StringComparer]::OrdinalIgnoreCase
|
|
186
|
+
)
|
|
187
|
+
foreach ($replacement in $replacements) {
|
|
188
|
+
[void]$cleanupDirectories.Add($replacement.TempDirectory)
|
|
189
|
+
}
|
|
190
|
+
foreach ($directory in $cleanupDirectories) {
|
|
191
|
+
try {
|
|
192
|
+
[System.IO.Directory]::Delete($directory, $true)
|
|
193
|
+
} catch {
|
|
194
|
+
Write-UpdateStatus ("Update installed but cleanup failed: " + $_.Exception.Message)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
101
200
|
function createDefaultUpdateDependencies(): UpdaterDependencies {
|
|
102
201
|
return {
|
|
103
202
|
fetchFn: fetch,
|
|
@@ -127,6 +226,20 @@ function createDefaultUpdateDependencies(): UpdaterDependencies {
|
|
|
127
226
|
const result = await stat(path);
|
|
128
227
|
return { mode: result.mode };
|
|
129
228
|
},
|
|
229
|
+
getCurrentProcessId: () => process.pid,
|
|
230
|
+
spawnDetached: async (command, args) => {
|
|
231
|
+
// Bun's detached children remain in its kill-on-close job object on Windows.
|
|
232
|
+
const bootstrap = Bun.spawn(["cmd.exe", "/d", "/c", "start", "", "/b", command, ...args], {
|
|
233
|
+
stdin: "ignore",
|
|
234
|
+
stdout: "ignore",
|
|
235
|
+
stderr: "ignore",
|
|
236
|
+
windowsHide: true,
|
|
237
|
+
});
|
|
238
|
+
const exitCode = await bootstrap.exited;
|
|
239
|
+
if (exitCode !== 0) {
|
|
240
|
+
throw new Error(`Failed to launch the deferred updater (cmd.exe exited with ${String(exitCode)}).`);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
130
243
|
};
|
|
131
244
|
}
|
|
132
245
|
|
|
@@ -307,7 +420,7 @@ export async function verifyReleaseAssetChecksum(
|
|
|
307
420
|
|
|
308
421
|
async function resolveInstalledBinaryPath(binaryName: string, productName: string, dependencies: UpdaterDependencies): Promise<string> {
|
|
309
422
|
const executablePath = dependencies.getExecutablePath();
|
|
310
|
-
const executableName = basename(executablePath);
|
|
423
|
+
const executableName = basename(executablePath).toLowerCase().replace(/\.exe$/, "");
|
|
311
424
|
if (executableName === "bun" || executableName.startsWith("bun-")) {
|
|
312
425
|
throw new Error(`${binaryName} update only works from an installed ${productName} binary. Use the installer script when running from source.`);
|
|
313
426
|
}
|
|
@@ -323,6 +436,7 @@ async function stageInstalledBinaryReplacement(
|
|
|
323
436
|
asset: ResolvedReleaseAsset,
|
|
324
437
|
config: UpdaterConfig,
|
|
325
438
|
checksumPolicy: Required<UpdaterChecksumPolicy>,
|
|
439
|
+
platform: ReleasePlatform,
|
|
326
440
|
dependencies: UpdaterDependencies,
|
|
327
441
|
): Promise<StagedBinaryReplacement> {
|
|
328
442
|
const targetPath = target.targetPath;
|
|
@@ -343,9 +457,11 @@ async function stageInstalledBinaryReplacement(
|
|
|
343
457
|
await verifyReleaseAssetChecksum(asset, payload, checksumPolicy, dependencies);
|
|
344
458
|
await dependencies.writeBinary(tempPath, payload);
|
|
345
459
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
460
|
+
if (platform.os !== "windows") {
|
|
461
|
+
const installedBinaryStat = await dependencies.statFile(targetPath);
|
|
462
|
+
const executableMode = installedBinaryStat.mode & 0o777;
|
|
463
|
+
await dependencies.chmodFile(tempPath, executableMode || 0o755);
|
|
464
|
+
}
|
|
349
465
|
staged = true;
|
|
350
466
|
return {
|
|
351
467
|
target,
|
|
@@ -404,11 +520,41 @@ async function replaceStagedBinaryReplacements(
|
|
|
404
520
|
}
|
|
405
521
|
}
|
|
406
522
|
|
|
407
|
-
async function
|
|
523
|
+
async function scheduleWindowsBinaryReplacements(
|
|
524
|
+
stagedReplacements: StagedBinaryReplacement[],
|
|
525
|
+
dependencies: UpdaterDependencies,
|
|
526
|
+
): Promise<void> {
|
|
527
|
+
const primary = stagedReplacements.at(-1);
|
|
528
|
+
if (!primary) {
|
|
529
|
+
throw new Error("No staged Windows binary replacements were provided.");
|
|
530
|
+
}
|
|
531
|
+
const statusPath = `${primary.target.targetPath}.update-error.log`;
|
|
532
|
+
const parentProcessId = dependencies.getCurrentProcessId();
|
|
533
|
+
const encodedHelper = Buffer.from(
|
|
534
|
+
windowsUpdateHelper(stagedReplacements, statusPath, parentProcessId),
|
|
535
|
+
"utf16le",
|
|
536
|
+
).toString("base64");
|
|
537
|
+
await dependencies.removeFile(statusPath);
|
|
538
|
+
await dependencies.spawnDetached("powershell.exe", [
|
|
539
|
+
"-NoLogo",
|
|
540
|
+
"-NoProfile",
|
|
541
|
+
"-NonInteractive",
|
|
542
|
+
"-ExecutionPolicy",
|
|
543
|
+
"Bypass",
|
|
544
|
+
"-EncodedCommand",
|
|
545
|
+
encodedHelper,
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function resolveInstalledBinaryTargets(
|
|
550
|
+
config: UpdaterConfig,
|
|
551
|
+
platform: ReleasePlatform,
|
|
552
|
+
dependencies: UpdaterDependencies,
|
|
553
|
+
): Promise<InstalledBinaryTarget[]> {
|
|
408
554
|
const primaryPath = await resolveInstalledBinaryPath(config.binaryName, config.productName ?? config.binaryName, dependencies);
|
|
409
555
|
const targets: InstalledBinaryTarget[] = [];
|
|
410
556
|
for (const companion of config.companionBinaries ?? []) {
|
|
411
|
-
const companionPath = join(dirname(primaryPath), companion.binaryName);
|
|
557
|
+
const companionPath = join(dirname(primaryPath), releaseBinaryFileName(companion.binaryName, platform));
|
|
412
558
|
if (await dependencies.fileExists(companionPath)) {
|
|
413
559
|
targets.push({
|
|
414
560
|
binaryName: companion.binaryName,
|
|
@@ -472,15 +618,33 @@ export async function runUpdateCommand(
|
|
|
472
618
|
}
|
|
473
619
|
|
|
474
620
|
const stagedReplacements: StagedBinaryReplacement[] = [];
|
|
621
|
+
let deferredWindowsReplacement = false;
|
|
475
622
|
try {
|
|
476
|
-
const installedTargets = await resolveInstalledBinaryTargets(normalizedConfig, dependencies);
|
|
623
|
+
const installedTargets = await resolveInstalledBinaryTargets(normalizedConfig, releasePlatform, dependencies);
|
|
477
624
|
for (const target of installedTargets) {
|
|
478
625
|
const asset = resolveReleaseAsset(release, releasePlatform, {
|
|
479
626
|
binaryName: target.binaryName,
|
|
480
627
|
assetPrefix: target.assetPrefix,
|
|
481
628
|
checksum: checksumPolicy,
|
|
482
629
|
});
|
|
483
|
-
stagedReplacements.push(await stageInstalledBinaryReplacement(
|
|
630
|
+
stagedReplacements.push(await stageInstalledBinaryReplacement(
|
|
631
|
+
target,
|
|
632
|
+
asset,
|
|
633
|
+
normalizedConfig,
|
|
634
|
+
checksumPolicy,
|
|
635
|
+
releasePlatform,
|
|
636
|
+
dependencies,
|
|
637
|
+
));
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (releasePlatform.os === "windows") {
|
|
641
|
+
await scheduleWindowsBinaryReplacements(stagedReplacements, dependencies);
|
|
642
|
+
deferredWindowsReplacement = true;
|
|
643
|
+
dependencies.out(
|
|
644
|
+
`Staged ${normalizedConfig.productName ?? normalizedConfig.binaryName} ${primaryAsset.version}. `
|
|
645
|
+
+ `The update will complete after process ${String(dependencies.getCurrentProcessId())} exits.`,
|
|
646
|
+
);
|
|
647
|
+
return 0;
|
|
484
648
|
}
|
|
485
649
|
|
|
486
650
|
await replaceStagedBinaryReplacements(stagedReplacements, normalizedConfig, dependencies);
|
|
@@ -493,7 +657,9 @@ export async function runUpdateCommand(
|
|
|
493
657
|
}
|
|
494
658
|
}
|
|
495
659
|
} finally {
|
|
496
|
-
|
|
660
|
+
if (!deferredWindowsReplacement) {
|
|
661
|
+
await cleanupStagedBinaryReplacements(stagedReplacements, dependencies);
|
|
662
|
+
}
|
|
497
663
|
}
|
|
498
664
|
|
|
499
665
|
return 0;
|