pinokiod 8.2.3 → 8.2.4
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/Dockerfile +3 -2
- package/kernel/anywhere/index.js +9 -1
- package/kernel/bin/bun.js +1 -1
- package/kernel/bin/conda-meta.js +15 -0
- package/kernel/bin/conda-pins.js +22 -0
- package/kernel/bin/conda.js +66 -5
- package/kernel/bin/index.js +4 -3
- package/kernel/bin/node.js +1 -1
- package/kernel/bin/tailscaled.js +3 -2
- package/kernel/bin/uv.js +1 -1
- package/kernel/shell.js +8 -1
- package/package.json +1 -1
- package/test/anywhere.test.js +21 -3
- package/test/conda-bin.test.js +126 -8
- package/test/node-bin.test.js +3 -3
- package/test/shell-run-template.test.js +46 -0
- package/test/uv-bin.test.js +5 -5
package/Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Production image for pinokiod with required system tooling
|
|
2
|
-
FROM node:
|
|
2
|
+
FROM node:24.22.1 AS build
|
|
3
3
|
WORKDIR /app
|
|
4
4
|
|
|
5
5
|
RUN apt-get update \
|
|
@@ -9,12 +9,13 @@ RUN apt-get update \
|
|
|
9
9
|
COPY package*.json ./
|
|
10
10
|
RUN npm ci --omit=dev && npm cache clean --force
|
|
11
11
|
|
|
12
|
-
FROM node:
|
|
12
|
+
FROM node:24.22.1 AS runtime
|
|
13
13
|
ENV NODE_ENV=production
|
|
14
14
|
WORKDIR /app
|
|
15
15
|
|
|
16
16
|
RUN apt-get update \
|
|
17
17
|
&& apt-get install -y --no-install-recommends \
|
|
18
|
+
ca-certificates \
|
|
18
19
|
bluez \
|
|
19
20
|
curl \
|
|
20
21
|
git \
|
package/kernel/anywhere/index.js
CHANGED
|
@@ -187,8 +187,11 @@ class Anywhere {
|
|
|
187
187
|
async autostart() {
|
|
188
188
|
// a pid file left behind by a crash or reboot must never point at an unrelated process
|
|
189
189
|
if (!(await this.daemonAlive())) fs.rmSync(this.pidPath(), { force: true })
|
|
190
|
+
if (!(await this.installed())) {
|
|
191
|
+
await this.stop()
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
190
194
|
if (!(await this.enabled())) return false
|
|
191
|
-
if (!(await this.installed())) return false
|
|
192
195
|
await this.start()
|
|
193
196
|
return true
|
|
194
197
|
}
|
|
@@ -199,6 +202,11 @@ class Anywhere {
|
|
|
199
202
|
}
|
|
200
203
|
async _start() {
|
|
201
204
|
this.error = null
|
|
205
|
+
if (!(await this.installed())) {
|
|
206
|
+
await this.stop()
|
|
207
|
+
this.error = "Update the tailscaled tool before starting Pinokio Anywhere."
|
|
208
|
+
throw new Error(this.error)
|
|
209
|
+
}
|
|
202
210
|
await this.loadConfig()
|
|
203
211
|
await fs.promises.mkdir(this.stateDir(), { recursive: true })
|
|
204
212
|
const bin = this.binPath()
|
package/kernel/bin/bun.js
CHANGED
package/kernel/bin/conda-meta.js
CHANGED
|
@@ -2,6 +2,8 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { execFile } = require('child_process')
|
|
4
4
|
const { managedPythonEnv } = require('../python_env')
|
|
5
|
+
const semver = require('semver')
|
|
6
|
+
const { PIP_MIN_VERSION } = require('./conda-pins')
|
|
5
7
|
|
|
6
8
|
async function buildCondaListFromMeta(minicondaPath, useCondaList) {
|
|
7
9
|
if (!useCondaList) {
|
|
@@ -108,7 +110,20 @@ function resolveCondaBinary(minicondaPath, platform = process.platform, allowFal
|
|
|
108
110
|
return allowFallback ? 'conda' : null
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
async function hasSecureManagedPip(prefix, platform = process.platform) {
|
|
114
|
+
const python = path.join(prefix, platform === 'win32' ? 'python.exe' : 'bin/python')
|
|
115
|
+
return await new Promise(resolve => {
|
|
116
|
+
execFile(python, ['-I', '-c', 'from importlib.metadata import version; print(version("pip"))'], {
|
|
117
|
+
env: managedPythonEnv(), windowsHide: true, timeout: 15000,
|
|
118
|
+
}, (error, stdout) => {
|
|
119
|
+
const version = !error && semver.valid(String(stdout).trim())
|
|
120
|
+
resolve(!!version && semver.gte(version, PIP_MIN_VERSION))
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
module.exports = {
|
|
112
126
|
buildCondaListFromMeta,
|
|
113
127
|
managedCondaRuns,
|
|
128
|
+
hasSecureManagedPip,
|
|
114
129
|
}
|
package/kernel/bin/conda-pins.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
const semver = require('semver')
|
|
2
2
|
|
|
3
3
|
const CONDA_PIN_VERSION = "26.5.3"
|
|
4
|
+
// Conda 26.9.4 has no Python 3.10 builds. Apply its CA-bundle requirements
|
|
5
|
+
// explicitly while retaining the managed Python version required by apps.
|
|
6
|
+
const TLS_PACKAGE_MINIMUMS = {
|
|
7
|
+
'ca-certificates': '2026.10.19',
|
|
8
|
+
certifi: '2026.10.19',
|
|
9
|
+
}
|
|
10
|
+
// conda-forge's patched pip requires Python 3.11; this official wheel supports 3.10.
|
|
11
|
+
// Bootstrap URL and hash: https://discuss.python.org/t/109576
|
|
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 --disable-pip-version-check install --no-index --no-deps "${PIP_INSTALL_SPEC}"`
|
|
15
|
+
const TLS_PACKAGE_SPECS = Object.entries(TLS_PACKAGE_MINIMUMS).map(([name, version]) => `${name}>=${version}`)
|
|
16
|
+
const hasSecureTlsPackages = (versions = {}) => Object.entries(TLS_PACKAGE_MINIMUMS).every(([name, minimum]) => {
|
|
17
|
+
const version = semver.coerce(versions[name])
|
|
18
|
+
return version && semver.gte(version, minimum)
|
|
19
|
+
})
|
|
4
20
|
const PYTHON_PIN_VERSION = "3.10.20"
|
|
5
21
|
const PYTHON_INSTALL_SPEC = `python=${PYTHON_PIN_VERSION}`
|
|
6
22
|
const WINDOWS_PYTHON_SSL_FIX_SPEC = `${PYTHON_INSTALL_SPEC}=*_1_cpython`
|
|
@@ -30,6 +46,12 @@ const isExpectedPythonPinned = (platform, version, build) => {
|
|
|
30
46
|
|
|
31
47
|
module.exports = {
|
|
32
48
|
CONDA_PIN_VERSION,
|
|
49
|
+
TLS_PACKAGE_MINIMUMS,
|
|
50
|
+
PIP_MIN_VERSION,
|
|
51
|
+
PIP_INSTALL_SPEC,
|
|
52
|
+
PIP_INSTALL_COMMAND,
|
|
53
|
+
TLS_PACKAGE_SPECS,
|
|
54
|
+
hasSecureTlsPackages,
|
|
33
55
|
PYTHON_INSTALL_SPEC,
|
|
34
56
|
WINDOWS_PYTHON_SSL_FIX_SPEC,
|
|
35
57
|
isExpectedCondaPinned,
|
package/kernel/bin/conda.js
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const { glob } = require('glob')
|
|
3
3
|
const semver = require('semver')
|
|
4
|
+
const { createHash } = require('crypto')
|
|
5
|
+
const { execFile } = require('child_process')
|
|
6
|
+
const { managedPythonEnv } = require('../python_env')
|
|
4
7
|
const { isPathRemovalBlockedError, removePath } = require('../path_removal')
|
|
5
|
-
const { buildCondaListFromMeta, managedCondaRuns } = require('./conda-meta')
|
|
8
|
+
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
|
|
6
9
|
const {
|
|
7
10
|
CONDA_PIN_VERSION,
|
|
11
|
+
PIP_INSTALL_COMMAND,
|
|
12
|
+
TLS_PACKAGE_MINIMUMS,
|
|
13
|
+
TLS_PACKAGE_SPECS,
|
|
14
|
+
hasSecureTlsPackages,
|
|
8
15
|
PYTHON_INSTALL_SPEC,
|
|
9
16
|
WINDOWS_PYTHON_SSL_FIX_SPEC,
|
|
10
17
|
isExpectedCondaPinned,
|
|
11
18
|
isExpectedPythonPinned,
|
|
12
19
|
} = require('./conda-pins')
|
|
13
20
|
|
|
14
|
-
const MINIFORGE_RELEASE = "26.
|
|
21
|
+
const MINIFORGE_RELEASE = "26.9.4-0"
|
|
15
22
|
const MINIFORGE_BASE_URL = `https://github.com/conda-forge/miniforge/releases/download/${MINIFORGE_RELEASE}`
|
|
23
|
+
// GitHub's published SHA-256 asset digests for Miniforge 26.9.4-0.
|
|
24
|
+
const MINIFORGE_SHA256 = {
|
|
25
|
+
darwin: {
|
|
26
|
+
x64: '4bf8d162f90a9ae79fbe1c3b152a9957b6cf4e7385758fa21e0340229d444df1',
|
|
27
|
+
arm64: '85512f5495465644e09655d84bc1df79d978353abd9ef0d0a8f3df75da44c760',
|
|
28
|
+
},
|
|
29
|
+
linux: {
|
|
30
|
+
x64: '621428f40496a90702753b09237794518a081d9979d9f845a525de761b6056f7',
|
|
31
|
+
arm64: 'd265b10be348403a3bb8d475ff989beba48db9baa5fe7ee28eb38213538056bb',
|
|
32
|
+
},
|
|
33
|
+
win32: { x64: '43638698f5ee811debae27b0dc3ab5c0300129bd400a847376c4bd2f6be15ceb' },
|
|
34
|
+
}
|
|
16
35
|
const CONDA_ROOT_DIR = "miniforge"
|
|
17
36
|
const LEGACY_CONDA_ROOT_DIR = "miniconda"
|
|
18
37
|
|
|
@@ -44,11 +63,22 @@ class Conda {
|
|
|
44
63
|
pinnedPackages() {
|
|
45
64
|
return [
|
|
46
65
|
`conda ==${CONDA_PIN_VERSION}`,
|
|
66
|
+
...TLS_PACKAGE_SPECS,
|
|
47
67
|
].join("\n")
|
|
48
68
|
}
|
|
49
69
|
async hasCondaMeta() {
|
|
50
70
|
return await this.kernel.exists(`bin/${CONDA_ROOT_DIR}/conda-meta`)
|
|
51
71
|
}
|
|
72
|
+
async verifyInstaller(installer) {
|
|
73
|
+
const expected = MINIFORGE_SHA256[this.kernel.platform]?.[this.kernel.arch]
|
|
74
|
+
const hash = createHash('sha256')
|
|
75
|
+
for await (const chunk of fs.createReadStream(this.kernel.bin.path(installer))) {
|
|
76
|
+
hash.update(chunk)
|
|
77
|
+
}
|
|
78
|
+
if (!expected || hash.digest('hex') !== expected) {
|
|
79
|
+
throw new Error('Miniforge installer SHA-256 verification failed. The installer was not run and existing environments were preserved.')
|
|
80
|
+
}
|
|
81
|
+
}
|
|
52
82
|
env() {
|
|
53
83
|
let base = {
|
|
54
84
|
CONDA_PREFIX: this.kernel.bin.path(CONDA_ROOT_DIR),
|
|
@@ -78,6 +108,7 @@ class Conda {
|
|
|
78
108
|
channel_priority: flexible
|
|
79
109
|
create_default_packages:
|
|
80
110
|
- ${PYTHON_INSTALL_SPEC}
|
|
111
|
+
${TLS_PACKAGE_SPECS.map(spec => ` - ${spec}`).join("\n")}
|
|
81
112
|
envs_dirs:
|
|
82
113
|
- ${this.kernel.bin.path(`${CONDA_ROOT_DIR}/envs`)}
|
|
83
114
|
pkgs_dirs:
|
|
@@ -138,12 +169,37 @@ report_errors: false`)
|
|
|
138
169
|
this.kernel.bin.installed.conda = conda
|
|
139
170
|
this.kernel.bin.installed.conda_versions = conda_versions
|
|
140
171
|
this.kernel.bin.installed.conda_builds = conda_builds
|
|
141
|
-
if (!(conda_check.conda && conda_check.mamba && conda_check.python)) {
|
|
172
|
+
if (!(conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions))) {
|
|
142
173
|
return false
|
|
143
174
|
}
|
|
144
|
-
return await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)
|
|
175
|
+
return await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform) && await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)
|
|
145
176
|
}
|
|
146
177
|
async install(req, ondata) {
|
|
178
|
+
await this.check()
|
|
179
|
+
const { conda_versions: versions, conda_builds: builds } = this.kernel.bin.installed
|
|
180
|
+
if (isExpectedCondaPinned(versions.conda) &&
|
|
181
|
+
isExpectedPythonPinned(this.kernel.platform, versions.python, builds.python) &&
|
|
182
|
+
await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)) {
|
|
183
|
+
// Refresh a working runtime in place, preserving named app environments.
|
|
184
|
+
await this.init()
|
|
185
|
+
const bootstrap = await this.kernel.bin.exec({
|
|
186
|
+
message: [PIP_INSTALL_COMMAND, `python -I -m pip --isolated install --no-deps --only-binary=:all: --index-url https://pypi.org/simple "certifi>=${TLS_PACKAGE_MINIMUMS.certifi}"`],
|
|
187
|
+
}, ondata)
|
|
188
|
+
if (bootstrap?.error?.length) throw new Error("Conda certificate bootstrap failed; existing environments were preserved.")
|
|
189
|
+
const python = this.kernel.bin.path(CONDA_ROOT_DIR, this.kernel.platform === 'win32' ? 'python.exe' : 'bin/python')
|
|
190
|
+
const output = await new Promise((resolve, reject) => execFile(python,
|
|
191
|
+
['-I', '-c', 'import certifi; from importlib.metadata import version; print(certifi.where()); print(version("certifi"))'],
|
|
192
|
+
{ env: managedPythonEnv(), windowsHide: true, timeout: 15000 },
|
|
193
|
+
(error, stdout) => error ? reject(error) : resolve(stdout.trim())))
|
|
194
|
+
const [caFile, certifiVersion] = output.split(/[\r\n]+/)
|
|
195
|
+
if (!semver.valid(certifiVersion) || !semver.gte(certifiVersion, TLS_PACKAGE_MINIMUMS.certifi)) throw new Error("The certificate bootstrap did not complete.")
|
|
196
|
+
const result = await this.kernel.bin.exec({
|
|
197
|
+
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],
|
|
198
|
+
env: { CONDA_SSL_VERIFY: caFile },
|
|
199
|
+
}, ondata)
|
|
200
|
+
if (result?.error?.length || !(await this.check())) throw new Error("Conda certificate update failed; existing environments were preserved.")
|
|
201
|
+
return
|
|
202
|
+
}
|
|
147
203
|
for(let i=0; i<5; i++) {
|
|
148
204
|
await this._install(req, ondata)
|
|
149
205
|
let installed = await this.check()
|
|
@@ -170,6 +226,7 @@ report_errors: false`)
|
|
|
170
226
|
|
|
171
227
|
ondata({ raw: `downloading installer: ${installer_url}...\r\n` })
|
|
172
228
|
await this.kernel.bin.download(installer_url, installer, ondata)
|
|
229
|
+
await this.verifyInstaller(installer)
|
|
173
230
|
|
|
174
231
|
const caddy = this.kernel.bin.mod && this.kernel.bin.mod.caddy
|
|
175
232
|
if (caddy && typeof caddy.running === "function" && await caddy.running()) {
|
|
@@ -227,13 +284,16 @@ report_errors: false`)
|
|
|
227
284
|
`"conda=${CONDA_PIN_VERSION}"`,
|
|
228
285
|
this.kernel.platform === "win32" ? `"${WINDOWS_PYTHON_SSL_FIX_SPEC}"` : `"${PYTHON_INSTALL_SPEC}"`,
|
|
229
286
|
`"conda-libmamba-solver>=25.4.0"`,
|
|
287
|
+
"pip",
|
|
288
|
+
...TLS_PACKAGE_SPECS.map(spec => `"${spec}"`),
|
|
230
289
|
]
|
|
231
290
|
|
|
232
291
|
let cmds = [
|
|
233
292
|
"conda clean -y --all",
|
|
234
293
|
`conda install -y --override-channels -c conda-forge ${condaPackages.join(" ")} ${mods}`.trim(),
|
|
294
|
+
PIP_INSTALL_COMMAND,
|
|
235
295
|
]
|
|
236
|
-
await this.kernel.bin.exec({
|
|
296
|
+
const result = await this.kernel.bin.exec({
|
|
237
297
|
message: cmds,
|
|
238
298
|
env: {
|
|
239
299
|
PIP_REQUIRE_VIRTUALENV: "false"
|
|
@@ -241,6 +301,7 @@ report_errors: false`)
|
|
|
241
301
|
}, (stream) => {
|
|
242
302
|
ondata(stream)
|
|
243
303
|
})
|
|
304
|
+
if (result?.error?.length) throw new Error("Conda package or pip installation failed.")
|
|
244
305
|
if (this.kernel.platform === "win32") {
|
|
245
306
|
// copy python.exe to python3.exe so you can run with both python3 and python
|
|
246
307
|
await fs.promises.copyFile(
|
package/kernel/bin/index.js
CHANGED
|
@@ -21,10 +21,11 @@ const LLVM = require('./llvm')
|
|
|
21
21
|
const VS = require("./vs")
|
|
22
22
|
const Cuda = require("./cuda")
|
|
23
23
|
const Torch = require("./torch")
|
|
24
|
-
const { buildCondaListFromMeta, managedCondaRuns } = require('./conda-meta')
|
|
24
|
+
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
|
|
25
25
|
const {
|
|
26
26
|
isExpectedCondaPinned,
|
|
27
27
|
isExpectedPythonPinned,
|
|
28
|
+
hasSecureTlsPackages,
|
|
28
29
|
} = require('./conda-pins')
|
|
29
30
|
const { glob } = require('glob')
|
|
30
31
|
const fakeUa = require('fake-useragent');
|
|
@@ -412,8 +413,8 @@ class Bin {
|
|
|
412
413
|
}
|
|
413
414
|
}
|
|
414
415
|
|
|
415
|
-
if (conda_check.conda && conda_check.mamba && conda_check.python) {
|
|
416
|
-
this.correct_conda = await managedCondaRuns(this.kernel.bin.path("miniforge"), this.platform)
|
|
416
|
+
if (conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions)) {
|
|
417
|
+
this.correct_conda = await hasSecureManagedPip(this.kernel.bin.path("miniforge"), this.platform) && await managedCondaRuns(this.kernel.bin.path("miniforge"), this.platform)
|
|
417
418
|
}
|
|
418
419
|
}
|
|
419
420
|
this.installed.conda = conda
|
package/kernel/bin/node.js
CHANGED
|
@@ -2,7 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const fetch = require('cross-fetch')
|
|
3
3
|
const { rimraf } = require('rimraf')
|
|
4
4
|
const decompress = require('decompress');
|
|
5
|
-
const NODE_VERSION = "24.
|
|
5
|
+
const NODE_VERSION = "24.22.1"
|
|
6
6
|
const PNPM_VERSION = "11.9.0"
|
|
7
7
|
|
|
8
8
|
const normalizeVersion = (version) => {
|
package/kernel/bin/tailscaled.js
CHANGED
|
@@ -3,8 +3,8 @@ const semver = require('semver')
|
|
|
3
3
|
|
|
4
4
|
// conda-forge ships only the daemon binary (no `tailscale` CLI); Pinokio
|
|
5
5
|
// drives it over its LocalAPI (see kernel/anywhere).
|
|
6
|
-
const TAILSCALED_SPEC = "tailscaled=1.104.
|
|
7
|
-
const TAILSCALED_REQUIREMENT = ">=1.104.
|
|
6
|
+
const TAILSCALED_SPEC = "tailscaled=1.104.2"
|
|
7
|
+
const TAILSCALED_REQUIREMENT = ">=1.104.2" // Blocks the compromised eMudhra intermediate keys.
|
|
8
8
|
|
|
9
9
|
class Tailscaled {
|
|
10
10
|
description = "Installs the Tailscale daemon for Pinokio Anywhere: reach this Pinokio and its apps from your phone or another computer over a private network."
|
|
@@ -18,6 +18,7 @@ class Tailscaled {
|
|
|
18
18
|
return this.kernel.bin.path("miniforge", "bin", "tailscaled")
|
|
19
19
|
}
|
|
20
20
|
async install(req, ondata) {
|
|
21
|
+
if (this.kernel.anywhere) await this.kernel.anywhere.stop()
|
|
21
22
|
await this.kernel.bin.exec({
|
|
22
23
|
message: [
|
|
23
24
|
// a stale libmamba index cache reports the package as missing even
|
package/kernel/bin/uv.js
CHANGED
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 } = require('./bin/conda-pins')
|
|
24
|
+
const { PYTHON_INSTALL_SPEC, PIP_INSTALL_COMMAND, PIP_MIN_VERSION } = 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
|
|
@@ -1399,10 +1399,17 @@ class Shell {
|
|
|
1399
1399
|
deactivate_path,
|
|
1400
1400
|
// timeout,
|
|
1401
1401
|
activate_command,
|
|
1402
|
+
this.isPowerShell()
|
|
1403
|
+
? `${PIP_INSTALL_COMMAND}; if ($LASTEXITCODE -ne 0) { exit 1 }`
|
|
1404
|
+
: `${PIP_INSTALL_COMMAND} || exit 1`,
|
|
1402
1405
|
// timeout,
|
|
1403
1406
|
]
|
|
1404
1407
|
}
|
|
1405
1408
|
}
|
|
1409
|
+
if (!use_uv) {
|
|
1410
|
+
const checkPip = `python -I -c "from importlib.metadata import distributions; assert all(tuple(map(int, d.version.split('.')[:3])) >= (${PIP_MIN_VERSION.split(".").join(", ")}) for d in distributions(name='pip')), 'Update pip in this app environment before running it.'"`
|
|
1411
|
+
venv_activation.push(this.isPowerShell() ? `${checkPip}; if ($LASTEXITCODE -ne 0) { exit 1 }` : `${checkPip} || exit 1`)
|
|
1412
|
+
}
|
|
1406
1413
|
} else {
|
|
1407
1414
|
venv_activation = []
|
|
1408
1415
|
}
|
package/package.json
CHANGED
package/test/anywhere.test.js
CHANGED
|
@@ -59,17 +59,20 @@ test('anywhere preset requires tailscaled but not caddy', () => {
|
|
|
59
59
|
assert.ok(!preset.conda_requirements.includes('caddy'))
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
test('tailscaled bin module pins 1.104.
|
|
62
|
+
test('tailscaled bin module pins 1.104.2 and checks the installed version', async () => {
|
|
63
63
|
const mod = new Tailscaled()
|
|
64
|
-
assert.equal(mod.cmd(), 'tailscaled=1.104.
|
|
64
|
+
assert.equal(mod.cmd(), 'tailscaled=1.104.2')
|
|
65
65
|
mod.kernel = {
|
|
66
66
|
platform: 'darwin',
|
|
67
67
|
bin: {
|
|
68
|
-
installed: { conda: new Set(['tailscaled']), conda_versions: { tailscaled: '1.104.
|
|
68
|
+
installed: { conda: new Set(['tailscaled']), conda_versions: { tailscaled: '1.104.2' } },
|
|
69
69
|
path: (...segments) => segments.join('/'),
|
|
70
70
|
},
|
|
71
71
|
}
|
|
72
72
|
assert.equal(await mod.installed(), true)
|
|
73
|
+
mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.1'
|
|
74
|
+
assert.equal(await mod.installed(), false)
|
|
75
|
+
mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.2'
|
|
73
76
|
assert.equal(mod.binPath(), 'miniforge/bin/tailscaled')
|
|
74
77
|
mod.kernel.bin.installed.conda_versions.tailscaled = '1.98.5'
|
|
75
78
|
assert.equal(await mod.installed(), false, 'older than 1.104 (no --windows-mode=dev) is not accepted')
|
|
@@ -441,3 +444,18 @@ test('sign-out resets the login state, and a link the server rejected is replace
|
|
|
441
444
|
fs.rmSync(tmp, { recursive: true, force: true })
|
|
442
445
|
}
|
|
443
446
|
})
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
test('old Tailscale daemons are stopped and cannot be adopted through direct start', async t => {
|
|
450
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-version-'))
|
|
451
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }))
|
|
452
|
+
const anywhere = new Anywhere(fakeKernel(root, { installed: false }))
|
|
453
|
+
let stopped = 0
|
|
454
|
+
anywhere.daemonAlive = async () => true
|
|
455
|
+
anywhere.enabled = async () => true
|
|
456
|
+
anywhere.stop = async () => { stopped++ }
|
|
457
|
+
assert.equal(await anywhere.autostart(), false)
|
|
458
|
+
assert.equal(stopped, 1)
|
|
459
|
+
await assert.rejects(anywhere.start(), /Update the tailscaled tool/)
|
|
460
|
+
assert.equal(stopped, 2)
|
|
461
|
+
})
|
package/test/conda-bin.test.js
CHANGED
|
@@ -6,16 +6,40 @@ const os = require('node:os')
|
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
const test = require('node:test')
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
let Bin
|
|
10
|
+
let Conda
|
|
11
11
|
const {
|
|
12
12
|
CONDA_PIN_VERSION,
|
|
13
13
|
PYTHON_INSTALL_SPEC,
|
|
14
14
|
WINDOWS_PYTHON_SSL_FIX_SPEC,
|
|
15
15
|
isExpectedCondaPinned,
|
|
16
16
|
isExpectedPythonPinned,
|
|
17
|
+
TLS_PACKAGE_MINIMUMS,
|
|
18
|
+
TLS_PACKAGE_SPECS,
|
|
19
|
+
PIP_INSTALL_SPEC,
|
|
20
|
+
PIP_MIN_VERSION,
|
|
17
21
|
} = require('../kernel/bin/conda-pins')
|
|
18
22
|
|
|
23
|
+
test.beforeEach(t => {
|
|
24
|
+
const realExecFile = childProcess.execFile.bind(childProcess)
|
|
25
|
+
t.mock.method(childProcess, 'execFile', (binary, args, options, done) => {
|
|
26
|
+
if (args[0] !== '-I' || args[1] !== '-c') return realExecFile(binary, args, options, done)
|
|
27
|
+
const prefix = path.basename(binary) === 'python.exe' ? path.dirname(binary) : path.dirname(path.dirname(binary))
|
|
28
|
+
if (args[2].includes('certifi.where()')) {
|
|
29
|
+
done(null, path.join(prefix, 'certificates.pem') + '\n2026.10.19\n')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
fs.readFile(path.join(prefix, 'test-pip-version'), 'utf8').then(
|
|
33
|
+
version => done(null, version), error => done(error, '')
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
for (const file of ['../kernel/bin/conda-meta', '../kernel/bin/conda', '../kernel/bin']) {
|
|
37
|
+
delete require.cache[require.resolve(file)]
|
|
38
|
+
}
|
|
39
|
+
Bin = require('../kernel/bin')
|
|
40
|
+
Conda = require('../kernel/bin/conda')
|
|
41
|
+
})
|
|
42
|
+
|
|
19
43
|
async function pathExists(target) {
|
|
20
44
|
return fs.access(target).then(() => true).catch(() => false)
|
|
21
45
|
}
|
|
@@ -49,6 +73,10 @@ async function writeHealthyCondaMeta(root, platform = 'win32', condaVersion = CO
|
|
|
49
73
|
await writeCondaMeta(root, 'conda', condaVersion, 'py310')
|
|
50
74
|
await writeCondaMeta(root, 'conda-libmamba-solver', '25.4.0', 'pyhd3eb1b0_0')
|
|
51
75
|
await writeCondaMeta(root, 'python', '3.10.20', platform === 'win32' ? 'h4de0772_1_cpython' : 'cpython')
|
|
76
|
+
for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
|
|
77
|
+
await writeCondaMeta(root, name, version)
|
|
78
|
+
}
|
|
79
|
+
await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
async function writeFakeManagedConda(root, platform = 'win32') {
|
|
@@ -87,6 +115,7 @@ function restoreEnv(name, value) {
|
|
|
87
115
|
function createConda(kernel) {
|
|
88
116
|
const conda = new Conda()
|
|
89
117
|
conda.kernel = kernel
|
|
118
|
+
conda.verifyInstaller = async () => {} // Installer lifecycle tests mock the download.
|
|
90
119
|
return conda
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -161,7 +190,7 @@ test('Conda uses Miniforge assets and writes conda-forge-only config', async ()
|
|
|
161
190
|
|
|
162
191
|
assert.equal(
|
|
163
192
|
conda.urls.win32.x64,
|
|
164
|
-
'https://github.com/conda-forge/miniforge/releases/download/26.
|
|
193
|
+
'https://github.com/conda-forge/miniforge/releases/download/26.9.4-0/Miniforge3-Windows-x86_64.exe'
|
|
165
194
|
)
|
|
166
195
|
|
|
167
196
|
await conda.init()
|
|
@@ -201,7 +230,7 @@ test('Conda init writes the current Conda pin into an existing runtime', async (
|
|
|
201
230
|
|
|
202
231
|
assert.equal(
|
|
203
232
|
await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
|
|
204
|
-
'conda ==26.5.3'
|
|
233
|
+
['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
|
|
205
234
|
)
|
|
206
235
|
})
|
|
207
236
|
|
|
@@ -253,13 +282,14 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
|
|
|
253
282
|
assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
|
|
254
283
|
const condaInstall = calls.find((call) => Array.isArray(call.message))
|
|
255
284
|
assert.ok(condaInstall)
|
|
285
|
+
assert.equal(condaInstall.message[2], `python -I -m pip --isolated --disable-pip-version-check install --no-index --no-deps "${PIP_INSTALL_SPEC}"`)
|
|
256
286
|
assert.equal(
|
|
257
287
|
condaInstall.message[1],
|
|
258
|
-
'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0"'
|
|
288
|
+
'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"'
|
|
259
289
|
)
|
|
260
290
|
assert.equal(
|
|
261
291
|
await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
|
|
262
|
-
'conda ==26.5.3'
|
|
292
|
+
['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
|
|
263
293
|
)
|
|
264
294
|
})
|
|
265
295
|
|
|
@@ -287,7 +317,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
|
|
|
287
317
|
assert.ok(condaInstall)
|
|
288
318
|
assert.equal(
|
|
289
319
|
condaInstall.message[1],
|
|
290
|
-
'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0"'
|
|
320
|
+
'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"'
|
|
291
321
|
)
|
|
292
322
|
})
|
|
293
323
|
|
|
@@ -319,7 +349,7 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
|
|
|
319
349
|
assert.ok(condaInstall)
|
|
320
350
|
assert.equal(
|
|
321
351
|
condaInstall.message[1],
|
|
322
|
-
'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" uv=0.11.23 nodejs=22.21.1 pnpm'
|
|
352
|
+
'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'
|
|
323
353
|
)
|
|
324
354
|
})
|
|
325
355
|
|
|
@@ -583,3 +613,91 @@ test('Conda install reports recovery details when the old runtime cannot be remo
|
|
|
583
613
|
fsModule.promises.rm = originalRm
|
|
584
614
|
}
|
|
585
615
|
})
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
test('stale certificate packages trigger the existing Conda setup checks', async t => {
|
|
619
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-certs-'))
|
|
620
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
621
|
+
await writeHealthyCondaMeta(root, 'darwin')
|
|
622
|
+
await writeFakeManagedConda(root, 'darwin')
|
|
623
|
+
for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
|
|
624
|
+
await fs.rm(path.join(root, 'bin/miniforge/conda-meta', `${name}-${version}.json`))
|
|
625
|
+
await writeCondaMeta(root, name, '2026.4.22')
|
|
626
|
+
}
|
|
627
|
+
assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
|
|
628
|
+
const bin = createBin(root, 'darwin')
|
|
629
|
+
await bin.tryList()
|
|
630
|
+
assert.equal(bin.correct_conda, false)
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
test('tampered Miniforge installers leave the existing runtime untouched', async t => {
|
|
634
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-hash-'))
|
|
635
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
636
|
+
await createMiniforge(root)
|
|
637
|
+
const kernel = createKernel(root, 'darwin')
|
|
638
|
+
kernel.bin.download = async () => fs.writeFile(path.join(root, 'bin/installer.sh'), 'tampered')
|
|
639
|
+
const conda = new Conda()
|
|
640
|
+
conda.kernel = kernel
|
|
641
|
+
await assert.rejects(conda._install({}, () => {}), /SHA-256 verification failed/)
|
|
642
|
+
assert.equal(await pathExists(path.join(root, 'bin/miniforge/Scripts/conda.exe')), true)
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
test('old pip cannot pass Conda readiness even with current CA packages', async t => {
|
|
647
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-ready-'))
|
|
648
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
649
|
+
await writeHealthyCondaMeta(root, 'darwin')
|
|
650
|
+
await writeFakeManagedConda(root, 'darwin')
|
|
651
|
+
await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '26.2.1')
|
|
652
|
+
assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
|
|
653
|
+
const bin = createBin(root, 'darwin')
|
|
654
|
+
await bin.tryList()
|
|
655
|
+
assert.equal(bin.correct_conda, false)
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
test('certificate refresh preserves named environments and uses the new CA bundle', async t => {
|
|
659
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-refresh-'))
|
|
660
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
661
|
+
await writeHealthyCondaMeta(root, 'darwin')
|
|
662
|
+
await writeFakeManagedConda(root, 'darwin')
|
|
663
|
+
const marker = path.join(root, 'bin/miniforge/envs/app/keep')
|
|
664
|
+
await fs.mkdir(path.dirname(marker), { recursive: true })
|
|
665
|
+
await fs.writeFile(marker, 'existing app')
|
|
666
|
+
const kernel = createKernel(root, 'darwin')
|
|
667
|
+
const calls = []
|
|
668
|
+
kernel.bin.download = async () => assert.fail('must not replace a working runtime')
|
|
669
|
+
kernel.bin.exec = async payload => { calls.push(payload); return {} }
|
|
670
|
+
await createConda(kernel).install({}, () => {})
|
|
671
|
+
assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
|
|
672
|
+
assert.equal(calls.length, 2)
|
|
673
|
+
assert.equal(calls[1].env.CONDA_SSL_VERIFY, path.join(root, 'bin/miniforge/certificates.pem'))
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
test('failed pip bootstrap stops certificate refresh without replacing environments', async t => {
|
|
677
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-failed-'))
|
|
678
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
679
|
+
await writeHealthyCondaMeta(root, 'darwin')
|
|
680
|
+
await writeFakeManagedConda(root, 'darwin')
|
|
681
|
+
const kernel = createKernel(root, 'darwin')
|
|
682
|
+
kernel.bin.exec = async () => ({ error: ['ERROR: hash verification failed'] })
|
|
683
|
+
kernel.bin.download = async () => assert.fail('must preserve the runtime')
|
|
684
|
+
await assert.rejects(createConda(kernel).install({}, () => {}), /bootstrap failed/)
|
|
685
|
+
assert.equal(await pathExists(path.join(root, 'bin/miniforge/conda-meta')), true)
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
test('fresh installation does not report a failed pip bootstrap as finished', async t => {
|
|
689
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-failed-'))
|
|
690
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
691
|
+
await fs.mkdir(path.join(root, 'bin'), { recursive: true })
|
|
692
|
+
const kernel = createKernel(root, 'darwin')
|
|
693
|
+
kernel.bin.mods = []
|
|
694
|
+
kernel.bin.download = async () => {}
|
|
695
|
+
kernel.bin.exec = async payload => {
|
|
696
|
+
if (payload.conda?.skip) {
|
|
697
|
+
await fs.mkdir(path.join(root, 'bin/miniforge/conda-meta'), { recursive: true })
|
|
698
|
+
return {}
|
|
699
|
+
}
|
|
700
|
+
return { error: ['ERROR: pip download failed'] }
|
|
701
|
+
}
|
|
702
|
+
await assert.rejects(createConda(kernel)._install({}, () => {}), /pip installation failed/)
|
|
703
|
+
})
|
package/test/node-bin.test.js
CHANGED
|
@@ -3,7 +3,7 @@ const test = require('node:test')
|
|
|
3
3
|
|
|
4
4
|
const Node = require('../kernel/bin/node')
|
|
5
5
|
|
|
6
|
-
function createNode({ nodeVersion = '24.
|
|
6
|
+
function createNode({ nodeVersion = '24.22.1', pnpmVersion = '11.9.0', hasNode = true, hasPnpm = true } = {}) {
|
|
7
7
|
const node = new Node()
|
|
8
8
|
const conda = new Set()
|
|
9
9
|
if (hasNode) conda.add('nodejs')
|
|
@@ -23,12 +23,12 @@ function createNode({ nodeVersion = '24.18.0', pnpmVersion = '11.9.0', hasNode =
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
test('Node bin pins Node.js 24 LTS and matching pnpm', () => {
|
|
26
|
-
assert.equal(createNode().cmd(), 'nodejs=24.
|
|
26
|
+
assert.equal(createNode().cmd(), 'nodejs=24.22.1 pnpm=11.9.0')
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
test('Node.installed accepts exactly the pinned Node.js and pnpm versions', async () => {
|
|
30
30
|
assert.equal(await createNode().installed(), true)
|
|
31
|
-
assert.equal(await createNode({ nodeVersion: '24.
|
|
31
|
+
assert.equal(await createNode({ nodeVersion: '24.22.1-h654b19f_0' }).installed(), true)
|
|
32
32
|
assert.equal(await createNode({ pnpmVersion: '11.9.0-h7c87c79_0' }).installed(), true)
|
|
33
33
|
assert.equal(await createNode({ nodeVersion: '22.21.1' }).installed(), false)
|
|
34
34
|
assert.equal(await createNode({ nodeVersion: '26.4.0' }).installed(), false)
|
|
@@ -377,3 +377,49 @@ test('redactEnvArgs summarizes protected argv env values', () => {
|
|
|
377
377
|
truncated: false
|
|
378
378
|
})
|
|
379
379
|
})
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
test('new venv bootstrap failure prevents app commands even with chain:*', { skip: process.platform === 'win32' }, async t => {
|
|
383
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-bootstrap-'))
|
|
384
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
385
|
+
const kernel = createKernel(root)
|
|
386
|
+
kernel.bin = { path: (...parts) => path.join(root, 'bin', ...parts) }
|
|
387
|
+
const shell = createShell(kernel)
|
|
388
|
+
shell.shell = 'bash'
|
|
389
|
+
shell.env = {}
|
|
390
|
+
shell.conda_hook = () => ':'
|
|
391
|
+
const fakeBin = path.join(root, 'fake-bin')
|
|
392
|
+
await fs.mkdir(fakeBin)
|
|
393
|
+
await fs.writeFile(path.join(fakeBin, 'python'), '#!/bin/sh\nif [ "$1" = "-m" ]; then mkdir -p "$3/bin"; touch "$3/bin/activate"; exit 0; fi\nexit 19\n', { mode: 0o755 })
|
|
394
|
+
const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', chain: '*', message: `touch '${root}/app-ran'` })
|
|
395
|
+
const result = require('node:child_process').spawnSync('bash', ['--noprofile', '--norc', '-c', shell.build(params)], { env: { ...process.env, PATH: fakeBin + ':' + process.env.PATH }, encoding: 'utf8' })
|
|
396
|
+
assert.notEqual(result.status, 0, result.stderr)
|
|
397
|
+
assert.equal(await fs.access(path.join(root, 'app-ran')).then(() => true, () => false), false)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('existing app environments do not run the creation-only pip bootstrap', async t => {
|
|
401
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-existing-env-'))
|
|
402
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
403
|
+
await fs.mkdir(path.join(root, 'env'))
|
|
404
|
+
const shell = createShell(createKernel(root))
|
|
405
|
+
shell.shell = 'bash'
|
|
406
|
+
const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', message: 'echo app' })
|
|
407
|
+
assert.doesNotMatch(shell.build(params), /pip.*install/)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
test('existing venv with outdated pip cannot bypass the guard on a retry', { skip: process.platform === 'win32' }, async t => {
|
|
412
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-retry-'))
|
|
413
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
414
|
+
await fs.mkdir(path.join(root, 'env/bin'), { recursive: true })
|
|
415
|
+
await fs.writeFile(path.join(root, 'env/bin/activate'), '')
|
|
416
|
+
const fakeBin = path.join(root, 'fake-bin')
|
|
417
|
+
await fs.mkdir(fakeBin)
|
|
418
|
+
await fs.writeFile(path.join(fakeBin, 'python'), '#!/bin/sh\nexit 19\n', { mode: 0o755 })
|
|
419
|
+
const shell = createShell(createKernel(root))
|
|
420
|
+
shell.shell = 'bash'
|
|
421
|
+
const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', chain: '*', message: `touch '${root}/app-ran'` })
|
|
422
|
+
const result = require('node:child_process').spawnSync('bash', ['--noprofile', '--norc', '-c', shell.build(params)], { env: { ...process.env, PATH: fakeBin + ':' + process.env.PATH }, encoding: 'utf8' })
|
|
423
|
+
assert.notEqual(result.status, 0, result.stderr)
|
|
424
|
+
assert.equal(await fs.access(path.join(root, 'app-ran')).then(() => true, () => false), false)
|
|
425
|
+
})
|
package/test/uv-bin.test.js
CHANGED
|
@@ -16,13 +16,13 @@ function createUV(version) {
|
|
|
16
16
|
return uv
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
test('UV bin pins uv to 0.
|
|
20
|
-
assert.equal(createUV('0.
|
|
19
|
+
test('UV bin pins uv to 0.13.4', () => {
|
|
20
|
+
assert.equal(createUV('0.13.4').cmd(), 'uv=0.13.4')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
test('UV.installed accepts exactly uv 0.
|
|
24
|
-
assert.equal(await createUV('0.
|
|
25
|
-
assert.equal(await createUV('0.
|
|
23
|
+
test('UV.installed accepts exactly uv 0.13.4', async () => {
|
|
24
|
+
assert.equal(await createUV('0.13.4').installed(), true)
|
|
25
|
+
assert.equal(await createUV('0.13.4-h1234567_0').installed(), true)
|
|
26
26
|
assert.equal(await createUV('0.11.22').installed(), false)
|
|
27
27
|
assert.equal(await createUV('0.12.0').installed(), false)
|
|
28
28
|
assert.equal(await createUV(null).installed(), false)
|