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.
- package/kernel/environment.js +2 -1
- package/kernel/pip_env.js +30 -0
- package/kernel/shell.js +13 -0
- package/package.json +1 -1
- package/test/pip-env.test.js +210 -0
package/kernel/environment.js
CHANGED
|
@@ -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:
|
|
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
|
@@ -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
|
+
})
|