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.
@@ -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'], { windowsHide: true }, (err, stdout) => {
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'], { windowsHide: true, timeout: 15000 }, (err) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.30",
3
+ "version": "8.0.31",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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')