restorm-cli 1.0.2 → 1.0.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.
Files changed (2) hide show
  1. package/lib/install.js +75 -9
  2. 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
- if (existsSync(join(dir, COMPLETE_MARKER)) && existsSync(exe)) {
163
- return { exe, version, cached: true }
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
- renameSync(archive, join(payload, artifact.exec))
182
- chmodSync(join(payload, artifact.exec), 0o755)
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
- if (!existsSync(exe)) throw e
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.2",
3
+ "version": "1.0.3",
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"