pinokiod 7.2.4 → 7.2.6

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.
@@ -1,13 +1,16 @@
1
+ const crypto = require("crypto")
1
2
  const fs = require("fs")
2
3
  const path = require("path")
3
4
  const { execFile } = require("child_process")
4
5
  const ParcelWatcher = require("@parcel/watcher")
5
6
  const semver = require("semver")
6
7
  const { rimraf } = require("rimraf")
8
+ const Util = require("../util")
7
9
 
8
10
  const RELEASE_VERSION = "8.0.1"
9
11
  const RELEASE_RANGE = ">=8.0.1 <8.1.0"
10
12
  const CONDA_SPEC = `ffmpeg=${RELEASE_VERSION}`
13
+ const CONDA_CHANNEL_FLAGS = "--override-channels -c conda-forge"
11
14
 
12
15
  const WINDOWS_GDK_PIXBUF_POST_LINK_NOOP = `@echo off
13
16
  rem Pinokio intentionally skips gdk-pixbuf loader cache generation for FFmpeg installs.
@@ -27,25 +30,36 @@ class Ffmpeg {
27
30
  FFMPEG_PATH: this.binaryPath("ffmpeg", activeKernel),
28
31
  FFPROBE_PATH: this.binaryPath("ffprobe", activeKernel)
29
32
  }
33
+ if (activeKernel.platform === "win32") {
34
+ env.PATH = [this.libraryDir(activeKernel)]
35
+ }
30
36
  if (activeKernel.platform === "linux") {
31
37
  env.LD_LIBRARY_PATH = [this.libraryDir(activeKernel)]
32
38
  }
33
39
  return env
34
40
  }
35
41
 
42
+ ffmpegPrefix(kernel = this.kernel) {
43
+ return kernel.bin.path("ffmpeg-env")
44
+ }
45
+
46
+ ffmpegPkgsDir(kernel = this.kernel) {
47
+ return kernel.bin.path("ffmpeg-pkgs")
48
+ }
49
+
36
50
  binaryPath(tool, kernel = this.kernel) {
37
51
  const filename = kernel.platform === "win32" ? `${tool}.exe` : tool
38
52
  if (kernel.platform === "win32") {
39
- return kernel.bin.path("miniconda", "Library", "bin", filename)
53
+ return path.resolve(this.ffmpegPrefix(kernel), "Library", "bin", filename)
40
54
  }
41
- return kernel.bin.path("miniconda", "bin", filename)
55
+ return path.resolve(this.ffmpegPrefix(kernel), "bin", filename)
42
56
  }
43
57
 
44
58
  libraryDir(kernel = this.kernel) {
45
59
  if (kernel.platform === "win32") {
46
- return kernel.bin.path("miniconda", "Library", "bin")
60
+ return path.resolve(this.ffmpegPrefix(kernel), "Library", "bin")
47
61
  }
48
- return kernel.bin.path("miniconda", "lib")
62
+ return path.resolve(this.ffmpegPrefix(kernel), "lib")
49
63
  }
50
64
 
51
65
  legacyStandalonePaths() {
@@ -59,11 +73,19 @@ class Ffmpeg {
59
73
  if (this.kernel.platform !== "darwin") {
60
74
  return
61
75
  }
62
- if (!this.isInstalledVersion()) {
63
- return
76
+ try {
77
+ if (!(await this.hasInstalledBinaryPaths())) {
78
+ await this.removeRuntimeExposure()
79
+ return
80
+ }
81
+ await this.selfTest()
82
+ await this.ensureBaseActivationHooks()
83
+ await this.syncMacUvLibraryShims()
84
+ await this.startMacUvLibraryWatcher()
85
+ } catch (error) {
86
+ await this.removeRuntimeExposure()
87
+ console.log("conda ffmpeg start check failed", error && error.message ? error.message : error)
64
88
  }
65
- await this.syncMacUvLibraryShims()
66
- await this.startMacUvLibraryWatcher()
67
89
  }
68
90
 
69
91
  async install(req, ondata) {
@@ -73,36 +95,53 @@ class Ffmpeg {
73
95
  } else {
74
96
  await this.installStandard(ondata)
75
97
  }
76
- await this.syncMacUvLibraryShims(ondata)
77
98
  await this.selfTest(ondata)
99
+ await this.ensureBaseActivationHooks()
100
+ await this.syncMacUvLibraryShims(ondata)
78
101
  }
79
102
 
80
103
  async installStandard(ondata) {
104
+ await this.resetInstallPrefix()
81
105
  await this.kernel.bin.exec({
106
+ env: {
107
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
108
+ },
82
109
  message: [
83
110
  "conda clean -y --all",
84
- `conda install -y -c conda-forge ${this.cmd()}`
111
+ `conda create -y -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
85
112
  ]
86
113
  }, ondata)
87
114
  }
88
115
 
89
116
  async installWindows(ondata) {
117
+ await this.resetInstallPrefix()
118
+ const env = {
119
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
120
+ }
121
+
90
122
  await this.kernel.bin.exec({
123
+ env,
91
124
  message: [
92
125
  "conda clean -y --all",
93
- `conda install -y --download-only -c conda-forge ${this.cmd()}`
126
+ `conda create -y --download-only -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
94
127
  ]
95
128
  }, ondata)
96
129
 
97
- await this.patchWindowsGdkPixbufPostLink(ondata)
130
+ await this.patchWindowsGdkPixbufPostLink(this.ffmpegPkgsDir(), ondata)
98
131
 
99
132
  await this.kernel.bin.exec({
100
- message: `conda install -y --offline -c conda-forge ${this.cmd()}`
133
+ env,
134
+ message: `conda create -y --offline -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
101
135
  }, ondata)
102
136
  }
103
137
 
104
- async patchWindowsGdkPixbufPostLink(ondata) {
105
- const pkgsDir = this.kernel.bin.path("miniconda", "pkgs")
138
+ async resetInstallPrefix() {
139
+ await rimraf(this.ffmpegPrefix())
140
+ await rimraf(this.ffmpegPkgsDir())
141
+ await fs.promises.mkdir(this.ffmpegPkgsDir(), { recursive: true })
142
+ }
143
+
144
+ async patchWindowsGdkPixbufPostLink(pkgsDir, ondata) {
106
145
  const entries = await fs.promises.readdir(pkgsDir, { withFileTypes: true })
107
146
  const packageDirs = entries
108
147
  .filter((entry) => entry.isDirectory() && /^gdk-pixbuf-/.test(entry.name))
@@ -113,17 +152,31 @@ class Ffmpeg {
113
152
  }
114
153
 
115
154
  let patchedCount = 0
155
+ let metadataCount = 0
116
156
  for (const packageDir of packageDirs) {
117
157
  const scripts = [
118
- path.resolve(packageDir, "Scripts", ".gdk-pixbuf-post-link.bat"),
119
- path.resolve(packageDir, "info", "recipe", "post-link.bat")
158
+ {
159
+ relativePath: "Scripts/.gdk-pixbuf-post-link.bat",
160
+ metadataRequired: true
161
+ },
162
+ {
163
+ relativePath: "info/recipe/post-link.bat",
164
+ metadataRequired: false
165
+ }
120
166
  ]
121
167
 
122
- for (const script of scripts) {
168
+ for (const { relativePath, metadataRequired } of scripts) {
169
+ const script = path.resolve(packageDir, ...relativePath.split("/"))
123
170
  try {
124
171
  await fs.promises.access(script)
125
172
  await fs.promises.writeFile(script, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
126
173
  patchedCount += 1
174
+ const updatedMetadata = await this.updateCondaPathsJson(packageDir, relativePath, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
175
+ if (updatedMetadata) {
176
+ metadataCount += 1
177
+ } else if (metadataRequired) {
178
+ throw new Error(`Patched ${relativePath} in ${packageDir}, but did not find a matching info/paths.json entry`)
179
+ }
127
180
  } catch (error) {
128
181
  if (error && error.code !== "ENOENT") {
129
182
  throw error
@@ -136,41 +189,329 @@ class Ffmpeg {
136
189
  throw new Error("Found gdk-pixbuf in the Conda cache, but did not find any post-link scripts to patch")
137
190
  }
138
191
 
139
- ondata({ raw: `patched ${patchedCount} gdk-pixbuf post-link script(s) in the Conda cache...\r\n` })
192
+ if (ondata) {
193
+ ondata({
194
+ raw: `patched ${patchedCount} gdk-pixbuf post-link script(s) in the Conda cache and refreshed ${metadataCount} paths.json entr${metadataCount === 1 ? "y" : "ies"}...\r\n`
195
+ })
196
+ }
140
197
  }
141
198
 
142
- async installed() {
199
+ async updateCondaPathsJson(packageDir, relativePath, contents) {
200
+ const pathsJsonPath = path.resolve(packageDir, "info", "paths.json")
201
+ let pathsJson
202
+
143
203
  try {
144
- if (!this.isInstalledVersion()) {
204
+ pathsJson = JSON.parse(await fs.promises.readFile(pathsJsonPath, "utf8"))
205
+ } catch (error) {
206
+ if (error && error.code === "ENOENT") {
145
207
  return false
146
208
  }
209
+ throw error
210
+ }
211
+
212
+ if (!pathsJson || !Array.isArray(pathsJson.paths)) {
213
+ return false
214
+ }
215
+
216
+ const normalizedPath = relativePath.replace(/\\/g, "/")
217
+ const entry = pathsJson.paths.find((item) => item && item._path === normalizedPath)
218
+ if (!entry) {
219
+ return false
220
+ }
221
+
222
+ const buffer = Buffer.isBuffer(contents) ? contents : Buffer.from(String(contents), "utf8")
223
+ entry.sha256 = crypto.createHash("sha256").update(buffer).digest("hex")
224
+ entry.size_in_bytes = buffer.length
225
+
226
+ await fs.promises.writeFile(pathsJsonPath, `${JSON.stringify(pathsJson, null, 2)}\n`)
227
+ return true
228
+ }
147
229
 
230
+ async hasInstalledBinaryPaths() {
231
+ try {
148
232
  await fs.promises.access(this.binaryPath("ffmpeg"))
149
233
  await fs.promises.access(this.binaryPath("ffprobe"))
234
+ return true
235
+ } catch (error) {
236
+ return false
237
+ }
238
+ }
239
+
240
+ async removeRuntimeExposure(ondata) {
241
+ await this.stopMacUvLibraryWatcher()
242
+ await this.removeMacUvLibraryShims(ondata)
243
+ await this.removeBaseActivationHooks()
244
+ }
245
+
246
+ async installed() {
247
+ try {
248
+ if (!(await this.hasInstalledBinaryPaths())) {
249
+ await this.removeRuntimeExposure()
250
+ return false
251
+ }
252
+
253
+ await this.selfTest()
254
+ await this.ensureBaseActivationHooks()
150
255
  await this.syncMacUvLibraryShims()
151
256
  await this.startMacUvLibraryWatcher()
152
- await this.selfTest()
153
257
  return true
154
258
  } catch (error) {
259
+ await this.removeRuntimeExposure()
155
260
  console.log("conda ffmpeg installed check failed", error && error.message ? error.message : error)
156
261
  return false
157
262
  }
158
263
  }
159
264
 
160
265
  async uninstall(req, ondata) {
161
- await this.stopMacUvLibraryWatcher()
162
- await this.removeMacUvLibraryShims(ondata)
163
- await this.kernel.bin.exec({
164
- message: "conda remove -y ffmpeg"
165
- }, ondata)
266
+ await this.removeRuntimeExposure(ondata)
267
+ const prefix = this.ffmpegPrefix()
268
+ const exists = await fs.promises.access(prefix).then(() => true).catch(() => false)
269
+ if (exists) {
270
+ try {
271
+ await this.kernel.bin.exec({
272
+ env: {
273
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
274
+ },
275
+ message: `conda remove -y -p "${prefix}" --all`
276
+ }, ondata)
277
+ } catch (error) {
278
+ await rimraf(prefix)
279
+ }
280
+ }
281
+ await rimraf(prefix)
282
+ await rimraf(this.ffmpegPkgsDir())
166
283
  await this.cleanupLegacyStandalone(ondata)
167
284
  }
168
285
 
169
- isInstalledVersion() {
170
- if (!this.kernel.bin.installed?.conda?.has("ffmpeg")) {
171
- return false
286
+ activationDirs() {
287
+ return {
288
+ activate: this.kernel.bin.path("miniconda", "etc", "conda", "activate.d"),
289
+ deactivate: this.kernel.bin.path("miniconda", "etc", "conda", "deactivate.d")
290
+ }
291
+ }
292
+
293
+ activationHookFiles() {
294
+ const { activate, deactivate } = this.activationDirs()
295
+ const files = [
296
+ {
297
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.sh"),
298
+ content: this.posixActivateSh(this.kernel.platform === "win32")
299
+ },
300
+ {
301
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.sh"),
302
+ content: this.posixDeactivateSh(this.kernel.platform === "win32")
303
+ }
304
+ ]
305
+
306
+ if (this.kernel.platform !== "win32") {
307
+ return files
308
+ }
309
+
310
+ return files.concat([
311
+ {
312
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.bat"),
313
+ content: this.windowsActivateBat()
314
+ },
315
+ {
316
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.bat"),
317
+ content: this.windowsDeactivateBat()
318
+ },
319
+ {
320
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.ps1"),
321
+ content: this.windowsActivatePs1()
322
+ },
323
+ {
324
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.ps1"),
325
+ content: this.windowsDeactivatePs1()
326
+ }
327
+ ])
328
+ }
329
+
330
+ async ensureBaseActivationHooks() {
331
+ const dirs = this.activationDirs()
332
+ await fs.promises.mkdir(dirs.activate, { recursive: true }).catch(() => {})
333
+ await fs.promises.mkdir(dirs.deactivate, { recursive: true }).catch(() => {})
334
+ for (const file of this.activationHookFiles()) {
335
+ await fs.promises.writeFile(file.path, file.content)
336
+ }
337
+ }
338
+
339
+ async removeBaseActivationHooks() {
340
+ for (const file of this.activationHookFiles()) {
341
+ await fs.promises.rm(file.path, { force: true }).catch(() => {})
172
342
  }
173
- return this.kernel.bin.installed?.conda_versions?.ffmpeg === RELEASE_VERSION
343
+ }
344
+
345
+ windowsActivateBat() {
346
+ const prefix = this.ffmpegPrefix()
347
+ const runtime = this.libraryDir()
348
+ return `@echo off
349
+ set "PINOKIO_FFMPEG_PREFIX=${prefix}"
350
+ set "PINOKIO_FFMPEG_RUNTIME=${runtime}"
351
+ set "FFMPEG_PATH=%PINOKIO_FFMPEG_RUNTIME%\\ffmpeg.exe"
352
+ set "FFPROBE_PATH=%PINOKIO_FFMPEG_RUNTIME%\\ffprobe.exe"
353
+ call :pinokio_ffmpeg_remove_from_path "%PINOKIO_FFMPEG_RUNTIME%"
354
+ set "PATH=%PINOKIO_FFMPEG_RUNTIME%;%PATH%"
355
+ goto :eof
356
+
357
+ :pinokio_ffmpeg_remove_from_path
358
+ setlocal EnableDelayedExpansion
359
+ set "_pinokio_target=%~1"
360
+ set "_pinokio_path=;%PATH%;"
361
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%;=;!"
362
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%\\;=;!"
363
+ if "!_pinokio_path:~0,1!"==";" set "_pinokio_path=!_pinokio_path:~1!"
364
+ if "!_pinokio_path:~-1!"==";" set "_pinokio_path=!_pinokio_path:~0,-1!"
365
+ endlocal & set "PATH=%_pinokio_path%"
366
+ exit /b 0
367
+ `
368
+ }
369
+
370
+ windowsDeactivateBat() {
371
+ return `@echo off
372
+ if defined PINOKIO_FFMPEG_RUNTIME call :pinokio_ffmpeg_remove_from_path "%PINOKIO_FFMPEG_RUNTIME%"
373
+ set "FFMPEG_PATH="
374
+ set "FFPROBE_PATH="
375
+ set "PINOKIO_FFMPEG_PREFIX="
376
+ set "PINOKIO_FFMPEG_RUNTIME="
377
+ goto :eof
378
+
379
+ :pinokio_ffmpeg_remove_from_path
380
+ setlocal EnableDelayedExpansion
381
+ set "_pinokio_target=%~1"
382
+ set "_pinokio_path=;%PATH%;"
383
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%;=;!"
384
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%\\;=;!"
385
+ if "!_pinokio_path:~0,1!"==";" set "_pinokio_path=!_pinokio_path:~1!"
386
+ if "!_pinokio_path:~-1!"==";" set "_pinokio_path=!_pinokio_path:~0,-1!"
387
+ endlocal & set "PATH=%_pinokio_path%"
388
+ exit /b 0
389
+ `
390
+ }
391
+
392
+ windowsActivatePs1() {
393
+ const prefix = this.ffmpegPrefix().replace(/\\/g, "\\\\")
394
+ const runtime = this.libraryDir().replace(/\\/g, "\\\\")
395
+ return `$Env:PINOKIO_FFMPEG_PREFIX = "${prefix}"
396
+ $Env:PINOKIO_FFMPEG_RUNTIME = "${runtime}"
397
+ $Env:FFMPEG_PATH = Join-Path $Env:PINOKIO_FFMPEG_RUNTIME "ffmpeg.exe"
398
+ $Env:FFPROBE_PATH = Join-Path $Env:PINOKIO_FFMPEG_RUNTIME "ffprobe.exe"
399
+ $pinokioParts = @()
400
+ if ($Env:Path) {
401
+ $pinokioParts = @($Env:Path -split ';' | Where-Object { $_ -and $_ -ne $Env:PINOKIO_FFMPEG_RUNTIME })
402
+ }
403
+ $Env:Path = (@($Env:PINOKIO_FFMPEG_RUNTIME) + $pinokioParts) -join ';'
404
+ `
405
+ }
406
+
407
+ windowsDeactivatePs1() {
408
+ return `if ($Env:PINOKIO_FFMPEG_RUNTIME) {
409
+ $pinokioParts = @()
410
+ if ($Env:Path) {
411
+ $pinokioParts = @($Env:Path -split ';' | Where-Object { $_ -and $_ -ne $Env:PINOKIO_FFMPEG_RUNTIME })
412
+ }
413
+ $Env:Path = $pinokioParts -join ';'
414
+ }
415
+ Remove-Item -Path Env:\\FFMPEG_PATH -ErrorAction SilentlyContinue
416
+ Remove-Item -Path Env:\\FFPROBE_PATH -ErrorAction SilentlyContinue
417
+ Remove-Item -Path Env:\\PINOKIO_FFMPEG_PREFIX -ErrorAction SilentlyContinue
418
+ Remove-Item -Path Env:\\PINOKIO_FFMPEG_RUNTIME -ErrorAction SilentlyContinue
419
+ `
420
+ }
421
+
422
+ posixActivateSh(forceWindowsPaths = false) {
423
+ const prefix = forceWindowsPaths ? Util.p2u(this.ffmpegPrefix()) : this.ffmpegPrefix()
424
+ const binDir = forceWindowsPaths ? Util.p2u(path.resolve(this.ffmpegPrefix(), "Library", "bin")) : path.resolve(this.ffmpegPrefix(), "bin")
425
+ const libDir = forceWindowsPaths ? "" : this.libraryDir()
426
+ return `pinokio_ffmpeg_prepend_path() {
427
+ local target="$1"
428
+ local current="\${2-}"
429
+ local result=""
430
+ local part
431
+ local old_ifs="$IFS"
432
+ IFS=':'
433
+ for part in $current; do
434
+ [ -n "$part" ] || continue
435
+ [ "$part" = "$target" ] && continue
436
+ if [ -n "$result" ]; then
437
+ result="$result:$part"
438
+ else
439
+ result="$part"
440
+ fi
441
+ done
442
+ IFS="$old_ifs"
443
+ if [ -n "$result" ]; then
444
+ printf '%s:%s' "$target" "$result"
445
+ else
446
+ printf '%s' "$target"
447
+ fi
448
+ }
449
+ pinokio_ffmpeg_remove_path() {
450
+ local target="$1"
451
+ local current="\${2-}"
452
+ local result=""
453
+ local part
454
+ local old_ifs="$IFS"
455
+ IFS=':'
456
+ for part in $current; do
457
+ [ -n "$part" ] || continue
458
+ [ "$part" = "$target" ] && continue
459
+ if [ -n "$result" ]; then
460
+ result="$result:$part"
461
+ else
462
+ result="$part"
463
+ fi
464
+ done
465
+ IFS="$old_ifs"
466
+ printf '%s' "$result"
467
+ }
468
+ export PINOKIO_FFMPEG_PREFIX="${prefix}"
469
+ export PINOKIO_FFMPEG_BIN="${binDir}"
470
+ export FFMPEG_PATH="$PINOKIO_FFMPEG_BIN/${forceWindowsPaths ? "ffmpeg.exe" : "ffmpeg"}"
471
+ export FFPROBE_PATH="$PINOKIO_FFMPEG_BIN/${forceWindowsPaths ? "ffprobe.exe" : "ffprobe"}"
472
+ export PATH="$(pinokio_ffmpeg_prepend_path "$PINOKIO_FFMPEG_BIN" "$PATH")"
473
+ ${forceWindowsPaths ? "" : `if [ "$(uname -s)" = "Linux" ]; then
474
+ export LD_LIBRARY_PATH="$(pinokio_ffmpeg_prepend_path "${libDir}" "\${LD_LIBRARY_PATH-}")"
475
+ fi
476
+ `}
477
+ unset -f pinokio_ffmpeg_prepend_path
478
+ unset -f pinokio_ffmpeg_remove_path
479
+ `
480
+ }
481
+
482
+ posixDeactivateSh(forceWindowsPaths = false) {
483
+ return `pinokio_ffmpeg_remove_path() {
484
+ local target="$1"
485
+ local current="\${2-}"
486
+ local result=""
487
+ local part
488
+ local old_ifs="$IFS"
489
+ IFS=':'
490
+ for part in $current; do
491
+ [ -n "$part" ] || continue
492
+ [ "$part" = "$target" ] && continue
493
+ if [ -n "$result" ]; then
494
+ result="$result:$part"
495
+ else
496
+ result="$part"
497
+ fi
498
+ done
499
+ IFS="$old_ifs"
500
+ printf '%s' "$result"
501
+ }
502
+ if [ -n "\${PINOKIO_FFMPEG_BIN-}" ]; then
503
+ export PATH="$(pinokio_ffmpeg_remove_path "$PINOKIO_FFMPEG_BIN" "$PATH")"
504
+ fi
505
+ ${forceWindowsPaths ? "" : `if [ "$(uname -s)" = "Linux" ] && [ -n "\${PINOKIO_FFMPEG_PREFIX-}" ]; then
506
+ export LD_LIBRARY_PATH="$(pinokio_ffmpeg_remove_path "${this.libraryDir()}" "\${LD_LIBRARY_PATH-}")"
507
+ fi
508
+ `}
509
+ unset FFMPEG_PATH
510
+ unset FFPROBE_PATH
511
+ unset PINOKIO_FFMPEG_PREFIX
512
+ unset PINOKIO_FFMPEG_BIN
513
+ unset -f pinokio_ffmpeg_remove_path
514
+ `
174
515
  }
175
516
 
176
517
  async cleanupLegacyStandalone(ondata) {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.2.4",
3
+ "version": "7.2.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "download_readme": "wget -O kernel/proto/PINOKIO.md https://raw.githubusercontent.com/pinokiocomputer/home/refs/heads/main/docs/README.md",
8
- "start": "node script/index"
8
+ "start": "node script/index",
9
+ "verify:ffmpeg": "node script/verify-ffmpeg.js"
9
10
  },
10
11
  "author": "",
11
12
  "license": "MIT",
@@ -0,0 +1,459 @@
1
+ #!/usr/bin/env node
2
+
3
+ const crypto = require("crypto")
4
+ const fs = require("fs")
5
+ const os = require("os")
6
+ const path = require("path")
7
+ const { spawn } = require("child_process")
8
+ const Kernel = require("../kernel")
9
+
10
+ function parseArgs(argv) {
11
+ const options = {
12
+ home: process.env.PINOKIO_HOME || path.resolve(process.cwd(), ".pinokio"),
13
+ reinstall: false,
14
+ skipInstall: false,
15
+ }
16
+
17
+ for (let i = 0; i < argv.length; i += 1) {
18
+ const arg = argv[i]
19
+ if (arg === "--home" && argv[i + 1]) {
20
+ options.home = path.resolve(argv[i + 1])
21
+ i += 1
22
+ } else if (arg === "--reinstall") {
23
+ options.reinstall = true
24
+ } else if (arg === "--skip-install") {
25
+ options.skipInstall = true
26
+ } else if (arg === "--help" || arg === "-h") {
27
+ options.help = true
28
+ } else {
29
+ throw new Error(`Unknown argument: ${arg}`)
30
+ }
31
+ }
32
+
33
+ return options
34
+ }
35
+
36
+ function usage() {
37
+ return [
38
+ "Usage: node script/verify-ffmpeg.js [--home <PINOKIO_HOME>] [--reinstall] [--skip-install]",
39
+ "",
40
+ "--reinstall remove and reinstall FFmpeg before verification",
41
+ "--skip-install only verify the current install state",
42
+ ].join("\n")
43
+ }
44
+
45
+ function logOnData(event) {
46
+ if (!event) {
47
+ return
48
+ }
49
+ if (typeof event.raw === "string") {
50
+ process.stdout.write(event.raw)
51
+ return
52
+ }
53
+ if (typeof event.html === "string") {
54
+ process.stdout.write(`${event.html.replace(/<[^>]+>/g, "")}\n`)
55
+ }
56
+ }
57
+
58
+ function mergeEnv(baseEnv, overlay) {
59
+ const env = { ...baseEnv }
60
+ for (const [key, value] of Object.entries(overlay || {})) {
61
+ if (Array.isArray(value)) {
62
+ const prefix = value.filter(Boolean).join(path.delimiter)
63
+ if (prefix.length === 0) {
64
+ continue
65
+ }
66
+ env[key] = env[key] ? `${prefix}${path.delimiter}${env[key]}` : prefix
67
+ } else if (value === undefined || value === null) {
68
+ delete env[key]
69
+ } else {
70
+ env[key] = String(value)
71
+ }
72
+ }
73
+ return env
74
+ }
75
+
76
+ function normalizePathForCompare(value) {
77
+ return String(value || "")
78
+ .replace(/\\/g, "/")
79
+ .replace(/\/+$/, "")
80
+ .toLowerCase()
81
+ }
82
+
83
+ function assert(condition, message) {
84
+ if (!condition) {
85
+ throw new Error(message)
86
+ }
87
+ }
88
+
89
+ async function exists(target) {
90
+ try {
91
+ await fs.promises.access(target)
92
+ return true
93
+ } catch (error) {
94
+ return false
95
+ }
96
+ }
97
+
98
+ async function runCommand(command, args, options = {}) {
99
+ return await new Promise((resolve, reject) => {
100
+ const child = spawn(command, args, {
101
+ cwd: options.cwd,
102
+ env: options.env,
103
+ windowsHide: true,
104
+ shell: false,
105
+ })
106
+
107
+ let stdout = ""
108
+ let stderr = ""
109
+ child.stdout.on("data", (chunk) => {
110
+ const text = chunk.toString()
111
+ stdout += text
112
+ if (options.stream) {
113
+ process.stdout.write(text)
114
+ }
115
+ })
116
+ child.stderr.on("data", (chunk) => {
117
+ const text = chunk.toString()
118
+ stderr += text
119
+ if (options.stream) {
120
+ process.stderr.write(text)
121
+ }
122
+ })
123
+ child.on("error", reject)
124
+ child.on("close", (code) => {
125
+ if (code === 0) {
126
+ resolve({ stdout, stderr, code })
127
+ } else {
128
+ reject(new Error(`Command failed (${code}): ${command} ${args.join(" ")}\n${stdout}${stderr}`))
129
+ }
130
+ })
131
+ })
132
+ }
133
+
134
+ function prefixedValue(output, prefix) {
135
+ const line = String(output || "").split(/\r?\n/).find((entry) => entry.startsWith(prefix))
136
+ if (!line) {
137
+ return ""
138
+ }
139
+ return line.slice(prefix.length)
140
+ }
141
+
142
+ function sectionValues(output, beginMarker, endMarker) {
143
+ const lines = String(output || "").split(/\r?\n/)
144
+ const begin = lines.findIndex((line) => line.trim() === beginMarker)
145
+ const end = lines.findIndex((line, index) => index > begin && line.trim() === endMarker)
146
+ if (begin === -1 || end === -1 || end <= begin) {
147
+ return []
148
+ }
149
+ return lines.slice(begin + 1, end).map((line) => line.trim()).filter(Boolean)
150
+ }
151
+
152
+ async function verifyHookFiles(ffmpeg) {
153
+ const hookFiles = ffmpeg.activationHookFiles()
154
+ for (const file of hookFiles) {
155
+ assert(await exists(file.path), `Missing activation hook: ${file.path}`)
156
+ }
157
+ }
158
+
159
+ async function verifyWindowsPatchedCache(ffmpeg) {
160
+ if (ffmpeg.kernel.platform !== "win32") {
161
+ return
162
+ }
163
+
164
+ const pkgsDir = ffmpeg.ffmpegPkgsDir()
165
+ const entries = await fs.promises.readdir(pkgsDir, { withFileTypes: true })
166
+ const packageDirs = entries
167
+ .filter((entry) => entry.isDirectory() && /^gdk-pixbuf-/.test(entry.name))
168
+ .map((entry) => path.resolve(pkgsDir, entry.name))
169
+
170
+ assert(packageDirs.length > 0, `No gdk-pixbuf package found under ${pkgsDir}`)
171
+
172
+ for (const packageDir of packageDirs) {
173
+ const scriptPath = path.resolve(packageDir, "Scripts", ".gdk-pixbuf-post-link.bat")
174
+ assert(await exists(scriptPath), `Missing patched gdk-pixbuf script: ${scriptPath}`)
175
+
176
+ const contents = await fs.promises.readFile(scriptPath)
177
+ assert(
178
+ contents.toString("utf8").includes("Pinokio intentionally skips gdk-pixbuf loader cache generation"),
179
+ `Unexpected gdk-pixbuf post-link contents in ${scriptPath}`
180
+ )
181
+
182
+ const pathsJsonPath = path.resolve(packageDir, "info", "paths.json")
183
+ const pathsJson = JSON.parse(await fs.promises.readFile(pathsJsonPath, "utf8"))
184
+ const entry = Array.isArray(pathsJson.paths)
185
+ ? pathsJson.paths.find((item) => item && item._path === "Scripts/.gdk-pixbuf-post-link.bat")
186
+ : null
187
+
188
+ assert(entry, `Missing paths.json entry for patched gdk-pixbuf script in ${pathsJsonPath}`)
189
+ assert(entry.size_in_bytes === contents.length, `paths.json size mismatch for ${scriptPath}`)
190
+
191
+ const sha256 = crypto.createHash("sha256").update(contents).digest("hex")
192
+ assert(entry.sha256 === sha256, `paths.json sha256 mismatch for ${scriptPath}`)
193
+ }
194
+ }
195
+
196
+ async function verifyPosixRuntime(ffmpeg, condaEnv) {
197
+ const shell = "/bin/bash"
198
+ const env = mergeEnv(process.env, condaEnv)
199
+ const expectedFfmpeg = ffmpeg.binaryPath("ffmpeg")
200
+ const expectedFfprobe = ffmpeg.binaryPath("ffprobe")
201
+ const expectedBinDir = path.dirname(expectedFfmpeg)
202
+ const expectedLibDir = ffmpeg.libraryDir()
203
+
204
+ const command = [
205
+ "set -e",
206
+ 'eval "$(conda shell.bash hook)"',
207
+ "conda deactivate || true",
208
+ "conda deactivate || true",
209
+ "conda deactivate || true",
210
+ "conda activate base",
211
+ 'printf "__FFMPEG__%s\\n" "$(command -v ffmpeg)"',
212
+ 'printf "__FFPROBE__%s\\n" "$(command -v ffprobe)"',
213
+ 'printf "__FFMPEG_PATH__%s\\n" "${FFMPEG_PATH-}"',
214
+ 'printf "__FFPROBE_PATH__%s\\n" "${FFPROBE_PATH-}"',
215
+ 'printf "__PATH__%s\\n" "$PATH"',
216
+ ffmpeg.kernel.platform === "linux"
217
+ ? 'printf "__LD_LIBRARY_PATH__%s\\n" "${LD_LIBRARY_PATH-}"'
218
+ : 'printf "__LD_LIBRARY_PATH__%s\\n" "${LD_LIBRARY_PATH-}"',
219
+ "ffmpeg -hide_banner -version | head -n 1",
220
+ "ffprobe -hide_banner -version | head -n 1",
221
+ "ffmpeg -hide_banner -encoders | grep -q libmp3lame",
222
+ ].join(" && ")
223
+
224
+ const { stdout } = await runCommand(shell, ["-lc", command], { env, stream: true })
225
+ const ffmpegResolved = prefixedValue(stdout, "__FFMPEG__")
226
+ const ffprobeResolved = prefixedValue(stdout, "__FFPROBE__")
227
+ const ffmpegPathEnv = prefixedValue(stdout, "__FFMPEG_PATH__")
228
+ const ffprobePathEnv = prefixedValue(stdout, "__FFPROBE_PATH__")
229
+ const shellPath = prefixedValue(stdout, "__PATH__")
230
+ const ldLibraryPath = prefixedValue(stdout, "__LD_LIBRARY_PATH__")
231
+
232
+ assert(
233
+ normalizePathForCompare(ffmpegResolved) === normalizePathForCompare(expectedFfmpeg),
234
+ `bash resolved ffmpeg to ${ffmpegResolved}, expected ${expectedFfmpeg}`
235
+ )
236
+ assert(
237
+ normalizePathForCompare(ffprobeResolved) === normalizePathForCompare(expectedFfprobe),
238
+ `bash resolved ffprobe to ${ffprobeResolved}, expected ${expectedFfprobe}`
239
+ )
240
+ assert(
241
+ normalizePathForCompare(ffmpegPathEnv) === normalizePathForCompare(expectedFfmpeg),
242
+ `FFMPEG_PATH was ${ffmpegPathEnv}, expected ${expectedFfmpeg}`
243
+ )
244
+ assert(
245
+ normalizePathForCompare(ffprobePathEnv) === normalizePathForCompare(expectedFfprobe),
246
+ `FFPROBE_PATH was ${ffprobePathEnv}, expected ${expectedFfprobe}`
247
+ )
248
+ assert(
249
+ shellPath.split(":").map((entry) => normalizePathForCompare(entry))[0] === normalizePathForCompare(expectedBinDir),
250
+ `PATH does not start with FFmpeg bin dir: ${expectedBinDir}`
251
+ )
252
+ if (ffmpeg.kernel.platform === "linux") {
253
+ assert(
254
+ ldLibraryPath.split(":").map((entry) => normalizePathForCompare(entry))[0] === normalizePathForCompare(expectedLibDir),
255
+ `LD_LIBRARY_PATH does not start with FFmpeg lib dir: ${expectedLibDir}`
256
+ )
257
+ }
258
+ }
259
+
260
+ async function verifyWindowsCmdRuntime(ffmpeg, condaEnv) {
261
+ const env = mergeEnv(process.env, condaEnv)
262
+ const expectedFfmpeg = ffmpeg.binaryPath("ffmpeg")
263
+ const expectedFfprobe = ffmpeg.binaryPath("ffprobe")
264
+ const expectedRuntimeDir = ffmpeg.libraryDir()
265
+
266
+ const command = [
267
+ "conda_hook",
268
+ "conda deactivate",
269
+ "conda deactivate",
270
+ "conda deactivate",
271
+ [
272
+ "conda activate base",
273
+ "echo __FFMPEG_PATH__%FFMPEG_PATH%",
274
+ "echo __FFPROBE_PATH__%FFPROBE_PATH%",
275
+ "echo __PATH__%PATH%",
276
+ "echo __FFMPEG_BEGIN__",
277
+ "where ffmpeg",
278
+ "echo __FFMPEG_END__",
279
+ "echo __FFPROBE_BEGIN__",
280
+ "where ffprobe",
281
+ "echo __FFPROBE_END__",
282
+ "ffmpeg -hide_banner -version",
283
+ "ffprobe -hide_banner -version",
284
+ 'ffmpeg -hide_banner -encoders | findstr /C:"libmp3lame"',
285
+ ].join(" && "),
286
+ ].join(" & ")
287
+
288
+ const shell = process.env.ComSpec || "cmd.exe"
289
+ const { stdout } = await runCommand(shell, ["/d", "/s", "/c", command], { env, stream: true })
290
+
291
+ const ffmpegPathEnv = prefixedValue(stdout, "__FFMPEG_PATH__")
292
+ const ffprobePathEnv = prefixedValue(stdout, "__FFPROBE_PATH__")
293
+ const shellPath = prefixedValue(stdout, "__PATH__")
294
+ const ffmpegResolved = sectionValues(stdout, "__FFMPEG_BEGIN__", "__FFMPEG_END__")[0]
295
+ const ffprobeResolved = sectionValues(stdout, "__FFPROBE_BEGIN__", "__FFPROBE_END__")[0]
296
+
297
+ assert(
298
+ normalizePathForCompare(ffmpegResolved) === normalizePathForCompare(expectedFfmpeg),
299
+ `cmd resolved ffmpeg to ${ffmpegResolved}, expected ${expectedFfmpeg}`
300
+ )
301
+ assert(
302
+ normalizePathForCompare(ffprobeResolved) === normalizePathForCompare(expectedFfprobe),
303
+ `cmd resolved ffprobe to ${ffprobeResolved}, expected ${expectedFfprobe}`
304
+ )
305
+ assert(
306
+ normalizePathForCompare(ffmpegPathEnv) === normalizePathForCompare(expectedFfmpeg),
307
+ `FFMPEG_PATH was ${ffmpegPathEnv}, expected ${expectedFfmpeg}`
308
+ )
309
+ assert(
310
+ normalizePathForCompare(ffprobePathEnv) === normalizePathForCompare(expectedFfprobe),
311
+ `FFPROBE_PATH was ${ffprobePathEnv}, expected ${expectedFfprobe}`
312
+ )
313
+ assert(
314
+ shellPath.split(";").map((entry) => normalizePathForCompare(entry))[0] === normalizePathForCompare(expectedRuntimeDir),
315
+ `PATH does not start with FFmpeg runtime dir: ${expectedRuntimeDir}`
316
+ )
317
+ }
318
+
319
+ async function verifyWindowsPowerShellRuntime(ffmpeg, condaEnv) {
320
+ const env = mergeEnv(process.env, condaEnv)
321
+ const expectedFfmpeg = ffmpeg.binaryPath("ffmpeg")
322
+ const expectedFfprobe = ffmpeg.binaryPath("ffprobe")
323
+ const expectedRuntimeDir = ffmpeg.libraryDir()
324
+
325
+ const script = [
326
+ "$ErrorActionPreference = 'Stop'",
327
+ "conda_hook",
328
+ "conda deactivate",
329
+ "conda deactivate",
330
+ "conda deactivate",
331
+ "conda activate base",
332
+ 'Write-Output ("__FFMPEG_PATH__" + $Env:FFMPEG_PATH)',
333
+ 'Write-Output ("__FFPROBE_PATH__" + $Env:FFPROBE_PATH)',
334
+ 'Write-Output ("__PATH__" + $Env:Path)',
335
+ 'Write-Output ("__FFMPEG__" + (Get-Command ffmpeg).Source)',
336
+ 'Write-Output ("__FFPROBE__" + (Get-Command ffprobe).Source)',
337
+ "ffmpeg -hide_banner -version | Select-Object -First 1",
338
+ "ffprobe -hide_banner -version | Select-Object -First 1",
339
+ "if (-not (ffmpeg -hide_banner -encoders | Select-String -SimpleMatch 'libmp3lame')) { exit 1 }",
340
+ ].join("; ")
341
+
342
+ const shell = process.env.SystemRoot
343
+ ? path.resolve(process.env.SystemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
344
+ : "powershell.exe"
345
+ const { stdout } = await runCommand(shell, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], { env, stream: true })
346
+
347
+ const ffmpegResolved = prefixedValue(stdout, "__FFMPEG__")
348
+ const ffprobeResolved = prefixedValue(stdout, "__FFPROBE__")
349
+ const ffmpegPathEnv = prefixedValue(stdout, "__FFMPEG_PATH__")
350
+ const ffprobePathEnv = prefixedValue(stdout, "__FFPROBE_PATH__")
351
+ const shellPath = prefixedValue(stdout, "__PATH__")
352
+
353
+ assert(
354
+ normalizePathForCompare(ffmpegResolved) === normalizePathForCompare(expectedFfmpeg),
355
+ `PowerShell resolved ffmpeg to ${ffmpegResolved}, expected ${expectedFfmpeg}`
356
+ )
357
+ assert(
358
+ normalizePathForCompare(ffprobeResolved) === normalizePathForCompare(expectedFfprobe),
359
+ `PowerShell resolved ffprobe to ${ffprobeResolved}, expected ${expectedFfprobe}`
360
+ )
361
+ assert(
362
+ normalizePathForCompare(ffmpegPathEnv) === normalizePathForCompare(expectedFfmpeg),
363
+ `PowerShell FFMPEG_PATH was ${ffmpegPathEnv}, expected ${expectedFfmpeg}`
364
+ )
365
+ assert(
366
+ normalizePathForCompare(ffprobePathEnv) === normalizePathForCompare(expectedFfprobe),
367
+ `PowerShell FFPROBE_PATH was ${ffprobePathEnv}, expected ${expectedFfprobe}`
368
+ )
369
+ assert(
370
+ shellPath.split(";").map((entry) => normalizePathForCompare(entry))[0] === normalizePathForCompare(expectedRuntimeDir),
371
+ `PowerShell PATH does not start with FFmpeg runtime dir: ${expectedRuntimeDir}`
372
+ )
373
+ }
374
+
375
+ async function main() {
376
+ const options = parseArgs(process.argv.slice(2))
377
+ if (options.help) {
378
+ console.log(usage())
379
+ return
380
+ }
381
+
382
+ process.env.PINOKIO_HOME = options.home
383
+ console.log(`[verify-ffmpeg] home=${options.home}`)
384
+ console.log(`[verify-ffmpeg] platform=${os.platform()} arch=${os.arch()}`)
385
+
386
+ const kernel = new Kernel({ store: {} })
387
+ await kernel.init({})
388
+ await kernel.shell.init()
389
+ await kernel.bin.init()
390
+ await kernel.bin.refreshInstalled()
391
+
392
+ if (kernel.refresh_interval) {
393
+ clearInterval(kernel.refresh_interval)
394
+ }
395
+ kernel.server_running = true
396
+
397
+ const conda = kernel.bin.mod("conda")
398
+ const ffmpeg = kernel.bin.mod("ffmpeg")
399
+
400
+ assert(conda, "Conda module was not initialized")
401
+ assert(ffmpeg, "FFmpeg module was not initialized")
402
+
403
+ if (!kernel.bin.installed.conda || !(await conda.installed())) {
404
+ console.log("[verify-ffmpeg] installing conda")
405
+ await kernel.bin.install({
406
+ params: [
407
+ {
408
+ name: "conda",
409
+ dependencies: [],
410
+ }
411
+ ]
412
+ }, logOnData)
413
+ await kernel.bin.refreshInstalled()
414
+ }
415
+
416
+ if (!options.skipInstall) {
417
+ const ffmpegInstalled = await ffmpeg.installed()
418
+ if (options.reinstall && ffmpegInstalled) {
419
+ console.log("[verify-ffmpeg] reinstall requested, removing existing ffmpeg")
420
+ await ffmpeg.uninstall({}, logOnData)
421
+ await kernel.bin.refreshInstalled()
422
+ }
423
+ if (options.reinstall || !(await ffmpeg.installed())) {
424
+ console.log("[verify-ffmpeg] installing ffmpeg")
425
+ await kernel.bin.install({
426
+ params: [
427
+ {
428
+ name: "ffmpeg",
429
+ }
430
+ ]
431
+ }, logOnData)
432
+ await kernel.bin.refreshInstalled()
433
+ }
434
+ }
435
+
436
+ assert(await ffmpeg.installed(), "FFmpeg module did not report installed after setup")
437
+ await ffmpeg.selfTest(logOnData)
438
+ await verifyHookFiles(ffmpeg)
439
+ await verifyWindowsPatchedCache(ffmpeg)
440
+
441
+ const condaEnv = conda.env()
442
+ if (kernel.platform === "win32") {
443
+ console.log("[verify-ffmpeg] verifying cmd.exe runtime")
444
+ await verifyWindowsCmdRuntime(ffmpeg, condaEnv)
445
+ console.log("[verify-ffmpeg] verifying PowerShell runtime")
446
+ await verifyWindowsPowerShellRuntime(ffmpeg, condaEnv)
447
+ } else {
448
+ console.log("[verify-ffmpeg] verifying bash runtime")
449
+ await verifyPosixRuntime(ffmpeg, condaEnv)
450
+ }
451
+
452
+ console.log("[verify-ffmpeg] all checks passed")
453
+ }
454
+
455
+ main().catch((error) => {
456
+ console.error("[verify-ffmpeg] failed")
457
+ console.error(error && error.stack ? error.stack : error)
458
+ process.exit(1)
459
+ })