pinokiod 7.4.3 → 7.5.0

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.
@@ -5,7 +5,7 @@ const { glob } = require('glob')
5
5
  class Cuda {
6
6
  description = "Installs CUDA and cuDNN libraries for NVIDIA GPU workloads."
7
7
  async hasNvTargetHeader() {
8
- const prefix = this.kernel.bin.path("miniconda")
8
+ const prefix = this.kernel.bin.path("miniforge")
9
9
  const patterns = [
10
10
  "Library/include/nv/target",
11
11
  "include/nv/target",
@@ -33,7 +33,7 @@ class Cuda {
33
33
  if (this.kernel.platform !== "win32") {
34
34
  return
35
35
  }
36
- const script = this.kernel.bin.path("miniconda/etc/conda/activate.d/pinokio/~cuda-nvcc_activate.bat")
36
+ const script = this.kernel.bin.path("miniforge/etc/conda/activate.d/pinokio/~cuda-nvcc_activate.bat")
37
37
  let content
38
38
  try {
39
39
  content = await fs.promises.readFile(script, "utf8")
@@ -68,7 +68,7 @@ class Cuda {
68
68
  if (this.kernel.platform !== "win32") {
69
69
  return
70
70
  }
71
- const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
71
+ const folder = this.kernel.bin.path("miniforge/etc/conda/activate.d")
72
72
  const stash = path.resolve(folder, "pinokio")
73
73
  await fs.promises.mkdir(stash, { recursive: true }).catch(() => {})
74
74
  const scripts = [
@@ -149,10 +149,10 @@ class Cuda {
149
149
  "vs2019_compiler_vars.bat",
150
150
  "vs2022_compiler_vars.bat",
151
151
  ]
152
- const folder = this.kernel.bin.path("miniconda/etc/conda/activate.d")
152
+ const folder = this.kernel.bin.path("miniforge/etc/conda/activate.d")
153
153
  let at_least_one_exists = false
154
154
  for(let item of deactivate_list) {
155
- let exists = await this.kernel.exists("bin/miniconda/etc/conda/activate.d/" + item)
155
+ let exists = await this.kernel.exists("bin/miniforge/etc/conda/activate.d/" + item)
156
156
  if (exists) {
157
157
  // break if at least one exists
158
158
  at_least_one_exists = true
@@ -190,11 +190,11 @@ class Cuda {
190
190
  env() {
191
191
  if (this.kernel.platform === 'win32') {
192
192
  return {
193
- CUDA_HOME: this.kernel.bin.path("miniconda/Library")
193
+ CUDA_HOME: this.kernel.bin.path("miniforge/Library")
194
194
  }
195
195
  } else {
196
196
  return {
197
- CUDA_HOME: this.kernel.bin.path("miniconda")
197
+ CUDA_HOME: this.kernel.bin.path("miniforge")
198
198
  }
199
199
  }
200
200
  }
@@ -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.0",
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) {
@@ -6013,7 +6013,7 @@ class Server {
6013
6013
  this.kernel.store.set("version", this.version.pinokiod)
6014
6014
  console.log("[DONE] Updating to the new version")
6015
6015
  console.log("not up to date. update py.")
6016
- // remove ~/bin/miniconda/py
6016
+ // remove ~/bin/py
6017
6017
  let p = path.resolve(home, "bin/py")
6018
6018
  console.log(`[TRY] reset ${p}`)
6019
6019
  await fse.remove(p)
@@ -10034,7 +10034,6 @@ class Server {
10034
10034
  return
10035
10035
  }
10036
10036
  // check bin folder
10037
- // let bin_path = this.kernel.path("bin/miniconda")
10038
10037
  // let bin_exists = await this.exists(bin_path)
10039
10038
  // if (!bin_exists) {
10040
10039
  // res.redirect("/setup")
@@ -11730,7 +11729,7 @@ class Server {
11730
11729
  managedLaunchOverrides.conda = { skip: true }
11731
11730
  }
11732
11731
  if (isWindowsPlatform && (provider.key === "codex" || provider.key === "claude")) {
11733
- const gitBashPath = this.kernel.path("bin/miniconda/Library/bin/bash.exe")
11732
+ const gitBashPath = this.kernel.path("bin/miniforge/Library/bin/bash.exe")
11734
11733
  const bashExists = await fs.promises.access(gitBashPath, fs.constants.F_OK).then(() => true).catch(() => false)
11735
11734
  if (bashExists) {
11736
11735
  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,254 @@
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
+ async function writeCurrentRuntimeStamp(root) {
50
+ await fs.writeFile(
51
+ path.join(root, 'bin', 'miniforge', '.pinokio-runtime.json'),
52
+ `${JSON.stringify({
53
+ distribution: 'miniforge',
54
+ release: '26.3.2-3',
55
+ epoch: 1,
56
+ }, null, 2)}\n`
57
+ )
58
+ }
59
+
60
+ function createConda(kernel) {
61
+ const conda = new Conda()
62
+ conda.kernel = kernel
63
+ return conda
64
+ }
65
+
66
+ test('Conda uses Miniforge assets and writes conda-forge-only config', async () => {
67
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-config-'))
68
+ const conda = createConda(createKernel(root))
69
+
70
+ assert.equal(
71
+ conda.urls.win32.x64,
72
+ 'https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-Windows-x86_64.exe'
73
+ )
74
+
75
+ await conda.init()
76
+ const condarc = await fs.readFile(path.join(root, 'condarc'), 'utf8')
77
+
78
+ assert.match(condarc, /^channels:\n - conda-forge\n/m)
79
+ assert.match(condarc, / - python=3\.10\.20\n/)
80
+ assert.match(condarc, /bin\/miniforge\/envs/)
81
+ assert.doesNotMatch(condarc, /bin\/miniconda\/envs/)
82
+ assert.doesNotMatch(condarc, /\bdefaults\b/)
83
+ assert.doesNotMatch(condarc, /auto_accept_tos/)
84
+ })
85
+
86
+ test('Conda pins Python 3.10.20 consistently across platforms', async () => {
87
+ assert.equal(PYTHON_INSTALL_SPEC, 'python=3.10.20')
88
+ assert.equal(WINDOWS_PYTHON_SSL_FIX_SPEC, 'python=3.10.20=*_1_cpython')
89
+
90
+ assert.equal(isExpectedPythonPinned('darwin', '3.10.20', 'cpython'), true)
91
+ assert.equal(isExpectedPythonPinned('linux', '3.10.20', 'cpython'), true)
92
+ assert.equal(isExpectedPythonPinned('darwin', '3.10.21', 'cpython'), false)
93
+ assert.equal(isExpectedPythonPinned('darwin', '3.11.0', 'cpython'), false)
94
+ assert.equal(isExpectedPythonPinned('win32', '3.10.20', 'h4de0772_0_cpython'), false)
95
+ assert.equal(isExpectedPythonPinned('win32', '3.10.20', 'h4de0772_1_cpython'), true)
96
+ })
97
+
98
+ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () => {
99
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-pin-darwin-'))
100
+ const kernel = createKernel(root, 'darwin')
101
+ const calls = []
102
+
103
+ kernel.bin.mods = []
104
+ kernel.bin.download = async () => {}
105
+ kernel.bin.rm = async () => {}
106
+ kernel.bin.exec = async (payload) => {
107
+ calls.push(payload)
108
+ if (payload && payload.conda && payload.conda.skip) {
109
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
110
+ }
111
+ }
112
+
113
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
114
+
115
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
116
+ const condaInstall = calls.find((call) => Array.isArray(call.message))
117
+ assert.ok(condaInstall)
118
+ assert.equal(
119
+ condaInstall.message[1],
120
+ 'conda install -y --override-channels -c conda-forge "python=3.10.20" "sqlite=3.47.2" "conda-libmamba-solver>=25.4.0"'
121
+ )
122
+ })
123
+
124
+ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async () => {
125
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-pin-win32-'))
126
+ const kernel = createKernel(root, 'win32')
127
+ const calls = []
128
+
129
+ kernel.bin.mods = []
130
+ kernel.bin.download = async () => {}
131
+ kernel.bin.rm = async () => {}
132
+ kernel.bin.exec = async (payload) => {
133
+ calls.push(payload)
134
+ if (payload && payload.conda && payload.conda.skip) {
135
+ const miniforge = path.join(root, 'bin', 'miniforge')
136
+ await fs.mkdir(path.join(miniforge, 'conda-meta'), { recursive: true })
137
+ await fs.writeFile(path.join(miniforge, 'python.exe'), 'fake python\n')
138
+ }
139
+ }
140
+
141
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
142
+
143
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
144
+ const condaInstall = calls.find((call) => Array.isArray(call.message))
145
+ assert.ok(condaInstall)
146
+ assert.equal(
147
+ condaInstall.message[1],
148
+ 'conda install -y --override-channels -c conda-forge "python=3.10.20=*_1_cpython" "sqlite=3.53.2" "conda-libmamba-solver>=25.4.0"'
149
+ )
150
+ })
151
+
152
+ test('Conda install replaces legacy miniconda with a compatibility alias to miniforge', async () => {
153
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-alias-'))
154
+ const legacy = await createLegacyMiniconda(root)
155
+ await fs.writeFile(path.join(legacy, 'legacy.txt'), 'old runtime\n')
156
+ const kernel = createKernel(root, 'darwin')
157
+
158
+ kernel.bin.mods = []
159
+ kernel.bin.download = async () => {}
160
+ kernel.bin.rm = async () => {}
161
+ kernel.bin.exec = async (payload) => {
162
+ if (payload && payload.conda && payload.conda.skip) {
163
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
164
+ }
165
+ }
166
+
167
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
168
+
169
+ const miniforge = path.join(root, 'bin', 'miniforge')
170
+ const miniconda = path.join(root, 'bin', 'miniconda')
171
+ assert.equal(await pathExists(path.join(miniconda, 'legacy.txt')), false)
172
+ assert.equal(await fs.realpath(miniconda), await fs.realpath(miniforge))
173
+ })
174
+
175
+ test('Conda install removes legacy miniconda symlinks without deleting their target', async () => {
176
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-legacy-symlink-'))
177
+ const external = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-external-'))
178
+ await fs.writeFile(path.join(external, 'keep.txt'), 'do not delete\n')
179
+ await fs.mkdir(path.join(root, 'bin'), { recursive: true })
180
+ await fs.symlink(external, path.join(root, 'bin', 'miniconda'), 'dir')
181
+ const kernel = createKernel(root, 'darwin')
182
+
183
+ kernel.bin.mods = []
184
+ kernel.bin.download = async () => {}
185
+ kernel.bin.rm = async () => {}
186
+ kernel.bin.exec = async (payload) => {
187
+ if (payload && payload.conda && payload.conda.skip) {
188
+ await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
189
+ }
190
+ }
191
+
192
+ await createConda(kernel)._install({ dependencies: [] }, () => {})
193
+
194
+ assert.equal(await pathExists(path.join(external, 'keep.txt')), true)
195
+ assert.equal(await fs.realpath(path.join(root, 'bin', 'miniconda')), await fs.realpath(path.join(root, 'bin', 'miniforge')))
196
+ })
197
+
198
+ test('Conda installed stays false when metadata check already marked Conda invalid', async () => {
199
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-metadata-invalid-'))
200
+ await createMiniforge(root)
201
+ const kernel = createKernel(root)
202
+ kernel.bin.correct_conda = false
203
+
204
+ assert.equal(await createConda(kernel).installed(), false)
205
+ })
206
+
207
+ test('Conda installed returns false for old Conda runtimes without the Miniforge stamp', async () => {
208
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-runtime-stale-'))
209
+ await createMiniforge(root)
210
+ const kernel = createKernel(root)
211
+
212
+ assert.equal(await createConda(kernel).installed(), false)
213
+ })
214
+
215
+ test('Conda install keeps the old runtime when the replacement installer download fails', async () => {
216
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-download-fails-'))
217
+ const miniconda = await createLegacyMiniconda(root)
218
+ const kernel = createKernel(root)
219
+
220
+ kernel.bin.download = async () => {
221
+ throw new Error('download failed')
222
+ }
223
+
224
+ await assert.rejects(
225
+ createConda(kernel)._install({ dependencies: [] }, () => {}),
226
+ /download failed/
227
+ )
228
+ assert.equal(await pathExists(miniconda), true)
229
+ assert.equal(await pathExists(path.join(root, 'bin', 'miniforge')), false)
230
+ })
231
+
232
+ test('Conda install reports manual recovery when the old runtime cannot be removed', async () => {
233
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-runtime-locked-'))
234
+ await createLegacyMiniconda(root)
235
+ const kernel = createKernel(root)
236
+ kernel.bin.download = async () => {}
237
+ const conda = createConda(kernel)
238
+ const originalRm = fsModule.promises.rm
239
+
240
+ fsModule.promises.rm = async () => {
241
+ const error = new Error('resource busy')
242
+ error.code = 'EBUSY'
243
+ throw error
244
+ }
245
+
246
+ try {
247
+ await assert.rejects(
248
+ conda._install({ dependencies: [] }, () => {}),
249
+ /delete that folder manually/
250
+ )
251
+ } finally {
252
+ fsModule.promises.rm = originalRm
253
+ }
254
+ })