pinokiod 7.5.33 → 7.5.35
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.js +126 -0
- package/package.json +1 -1
- package/server/views/app.ejs +4 -1
- package/test/conda-bin.test.js +139 -0
package/kernel/bin/conda.js
CHANGED
|
@@ -69,6 +69,79 @@ const LEGACY_SSL_CERT_DIR_HOOK_FILES = [
|
|
|
69
69
|
"zz_pinokio_unset_ssl_cert_dir-win.ps1",
|
|
70
70
|
"zz_pinokio_unset_ssl_cert_dir-win.sh",
|
|
71
71
|
]
|
|
72
|
+
const WINDOWS_CONDA_ACTIVATE_RELATIVE_PATH = path.join(
|
|
73
|
+
"Lib",
|
|
74
|
+
"site-packages",
|
|
75
|
+
"conda",
|
|
76
|
+
"shell",
|
|
77
|
+
"condabin",
|
|
78
|
+
"_conda_activate.bat"
|
|
79
|
+
)
|
|
80
|
+
const WINDOWS_CONDA_ACTIVATE_PINOKIO_MARKER = ":PINOKIO_APPLY_CONDA_ENV"
|
|
81
|
+
|
|
82
|
+
function windowsCondaActivateOriginalBlock(eol) {
|
|
83
|
+
return [
|
|
84
|
+
" ) ELSE ENDLOCAL & (",
|
|
85
|
+
' FOR /F "tokens=1,* delims==" %%A IN (%%T) DO (',
|
|
86
|
+
' :: A, B = "key", "value"',
|
|
87
|
+
' IF "%CONDA_DEBUG%" == "1" ECHO DEBUG: "%%A=%%B">&2',
|
|
88
|
+
' IF "%%A"=="_CONDA_SCRIPT" (',
|
|
89
|
+
" :: Script execution, fast exit if activation scripts fail",
|
|
90
|
+
' CALL "%%B" || (',
|
|
91
|
+
' ECHO ERROR: Activation script "%%B" failed with code %ERRORLEVEL%.>&2',
|
|
92
|
+
' CALL :DELETE "%%T"',
|
|
93
|
+
' CALL :DELETE "%__conda_tmp%"',
|
|
94
|
+
" EXIT /B 4",
|
|
95
|
+
" )",
|
|
96
|
+
' ) ELSE IF "%%B"=="" (',
|
|
97
|
+
" :: Unset variable",
|
|
98
|
+
' SET "%%A="',
|
|
99
|
+
" ) ELSE (",
|
|
100
|
+
" :: Set variable",
|
|
101
|
+
' SET "%%A=%%B"',
|
|
102
|
+
" )",
|
|
103
|
+
" )",
|
|
104
|
+
" :: Clean up",
|
|
105
|
+
' CALL :DELETE "%%T"',
|
|
106
|
+
' CALL :DELETE "%__conda_tmp%"',
|
|
107
|
+
" EXIT /B 0",
|
|
108
|
+
" )",
|
|
109
|
+
].join(eol)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function windowsCondaActivatePatchedBlock(eol) {
|
|
113
|
+
return [
|
|
114
|
+
' ) ELSE ENDLOCAL & @CALL :PINOKIO_APPLY_CONDA_ENV "%%T" "%__conda_tmp%" & @GOTO :EOF',
|
|
115
|
+
].join(eol)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function windowsCondaActivatePatchRoutines(eol) {
|
|
119
|
+
return [
|
|
120
|
+
":PINOKIO_APPLY_CONDA_ENV",
|
|
121
|
+
"@ECHO OFF",
|
|
122
|
+
'@FOR /F "tokens=1,* delims==" %%A IN (%~1) DO (',
|
|
123
|
+
'@IF "%CONDA_DEBUG%" == "1" ECHO DEBUG: "%%A=%%B">&2',
|
|
124
|
+
'@IF "%%A"=="_CONDA_SCRIPT" (',
|
|
125
|
+
'@CALL "%%B" || (',
|
|
126
|
+
'@ECHO ERROR: Activation script "%%B" failed with code %ERRORLEVEL%.>&2',
|
|
127
|
+
'@CALL :DELETE "%~1"',
|
|
128
|
+
'@CALL :DELETE "%~2"',
|
|
129
|
+
"@ECHO ON",
|
|
130
|
+
"@EXIT /B 4",
|
|
131
|
+
")",
|
|
132
|
+
') ELSE IF "%%B"=="" (',
|
|
133
|
+
'@SET "%%A="',
|
|
134
|
+
") ELSE (",
|
|
135
|
+
'@SET "%%A=%%B"',
|
|
136
|
+
")",
|
|
137
|
+
")",
|
|
138
|
+
'@CALL :DELETE "%~1"',
|
|
139
|
+
'@CALL :DELETE "%~2"',
|
|
140
|
+
"@ECHO ON",
|
|
141
|
+
"@EXIT /B 0",
|
|
142
|
+
"",
|
|
143
|
+
].join(eol)
|
|
144
|
+
}
|
|
72
145
|
|
|
73
146
|
class Conda {
|
|
74
147
|
description = "Pinokio uses Conda to install various useful programs in an isolated manner."
|
|
@@ -161,6 +234,57 @@ class Conda {
|
|
|
161
234
|
}
|
|
162
235
|
}
|
|
163
236
|
}
|
|
237
|
+
async ensureWindowsCondaActivateEchoPatch() {
|
|
238
|
+
if (this.kernel.platform !== "win32" || !(await this.hasCondaMeta())) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
const activatePath = this.kernel.bin.path(CONDA_ROOT_DIR, WINDOWS_CONDA_ACTIVATE_RELATIVE_PATH)
|
|
242
|
+
let content
|
|
243
|
+
try {
|
|
244
|
+
content = await fs.promises.readFile(activatePath, "utf8")
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error && error.code === "ENOENT") {
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
throw error
|
|
250
|
+
}
|
|
251
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n"
|
|
252
|
+
const routineAnchor = `${eol}:: Routine to delete a temp file if CONDA_DEBUG is not set`
|
|
253
|
+
if (!content.includes(routineAnchor)) {
|
|
254
|
+
console.warn("Could not patch Conda CMD activation echo leak; missing _conda_activate.bat routine anchor")
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (content.includes(WINDOWS_CONDA_ACTIVATE_PINOKIO_MARKER)) {
|
|
258
|
+
if (content.includes(`${WINDOWS_CONDA_ACTIVATE_PINOKIO_MARKER}${eol}@ECHO OFF`)) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
const routineStart = content.indexOf(WINDOWS_CONDA_ACTIVATE_PINOKIO_MARKER)
|
|
262
|
+
const routineAnchorIndex = content.indexOf(routineAnchor, routineStart)
|
|
263
|
+
if (routineAnchorIndex < 0) {
|
|
264
|
+
console.warn("Could not update Conda CMD activation echo patch; missing _conda_activate.bat routine anchor")
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
const patched = [
|
|
268
|
+
content.slice(0, routineStart),
|
|
269
|
+
windowsCondaActivatePatchRoutines(eol),
|
|
270
|
+
content.slice(routineAnchorIndex + eol.length),
|
|
271
|
+
].join("")
|
|
272
|
+
await fs.promises.writeFile(activatePath, patched)
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
const originalBlock = windowsCondaActivateOriginalBlock(eol)
|
|
276
|
+
if (!content.includes(originalBlock)) {
|
|
277
|
+
console.warn("Could not patch Conda CMD activation echo leak; unexpected _conda_activate.bat")
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
const patched = content
|
|
281
|
+
.replace(originalBlock, windowsCondaActivatePatchedBlock(eol))
|
|
282
|
+
.replace(
|
|
283
|
+
routineAnchor,
|
|
284
|
+
`${eol}${windowsCondaActivatePatchRoutines(eol)}:: Routine to delete a temp file if CONDA_DEBUG is not set`
|
|
285
|
+
)
|
|
286
|
+
await fs.promises.writeFile(activatePath, patched)
|
|
287
|
+
}
|
|
164
288
|
async init() {
|
|
165
289
|
if (this.kernel.homedir) {
|
|
166
290
|
console.log("condarc init")
|
|
@@ -183,6 +307,7 @@ report_errors: false`)
|
|
|
183
307
|
await this.ensureCompatibilityAlias()
|
|
184
308
|
}
|
|
185
309
|
await this.ensureWindowsOpenSslHooks()
|
|
310
|
+
await this.ensureWindowsCondaActivateEchoPatch()
|
|
186
311
|
}
|
|
187
312
|
}
|
|
188
313
|
async check() {
|
|
@@ -331,6 +456,7 @@ report_errors: false`)
|
|
|
331
456
|
)
|
|
332
457
|
}
|
|
333
458
|
await this.ensureWindowsOpenSslHooks()
|
|
459
|
+
await this.ensureWindowsCondaActivateEchoPatch()
|
|
334
460
|
await this.ensureCompatibilityAlias()
|
|
335
461
|
ondata({ raw: `Install finished\r\n` })
|
|
336
462
|
await this.kernel.bin.rm(installer, ondata)
|
package/package.json
CHANGED
package/server/views/app.ejs
CHANGED
|
@@ -11040,6 +11040,9 @@ const rerenderMenuSection = (container, html) => {
|
|
|
11040
11040
|
try {
|
|
11041
11041
|
setLogsNewBadge(window.sessionStorage.getItem(logsNotificationKey) === "1")
|
|
11042
11042
|
} catch (_) {}
|
|
11043
|
+
if (document.querySelector(".appcanvas") && document.querySelector(".appcanvas").dataset.pageError === "1") {
|
|
11044
|
+
setLogsNewBadge(true)
|
|
11045
|
+
}
|
|
11043
11046
|
const pluginInstallPreflightCache = new Map()
|
|
11044
11047
|
const normalizePluginPathFromLaunchHref = (rawHref) => {
|
|
11045
11048
|
if (!rawHref || typeof rawHref !== "string") {
|
|
@@ -12348,7 +12351,7 @@ const rerenderMenuSection = (container, html) => {
|
|
|
12348
12351
|
}
|
|
12349
12352
|
return
|
|
12350
12353
|
}
|
|
12351
|
-
if (event.data.e === "pinokio:logs-new" || event.data.type === "pinokio:logs-new") {
|
|
12354
|
+
if (event.data.e === "pinokio:logs-new" || event.data.type === "pinokio:logs-new" || event.data.e === "pinokio:open-logs" || event.data.type === "pinokio:open-logs") {
|
|
12352
12355
|
setLogsNewBadge(true)
|
|
12353
12356
|
return
|
|
12354
12357
|
}
|
package/test/conda-bin.test.js
CHANGED
|
@@ -125,6 +125,58 @@ const EXPECTED_WINDOWS_OPENSSL_HOOKS = {
|
|
|
125
125
|
].join('\n'),
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
const WINDOWS_CONDA_ACTIVATE_PATH = path.join(
|
|
129
|
+
'bin',
|
|
130
|
+
'miniforge',
|
|
131
|
+
'Lib',
|
|
132
|
+
'site-packages',
|
|
133
|
+
'conda',
|
|
134
|
+
'shell',
|
|
135
|
+
'condabin',
|
|
136
|
+
'_conda_activate.bat'
|
|
137
|
+
)
|
|
138
|
+
const WINDOWS_CONDA_ACTIVATE_ORIGINAL_BLOCK = [
|
|
139
|
+
' ) ELSE ENDLOCAL & (',
|
|
140
|
+
' FOR /F "tokens=1,* delims==" %%A IN (%%T) DO (',
|
|
141
|
+
' :: A, B = "key", "value"',
|
|
142
|
+
' IF "%CONDA_DEBUG%" == "1" ECHO DEBUG: "%%A=%%B">&2',
|
|
143
|
+
' IF "%%A"=="_CONDA_SCRIPT" (',
|
|
144
|
+
' :: Script execution, fast exit if activation scripts fail',
|
|
145
|
+
' CALL "%%B" || (',
|
|
146
|
+
' ECHO ERROR: Activation script "%%B" failed with code %ERRORLEVEL%.>&2',
|
|
147
|
+
' CALL :DELETE "%%T"',
|
|
148
|
+
' CALL :DELETE "%__conda_tmp%"',
|
|
149
|
+
' EXIT /B 4',
|
|
150
|
+
' )',
|
|
151
|
+
' ) ELSE IF "%%B"=="" (',
|
|
152
|
+
' :: Unset variable',
|
|
153
|
+
' SET "%%A="',
|
|
154
|
+
' ) ELSE (',
|
|
155
|
+
' :: Set variable',
|
|
156
|
+
' SET "%%A=%%B"',
|
|
157
|
+
' )',
|
|
158
|
+
' )',
|
|
159
|
+
' :: Clean up',
|
|
160
|
+
' CALL :DELETE "%%T"',
|
|
161
|
+
' CALL :DELETE "%__conda_tmp%"',
|
|
162
|
+
' EXIT /B 0',
|
|
163
|
+
' )',
|
|
164
|
+
].join('\r\n')
|
|
165
|
+
const WINDOWS_CONDA_ACTIVATE_SCRIPT = [
|
|
166
|
+
'@ECHO OFF',
|
|
167
|
+
'SETLOCAL',
|
|
168
|
+
'FOR /F "delims=" %%T IN (%__conda_tmp%) DO (',
|
|
169
|
+
' IF NOT EXIST "%%T" (',
|
|
170
|
+
' ENDLOCAL & EXIT /B 3',
|
|
171
|
+
WINDOWS_CONDA_ACTIVATE_ORIGINAL_BLOCK,
|
|
172
|
+
')',
|
|
173
|
+
'',
|
|
174
|
+
':: Routine to delete a temp file if CONDA_DEBUG is not set',
|
|
175
|
+
':DELETE',
|
|
176
|
+
'GOTO :EOF',
|
|
177
|
+
'',
|
|
178
|
+
].join('\r\n')
|
|
179
|
+
|
|
128
180
|
async function seedWindowsOpenSslHooks(root, condaRootName) {
|
|
129
181
|
const condaHookRoot = path.join(root, 'bin', condaRootName, 'etc', 'conda')
|
|
130
182
|
for (const hookRelativePath of Object.keys(EXPECTED_WINDOWS_OPENSSL_HOOKS)) {
|
|
@@ -143,6 +195,13 @@ async function seedWindowsOpenSslHooks(root, condaRootName) {
|
|
|
143
195
|
}
|
|
144
196
|
}
|
|
145
197
|
|
|
198
|
+
async function seedWindowsCondaActivateScript(root, content = WINDOWS_CONDA_ACTIVATE_SCRIPT) {
|
|
199
|
+
const activatePath = path.join(root, WINDOWS_CONDA_ACTIVATE_PATH)
|
|
200
|
+
await fs.mkdir(path.dirname(activatePath), { recursive: true })
|
|
201
|
+
await fs.writeFile(activatePath, content)
|
|
202
|
+
return activatePath
|
|
203
|
+
}
|
|
204
|
+
|
|
146
205
|
async function assertWindowsOpenSslHooksPatched(root, condaRootName) {
|
|
147
206
|
const condaHookRoot = path.join(root, 'bin', condaRootName, 'etc', 'conda')
|
|
148
207
|
for (const [hookRelativePath, expectedContent] of Object.entries(EXPECTED_WINDOWS_OPENSSL_HOOKS)) {
|
|
@@ -203,6 +262,83 @@ test('Conda init patches Windows OpenSSL hooks for legacy Miniconda roots', asyn
|
|
|
203
262
|
await assertWindowsOpenSslHooksPatched(root, 'miniconda')
|
|
204
263
|
})
|
|
205
264
|
|
|
265
|
+
test('Conda init patches Windows CMD activation echo leak idempotently', async () => {
|
|
266
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-cmd-activate-patch-'))
|
|
267
|
+
await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
|
|
268
|
+
const activatePath = await seedWindowsCondaActivateScript(root)
|
|
269
|
+
const conda = createConda(createKernel(root))
|
|
270
|
+
|
|
271
|
+
await conda.init()
|
|
272
|
+
const patched = await fs.readFile(activatePath, 'utf8')
|
|
273
|
+
await conda.init()
|
|
274
|
+
|
|
275
|
+
assert.equal(await fs.readFile(activatePath, 'utf8'), patched)
|
|
276
|
+
assert.equal(patched.includes(WINDOWS_CONDA_ACTIVATE_ORIGINAL_BLOCK), false)
|
|
277
|
+
assert.match(patched, /ENDLOCAL & @CALL :PINOKIO_APPLY_CONDA_ENV "%%T" "%__conda_tmp%" & @GOTO :EOF/)
|
|
278
|
+
assert.match(patched, /:PINOKIO_APPLY_CONDA_ENV\r\n@ECHO OFF/)
|
|
279
|
+
assert.match(patched, /@CALL "%%B" \|\| \(/)
|
|
280
|
+
assert.match(patched, /@ECHO ON\r\n@EXIT \/B 0/)
|
|
281
|
+
assert.match(patched, /:: Routine to delete a temp file if CONDA_DEBUG is not set/)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('Conda init updates the earlier Windows CMD activation echo patch', async () => {
|
|
285
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-cmd-activate-update-patch-'))
|
|
286
|
+
await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
|
|
287
|
+
const oldPatch = WINDOWS_CONDA_ACTIVATE_SCRIPT
|
|
288
|
+
.replace(
|
|
289
|
+
WINDOWS_CONDA_ACTIVATE_ORIGINAL_BLOCK,
|
|
290
|
+
' ) ELSE ENDLOCAL & @CALL :PINOKIO_APPLY_CONDA_ENV "%%T" "%__conda_tmp%" & @GOTO :EOF'
|
|
291
|
+
)
|
|
292
|
+
.replace(
|
|
293
|
+
'\r\n:: Routine to delete a temp file if CONDA_DEBUG is not set',
|
|
294
|
+
[
|
|
295
|
+
'\r\n:PINOKIO_APPLY_CONDA_ENV',
|
|
296
|
+
'@FOR /F "tokens=1,* delims==" %%A IN (%~1) DO (',
|
|
297
|
+
'@IF "%CONDA_DEBUG%" == "1" ECHO DEBUG: "%%A=%%B">&2',
|
|
298
|
+
')',
|
|
299
|
+
'@CALL :DELETE "%~1"',
|
|
300
|
+
'@CALL :DELETE "%~2"',
|
|
301
|
+
'@EXIT /B 0',
|
|
302
|
+
':: Routine to delete a temp file if CONDA_DEBUG is not set',
|
|
303
|
+
].join('\r\n')
|
|
304
|
+
)
|
|
305
|
+
const activatePath = await seedWindowsCondaActivateScript(root, oldPatch)
|
|
306
|
+
const conda = createConda(createKernel(root))
|
|
307
|
+
|
|
308
|
+
await conda.init()
|
|
309
|
+
|
|
310
|
+
const patched = await fs.readFile(activatePath, 'utf8')
|
|
311
|
+
assert.match(patched, /:PINOKIO_APPLY_CONDA_ENV\r\n@ECHO OFF/)
|
|
312
|
+
assert.match(patched, /@ECHO ON\r\n@EXIT \/B 0/)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('Conda init leaves unexpected Windows CMD activation script unchanged', async () => {
|
|
316
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-cmd-activate-unexpected-'))
|
|
317
|
+
await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
|
|
318
|
+
const unexpected = '@ECHO OFF\r\nREM unexpected upstream script\r\n'
|
|
319
|
+
const activatePath = await seedWindowsCondaActivateScript(root, unexpected)
|
|
320
|
+
const conda = createConda(createKernel(root))
|
|
321
|
+
|
|
322
|
+
await conda.init()
|
|
323
|
+
|
|
324
|
+
assert.equal(await fs.readFile(activatePath, 'utf8'), unexpected)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('Conda init leaves Windows CMD activation script unchanged when patch anchor is missing', async () => {
|
|
328
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-cmd-activate-missing-anchor-'))
|
|
329
|
+
await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
|
|
330
|
+
const missingAnchor = WINDOWS_CONDA_ACTIVATE_SCRIPT.replace(
|
|
331
|
+
'\r\n:: Routine to delete a temp file if CONDA_DEBUG is not set',
|
|
332
|
+
'\r\n:: unexpected routine header'
|
|
333
|
+
)
|
|
334
|
+
const activatePath = await seedWindowsCondaActivateScript(root, missingAnchor)
|
|
335
|
+
const conda = createConda(createKernel(root))
|
|
336
|
+
|
|
337
|
+
await conda.init()
|
|
338
|
+
|
|
339
|
+
assert.equal(await fs.readFile(activatePath, 'utf8'), missingAnchor)
|
|
340
|
+
})
|
|
341
|
+
|
|
206
342
|
test('Conda pins Python 3.10.20 consistently across platforms', async () => {
|
|
207
343
|
assert.equal(PYTHON_INSTALL_SPEC, 'python=3.10.20')
|
|
208
344
|
assert.equal(WINDOWS_PYTHON_SSL_FIX_SPEC, 'python=3.10.20=*_1_cpython')
|
|
@@ -245,6 +381,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
|
|
|
245
381
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-python-pin-win32-'))
|
|
246
382
|
const kernel = createKernel(root, 'win32')
|
|
247
383
|
const calls = []
|
|
384
|
+
let activatePath
|
|
248
385
|
|
|
249
386
|
kernel.bin.mods = []
|
|
250
387
|
kernel.bin.download = async () => {}
|
|
@@ -255,6 +392,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
|
|
|
255
392
|
const miniforge = path.join(root, 'bin', 'miniforge')
|
|
256
393
|
await fs.mkdir(path.join(miniforge, 'conda-meta'), { recursive: true })
|
|
257
394
|
await fs.writeFile(path.join(miniforge, 'python.exe'), 'fake python\n')
|
|
395
|
+
activatePath = await seedWindowsCondaActivateScript(root)
|
|
258
396
|
}
|
|
259
397
|
}
|
|
260
398
|
|
|
@@ -267,6 +405,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
|
|
|
267
405
|
condaInstall.message[1],
|
|
268
406
|
'conda install -y --override-channels -c conda-forge "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0"'
|
|
269
407
|
)
|
|
408
|
+
assert.match(await fs.readFile(activatePath, 'utf8'), /:PINOKIO_APPLY_CONDA_ENV/)
|
|
270
409
|
})
|
|
271
410
|
|
|
272
411
|
test('Conda install replaces legacy miniconda with a compatibility alias to miniforge', async () => {
|