pinokiod 8.2.0 → 8.2.1

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.
@@ -3,6 +3,7 @@ const portfinder = require('portfinder-cp')
3
3
  const fs = require('fs')
4
4
  const Util = require('./util')
5
5
  const ManagedSkills = require('./managed_skills')
6
+ const { pipConfigDisabledPath } = require('./pip_env')
6
7
  const TEMP_ENV_KEYS = ["TMP", "TEMP", "TMPDIR", "PIP_TMPDIR"]
7
8
  const CACHE_ENV_KEYS = ["UV_CACHE_DIR", "PIP_CACHE_DIR"]
8
9
  const CACHE_PREFLIGHT_KEYS = TEMP_ENV_KEYS.concat(CACHE_ENV_KEYS)
@@ -311,7 +312,7 @@ const ENVS = async () => {
311
312
  type: ["system"],
312
313
  key: "PIP_CONFIG_FILE",
313
314
  hidden: true,
314
- val: "./pipconfig"
315
+ val: pipConfigDisabledPath(process.platform)
315
316
  }, {
316
317
  type: ["system"],
317
318
  key: "CONDARC",
@@ -0,0 +1,30 @@
1
+ const fs = require('fs/promises')
2
+ const path = require('path')
3
+
4
+ function pipConfigDisabledPath(platform) {
5
+ // pip compares this string exactly with Python's os.devnull.
6
+ return platform === 'win32' ? 'nul' : '/dev/null'
7
+ }
8
+
9
+ async function normalizeLegacyPipConfig(source, { platform, homedir }) {
10
+ if (typeof source.PIP_CONFIG_FILE !== 'string') return source
11
+ const legacyPath = path.resolve(homedir, 'pipconfig')
12
+ const configuredPath = path.resolve(source.PIP_CONFIG_FILE)
13
+ const comparable = value => platform === 'win32' ? value.toLowerCase() : value
14
+ if (comparable(configuredPath) !== comparable(legacyPath)) return source
15
+
16
+ try {
17
+ // The old config may have been replaced with a directory, device, or pipe.
18
+ if (!(await fs.stat(legacyPath)).isFile()) return source
19
+ const contents = await fs.readFile(legacyPath, 'utf8')
20
+ if (!/^\s*\[global\]\s+timeout\s*=\s*1000\s*$/.test(contents)) return source
21
+ } catch (error) {
22
+ // Failure to recognize the stock config must not prevent shell startup.
23
+ if (error.code !== 'ENOENT') return source
24
+ }
25
+ // Existing ENVIRONMENT files point at this generated timeout-only config.
26
+ // Retain customized configs as intentional overrides; never rewrite files.
27
+ return { ...source, PIP_CONFIG_FILE: pipConfigDisabledPath(platform) }
28
+ }
29
+
30
+ module.exports = { normalizeLegacyPipConfig, pipConfigDisabledPath }
package/kernel/shell.js CHANGED
@@ -15,6 +15,7 @@ const sudo = require("sudo-prompt-programfiles-x86");
15
15
  const unparse = require('yargs-unparser-custom-flag');
16
16
  const Util = require('./util')
17
17
  const Environment = require('./environment')
18
+ const { normalizeLegacyPipConfig, pipConfigDisabledPath } = require('./pip_env')
18
19
  const { applyWindowsNodePackageManagerEnv } = require('./windows_node_package_manager_env')
19
20
  const ShellParser = require('./shell_parser')
20
21
  const AnsiStreamTracker = require('./ansi_stream_tracker')
@@ -223,6 +224,10 @@ class Shell {
223
224
  let app_env = await Environment.get(api_path, this.kernel)
224
225
  this.env = Object.assign(this.env, app_env)
225
226
  }
227
+ this.env = await normalizeLegacyPipConfig(this.env, {
228
+ platform: this.platform,
229
+ homedir: this.kernel.homedir,
230
+ })
226
231
  stripInheritedCondaActivationState(this.env)
227
232
  let PATH_KEY = Object.keys(this.env).find((key) => key.toLowerCase() === "path") || "PATH";
228
233
  if (!this.env[PATH_KEY]) {
@@ -290,6 +295,14 @@ class Shell {
290
295
  }
291
296
  }
292
297
 
298
+ // An explicitly selected config supplies its own timeout defaults.
299
+ if (this.env.PIP_CONFIG_FILE === pipConfigDisabledPath(this.platform)) {
300
+ const hasPipTimeout = Object.keys(this.env).some(key => {
301
+ const name = this.platform === 'win32' ? key.toUpperCase() : key
302
+ return (name === 'PIP_TIMEOUT' || name === 'PIP_DEFAULT_TIMEOUT') && this.env[key] !== undefined
303
+ })
304
+ if (!hasPipTimeout) this.env.PIP_TIMEOUT = "1000"
305
+ }
293
306
  setDefaultEnvValue(this.env, "UV_HTTP_TIMEOUT", "60")
294
307
  setDefaultEnvValue(this.env, "HF_HUB_DISABLE_UPDATE_CHECK", "1")
295
308
  setDefaultEnvValue(this.env, "HF_TOKEN_PATH", this.kernel.envs?.HF_TOKEN_PATH || path.resolve(this.kernel.homedir, "cache", "HF_AUTH", "token"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.0",
3
+ "version": "8.2.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,210 @@
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 { spawnSync } = require('node:child_process')
6
+ const test = require('node:test')
7
+ const Shell = require('../kernel/shell')
8
+ const Environment = require('../kernel/environment')
9
+ const { pipConfigDisabledPath } = require('../kernel/pip_env')
10
+
11
+ async function fixture(t) {
12
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-env-'))
13
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
14
+ const appDir = path.join(root, 'api', 'demo')
15
+ const script = path.join(appDir, 'install.js')
16
+ await fs.mkdir(appDir, { recursive: true })
17
+ await fs.writeFile(script, '')
18
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'PIP_CONFIG_FILE=./pipconfig\nPIP_CACHE_DIR=./cache/PIP_CACHE_DIR\n')
19
+ await fs.writeFile(path.join(root, 'pipconfig'), '[global]\r\n timeout = 1000\r\n')
20
+ const kernel = {
21
+ homedir: root,
22
+ envs: {},
23
+ bracketedPasteSupport: {},
24
+ connect: { keys: async () => null },
25
+ exists: async filepath => fs.access(filepath).then(() => true).catch(() => false),
26
+ which: () => null,
27
+ path: (...parts) => path.join(root, ...parts),
28
+ }
29
+ async function shell(platform = process.platform, env = {}) {
30
+ const shell = new Shell(kernel)
31
+ shell.platform = platform
32
+ await shell.init_env({ path: appDir, $parent: { path: script }, env })
33
+ return shell
34
+ }
35
+ return { root, appDir, kernel, shell }
36
+ }
37
+
38
+ test('managed shells preserve inherited pip options and migrate stock config without editing files', async t => {
39
+ const { root, shell } = await fixture(t)
40
+ const host = {
41
+ PIP_CONFIG_FILE: 'host.ini', PIP_INDEX_URL: 'https://host.invalid',
42
+ PIP_EXTRA_INDEX_URL: 'https://nvidia.invalid', PIP_TRUSTED_HOST: 'nvidia.invalid',
43
+ PIP_NO_CACHE_DIR: 'true', PIP_NO_INDEX: 'true', PIP_TARGET: 'host-target',
44
+ pip_constraint: 'host-constraints.txt', PIP_TIMEOUT: '1', PIP_CACHE_DIR: 'host-cache',
45
+ PIP_CERT: 'host-ca.pem', PIP_PROXY: 'http://host-proxy.invalid', PIP_TMPDIR: 'host-temp',
46
+ }
47
+ const before = Object.fromEntries(Object.keys(host).map(key => [key, process.env[key]]))
48
+ Object.assign(process.env, host)
49
+ t.after(() => {
50
+ for (const [key, value] of Object.entries(before)) {
51
+ if (value === undefined) delete process.env[key]
52
+ else process.env[key] = value
53
+ }
54
+ })
55
+ for (const platform of ['win32', 'darwin', 'linux']) {
56
+ const managed = await shell(platform)
57
+ assert.equal(managed.env.PIP_CONFIG_FILE, platform === 'win32' ? 'nul' : '/dev/null')
58
+ assert.equal(managed.env.PIP_TIMEOUT, '1')
59
+ assert.equal(managed.env.PIP_CACHE_DIR, path.join(root, 'cache', 'PIP_CACHE_DIR'))
60
+ for (const key of Object.keys(host).filter(key => !['PIP_CONFIG_FILE', 'PIP_CACHE_DIR'].includes(key))) {
61
+ assert.equal(managed.env[key], host[key], key)
62
+ }
63
+ }
64
+ assert.equal(await fs.readFile(path.join(root, 'ENVIRONMENT'), 'utf8'), 'PIP_CONFIG_FILE=./pipconfig\nPIP_CACHE_DIR=./cache/PIP_CACHE_DIR\n')
65
+ assert.equal(await fs.readFile(path.join(root, 'pipconfig'), 'utf8'), '[global]\r\n timeout = 1000\r\n')
66
+ for (const [key, value] of Object.entries(host)) assert.equal(process.env[key], value)
67
+ })
68
+
69
+ test('Pinokio, app and step settings override managed pip defaults in order', async t => {
70
+ const { root, appDir, shell } = await fixture(t)
71
+ await fs.appendFile(path.join(root, 'ENVIRONMENT'), 'PIP_INDEX_URL=https://mirror.invalid/simple\nPIP_TIMEOUT=120\n')
72
+ const system = await shell()
73
+ assert.equal(system.env.PIP_INDEX_URL, 'https://mirror.invalid/simple')
74
+ assert.equal(system.env.PIP_TIMEOUT, '120')
75
+ await fs.writeFile(path.join(appDir, 'ENVIRONMENT'), 'PIP_INDEX_URL=https://app.invalid/simple\nPIP_EXTRA_INDEX_URL=https://app-extra.invalid/simple\n')
76
+ assert.equal((await shell()).env.PIP_INDEX_URL, 'https://app.invalid/simple')
77
+ const step = await shell(process.platform, {
78
+ PIP_INDEX_URL: 'https://step.invalid/simple', PIP_CONFIG_FILE: path.join(root, 'explicit.ini'),
79
+ PIP_NO_CACHE_DIR: 'true', PIP_TIMEOUT: '60',
80
+ })
81
+ assert.equal(step.env.PIP_INDEX_URL, 'https://step.invalid/simple')
82
+ assert.equal(step.env.PIP_EXTRA_INDEX_URL, 'https://app-extra.invalid/simple')
83
+ assert.equal(step.env.PIP_CONFIG_FILE, path.join(root, 'explicit.ini'))
84
+ assert.equal(step.env.PIP_NO_CACHE_DIR, 'true')
85
+ assert.equal(step.env.PIP_TIMEOUT, '60')
86
+ })
87
+
88
+ test('migration retains custom pip configs and isolates missing stock configs', async t => {
89
+ const { root, appDir, shell } = await fixture(t)
90
+ await fs.writeFile(path.join(root, 'pipconfig'), '[global]\ntimeout = 120\nindex-url = https://mirror.invalid/simple\n')
91
+ const custom = await shell()
92
+ assert.equal(custom.env.PIP_CONFIG_FILE, path.join(root, 'pipconfig'))
93
+ assert.equal(custom.env.PIP_TIMEOUT, undefined)
94
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'PIP_CONFIG_FILE=./custom.ini\n')
95
+ assert.equal((await shell()).env.PIP_CONFIG_FILE, path.join(root, 'custom.ini'))
96
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'PIP_CONFIG_FILE=./pipconfig\n')
97
+ await fs.rm(path.join(root, 'pipconfig'))
98
+ await fs.writeFile(path.join(appDir, 'ENVIRONMENT'), `PIP_CONFIG_FILE=${path.join(root, 'pipconfig')}\n`)
99
+ assert.equal((await shell('win32')).env.PIP_CONFIG_FILE, 'nul')
100
+ })
101
+
102
+ test('new system ENVIRONMENT uses the null device instead of the legacy pipconfig', async t => {
103
+ const { root, kernel } = await fixture(t)
104
+ const generated = await Environment.ENV('system', root, kernel)
105
+ assert.ok(generated.split('\n').includes(`PIP_CONFIG_FILE=${process.platform === 'win32' ? 'nul' : '/dev/null'}`))
106
+ })
107
+
108
+ test('config isolation adds no cache/temp fallbacks and retains the timeout alias', async t => {
109
+ const { root, shell } = await fixture(t)
110
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'PIP_CONFIG_FILE=./pipconfig\n')
111
+ const managed = await shell(process.platform, { PIP_DEFAULT_TIMEOUT: '120' })
112
+ assert.equal(managed.env.PIP_CACHE_DIR, process.env.PIP_CACHE_DIR)
113
+ assert.equal(managed.env.PIP_TMPDIR, process.env.PIP_TMPDIR)
114
+ assert.equal(managed.env.PIP_TIMEOUT, process.env.PIP_TIMEOUT)
115
+ assert.equal(managed.env.PIP_DEFAULT_TIMEOUT, '120')
116
+ assert.equal((await shell(process.platform, { PIP_TIMEOUT: 60 })).env.PIP_TIMEOUT, 60)
117
+ assert.equal((await shell(process.platform, { PIP_DEFAULT_TIMEOUT: 0 })).env.PIP_TIMEOUT, process.env.PIP_TIMEOUT)
118
+ })
119
+
120
+ test('an unreadable or non-file legacy pipconfig does not prevent shell startup', async t => {
121
+ const { root, shell } = await fixture(t)
122
+ const legacyPath = path.join(root, 'pipconfig')
123
+ const readFile = fs.readFile
124
+ const mock = t.mock.method(fs, 'readFile', async (filepath, ...args) => {
125
+ if (filepath === legacyPath) throw Object.assign(new Error('Access denied'), { code: 'EACCES' })
126
+ return readFile(filepath, ...args)
127
+ })
128
+ assert.equal((await shell()).env.PIP_CONFIG_FILE, legacyPath)
129
+ mock.mock.restore()
130
+ await fs.rm(legacyPath)
131
+ await fs.mkdir(legacyPath)
132
+ assert.equal((await shell()).env.PIP_CONFIG_FILE, legacyPath)
133
+ })
134
+
135
+ test('Windows timeout defaults respect mixed-case names and numeric values', async t => {
136
+ const { shell } = await fixture(t)
137
+ const timeout = await shell('win32', { PIP_TIMEOUT: undefined, PIP_DEFAULT_TIMEOUT: undefined, pip_timeout: 60 })
138
+ assert.equal(timeout.env.PIP_TIMEOUT, undefined)
139
+ assert.equal(timeout.env.pip_timeout, 60)
140
+ const alias = await shell('win32', { PIP_TIMEOUT: undefined, PIP_DEFAULT_TIMEOUT: undefined, pip_default_timeout: 0 })
141
+ assert.equal(alias.env.PIP_TIMEOUT, undefined)
142
+ assert.equal(alias.env.pip_default_timeout, 0)
143
+ const unix = await shell('darwin', { PIP_TIMEOUT: undefined, PIP_DEFAULT_TIMEOUT: undefined, pip_timeout: 60 })
144
+ assert.equal(unix.env.PIP_TIMEOUT, '1000')
145
+ })
146
+
147
+ test('an app custom config avoids inspecting the root legacy file', async t => {
148
+ const { root, appDir, shell } = await fixture(t)
149
+ await fs.writeFile(path.join(appDir, 'ENVIRONMENT'), 'PIP_CONFIG_FILE=./custom.ini\n')
150
+ const readFile = fs.readFile
151
+ let legacyReads = 0
152
+ t.mock.method(fs, 'readFile', async (filepath, ...args) => {
153
+ if (filepath === path.join(root, 'pipconfig')) legacyReads++
154
+ return readFile(filepath, ...args)
155
+ })
156
+ assert.equal((await shell()).env.PIP_CONFIG_FILE, path.join(appDir, 'custom.ini'))
157
+ assert.equal(legacyReads, 0)
158
+ })
159
+
160
+ test('real pip ignores global, user and site config while retaining cache, timeout and launcher flags', async t => {
161
+ const python = process.env.PINOKIO_TEST_PYTHON || (process.platform === 'win32' ? 'python' : 'python3')
162
+ const available = spawnSync(python, ['-m', 'pip', '--version'], { encoding: 'utf8' })
163
+ if (available.status !== 0) return t.skip('Python with pip is not available')
164
+ const { shell } = await fixture(t)
165
+ const managed = await shell()
166
+ const code = `
167
+ import json, os, tempfile
168
+ from pathlib import Path
169
+ from unittest.mock import patch
170
+ import pip._internal.configuration as cfg
171
+ from pip._internal.commands import create_command
172
+ with tempfile.TemporaryDirectory() as tmp:
173
+ root = Path(tmp)
174
+ locations = {}
175
+ snapshots = {}
176
+ for kind in ('global', 'user', 'site'):
177
+ target = root / (kind + '.ini')
178
+ target.write_text('[global]\\nextra-index-url = https://' + kind + '.invalid/simple\\ntrusted-host = nvidia.invalid\\nno-cache-dir = true\\n')
179
+ locations[kind] = [str(target)]
180
+ snapshots[target] = target.read_bytes()
181
+ for platform, sentinel in [('win32', 'nul'), ('darwin', '/dev/null'), ('linux', '/dev/null')]:
182
+ configured = json.loads(os.environ['PINOKIO_TEST_PIP_PATHS'])[platform]
183
+ # Model only the Windows os.devnull string; this is not native Windows execution.
184
+ with patch.object(cfg, 'get_configuration_files', return_value=locations), patch.object(cfg.os, 'devnull', sentinel):
185
+ with patch.dict(os.environ, {'PIP_CONFIG_FILE': configured}):
186
+ for flags in ([], ['--extra-index-url', 'https://download.pytorch.org/whl/cu130']):
187
+ options, _ = create_command('install').parse_args(flags)
188
+ assert options.index_url == 'https://pypi.org/simple', options.index_url
189
+ assert options.extra_index_urls == ([] if not flags else [flags[1]]), options.extra_index_urls
190
+ assert not options.trusted_hosts, options.trusted_hosts
191
+ assert options.cache_dir == os.environ['PIP_CACHE_DIR'], options.cache_dir
192
+ assert options.timeout == 1000, options.timeout
193
+ options, _ = create_command('install').parse_args(['--index-url', 'https://launcher.invalid/simple'])
194
+ assert options.index_url == 'https://launcher.invalid/simple'
195
+ if platform == 'win32':
196
+ # Negative control: the old uppercase value must NOT disable configs.
197
+ with patch.dict(os.environ, {'PIP_CONFIG_FILE': 'NUL'}):
198
+ options, _ = create_command('install').parse_args([])
199
+ assert options.extra_index_urls == ['https://site.invalid/simple']
200
+ for target, snapshot in snapshots.items():
201
+ assert target.read_bytes() == snapshot
202
+ `
203
+ const env = Object.fromEntries(Object.entries(managed.env).filter(([key]) => !key.toUpperCase().startsWith('PIP_')))
204
+ Object.assign(env, {
205
+ PIP_CONFIG_FILE: managed.env.PIP_CONFIG_FILE, PIP_CACHE_DIR: managed.env.PIP_CACHE_DIR, PIP_TIMEOUT: '1000',
206
+ PINOKIO_TEST_PIP_PATHS: JSON.stringify(Object.fromEntries(['win32', 'darwin', 'linux'].map(platform => [platform, pipConfigDisabledPath(platform)]))),
207
+ })
208
+ const result = spawnSync(python, ['-c', code], { encoding: 'utf8', env })
209
+ assert.equal(result.status, 0, result.stderr || result.error?.message)
210
+ })