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.
- package/kernel/bin/conda-meta.js +2 -2
- package/kernel/bin/conda-pins.js +1 -2
- package/kernel/bin/conda.js +5 -16
- package/kernel/shell.js +3 -6
- package/package.json +1 -1
- package/test/conda-bin.test.js +97 -47
- package/test/shell-run-template.test.js +30 -19
package/kernel/bin/conda-meta.js
CHANGED
|
@@ -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', '-
|
|
116
|
+
execFile(python, ['-I', '-m', 'pip', '--version'], {
|
|
117
117
|
env: managedPythonEnv(), windowsHide: true, timeout: 15000,
|
|
118
118
|
}, (error, stdout) => {
|
|
119
|
-
const version = !error && semver.
|
|
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
|
})
|
package/kernel/bin/conda-pins.js
CHANGED
|
@@ -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
|
|
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])
|
package/kernel/bin/conda.js
CHANGED
|
@@ -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: [
|
|
195
|
-
|
|
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
|
-
|
|
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,
|
|
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
package/test/conda-bin.test.js
CHANGED
|
@@ -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'
|
|
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
|
-
|
|
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.
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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(
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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
|
-
|
|
684
|
-
|
|
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({}, () => {}), /
|
|
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
|
|
692
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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('
|
|
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|
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
assert.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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,
|
|
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
|
|
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(
|
|
450
|
-
|
|
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
|
|
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}`,
|
|
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,
|
|
464
|
-
assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'),
|
|
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
|
})
|