pinokiod 8.0.30 → 8.0.31
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 +10 -2
- package/kernel/connect/providers/huggingface/index.js +2 -0
- package/kernel/python_env.js +14 -0
- package/kernel/util.js +4 -1
- package/package.json +1 -1
- package/test/conda-bin.test.js +61 -0
- package/test/filepicker-python-env.test.js +55 -0
- package/test/huggingface-connect.test.js +35 -0
package/kernel/bin/conda-meta.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { execFile } = require('child_process')
|
|
4
|
+
const { managedPythonEnv } = require('../python_env')
|
|
4
5
|
|
|
5
6
|
async function buildCondaListFromMeta(minicondaPath, useCondaList) {
|
|
6
7
|
if (!useCondaList) {
|
|
@@ -59,7 +60,10 @@ async function runCondaList(minicondaPath) {
|
|
|
59
60
|
return { response: '', source: 'conda-list' }
|
|
60
61
|
}
|
|
61
62
|
return await new Promise((resolve) => {
|
|
62
|
-
execFile(condaBinary, ['list'], {
|
|
63
|
+
execFile(condaBinary, ['list'], {
|
|
64
|
+
env: managedPythonEnv(),
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
}, (err, stdout) => {
|
|
63
67
|
if (err) {
|
|
64
68
|
resolve({ response: '', source: 'conda-list' })
|
|
65
69
|
} else {
|
|
@@ -75,7 +79,11 @@ async function managedCondaRuns(minicondaPath, platform = process.platform) {
|
|
|
75
79
|
return false
|
|
76
80
|
}
|
|
77
81
|
return await new Promise((resolve) => {
|
|
78
|
-
execFile(condaBinary, ['--version'], {
|
|
82
|
+
execFile(condaBinary, ['--version'], {
|
|
83
|
+
env: managedPythonEnv(),
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
timeout: 15000,
|
|
86
|
+
}, (err) => {
|
|
79
87
|
resolve(!err)
|
|
80
88
|
})
|
|
81
89
|
})
|
|
@@ -3,6 +3,7 @@ const fs = require('fs')
|
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const fetch = require('cross-fetch')
|
|
5
5
|
const Environment = require('../../../environment')
|
|
6
|
+
const { managedPythonEnv } = require('../../../python_env')
|
|
6
7
|
|
|
7
8
|
const stripAnsi = (value) => String(value || "").replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "")
|
|
8
9
|
|
|
@@ -56,6 +57,7 @@ class Huggingface {
|
|
|
56
57
|
if (context.env && typeof context.env === "object") {
|
|
57
58
|
env = Object.assign(env, context.env)
|
|
58
59
|
}
|
|
60
|
+
env = managedPythonEnv(env)
|
|
59
61
|
if (!env.HF_TOKEN_PATH) {
|
|
60
62
|
env.HF_TOKEN_PATH = this.defaultTokenPath()
|
|
61
63
|
} else if (!path.isAbsolute(env.HF_TOKEN_PATH)) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function managedPythonEnv(source = process.env) {
|
|
2
|
+
const env = { ...(source || {}) }
|
|
3
|
+
for (const key of Object.keys(env)) {
|
|
4
|
+
if (["PYTHONNOUSERSITE", "PYTHONPATH"].includes(key.toUpperCase())) {
|
|
5
|
+
delete env[key]
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
env.PYTHONNOUSERSITE = "1"
|
|
9
|
+
return env
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
managedPythonEnv,
|
|
14
|
+
}
|
package/kernel/util.js
CHANGED
|
@@ -13,6 +13,7 @@ const child_process = require('node:child_process');
|
|
|
13
13
|
const {auto: normalizeEOL} = require("eol");
|
|
14
14
|
const {EOL} = require("os");
|
|
15
15
|
const { randomUUID } = require('crypto');
|
|
16
|
+
const { managedPythonEnv } = require('./python_env')
|
|
16
17
|
const fsp = fs.promises;
|
|
17
18
|
const breakPattern = /\n/g;
|
|
18
19
|
const breakReplacement = "\\n";
|
|
@@ -203,7 +204,9 @@ const filepicker = async(req, ondata, kernel) => {
|
|
|
203
204
|
} else {
|
|
204
205
|
python = kernel.path("bin/miniforge/bin/python")
|
|
205
206
|
}
|
|
206
|
-
const proc = spawn(python, [picker_path]
|
|
207
|
+
const proc = spawn(python, [picker_path], {
|
|
208
|
+
env: managedPythonEnv(),
|
|
209
|
+
})
|
|
207
210
|
|
|
208
211
|
let output = "";
|
|
209
212
|
proc.stdout.on("data", (chunk) => output += chunk);
|
package/package.json
CHANGED
package/test/conda-bin.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict')
|
|
2
|
+
const childProcess = require('node:child_process')
|
|
2
3
|
const fsModule = require('node:fs')
|
|
3
4
|
const fs = require('node:fs/promises')
|
|
4
5
|
const os = require('node:os')
|
|
@@ -75,6 +76,14 @@ async function createLegacyMiniconda(root) {
|
|
|
75
76
|
return createCondaRoot(root, 'miniconda')
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function restoreEnv(name, value) {
|
|
80
|
+
if (typeof value === 'undefined') {
|
|
81
|
+
delete process.env[name]
|
|
82
|
+
} else {
|
|
83
|
+
process.env[name] = value
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
function createConda(kernel) {
|
|
79
88
|
const conda = new Conda()
|
|
80
89
|
conda.kernel = kernel
|
|
@@ -94,6 +103,58 @@ function createBin(root, platform = 'win32') {
|
|
|
94
103
|
return bin
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
test('managed Conda subprocesses ignore external Python packages', async (t) => {
|
|
107
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-env-'))
|
|
108
|
+
const platform = process.platform === 'win32' ? 'win32' : process.platform
|
|
109
|
+
const miniforge = path.join(root, 'bin', 'miniforge')
|
|
110
|
+
const versionCapture = path.join(root, 'version-env.json')
|
|
111
|
+
const listCapture = path.join(root, 'list-env.json')
|
|
112
|
+
const oldCapturePath = process.env.PINOKIO_TEST_ENV_CAPTURE
|
|
113
|
+
const oldNoUserSite = process.env.PYTHONNOUSERSITE
|
|
114
|
+
const oldPythonPath = process.env.PYTHONPATH
|
|
115
|
+
|
|
116
|
+
await writeFakeManagedConda(root, platform)
|
|
117
|
+
const condaMetaPath = require.resolve('../kernel/bin/conda-meta')
|
|
118
|
+
const realExecFile = childProcess.execFile.bind(childProcess)
|
|
119
|
+
delete require.cache[condaMetaPath]
|
|
120
|
+
t.mock.method(childProcess, 'execFile', (_binary, _args, options, done) => {
|
|
121
|
+
return realExecFile(process.execPath, [
|
|
122
|
+
'-e',
|
|
123
|
+
`require('node:fs').writeFileSync(process.env.PINOKIO_TEST_ENV_CAPTURE, JSON.stringify({
|
|
124
|
+
pythonNoUserSite: process.env.PYTHONNOUSERSITE,
|
|
125
|
+
pythonPath: process.env.PYTHONPATH || null
|
|
126
|
+
}))`,
|
|
127
|
+
], options, done)
|
|
128
|
+
})
|
|
129
|
+
t.after(() => {
|
|
130
|
+
delete require.cache[condaMetaPath]
|
|
131
|
+
})
|
|
132
|
+
const { buildCondaListFromMeta, managedCondaRuns } = require('../kernel/bin/conda-meta')
|
|
133
|
+
|
|
134
|
+
process.env.PYTHONNOUSERSITE = '0'
|
|
135
|
+
process.env.PYTHONPATH = 'external-packages'
|
|
136
|
+
try {
|
|
137
|
+
process.env.PINOKIO_TEST_ENV_CAPTURE = versionCapture
|
|
138
|
+
assert.equal(await managedCondaRuns(miniforge, platform), true)
|
|
139
|
+
assert.deepEqual(JSON.parse(await fs.readFile(versionCapture, 'utf8')), {
|
|
140
|
+
pythonNoUserSite: '1',
|
|
141
|
+
pythonPath: null,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
process.env.PINOKIO_TEST_ENV_CAPTURE = listCapture
|
|
145
|
+
await buildCondaListFromMeta(miniforge, true)
|
|
146
|
+
assert.deepEqual(JSON.parse(await fs.readFile(listCapture, 'utf8')), {
|
|
147
|
+
pythonNoUserSite: '1',
|
|
148
|
+
pythonPath: null,
|
|
149
|
+
})
|
|
150
|
+
} finally {
|
|
151
|
+
restoreEnv('PINOKIO_TEST_ENV_CAPTURE', oldCapturePath)
|
|
152
|
+
restoreEnv('PYTHONNOUSERSITE', oldNoUserSite)
|
|
153
|
+
restoreEnv('PYTHONPATH', oldPythonPath)
|
|
154
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
97
158
|
test('Conda uses Miniforge assets and writes conda-forge-only config', async () => {
|
|
98
159
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-miniforge-config-'))
|
|
99
160
|
const conda = createConda(createKernel(root))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const fs = require('node:fs/promises')
|
|
3
|
+
const os = require('node:os')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const test = require('node:test')
|
|
6
|
+
|
|
7
|
+
const Util = require('../kernel/util')
|
|
8
|
+
|
|
9
|
+
function restoreEnv(name, value) {
|
|
10
|
+
if (typeof value === 'undefined') {
|
|
11
|
+
delete process.env[name]
|
|
12
|
+
} else {
|
|
13
|
+
process.env[name] = value
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('file picker isolates its managed Python from external packages', async () => {
|
|
18
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-filepicker-python-env-'))
|
|
19
|
+
const pickerPath = path.join(root, 'picker.js')
|
|
20
|
+
const oldNoUserSite = process.env.PYTHONNOUSERSITE
|
|
21
|
+
const oldPythonPath = process.env.PYTHONPATH
|
|
22
|
+
await fs.writeFile(pickerPath, `
|
|
23
|
+
let input = ''
|
|
24
|
+
process.stdin.setEncoding('utf8')
|
|
25
|
+
process.stdin.on('data', (chunk) => { input += chunk })
|
|
26
|
+
process.stdin.on('end', () => {
|
|
27
|
+
JSON.parse(input)
|
|
28
|
+
process.stdout.write(JSON.stringify({
|
|
29
|
+
paths: [process.env.PYTHONNOUSERSITE, process.env.PYTHONPATH || null]
|
|
30
|
+
}))
|
|
31
|
+
})
|
|
32
|
+
`)
|
|
33
|
+
|
|
34
|
+
process.env.PYTHONNOUSERSITE = '0'
|
|
35
|
+
process.env.PYTHONPATH = 'external-packages'
|
|
36
|
+
try {
|
|
37
|
+
const kernel = {
|
|
38
|
+
platform: 'linux',
|
|
39
|
+
path: (target) => {
|
|
40
|
+
if (target === 'bin/py/picker.py') return pickerPath
|
|
41
|
+
if (target === 'bin/miniforge/bin/python') return process.execPath
|
|
42
|
+
throw new Error(`Unexpected path: ${target}`)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
const result = await Util.filepicker({ params: {} }, () => {}, kernel)
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(result, {
|
|
48
|
+
paths: ['1', null],
|
|
49
|
+
})
|
|
50
|
+
} finally {
|
|
51
|
+
restoreEnv('PYTHONNOUSERSITE', oldNoUserSite)
|
|
52
|
+
restoreEnv('PYTHONPATH', oldPythonPath)
|
|
53
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
54
|
+
}
|
|
55
|
+
})
|
|
@@ -79,6 +79,41 @@ test('Hugging Face connect preserves configured HF paths and ignores ambient HF_
|
|
|
79
79
|
}
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
+
test('Hugging Face connect isolates its managed Python from external packages', async () => {
|
|
83
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-python-env-'))
|
|
84
|
+
const childEnvPath = path.join(root, 'child-env.json')
|
|
85
|
+
await fs.writeFile(
|
|
86
|
+
path.join(root, 'ENVIRONMENT'),
|
|
87
|
+
'PYTHONNOUSERSITE=0\nPYTHONPATH=system-packages\n'
|
|
88
|
+
)
|
|
89
|
+
try {
|
|
90
|
+
const provider = new HuggingfaceConnect(createKernel(root), {})
|
|
91
|
+
provider.hfPath = () => process.execPath
|
|
92
|
+
const { env } = await provider.runHf([
|
|
93
|
+
'-e',
|
|
94
|
+
'require("node:fs").writeFileSync(process.argv[1], JSON.stringify({ pythonNoUserSite: process.env.PYTHONNOUSERSITE, pythonPath: process.env.PYTHONPATH || null }))',
|
|
95
|
+
childEnvPath
|
|
96
|
+
], {}, {
|
|
97
|
+
env: {
|
|
98
|
+
PythonNoUserSite: '0',
|
|
99
|
+
PythonPath: 'command-packages'
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
const childEnv = JSON.parse(await fs.readFile(childEnvPath, 'utf8'))
|
|
103
|
+
|
|
104
|
+
assert.equal(env.PYTHONNOUSERSITE, '1')
|
|
105
|
+
assert.equal(env.PYTHONPATH, undefined)
|
|
106
|
+
assert.equal(env.PythonNoUserSite, undefined)
|
|
107
|
+
assert.equal(env.PythonPath, undefined)
|
|
108
|
+
assert.deepEqual(childEnv, {
|
|
109
|
+
pythonNoUserSite: '1',
|
|
110
|
+
pythonPath: null
|
|
111
|
+
})
|
|
112
|
+
} finally {
|
|
113
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
82
117
|
test('Hugging Face connect uses the app token path for its managed CLI', async () => {
|
|
83
118
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-connect-app-env-'))
|
|
84
119
|
const appDir = path.join(root, 'api', 'demo')
|