pinokiod 7.4.3 → 7.5.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.
@@ -23,8 +23,8 @@ const Cuda = require("./cuda")
23
23
  const Torch = require("./torch")
24
24
  const { buildCondaListFromMeta } = require('./conda-meta')
25
25
  const {
26
+ isExpectedPythonPinned,
26
27
  isExpectedSqlitePinned,
27
- isWindowsPythonSslFixed,
28
28
  } = require('./conda-pins')
29
29
  const { glob } = require('glob')
30
30
  const fakeUa = require('fake-useragent');
@@ -360,9 +360,9 @@ class Bin {
360
360
  // if importlib_metadata || uvicorn || fastapi exist in the base environment, this.correct_conda = false
361
361
  let site_packages_path
362
362
  if (this.platform === "win32") {
363
- site_packages_path = path.resolve(this.kernel.homedir, "bin/miniconda/Lib/site-packages")
363
+ site_packages_path = path.resolve(this.kernel.homedir, "bin/miniforge/Lib/site-packages")
364
364
  } else {
365
- site_packages_path = path.resolve(this.kernel.homedir, "bin/miniconda/lib/python3.10/site-packages")
365
+ site_packages_path = path.resolve(this.kernel.homedir, "bin/miniforge/lib/python3.10/site-packages")
366
366
  }
367
367
  // // check if any of 'uvicorn', 'importlib_metadata', 'fastapi' exists
368
368
  // let module_paths = ["fastapi", "uvicorn", "importlib_metadata"].map((name) => {
@@ -384,7 +384,7 @@ class Bin {
384
384
  if (to_reset_exists) {
385
385
  this.correct_conda = false
386
386
  } else {
387
- let res = await buildCondaListFromMeta(this.kernel.bin.path("miniconda"))
387
+ let res = await buildCondaListFromMeta(this.kernel.bin.path("miniforge"))
388
388
  let lines = res.response.split(/[\r\n]+/)
389
389
  for(let line of lines) {
390
390
  if (start) {
@@ -400,11 +400,8 @@ class Bin {
400
400
  conda_check.conda = true
401
401
  }
402
402
  if (name === "conda-libmamba-solver") {
403
- //if (String(version) === "24.7.0") {
404
- let channel = chunks[3]
405
403
  let coerced = semver.coerce(version)
406
404
  let mamba_requirement = ">=25.4.0"
407
- //if (semver.satisfies(coerced, mamba_requirement) && channel === "conda-forge") {
408
405
  if (semver.satisfies(coerced, mamba_requirement)) {
409
406
  conda_check.mamba = true
410
407
  }
@@ -417,7 +414,7 @@ class Bin {
417
414
  }
418
415
  }
419
416
  if (name === "python") {
420
- conda_check.python = this.platform !== "win32" || isWindowsPythonSslFixed(version, build)
417
+ conda_check.python = isExpectedPythonPinned(this.platform, version, build)
421
418
  }
422
419
  }
423
420
  } else {
@@ -427,8 +424,7 @@ class Bin {
427
424
  }
428
425
  }
429
426
 
430
- if (conda_check.conda && conda_check.mamba && conda_check.sqlite && (this.platform !== "win32" || conda_check.python)) {
431
- //if (conda_check.conda && conda_check.mamba) {
427
+ if (conda_check.conda && conda_check.mamba && conda_check.sqlite && conda_check.python) {
432
428
  this.correct_conda = true
433
429
  }
434
430
  }
@@ -449,7 +445,7 @@ class Bin {
449
445
  // 1. conda
450
446
 
451
447
  // check conda location and see if it exists. only run if it exists
452
- let conda_path = path.resolve(this.kernel.homedir, "bin", "miniconda")
448
+ let conda_path = path.resolve(this.kernel.homedir, "bin", "miniforge")
453
449
  let conda_exists = await this.exists(conda_path)
454
450
 
455
451
  let start
@@ -513,7 +509,7 @@ class Bin {
513
509
  }
514
510
 
515
511
  // /// B. base path initialization
516
- // let conda_meta_path = this.kernel.bin.path("miniconda", "conda-meta")
512
+ // let conda_meta_path = this.kernel.bin.path("miniforge", "conda-meta")
517
513
  // const metaFiles = await glob("*.json", {
518
514
  // cwd: conda_meta_path
519
515
  // })
@@ -39,11 +39,11 @@ class Huggingface {
39
39
  }
40
40
  hfPath() {
41
41
  const candidates = this.kernel.platform === "win32" ? [
42
- this.kernel.path("bin", "miniconda", "Scripts", "hf.exe"),
43
- this.kernel.path("bin", "miniconda", "Scripts", "hf"),
44
- this.kernel.path("bin", "miniconda", "bin", "hf"),
42
+ this.kernel.path("bin", "miniforge", "Scripts", "hf.exe"),
43
+ this.kernel.path("bin", "miniforge", "Scripts", "hf"),
44
+ this.kernel.path("bin", "miniforge", "bin", "hf"),
45
45
  ] : [
46
- this.kernel.path("bin", "miniconda", "bin", "hf"),
46
+ this.kernel.path("bin", "miniforge", "bin", "hf"),
47
47
  ]
48
48
  for (const candidate of candidates) {
49
49
  if (fs.existsSync(candidate)) {
package/kernel/index.js CHANGED
@@ -43,7 +43,6 @@ const AppLauncher = require('./app_launcher')
43
43
  const ReadyState = require('./ready')
44
44
  const Autolaunch = require('./autolaunch')
45
45
  const LaunchRequirements = require('./launch_requirements')
46
- const LegacyCleanup = require('./legacy_cleanup')
47
46
  const { DownloaderHelper } = require('node-downloader-helper');
48
47
  const { ProxyAgent } = require('proxy-agent');
49
48
  const fakeUa = require('fake-useragent');
@@ -101,7 +100,6 @@ class Kernel {
101
100
  this.launchRequirements = new LaunchRequirements(this)
102
101
  this.autolaunch = new Autolaunch(this)
103
102
  this.autolaunch_status = this.autolaunch.startupStatus()
104
- this.legacyCleanup = new LegacyCleanup(this)
105
103
  this.sysReady = new Promise((resolve) => {
106
104
  this._resolveSysReady = resolve
107
105
  })
@@ -1194,11 +1192,6 @@ class Kernel {
1194
1192
  await Environment.ensurePinokioCacheDirs(this, {
1195
1193
  throwOnFailure: true
1196
1194
  })
1197
- await this.legacyCleanup.runOnce((event) => {
1198
- if (event && typeof event.raw === "string") {
1199
- console.log(event.raw.trimEnd())
1200
- }
1201
- })
1202
1195
 
1203
1196
  // if key.json doesn't exist, create an empty json file
1204
1197
  let ee = await this.exists(this.homedir, "key.json")
package/kernel/shell.js CHANGED
@@ -21,6 +21,7 @@ const ShellParser = require('./shell_parser')
21
21
  const AnsiStreamTracker = require('./ansi_stream_tracker')
22
22
  const ShellStateSync = require('./shell_state_sync')
23
23
  const ShellRunTemplate = require('./api/shell_run_template')
24
+ const { PYTHON_INSTALL_SPEC } = require('./bin/conda-pins')
24
25
  const home = os.homedir()
25
26
 
26
27
  function normalizeComparablePath(filePath, platform) {
@@ -959,7 +960,7 @@ class Shell {
959
960
  conda_hook () {
960
961
  if (this.platform === "win32") {
961
962
  if (/bash/i.test(this.shell)) {
962
- const conda_sh = this.kernel.path("bin/miniconda/etc/profile.d/conda.sh")
963
+ const conda_sh = this.kernel.path("bin/miniforge/etc/profile.d/conda.sh")
963
964
  return `source ${this.quoteArgForShell(Util.p2u(conda_sh))}`
964
965
  } else {
965
966
  return "conda_hook"
@@ -986,7 +987,7 @@ class Shell {
986
987
  // 1. conda
987
988
  let conda_path
988
989
  let conda_name
989
- let conda_python = "python=3.10"
990
+ let conda_python = PYTHON_INSTALL_SPEC
990
991
  let conda_args
991
992
  let conda_activate
992
993
 
@@ -1106,7 +1107,7 @@ class Shell {
1106
1107
  // timeout,
1107
1108
  ]
1108
1109
  } else {
1109
- let envs_path = this.kernel.bin.path("miniconda/envs")
1110
+ let envs_path = this.kernel.bin.path("miniforge/envs")
1110
1111
  let env_path = path.resolve(envs_path, conda_name)
1111
1112
  let env_exists = await this.exists(env_path)
1112
1113
  if (env_exists) {
@@ -1185,7 +1186,7 @@ class Shell {
1185
1186
  console.log(`Unsupported arch: os.arch()=${architecture}, process.arch=${armArchitecture}`)
1186
1187
  }
1187
1188
 
1188
- const activate_root = this.kernel.bin.path("miniconda/etc/conda/activate.d")
1189
+ const activate_root = this.kernel.bin.path("miniforge/etc/conda/activate.d")
1189
1190
  const logDir = this.kernel.path("cache", "logs")
1190
1191
  await fs.promises.mkdir(logDir, { recursive: true }).catch(() => {})
1191
1192
  const logSuffix = this.id || Date.now()
package/kernel/sysinfo.js CHANGED
@@ -206,7 +206,7 @@ class Sysinfo {
206
206
  // console.log(_res)
207
207
  // console.log("############### DEBUGGING ENV FINISH")
208
208
 
209
- let conda_path = path.resolve(this.kernel.homedir, "bin", "miniconda")
209
+ let conda_path = path.resolve(this.kernel.homedir, "bin", "miniforge")
210
210
  let conda_exists = await this.exists(conda_path)
211
211
 
212
212
  let res
package/kernel/util.js CHANGED
@@ -199,9 +199,9 @@ const filepicker = async(req, ondata, kernel) => {
199
199
  let picker_path = kernel.path("bin/py/picker.py")
200
200
  let python
201
201
  if (kernel.platform === "win32") {
202
- python = kernel.path("bin/miniconda/python")
202
+ python = kernel.path("bin/miniforge/python")
203
203
  } else {
204
- python = kernel.path("bin/miniconda/bin/python")
204
+ python = kernel.path("bin/miniforge/bin/python")
205
205
  }
206
206
  const proc = spawn(python, [picker_path])
207
207
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.4.3",
3
+ "version": "7.5.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -501,7 +501,7 @@ class Server {
501
501
  if (this.kernel.platform !== "win32") {
502
502
  return null
503
503
  }
504
- const bashPath = this.kernel.path("bin/miniconda/Library/bin/bash.exe")
504
+ const bashPath = this.kernel.path("bin/miniforge/Library/bin/bash.exe")
505
505
  return await this.exists(bashPath) ? bashPath : null
506
506
  }
507
507
  running_dynamic (name, menu, selected_query) {
@@ -11730,7 +11730,7 @@ class Server {
11730
11730
  managedLaunchOverrides.conda = { skip: true }
11731
11731
  }
11732
11732
  if (isWindowsPlatform && (provider.key === "codex" || provider.key === "claude")) {
11733
- const gitBashPath = this.kernel.path("bin/miniconda/Library/bin/bash.exe")
11733
+ const gitBashPath = this.kernel.path("bin/miniforge/Library/bin/bash.exe")
11734
11734
  const bashExists = await fs.promises.access(gitBashPath, fs.constants.F_OK).then(() => true).catch(() => false)
11735
11735
  if (bashExists) {
11736
11736
  if (provider.key === "claude") {
@@ -7,12 +7,12 @@ module.exports = {
7
7
  id: "run",
8
8
  method: "shell.run",
9
9
  params: {
10
- shell: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}",
10
+ shell: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
11
11
  conda: {
12
12
  skip: true
13
13
  },
14
14
  env: {
15
- CLAUDE_CODE_GIT_BASH_PATH: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}"
15
+ CLAUDE_CODE_GIT_BASH_PATH: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}"
16
16
  },
17
17
  message: {
18
18
  _: [
@@ -8,12 +8,12 @@ module.exports = {
8
8
  id: "run",
9
9
  method: "shell.run",
10
10
  params: {
11
- shell: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}",
11
+ shell: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
12
12
  conda: {
13
13
  skip: true
14
14
  },
15
15
  env: {
16
- CLAUDE_CODE_GIT_BASH_PATH: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}",
16
+ CLAUDE_CODE_GIT_BASH_PATH: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
17
17
  CLAUBBIT: "true"
18
18
  },
19
19
  message: {
@@ -7,7 +7,7 @@ module.exports = {
7
7
  id: "run",
8
8
  method: "shell.run",
9
9
  params: {
10
- shell: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}",
10
+ shell: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
11
11
  conda: {
12
12
  skip: true
13
13
  },
@@ -8,7 +8,7 @@ module.exports = {
8
8
  id: "run",
9
9
  method: "shell.run",
10
10
  params: {
11
- shell: "{{kernel.path('bin/miniconda/Library/bin/bash.exe')}}",
11
+ shell: "{{kernel.path('bin/miniforge/Library/bin/bash.exe')}}",
12
12
  conda: {
13
13
  skip: true
14
14
  },
@@ -0,0 +1,249 @@
1
+ const assert = require('node:assert/strict')
2
+ const fsModule = require('node:fs')
3
+ const fs = require('node:fs/promises')
4
+ const os = require('node:os')
5
+ const path = require('node:path')
6
+ const test = require('node:test')
7
+
8
+ const Conda = require('../kernel/bin/conda')
9
+ const {
10
+ PYTHON_INSTALL_SPEC,
11
+ WINDOWS_PYTHON_SSL_FIX_SPEC,
12
+ isExpectedPythonPinned,
13
+ } = require('../kernel/bin/conda-pins')
14
+
15
+ async function pathExists(target) {
16
+ return fs.access(target).then(() => true).catch(() => false)
17
+ }
18
+
19
+ function createKernel(root, platform = 'win32') {
20
+ return {
21
+ arch: 'x64',
22
+ homedir: root,
23
+ platform,
24
+ path: (...parts) => path.join(root, ...parts),
25
+ exists: async (...parts) => pathExists(path.join(root, ...parts)),
26
+ bin: {
27
+ correct_conda: true,
28
+ path: (...parts) => path.join(root, 'bin', ...parts),
29
+ exists: async (target) => pathExists(path.join(root, 'bin', target)),
30
+ },
31
+ }
32
+ }
33
+
34
+ async function createCondaRoot(root, name) {
35
+ const condaRoot = path.join(root, 'bin', name)
36
+ await fs.mkdir(path.join(condaRoot, 'Scripts'), { recursive: true })
37
+ await fs.writeFile(path.join(condaRoot, 'Scripts', 'conda.exe'), 'fake conda\n')
38
+ return condaRoot
39
+ }
40
+
41
+ async function createMiniforge(root) {
42
+ return createCondaRoot(root, 'miniforge')
43
+ }
44
+
45
+ async function createLegacyMiniconda(root) {
46
+ return createCondaRoot(root, 'miniconda')
47
+ }
48
+
49
+ function createConda(kernel) {
50
+ const conda = new Conda()
51
+ conda.kernel = kernel
52
+ return conda
53
+ }
54
+
55
+ test('Conda uses Miniforge assets and writes conda-forge-only config', async () => {
56
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-config-'))
57
+ const conda = createConda(createKernel(root))
58
+
59
+ assert.equal(
60
+ conda.urls.win32.x64,
61
+ 'https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-Windows-x86_64.exe'
62
+ )
63
+
64
+ await conda.init()
65
+ const condarc = await fs.readFile(path.join(root, 'condarc'), 'utf8')
66
+
67
+ assert.match(condarc, /^channels:\n - conda-forge\n/m)
68
+ assert.match(condarc, / - python=3\.10\.20\n/)
69
+ assert.match(condarc, /bin\/miniforge\/envs/)
70
+ assert.doesNotMatch(condarc, /bin\/miniconda\/envs/)
71
+ assert.doesNotMatch(condarc, /\bdefaults\b/)
72
+ assert.doesNotMatch(condarc, /auto_accept_tos/)
73
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniforge')), false)
74
+ })
75
+
76
+ test('Conda init writes activation hooks only after Miniforge exists', async () => {
77
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-init-hooks-'))
78
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
79
+ const conda = createConda(createKernel(root))
80
+
81
+ await conda.init()
82
+
83
+ assert.equal(
84
+ await pathExists(path.join(root, 'bin', 'miniforge', 'etc', 'conda', 'activate.d', 'zz_pinokio_unset_ssl_cert_dir-win.bat')),
85
+ true
86
+ )
87
+ })
88
+
89
+ test('Conda pins Python 3.10.20 consistently across platforms', async () => {
90
+ assert.equal(PYTHON_INSTALL_SPEC, 'python=3.10.20')
91
+ assert.equal(WINDOWS_PYTHON_SSL_FIX_SPEC, 'python=3.10.20=*_1_cpython')
92
+
93
+ assert.equal(isExpectedPythonPinned('darwin', '3.10.20', 'cpython'), true)
94
+ assert.equal(isExpectedPythonPinned('linux', '3.10.20', 'cpython'), true)
95
+ assert.equal(isExpectedPythonPinned('darwin', '3.10.21', 'cpython'), false)
96
+ assert.equal(isExpectedPythonPinned('darwin', '3.11.0', 'cpython'), false)
97
+ assert.equal(isExpectedPythonPinned('win32', '3.10.20', 'h4de0772_0_cpython'), false)
98
+ assert.equal(isExpectedPythonPinned('win32', '3.10.20', 'h4de0772_1_cpython'), true)
99
+ })
100
+
101
+ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () => {
102
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-pin-darwin-'))
103
+ const kernel = createKernel(root, 'darwin')
104
+ const calls = []
105
+
106
+ kernel.bin.mods = []
107
+ kernel.bin.download = async () => {}
108
+ kernel.bin.rm = async () => {}
109
+ kernel.bin.exec = async (payload) => {
110
+ calls.push(payload)
111
+ if (payload && payload.conda && payload.conda.skip) {
112
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
113
+ }
114
+ }
115
+
116
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
117
+
118
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
119
+ const condaInstall = calls.find((call) => Array.isArray(call.message))
120
+ assert.ok(condaInstall)
121
+ assert.equal(
122
+ condaInstall.message[1],
123
+ 'conda install -y --override-channels -c conda-forge "python=3.10.20" "sqlite=3.47.2" "conda-libmamba-solver>=25.4.0"'
124
+ )
125
+ })
126
+
127
+ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async () => {
128
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-pin-win32-'))
129
+ const kernel = createKernel(root, 'win32')
130
+ const calls = []
131
+
132
+ kernel.bin.mods = []
133
+ kernel.bin.download = async () => {}
134
+ kernel.bin.rm = async () => {}
135
+ kernel.bin.exec = async (payload) => {
136
+ calls.push(payload)
137
+ if (payload && payload.conda && payload.conda.skip) {
138
+ const miniforge = path.join(root, 'bin', 'miniforge')
139
+ await fs.mkdir(path.join(miniforge, 'conda-meta'), { recursive: true })
140
+ await fs.writeFile(path.join(miniforge, 'python.exe'), 'fake python\n')
141
+ }
142
+ }
143
+
144
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
145
+
146
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
147
+ const condaInstall = calls.find((call) => Array.isArray(call.message))
148
+ assert.ok(condaInstall)
149
+ assert.equal(
150
+ condaInstall.message[1],
151
+ 'conda install -y --override-channels -c conda-forge "python=3.10.20=*_1_cpython" "sqlite=3.53.2" "conda-libmamba-solver>=25.4.0"'
152
+ )
153
+ })
154
+
155
+ test('Conda install replaces legacy miniconda with a compatibility alias to miniforge', async () => {
156
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-alias-'))
157
+ const legacy = await createLegacyMiniconda(root)
158
+ await fs.writeFile(path.join(legacy, 'legacy.txt'), 'old runtime\n')
159
+ const kernel = createKernel(root, 'darwin')
160
+
161
+ kernel.bin.mods = []
162
+ kernel.bin.download = async () => {}
163
+ kernel.bin.rm = async () => {}
164
+ kernel.bin.exec = async (payload) => {
165
+ if (payload && payload.conda && payload.conda.skip) {
166
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
167
+ }
168
+ }
169
+
170
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
171
+
172
+ const miniforge = path.join(root, 'bin', 'miniforge')
173
+ const miniconda = path.join(root, 'bin', 'miniconda')
174
+ assert.equal(await pathExists(path.join(miniconda, 'legacy.txt')), false)
175
+ assert.equal(await fs.realpath(miniconda), await fs.realpath(miniforge))
176
+ })
177
+
178
+ test('Conda install removes legacy miniconda symlinks without deleting their target', async () => {
179
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-legacy-symlink-'))
180
+ const external = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-external-'))
181
+ await fs.writeFile(path.join(external, 'keep.txt'), 'do not delete\n')
182
+ await fs.mkdir(path.join(root, 'bin'), { recursive: true })
183
+ await fs.symlink(external, path.join(root, 'bin', 'miniconda'), 'dir')
184
+ const kernel = createKernel(root, 'darwin')
185
+
186
+ kernel.bin.mods = []
187
+ kernel.bin.download = async () => {}
188
+ kernel.bin.rm = async () => {}
189
+ kernel.bin.exec = async (payload) => {
190
+ if (payload && payload.conda && payload.conda.skip) {
191
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
192
+ }
193
+ }
194
+
195
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
196
+
197
+ assert.equal(await pathExists(path.join(external, 'keep.txt')), true)
198
+ assert.equal(await fs.realpath(path.join(root, 'bin', 'miniconda')), await fs.realpath(path.join(root, 'bin', 'miniforge')))
199
+ })
200
+
201
+ test('Conda installed stays false when metadata check already marked Conda invalid', async () => {
202
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-metadata-invalid-'))
203
+ await createMiniforge(root)
204
+ const kernel = createKernel(root)
205
+ kernel.bin.correct_conda = false
206
+
207
+ assert.equal(await createConda(kernel).installed(), false)
208
+ })
209
+
210
+ test('Conda install keeps the old runtime when the replacement installer download fails', async () => {
211
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-download-fails-'))
212
+ const miniconda = await createLegacyMiniconda(root)
213
+ const kernel = createKernel(root)
214
+
215
+ kernel.bin.download = async () => {
216
+ throw new Error('download failed')
217
+ }
218
+
219
+ await assert.rejects(
220
+ createConda(kernel)._install({ dependencies: [] }, () => {}),
221
+ /download failed/
222
+ )
223
+ assert.equal(await pathExists(miniconda), true)
224
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniforge')), false)
225
+ })
226
+
227
+ test('Conda install reports manual recovery when the old runtime cannot be removed', async () => {
228
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-runtime-locked-'))
229
+ await createLegacyMiniconda(root)
230
+ const kernel = createKernel(root)
231
+ kernel.bin.download = async () => {}
232
+ const conda = createConda(kernel)
233
+ const originalRm = fsModule.promises.rm
234
+
235
+ fsModule.promises.rm = async () => {
236
+ const error = new Error('resource busy')
237
+ error.code = 'EBUSY'
238
+ throw error
239
+ }
240
+
241
+ try {
242
+ await assert.rejects(
243
+ conda._install({ dependencies: [] }, () => {}),
244
+ /delete that folder manually/
245
+ )
246
+ } finally {
247
+ fsModule.promises.rm = originalRm
248
+ }
249
+ })
@@ -1,133 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
-
4
- const LEGACY_FFMPEG_HOOK_NAMES = [
5
- 'zz_pinokio_ffmpeg.sh',
6
- 'zz_pinokio_ffmpeg.bat',
7
- 'zz_pinokio_ffmpeg.ps1',
8
- ]
9
-
10
- const REMOVE_RETRY_COUNT = 5
11
- const REMOVE_RETRY_DELAY_MS = 100
12
-
13
- async function pathExists(target) {
14
- return fs.promises.access(target).then(() => true).catch(() => false)
15
- }
16
-
17
- function legacyFfmpegRuntimePaths(kernel) {
18
- if (!kernel || !kernel.bin || typeof kernel.bin.path !== 'function') {
19
- return []
20
- }
21
- return [
22
- kernel.bin.path('ffmpeg-env'),
23
- kernel.bin.path('ffmpeg-pkgs'),
24
- kernel.bin.path('ffmpeg'),
25
- kernel.bin.path('ffmpeg-tmp'),
26
- ]
27
- }
28
-
29
- function legacyFfmpegHookFiles(kernel) {
30
- if (!kernel || !kernel.bin || typeof kernel.bin.path !== 'function') {
31
- return []
32
- }
33
- const dirs = [
34
- kernel.bin.path('miniconda', 'etc', 'conda', 'activate.d'),
35
- kernel.bin.path('miniconda', 'etc', 'conda', 'deactivate.d'),
36
- ]
37
- return dirs.flatMap((dir) => LEGACY_FFMPEG_HOOK_NAMES.map((name) => path.resolve(dir, name)))
38
- }
39
-
40
- function manualFfmpegHookCleanupMessage(failures) {
41
- const files = failures.map((failure) => failure.target)
42
- return [
43
- 'Pinokio could not remove old FFmpeg Conda activation hooks.',
44
- '',
45
- 'These old hooks can break the base Conda environment by adding the removed ffmpeg-env runtime to PATH.',
46
- '',
47
- 'Close all Pinokio windows and any terminals using Pinokio, then delete these files if they exist:',
48
- ...files.map((file) => `- ${file}`),
49
- '',
50
- 'After deleting them, reopen Pinokio.',
51
- ].join('\n')
52
- }
53
-
54
- class LegacyFfmpegCleanupError extends Error {
55
- constructor(failures) {
56
- super(manualFfmpegHookCleanupMessage(failures))
57
- this.name = 'LegacyFfmpegCleanupError'
58
- this.failures = failures
59
- }
60
- }
61
-
62
- async function hasAnyPath(paths) {
63
- for (const target of paths) {
64
- if (await pathExists(target)) {
65
- return true
66
- }
67
- }
68
- return false
69
- }
70
-
71
- async function hasCleanupTargets(kernel) {
72
- return await hasAnyPath(legacyFfmpegHookFiles(kernel)) ||
73
- await hasAnyPath(legacyFfmpegRuntimePaths(kernel))
74
- }
75
-
76
- async function removeLegacyPath(target, ondata) {
77
- const existed = await pathExists(target)
78
- if (!existed) {
79
- return { target, found: false, removed: false }
80
- }
81
-
82
- try {
83
- await fs.promises.rm(target, {
84
- recursive: true,
85
- force: true,
86
- maxRetries: REMOVE_RETRY_COUNT,
87
- retryDelay: REMOVE_RETRY_DELAY_MS,
88
- })
89
- } catch (error) {
90
- if (ondata) {
91
- const reason = error && (error.code || error.message) ? error.code || error.message : String(error)
92
- ondata({ raw: `could not remove legacy path: ${target} (${reason})\r\n` })
93
- }
94
- return { target, found: true, removed: false, error }
95
- }
96
-
97
- if (ondata) {
98
- ondata({ raw: `removed legacy path: ${target}\r\n` })
99
- }
100
- return { target, found: true, removed: true }
101
- }
102
-
103
- async function run(kernel, ondata) {
104
- const results = []
105
- const hookFailures = []
106
-
107
- for (const target of legacyFfmpegHookFiles(kernel)) {
108
- const result = await removeLegacyPath(target, ondata)
109
- results.push(result)
110
- if (result.found && !result.removed) {
111
- hookFailures.push(result)
112
- }
113
- }
114
-
115
- if (hookFailures.length > 0) {
116
- throw new LegacyFfmpegCleanupError(hookFailures)
117
- }
118
-
119
- for (const target of legacyFfmpegRuntimePaths(kernel)) {
120
- results.push(await removeLegacyPath(target, ondata))
121
- }
122
-
123
- return results
124
- }
125
-
126
- module.exports = {
127
- LegacyFfmpegCleanupError,
128
- hasCleanupTargets,
129
- legacyFfmpegHookFiles,
130
- legacyFfmpegRuntimePaths,
131
- name: 'ffmpeg',
132
- run,
133
- }