pinokiod 8.2.8 → 8.2.9
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/shell.js +4 -4
- package/package.json +1 -1
- package/server/index.js +22 -7
- package/server/public/anywhere.webmanifest +1 -1
- package/server/views/partials/home_server_popover.ejs +1 -0
- package/server/views/remote.ejs +97 -0
- package/test/anywhere-remote.test.js +48 -0
- package/test/shell-run-template.test.js +125 -5
package/kernel/shell.js
CHANGED
|
@@ -1280,9 +1280,9 @@ class Shell {
|
|
|
1280
1280
|
// calling vcvars, but vcvars rebuilds LIB and drops that entry.
|
|
1281
1281
|
// Restore both the active Conda environment and managed CUDA import
|
|
1282
1282
|
// libraries after all compiler activation scripts have completed.
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1285
|
-
conda_activation.push(`CALL set "LIB
|
|
1283
|
+
// Escape the quotes and variable names for interactive cmd.exe so
|
|
1284
|
+
// CALL expands the current values only after activation has finished.
|
|
1285
|
+
conda_activation.push(`CALL set ^"LIB=%^CONDA_PREFIX%\\Library\\lib;%^CUDA_HOME%\\lib;%^LIB%^"`)
|
|
1286
1286
|
} catch (e) {
|
|
1287
1287
|
console.log('vc vars setup', e)
|
|
1288
1288
|
}
|
|
@@ -1393,7 +1393,7 @@ class Shell {
|
|
|
1393
1393
|
} else {
|
|
1394
1394
|
// when python version is not specified, use the default python -m venv
|
|
1395
1395
|
venv_activation = [
|
|
1396
|
-
|
|
1396
|
+
`python -m venv ${shellEnvPath}`,
|
|
1397
1397
|
activate_command,
|
|
1398
1398
|
// `python -m pip install --upgrade pip setuptools wheel`,
|
|
1399
1399
|
deactivate_path,
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1043,12 +1043,17 @@ class Server {
|
|
|
1043
1043
|
if (light) {
|
|
1044
1044
|
return snap
|
|
1045
1045
|
}
|
|
1046
|
+
let entries = []
|
|
1047
|
+
try {
|
|
1048
|
+
entries = await this.kernel.api.listApps()
|
|
1049
|
+
} catch (_) { }
|
|
1050
|
+
snap.installed = entries.length
|
|
1046
1051
|
snap.apps = []
|
|
1047
1052
|
if (snap.phase === "ready") {
|
|
1048
1053
|
const serveByLocalPort = new Map((snap.serve || []).map((entry) => [entry.local, entry]))
|
|
1049
1054
|
let running = []
|
|
1050
1055
|
try {
|
|
1051
|
-
running = await this.collectHomeServerApps()
|
|
1056
|
+
running = await this.collectHomeServerApps(entries)
|
|
1052
1057
|
} catch (_) {
|
|
1053
1058
|
running = []
|
|
1054
1059
|
}
|
|
@@ -1056,6 +1061,7 @@ class Server {
|
|
|
1056
1061
|
const port = this.portFromUrl(app.ready_url || app.url)
|
|
1057
1062
|
const entry = port ? serveByLocalPort.get(port) : null
|
|
1058
1063
|
snap.apps.push({
|
|
1064
|
+
id: app.id,
|
|
1059
1065
|
name: app.name,
|
|
1060
1066
|
icon: app.icon,
|
|
1061
1067
|
state: app.state,
|
|
@@ -1076,16 +1082,18 @@ class Server {
|
|
|
1076
1082
|
}
|
|
1077
1083
|
return snap
|
|
1078
1084
|
}
|
|
1079
|
-
|
|
1085
|
+
// `entries`: an app list from kernel.api.listApps() the caller already has
|
|
1086
|
+
async collectHomeServerApps(entries = null) {
|
|
1080
1087
|
const apps = []
|
|
1081
1088
|
if (!this.kernel || !this.kernel.api || typeof this.kernel.api.listApps !== "function") {
|
|
1082
1089
|
return apps
|
|
1083
1090
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1091
|
+
if (!Array.isArray(entries)) {
|
|
1092
|
+
try {
|
|
1093
|
+
entries = await this.kernel.api.listApps()
|
|
1094
|
+
} catch (_) {
|
|
1095
|
+
entries = []
|
|
1096
|
+
}
|
|
1089
1097
|
}
|
|
1090
1098
|
for (const entry of entries) {
|
|
1091
1099
|
const appId = entry && (entry.id || entry.name) ? String(entry.id || entry.name) : ""
|
|
@@ -14261,6 +14269,13 @@ class Server {
|
|
|
14261
14269
|
agent: req.agent,
|
|
14262
14270
|
})
|
|
14263
14271
|
}))
|
|
14272
|
+
// the phone-first launcher: running apps with their stable Tailscale addresses
|
|
14273
|
+
this.app.get("/remote", ex(async (req, res) => {
|
|
14274
|
+
res.render("remote", {
|
|
14275
|
+
status: await this.composeAnywhereStatus(),
|
|
14276
|
+
theme: this.theme,
|
|
14277
|
+
})
|
|
14278
|
+
}))
|
|
14264
14279
|
this.app.get("/anywhere/status", ex(async (req, res) => {
|
|
14265
14280
|
res.set("Cache-Control", "no-store")
|
|
14266
14281
|
res.json(await this.composeAnywhereStatus({ light: req.query.light === "1" }))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"Pinokio","short_name":"Pinokio","start_url":"/","scope":"/","display":"standalone","background_color":"#ffffff","theme_color":"#000000","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"},{"src":"/apple-touch-icon.png","sizes":"180x180","type":"image/png"}]}
|
|
1
|
+
{"name":"Pinokio","short_name":"Pinokio","start_url":"/remote","scope":"/","display":"standalone","background_color":"#ffffff","theme_color":"#000000","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"},{"src":"/apple-touch-icon.png","sizes":"180x180","type":"image/png"}]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<a class="btn2 home-server-popover__trigger" href="/remote" data-tippy-content="Launcher" title="Launcher" aria-label="Launcher"><i class="fa-solid fa-table-cells-large"></i></a>
|
|
1
2
|
<div class="home-server-popover" data-home-server-popover data-home-server-status="loading">
|
|
2
3
|
<button
|
|
3
4
|
type="button"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Pinokio</title>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
7
|
+
<base target="_top">
|
|
8
|
+
<% if (locals.anywhere_host) { %>
|
|
9
|
+
<link rel="manifest" href="/anywhere.webmanifest">
|
|
10
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
11
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
12
|
+
<meta name="apple-mobile-web-app-title" content="Pinokio">
|
|
13
|
+
<% } %>
|
|
14
|
+
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
15
|
+
<link href="/css/solid.min.css" rel="stylesheet">
|
|
16
|
+
<style>
|
|
17
|
+
:root { --bg: #f6f5f1; --card: #fff; --text: #1a1a1a; --muted: #6b6a66; --line: #dcdad2; --accent: #1a1a1a; --on: #fff; }
|
|
18
|
+
body.dark { --bg: #141414; --card: #1e1e1e; --text: #f1f1f1; --muted: #a8a69f; --line: #333; --accent: #f1f1f1; --on: #141414; }
|
|
19
|
+
* { box-sizing: border-box; }
|
|
20
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-text-size-adjust: 100%; }
|
|
21
|
+
main { max-width: 480px; margin: 0 auto; padding: 24px 16px 48px; }
|
|
22
|
+
h1 { font-size: 22px; font-weight: 600; margin: 0 0 18px; }
|
|
23
|
+
h2 { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 8px; }
|
|
24
|
+
.card { display: flex; align-items: center; gap: 12px; background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 12px; margin: 0 0 10px; }
|
|
25
|
+
.card img { width: 44px; height: 44px; border-radius: 10px; object-fit: cover; background: var(--bg); flex: none; }
|
|
26
|
+
.card .body { flex: 1; min-width: 0; }
|
|
27
|
+
.card .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
28
|
+
.card .sub { font-size: 13px; color: var(--muted); }
|
|
29
|
+
.card .sub a { color: inherit; }
|
|
30
|
+
.open { flex: none; padding: 10px 16px; border-radius: 10px; background: var(--accent); color: var(--on); font-weight: 600; text-decoration: none; }
|
|
31
|
+
.state { flex: none; font-size: 13px; color: var(--muted); }
|
|
32
|
+
.notice { text-align: center; padding: 28px 16px; color: var(--muted); }
|
|
33
|
+
.notice strong { display: block; color: var(--text); font-size: 17px; margin-bottom: 4px; }
|
|
34
|
+
.door { display: flex; align-items: center; gap: 12px; border-radius: 14px; padding: 14px; text-decoration: none; color: var(--text); margin-top: 14px; }
|
|
35
|
+
.door.dashed { border: 1px dashed var(--muted); }
|
|
36
|
+
.door.solid { background: var(--accent); color: var(--on); }
|
|
37
|
+
.door .body { flex: 1; }
|
|
38
|
+
.door .title { font-weight: 600; }
|
|
39
|
+
.door .cap { font-size: 13px; opacity: 0.75; }
|
|
40
|
+
.door i { font-size: 18px; }
|
|
41
|
+
.full { display: block; text-align: center; margin-top: 18px; color: var(--muted); font-size: 14px; text-decoration: none; }
|
|
42
|
+
.full i { margin-right: 6px; }
|
|
43
|
+
[hidden] { display: none !important; }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body class="<%=theme%>">
|
|
47
|
+
<main>
|
|
48
|
+
<h1>Pinokio</h1>
|
|
49
|
+
<section id="running" hidden>
|
|
50
|
+
<h2>Running now</h2>
|
|
51
|
+
<div id="cards"></div>
|
|
52
|
+
</section>
|
|
53
|
+
<div id="notice" class="notice" hidden><strong>Nothing is running</strong>Start an app on your computer and it shows up here.</div>
|
|
54
|
+
<a id="door" class="door" href="/home">
|
|
55
|
+
<i class="fa-solid fa-plus"></i>
|
|
56
|
+
<div class="body"><div class="title" id="door-title"></div><div class="cap" id="door-cap"></div></div>
|
|
57
|
+
<i class="fa-solid fa-chevron-right"></i>
|
|
58
|
+
</a>
|
|
59
|
+
<a class="full" href="/home"><i class="fa-solid fa-house"></i> Open full Pinokio</a>
|
|
60
|
+
</main>
|
|
61
|
+
<script>
|
|
62
|
+
(() => {
|
|
63
|
+
let current = <%- JSON.stringify(status || {}).replace(/</g, '\\u003c') %>
|
|
64
|
+
const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
65
|
+
const port = (url) => { try { return new URL(url).port } catch (_) { return '' } }
|
|
66
|
+
const render = (status) => {
|
|
67
|
+
current = status || {}
|
|
68
|
+
const apps = Array.isArray(current.apps) ? current.apps : []
|
|
69
|
+
document.getElementById('running').hidden = apps.length === 0
|
|
70
|
+
document.getElementById('notice').hidden = apps.length > 0
|
|
71
|
+
document.getElementById('cards').innerHTML = apps.map((app) => `
|
|
72
|
+
<div class="card">
|
|
73
|
+
<img src="${esc(app.icon || '/pinokio-black.png')}" alt="">
|
|
74
|
+
<div class="body">
|
|
75
|
+
<div class="name">${esc(app.name)}</div>
|
|
76
|
+
<div class="sub">${app.remote ? ':' + esc(port(app.remote)) + ' · ' : ''}<a href="/p/${encodeURIComponent(app.id || '')}">${app.remote ? 'manage' : 'terminal'}</a></div>
|
|
77
|
+
</div>
|
|
78
|
+
${app.remote ? `<a class="open" href="${esc(app.remote)}">Open</a>` : `<span class="state">${esc(app.state || 'starting')}…</span>`}
|
|
79
|
+
</div>`).join('')
|
|
80
|
+
const door = document.getElementById('door')
|
|
81
|
+
door.className = 'door ' + (apps.length > 0 ? 'dashed' : 'solid')
|
|
82
|
+
document.getElementById('door-title').textContent = apps.length > 0 ? 'Start another app' : 'Start an app'
|
|
83
|
+
document.getElementById('door-cap').textContent = `${current.installed || 0} installed apps in Pinokio`
|
|
84
|
+
}
|
|
85
|
+
const poll = async () => {
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch('/anywhere/status', { cache: 'no-store' })
|
|
88
|
+
if (response.ok) render(await response.json())
|
|
89
|
+
} catch (_) { }
|
|
90
|
+
setTimeout(poll, 5000)
|
|
91
|
+
}
|
|
92
|
+
render(current)
|
|
93
|
+
setTimeout(poll, 5000)
|
|
94
|
+
})()
|
|
95
|
+
</script>
|
|
96
|
+
</body>
|
|
97
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const test = require('node:test')
|
|
4
|
+
const ejs = require('ejs')
|
|
5
|
+
const Server = require('../server/index.js')
|
|
6
|
+
|
|
7
|
+
const view = path.resolve(__dirname, '..', 'server', 'views', 'remote.ejs')
|
|
8
|
+
|
|
9
|
+
// composeAnywhereStatus with its collaborators stubbed: the launcher needs the
|
|
10
|
+
// installed count and each running app's id next to its remote address.
|
|
11
|
+
test('anywhere status carries the installed count and each app id', async () => {
|
|
12
|
+
const fake = {
|
|
13
|
+
kernel: {
|
|
14
|
+
anywhere: {
|
|
15
|
+
snapshot: async () => ({ phase: 'ready', connectUrl: null, serve: [{ local: 7860, url: 'https://pinokio.tail1.ts.net:43000' }] }),
|
|
16
|
+
},
|
|
17
|
+
api: { listApps: async () => [{ id: 'comfy' }, { id: 'forge' }, { id: 'idle' }] },
|
|
18
|
+
},
|
|
19
|
+
collectHomeServerApps: async () => [
|
|
20
|
+
{ id: 'comfy', name: 'ComfyUI', icon: '/c.png', ready_url: 'http://127.0.0.1:7860', state: 'ready' },
|
|
21
|
+
{ id: 'forge', name: 'Forge', icon: '/f.png', ready_url: '', state: 'starting' },
|
|
22
|
+
],
|
|
23
|
+
portFromUrl: Server.prototype.portFromUrl,
|
|
24
|
+
}
|
|
25
|
+
const status = await Server.prototype.composeAnywhereStatus.call(fake, {})
|
|
26
|
+
assert.equal(status.installed, 3)
|
|
27
|
+
assert.deepEqual(status.apps.map((a) => [a.id, a.remote]), [['comfy', 'https://pinokio.tail1.ts.net:43000'], ['forge', null]])
|
|
28
|
+
const light = await Server.prototype.composeAnywhereStatus.call(fake, { light: true })
|
|
29
|
+
assert.equal(light.installed, undefined, 'the light status is unchanged')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('the launcher page renders the running apps and one door to the full interface', async () => {
|
|
33
|
+
const render = (status) => ejs.renderFile(view, { status, theme: 'light', locals: {} })
|
|
34
|
+
|
|
35
|
+
const none = await render({ installed: 12, apps: [] })
|
|
36
|
+
assert.match(none, /Nothing is running/)
|
|
37
|
+
assert.match(none, /href="\/home"/)
|
|
38
|
+
assert.match(none, /Open full Pinokio/)
|
|
39
|
+
assert.match(none, /"installed":12/)
|
|
40
|
+
|
|
41
|
+
const some = await render({ installed: 12, apps: [
|
|
42
|
+
{ id: 'comfy', name: 'ComfyUI', icon: '/c.png', state: 'ready', remote: 'https://pinokio.tail1.ts.net:43000' },
|
|
43
|
+
{ id: 'forge', name: 'Forge', icon: '/f.png', state: 'starting', remote: null },
|
|
44
|
+
] })
|
|
45
|
+
assert.match(some, /"remote":"https:\/\/pinokio\.tail1\.ts\.net:43000"/)
|
|
46
|
+
assert.match(some, /"id":"forge"/)
|
|
47
|
+
assert.doesNotMatch(some, /<link rel="manifest"/, 'manifest tags only for the Tailscale host')
|
|
48
|
+
})
|
|
@@ -183,8 +183,8 @@ test('Shell.activate restores Conda and CUDA library paths after Windows compile
|
|
|
183
183
|
const compilerScript = path.join(activateRoot, 'vs2019_compiler_vars.bat')
|
|
184
184
|
const cudaScript = path.join(activateRoot, '~cuda-nvcc_activate.bat')
|
|
185
185
|
await fs.mkdir(activateRoot, { recursive: true })
|
|
186
|
-
await fs.writeFile(compilerScript, '@
|
|
187
|
-
await fs.writeFile(cudaScript, '@
|
|
186
|
+
await fs.writeFile(compilerScript, '@set "CONDA_PREFIX=C:\\active conda"\r\n@set "LIB=C:\\Visual Studio (x86)\\lib"\r\n')
|
|
187
|
+
await fs.writeFile(cudaScript, '@set "CUDA_HOME=C:\\active cuda"\r\n')
|
|
188
188
|
|
|
189
189
|
const kernel = createKernel(root)
|
|
190
190
|
kernel.bin = {
|
|
@@ -199,6 +199,7 @@ test('Shell.activate restores Conda and CUDA library paths after Windows compile
|
|
|
199
199
|
|
|
200
200
|
const params = await shell.activate({
|
|
201
201
|
build: true,
|
|
202
|
+
conda: { skip: true },
|
|
202
203
|
path: root,
|
|
203
204
|
message: ['uv pip install cupy'],
|
|
204
205
|
})
|
|
@@ -206,7 +207,7 @@ test('Shell.activate restores Conda and CUDA library paths after Windows compile
|
|
|
206
207
|
const activation = params.message[0]
|
|
207
208
|
const compilerIndex = activation.indexOf(`CALL "${compilerScript}"`)
|
|
208
209
|
const cudaIndex = activation.indexOf(`CALL "${cudaScript}"`)
|
|
209
|
-
const libraryCommand = 'CALL set "LIB
|
|
210
|
+
const libraryCommand = 'CALL set ^"LIB=%^CONDA_PREFIX%\\Library\\lib;%^CUDA_HOME%\\lib;%^LIB%^"'
|
|
210
211
|
const libraryIndex = activation.indexOf(libraryCommand)
|
|
211
212
|
|
|
212
213
|
assert.notEqual(compilerIndex, -1)
|
|
@@ -215,6 +216,23 @@ test('Shell.activate restores Conda and CUDA library paths after Windows compile
|
|
|
215
216
|
assert.ok(compilerIndex < cudaIndex)
|
|
216
217
|
assert.ok(cudaIndex < libraryIndex)
|
|
217
218
|
assert.equal(params.message[1], 'uv pip install cupy')
|
|
219
|
+
|
|
220
|
+
await t.test('reads activated values in interactive cmd.exe', {
|
|
221
|
+
skip: process.platform !== 'win32' && 'requires Windows cmd.exe',
|
|
222
|
+
}, () => {
|
|
223
|
+
for (const mode of ['OFF', 'ON']) {
|
|
224
|
+
const result = spawnSync('cmd.exe', ['/D', '/Q', `/V:${mode}`], {
|
|
225
|
+
env: { ...process.env, CONDA_PREFIX: 'stale-conda', CUDA_HOME: 'stale-cuda', LIB: 'stale-lib' },
|
|
226
|
+
input: `${activation} & echo. & set LIB\r\nexit\r\n`,
|
|
227
|
+
encoding: 'utf8',
|
|
228
|
+
timeout: 10000,
|
|
229
|
+
windowsHide: true,
|
|
230
|
+
})
|
|
231
|
+
assert.ifError(result.error)
|
|
232
|
+
assert.equal(result.status, 0, result.stdout + result.stderr)
|
|
233
|
+
assert.match(result.stdout, /^LIB=C:\\active conda\\Library\\lib;C:\\active cuda\\lib;C:\\Visual Studio \(x86\)\\lib\r?$/m)
|
|
234
|
+
}
|
|
235
|
+
})
|
|
218
236
|
})
|
|
219
237
|
|
|
220
238
|
test('Shell.init_env defaults the uv HTTP timeout without overriding apps', async () => {
|
|
@@ -385,7 +403,7 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
385
403
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-selection-'))
|
|
386
404
|
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
387
405
|
const cases = [
|
|
388
|
-
{ venv:
|
|
406
|
+
{ venv: "default env $NAME O'Brien (test)" },
|
|
389
407
|
{ venv: { path: 'object-default-env' } },
|
|
390
408
|
{ venv: 'explicit-env', venv_python: '3.10', version: '3.10' },
|
|
391
409
|
{ venv: { path: 'object-env', python: '3.11' }, version: '3.11' },
|
|
@@ -427,7 +445,7 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
427
445
|
assert.equal(command.split(PIP_INSTALL_COMMAND).length - 1, 1)
|
|
428
446
|
assert.ok(command.indexOf(PIP_INSTALL_COMMAND) > command.lastIndexOf('activate'))
|
|
429
447
|
const envPath = path.resolve(root, typeof scenario.venv === 'string' ? scenario.venv : scenario.venv.path)
|
|
430
|
-
const shellPath = platform === 'win32' && /bash/i.test(shellName) ?
|
|
448
|
+
const shellPath = shell.isCmdShell() ? `"${envPath}"` : shell.quoteArgForShell(platform === 'win32' && /bash/i.test(shellName) ? require('../kernel/util').p2u(envPath) : envPath)
|
|
431
449
|
for (const step of [`python -m venv ${shellPath}`, PIP_INSTALL_COMMAND]) {
|
|
432
450
|
assert.ok(command.includes(shell.isPowerShell()
|
|
433
451
|
? `${step}; if ($LASTEXITCODE -ne 0) { exit 1 }`
|
|
@@ -436,6 +454,16 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
436
454
|
} else {
|
|
437
455
|
assert.doesNotMatch(command, /-m pip/)
|
|
438
456
|
}
|
|
457
|
+
if (scenario.venv) {
|
|
458
|
+
const envPath = path.resolve(root, typeof scenario.venv === 'string' ? scenario.venv : scenario.venv.path)
|
|
459
|
+
const script = path.resolve(envPath, platform === 'win32' ? 'Scripts' : 'bin', shell.isPowerShell() ? 'Activate.ps1' : 'activate')
|
|
460
|
+
const scriptPath = shell.isCmdShell() ? `"${script}"` : shell.quoteArgForShell(platform === 'win32' && /bash/i.test(shellName) ? require('../kernel/util').p2u(script) : script)
|
|
461
|
+
assert.ok(command.includes(shell.isPowerShell()
|
|
462
|
+
? `& ${scriptPath}`
|
|
463
|
+
: shell.isCmdShell() ? scriptPath : `source ${scriptPath}`), command)
|
|
464
|
+
assert.ok(command.includes(shell.isPowerShell() ? '; if (!$?) { exit 1 }' : ' || exit 1'), command)
|
|
465
|
+
if (shell.isPowerShell()) assert.doesNotMatch(command, /Scripts[\\/]deactivate/)
|
|
466
|
+
}
|
|
439
467
|
assert.equal(shell.env.UV_PYTHON_PREFERENCE, 'only-managed')
|
|
440
468
|
}
|
|
441
469
|
}
|
|
@@ -475,3 +503,95 @@ fi
|
|
|
475
503
|
assert.equal(result.stdout.includes('PINOKIO_PIP_BOOTSTRAP_RAN'), venvExit === 0)
|
|
476
504
|
}
|
|
477
505
|
})
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
test('venv paths remain literal through creation and repeated activation', async t => {
|
|
509
|
+
if (process.platform === 'win32') return t.skip('executes bash commands')
|
|
510
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-paths-'))
|
|
511
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
512
|
+
const bin = path.join(root, 'bin')
|
|
513
|
+
const project = path.join(root, "project & $NAME O'Brien (test)")
|
|
514
|
+
await fs.mkdir(bin)
|
|
515
|
+
await fs.mkdir(project)
|
|
516
|
+
const fixture = path.join(root, 'activate')
|
|
517
|
+
await fs.writeFile(fixture, 'export VIRTUAL_ENV="$PINOKIO_TEST_ENV"\ndeactivate() { unset VIRTUAL_ENV; }\n')
|
|
518
|
+
const executable = `#!/bin/sh
|
|
519
|
+
if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
|
|
520
|
+
[ "$#" = "3" ] || exit 9
|
|
521
|
+
target=$3
|
|
522
|
+
elif [ "$1" = "venv" ]; then
|
|
523
|
+
[ "$#" = "4" ] || exit 9
|
|
524
|
+
target=$2
|
|
525
|
+
else
|
|
526
|
+
echo "PINOKIO_BOOTSTRAP_TARGET=$VIRTUAL_ENV"
|
|
527
|
+
exit 0
|
|
528
|
+
fi
|
|
529
|
+
printf '%s' "$target" > "$PINOKIO_TEST_CREATED"
|
|
530
|
+
mkdir -p "$target/bin"
|
|
531
|
+
cp "$PINOKIO_TEST_ACTIVATE" "$target/bin/activate"
|
|
532
|
+
`
|
|
533
|
+
await fs.writeFile(path.join(bin, 'python'), executable, { mode: 0o755 })
|
|
534
|
+
await fs.writeFile(path.join(bin, 'uv'), executable, { mode: 0o755 })
|
|
535
|
+
for (const explicit of [false, true]) {
|
|
536
|
+
const venv = explicit ? 'uv env $NAME' : 'default env $NAME'
|
|
537
|
+
const envPath = path.join(project, venv)
|
|
538
|
+
const created = path.join(root, 'created')
|
|
539
|
+
for (const existing of [false, true]) {
|
|
540
|
+
const shell = createShell(createKernel(root))
|
|
541
|
+
shell.shell = 'bash'
|
|
542
|
+
const params = await shell.activate({
|
|
543
|
+
path: project, conda: { skip: true }, venv,
|
|
544
|
+
...(explicit ? { venv_python: '3.10' } : {}),
|
|
545
|
+
chain: '*', message: 'printf "PINOKIO_APP_TARGET=%s\\n" "$VIRTUAL_ENV"',
|
|
546
|
+
})
|
|
547
|
+
const result = spawnSync('/bin/bash', ['-c', shell.build(params)], {
|
|
548
|
+
encoding: 'utf8', env: {
|
|
549
|
+
...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
550
|
+
VIRTUAL_ENV: '', NAME: 'must-not-expand',
|
|
551
|
+
PINOKIO_TEST_ENV: envPath, PINOKIO_TEST_CREATED: created, PINOKIO_TEST_ACTIVATE: fixture,
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
assert.equal(result.status, 0, result.stdout + result.stderr)
|
|
555
|
+
assert.equal(await fs.readFile(created, 'utf8'), envPath)
|
|
556
|
+
assert.ok(result.stdout.includes(`PINOKIO_APP_TARGET=${envPath}\n`), result.stdout)
|
|
557
|
+
assert.equal(result.stdout.includes(`PINOKIO_BOOTSTRAP_TARGET=${envPath}\n`), !explicit && !existing)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
test('failed activation stops before pip or application commands', async t => {
|
|
563
|
+
if (process.platform === 'win32') return t.skip('executes bash commands')
|
|
564
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-activation-'))
|
|
565
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
566
|
+
const bin = path.join(root, 'bin')
|
|
567
|
+
await fs.mkdir(bin)
|
|
568
|
+
await fs.writeFile(path.join(bin, 'python'), `#!/bin/sh
|
|
569
|
+
if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
|
|
570
|
+
mkdir -p "$3/bin"
|
|
571
|
+
if [ "$PINOKIO_TEST_FAILURE" = "second" ]; then
|
|
572
|
+
echo 'deactivate() { rm -f "$PINOKIO_TEST_ENV/bin/activate"; }' > "$3/bin/activate"
|
|
573
|
+
fi
|
|
574
|
+
else
|
|
575
|
+
echo PINOKIO_BOOTSTRAP_RAN
|
|
576
|
+
fi
|
|
577
|
+
`, { mode: 0o755 })
|
|
578
|
+
for (const failure of ['first', 'second', 'existing']) {
|
|
579
|
+
const venv = `${failure} env`
|
|
580
|
+
if (failure === 'existing') await fs.mkdir(path.join(root, venv))
|
|
581
|
+
const shell = createShell(createKernel(root))
|
|
582
|
+
shell.shell = 'bash'
|
|
583
|
+
const params = await shell.activate({
|
|
584
|
+
path: root, conda: { skip: true }, venv,
|
|
585
|
+
chain: '*', message: 'echo PINOKIO_APPLICATION_RAN',
|
|
586
|
+
})
|
|
587
|
+
const result = spawnSync('/bin/bash', ['-c', shell.build(params)], {
|
|
588
|
+
encoding: 'utf8', env: {
|
|
589
|
+
...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
590
|
+
PINOKIO_TEST_FAILURE: failure, PINOKIO_TEST_ENV: path.join(root, venv),
|
|
591
|
+
},
|
|
592
|
+
})
|
|
593
|
+
assert.equal(result.status, 1, `${failure}: ${result.stdout}${result.stderr}`)
|
|
594
|
+
assert.equal(result.stdout.includes('PINOKIO_BOOTSTRAP_RAN'), false, failure)
|
|
595
|
+
assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), false, failure)
|
|
596
|
+
}
|
|
597
|
+
})
|