pinokiod 8.2.7 → 8.2.8

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.
@@ -113,10 +113,10 @@ function resolveCondaBinary(minicondaPath, platform = process.platform, allowFal
113
113
  async function hasSecureManagedPip(prefix, platform = process.platform) {
114
114
  const python = path.join(prefix, platform === 'win32' ? 'python.exe' : 'bin/python')
115
115
  return await new Promise(resolve => {
116
- execFile(python, ['-I', '-c', 'from importlib.metadata import version; print(version("pip"))'], {
116
+ execFile(python, ['-I', '-m', 'pip', '--version'], {
117
117
  env: managedPythonEnv(), windowsHide: true, timeout: 15000,
118
118
  }, (error, stdout) => {
119
- const version = !error && semver.valid(String(stdout).trim())
119
+ const version = !error && semver.coerce(String(stdout).trim().match(/^pip\s+(\S+)/)?.[1] || '')
120
120
  resolve(!!version && semver.gte(version, PIP_MIN_VERSION))
121
121
  })
122
122
  })
@@ -10,8 +10,7 @@ const TLS_PACKAGE_MINIMUMS = {
10
10
  // conda-forge's patched pip requires Python 3.11; this official wheel supports 3.10.
11
11
  // Bootstrap URL and hash: https://discuss.python.org/t/109576
12
12
  const PIP_MIN_VERSION = '26.2.2'
13
- const PIP_INSTALL_SPEC = 'pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70'
14
- const PIP_INSTALL_COMMAND = `python -I -m pip --isolated install --no-index "${PIP_INSTALL_SPEC}"`
13
+ const PIP_INSTALL_COMMAND = 'python -I -m pip --isolated install --no-index "pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70"'
15
14
  const TLS_PACKAGE_SPECS = Object.entries(TLS_PACKAGE_MINIMUMS).map(([name, version]) => `${name}>=${version}`)
16
15
  const hasSecureTlsPackages = (versions = {}) => Object.entries(TLS_PACKAGE_MINIMUMS).every(([name, minimum]) => {
17
16
  const version = semver.coerce(versions[name])
@@ -2,8 +2,6 @@ const fs = require('fs')
2
2
  const { glob } = require('glob')
3
3
  const semver = require('semver')
4
4
  const { createHash } = require('crypto')
5
- const { execFile } = require('child_process')
6
- const { managedPythonEnv } = require('../python_env')
7
5
  const { isPathRemovalBlockedError, removePath } = require('../path_removal')
8
6
  const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
9
7
  const {
@@ -181,18 +179,11 @@ report_errors: false`)
181
179
  await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)) {
182
180
  // Refresh a working runtime in place, preserving named app environments.
183
181
  await this.init()
184
- const bootstrap = await this.kernel.bin.exec({
185
- message: PIP_INSTALL_COMMAND,
186
- }, ondata)
187
- if (bootstrap?.error?.length || !(await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform))) throw new Error("Conda certificate bootstrap failed; existing environments were preserved.")
188
- const python = this.kernel.bin.path(CONDA_ROOT_DIR, this.kernel.platform === 'win32' ? 'python.exe' : 'bin/python')
189
- const caFile = await new Promise((resolve, reject) => execFile(python,
190
- ['-I', '-c', 'from pip._vendor import certifi; print(certifi.where())'],
191
- { env: managedPythonEnv(), windowsHide: true, timeout: 15000 },
192
- (error, stdout) => error ? reject(error) : resolve(stdout.trim())))
193
182
  const result = await this.kernel.bin.exec({
194
- message: [`conda install -y --override-channels -c conda-forge "${this.kernel.platform === "win32" ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC}" "conda-libmamba-solver>=25.4.0" pip ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(" ")}`, PIP_INSTALL_COMMAND],
195
- env: { CONDA_SSL_VERIFY: caFile },
183
+ message: [
184
+ `conda install -y --override-channels -c conda-forge "${this.kernel.platform === "win32" ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC}" "conda-libmamba-solver>=25.4.0" ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(" ")}`,
185
+ PIP_INSTALL_COMMAND,
186
+ ],
196
187
  }, ondata)
197
188
  if (result?.error?.length || !(await this.check())) throw new Error("Conda certificate update failed; existing environments were preserved.")
198
189
  return
@@ -281,7 +272,6 @@ report_errors: false`)
281
272
  `"conda=${CONDA_PIN_VERSION}"`,
282
273
  this.kernel.platform === "win32" ? `"${WINDOWS_PYTHON_SSL_FIX_SPEC}"` : `"${PYTHON_INSTALL_SPEC}"`,
283
274
  `"conda-libmamba-solver>=25.4.0"`,
284
- "pip",
285
275
  ...TLS_PACKAGE_SPECS.map(spec => `"${spec}"`),
286
276
  ]
287
277
 
@@ -290,7 +280,7 @@ report_errors: false`)
290
280
  `conda install -y --override-channels -c conda-forge ${condaPackages.join(" ")} ${mods}`.trim(),
291
281
  PIP_INSTALL_COMMAND,
292
282
  ]
293
- const result = await this.kernel.bin.exec({
283
+ await this.kernel.bin.exec({
294
284
  message: cmds,
295
285
  env: {
296
286
  PIP_REQUIRE_VIRTUALENV: "false"
@@ -298,7 +288,6 @@ report_errors: false`)
298
288
  }, (stream) => {
299
289
  ondata(stream)
300
290
  })
301
- if (result?.error?.length) throw new Error("Conda package or pip installation failed.")
302
291
  if (this.kernel.platform === "win32") {
303
292
  // copy python.exe to python3.exe so you can run with both python3 and python
304
293
  await fs.promises.copyFile(
package/kernel/shell.js CHANGED
@@ -21,7 +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, PIP_MIN_VERSION } = require('./bin/conda-pins')
24
+ const { PYTHON_INSTALL_SPEC, PIP_INSTALL_COMMAND } = require('./bin/conda-pins')
25
25
  const CondaRuntimeGuard = require('./shell_conda_runtime_guard')
26
26
  const home = os.homedir()
27
27
  const WINDOWS_CMD_CONDA_COMMAND_PATTERN = /(?:^|[&|()]\s*)@?\s*(?:call\s+)?(?:"[^"]*[\\/]conda(?:\.bat|\.exe)?"|(?:[a-z]:)?[^&|()\s"]*[\\/]conda(?:\.bat|\.exe)?|conda(?:\.bat|\.exe)?)\s+/i
@@ -1393,20 +1393,17 @@ class Shell {
1393
1393
  } else {
1394
1394
  // when python version is not specified, use the default python -m venv
1395
1395
  venv_activation = [
1396
- `python -m venv ${shellEnvPath}`,
1396
+ this.isPowerShell() ? `python -m venv ${shellEnvPath}; if ($LASTEXITCODE -ne 0) { exit 1 }` : `python -m venv ${shellEnvPath} || exit 1`,
1397
1397
  activate_command,
1398
1398
  // `python -m pip install --upgrade pip setuptools wheel`,
1399
1399
  deactivate_path,
1400
1400
  // timeout,
1401
1401
  activate_command,
1402
1402
  // timeout,
1403
+ this.isPowerShell() ? `${PIP_INSTALL_COMMAND}; if ($LASTEXITCODE -ne 0) { exit 1 }` : `${PIP_INSTALL_COMMAND} || exit 1`,
1403
1404
  ]
1404
1405
  }
1405
1406
  }
1406
- if (!use_uv) {
1407
- const updatePip = `uv pip install --exclude-newer-package pip=false "pip>=${PIP_MIN_VERSION}"`
1408
- venv_activation.push(this.isPowerShell() ? `${updatePip}; if ($LASTEXITCODE -ne 0) { exit 1 }` : `${updatePip} || exit 1`)
1409
- }
1410
1407
  } else {
1411
1408
  venv_activation = []
1412
1409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.7",
3
+ "version": "8.2.8",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -16,20 +16,17 @@ const {
16
16
  isExpectedPythonPinned,
17
17
  TLS_PACKAGE_SPECS,
18
18
  PIP_MIN_VERSION,
19
+ PIP_INSTALL_COMMAND,
19
20
  } = require('../kernel/bin/conda-pins')
20
21
 
21
22
  test.beforeEach(t => {
22
23
  const realExecFile = childProcess.execFile.bind(childProcess)
23
24
  t.mock.method(childProcess, 'execFile', (binary, args, options, done) => {
24
- if (args[0] !== '-I' || args[1] !== '-c') return realExecFile(binary, args, options, done)
25
+ if (args[0] !== '-I') return realExecFile(binary, args, options, done)
25
26
  const prefix = path.basename(binary) === 'python.exe' ? path.dirname(binary) : path.dirname(path.dirname(binary))
26
- if (args[2].includes('certifi.where()')) {
27
- assert.match(args[2], /^from pip\._vendor import certifi;/)
28
- done(null, path.join(prefix, 'certificates.pem') + '\n')
29
- return
30
- }
27
+ assert.deepEqual(args, ['-I', '-m', 'pip', '--version'])
31
28
  fs.readFile(path.join(prefix, 'test-pip-version'), 'utf8').then(
32
- version => done(null, version), error => done(error, '')
29
+ version => done(null, `pip ${version.trim()} from ${prefix} (python 3.10)`), error => done(error, '')
33
30
  )
34
31
  })
35
32
  for (const file of ['../kernel/bin/conda-meta', '../kernel/bin/conda', '../kernel/bin']) {
@@ -281,10 +278,10 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
281
278
  assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
282
279
  const condaInstall = calls.find((call) => Array.isArray(call.message))
283
280
  assert.ok(condaInstall)
284
- assert.match(condaInstall.message[2], /^python -I -m pip --isolated install --no-index "pip @ https:\/\/files\.pythonhosted\.org\/packages\/[a-f0-9/]+\/pip-26\.2\.2-py3-none-any\.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70"$/)
281
+ assert.equal(condaInstall.message[2], PIP_INSTALL_COMMAND)
285
282
  assert.equal(
286
283
  condaInstall.message[1],
287
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
284
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
288
285
  )
289
286
  assert.equal(
290
287
  await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
@@ -316,7 +313,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
316
313
  assert.ok(condaInstall)
317
314
  assert.equal(
318
315
  condaInstall.message[1],
319
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
316
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
320
317
  )
321
318
  })
322
319
 
@@ -348,7 +345,7 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
348
345
  assert.ok(condaInstall)
349
346
  assert.equal(
350
347
  condaInstall.message[1],
351
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19" uv=0.11.23 nodejs=22.21.1 pnpm'
348
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19" uv=0.11.23 nodejs=22.21.1 pnpm'
352
349
  )
353
350
  })
354
351
 
@@ -629,78 +626,131 @@ test('stale certificate packages trigger the existing Conda setup checks', async
629
626
  assert.equal(bin.correct_conda, false)
630
627
  })
631
628
 
632
- test('tampered Miniforge installers leave the existing runtime untouched', async t => {
629
+ test('tampered Miniforge installers stop immediately and leave the runtime untouched', async t => {
633
630
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-hash-'))
634
631
  t.after(() => fs.rm(root, { recursive: true, force: true }))
635
632
  await createMiniforge(root)
636
633
  const kernel = createKernel(root, 'darwin')
637
- kernel.bin.download = async () => fs.writeFile(path.join(root, 'bin/installer.sh'), 'tampered')
634
+ let downloads = 0
635
+ kernel.bin.download = async () => {
636
+ downloads++
637
+ await fs.writeFile(path.join(root, 'bin/installer.sh'), 'tampered')
638
+ }
639
+ kernel.bin.mod = { caddy: { running: () => assert.fail('must verify before stopping services') } }
638
640
  const conda = new Conda()
639
641
  conda.kernel = kernel
640
- await assert.rejects(conda._install({}, () => {}), /SHA-256 verification failed/)
642
+ await assert.rejects(conda.install({}, () => {}), /SHA-256 verification failed/)
643
+ assert.equal(downloads, 1)
641
644
  assert.equal(await pathExists(path.join(root, 'bin/miniforge/Scripts/conda.exe')), true)
642
645
  })
643
646
 
644
-
645
- test('old pip cannot pass Conda readiness even with current CA packages', async t => {
647
+ test('old or missing pip cannot pass readiness even with current CA packages', async t => {
646
648
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-ready-'))
647
649
  t.after(() => fs.rm(root, { recursive: true, force: true }))
648
650
  await writeHealthyCondaMeta(root, 'darwin')
649
651
  await writeFakeManagedConda(root, 'darwin')
650
- await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '26.2.1')
651
- assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
652
- const bin = createBin(root, 'darwin')
653
- await bin.tryList()
654
- assert.equal(bin.correct_conda, false)
652
+ for (const version of ['26.2.1', null]) {
653
+ const pip = path.join(root, 'bin/miniforge/test-pip-version')
654
+ if (version) await fs.writeFile(pip, version)
655
+ else await fs.rm(pip)
656
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
657
+ const bin = createBin(root, 'darwin')
658
+ await bin.tryList()
659
+ assert.equal(bin.correct_conda, false)
660
+ }
655
661
  })
656
662
 
657
- test("certificate refresh preserves named environments and uses pip's bundled CA", async t => {
663
+ test('certificate refresh preserves named environments and existing configuration', async t => {
658
664
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-refresh-'))
659
665
  t.after(() => fs.rm(root, { recursive: true, force: true }))
660
- await writeHealthyCondaMeta(root, 'darwin')
661
- await writeFakeManagedConda(root, 'darwin')
662
- const marker = path.join(root, 'bin/miniforge/envs/app/keep')
663
- await fs.mkdir(path.dirname(marker), { recursive: true })
664
- await fs.writeFile(marker, 'existing app')
665
- const kernel = createKernel(root, 'darwin')
666
- const calls = []
667
- kernel.bin.download = async () => assert.fail('must not replace a working runtime')
668
- kernel.bin.exec = async payload => { calls.push(payload); return {} }
669
- await createConda(kernel).install({}, () => {})
670
- assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
671
- assert.equal(calls.length, 2)
672
- assert.match(calls[0].message, /^python -I -m pip --isolated install --no-index /)
673
- assert.equal(calls[1].env.CONDA_SSL_VERIFY, path.join(root, 'bin/miniforge/certificates.pem'))
666
+ for (const platform of ['win32', 'darwin', 'linux']) {
667
+ const dir = path.join(root, platform)
668
+ await writeHealthyCondaMeta(dir, platform)
669
+ await writeFakeManagedConda(dir, platform)
670
+ await fs.writeFile(path.join(dir, 'bin/miniforge/test-pip-version'), '23.0.1')
671
+ const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
672
+ await fs.mkdir(path.dirname(marker), { recursive: true })
673
+ await fs.writeFile(marker, 'existing app')
674
+ const config = 'CONDA_SSL_VERIFY=private CA.pem\nPIP_CONFIG_FILE=pip.conf\n'
675
+ await fs.writeFile(path.join(dir, 'ENVIRONMENT'), config)
676
+ const kernel = createKernel(dir, platform)
677
+ const calls = []
678
+ kernel.bin.download = async () => assert.fail('must not replace a working runtime')
679
+ kernel.bin.exec = async payload => {
680
+ calls.push(payload)
681
+ await writeHealthyCondaMeta(dir, platform)
682
+ return {}
683
+ }
684
+ await createConda(kernel).install({}, () => {})
685
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
686
+ assert.equal(await fs.readFile(path.join(dir, 'ENVIRONMENT'), 'utf8'), config)
687
+ assert.equal(calls.length, 1)
688
+ assert.equal(calls[0].env, undefined)
689
+ assert.equal(calls[0]['-env'], undefined)
690
+ const python = platform === 'win32' ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC
691
+ assert.deepEqual(calls[0].message, [
692
+ `conda install -y --override-channels -c conda-forge "${python}" "conda-libmamba-solver>=25.4.0" ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(' ')}`,
693
+ PIP_INSTALL_COMMAND,
694
+ ])
695
+ }
674
696
  })
675
697
 
676
- test('failed pip bootstrap stops certificate refresh without replacing environments', async t => {
698
+ test('failed refresh reports an error without falling back to runtime replacement', async t => {
677
699
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-failed-'))
678
700
  t.after(() => fs.rm(root, { recursive: true, force: true }))
679
701
  await writeHealthyCondaMeta(root, 'darwin')
680
702
  await writeFakeManagedConda(root, 'darwin')
703
+ const marker = path.join(root, 'bin/miniforge/envs/app/keep')
704
+ await fs.mkdir(path.dirname(marker), { recursive: true })
705
+ await fs.writeFile(marker, 'preserve')
681
706
  const kernel = createKernel(root, 'darwin')
682
707
  kernel.bin.download = async () => assert.fail('must preserve the runtime')
683
- await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '26.2.1')
684
- for (const response of [{ error: ['ERROR: hash verification failed'] }, {}]) {
708
+ for (const response of [{ error: ['ERROR: download failed'] }, {}]) {
709
+ await fs.rm(path.join(root, 'bin/miniforge/test-pip-version'), { force: true })
685
710
  kernel.bin.exec = async () => response
686
- await assert.rejects(createConda(kernel).install({}, () => {}), /bootstrap failed/)
711
+ await assert.rejects(createConda(kernel).install({}, () => {}), /certificate update failed; existing environments were preserved/)
712
+ assert.equal(await fs.readFile(marker, 'utf8'), 'preserve')
687
713
  }
688
- assert.equal(await pathExists(path.join(root, 'bin/miniforge/conda-meta')), true)
689
714
  })
690
715
 
691
- test('fresh installation does not report a failed pip bootstrap as finished', async t => {
692
- const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-failed-'))
716
+ test('fresh installation retains readiness-based retries after package or pip errors', async t => {
717
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-retry-'))
693
718
  t.after(() => fs.rm(root, { recursive: true, force: true }))
694
719
  await fs.mkdir(path.join(root, 'bin'), { recursive: true })
695
720
  const kernel = createKernel(root, 'darwin')
721
+ let attempts = 0
696
722
  kernel.bin.mods = []
697
- kernel.bin.download = async () => {}
723
+ kernel.bin.download = async () => { attempts++ }
724
+ kernel.bin.rm = async () => {}
698
725
  kernel.bin.exec = async payload => {
699
726
  if (payload.conda?.skip) {
700
- await fs.mkdir(path.join(root, 'bin/miniforge/conda-meta'), { recursive: true })
727
+ await writeHealthyCondaMeta(root, 'darwin')
728
+ await writeFakeManagedConda(root, 'darwin')
729
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '23.0.1')
701
730
  return {}
702
731
  }
703
- return { error: ['ERROR: pip download failed'] }
732
+ assert.deepEqual(payload.env, { PIP_REQUIRE_VIRTUALENV: 'false' })
733
+ assert.equal(payload.message[2], PIP_INSTALL_COMMAND)
734
+ if (attempts === 1) return { error: ['ERROR: pip download failed'] }
735
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
736
+ return {}
737
+ }
738
+ const conda = createConda(kernel)
739
+ await conda.install({}, () => {})
740
+ assert.equal(attempts, 2)
741
+ assert.equal(await conda.check(), true)
742
+ })
743
+
744
+ test('pip readiness accepts two-part release versions through the actual pip CLI', async t => {
745
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-release-'))
746
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
747
+ await writeHealthyCondaMeta(root, 'darwin')
748
+ await writeFakeManagedConda(root, 'darwin')
749
+ for (const version of ['26.3', '27.0']) {
750
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), version)
751
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), true)
752
+ const bin = createBin(root, 'darwin')
753
+ await bin.tryList()
754
+ assert.equal(bin.correct_conda, true)
704
755
  }
705
- await assert.rejects(createConda(kernel)._install({}, () => {}), /pip installation failed/)
706
756
  })
@@ -7,7 +7,7 @@ const { spawnSync } = require('node:child_process')
7
7
 
8
8
  const ShellRunTemplate = require('../kernel/api/shell_run_template')
9
9
  const Shell = require('../kernel/shell')
10
- const { PIP_MIN_VERSION } = require('../kernel/bin/conda-pins')
10
+ const { PIP_INSTALL_COMMAND } = require('../kernel/bin/conda-pins')
11
11
 
12
12
  function createKernel(root = process.cwd()) {
13
13
  return {
@@ -381,7 +381,7 @@ test('redactEnvArgs summarizes protected argv env values', () => {
381
381
  })
382
382
 
383
383
 
384
- test('venvs preserve creation behavior and enforce the pip minimum across shells', async t => {
384
+ test('only new default venvs bootstrap pip across supported shells', async t => {
385
385
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-selection-'))
386
386
  t.after(() => fs.rm(root, { recursive: true, force: true }))
387
387
  const cases = [
@@ -413,7 +413,7 @@ test('venvs preserve creation behavior and enforce the pip minimum across shells
413
413
  message: 'echo PINOKIO_REQUIREMENTS_RAN',
414
414
  })
415
415
  const command = shell.build(params)
416
- assert.doesNotMatch(command, /--seed|--no-config|--python-preference|python -I|files\.pythonhosted\.org/)
416
+ assert.doesNotMatch(command, /--seed|--no-config|--python-preference|uv pip install/)
417
417
  assert.match(command, /echo PINOKIO_REQUIREMENTS_RAN$/)
418
418
  if (!scenario.venv || scenario.existing) {
419
419
  assert.doesNotMatch(command, /python -m venv|uv venv/)
@@ -423,15 +423,18 @@ test('venvs preserve creation behavior and enforce the pip minimum across shells
423
423
  } else {
424
424
  assert.match(command, /python -m venv /)
425
425
  }
426
- const updatePip = `uv pip install --exclude-newer-package pip=false "pip>=${PIP_MIN_VERSION}"`
427
- if (scenario.venv && !scenario.version) {
428
- assert.equal(command.split(updatePip).length - 1, 1)
429
- assert.ok(command.indexOf(updatePip) > command.lastIndexOf('activate'))
430
- assert.ok(command.includes(shell.isPowerShell()
431
- ? `${updatePip}; if ($LASTEXITCODE -ne 0) { exit 1 }`
432
- : `${updatePip} || exit 1`))
426
+ if (scenario.venv && !scenario.version && !scenario.existing) {
427
+ assert.equal(command.split(PIP_INSTALL_COMMAND).length - 1, 1)
428
+ assert.ok(command.indexOf(PIP_INSTALL_COMMAND) > command.lastIndexOf('activate'))
429
+ const envPath = path.resolve(root, typeof scenario.venv === 'string' ? scenario.venv : scenario.venv.path)
430
+ const shellPath = platform === 'win32' && /bash/i.test(shellName) ? `"${require('../kernel/util').p2u(envPath)}"` : envPath
431
+ for (const step of [`python -m venv ${shellPath}`, PIP_INSTALL_COMMAND]) {
432
+ assert.ok(command.includes(shell.isPowerShell()
433
+ ? `${step}; if ($LASTEXITCODE -ne 0) { exit 1 }`
434
+ : `${step} || exit 1`))
435
+ }
433
436
  } else {
434
- assert.doesNotMatch(command, /uv pip install/)
437
+ assert.doesNotMatch(command, /-m pip/)
435
438
  }
436
439
  assert.equal(shell.env.UV_PYTHON_PREFERENCE, 'only-managed')
437
440
  }
@@ -439,15 +442,22 @@ test('venvs preserve creation behavior and enforce the pip minimum across shells
439
442
  })
440
443
 
441
444
 
442
- test('failed pip updates stop the application even with an unconditional command chain', async t => {
445
+ test('failed venv creation or pip bootstrap stops an unconditional command chain', async t => {
443
446
  if (process.platform === 'win32') return t.skip('executes the POSIX shell failure guard')
444
447
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-failure-'))
445
448
  t.after(() => fs.rm(root, { recursive: true, force: true }))
446
449
  const binDir = path.join(root, 'bin')
447
- await fs.mkdir(path.join(root, 'env', 'bin'), { recursive: true })
448
450
  await fs.mkdir(binDir)
449
- await fs.writeFile(path.join(root, 'env', 'bin', 'activate'), 'true\n')
450
- await fs.writeFile(path.join(binDir, 'uv'), '#!/bin/sh\nexit "$PINOKIO_TEST_UV_EXIT_CODE"\n', { mode: 0o755 })
451
+ await fs.writeFile(path.join(binDir, 'python'), `#!/bin/sh
452
+ if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
453
+ [ "$PINOKIO_TEST_VENV_EXIT_CODE" = "0" ] || exit "$PINOKIO_TEST_VENV_EXIT_CODE"
454
+ mkdir -p "$3/bin"
455
+ echo 'deactivate() { :; }' > "$3/bin/activate"
456
+ else
457
+ echo PINOKIO_PIP_BOOTSTRAP_RAN
458
+ exit "$PINOKIO_TEST_PIP_EXIT_CODE"
459
+ fi
460
+ `, { mode: 0o755 })
451
461
  const shell = createShell(createKernel(root))
452
462
  shell.shell = 'bash'
453
463
  const params = await shell.activate({
@@ -455,12 +465,13 @@ test('failed pip updates stop the application even with an unconditional command
455
465
  chain: '*', message: 'echo PINOKIO_APPLICATION_RAN',
456
466
  })
457
467
  const command = shell.build(params)
458
- for (const exitCode of [0, 7]) {
468
+ for (const [venvExit, pipExit] of [[9, 0], [0, 7], [0, 0]]) {
459
469
  const result = spawnSync('/bin/bash', ['-c', command], {
460
470
  encoding: 'utf8',
461
- env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH}`, PINOKIO_TEST_UV_EXIT_CODE: String(exitCode) },
471
+ env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH}`, PINOKIO_TEST_VENV_EXIT_CODE: String(venvExit), PINOKIO_TEST_PIP_EXIT_CODE: String(pipExit) },
462
472
  })
463
- assert.equal(result.status, exitCode ? 1 : 0, result.stderr)
464
- assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), exitCode === 0)
473
+ assert.equal(result.status, venvExit || pipExit ? 1 : 0, result.stderr)
474
+ assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), venvExit === 0 && pipExit === 0)
475
+ assert.equal(result.stdout.includes('PINOKIO_PIP_BOOTSTRAP_RAN'), venvExit === 0)
465
476
  }
466
477
  })