restorm-cli 1.0.2 → 1.0.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/lib/install.js +75 -9
- package/package.json +1 -1
package/lib/install.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
createWriteStream,
|
|
17
17
|
existsSync,
|
|
18
18
|
mkdirSync,
|
|
19
|
+
readFileSync,
|
|
19
20
|
readdirSync,
|
|
20
21
|
renameSync,
|
|
21
22
|
rmSync,
|
|
@@ -29,6 +30,32 @@ const resolve = require('./resolve')
|
|
|
29
30
|
|
|
30
31
|
const COMPLETE_MARKER = '.complete'
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* What a finished install left behind: the path, relative to the cache
|
|
35
|
+
* directory, of the binary to run. Returns null when there is no completed
|
|
36
|
+
* install here.
|
|
37
|
+
*
|
|
38
|
+
* 1.0.2 wrote the bare version string into this file and always ran the file
|
|
39
|
+
* the manifest named. Such a cache is still valid — it just cannot say what to
|
|
40
|
+
* execute, so the caller falls back to the manifest, exactly as that version
|
|
41
|
+
* did. Re-downloading 400 MB to upgrade a marker format would be a poor trade.
|
|
42
|
+
*/
|
|
43
|
+
function readMarker(path, fallbackExec) {
|
|
44
|
+
let raw
|
|
45
|
+
try {
|
|
46
|
+
raw = readFileSync(path, 'utf8').trim()
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw)
|
|
52
|
+
if (parsed && typeof parsed.exec === 'string') return parsed.exec
|
|
53
|
+
} catch {
|
|
54
|
+
/* the 1.0.2 format: a bare version string */
|
|
55
|
+
}
|
|
56
|
+
return fallbackExec
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
/**
|
|
33
60
|
* Asks for this package's own version first, and falls back to the newest
|
|
34
61
|
* release if that manifest is not there — which happens for exactly one reason
|
|
@@ -87,6 +114,33 @@ async function downloadVerified(url, dest, sha256, { fetchImpl, onProgress } = {
|
|
|
87
114
|
return actual
|
|
88
115
|
}
|
|
89
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Unpack an AppImage into the cache and return the path, relative to the cache
|
|
119
|
+
* directory, of what should actually be executed.
|
|
120
|
+
*
|
|
121
|
+
* An AppImage mounts itself with FUSE to run — and FUSE is exactly what a CI
|
|
122
|
+
* runner does not have: Ubuntu stopped shipping `libfuse2` with 22.04, so every
|
|
123
|
+
* GitHub-hosted Linux runner and every plain container answers `AppImages
|
|
124
|
+
* require FUSE to run`. Unpacking it once, here, removes that dependency for
|
|
125
|
+
* good: the extracted tree runs anywhere, and the cache is what makes paying
|
|
126
|
+
* the extraction once acceptable.
|
|
127
|
+
*
|
|
128
|
+
* `--appimage-extract` is self-contained and needs no FUSE itself. If it fails
|
|
129
|
+
* anyway, the image is kept and run directly — a machine with FUSE is no worse
|
|
130
|
+
* off than before.
|
|
131
|
+
*/
|
|
132
|
+
function unpackAppImage(image, dir, fileName, log) {
|
|
133
|
+
const r = spawnSync(image, ['--appimage-extract'], { cwd: dir, stdio: 'ignore' })
|
|
134
|
+
const appRun = join(dir, 'squashfs-root', 'AppRun')
|
|
135
|
+
if (r.error || r.status !== 0 || !existsSync(appRun)) {
|
|
136
|
+
if (log) log('Could not unpack the AppImage — running the image itself (this needs FUSE).')
|
|
137
|
+
return fileName
|
|
138
|
+
}
|
|
139
|
+
// The image is ~400 MB and nothing reads it once unpacked.
|
|
140
|
+
rmSync(image, { force: true })
|
|
141
|
+
return join('squashfs-root', 'AppRun')
|
|
142
|
+
}
|
|
143
|
+
|
|
90
144
|
function extractArchive(platform, archive, dir) {
|
|
91
145
|
const candidates = resolve.extractCommands(platform, archive, dir)
|
|
92
146
|
if (candidates.length === 0) throw new Error(`no extractor known for ${platform}`)
|
|
@@ -157,12 +211,18 @@ async function ensureBuild(options = {}) {
|
|
|
157
211
|
const root = resolve.cacheRoot(env, platform, home)
|
|
158
212
|
const versionDir = join(root, version)
|
|
159
213
|
const dir = join(versionDir, resolve.platformKey(platform, arch))
|
|
160
|
-
const exe = join(dir, artifact.exec)
|
|
161
214
|
|
|
162
|
-
|
|
163
|
-
|
|
215
|
+
// What to execute is decided at install time (an unpacked AppImage does not
|
|
216
|
+
// run through the file the manifest names), so the marker records it rather
|
|
217
|
+
// than leaving a later run to guess.
|
|
218
|
+
const cachedExec = readMarker(join(dir, COMPLETE_MARKER), artifact.exec)
|
|
219
|
+
if (cachedExec) {
|
|
220
|
+
const exe = join(dir, cachedExec)
|
|
221
|
+
if (existsSync(exe)) return { exe, version, cached: true }
|
|
164
222
|
}
|
|
165
223
|
|
|
224
|
+
let installedExec = artifact.exec
|
|
225
|
+
|
|
166
226
|
// Everything below happens under a private staging directory, so two runs
|
|
167
227
|
// racing on the same cache (two CI jobs on one machine) cannot see each
|
|
168
228
|
// other's partial work. The loser of the race throws its copy away.
|
|
@@ -177,9 +237,12 @@ async function ensureBuild(options = {}) {
|
|
|
177
237
|
|
|
178
238
|
const payload = join(stage, 'payload')
|
|
179
239
|
mkdirSync(payload, { recursive: true })
|
|
240
|
+
let execPath = artifact.exec
|
|
180
241
|
if (artifact.kind === 'appimage') {
|
|
181
|
-
|
|
182
|
-
|
|
242
|
+
const image = join(payload, artifact.exec)
|
|
243
|
+
renameSync(archive, image)
|
|
244
|
+
chmodSync(image, 0o755)
|
|
245
|
+
execPath = unpackAppImage(image, payload, artifact.exec, log)
|
|
183
246
|
} else {
|
|
184
247
|
extractArchive(platform, archive, payload)
|
|
185
248
|
rmSync(archive, { force: true })
|
|
@@ -196,21 +259,24 @@ async function ensureBuild(options = {}) {
|
|
|
196
259
|
|
|
197
260
|
// The marker goes in before the directory is published, so the name a
|
|
198
261
|
// later run tests for can only appear on a finished install.
|
|
199
|
-
writeFileSync(join(payload, COMPLETE_MARKER), `${version}\n`)
|
|
262
|
+
writeFileSync(join(payload, COMPLETE_MARKER), `${JSON.stringify({ version, exec: execPath })}\n`)
|
|
200
263
|
mkdirSync(versionDir, { recursive: true })
|
|
201
264
|
try {
|
|
202
265
|
renameSync(payload, dir)
|
|
203
266
|
} catch (e) {
|
|
204
267
|
// Another process published the same version first — theirs is as good
|
|
205
|
-
// as ours, and verified the same way.
|
|
206
|
-
|
|
268
|
+
// as ours, and verified the same way. Their marker decides what runs.
|
|
269
|
+
const theirs = readMarker(join(dir, COMPLETE_MARKER), artifact.exec)
|
|
270
|
+
if (!theirs) throw e
|
|
271
|
+
execPath = theirs
|
|
207
272
|
}
|
|
273
|
+
installedExec = execPath
|
|
208
274
|
} finally {
|
|
209
275
|
rmSync(stage, { recursive: true, force: true })
|
|
210
276
|
}
|
|
211
277
|
|
|
212
278
|
prune(root, version, log)
|
|
213
|
-
return { exe, version, cached: false }
|
|
279
|
+
return { exe: join(dir, installedExec), version, cached: false }
|
|
214
280
|
}
|
|
215
281
|
|
|
216
282
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "restorm-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Restorm — local-first API testing environment (REST, GraphQL, gRPC, WebSocket, MQTT, Redis, AMQP…). Runs Restorm from a terminal or from CI: your installation if you have one, otherwise the official build, downloaded once, checksum-verified and cached.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"restorm": "bin/restorm.js"
|