pinokiod 7.2.1 → 7.2.5

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/kernel/bin/ffmpeg.js +693 -224
  2. package/package.json +1 -1
@@ -2,235 +2,718 @@ const crypto = require("crypto")
2
2
  const fs = require("fs")
3
3
  const path = require("path")
4
4
  const { execFile } = require("child_process")
5
+ const ParcelWatcher = require("@parcel/watcher")
5
6
  const semver = require("semver")
6
7
  const { rimraf } = require("rimraf")
7
- const decompress = require("decompress")
8
-
9
- const RELEASE_VERSION = "8.1"
10
- const RELEASE_RANGE = ">=8.1.0 <8.2.0"
11
-
12
- const ARTIFACTS = {
13
- win32: {
14
- x64: {
15
- version: RELEASE_VERSION,
16
- source: "Gyan Windows essentials build",
17
- archives: [{
18
- url: "https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-8.1-essentials_build.zip",
19
- sha256: "8748283d821613d930b0e7be685aaa9df4ca6f0ad4d0c42fd02622b3623463c6",
20
- binaries: {
21
- ffmpeg: "ffmpeg.exe",
22
- ffprobe: "ffprobe.exe"
23
- }
24
- }]
25
- }
26
- },
27
- darwin: {
28
- x64: {
29
- version: RELEASE_VERSION,
30
- source: "Martin Riedl macOS amd64 release",
31
- archives: [{
32
- url: "https://ffmpeg.martin-riedl.de/download/macos/amd64/1774556648_8.1/ffmpeg.zip",
33
- sha256: "eaa8aa619f8eccc7f548a730097f5d299cbf2d418888421c137557344d821130",
34
- binaries: {
35
- ffmpeg: "ffmpeg"
36
- }
37
- }, {
38
- url: "https://ffmpeg.martin-riedl.de/download/macos/amd64/1774556648_8.1/ffprobe.zip",
39
- sha256: "221bd0716dc15daf5745c5503773e5c23264c10c5ea956aa17ef492bbc0b0ac7",
40
- binaries: {
41
- ffprobe: "ffprobe"
42
- }
43
- }]
44
- },
45
- arm64: {
46
- version: RELEASE_VERSION,
47
- source: "Martin Riedl macOS arm64 release",
48
- archives: [{
49
- url: "https://ffmpeg.martin-riedl.de/download/macos/arm64/1774549676_8.1/ffmpeg.zip",
50
- sha256: "cc3a7e0cce36c5eca6c17eeb93830984c657637a8e710dc98f19c8051201fa3a",
51
- binaries: {
52
- ffmpeg: "ffmpeg"
53
- }
54
- }, {
55
- url: "https://ffmpeg.martin-riedl.de/download/macos/arm64/1774549676_8.1/ffprobe.zip",
56
- sha256: "fd2e6b7fad9c9aa2bec17c0d7211b5afcc00b4b5c9b63c120985e80c3c198af6",
57
- binaries: {
58
- ffprobe: "ffprobe"
59
- }
60
- }]
61
- }
62
- },
63
- linux: {
64
- x64: {
65
- version: RELEASE_VERSION,
66
- source: "Martin Riedl Linux amd64 release",
67
- archives: [{
68
- url: "https://ffmpeg.martin-riedl.de/download/linux/amd64/1774550169_8.1/ffmpeg.zip",
69
- sha256: "49f9a3642387626f82fd70dd6a268807efe23e0560d6934a6531d6e3e668f18f",
70
- binaries: {
71
- ffmpeg: "ffmpeg"
72
- }
73
- }, {
74
- url: "https://ffmpeg.martin-riedl.de/download/linux/amd64/1774550169_8.1/ffprobe.zip",
75
- sha256: "422082501af33fabb3946d101d098e5105f44492e5a16357c3fac79421544b0e",
76
- binaries: {
77
- ffprobe: "ffprobe"
78
- }
79
- }]
80
- },
81
- arm64: {
82
- version: RELEASE_VERSION,
83
- source: "Martin Riedl Linux arm64 release",
84
- archives: [{
85
- url: "https://ffmpeg.martin-riedl.de/download/linux/arm64/1774548896_8.1/ffmpeg.zip",
86
- sha256: "87000dd625a4f409a5baf71ac177c22210db04ea144e01241713ab196ed39689",
87
- binaries: {
88
- ffmpeg: "ffmpeg"
89
- }
90
- }, {
91
- url: "https://ffmpeg.martin-riedl.de/download/linux/arm64/1774548896_8.1/ffprobe.zip",
92
- sha256: "eb56a190dea6bdd08da2c1e63d7c7523817384eff4dff227f4b088e56205414b",
93
- binaries: {
94
- ffprobe: "ffprobe"
95
- }
96
- }]
97
- }
98
- }
99
- }
8
+ const Util = require("../util")
9
+
10
+ const RELEASE_VERSION = "8.0.1"
11
+ const RELEASE_RANGE = ">=8.0.1 <8.1.0"
12
+ 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
+ `
100
19
 
101
20
  class Ffmpeg {
102
- description = "Installs standalone FFmpeg and FFprobe binaries with MP3 export support."
21
+ description = "Installs FFmpeg for audio and video processing."
103
22
 
104
- artifact() {
105
- const platform = this.kernel.platform
106
- const arch = this.kernel.arch
107
- const spec = ARTIFACTS[platform] && ARTIFACTS[platform][arch]
108
- if (!spec) {
109
- throw new Error(`Standalone FFmpeg is not configured for ${platform}/${arch}`)
23
+ cmd() {
24
+ return CONDA_SPEC
25
+ }
26
+
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)]
110
38
  }
111
- return spec
39
+ return env
112
40
  }
113
41
 
114
- rootPath() {
115
- return this.kernel.bin.path("ffmpeg")
42
+ ffmpegPrefix(kernel = this.kernel) {
43
+ return kernel.bin.path("ffmpeg-env")
116
44
  }
117
45
 
118
- binDir() {
119
- return this.kernel.bin.path("ffmpeg", "bin")
46
+ ffmpegPkgsDir(kernel = this.kernel) {
47
+ return kernel.bin.path("ffmpeg-pkgs")
120
48
  }
121
49
 
122
- tempDir() {
123
- return this.kernel.bin.path("ffmpeg-tmp")
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)
124
56
  }
125
57
 
126
- manifestPath() {
127
- return this.kernel.bin.path("ffmpeg", "INSTALL.json")
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")
128
63
  }
129
64
 
130
- binaryFilename(tool) {
131
- return this.kernel.platform === "win32" ? `${tool}.exe` : tool
65
+ legacyStandalonePaths() {
66
+ return [
67
+ this.kernel.bin.path("ffmpeg"),
68
+ this.kernel.bin.path("ffmpeg-tmp")
69
+ ]
132
70
  }
133
71
 
134
- binaryPath(tool) {
135
- return path.resolve(this.binDir(), this.binaryFilename(tool))
72
+ async start() {
73
+ if (this.kernel.platform !== "darwin") {
74
+ return
75
+ }
76
+ if (!(await this.hasInstalledBinaryPaths())) {
77
+ await this.removeBaseActivationHooks()
78
+ return
79
+ }
80
+ await this.ensureBaseActivationHooks()
81
+ await this.syncMacUvLibraryShims()
82
+ await this.startMacUvLibraryWatcher()
136
83
  }
137
84
 
138
- env() {
139
- return {
140
- PATH: [this.binDir()],
141
- FFMPEG_PATH: this.binaryPath("ffmpeg"),
142
- FFPROBE_PATH: this.binaryPath("ffprobe")
85
+ async install(req, ondata) {
86
+ await this.cleanupLegacyStandalone(ondata)
87
+ if (this.kernel.platform === "win32") {
88
+ await this.installWindows(ondata)
89
+ } else {
90
+ await this.installStandard(ondata)
143
91
  }
92
+ await this.selfTest(ondata)
93
+ await this.ensureBaseActivationHooks()
94
+ await this.syncMacUvLibraryShims(ondata)
144
95
  }
145
96
 
146
- hasLegacyCondaFfmpeg() {
147
- const condaInstalled = this.kernel.bin.installed && this.kernel.bin.installed.conda
148
- return !!(condaInstalled && condaInstalled.has("ffmpeg"))
97
+ async installStandard(ondata) {
98
+ await this.resetInstallPrefix()
99
+ await this.kernel.bin.exec({
100
+ env: {
101
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
102
+ },
103
+ message: [
104
+ "conda clean -y --all",
105
+ `conda create -y -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
106
+ ]
107
+ }, ondata)
149
108
  }
150
109
 
151
- async install(req, ondata) {
152
- const spec = this.artifact()
153
- const rootPath = this.rootPath()
154
- const tempDir = this.tempDir()
155
- const binDir = this.binDir()
156
- const downloads = []
110
+ async installWindows(ondata) {
111
+ await this.resetInstallPrefix()
112
+ const env = {
113
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
114
+ }
157
115
 
158
- ondata({ raw: `preparing standalone ffmpeg ${spec.version} (${this.kernel.platform}/${this.kernel.arch})...\r\n` })
159
- await rimraf(tempDir)
160
- await rimraf(rootPath)
116
+ await this.kernel.bin.exec({
117
+ env,
118
+ message: [
119
+ "conda clean -y --all",
120
+ `conda create -y --download-only -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
121
+ ]
122
+ }, ondata)
161
123
 
162
- try {
163
- await fs.promises.mkdir(tempDir, { recursive: true })
164
- await fs.promises.mkdir(binDir, { recursive: true })
165
-
166
- for (let index = 0; index < spec.archives.length; index++) {
167
- const archive = spec.archives[index]
168
- const filename = `${this.kernel.platform}-${this.kernel.arch}-${index}-${path.basename(new URL(archive.url).pathname)}`
169
- const archivePath = this.kernel.bin.path(filename)
170
- const extractDir = path.resolve(tempDir, `archive-${index}`)
171
-
172
- downloads.push(archivePath)
173
- await this.kernel.bin.download(archive.url, filename, ondata)
174
- await this.verifyChecksum(archivePath, archive.sha256)
175
-
176
- ondata({ raw: `extracting ${filename}...\r\n` })
177
- await fs.promises.mkdir(extractDir, { recursive: true })
178
- await decompress(archivePath, extractDir)
179
-
180
- for (const [tool, expectedName] of Object.entries(archive.binaries)) {
181
- const source = await this.findFileByName(extractDir, expectedName)
182
- if (!source) {
183
- throw new Error(`Could not find ${expectedName} inside ${filename}`)
124
+ await this.patchWindowsGdkPixbufPostLink(this.ffmpegPkgsDir(), ondata)
125
+
126
+ await this.kernel.bin.exec({
127
+ env,
128
+ message: `conda create -y --offline -p "${this.ffmpegPrefix()}" ${CONDA_CHANNEL_FLAGS} ${this.cmd()}`
129
+ }, ondata)
130
+ }
131
+
132
+ async resetInstallPrefix() {
133
+ await rimraf(this.ffmpegPrefix())
134
+ await rimraf(this.ffmpegPkgsDir())
135
+ await fs.promises.mkdir(this.ffmpegPkgsDir(), { recursive: true })
136
+ }
137
+
138
+ async patchWindowsGdkPixbufPostLink(pkgsDir, ondata) {
139
+ const entries = await fs.promises.readdir(pkgsDir, { withFileTypes: true })
140
+ const packageDirs = entries
141
+ .filter((entry) => entry.isDirectory() && /^gdk-pixbuf-/.test(entry.name))
142
+ .map((entry) => path.resolve(pkgsDir, entry.name))
143
+
144
+ if (packageDirs.length === 0) {
145
+ throw new Error("Could not find downloaded gdk-pixbuf package in the Conda cache after --download-only")
146
+ }
147
+
148
+ let patchedCount = 0
149
+ let metadataCount = 0
150
+ for (const packageDir of packageDirs) {
151
+ const scripts = [
152
+ {
153
+ relativePath: "Scripts/.gdk-pixbuf-post-link.bat",
154
+ metadataRequired: true
155
+ },
156
+ {
157
+ relativePath: "info/recipe/post-link.bat",
158
+ metadataRequired: false
159
+ }
160
+ ]
161
+
162
+ for (const { relativePath, metadataRequired } of scripts) {
163
+ const script = path.resolve(packageDir, ...relativePath.split("/"))
164
+ try {
165
+ await fs.promises.access(script)
166
+ await fs.promises.writeFile(script, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
167
+ patchedCount += 1
168
+ const updatedMetadata = await this.updateCondaPathsJson(packageDir, relativePath, WINDOWS_GDK_PIXBUF_POST_LINK_NOOP)
169
+ if (updatedMetadata) {
170
+ metadataCount += 1
171
+ } else if (metadataRequired) {
172
+ throw new Error(`Patched ${relativePath} in ${packageDir}, but did not find a matching info/paths.json entry`)
184
173
  }
185
- const destination = this.binaryPath(tool)
186
- await fs.promises.copyFile(source, destination)
187
- if (this.kernel.platform !== "win32") {
188
- await fs.promises.chmod(destination, 0o755)
174
+ } catch (error) {
175
+ if (error && error.code !== "ENOENT") {
176
+ throw error
189
177
  }
190
178
  }
191
179
  }
180
+ }
181
+
182
+ if (patchedCount === 0) {
183
+ throw new Error("Found gdk-pixbuf in the Conda cache, but did not find any post-link scripts to patch")
184
+ }
185
+
186
+ if (ondata) {
187
+ ondata({
188
+ 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`
189
+ })
190
+ }
191
+ }
192
+
193
+ async updateCondaPathsJson(packageDir, relativePath, contents) {
194
+ const pathsJsonPath = path.resolve(packageDir, "info", "paths.json")
195
+ let pathsJson
192
196
 
193
- await this.writeManifest(spec)
194
- await this.selfTest(ondata)
195
- await this.ensureLegacyCondaFfmpegRemoved(ondata)
196
- ondata({ raw: `ffmpeg ${spec.version} installed from ${spec.source}\r\n` })
197
+ try {
198
+ pathsJson = JSON.parse(await fs.promises.readFile(pathsJsonPath, "utf8"))
197
199
  } catch (error) {
198
- await rimraf(rootPath)
199
- throw error
200
- } finally {
201
- for (const download of downloads) {
202
- await fs.promises.rm(download, { force: true }).catch(() => {})
200
+ if (error && error.code === "ENOENT") {
201
+ return false
203
202
  }
204
- await rimraf(tempDir)
203
+ throw error
204
+ }
205
+
206
+ if (!pathsJson || !Array.isArray(pathsJson.paths)) {
207
+ return false
205
208
  }
209
+
210
+ const normalizedPath = relativePath.replace(/\\/g, "/")
211
+ const entry = pathsJson.paths.find((item) => item && item._path === normalizedPath)
212
+ if (!entry) {
213
+ return false
214
+ }
215
+
216
+ const buffer = Buffer.isBuffer(contents) ? contents : Buffer.from(String(contents), "utf8")
217
+ entry.sha256 = crypto.createHash("sha256").update(buffer).digest("hex")
218
+ entry.size_in_bytes = buffer.length
219
+
220
+ await fs.promises.writeFile(pathsJsonPath, `${JSON.stringify(pathsJson, null, 2)}\n`)
221
+ return true
206
222
  }
207
223
 
208
- async installed() {
224
+ async hasInstalledBinaryPaths() {
209
225
  try {
210
226
  await fs.promises.access(this.binaryPath("ffmpeg"))
211
227
  await fs.promises.access(this.binaryPath("ffprobe"))
212
- if (this.hasLegacyCondaFfmpeg()) {
213
- console.log("standalone ffmpeg installed check failed: legacy conda ffmpeg is still present")
228
+ return true
229
+ } catch (error) {
230
+ return false
231
+ }
232
+ }
233
+
234
+ async installed() {
235
+ try {
236
+ if (!(await this.hasInstalledBinaryPaths())) {
237
+ await this.removeBaseActivationHooks()
214
238
  return false
215
239
  }
240
+
241
+ await this.ensureBaseActivationHooks()
242
+ await this.syncMacUvLibraryShims()
243
+ await this.startMacUvLibraryWatcher()
216
244
  await this.selfTest()
217
245
  return true
218
246
  } catch (error) {
219
- console.log("standalone ffmpeg installed check failed", error && error.message ? error.message : error)
247
+ console.log("conda ffmpeg installed check failed", error && error.message ? error.message : error)
220
248
  return false
221
249
  }
222
250
  }
223
251
 
224
252
  async uninstall(req, ondata) {
225
- ondata({ raw: "cleaning up\r\n" })
226
- await rimraf(this.rootPath())
227
- await rimraf(this.tempDir())
228
- ondata({ raw: "finished cleaning up\r\n" })
253
+ await this.stopMacUvLibraryWatcher()
254
+ await this.removeMacUvLibraryShims(ondata)
255
+ await this.removeBaseActivationHooks()
256
+ const prefix = this.ffmpegPrefix()
257
+ const exists = await fs.promises.access(prefix).then(() => true).catch(() => false)
258
+ if (exists) {
259
+ try {
260
+ await this.kernel.bin.exec({
261
+ env: {
262
+ CONDA_PKGS_DIRS: this.ffmpegPkgsDir()
263
+ },
264
+ message: `conda remove -y -p "${prefix}" --all`
265
+ }, ondata)
266
+ } catch (error) {
267
+ await rimraf(prefix)
268
+ }
269
+ }
270
+ await rimraf(prefix)
271
+ await rimraf(this.ffmpegPkgsDir())
272
+ await this.cleanupLegacyStandalone(ondata)
273
+ }
274
+
275
+ activationDirs() {
276
+ return {
277
+ activate: this.kernel.bin.path("miniconda", "etc", "conda", "activate.d"),
278
+ deactivate: this.kernel.bin.path("miniconda", "etc", "conda", "deactivate.d")
279
+ }
280
+ }
281
+
282
+ activationHookFiles() {
283
+ const { activate, deactivate } = this.activationDirs()
284
+ const files = [
285
+ {
286
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.sh"),
287
+ content: this.posixActivateSh(this.kernel.platform === "win32")
288
+ },
289
+ {
290
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.sh"),
291
+ content: this.posixDeactivateSh(this.kernel.platform === "win32")
292
+ }
293
+ ]
294
+
295
+ if (this.kernel.platform !== "win32") {
296
+ return files
297
+ }
298
+
299
+ return files.concat([
300
+ {
301
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.bat"),
302
+ content: this.windowsActivateBat()
303
+ },
304
+ {
305
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.bat"),
306
+ content: this.windowsDeactivateBat()
307
+ },
308
+ {
309
+ path: path.resolve(activate, "zz_pinokio_ffmpeg.ps1"),
310
+ content: this.windowsActivatePs1()
311
+ },
312
+ {
313
+ path: path.resolve(deactivate, "zz_pinokio_ffmpeg.ps1"),
314
+ content: this.windowsDeactivatePs1()
315
+ }
316
+ ])
317
+ }
318
+
319
+ async ensureBaseActivationHooks() {
320
+ const dirs = this.activationDirs()
321
+ await fs.promises.mkdir(dirs.activate, { recursive: true }).catch(() => {})
322
+ await fs.promises.mkdir(dirs.deactivate, { recursive: true }).catch(() => {})
323
+ for (const file of this.activationHookFiles()) {
324
+ await fs.promises.writeFile(file.path, file.content)
325
+ }
326
+ }
327
+
328
+ async removeBaseActivationHooks() {
329
+ for (const file of this.activationHookFiles()) {
330
+ await fs.promises.rm(file.path, { force: true }).catch(() => {})
331
+ }
332
+ }
333
+
334
+ windowsActivateBat() {
335
+ const prefix = this.ffmpegPrefix()
336
+ const runtime = this.libraryDir()
337
+ return `@echo off
338
+ set "PINOKIO_FFMPEG_PREFIX=${prefix}"
339
+ set "PINOKIO_FFMPEG_RUNTIME=${runtime}"
340
+ set "FFMPEG_PATH=%PINOKIO_FFMPEG_RUNTIME%\\ffmpeg.exe"
341
+ set "FFPROBE_PATH=%PINOKIO_FFMPEG_RUNTIME%\\ffprobe.exe"
342
+ call :pinokio_ffmpeg_remove_from_path "%PINOKIO_FFMPEG_RUNTIME%"
343
+ set "PATH=%PINOKIO_FFMPEG_RUNTIME%;%PATH%"
344
+ goto :eof
345
+
346
+ :pinokio_ffmpeg_remove_from_path
347
+ setlocal EnableDelayedExpansion
348
+ set "_pinokio_target=%~1"
349
+ set "_pinokio_path=;%PATH%;"
350
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%;=;!"
351
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%\\;=;!"
352
+ if "!_pinokio_path:~0,1!"==";" set "_pinokio_path=!_pinokio_path:~1!"
353
+ if "!_pinokio_path:~-1!"==";" set "_pinokio_path=!_pinokio_path:~0,-1!"
354
+ endlocal & set "PATH=%_pinokio_path%"
355
+ exit /b 0
356
+ `
357
+ }
358
+
359
+ windowsDeactivateBat() {
360
+ return `@echo off
361
+ if defined PINOKIO_FFMPEG_RUNTIME call :pinokio_ffmpeg_remove_from_path "%PINOKIO_FFMPEG_RUNTIME%"
362
+ set "FFMPEG_PATH="
363
+ set "FFPROBE_PATH="
364
+ set "PINOKIO_FFMPEG_PREFIX="
365
+ set "PINOKIO_FFMPEG_RUNTIME="
366
+ goto :eof
367
+
368
+ :pinokio_ffmpeg_remove_from_path
369
+ setlocal EnableDelayedExpansion
370
+ set "_pinokio_target=%~1"
371
+ set "_pinokio_path=;%PATH%;"
372
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%;=;!"
373
+ set "_pinokio_path=!_pinokio_path:;%_pinokio_target%\\;=;!"
374
+ if "!_pinokio_path:~0,1!"==";" set "_pinokio_path=!_pinokio_path:~1!"
375
+ if "!_pinokio_path:~-1!"==";" set "_pinokio_path=!_pinokio_path:~0,-1!"
376
+ endlocal & set "PATH=%_pinokio_path%"
377
+ exit /b 0
378
+ `
379
+ }
380
+
381
+ windowsActivatePs1() {
382
+ const prefix = this.ffmpegPrefix().replace(/\\/g, "\\\\")
383
+ const runtime = this.libraryDir().replace(/\\/g, "\\\\")
384
+ return `$Env:PINOKIO_FFMPEG_PREFIX = "${prefix}"
385
+ $Env:PINOKIO_FFMPEG_RUNTIME = "${runtime}"
386
+ $Env:FFMPEG_PATH = Join-Path $Env:PINOKIO_FFMPEG_RUNTIME "ffmpeg.exe"
387
+ $Env:FFPROBE_PATH = Join-Path $Env:PINOKIO_FFMPEG_RUNTIME "ffprobe.exe"
388
+ $pinokioParts = @()
389
+ if ($Env:Path) {
390
+ $pinokioParts = @($Env:Path -split ';' | Where-Object { $_ -and $_ -ne $Env:PINOKIO_FFMPEG_RUNTIME })
391
+ }
392
+ $Env:Path = (@($Env:PINOKIO_FFMPEG_RUNTIME) + $pinokioParts) -join ';'
393
+ `
394
+ }
395
+
396
+ windowsDeactivatePs1() {
397
+ return `if ($Env:PINOKIO_FFMPEG_RUNTIME) {
398
+ $pinokioParts = @()
399
+ if ($Env:Path) {
400
+ $pinokioParts = @($Env:Path -split ';' | Where-Object { $_ -and $_ -ne $Env:PINOKIO_FFMPEG_RUNTIME })
401
+ }
402
+ $Env:Path = $pinokioParts -join ';'
403
+ }
404
+ Remove-Item -Path Env:\\FFMPEG_PATH -ErrorAction SilentlyContinue
405
+ Remove-Item -Path Env:\\FFPROBE_PATH -ErrorAction SilentlyContinue
406
+ Remove-Item -Path Env:\\PINOKIO_FFMPEG_PREFIX -ErrorAction SilentlyContinue
407
+ Remove-Item -Path Env:\\PINOKIO_FFMPEG_RUNTIME -ErrorAction SilentlyContinue
408
+ `
409
+ }
410
+
411
+ posixActivateSh(forceWindowsPaths = false) {
412
+ const prefix = forceWindowsPaths ? Util.p2u(this.ffmpegPrefix()) : this.ffmpegPrefix()
413
+ const binDir = forceWindowsPaths ? Util.p2u(path.resolve(this.ffmpegPrefix(), "Library", "bin")) : path.resolve(this.ffmpegPrefix(), "bin")
414
+ const libDir = forceWindowsPaths ? "" : this.libraryDir()
415
+ return `pinokio_ffmpeg_prepend_path() {
416
+ local target="$1"
417
+ local current="\${2-}"
418
+ local result=""
419
+ local part
420
+ local old_ifs="$IFS"
421
+ IFS=':'
422
+ for part in $current; do
423
+ [ -n "$part" ] || continue
424
+ [ "$part" = "$target" ] && continue
425
+ if [ -n "$result" ]; then
426
+ result="$result:$part"
427
+ else
428
+ result="$part"
429
+ fi
430
+ done
431
+ IFS="$old_ifs"
432
+ if [ -n "$result" ]; then
433
+ printf '%s:%s' "$target" "$result"
434
+ else
435
+ printf '%s' "$target"
436
+ fi
437
+ }
438
+ pinokio_ffmpeg_remove_path() {
439
+ local target="$1"
440
+ local current="\${2-}"
441
+ local result=""
442
+ local part
443
+ local old_ifs="$IFS"
444
+ IFS=':'
445
+ for part in $current; do
446
+ [ -n "$part" ] || continue
447
+ [ "$part" = "$target" ] && continue
448
+ if [ -n "$result" ]; then
449
+ result="$result:$part"
450
+ else
451
+ result="$part"
452
+ fi
453
+ done
454
+ IFS="$old_ifs"
455
+ printf '%s' "$result"
456
+ }
457
+ export PINOKIO_FFMPEG_PREFIX="${prefix}"
458
+ export PINOKIO_FFMPEG_BIN="${binDir}"
459
+ export FFMPEG_PATH="$PINOKIO_FFMPEG_BIN/${forceWindowsPaths ? "ffmpeg.exe" : "ffmpeg"}"
460
+ export FFPROBE_PATH="$PINOKIO_FFMPEG_BIN/${forceWindowsPaths ? "ffprobe.exe" : "ffprobe"}"
461
+ export PATH="$(pinokio_ffmpeg_prepend_path "$PINOKIO_FFMPEG_BIN" "$PATH")"
462
+ ${forceWindowsPaths ? "" : `if [ "$(uname -s)" = "Linux" ]; then
463
+ export LD_LIBRARY_PATH="$(pinokio_ffmpeg_prepend_path "${libDir}" "\${LD_LIBRARY_PATH-}")"
464
+ fi
465
+ `}
466
+ unset -f pinokio_ffmpeg_prepend_path
467
+ unset -f pinokio_ffmpeg_remove_path
468
+ `
469
+ }
470
+
471
+ posixDeactivateSh(forceWindowsPaths = false) {
472
+ return `pinokio_ffmpeg_remove_path() {
473
+ local target="$1"
474
+ local current="\${2-}"
475
+ local result=""
476
+ local part
477
+ local old_ifs="$IFS"
478
+ IFS=':'
479
+ for part in $current; do
480
+ [ -n "$part" ] || continue
481
+ [ "$part" = "$target" ] && continue
482
+ if [ -n "$result" ]; then
483
+ result="$result:$part"
484
+ else
485
+ result="$part"
486
+ fi
487
+ done
488
+ IFS="$old_ifs"
489
+ printf '%s' "$result"
490
+ }
491
+ if [ -n "\${PINOKIO_FFMPEG_BIN-}" ]; then
492
+ export PATH="$(pinokio_ffmpeg_remove_path "$PINOKIO_FFMPEG_BIN" "$PATH")"
493
+ fi
494
+ ${forceWindowsPaths ? "" : `if [ "$(uname -s)" = "Linux" ] && [ -n "\${PINOKIO_FFMPEG_PREFIX-}" ]; then
495
+ export LD_LIBRARY_PATH="$(pinokio_ffmpeg_remove_path "${this.libraryDir()}" "\${LD_LIBRARY_PATH-}")"
496
+ fi
497
+ `}
498
+ unset FFMPEG_PATH
499
+ unset FFPROBE_PATH
500
+ unset PINOKIO_FFMPEG_PREFIX
501
+ unset PINOKIO_FFMPEG_BIN
502
+ unset -f pinokio_ffmpeg_remove_path
503
+ `
504
+ }
505
+
506
+ async cleanupLegacyStandalone(ondata) {
507
+ for (const target of this.legacyStandalonePaths()) {
508
+ const exists = await fs.promises.access(target).then(() => true).catch(() => false)
509
+ if (exists) {
510
+ if (ondata) {
511
+ ondata({ raw: `removing legacy standalone ffmpeg files from ${target}...\r\n` })
512
+ }
513
+ await rimraf(target)
514
+ }
515
+ }
516
+ }
517
+
518
+ uvPythonRoot(kernel = this.kernel) {
519
+ return kernel.path("cache", "XDG_DATA_HOME", "uv", "python")
520
+ }
521
+
522
+ async startMacUvLibraryWatcher() {
523
+ if (this.kernel.platform !== "darwin" || this.macUvLibraryWatcher) {
524
+ return
525
+ }
526
+
527
+ const root = this.uvPythonRoot()
528
+ await fs.promises.mkdir(root, { recursive: true })
529
+ this.macUvLibraryWatcher = await ParcelWatcher.subscribe(root, (error, events) => {
530
+ if (error) {
531
+ console.warn("ffmpeg uv library watcher error", error && error.message ? error.message : error)
532
+ return
533
+ }
534
+ if (!events || events.length === 0) {
535
+ return
536
+ }
537
+ this.scheduleMacUvLibraryShimSync()
538
+ })
539
+ }
540
+
541
+ async stopMacUvLibraryWatcher() {
542
+ if (this.macUvLibraryShimSyncTimer) {
543
+ clearTimeout(this.macUvLibraryShimSyncTimer)
544
+ this.macUvLibraryShimSyncTimer = null
545
+ }
546
+ if (this.macUvLibraryWatcher) {
547
+ await this.macUvLibraryWatcher.unsubscribe()
548
+ this.macUvLibraryWatcher = null
549
+ }
550
+ }
551
+
552
+ scheduleMacUvLibraryShimSync() {
553
+ if (this.macUvLibraryShimSyncTimer) {
554
+ clearTimeout(this.macUvLibraryShimSyncTimer)
555
+ }
556
+ this.macUvLibraryShimSyncTimer = setTimeout(async () => {
557
+ this.macUvLibraryShimSyncTimer = null
558
+ try {
559
+ await this.syncMacUvLibraryShims()
560
+ } catch (error) {
561
+ console.warn("ffmpeg uv library shim sync error", error && error.message ? error.message : error)
562
+ }
563
+ }, 250)
564
+ }
565
+
566
+ async uvLibraryDirs(kernel = this.kernel) {
567
+ if (kernel.platform !== "darwin") {
568
+ return []
569
+ }
570
+
571
+ const root = this.uvPythonRoot(kernel)
572
+ const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
573
+ const dirs = []
574
+
575
+ for (const entry of entries) {
576
+ if (!entry.isDirectory()) {
577
+ continue
578
+ }
579
+ const libDir = path.resolve(root, entry.name, "lib")
580
+ const exists = await fs.promises.access(libDir).then(() => true).catch(() => false)
581
+ if (exists) {
582
+ dirs.push(libDir)
583
+ }
584
+ }
585
+
586
+ return dirs
587
+ }
588
+
589
+ async ffmpegLibraryFiles(kernel = this.kernel) {
590
+ if (kernel.platform !== "darwin") {
591
+ return []
592
+ }
593
+
594
+ const dir = this.libraryDir(kernel)
595
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => [])
596
+ return entries
597
+ .filter((entry) => entry.isFile() || entry.isSymbolicLink())
598
+ .map((entry) => entry.name)
599
+ .filter((name) => /^lib(?:av|sw)[^.]+(?:\.\d+)*\.dylib$/i.test(name))
600
+ .sort()
601
+ }
602
+
603
+ async syncMacUvLibraryShims(ondata) {
604
+ if (this.kernel.platform !== "darwin") {
605
+ return
606
+ }
607
+
608
+ const [libraryDirs, libraryFiles] = await Promise.all([
609
+ this.uvLibraryDirs(),
610
+ this.ffmpegLibraryFiles()
611
+ ])
612
+
613
+ if (libraryDirs.length === 0 || libraryFiles.length === 0) {
614
+ return
615
+ }
616
+
617
+ let createdCount = 0
618
+ let refreshedCount = 0
619
+ const sourceDir = this.libraryDir()
620
+
621
+ for (const libDir of libraryDirs) {
622
+ for (const filename of libraryFiles) {
623
+ const source = path.resolve(sourceDir, filename)
624
+ const target = path.resolve(libDir, filename)
625
+ const desiredLink = path.relative(libDir, source)
626
+
627
+ let stat
628
+ try {
629
+ stat = await fs.promises.lstat(target)
630
+ } catch (error) {
631
+ if (!error || error.code !== "ENOENT") {
632
+ throw error
633
+ }
634
+ }
635
+
636
+ if (!stat) {
637
+ await fs.promises.symlink(desiredLink, target)
638
+ createdCount += 1
639
+ continue
640
+ }
641
+
642
+ if (!stat.isSymbolicLink()) {
643
+ continue
644
+ }
645
+
646
+ const currentLink = await fs.promises.readlink(target)
647
+ if (currentLink === desiredLink) {
648
+ continue
649
+ }
650
+
651
+ await fs.promises.unlink(target)
652
+ await fs.promises.symlink(desiredLink, target)
653
+ refreshedCount += 1
654
+ }
655
+ }
656
+
657
+ if (ondata && (createdCount > 0 || refreshedCount > 0)) {
658
+ ondata({
659
+ raw: `synced ${createdCount + refreshedCount} FFmpeg dylib shim(s) into uv Python runtime libraries...\r\n`
660
+ })
661
+ }
662
+ }
663
+
664
+ async removeMacUvLibraryShims(ondata) {
665
+ if (this.kernel.platform !== "darwin") {
666
+ return
667
+ }
668
+
669
+ const [libraryDirs, libraryFiles] = await Promise.all([
670
+ this.uvLibraryDirs(),
671
+ this.ffmpegLibraryFiles()
672
+ ])
673
+
674
+ if (libraryDirs.length === 0 || libraryFiles.length === 0) {
675
+ return
676
+ }
677
+
678
+ const sourceDir = this.libraryDir()
679
+ let removedCount = 0
680
+
681
+ for (const libDir of libraryDirs) {
682
+ for (const filename of libraryFiles) {
683
+ const target = path.resolve(libDir, filename)
684
+
685
+ let stat
686
+ try {
687
+ stat = await fs.promises.lstat(target)
688
+ } catch (error) {
689
+ if (!error || error.code !== "ENOENT") {
690
+ throw error
691
+ }
692
+ }
693
+
694
+ if (!stat || !stat.isSymbolicLink()) {
695
+ continue
696
+ }
697
+
698
+ const currentLink = await fs.promises.readlink(target)
699
+ const resolved = path.resolve(libDir, currentLink)
700
+ if (path.dirname(resolved) !== sourceDir) {
701
+ continue
702
+ }
703
+
704
+ await fs.promises.unlink(target)
705
+ removedCount += 1
706
+ }
707
+ }
708
+
709
+ if (ondata && removedCount > 0) {
710
+ ondata({ raw: `removed ${removedCount} FFmpeg dylib shim(s) from uv Python runtime libraries...\r\n` })
711
+ }
229
712
  }
230
713
 
231
714
  async selfTest(ondata) {
232
715
  if (ondata) {
233
- ondata({ raw: "verifying ffmpeg binaries...\r\n" })
716
+ ondata({ raw: "verifying ffmpeg installation...\r\n" })
234
717
  }
235
718
 
236
719
  const ffmpegVersionOutput = await this.execBinary(this.binaryPath("ffmpeg"), ["-version"])
@@ -250,66 +733,52 @@ class Ffmpeg {
250
733
  throw new Error(`Unexpected ffprobe version: ${this.firstLine(ffprobeVersionOutput)}`)
251
734
  }
252
735
 
736
+ await this.assertSharedLibraries()
253
737
  return true
254
738
  }
255
739
 
256
- async ensureLegacyCondaFfmpegRemoved(ondata) {
257
- await this.kernel.bin.refreshInstalled()
258
- if (!this.hasLegacyCondaFfmpeg()) {
259
- return
260
- }
740
+ async assertSharedLibraries() {
741
+ const dir = this.libraryDir()
742
+ const entries = await fs.promises.readdir(dir).catch(() => {
743
+ throw new Error(`Missing FFmpeg library directory: ${dir}`)
744
+ })
261
745
 
262
- ondata({ raw: "removing legacy conda ffmpeg from the base environment...\r\n" })
263
- await this.kernel.bin.exec({
264
- message: "conda remove -y ffmpeg"
265
- }, ondata)
266
- await this.kernel.bin.refreshInstalled()
267
- if (this.hasLegacyCondaFfmpeg()) {
268
- throw new Error("Legacy conda ffmpeg is still installed in the base environment")
746
+ const patterns = this.sharedLibraryPatterns()
747
+ for (const pattern of patterns) {
748
+ if (!entries.some((name) => pattern.test(name))) {
749
+ throw new Error(`Missing FFmpeg shared library matching ${pattern}`)
750
+ }
269
751
  }
270
752
  }
271
753
 
272
- async verifyChecksum(filePath, expectedHash) {
273
- const actualHash = await this.sha256(filePath)
274
- if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
275
- throw new Error(`Checksum mismatch for ${path.basename(filePath)}: expected ${expectedHash}, got ${actualHash}`)
754
+ sharedLibraryPatterns() {
755
+ if (this.kernel.platform === "win32") {
756
+ return [
757
+ /^avcodec-\d+\.dll$/i,
758
+ /^avformat-\d+\.dll$/i,
759
+ /^avutil-\d+\.dll$/i,
760
+ /^swresample-\d+\.dll$/i,
761
+ /^swscale-\d+\.dll$/i
762
+ ]
276
763
  }
277
- }
278
-
279
- async sha256(filePath) {
280
- const buffer = await fs.promises.readFile(filePath)
281
- return crypto.createHash("sha256").update(buffer).digest("hex")
282
- }
283
764
 
284
- async writeManifest(spec) {
285
- const manifest = {
286
- version: spec.version,
287
- platform: this.kernel.platform,
288
- arch: this.kernel.arch,
289
- source: spec.source,
290
- installed_at: new Date().toISOString(),
291
- binaries: {
292
- ffmpeg: this.binaryPath("ffmpeg"),
293
- ffprobe: this.binaryPath("ffprobe")
294
- }
765
+ if (this.kernel.platform === "darwin") {
766
+ return [
767
+ /^libavcodec(\.\d+)*\.dylib$/i,
768
+ /^libavformat(\.\d+)*\.dylib$/i,
769
+ /^libavutil(\.\d+)*\.dylib$/i,
770
+ /^libswresample(\.\d+)*\.dylib$/i,
771
+ /^libswscale(\.\d+)*\.dylib$/i
772
+ ]
295
773
  }
296
- await fs.promises.writeFile(this.manifestPath(), JSON.stringify(manifest, null, 2))
297
- }
298
774
 
299
- async findFileByName(root, targetName) {
300
- const entries = await fs.promises.readdir(root, { withFileTypes: true })
301
- for (const entry of entries) {
302
- const fullPath = path.resolve(root, entry.name)
303
- if (entry.isDirectory()) {
304
- const found = await this.findFileByName(fullPath, targetName)
305
- if (found) {
306
- return found
307
- }
308
- } else if (entry.name === targetName) {
309
- return fullPath
310
- }
311
- }
312
- return null
775
+ return [
776
+ /^libavcodec\.so(\.\d+)*$/i,
777
+ /^libavformat\.so(\.\d+)*$/i,
778
+ /^libavutil\.so(\.\d+)*$/i,
779
+ /^libswresample\.so(\.\d+)*$/i,
780
+ /^libswscale\.so(\.\d+)*$/i
781
+ ]
313
782
  }
314
783
 
315
784
  execBinary(file, args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.2.1",
3
+ "version": "7.2.5",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {