pinokiod 7.3.15 → 7.4.1

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 (52) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/bin/ffmpeg.js +9 -791
  5. package/kernel/environment.js +89 -1
  6. package/kernel/git.js +9 -19
  7. package/kernel/index.js +142 -43
  8. package/kernel/launch_requirements.js +1115 -0
  9. package/kernel/procs.js +1 -1
  10. package/kernel/ready.js +231 -0
  11. package/kernel/script.js +16 -0
  12. package/kernel/shells.js +9 -1
  13. package/kernel/workspace_status.js +111 -45
  14. package/package.json +2 -3
  15. package/server/autolaunch.js +725 -0
  16. package/server/index.js +244 -411
  17. package/server/views/app.ejs +267 -160
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  21. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  22. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  23. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  24. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  25. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  26. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  27. package/server/views/partials/home_action_modal.ejs +4 -1
  28. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  29. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  30. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  31. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  32. package/server/views/terminal.ejs +196 -2
  33. package/test/home-autolaunch-live-ui.test.js +455 -0
  34. package/test/launch-requirements-browser.test.js +579 -0
  35. package/test/launch-requirements-contract-coverage.test.js +627 -0
  36. package/test/launch-requirements-real-browser.js +625 -0
  37. package/test/launch-requirements-status-client.test.js +132 -0
  38. package/test/launch-requirements.test.js +1806 -0
  39. package/test/launch-settings-ui.test.js +370 -0
  40. package/test/procs.test.js +49 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/script/verify-ffmpeg.js +0 -459
  52. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -1,21 +1,5 @@
1
- const crypto = require("crypto")
2
- const fs = require("fs")
3
- const path = require("path")
4
- const { execFile } = require("child_process")
5
- const ParcelWatcher = require("@parcel/watcher")
6
- const semver = require("semver")
7
- const { rimraf } = require("rimraf")
8
- const Util = require("../util")
9
-
10
- const RELEASE_VERSION = "8.0.1"
11
- const RELEASE_RANGE = ">=8.0.1 <8.1.0"
1
+ const RELEASE_VERSION = "8.1.2"
12
2
  const CONDA_SPEC = `ffmpeg=${RELEASE_VERSION}`
13
- const CONDA_CHANNEL_FLAGS = "--override-channels -c conda-forge"
14
-
15
- const WINDOWS_GDK_PIXBUF_POST_LINK_NOOP = `@echo off
16
- rem Pinokio intentionally skips gdk-pixbuf loader cache generation for FFmpeg installs.
17
- exit /b 0
18
- `
19
3
 
20
4
  class Ffmpeg {
21
5
  description = "Installs FFmpeg for audio and video processing."
@@ -24,792 +8,26 @@ class Ffmpeg {
24
8
  return CONDA_SPEC
25
9
  }
26
10
 
27
- env(kernel) {
28
- const activeKernel = kernel || this.kernel
29
- const env = {
30
- FFMPEG_PATH: this.binaryPath("ffmpeg", activeKernel),
31
- FFPROBE_PATH: this.binaryPath("ffprobe", activeKernel)
32
- }
33
- if (activeKernel.platform === "win32") {
34
- env.PATH = [this.libraryDir(activeKernel)]
35
- }
36
- if (activeKernel.platform === "linux") {
37
- env.LD_LIBRARY_PATH = [this.libraryDir(activeKernel)]
38
- }
39
- return env
40
- }
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
-
50
- binaryPath(tool, kernel = this.kernel) {
51
- const filename = kernel.platform === "win32" ? `${tool}.exe` : tool
52
- if (kernel.platform === "win32") {
53
- return path.resolve(this.ffmpegPrefix(kernel), "Library", "bin", filename)
54
- }
55
- return path.resolve(this.ffmpegPrefix(kernel), "bin", filename)
56
- }
57
-
58
- libraryDir(kernel = this.kernel) {
59
- if (kernel.platform === "win32") {
60
- return path.resolve(this.ffmpegPrefix(kernel), "Library", "bin")
61
- }
62
- return path.resolve(this.ffmpegPrefix(kernel), "lib")
63
- }
64
-
65
- legacyStandalonePaths() {
66
- return [
67
- this.kernel.bin.path("ffmpeg"),
68
- this.kernel.bin.path("ffmpeg-tmp")
69
- ]
70
- }
71
-
72
- async start() {
73
- if (this.kernel.platform !== "darwin") {
74
- return
75
- }
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)
88
- }
89
- }
90
-
91
11
  async install(req, ondata) {
92
- await this.cleanupLegacyStandalone(ondata)
93
- if (this.kernel.platform === "win32") {
94
- await this.installWindows(ondata)
95
- } else {
96
- await this.installStandard(ondata)
97
- }
98
- await this.selfTest(ondata)
99
- await this.ensureBaseActivationHooks()
100
- await this.syncMacUvLibraryShims(ondata)
101
- }
102
-
103
- async installStandard(ondata) {
104
- await this.resetInstallPrefix()
105
12
  await this.kernel.bin.exec({
106
- env: {
107
- CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
108
- },
109
13
  message: [
110
14
  "conda clean -y --all",
111
- `conda create -y -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
15
+ `conda install -y -c conda-forge ${this.cmd()}`
112
16
  ]
113
17
  }, ondata)
114
18
  }
115
19
 
116
- async installWindows(ondata) {
117
- await this.resetInstallPrefix()
118
- const env = {
119
- CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
120
- }
121
-
122
- await this.kernel.bin.exec({
123
- env,
124
- message: [
125
- "conda clean -y --all",
126
- `conda create -y --download-only -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
127
- ]
128
- }, ondata)
129
-
130
- await this.patchWindowsGdkPixbufPostLink(this.ffmpegPkgsDir(), ondata)
131
-
132
- await this.kernel.bin.exec({
133
- env,
134
- message: `conda create -y --offline -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
135
- }, ondata)
136
- }
137
-
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) {
145
- const entries = await fs.promises.readdir(pkgsDir, { withFileTypes: true })
146
- const packageDirs = entries
147
- .filter((entry) => entry.isDirectory() && /^gdk-pixbuf-/.test(entry.name))
148
- .map((entry) => path.resolve(pkgsDir, entry.name))
149
-
150
- if (packageDirs.length === 0) {
151
- throw new Error("Could not find downloaded gdk-pixbuf package in the Conda cache after --download-only")
152
- }
153
-
154
- let patchedCount = 0
155
- let metadataCount = 0
156
- for (const packageDir of packageDirs) {
157
- const scripts = [
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
- }
166
- ]
167
-
168
- for (const { relativePath, metadataRequired } of scripts) {
169
- const script = path.resolve(packageDir, ...relativePath.split("/"))
170
- try {
171
- await fs.promises.access(script)
172
- await fs.promises.writeFile(script, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
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
- }
180
- } catch (error) {
181
- if (error && error.code !== "ENOENT") {
182
- throw error
183
- }
184
- }
185
- }
186
- }
187
-
188
- if (patchedCount === 0) {
189
- throw new Error("Found gdk-pixbuf in the Conda cache, but did not find any post-link scripts to patch")
190
- }
191
-
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
- }
197
- }
198
-
199
- async updateCondaPathsJson(packageDir, relativePath, contents) {
200
- const pathsJsonPath = path.resolve(packageDir, "info", "paths.json")
201
- let pathsJson
202
-
203
- try {
204
- pathsJson = JSON.parse(await fs.promises.readFile(pathsJsonPath, "utf8"))
205
- } catch (error) {
206
- if (error && error.code === "ENOENT") {
207
- return false
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
- }
229
-
230
- async hasInstalledBinaryPaths() {
231
- try {
232
- await fs.promises.access(this.binaryPath("ffmpeg"))
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
20
  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()
255
- await this.syncMacUvLibraryShims()
256
- await this.startMacUvLibraryWatcher()
257
- return true
258
- } catch (error) {
259
- await this.removeRuntimeExposure()
260
- console.log("conda ffmpeg installed check failed", error && error.message ? error.message : error)
261
- return false
262
- }
21
+ return !!(
22
+ this.kernel.bin.installed?.conda?.has("ffmpeg") &&
23
+ this.kernel.bin.installed?.conda_versions?.ffmpeg === RELEASE_VERSION
24
+ )
263
25
  }
264
26
 
265
27
  async uninstall(req, 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())
283
- await this.cleanupLegacyStandalone(ondata)
284
- }
285
-
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(() => {})
342
- }
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
- `
515
- }
516
-
517
- async cleanupLegacyStandalone(ondata) {
518
- for (const target of this.legacyStandalonePaths()) {
519
- const exists = await fs.promises.access(target).then(() => true).catch(() => false)
520
- if (exists) {
521
- if (ondata) {
522
- ondata({ raw: `removing legacy standalone ffmpeg files from ${target}...\r\n` })
523
- }
524
- await rimraf(target)
525
- }
526
- }
527
- }
528
-
529
- uvPythonRoot(kernel = this.kernel) {
530
- return kernel.path("cache", "XDG_DATA_HOME", "uv", "python")
531
- }
532
-
533
- async startMacUvLibraryWatcher() {
534
- if (this.kernel.platform !== "darwin" || this.macUvLibraryWatcher) {
535
- return
536
- }
537
-
538
- const root = this.uvPythonRoot()
539
- await fs.promises.mkdir(root, { recursive: true })
540
- this.macUvLibraryWatcher = await ParcelWatcher.subscribe(root, (error, events) => {
541
- if (error) {
542
- console.warn("ffmpeg uv library watcher error", error && error.message ? error.message : error)
543
- return
544
- }
545
- if (!events || events.length === 0) {
546
- return
547
- }
548
- this.scheduleMacUvLibraryShimSync()
549
- })
550
- }
551
-
552
- async stopMacUvLibraryWatcher() {
553
- if (this.macUvLibraryShimSyncTimer) {
554
- clearTimeout(this.macUvLibraryShimSyncTimer)
555
- this.macUvLibraryShimSyncTimer = null
556
- }
557
- if (this.macUvLibraryWatcher) {
558
- await this.macUvLibraryWatcher.unsubscribe()
559
- this.macUvLibraryWatcher = null
560
- }
561
- }
562
-
563
- scheduleMacUvLibraryShimSync() {
564
- if (this.macUvLibraryShimSyncTimer) {
565
- clearTimeout(this.macUvLibraryShimSyncTimer)
566
- }
567
- this.macUvLibraryShimSyncTimer = setTimeout(async () => {
568
- this.macUvLibraryShimSyncTimer = null
569
- try {
570
- await this.syncMacUvLibraryShims()
571
- } catch (error) {
572
- console.warn("ffmpeg uv library shim sync error", error && error.message ? error.message : error)
573
- }
574
- }, 250)
575
- }
576
-
577
- async uvLibraryDirs(kernel = this.kernel) {
578
- if (kernel.platform !== "darwin") {
579
- return []
580
- }
581
-
582
- const root = this.uvPythonRoot(kernel)
583
- const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
584
- const dirs = []
585
-
586
- for (const entry of entries) {
587
- if (!entry.isDirectory()) {
588
- continue
589
- }
590
- const libDir = path.resolve(root, entry.name, "lib")
591
- const exists = await fs.promises.access(libDir).then(() => true).catch(() => false)
592
- if (exists) {
593
- dirs.push(libDir)
594
- }
595
- }
596
-
597
- return dirs
598
- }
599
-
600
- async ffmpegLibraryFiles(kernel = this.kernel) {
601
- if (kernel.platform !== "darwin") {
602
- return []
603
- }
604
-
605
- const dir = this.libraryDir(kernel)
606
- const entries = await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => [])
607
- return entries
608
- .filter((entry) => entry.isFile() || entry.isSymbolicLink())
609
- .map((entry) => entry.name)
610
- .filter((name) => /^lib(?:av|sw)[^.]+(?:\.\d+)*\.dylib$/i.test(name))
611
- .sort()
612
- }
613
-
614
- async syncMacUvLibraryShims(ondata) {
615
- if (this.kernel.platform !== "darwin") {
616
- return
617
- }
618
-
619
- const [libraryDirs, libraryFiles] = await Promise.all([
620
- this.uvLibraryDirs(),
621
- this.ffmpegLibraryFiles()
622
- ])
623
-
624
- if (libraryDirs.length === 0 || libraryFiles.length === 0) {
625
- return
626
- }
627
-
628
- let createdCount = 0
629
- let refreshedCount = 0
630
- const sourceDir = this.libraryDir()
631
-
632
- for (const libDir of libraryDirs) {
633
- for (const filename of libraryFiles) {
634
- const source = path.resolve(sourceDir, filename)
635
- const target = path.resolve(libDir, filename)
636
- const desiredLink = path.relative(libDir, source)
637
-
638
- let stat
639
- try {
640
- stat = await fs.promises.lstat(target)
641
- } catch (error) {
642
- if (!error || error.code !== "ENOENT") {
643
- throw error
644
- }
645
- }
646
-
647
- if (!stat) {
648
- await fs.promises.symlink(desiredLink, target)
649
- createdCount += 1
650
- continue
651
- }
652
-
653
- if (!stat.isSymbolicLink()) {
654
- continue
655
- }
656
-
657
- const currentLink = await fs.promises.readlink(target)
658
- if (currentLink === desiredLink) {
659
- continue
660
- }
661
-
662
- await fs.promises.unlink(target)
663
- await fs.promises.symlink(desiredLink, target)
664
- refreshedCount += 1
665
- }
666
- }
667
-
668
- if (ondata && (createdCount > 0 || refreshedCount > 0)) {
669
- ondata({
670
- raw: `synced ${createdCount + refreshedCount} FFmpeg dylib shim(s) into uv Python runtime libraries...\r\n`
671
- })
672
- }
673
- }
674
-
675
- async removeMacUvLibraryShims(ondata) {
676
- if (this.kernel.platform !== "darwin") {
677
- return
678
- }
679
-
680
- const [libraryDirs, libraryFiles] = await Promise.all([
681
- this.uvLibraryDirs(),
682
- this.ffmpegLibraryFiles()
683
- ])
684
-
685
- if (libraryDirs.length === 0 || libraryFiles.length === 0) {
686
- return
687
- }
688
-
689
- const sourceDir = this.libraryDir()
690
- let removedCount = 0
691
-
692
- for (const libDir of libraryDirs) {
693
- for (const filename of libraryFiles) {
694
- const target = path.resolve(libDir, filename)
695
-
696
- let stat
697
- try {
698
- stat = await fs.promises.lstat(target)
699
- } catch (error) {
700
- if (!error || error.code !== "ENOENT") {
701
- throw error
702
- }
703
- }
704
-
705
- if (!stat || !stat.isSymbolicLink()) {
706
- continue
707
- }
708
-
709
- const currentLink = await fs.promises.readlink(target)
710
- const resolved = path.resolve(libDir, currentLink)
711
- if (path.dirname(resolved) !== sourceDir) {
712
- continue
713
- }
714
-
715
- await fs.promises.unlink(target)
716
- removedCount += 1
717
- }
718
- }
719
-
720
- if (ondata && removedCount > 0) {
721
- ondata({ raw: `removed ${removedCount} FFmpeg dylib shim(s) from uv Python runtime libraries...\r\n` })
722
- }
723
- }
724
-
725
- async selfTest(ondata) {
726
- if (ondata) {
727
- ondata({ raw: "verifying ffmpeg installation...\r\n" })
728
- }
729
-
730
- const ffmpegVersionOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-version"])
731
- const ffmpegVersion = semver.coerce(ffmpegVersionOutput)
732
- if (!ffmpegVersion || !semver.satisfies(ffmpegVersion, RELEASE_RANGE)) {
733
- throw new Error(`Unexpected ffmpeg version: ${this.firstLine(ffmpegVersionOutput)}`)
734
- }
735
-
736
- const encoderOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-hide_banner", "-encoders"])
737
- if (!/\blibmp3lame\b/i.test(encoderOutput)) {
738
- throw new Error("FFmpeg was installed without libmp3lame support")
739
- }
740
-
741
- const ffprobeVersionOutput = await this.execBinary(this.binaryPath("ffprobe"), ["-version"])
742
- const ffprobeVersion = semver.coerce(ffprobeVersionOutput)
743
- if (!ffprobeVersion || !semver.satisfies(ffprobeVersion, RELEASE_RANGE)) {
744
- throw new Error(`Unexpected ffprobe version: ${this.firstLine(ffprobeVersionOutput)}`)
745
- }
746
-
747
- await this.assertSharedLibraries()
748
- return true
749
- }
750
-
751
- async assertSharedLibraries() {
752
- const dir = this.libraryDir()
753
- const entries = await fs.promises.readdir(dir).catch(() => {
754
- throw new Error(`Missing FFmpeg library directory: ${dir}`)
755
- })
756
-
757
- const patterns = this.sharedLibraryPatterns()
758
- for (const pattern of patterns) {
759
- if (!entries.some((name) => pattern.test(name))) {
760
- throw new Error(`Missing FFmpeg shared library matching ${pattern}`)
761
- }
762
- }
763
- }
764
-
765
- sharedLibraryPatterns() {
766
- if (this.kernel.platform === "win32") {
767
- return [
768
- /^avcodec-\d+\.dll$/i,
769
- /^avformat-\d+\.dll$/i,
770
- /^avutil-\d+\.dll$/i,
771
- /^swresample-\d+\.dll$/i,
772
- /^swscale-\d+\.dll$/i
773
- ]
774
- }
775
-
776
- if (this.kernel.platform === "darwin") {
777
- return [
778
- /^libavcodec(\.\d+)*\.dylib$/i,
779
- /^libavformat(\.\d+)*\.dylib$/i,
780
- /^libavutil(\.\d+)*\.dylib$/i,
781
- /^libswresample(\.\d+)*\.dylib$/i,
782
- /^libswscale(\.\d+)*\.dylib$/i
783
- ]
784
- }
785
-
786
- return [
787
- /^libavcodec\.so(\.\d+)*$/i,
788
- /^libavformat\.so(\.\d+)*$/i,
789
- /^libavutil\.so(\.\d+)*$/i,
790
- /^libswresample\.so(\.\d+)*$/i,
791
- /^libswscale\.so(\.\d+)*$/i
792
- ]
793
- }
794
-
795
- execBinary(file, args) {
796
- return new Promise((resolve, reject) => {
797
- execFile(file, args, {
798
- windowsHide: true,
799
- maxBuffer: 32 * 1024 * 1024
800
- }, (error, stdout, stderr) => {
801
- const output = `${stdout || ""}${stderr || ""}`
802
- if (error) {
803
- reject(new Error(output || error.message))
804
- return
805
- }
806
- resolve(output)
807
- })
808
- })
809
- }
810
-
811
- firstLine(output) {
812
- return String(output || "").split(/\r?\n/).find(Boolean) || ""
28
+ await this.kernel.bin.exec({
29
+ message: "conda remove ffmpeg",
30
+ }, ondata)
813
31
  }
814
32
  }
815
33