@poe-platform/safe-bash 0.1.825 → 0.1.827
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/dist/image-ast/index.js +39 -18
- package/dist/pdf-ast/image.js +1 -1
- package/dist/pdf-ast/index.js +1 -1
- package/dist/pdf-ast/vendor/pdfjs-fonts.mjs +9 -3
- package/dist/safe-bash/chunks/{chunk-AGHV53VF.js → chunk-M6XFU6LQ.js} +3 -1
- package/dist/safe-bash/chunks/{chunk-363QOIDR.js → chunk-T6FHWSHM.js} +3 -1
- package/dist/safe-bash/command-chunks/{chunk-ZPBL7TYJ.js → chunk-2JRHCVMQ.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-FMGNXOMV.js → chunk-2L3NMRLG.js} +7 -3
- package/dist/safe-bash/command-chunks/{chunk-DAQO64WC.js → chunk-2YKOJPIW.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-EPYJSPOY.js → chunk-3QCW4VQF.js} +12 -8
- package/dist/safe-bash/command-chunks/{chunk-YRBFNT5B.js → chunk-3YL2TMLA.js} +10 -1
- package/dist/safe-bash/command-chunks/{chunk-5H7ZHP7F.js → chunk-43NDRBKY.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-BBOYR6LV.js → chunk-46GA576V.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-WPT2FCHH.js → chunk-4SLPM2WM.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-7ZQVMPSB.js → chunk-5CAXR62E.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-3DTHX5UU.js → chunk-66653AEZ.js} +40 -14
- package/dist/safe-bash/command-chunks/{chunk-ETFJ4L7F.js → chunk-6OLE6CLV.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-ZIDESNHW.js → chunk-7M3M6FKC.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-2KMNVJB7.js → chunk-A7PKUIJT.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-AVSINNZ6.js → chunk-DEQ47SBQ.js} +5 -5
- package/dist/safe-bash/command-chunks/{chunk-EV7A5FNN.js → chunk-FFFRLPXU.js} +2 -2
- package/dist/safe-bash/command-chunks/{chunk-4MVQE6CT.js → chunk-FYTBEGSY.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-LFTGMUAV.js → chunk-HQIZR6EU.js} +192 -489
- package/dist/safe-bash/command-chunks/chunk-HS2L7I4N.js +219 -0
- package/dist/safe-bash/command-chunks/{chunk-WKS2PAZL.js → chunk-HUGKO4ZE.js} +10 -1
- package/dist/safe-bash/command-chunks/chunk-IGXH5W5Z.js +111 -0
- package/dist/safe-bash/command-chunks/{chunk-CMJBRGU2.js → chunk-IHQZKUBS.js} +1 -1
- package/dist/safe-bash/command-chunks/chunk-JADJ4BMN.js +1519 -0
- package/dist/safe-bash/command-chunks/{chunk-5AQAWIYK.js → chunk-JEZC5V3B.js} +4 -0
- package/dist/safe-bash/command-chunks/{chunk-YB5TNDY6.js → chunk-K5VJ4NNL.js} +39 -5
- package/dist/safe-bash/command-chunks/{chunk-7RQGRFHJ.js → chunk-KHWDEAE4.js} +2 -2
- package/dist/safe-bash/command-chunks/chunk-L54KVUSY.js +604 -0
- package/dist/safe-bash/command-chunks/{chunk-3NJETNC2.js → chunk-MBXNG6C3.js} +2 -2
- package/dist/safe-bash/command-chunks/{chunk-HCPEYQKT.js → chunk-N4SH6QY5.js} +285 -302
- package/dist/safe-bash/command-chunks/{chunk-3DID6MQY.js → chunk-OWTQYLID.js} +2 -2
- package/dist/safe-bash/command-chunks/{chunk-4EA7WI3L.js → chunk-QOOQD6BD.js} +42 -33
- package/dist/safe-bash/command-chunks/{chunk-RQTFK7AV.js → chunk-QZYEI4N6.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-R3SLT3ZX.js → chunk-R6GON4H3.js} +11 -1
- package/dist/safe-bash/command-chunks/{chunk-2GILUQJY.js → chunk-R7ZGDHME.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-HNEWGADJ.js → chunk-RUXOFW5G.js} +7 -6
- package/dist/safe-bash/command-chunks/{chunk-LKAFLJ5L.js → chunk-RWQKX44L.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-PKH57OIC.js → chunk-SG2KOOD4.js} +2 -2
- package/dist/safe-bash/command-chunks/chunk-T4TGJYFV.js +60 -0
- package/dist/safe-bash/command-chunks/chunk-TGSNVLFZ.js +105 -0
- package/dist/safe-bash/command-chunks/{chunk-NXCJNPL3.js → chunk-UCE77LYV.js} +2 -1
- package/dist/safe-bash/command-chunks/{chunk-EWECMOQ5.js → chunk-UIHPHIKI.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-XSZNL3GV.js → chunk-UJPWPRVB.js} +191 -146
- package/dist/safe-bash/command-chunks/{chunk-E5CDHXFA.js → chunk-VOR6YDZC.js} +26 -1
- package/dist/safe-bash/command-chunks/chunk-VSNVYOK2.js +30 -0
- package/dist/safe-bash/command-chunks/{chunk-4XYGJB24.js → chunk-VTH4AORO.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-W5LH7D23.js → chunk-WKKJJBJQ.js} +8 -8
- package/dist/safe-bash/command-chunks/{chunk-OLXMWQJ5.js → chunk-WRO3PB2M.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-RUAGHZNF.js → chunk-X5ODTF3E.js} +7 -5
- package/dist/safe-bash/command-chunks/{chunk-LH4VBNF6.js → chunk-XGTSP2CP.js} +84 -16
- package/dist/safe-bash/command-chunks/{chunk-K576GZED.js → chunk-XTHKXCF2.js} +2 -2
- package/dist/safe-bash/command-chunks/{chunk-J3VS55K4.js → chunk-YHRESVJ7.js} +9 -3
- package/dist/safe-bash/command-chunks/{chunk-UWIDPTOB.js → chunk-YSCKOUQP.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-UYNJF5QN.js → chunk-ZALAWNOE.js} +76 -122
- package/dist/safe-bash/command-chunks/{chunk-OT75C5YS.js → chunk-ZYXKW2K7.js} +1 -1
- package/dist/safe-bash/command-chunks/{pdf-renderer-346WYKSZ.js → pdf-renderer-5QWOVFA4.js} +1 -1
- package/dist/safe-bash/command-chunks/{prompt-tool-debug-3POFNRYP.js → prompt-tool-debug-G7ATE7D5.js} +6 -4
- package/dist/safe-bash/commands/llm/index.browser.js +5 -1
- package/dist/safe-bash/commands/llm/index.js +1 -1
- package/dist/safe-bash/commands/python/executor.browser.js +3 -1
- package/dist/safe-bash/commands/python/executor.js +1 -1
- package/dist/safe-bash/commands/python/index.browser.js +5 -1
- package/dist/safe-bash/commands/python/index.js +1 -1
- package/dist/safe-bash/core.browser.js +2 -2
- package/dist/safe-bash/full-core.browser.js +10 -2
- package/dist/safe-bash-command-awk/awk-runtime.js +2 -2
- package/dist/safe-bash-command-awk/awk-syntax.js +1 -1
- package/dist/safe-bash-command-awk/awk.js +3 -3
- package/dist/safe-bash-command-awk/index.js +3 -3
- package/dist/safe-bash-command-gpg/index.js +11 -3
- package/dist/safe-bash-command-imagemagick/index.js +2 -2
- package/dist/safe-bash-command-llm/collections.js +6 -4
- package/dist/safe-bash-command-llm/command.js +4 -4
- package/dist/safe-bash-command-llm/import-json-document.d.ts +9 -2
- package/dist/safe-bash-command-llm/index.d.ts +2 -1
- package/dist/safe-bash-command-llm/index.js +10 -4
- package/dist/safe-bash-command-llm/loader-provider.d.ts +5 -0
- package/dist/safe-bash-command-mcp/index.js +10 -13
- package/dist/safe-bash-command-pdftoppm/index.js +2 -2
- package/dist/safe-bash-command-playwright-cli/index.js +2 -2
- package/dist/safe-bash-command-playwright-cli/playwright/ability-execution.d.ts +1 -0
- package/dist/safe-bash-command-playwright-cli/playwright/ability-execution.js +1 -1
- package/dist/safe-bash-command-playwright-cli/playwright/controller.d.ts +2 -0
- package/dist/safe-bash-command-playwright-cli/playwright/controller.js +2 -2
- package/dist/safe-bash-command-playwright-cli/playwright/index.js +2 -2
- package/dist/safe-bash-command-python/build-backend.d.ts +2 -0
- package/dist/safe-bash-command-python/cache.d.ts +1 -1
- package/dist/safe-bash-command-python/executor.d.ts +5 -2
- package/dist/safe-bash-command-python/executor.js +11 -6
- package/dist/safe-bash-command-python/host-capabilities.js +1 -1
- package/dist/safe-bash-command-python/index.js +11 -6
- package/dist/safe-bash-command-python/jspi.js +5 -3
- package/dist/safe-bash-command-python/manifest-file.d.ts +44 -0
- package/dist/safe-bash-command-python/manifest.d.ts +45 -0
- package/dist/safe-bash-command-python/provisioning.d.ts +20 -1
- package/dist/safe-bash-command-python/provisioning.js +3 -2
- package/dist/safe-bash-command-python/reply.js +7 -11
- package/dist/safe-bash-command-python/source-archive.js +2 -2
- package/dist/safe-bash-command-python/source-zip.js +2 -2
- package/dist/safe-bash-command-python/worker.js +34 -21
- package/dist/safe-bash-command-sqlite3/engine.d.ts +4 -0
- package/dist/safe-bash-command-sqlite3/index.js +174 -18
- package/dist/safe-bash-command-svgo/index.js +1 -1
- package/dist/safe-bash-command-tar/create.js +3 -3
- package/dist/safe-bash-command-tar/extract.js +3 -3
- package/dist/safe-bash-command-tar/index.js +5 -5
- package/dist/safe-bash-command-tar/modes.js +5 -5
- package/dist/safe-bash-command-tar/options.d.ts +1 -1
- package/dist/safe-bash-command-tar/options.js +1 -1
- package/dist/safe-bash-command-tar/stream.js +1 -1
- package/dist/safe-bash-command-wkhtmltopdf/index.js +1 -1
- package/dist/safe-bash-command-xan/argv.js +1 -1
- package/dist/safe-bash-command-xan/commands.js +3 -3
- package/dist/safe-bash-command-xan/index.js +3 -3
- package/dist/safe-bash-command-xan/io.js +2 -2
- package/dist/safe-bash-compression-engine/bounded-codec.js +3 -3
- package/dist/safe-bash-compression-engine/codec-loader.js +2 -2
- package/dist/safe-bash-compression-engine/files.js +6 -6
- package/dist/safe-bash-compression-engine/index.js +5 -5
- package/dist/safe-bash-compression-engine/operand.js +6 -6
- package/dist/safe-bash-compression-engine/options.js +1 -1
- package/dist/safe-bash-compression-engine/stream.js +5 -5
- package/dist/safe-bash-compression-engine/zstd-decode.js +4 -4
- package/dist/safe-bash-contracts/filesystem-descriptor.js +1 -1
- package/dist/safe-bash-contracts/filesystem-output.js +1 -1
- package/dist/safe-bash-csv-engine/index.js +6 -6
- package/dist/safe-bash-csv-engine/python-codec-aliases.js +1 -1
- package/dist/safe-bash-csv-engine/python-codepages.js +3 -3
- package/dist/safe-bash-csv-engine/python-file-codepages.js +2 -2
- package/dist/safe-bash-csv-engine/python-utf16.js +4 -4
- package/dist/safe-bash-csv-engine/text-decoder.js +6 -6
- package/dist/safe-bash-graphviz-engine/index.js +1 -1
- package/dist/safe-bash-pager-engine/index.js +2 -2
- package/dist/safe-bash-query-engine/capture.js +3 -3
- package/dist/safe-bash-query-engine/delpaths.js +3 -3
- package/dist/safe-bash-query-engine/formats.js +2 -2
- package/dist/safe-bash-query-engine/index.js +10 -10
- package/dist/safe-bash-query-engine/indices.js +3 -3
- package/dist/safe-bash-query-engine/input.d.ts +3 -3
- package/dist/safe-bash-query-engine/input.js +1 -1
- package/dist/safe-bash-query-engine/interpreter.js +9 -9
- package/dist/safe-bash-query-engine/query-core.js +10 -10
- package/dist/safe-bash-query-engine/regex.js +4 -4
- package/dist/safe-bash-query-engine/splits.js +5 -5
- package/dist/safe-bash-query-engine/values.js +2 -2
- package/package.json +2 -2
- package/dist/safe-bash/command-chunks/chunk-OOYVZAWL.js +0 -90
- package/dist/safe-bash/command-chunks/chunk-RZOHZGEZ.js +0 -772
- package/dist/safe-bash/command-chunks/chunk-WIUEM7HP.js +0 -58
- package/dist/safe-bash/command-chunks/chunk-WY6THI6J.js +0 -673
- package/dist/safe-bash/command-chunks/chunk-ZERCTWUI.js +0 -467
|
@@ -0,0 +1,1519 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPythonRecordReader,
|
|
3
|
+
readBackedPythonManifest,
|
|
4
|
+
withPythonManifestPublication
|
|
5
|
+
} from "./chunk-HS2L7I4N.js";
|
|
6
|
+
import {
|
|
7
|
+
openPythonPackageFile
|
|
8
|
+
} from "./chunk-UCE77LYV.js";
|
|
9
|
+
|
|
10
|
+
// packages/safe-bash-command-python/src/manifest-file.ts
|
|
11
|
+
import { jsonValues } from "../../safe-bash-query-engine/input.js";
|
|
12
|
+
import { Budget, resolveJqLimits } from "../../safe-bash-query-engine/limits.js";
|
|
13
|
+
import { compareIdentity, compareFileVersion } from "@poe-platform/safe-fs/core";
|
|
14
|
+
import { FsError } from "../../safe-bash-contracts/index.js";
|
|
15
|
+
import { createBufferedOutput } from "../../safe-bash-contracts/io.js";
|
|
16
|
+
import { resolvePath } from "../../safe-bash-contracts/path.js";
|
|
17
|
+
import { yieldTurn } from "../../safe-bash-contracts/yield.js";
|
|
18
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
19
|
+
import { bytesToHex } from "../../safe-bash-io-engine/byte-encoding.js";
|
|
20
|
+
|
|
21
|
+
// packages/safe-bash-command-python/src/manifest.ts
|
|
22
|
+
import { serializeLlmJsonValue } from "../../safe-bash-command-llm/index.js";
|
|
23
|
+
import { toByteSource } from "../../safe-bash-contracts/io.js";
|
|
24
|
+
function readPackageManifest(value) {
|
|
25
|
+
const strings = (values) => Array.isArray(values) && values.every((item) => typeof item === "string");
|
|
26
|
+
if (Array.isArray(value)) return strings(value) ? value : void 0;
|
|
27
|
+
if (!value || typeof value !== "object") return;
|
|
28
|
+
const record = value, version = record.version;
|
|
29
|
+
if (![1, 2, 3].includes(version) || Object.keys(value).length !== (version === 1 ? 2 : 3)) return;
|
|
30
|
+
if (version !== 1 && (!Array.isArray(record.records) || !record.records.every((row) => Array.isArray(row) && row.length === version + 3 && row.every((part, index) => index > 2 && index < 5 ? strings(part) : typeof part === "string" || index === 5 && part === null)))) return;
|
|
31
|
+
return strings(record.installed) ? record.installed : void 0;
|
|
32
|
+
}
|
|
33
|
+
function createPythonPackageStreamingManifestStore(store) {
|
|
34
|
+
const publish = async (scope, revision, input, options) => {
|
|
35
|
+
let consumed = false;
|
|
36
|
+
const source = (async function* () {
|
|
37
|
+
let size = 0;
|
|
38
|
+
for await (const bytes of input) {
|
|
39
|
+
options.signal.throwIfAborted();
|
|
40
|
+
size += bytes.length;
|
|
41
|
+
if (size > options.maxBytes) throw Object.assign(new Error("Python package manifest exceeds maxManifestBytes"), { code: "EPACKAGE" });
|
|
42
|
+
yield bytes;
|
|
43
|
+
}
|
|
44
|
+
consumed = true;
|
|
45
|
+
})();
|
|
46
|
+
try {
|
|
47
|
+
const committed = await store.compareAndSet(scope, revision, source, options);
|
|
48
|
+
options.signal.throwIfAborted();
|
|
49
|
+
if (committed === true && !consumed) throw new Error("Python manifest store returned before consuming its source");
|
|
50
|
+
return committed;
|
|
51
|
+
} finally {
|
|
52
|
+
await source.return();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
get: store.get.bind(store),
|
|
57
|
+
compareAndSet(scope, revision, bytes, options) {
|
|
58
|
+
return store.compareAndSet(scope, revision, toByteSource(bytes), options);
|
|
59
|
+
},
|
|
60
|
+
compareAndSetSource: publish,
|
|
61
|
+
compareAndSetSnapshot(scope, revision, snapshot, options) {
|
|
62
|
+
return publish(scope, revision, serializeLlmJsonValue(snapshot, options.signal), options);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
var PythonPackageConflictError = class extends Error {
|
|
67
|
+
code = "EPACKAGECONFLICT";
|
|
68
|
+
retryable = true;
|
|
69
|
+
constructor() {
|
|
70
|
+
super("Python package environment changed during installation; retry the command");
|
|
71
|
+
this.name = "PythonPackageConflictError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function createPythonPackageManifestStore(options = {}) {
|
|
75
|
+
const maxBytes = options.maxBytes ?? Infinity;
|
|
76
|
+
const maxEntries = options.maxEntries ?? Infinity;
|
|
77
|
+
const maxScopeLength = options.maxScopeLength ?? Infinity;
|
|
78
|
+
for (const [name, value] of Object.entries({ maxBytes, maxEntries, maxScopeLength })) {
|
|
79
|
+
if (value !== Infinity && (!Number.isSafeInteger(value) || value < 1)) {
|
|
80
|
+
throw new RangeError(`Python manifest ${name} must be a positive safe integer or Infinity`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
84
|
+
let retainedBytes = 0;
|
|
85
|
+
let revision = 0n;
|
|
86
|
+
let disposed = false;
|
|
87
|
+
const check = (scope, signal) => {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
if (disposed) throw new Error("Python manifest store is disposed");
|
|
90
|
+
if (typeof scope !== "string" || !scope) throw new TypeError("Invalid Python manifest scope");
|
|
91
|
+
if (scope.length > maxScopeLength) throw new RangeError("Python manifest scope budget exhausted");
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
async get(scope, { signal }) {
|
|
95
|
+
check(scope, signal);
|
|
96
|
+
const value = manifests.get(scope);
|
|
97
|
+
return value ? { revision: value.revision, bytes: value.bytes.slice() } : void 0;
|
|
98
|
+
},
|
|
99
|
+
async compareAndSet(scope, expected, bytes, { signal }) {
|
|
100
|
+
check(scope, signal);
|
|
101
|
+
const current = manifests.get(scope);
|
|
102
|
+
if (current?.revision !== expected) return false;
|
|
103
|
+
if (!(bytes instanceof Uint8Array)) throw new TypeError("Python manifest must contain bytes");
|
|
104
|
+
const nextBytes = retainedBytes - (current?.bytes.length ?? 0) + bytes.length;
|
|
105
|
+
if (nextBytes > maxBytes || !current && manifests.size >= maxEntries) {
|
|
106
|
+
throw new RangeError("Python manifest store budget exhausted");
|
|
107
|
+
}
|
|
108
|
+
manifests.set(scope, { revision: String(++revision), bytes: Uint8Array.from(bytes) });
|
|
109
|
+
retainedBytes = nextBytes;
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
dispose() {
|
|
113
|
+
disposed = true;
|
|
114
|
+
manifests.clear();
|
|
115
|
+
retainedBytes = 0;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// packages/safe-bash-command-python/src/manifest-file.ts
|
|
121
|
+
function createPythonPackageFileManifestStore({ fs, directory, maxCacheBytes = Infinity, filename }) {
|
|
122
|
+
if (maxCacheBytes !== Infinity && (!Number.isSafeInteger(maxCacheBytes) || maxCacheBytes < 1)) throw new RangeError("Python manifest maxCacheBytes must be positive");
|
|
123
|
+
const root = resolvePath("/", directory);
|
|
124
|
+
const observations = /* @__PURE__ */ new Map();
|
|
125
|
+
const path = (scope) => {
|
|
126
|
+
if (typeof scope !== "string" || !scope) throw new TypeError("Invalid Python manifest scope");
|
|
127
|
+
return resolvePath(root, filename ?? bytesToHex(sha256(new TextEncoder().encode(scope))) + ".json");
|
|
128
|
+
};
|
|
129
|
+
async function read(scope, { signal, maxBytes = Infinity }, consume) {
|
|
130
|
+
signal.throwIfAborted();
|
|
131
|
+
let stat;
|
|
132
|
+
try {
|
|
133
|
+
stat = await fs.lstat(path(scope), { signal });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
signal.throwIfAborted();
|
|
136
|
+
if (error instanceof FsError && error.code === "ENOENT") {
|
|
137
|
+
observations.delete(scope);
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (stat.size > maxBytes) throw new Error("Python package manifest exceeds maxManifestBytes");
|
|
143
|
+
const file = await openPythonPackageFile({ fs, signal, cwd: root }, path(scope), Infinity, stat);
|
|
144
|
+
if (!file) throw new Error("Python manifests require retained caller reads");
|
|
145
|
+
try {
|
|
146
|
+
const value = await consume(file);
|
|
147
|
+
signal.throwIfAborted();
|
|
148
|
+
let observed = observations.get(scope);
|
|
149
|
+
if (!observed || compareIdentity(observed.stat, stat) !== "same" || !compareFileVersion(observed.stat, stat)) observed = { revision: crypto.randomUUID(), stat };
|
|
150
|
+
observations.set(scope, observed);
|
|
151
|
+
return { revision: observed.revision, value };
|
|
152
|
+
} finally {
|
|
153
|
+
await file.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function* chunks(file) {
|
|
157
|
+
for (let offset = 0; offset < file.size; ) {
|
|
158
|
+
const bytes = await file.read(offset, Math.min(65536, file.size - offset));
|
|
159
|
+
offset += bytes.length;
|
|
160
|
+
yield bytes;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const store = createPythonPackageStreamingManifestStore({
|
|
164
|
+
async get(scope, options) {
|
|
165
|
+
const result = await read(scope, options, async (file) => {
|
|
166
|
+
const bytes = new Uint8Array(file.size);
|
|
167
|
+
let offset = 0;
|
|
168
|
+
for await (const chunk of chunks(file)) {
|
|
169
|
+
bytes.set(chunk, offset);
|
|
170
|
+
offset += chunk.length;
|
|
171
|
+
}
|
|
172
|
+
return bytes;
|
|
173
|
+
});
|
|
174
|
+
return result && { revision: result.revision, bytes: result.value };
|
|
175
|
+
},
|
|
176
|
+
async compareAndSet(scope, expected, source, { signal }) {
|
|
177
|
+
signal.throwIfAborted();
|
|
178
|
+
const filename2 = path(scope), observed = observations.get(scope), settings = { signal };
|
|
179
|
+
if (observed?.revision !== expected) return false;
|
|
180
|
+
await fs.mkdir(root, { recursive: true, signal });
|
|
181
|
+
const caps = await fs.capabilitiesFor?.(root, { signal, create: true }) ?? fs.capabilities;
|
|
182
|
+
if (!caps.atomicFileStaging || !caps.retainedStagingWrite || !caps.retainedStagingCleanup || !caps.guardedStagingPublication || !fs.createStagedFile || !fs.publishStagedFile || !fs.prepareStagingResolution) throw new Error("Python manifests require guarded retained atomic staging");
|
|
183
|
+
const resolution = await fs.prepareStagingResolution(filename2, settings);
|
|
184
|
+
if (observed ? !resolution.destination || compareIdentity(observed.stat, resolution.destination) !== "same" || !compareFileVersion(observed.stat, resolution.destination) : resolution.destination !== null) return false;
|
|
185
|
+
let stage;
|
|
186
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
187
|
+
try {
|
|
188
|
+
stage = await fs.createStagedFile(resolvePath(root, ".python-manifest-" + crypto.randomUUID()), "snapshot", { type: "file", data: new Uint8Array() }, { parent: resolution.parent, mode: 384, retainCleanup: true, signal });
|
|
189
|
+
break;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
signal.throwIfAborted();
|
|
192
|
+
if (!(error instanceof FsError) || error.code !== "EEXIST") throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!stage) throw new Error("Python manifest staging names exhausted");
|
|
196
|
+
const owned = stage;
|
|
197
|
+
try {
|
|
198
|
+
if (!owned.writer || !owned.cleanup) throw new Error("Python manifest staging handles missing");
|
|
199
|
+
const writer = owned.writer;
|
|
200
|
+
const output = createBufferedOutput({ async write(bytes) {
|
|
201
|
+
await writer.write(bytes, settings);
|
|
202
|
+
await yieldTurn(signal);
|
|
203
|
+
} }, signal, 65536);
|
|
204
|
+
for await (const bytes of source) await output.write(bytes);
|
|
205
|
+
await output.flush();
|
|
206
|
+
const stat = await writer.finish(settings);
|
|
207
|
+
if (stat.size > maxCacheBytes) throw new RangeError("Python package manifest exceeds maxCacheBytes");
|
|
208
|
+
try {
|
|
209
|
+
await fs.publishStagedFile({ ...owned, file: { ...owned.file, stat } }, filename2, { parent: resolution.parent, destination: observed?.stat ?? null, ancestors: resolution.ancestors, commitGuard: resolution.validate, signal });
|
|
210
|
+
} catch (error) {
|
|
211
|
+
signal.throwIfAborted();
|
|
212
|
+
if (error instanceof FsError && ["EAGAIN", "EEXIST"].includes(error.code)) return false;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
if (observations.get(scope) === observed) observations.delete(scope);
|
|
216
|
+
return true;
|
|
217
|
+
} finally {
|
|
218
|
+
try {
|
|
219
|
+
await owned.cleanup?.remove();
|
|
220
|
+
} finally {
|
|
221
|
+
await owned.cleanup?.close();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return { ...store, async openSnapshot(scope, options) {
|
|
227
|
+
let snapshot;
|
|
228
|
+
try {
|
|
229
|
+
const result = await read(scope, options, async (file) => snapshot = await readBackedPythonManifest(fs, root, options.signal, chunks(file)));
|
|
230
|
+
return result && { ...result.value, revision: result.revision };
|
|
231
|
+
} catch (error) {
|
|
232
|
+
await snapshot?.close();
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}, getSnapshot(scope, options) {
|
|
236
|
+
return read(scope, options, async (file) => {
|
|
237
|
+
let value;
|
|
238
|
+
for await (const item of jsonValues(chunks(file), new Budget(resolveJqLimits({ maxInputBytes: options.maxBytes }), options.signal), { profile: "javascript" })) value = item;
|
|
239
|
+
return value;
|
|
240
|
+
});
|
|
241
|
+
} };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// packages/safe-bash-command-python/src/manifest-configured.ts
|
|
245
|
+
import { bytesToHex as bytesToHex2 } from "../../safe-bash-io-engine/byte-encoding.js";
|
|
246
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
247
|
+
import { FsError as FsError2 } from "../../safe-bash-contracts/index.js";
|
|
248
|
+
import { dirname } from "../../safe-bash-contracts/path.js";
|
|
249
|
+
async function createConfiguredPythonManifestStore(context, directory, filename, maxBytes, maxCacheBytes) {
|
|
250
|
+
const { fs, signal, cwd } = context;
|
|
251
|
+
let caps;
|
|
252
|
+
for (let path = directory; ; path = dirname(path)) {
|
|
253
|
+
try {
|
|
254
|
+
caps = await fs.capabilitiesFor?.(path, { signal, create: true }) ?? fs.capabilities;
|
|
255
|
+
break;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof FsError2) || error.code !== "ENOENT" || path === "/") throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const native = caps.atomicFileStaging && caps.retainedStagingWrite && caps.retainedStagingCleanup && caps.guardedStagingPublication && fs.prepareStagingResolution;
|
|
261
|
+
const root = native ? directory : cwd + "/.python-packages/manifests/" + bytesToHex2(sha2562(new TextEncoder().encode(directory)));
|
|
262
|
+
const store = createPythonPackageFileManifestStore({ fs, directory: root, filename, ...maxCacheBytes === void 0 ? {} : { maxCacheBytes } });
|
|
263
|
+
if (!native) {
|
|
264
|
+
const stat = async (path) => {
|
|
265
|
+
try {
|
|
266
|
+
return await fs.lstat(path, { signal });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!(error instanceof FsError2) || error.code !== "ENOENT") throw error;
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
if (await stat(root + "/" + filename)) return store;
|
|
273
|
+
const legacy = await stat(directory + "/" + filename);
|
|
274
|
+
if (!legacy) return store;
|
|
275
|
+
if (legacy.type !== "file" || legacy.size > maxBytes) throw new Error("Invalid legacy Python package manifest");
|
|
276
|
+
if (!fs.readStream) throw new Error("Legacy Python manifest migration requires streaming reads");
|
|
277
|
+
await store.compareAndSetSource(filename, void 0, fs.readStream(directory + "/" + filename, { signal }), { signal, maxBytes });
|
|
278
|
+
}
|
|
279
|
+
return store;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// packages/safe-bash-command-python/src/provisioning.ts
|
|
283
|
+
import { serializeLlmJsonValue as serializeLlmJsonValue2 } from "../../safe-bash-command-llm/index.js";
|
|
284
|
+
|
|
285
|
+
// packages/safe-bash-command-python/src/installation-root.ts
|
|
286
|
+
import { compareIdentity as compareIdentity2 } from "@poe-platform/safe-fs/core";
|
|
287
|
+
import { retainFileSystemCleanup } from "@poe-platform/safe-fs/core";
|
|
288
|
+
import { resolvePath as resolvePath2 } from "../../safe-bash-contracts/path.js";
|
|
289
|
+
var serial = 0;
|
|
290
|
+
var PythonInstallationRoot = class {
|
|
291
|
+
constructor(context) {
|
|
292
|
+
this.context = context;
|
|
293
|
+
}
|
|
294
|
+
context;
|
|
295
|
+
initializing;
|
|
296
|
+
closing;
|
|
297
|
+
cleanup;
|
|
298
|
+
owned;
|
|
299
|
+
path(context) {
|
|
300
|
+
if (this.closing) return Promise.reject(new Error("Python installation root is closed"));
|
|
301
|
+
const initialized = this.initializing ??= (async () => {
|
|
302
|
+
const { fs, signal, cwd } = this.context;
|
|
303
|
+
signal.throwIfAborted();
|
|
304
|
+
this.cleanup = retainFileSystemCleanup(fs, async (view) => {
|
|
305
|
+
if (!this.owned) return;
|
|
306
|
+
const { path, parent: parent2, identity } = this.owned, current = await view.lstat(path);
|
|
307
|
+
if (compareIdentity2(identity, current) !== "same") throw new Error("Python installation root identity changed");
|
|
308
|
+
await view.removeTreeConditional(path, { parent: parent2, expected: current });
|
|
309
|
+
this.owned = void 0;
|
|
310
|
+
});
|
|
311
|
+
await fs.mkdir(cwd, { recursive: true, signal });
|
|
312
|
+
const parentPath = await fs.realpath(cwd, { signal });
|
|
313
|
+
const parent = await fs.stat(parentPath, { signal });
|
|
314
|
+
const capabilities = await fs.capabilitiesFor?.(parentPath, { signal }) ?? fs.capabilities;
|
|
315
|
+
if (!fs.prepareDirectory || !fs.removeTreeConditional || !capabilities.atomicTreeRemoval) throw new Error("Python extracted packages require conditional caller-owned directories");
|
|
316
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
317
|
+
const path = resolvePath2(parentPath, ".python-install-" + ++serial);
|
|
318
|
+
try {
|
|
319
|
+
const identity = await fs.prepareDirectory(path, { parent, expected: null, mode: 448, signal });
|
|
320
|
+
this.owned = { path, parent, identity };
|
|
321
|
+
signal.throwIfAborted();
|
|
322
|
+
return path;
|
|
323
|
+
} catch (error) {
|
|
324
|
+
signal.throwIfAborted();
|
|
325
|
+
if (!error || typeof error !== "object" || !("code" in error) || !["EEXIST", "EAGAIN"].includes(String(error.code))) throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw new Error("Python installation root names exhausted");
|
|
329
|
+
})();
|
|
330
|
+
return context ? initialized.then(async (path) => {
|
|
331
|
+
if (compareIdentity2(this.owned.identity, await context.fs.lstat(path, { signal: context.signal })) !== "same") throw new Error("Python installation root identity changed");
|
|
332
|
+
return path;
|
|
333
|
+
}) : initialized;
|
|
334
|
+
}
|
|
335
|
+
close() {
|
|
336
|
+
return this.closing ??= (async () => {
|
|
337
|
+
await this.initializing?.catch(() => {
|
|
338
|
+
});
|
|
339
|
+
await this.cleanup?.();
|
|
340
|
+
})();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// packages/safe-bash-command-python/src/manifest-default.ts
|
|
345
|
+
function createDefaultPythonManifestStore(maxCacheBytes) {
|
|
346
|
+
let root;
|
|
347
|
+
return {
|
|
348
|
+
forContext(context) {
|
|
349
|
+
let store;
|
|
350
|
+
const writable = () => store ??= (async () => {
|
|
351
|
+
const owned = root ??= new PythonInstallationRoot(context);
|
|
352
|
+
let directory;
|
|
353
|
+
try {
|
|
354
|
+
directory = await owned.path();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
await owned.close();
|
|
357
|
+
if (root === owned) root = void 0;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
await owned.path(context);
|
|
361
|
+
return createPythonPackageFileManifestStore({ fs: context.fs, directory, ...maxCacheBytes === void 0 ? {} : { maxCacheBytes } });
|
|
362
|
+
})();
|
|
363
|
+
return {
|
|
364
|
+
async get(scope, options) {
|
|
365
|
+
return root ? (await writable()).get(scope, options) : void 0;
|
|
366
|
+
},
|
|
367
|
+
async openSnapshot(scope, options) {
|
|
368
|
+
return root ? (await writable()).openSnapshot(scope, options) : void 0;
|
|
369
|
+
},
|
|
370
|
+
async getSnapshot(scope, options) {
|
|
371
|
+
return root ? (await writable()).getSnapshot(scope, options) : void 0;
|
|
372
|
+
},
|
|
373
|
+
async compareAndSet(scope, revision, bytes, options) {
|
|
374
|
+
return (await writable()).compareAndSet(scope, revision, bytes, options);
|
|
375
|
+
},
|
|
376
|
+
async compareAndSetSource(scope, revision, source, options) {
|
|
377
|
+
return (await writable()).compareAndSetSource(scope, revision, source, options);
|
|
378
|
+
},
|
|
379
|
+
async compareAndSetSnapshot(scope, revision, snapshot, options) {
|
|
380
|
+
return (await writable()).compareAndSetSnapshot(scope, revision, snapshot, options);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
async close() {
|
|
385
|
+
await root?.close();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// packages/safe-bash-command-python/src/provisioning.ts
|
|
391
|
+
import { header } from "../../safe-bash-network-engine/shared.js";
|
|
392
|
+
import { bytesToHex as bytesToHex3 } from "../../safe-bash-io-engine/byte-encoding.js";
|
|
393
|
+
|
|
394
|
+
// packages/safe-bash-command-python/src/package-index.ts
|
|
395
|
+
import { ArchiveMetadataMap } from "../../safe-bash-io-engine/commands/archive/metadata.js";
|
|
396
|
+
import { createArchiveScratchFactory } from "../../safe-bash-io-engine/commands/archive/scratch.js";
|
|
397
|
+
import { DEFAULT_ARCHIVE_LIMITS } from "../../safe-bash-io-engine/commands/archive/internal.js";
|
|
398
|
+
import { toByteSource as toByteSource2 } from "../../safe-bash-contracts/index.js";
|
|
399
|
+
async function exact(source, offset, length) {
|
|
400
|
+
const bytes = new Uint8Array(length);
|
|
401
|
+
for (let position = 0; position < length; ) {
|
|
402
|
+
const chunk = await source.read(offset + position, Math.min(65536, length - position));
|
|
403
|
+
if (!chunk.length || chunk.length > length - position) throw new Error("Invalid native ZIP index read");
|
|
404
|
+
bytes.set(chunk, position);
|
|
405
|
+
position += chunk.length;
|
|
406
|
+
}
|
|
407
|
+
return bytes;
|
|
408
|
+
}
|
|
409
|
+
var PythonPackageIndex = class {
|
|
410
|
+
#scratch;
|
|
411
|
+
#spool;
|
|
412
|
+
#data;
|
|
413
|
+
#offsets;
|
|
414
|
+
#dataSource;
|
|
415
|
+
#offsetSource;
|
|
416
|
+
#ends;
|
|
417
|
+
#size = 0;
|
|
418
|
+
#pages = [];
|
|
419
|
+
#cached;
|
|
420
|
+
#names;
|
|
421
|
+
#headers;
|
|
422
|
+
#boundaries;
|
|
423
|
+
#count = 0;
|
|
424
|
+
#sealed = false;
|
|
425
|
+
#failed;
|
|
426
|
+
#tail = Promise.resolve();
|
|
427
|
+
#closing;
|
|
428
|
+
#context;
|
|
429
|
+
#mode;
|
|
430
|
+
constructor(context, mode = "wheel") {
|
|
431
|
+
this.#context = context;
|
|
432
|
+
this.#mode = mode;
|
|
433
|
+
const discard = { async write() {
|
|
434
|
+
} };
|
|
435
|
+
this.#scratch = createArchiveScratchFactory({ context: { ...context, command: "python", args: [], env: {}, stdin: toByteSource2(""), stdout: discard, stderr: discard }, limits: DEFAULT_ARCHIVE_LIMITS, operation: (action) => Promise.resolve().then(action) }, context.cwd);
|
|
436
|
+
const metadata = this.#spool = async () => {
|
|
437
|
+
const spool = await this.#scratch();
|
|
438
|
+
let buffer = new Uint8Array(16384), used = 0;
|
|
439
|
+
const flush = async () => {
|
|
440
|
+
if (used) {
|
|
441
|
+
await spool.append(buffer.subarray(0, used));
|
|
442
|
+
used = 0;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
return { append: async (bytes) => {
|
|
446
|
+
if (!buffer) throw new Error("Python wheel scratch is sealed");
|
|
447
|
+
for (let at = 0; at < bytes.length; ) {
|
|
448
|
+
const length = Math.min(buffer.length - used, bytes.length - at);
|
|
449
|
+
buffer.set(bytes.subarray(at, at + length), used);
|
|
450
|
+
used += length;
|
|
451
|
+
at += length;
|
|
452
|
+
if (used === buffer.length) await flush();
|
|
453
|
+
}
|
|
454
|
+
}, close: async () => {
|
|
455
|
+
buffer = void 0;
|
|
456
|
+
used = 0;
|
|
457
|
+
await spool.close();
|
|
458
|
+
}, finish: async () => {
|
|
459
|
+
await flush();
|
|
460
|
+
buffer = void 0;
|
|
461
|
+
const source = await spool.finish();
|
|
462
|
+
return { size: source.size, read: async (offset, length) => {
|
|
463
|
+
if (offset >= source.size) return new Uint8Array();
|
|
464
|
+
const base = Math.floor(offset / 16384) * 16384;
|
|
465
|
+
const found = this.#pages.findIndex((page2) => page2.source === source && page2.offset === base);
|
|
466
|
+
const page = found < 0 ? { source, offset: base, bytes: await exact(source, base, Math.min(16384, source.size - base)) } : this.#pages.splice(found, 1)[0];
|
|
467
|
+
this.#pages.push(page);
|
|
468
|
+
if (this.#pages.length > 4) this.#pages.shift();
|
|
469
|
+
return page.bytes.subarray(offset - base, Math.min(page.bytes.length, offset - base + length));
|
|
470
|
+
} };
|
|
471
|
+
} };
|
|
472
|
+
};
|
|
473
|
+
this.#boundaries = new ArchiveMetadataMap(metadata, context.signal);
|
|
474
|
+
this.#names = new ArchiveMetadataMap(metadata, context.signal);
|
|
475
|
+
this.#headers = new ArchiveMetadataMap(metadata, context.signal);
|
|
476
|
+
}
|
|
477
|
+
execute(operation, args) {
|
|
478
|
+
if (this.#closing) return Promise.reject(new Error("Python wheel index is closed"));
|
|
479
|
+
const work = this.#tail.then(async () => {
|
|
480
|
+
this.#context.signal.throwIfAborted();
|
|
481
|
+
if (this.#failed) throw this.#failed.error;
|
|
482
|
+
if (this.#mode !== "wheel") {
|
|
483
|
+
const name = args[0];
|
|
484
|
+
if (operation === "has" && (this.#sealed || this.#mode === "records") && typeof name === "string") return this.#names.has(name);
|
|
485
|
+
if (operation === "get" && this.#mode === "records" && this.#sealed && typeof name === "string") return await this.#names.get(name) ?? null;
|
|
486
|
+
if (this.#sealed) throw new Error("Python package names are sealed");
|
|
487
|
+
if (operation === "add" && typeof name === "string") {
|
|
488
|
+
const value = this.#mode === "records" ? args[1] : 0;
|
|
489
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Invalid package record ordinal");
|
|
490
|
+
await this.#names.set(name, value);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (operation === "seal") {
|
|
494
|
+
this.#sealed = true;
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
throw new Error("Invalid Python package name operation");
|
|
498
|
+
}
|
|
499
|
+
if (operation === "append") {
|
|
500
|
+
if (this.#sealed) throw new Error("Python wheel index is sealed");
|
|
501
|
+
const [name, offset, payload] = args;
|
|
502
|
+
if (typeof name !== "string" || typeof offset !== "string" || typeof payload !== "string") throw new Error("Invalid native ZIP entry");
|
|
503
|
+
const header2 = BigInt(offset), ordinal = this.#count++;
|
|
504
|
+
if (!Number.isSafeInteger(this.#count) || header2 <= -(1n << 128n) || header2 >= 1n << 128n) throw new Error("Invalid native ZIP coordinate");
|
|
505
|
+
this.#data ??= await this.#spool();
|
|
506
|
+
this.#offsets ??= await this.#spool();
|
|
507
|
+
const bytes = new TextEncoder().encode(payload), pointer = new Uint8Array(16), view = new DataView(pointer.buffer);
|
|
508
|
+
view.setBigUint64(0, BigInt(this.#size), true);
|
|
509
|
+
view.setBigUint64(8, BigInt(bytes.length), true);
|
|
510
|
+
for (let at = 0; at < bytes.length; at += 65536) await this.#data.append(bytes.subarray(at, at + 65536));
|
|
511
|
+
await this.#offsets.append(pointer);
|
|
512
|
+
this.#size += bytes.length;
|
|
513
|
+
if (!Number.isSafeInteger(this.#size)) throw new Error("Native ZIP index size overflow");
|
|
514
|
+
await this.#names.set(name, ordinal);
|
|
515
|
+
const order = ((1n << 128n) - header2).toString().padStart(40, "0") + ":" + String(Number.MAX_SAFE_INTEGER - ordinal).padStart(16, "0");
|
|
516
|
+
await this.#headers.set(order, { ordinal, offset });
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
if (operation === "seal") {
|
|
520
|
+
if (this.#sealed) throw new Error("Python wheel index is sealed");
|
|
521
|
+
if (typeof args[0] !== "string") throw new Error("Invalid native ZIP directory offset");
|
|
522
|
+
let end = args[0];
|
|
523
|
+
BigInt(end);
|
|
524
|
+
for await (const [, entry] of this.#headers.sortedEntries()) {
|
|
525
|
+
await this.#boundaries.set(String(entry.ordinal).padStart(16, "0"), end);
|
|
526
|
+
end = entry.offset;
|
|
527
|
+
}
|
|
528
|
+
const boundaries = await this.#spool();
|
|
529
|
+
for await (const [, end2] of this.#boundaries.sortedEntries()) {
|
|
530
|
+
const value = BigInt.asUintN(128, BigInt(end2)), bytes = new Uint8Array(16), view = new DataView(bytes.buffer);
|
|
531
|
+
view.setBigUint64(0, value & (1n << 64n) - 1n, true);
|
|
532
|
+
view.setBigUint64(8, value >> 64n, true);
|
|
533
|
+
await boundaries.append(bytes);
|
|
534
|
+
}
|
|
535
|
+
this.#ends = await boundaries.finish();
|
|
536
|
+
this.#dataSource = await this.#data?.finish();
|
|
537
|
+
this.#offsetSource = await this.#offsets?.finish();
|
|
538
|
+
this.#sealed = true;
|
|
539
|
+
await Promise.all([this.#headers.close(), this.#boundaries.close()]);
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
if (operation === "get" || operation === "name") {
|
|
543
|
+
if (!this.#sealed) throw new Error("Python wheel index is not sealed");
|
|
544
|
+
const ordinal = operation === "name" && typeof args[0] === "string" ? await this.#names.get(args[0]) : args[0];
|
|
545
|
+
const offset = args[1];
|
|
546
|
+
if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid native ZIP response offset");
|
|
547
|
+
if (ordinal === void 0) return "null".slice(offset, offset + 8192);
|
|
548
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 0 || ordinal >= this.#count) throw new Error("Invalid native ZIP entry ordinal");
|
|
549
|
+
if (this.#cached?.ordinal === ordinal) return this.#cached.response.slice(offset, offset + 8192);
|
|
550
|
+
const pointer = await exact(this.#offsetSource, ordinal * 16, 16), view = new DataView(pointer.buffer);
|
|
551
|
+
const payload = new TextDecoder("utf-8", { fatal: true }).decode(await exact(this.#dataSource, Number(view.getBigUint64(0, true)), Number(view.getBigUint64(8, true))));
|
|
552
|
+
const boundary = await exact(this.#ends, ordinal * 16, 16), endView = new DataView(boundary.buffer);
|
|
553
|
+
const end = BigInt.asIntN(128, endView.getBigUint64(0, true) | endView.getBigUint64(8, true) << 64n).toString();
|
|
554
|
+
this.#cached = { ordinal, response: JSON.stringify([payload, end]) };
|
|
555
|
+
return this.#cached.response.slice(offset, offset + 8192);
|
|
556
|
+
}
|
|
557
|
+
throw new Error("Invalid native ZIP index operation");
|
|
558
|
+
});
|
|
559
|
+
this.#tail = work.catch((error) => {
|
|
560
|
+
this.#failed ??= { error };
|
|
561
|
+
});
|
|
562
|
+
return work;
|
|
563
|
+
}
|
|
564
|
+
close() {
|
|
565
|
+
return this.#closing ??= this.#tail.then(async () => {
|
|
566
|
+
this.#cached = void 0;
|
|
567
|
+
this.#pages.length = 0;
|
|
568
|
+
const results = await Promise.allSettled([this.#boundaries.close(), this.#names.close(), this.#headers.close()]);
|
|
569
|
+
try {
|
|
570
|
+
await this.#scratch.close();
|
|
571
|
+
} catch (reason) {
|
|
572
|
+
results.push({ status: "rejected", reason });
|
|
573
|
+
}
|
|
574
|
+
const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
575
|
+
if (errors.length === 1) throw errors[0];
|
|
576
|
+
if (errors.length) throw new AggregateError(errors, "Python wheel index cleanup failed");
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// packages/safe-bash-command-python/src/build-wheel.ts
|
|
582
|
+
import { FsError as FsError4 } from "../../safe-bash-contracts/index.js";
|
|
583
|
+
import { resolvePath as resolvePath4, basename } from "../../safe-bash-contracts/path.js";
|
|
584
|
+
|
|
585
|
+
// packages/safe-bash-command-python/src/package-download.ts
|
|
586
|
+
import { FsError as FsError3 } from "../../safe-bash-contracts/index.js";
|
|
587
|
+
import { resolvePath as resolvePath3 } from "../../safe-bash-contracts/path.js";
|
|
588
|
+
import { interruptible } from "../../safe-bash-contracts/runtime-control.js";
|
|
589
|
+
import { yieldTurn as yieldTurn2 } from "../../safe-bash-contracts/yield.js";
|
|
590
|
+
var serial2 = 0;
|
|
591
|
+
async function stagePythonPackage(context, directory, body, maxBytes, progress, verify, publish) {
|
|
592
|
+
const { fs, signal } = context, settings = { signal };
|
|
593
|
+
const caps = await fs.capabilitiesFor?.(directory, settings) ?? fs.capabilities;
|
|
594
|
+
if (!caps.retainedStagingWrite || !caps.retainedStagingCleanup || !caps.retainedRead || !fs.createStagedFile || !fs.openReadFile || publish && (!caps.atomicFileStaging || !fs.publishStagedFile)) return void 0;
|
|
595
|
+
const parent = await fs.stat(directory, settings);
|
|
596
|
+
let stage;
|
|
597
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
598
|
+
try {
|
|
599
|
+
stage = await fs.createStagedFile(resolvePath3(directory, ".python-package-" + ++serial2), "wheel", { type: "file", data: new Uint8Array(0) }, { parent, mode: 384, retainCleanup: true, signal });
|
|
600
|
+
break;
|
|
601
|
+
} catch (error) {
|
|
602
|
+
signal.throwIfAborted();
|
|
603
|
+
if (!(error instanceof FsError3) || error.code !== "EEXIST") throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!stage) throw new Error("Python package staging names exhausted");
|
|
607
|
+
const owner = stage;
|
|
608
|
+
let artifact;
|
|
609
|
+
let closing;
|
|
610
|
+
const close = () => closing ??= (async () => {
|
|
611
|
+
try {
|
|
612
|
+
await artifact?.close();
|
|
613
|
+
} finally {
|
|
614
|
+
try {
|
|
615
|
+
await owner.cleanup?.remove();
|
|
616
|
+
} finally {
|
|
617
|
+
await owner.cleanup?.close();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
})();
|
|
621
|
+
try {
|
|
622
|
+
if (!owner.cleanup || !owner.writer) throw new Error("Python package staging handles missing");
|
|
623
|
+
let count = 0;
|
|
624
|
+
const iterator = body[Symbol.asyncIterator]();
|
|
625
|
+
let ended = false;
|
|
626
|
+
try {
|
|
627
|
+
while (true) {
|
|
628
|
+
const next = await interruptible(Promise.resolve().then(() => {
|
|
629
|
+
signal.throwIfAborted();
|
|
630
|
+
return iterator.next();
|
|
631
|
+
}), signal);
|
|
632
|
+
signal.throwIfAborted();
|
|
633
|
+
if (next.done) {
|
|
634
|
+
ended = true;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
const bytes = next.value;
|
|
638
|
+
if (!(bytes instanceof Uint8Array)) throw new TypeError("Python package body must yield bytes");
|
|
639
|
+
if (count + bytes.length > maxBytes) throw new Error("Package download exceeds maxDownloadBytes");
|
|
640
|
+
for (let offset = 0; offset < bytes.length; offset += 65536) {
|
|
641
|
+
const chunk = Uint8Array.from(bytes.subarray(offset, offset + 65536));
|
|
642
|
+
await owner.writer.write(chunk, settings);
|
|
643
|
+
count += chunk.length;
|
|
644
|
+
await yieldTurn2(signal);
|
|
645
|
+
}
|
|
646
|
+
if (bytes.length) progress?.(count);
|
|
647
|
+
}
|
|
648
|
+
} finally {
|
|
649
|
+
if (!ended) {
|
|
650
|
+
const returned = Promise.resolve().then(() => iterator.return?.());
|
|
651
|
+
if (signal.aborted) void returned.catch(() => {
|
|
652
|
+
});
|
|
653
|
+
else await interruptible(returned, signal);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const stat = await owner.writer.finish(settings);
|
|
657
|
+
if (stat.size !== count) throw new Error("Python package staging size mismatch");
|
|
658
|
+
artifact = await openPythonPackageFile(context, owner.file.path, maxBytes, stat);
|
|
659
|
+
if (!artifact) throw new Error("Python package retained reads unavailable");
|
|
660
|
+
verify?.(artifact.key);
|
|
661
|
+
if (publish) {
|
|
662
|
+
const key = artifact.key, path = publish(key);
|
|
663
|
+
await artifact.close();
|
|
664
|
+
artifact = void 0;
|
|
665
|
+
let destination = null;
|
|
666
|
+
try {
|
|
667
|
+
destination = await fs.lstat(path, settings);
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (!(error instanceof FsError3) || error.code !== "ENOENT") throw error;
|
|
670
|
+
}
|
|
671
|
+
await fs.publishStagedFile({ ...owner, file: { ...owner.file, stat } }, path, { parent, destination, signal });
|
|
672
|
+
artifact = await openPythonPackageFile(context, path, maxBytes);
|
|
673
|
+
if (!artifact || artifact.key !== key) throw new Error("Published Python package integrity mismatch");
|
|
674
|
+
}
|
|
675
|
+
return { ...artifact, close };
|
|
676
|
+
} catch (error) {
|
|
677
|
+
try {
|
|
678
|
+
await close();
|
|
679
|
+
} catch (cleanup) {
|
|
680
|
+
throw new AggregateError([error, cleanup], error instanceof Error ? error.message : "Python package download failed");
|
|
681
|
+
}
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// packages/safe-bash-command-python/src/build-wheel.ts
|
|
687
|
+
async function publishPythonBuildWheel(source, directory, maxBytes, context) {
|
|
688
|
+
const { fs, signal } = context, settings = { signal };
|
|
689
|
+
let artifact = typeof source === "string" ? void 0 : source.artifact;
|
|
690
|
+
let reading;
|
|
691
|
+
try {
|
|
692
|
+
signal.throwIfAborted();
|
|
693
|
+
if (maxBytes !== Infinity && (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) throw new RangeError("Invalid Python wheel size limit");
|
|
694
|
+
const filename = typeof source === "string" ? basename(source) : source.filename;
|
|
695
|
+
if (basename(filename) !== filename || !filename.endsWith(".whl") || filename.includes("\\") || filename.includes("\0")) throw new TypeError("Invalid Python build wheel filename");
|
|
696
|
+
const root = await fs.realpath(resolvePath4(context.cwd, directory), settings);
|
|
697
|
+
const caps = await fs.capabilitiesFor?.(root, settings) ?? fs.capabilities;
|
|
698
|
+
if (!fs.confineExtraction || !fs.prepareDirectory || !caps.retainedStagingWrite || !caps.retainedStagingCleanup || !caps.retainedRead || !caps.atomicFileStaging) throw new Error("Python wheel publication requires confined retained atomic staging");
|
|
699
|
+
if (typeof source === "string") artifact = await openPythonPackageFile(context, resolvePath4(context.cwd, source), maxBytes);
|
|
700
|
+
if (!artifact) throw new Error("Python wheel publication requires retained source reads");
|
|
701
|
+
if (artifact.size > maxBytes) throw new Error("Canonical package exceeds maxDownloadBytes");
|
|
702
|
+
const retained = artifact;
|
|
703
|
+
const confined = await fs.confineExtraction([root], settings);
|
|
704
|
+
const destination = new Proxy(confined, { get(target, key) {
|
|
705
|
+
const owner = key === "openReadFile" ? fs : target, value = Reflect.get(owner, key);
|
|
706
|
+
return typeof value === "function" ? value.bind(owner) : value;
|
|
707
|
+
} });
|
|
708
|
+
const parent = await destination.stat(root, settings), path = resolvePath4(root, artifact.key);
|
|
709
|
+
try {
|
|
710
|
+
await destination.prepareDirectory(path, { parent, expected: null, mode: 448, signal });
|
|
711
|
+
} catch (error) {
|
|
712
|
+
if (!(error instanceof FsError4) || !["EEXIST", "EAGAIN"].includes(error.code)) throw error;
|
|
713
|
+
}
|
|
714
|
+
if ((await destination.lstat(path, settings)).type !== "directory") throw new Error("Python wheel publication directory changed");
|
|
715
|
+
const output = resolvePath4(path, filename);
|
|
716
|
+
const body = async function* () {
|
|
717
|
+
for (let offset = 0; offset < retained.size; ) {
|
|
718
|
+
const bytes = await (reading = Promise.resolve(retained.read(offset, Math.min(65536, retained.size - offset))));
|
|
719
|
+
if (!bytes.length || bytes.length > Math.min(65536, retained.size - offset)) throw new Error("Invalid Python wheel read");
|
|
720
|
+
offset += bytes.length;
|
|
721
|
+
yield bytes;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
const published = await stagePythonPackage({ ...context, fs: destination }, path, body(), maxBytes, void 0, (key) => {
|
|
725
|
+
if (key !== retained.key) throw new Error("Built Python wheel integrity mismatch");
|
|
726
|
+
}, () => output);
|
|
727
|
+
if (!published) throw new Error("Python wheel publication requires retained atomic staging");
|
|
728
|
+
try {
|
|
729
|
+
return { url: "file://" + output.split("/").map(encodeURIComponent).join("/"), digest: published.key, size: published.size };
|
|
730
|
+
} finally {
|
|
731
|
+
await published.close();
|
|
732
|
+
}
|
|
733
|
+
} finally {
|
|
734
|
+
try {
|
|
735
|
+
await reading?.catch(() => {
|
|
736
|
+
});
|
|
737
|
+
} finally {
|
|
738
|
+
await artifact?.close?.();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// packages/safe-bash-command-python/src/provisioning.ts
|
|
744
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
745
|
+
import { resolvePath as resolve, dirname as dirname2, basename as basename2 } from "../../safe-bash-contracts/path.js";
|
|
746
|
+
import { inheritYieldCheckpoint } from "../../safe-bash-contracts/yield.js";
|
|
747
|
+
|
|
748
|
+
// packages/safe-bash-command-python/src/cache.ts
|
|
749
|
+
var pythonPackageRuntimeKey = "pyodide-314.0.6-cp314-emscripten-wasm32-v1";
|
|
750
|
+
function createPythonPackageCache(options = {}) {
|
|
751
|
+
const maxBytes = options.maxBytes ?? Infinity;
|
|
752
|
+
if (options.maxBytes !== void 0 && (!Number.isSafeInteger(maxBytes) || maxBytes < 1)) throw new RangeError("Python cache maxBytes must be a positive integer");
|
|
753
|
+
const values = /* @__PURE__ */ new Map();
|
|
754
|
+
const manifestKey = pythonPackageRuntimeKey + "-environment";
|
|
755
|
+
let retainedBytes = 0;
|
|
756
|
+
let disposed = false;
|
|
757
|
+
return {
|
|
758
|
+
async get(key) {
|
|
759
|
+
if (disposed) throw new Error("Python package cache is disposed");
|
|
760
|
+
return values.get(key)?.slice();
|
|
761
|
+
},
|
|
762
|
+
async set(key, bytes) {
|
|
763
|
+
if (disposed) throw new Error("Python package cache is disposed");
|
|
764
|
+
if (key === manifestKey && bytes.length > maxBytes) throw new RangeError("Python package manifest exceeds maxCacheBytes");
|
|
765
|
+
const previous = values.get(key);
|
|
766
|
+
if (previous) {
|
|
767
|
+
retainedBytes -= previous.length;
|
|
768
|
+
values.delete(key);
|
|
769
|
+
}
|
|
770
|
+
if (bytes.length > maxBytes) return;
|
|
771
|
+
while (values.size && retainedBytes + bytes.length > maxBytes) {
|
|
772
|
+
const oldest = [...values.keys()].find((entry) => entry !== manifestKey);
|
|
773
|
+
if (oldest === void 0) return;
|
|
774
|
+
retainedBytes -= values.get(oldest).length;
|
|
775
|
+
values.delete(oldest);
|
|
776
|
+
}
|
|
777
|
+
values.set(key, Uint8Array.from(bytes));
|
|
778
|
+
retainedBytes += bytes.length;
|
|
779
|
+
},
|
|
780
|
+
dispose() {
|
|
781
|
+
disposed = true;
|
|
782
|
+
values.clear();
|
|
783
|
+
retainedBytes = 0;
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// packages/safe-bash-command-python/src/provisioning.ts
|
|
789
|
+
var pythonDocumentPackages = Object.freeze([
|
|
790
|
+
"lxml==6.0.2",
|
|
791
|
+
"pillow==12.2.0",
|
|
792
|
+
"python-docx==1.2.0",
|
|
793
|
+
"openpyxl==3.1.5",
|
|
794
|
+
"XlsxWriter==3.2.9",
|
|
795
|
+
"pypdf==6.18.1",
|
|
796
|
+
"fpdf2==2.8.8",
|
|
797
|
+
"fonttools==4.65.0",
|
|
798
|
+
"defusedxml==0.7.1",
|
|
799
|
+
"et-xmlfile==2.0.0",
|
|
800
|
+
"typing-extensions==4.16.0"
|
|
801
|
+
]);
|
|
802
|
+
var encoder = new TextEncoder();
|
|
803
|
+
var decoder = new TextDecoder();
|
|
804
|
+
function digest(value) {
|
|
805
|
+
return bytesToHex3(sha2563(value));
|
|
806
|
+
}
|
|
807
|
+
function validDigest(value) {
|
|
808
|
+
if (typeof value !== "string" || value.length !== 64) return false;
|
|
809
|
+
for (const char of value) if (!"0123456789abcdef".includes(char)) return false;
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
function failure(message, cause) {
|
|
813
|
+
return Object.assign(new Error(message, { cause }), { code: "EPACKAGE" });
|
|
814
|
+
}
|
|
815
|
+
function missing(error) {
|
|
816
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
817
|
+
}
|
|
818
|
+
function* requirementLines(source) {
|
|
819
|
+
let start = 0, pending = "";
|
|
820
|
+
for (let end = 0; end <= source.length; end++) {
|
|
821
|
+
if (end < source.length && !"\n\r\v\f\x85\u2028\u2029".includes(source[end])) continue;
|
|
822
|
+
let line = source.slice(start, end);
|
|
823
|
+
if (source[end] === "\r" && source[end + 1] === "\n") end++;
|
|
824
|
+
start = end + 1;
|
|
825
|
+
if (line.trimStart()[0] === "#") line = " " + line;
|
|
826
|
+
else if (line.endsWith("\\")) {
|
|
827
|
+
let first = 0, last = line.length;
|
|
828
|
+
while (line[first] === "\\") first++;
|
|
829
|
+
while (line[last - 1] === "\\") last--;
|
|
830
|
+
pending += line.slice(first, last);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
yield pending + line;
|
|
834
|
+
pending = "";
|
|
835
|
+
}
|
|
836
|
+
yield pending;
|
|
837
|
+
}
|
|
838
|
+
function expandRequirement(source, env = {}) {
|
|
839
|
+
let text = source;
|
|
840
|
+
for (let start = source.indexOf("${"); start >= 0; start = source.indexOf("${", start + 2)) {
|
|
841
|
+
let end = start + 2;
|
|
842
|
+
while (end < source.length && "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".includes(source[end])) end++;
|
|
843
|
+
if (end === start + 2 || source[end] !== "}") continue;
|
|
844
|
+
const name = source.slice(start + 2, end), value = Object.hasOwn(env, name) && env[name];
|
|
845
|
+
if (value) text = text.replaceAll(source.slice(start, end + 1), () => value);
|
|
846
|
+
}
|
|
847
|
+
return text;
|
|
848
|
+
}
|
|
849
|
+
function requirementOptions(line) {
|
|
850
|
+
const tokens = line.split(" "), start = tokens.findIndex((token) => token.startsWith("-"));
|
|
851
|
+
if (start < 0) return;
|
|
852
|
+
const requirement = tokens.slice(0, start).join(" ");
|
|
853
|
+
line = tokens.slice(start).join(" ");
|
|
854
|
+
if (!["-r", "-c", "-i", "--requirem", "--const", "--index-url", "--extra-index-url", "--no-index", "--pre"].some((prefix) => line.startsWith(prefix))) return;
|
|
855
|
+
const words = [];
|
|
856
|
+
let word = "", quote = "", active = false;
|
|
857
|
+
for (let index = 0; index < line.length; index++) {
|
|
858
|
+
const char = line[index];
|
|
859
|
+
if (char === "\\" && quote !== "'") {
|
|
860
|
+
const next = line[++index];
|
|
861
|
+
if (next === void 0) throw failure("No escaped character");
|
|
862
|
+
if (quote && next !== quote && next !== "\\") word += "\\";
|
|
863
|
+
word += next;
|
|
864
|
+
active = true;
|
|
865
|
+
} else if (quote) {
|
|
866
|
+
if (char === quote) quote = "";
|
|
867
|
+
else word += char;
|
|
868
|
+
} else if (char === '"' || char === "'") {
|
|
869
|
+
quote = char;
|
|
870
|
+
active = true;
|
|
871
|
+
} else if (" \r\n".includes(char)) {
|
|
872
|
+
if (active) {
|
|
873
|
+
words.push(word);
|
|
874
|
+
word = "";
|
|
875
|
+
active = false;
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
word += char;
|
|
879
|
+
active = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (quote) throw failure("No closing quotation");
|
|
883
|
+
if (active) words.push(word);
|
|
884
|
+
let included, constrained;
|
|
885
|
+
const options = { extraIndexUrls: [], ...requirement ? { requirement } : {} };
|
|
886
|
+
for (let index = 0; index < words.length; index++) {
|
|
887
|
+
const word2 = words[index], option = word2.split("=", 1)[0];
|
|
888
|
+
if (word2 === "--pre") {
|
|
889
|
+
options.pre = true;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (word2 === "--no-index") {
|
|
893
|
+
options.noIndex = true;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const indexKind = word2.startsWith("-i") || option === "--index-url" ? "indexUrl" : option === "--extra-index-url" ? "extraIndexUrls" : void 0;
|
|
897
|
+
const kind = indexKind ?? ["requirement", "constraint"].find((name) => word2.startsWith("-" + name[0]) || option.startsWith("--" + name.slice(0, name === "requirement" ? 8 : 5)) && ("--" + name).startsWith(option));
|
|
898
|
+
if (!kind) {
|
|
899
|
+
if (word2.startsWith("-")) throw failure(`Unsupported requirement option: ${word2}`);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const path = word2.startsWith("--") ? word2.length === option.length ? words[++index] : word2.slice(option.length + 1) : word2.length === 2 ? words[++index] : word2.slice(2);
|
|
903
|
+
if (path === void 0) throw failure(`Requirement option needs a file: ${word2}`);
|
|
904
|
+
if (kind === "requirement") included ??= path;
|
|
905
|
+
else if (kind === "constraint") constrained ??= path;
|
|
906
|
+
else if (kind === "indexUrl") options.indexUrl = path;
|
|
907
|
+
else options.extraIndexUrls.push(path);
|
|
908
|
+
}
|
|
909
|
+
return included !== void 0 ? { ...options, include: [included, false] } : constrained !== void 0 ? { ...options, include: [constrained, true] } : options;
|
|
910
|
+
}
|
|
911
|
+
function normalizeRequirement(value, cwd) {
|
|
912
|
+
const requirement = value.trim();
|
|
913
|
+
if (!requirement || requirement.startsWith("-")) throw failure(`Unsupported requirement: ${value}`);
|
|
914
|
+
if (requirement.endsWith(".whl") && !requirement.includes("://")) return "file://" + resolve(cwd, requirement).split("/").map(encodeURIComponent).join("/");
|
|
915
|
+
return requirement;
|
|
916
|
+
}
|
|
917
|
+
function artifactKey(digest2) {
|
|
918
|
+
return pythonPackageRuntimeKey + "-sha256-" + digest2;
|
|
919
|
+
}
|
|
920
|
+
function createPythonPackageEnvironment(options = {}) {
|
|
921
|
+
const { manifestStore } = options;
|
|
922
|
+
let ownedManifest;
|
|
923
|
+
if (options.cache && options.cacheDirectory) throw new TypeError("Choose package cache or cacheDirectory, not both");
|
|
924
|
+
if (manifestStore && (typeof options.scope !== "string" || !options.scope.trim())) throw new TypeError("Shared Python manifests require an explicit nonempty scope");
|
|
925
|
+
if (options.scope !== void 0 && !manifestStore) throw new TypeError("Python scope requires a manifestStore");
|
|
926
|
+
const manifestKey = pythonPackageRuntimeKey + "-environment" + (manifestStore ? "-" + digest(encoder.encode(JSON.stringify(options.scope))) : "");
|
|
927
|
+
if (options.profile !== void 0 && options.profile !== "documents") throw new TypeError("Unknown Python package profile");
|
|
928
|
+
const maxBytes = options.maxDownloadBytes ?? Infinity;
|
|
929
|
+
const maxManifestBytes = options.maxManifestBytes ?? Infinity;
|
|
930
|
+
const maxMetadataBytes = options.maxMetadataBytes ?? Infinity;
|
|
931
|
+
for (const name of ["maxDownloadBytes", "maxManifestBytes", "maxMetadataBytes", "maxRequirementBytes", "maxCacheBytes"]) {
|
|
932
|
+
const value = options[name];
|
|
933
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < 1)) throw new RangeError(`${name} must be a positive integer`);
|
|
934
|
+
}
|
|
935
|
+
const checkManifest = (bytes) => {
|
|
936
|
+
if (bytes && bytes.length > maxManifestBytes) throw failure("Python package manifest exceeds maxManifestBytes");
|
|
937
|
+
};
|
|
938
|
+
const checkMetadata = (bytes) => {
|
|
939
|
+
if (bytes.length > maxMetadataBytes) throw failure("Python package cache metadata exceeds maxMetadataBytes");
|
|
940
|
+
return bytes;
|
|
941
|
+
};
|
|
942
|
+
const cacheRecord = (key, headers, url) => checkMetadata(encoder.encode(JSON.stringify({ digest: key, headers, url })));
|
|
943
|
+
const defaultCache = createPythonPackageCache({ maxBytes: options.maxCacheBytes });
|
|
944
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
945
|
+
const controller = new AbortController();
|
|
946
|
+
const pending = /* @__PURE__ */ new Set();
|
|
947
|
+
let cleanupFailure;
|
|
948
|
+
const track = (work, cleanup = false) => {
|
|
949
|
+
pending.add(work);
|
|
950
|
+
void work.then(() => pending.delete(work), (error) => {
|
|
951
|
+
if (cleanup) cleanupFailure ??= { error };
|
|
952
|
+
pending.delete(work);
|
|
953
|
+
});
|
|
954
|
+
return work;
|
|
955
|
+
};
|
|
956
|
+
function release(session, all = false) {
|
|
957
|
+
const artifacts = all ? [...session.retained.values()] : [];
|
|
958
|
+
if (all) {
|
|
959
|
+
artifacts.push(...Object.values(session.indexes));
|
|
960
|
+
session.indexes = {};
|
|
961
|
+
}
|
|
962
|
+
if (all) {
|
|
963
|
+
if (session.snapshot) artifacts.push(session.snapshot);
|
|
964
|
+
session.snapshot = void 0;
|
|
965
|
+
session.retained.clear();
|
|
966
|
+
session.records = void 0;
|
|
967
|
+
if (session.recordReader) artifacts.push(session.recordReader);
|
|
968
|
+
session.recordReader = void 0;
|
|
969
|
+
}
|
|
970
|
+
if (session.closed && session.installationRoot) {
|
|
971
|
+
artifacts.push(session.installationRoot);
|
|
972
|
+
session.installationRoot = void 0;
|
|
973
|
+
}
|
|
974
|
+
if (session.opened) {
|
|
975
|
+
artifacts.push(session.opened);
|
|
976
|
+
session.opened = void 0;
|
|
977
|
+
}
|
|
978
|
+
if (!artifacts.length) return session.retiring ?? Promise.resolve();
|
|
979
|
+
return track(session.retiring = Promise.allSettled([session.retiring, ...artifacts.map((artifact) => Promise.resolve().then(() => artifact.close?.()))]).then((results) => {
|
|
980
|
+
for (const result of results) if (result.status === "rejected") throw result.reason;
|
|
981
|
+
}), true);
|
|
982
|
+
}
|
|
983
|
+
let disposed = false;
|
|
984
|
+
let disposing;
|
|
985
|
+
const admit = (operation) => (...args) => track(Promise.resolve().then(() => {
|
|
986
|
+
if (disposed) throw failure("Python package environment is disposed");
|
|
987
|
+
return operation(...args);
|
|
988
|
+
}));
|
|
989
|
+
let counter = 0;
|
|
990
|
+
let committing = Promise.resolve();
|
|
991
|
+
async function prepare(input) {
|
|
992
|
+
const invocation = new AbortController();
|
|
993
|
+
const signal = AbortSignal.any([input.signal, controller.signal, invocation.signal]);
|
|
994
|
+
inheritYieldCheckpoint(input.signal, signal);
|
|
995
|
+
const context = { ...input, signal };
|
|
996
|
+
signal.throwIfAborted();
|
|
997
|
+
const directory = options.cacheDirectory === void 0 ? void 0 : resolve(context.cwd, options.cacheDirectory, pythonPackageRuntimeKey);
|
|
998
|
+
const noCache = context.noCache ?? options.noCache ?? false;
|
|
999
|
+
const artifactDirectory = directory ?? (!options.cache && options.maxCacheBytes === void 0 && !noCache ? resolve(context.cwd, ".python-packages", "cache", pythonPackageRuntimeKey) : void 0);
|
|
1000
|
+
const cache = options.cache ?? (!artifactDirectory ? defaultCache : {
|
|
1001
|
+
async get(key) {
|
|
1002
|
+
try {
|
|
1003
|
+
return await context.fs.readFile(resolve(artifactDirectory, key), { signal });
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (missing(error)) return void 0;
|
|
1006
|
+
throw error;
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
async set(key, bytes) {
|
|
1010
|
+
await context.fs.mkdir(artifactDirectory, { recursive: true, signal });
|
|
1011
|
+
await context.fs.writeFile(resolve(artifactDirectory, key), bytes, { signal });
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
const store = manifestStore ?? (directory ? await createConfiguredPythonManifestStore(context, directory, manifestKey, maxManifestBytes, options.maxCacheBytes) : (ownedManifest ??= createDefaultPythonManifestStore(options.maxCacheBytes)).forContext(context));
|
|
1015
|
+
const open = input.recordTransport === "host" ? store.openSnapshot : void 0;
|
|
1016
|
+
let backed, adopted = false;
|
|
1017
|
+
try {
|
|
1018
|
+
const structured = open ? void 0 : store.getSnapshot;
|
|
1019
|
+
let snapshot;
|
|
1020
|
+
try {
|
|
1021
|
+
if (open) backed = await open.call(store, manifestKey, { signal, maxBytes: maxManifestBytes });
|
|
1022
|
+
else snapshot = await (structured ? structured.call(store, manifestKey, { signal, maxBytes: maxManifestBytes }) : store.get(manifestKey, context));
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
signal.throwIfAborted();
|
|
1025
|
+
throw failure("Cannot read Python package environment manifest", error);
|
|
1026
|
+
}
|
|
1027
|
+
signal.throwIfAborted();
|
|
1028
|
+
if (snapshot !== void 0 && (typeof snapshot !== "object" || !snapshot || typeof snapshot.revision !== "string" || !snapshot.revision || snapshot.revision.length > 1024 || !structured && !(snapshot.bytes instanceof Uint8Array))) throw failure("Invalid Python package manifest snapshot");
|
|
1029
|
+
if (backed && (!backed.revision || typeof backed.revision !== "string" || backed.revision.length > 1024 || ![0, 1, 2, 3].includes(backed.version) || !readPackageManifest(backed.installed) || !Number.isSafeInteger(backed.recordCount) || backed.recordCount < -1 || typeof backed.readRecord !== "function" || typeof backed.close !== "function")) throw failure("Invalid Python package manifest snapshot");
|
|
1030
|
+
const manifestRevision = backed?.revision ?? snapshot?.revision;
|
|
1031
|
+
const stored = snapshot?.bytes;
|
|
1032
|
+
signal.throwIfAborted();
|
|
1033
|
+
checkManifest(stored);
|
|
1034
|
+
let previous;
|
|
1035
|
+
try {
|
|
1036
|
+
previous = open ? backed?.installed ?? [] : structured ? snapshot ? snapshot.value : [] : stored === void 0 ? [] : JSON.parse(decoder.decode(stored));
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
1039
|
+
const saved = readPackageManifest(previous);
|
|
1040
|
+
if (!saved) throw failure("Invalid Python package environment manifest");
|
|
1041
|
+
const legacy = backed ? backed.version === 0 : Array.isArray(previous);
|
|
1042
|
+
const restore = saved.map((value) => normalizeRequirement(value, context.cwd));
|
|
1043
|
+
const requirements = [...options.profile ? pythonDocumentPackages : [], ...options.requirements ?? [], ...context.requirements ?? []].map((value) => normalizeRequirement(value, context.cwd));
|
|
1044
|
+
const constraints = [...options.constraints ?? [], ...context.constraints ?? []];
|
|
1045
|
+
const controls = {};
|
|
1046
|
+
for (const key of ["pre", "upgrade", "forceReinstall", "noDeps"]) if (context[key] ?? options[key]) controls[key] = true;
|
|
1047
|
+
let indexUrls = context.noIndex ?? options.noIndex ? [] : [context.indexUrl ?? options.indexUrl ?? "https://pypi.org/simple", ...context.extraIndexUrls ?? options.extraIndexUrls ?? []];
|
|
1048
|
+
const activeFiles = /* @__PURE__ */ new Set();
|
|
1049
|
+
let requirementBytes = 0;
|
|
1050
|
+
const readRequirements = async (path, constraint = false) => {
|
|
1051
|
+
signal.throwIfAborted();
|
|
1052
|
+
let source, identity;
|
|
1053
|
+
try {
|
|
1054
|
+
identity = await context.fs.realpath(path, { signal });
|
|
1055
|
+
if (activeFiles.has(identity)) throw failure(`Recursive Python requirements: ${path}`);
|
|
1056
|
+
activeFiles.add(identity);
|
|
1057
|
+
const bytes = await context.fs.readFile(path, { signal, ...options.maxRequirementBytes !== void 0 ? { maxBytes: options.maxRequirementBytes - requirementBytes } : {} });
|
|
1058
|
+
requirementBytes += bytes.length;
|
|
1059
|
+
if (requirementBytes > (options.maxRequirementBytes ?? Infinity)) throw failure("Python requirements byte limit exceeded");
|
|
1060
|
+
source = decoder.decode(bytes);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
signal.throwIfAborted();
|
|
1063
|
+
throw failure(`Cannot read Python requirements ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1064
|
+
}
|
|
1065
|
+
for (const line of requirementLines(source)) {
|
|
1066
|
+
let comment = line.indexOf("#");
|
|
1067
|
+
while (comment > 0 && line[comment - 1].trim()) comment = line.indexOf("#", comment + 1);
|
|
1068
|
+
const text = (comment < 0 ? line : line.slice(0, comment)).trim();
|
|
1069
|
+
if (!text) continue;
|
|
1070
|
+
const expanded = expandRequirement(text, context.env);
|
|
1071
|
+
const parsed = requirementOptions(expanded);
|
|
1072
|
+
if (parsed?.requirement) (constraint ? constraints : requirements).push(normalizeRequirement(parsed.requirement, dirname2(path)));
|
|
1073
|
+
else if (parsed?.include) await readRequirements(resolve(dirname2(path), parsed.include[0]), parsed.include[1]);
|
|
1074
|
+
else if (parsed) {
|
|
1075
|
+
if (parsed.indexUrl) indexUrls = [parsed.indexUrl];
|
|
1076
|
+
if (parsed.noIndex) indexUrls = [];
|
|
1077
|
+
indexUrls.push(...parsed.extraIndexUrls);
|
|
1078
|
+
if (parsed.pre) controls.pre = true;
|
|
1079
|
+
} else (constraint ? constraints : requirements).push(expanded[0] === "-" && options.prepareRequirements ? expanded : normalizeRequirement(expanded, dirname2(path)));
|
|
1080
|
+
}
|
|
1081
|
+
activeFiles.delete(identity);
|
|
1082
|
+
};
|
|
1083
|
+
for (const file of [...options.constraintFiles ?? [], ...context.constraintFiles ?? []]) await readRequirements(resolve(context.cwd, file), true);
|
|
1084
|
+
for (const file of [...options.requirementFiles ?? [], ...context.requirementFiles ?? []]) await readRequirements(resolve(context.cwd, file));
|
|
1085
|
+
if ((options.editable?.length || context.editable?.length) && !options.prepareRequirements) throw failure("Editable packages require a source package environment");
|
|
1086
|
+
const requested = [...new Set(await options.prepareRequirements?.(requirements, { ...context, ...controls, noIndex: indexUrls.length === 0, ...indexUrls.length ? { indexUrl: indexUrls[0], extraIndexUrls: indexUrls.slice(1) } : {} }) ?? requirements)];
|
|
1087
|
+
signal.throwIfAborted();
|
|
1088
|
+
const session = String(++counter);
|
|
1089
|
+
const unique = [.../* @__PURE__ */ new Set([...restore, ...requested])];
|
|
1090
|
+
const aborted = () => {
|
|
1091
|
+
const current = sessions.get(session);
|
|
1092
|
+
if (current) {
|
|
1093
|
+
current.closed = true;
|
|
1094
|
+
void release(current, true);
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const offline = context.offline ?? options.offline ?? false;
|
|
1098
|
+
const records = previous.records;
|
|
1099
|
+
const hostRecords = input.recordTransport === "host";
|
|
1100
|
+
sessions.set(session, { ...context, snapshot: backed, records: hostRecords ? records : void 0, indexes: {}, cacheDirectory: directory, artifactDirectory, noCache, cache, manifestStore: store, manifestRevision, controller: invocation, offline, requirements: unique, opening: false, retained: /* @__PURE__ */ new Map(), closed: false, aborted });
|
|
1101
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
1102
|
+
adopted = true;
|
|
1103
|
+
return { session, indexUrls, requirements: unique, restore, requested, ...store.compareAndSetSource ? { streamManifest: true } : {}, ...constraints.length ? { constraints: [...new Set(constraints)] } : {}, legacy, ...hostRecords ? { recordCount: backed?.recordCount ?? records?.length ?? -1 } : { records }, ...controls, ...input.uninstall ? { uninstall: input.uninstall } : {}, offline };
|
|
1104
|
+
} finally {
|
|
1105
|
+
if (!adopted) await backed?.close();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function dispatch(op, args, _context) {
|
|
1109
|
+
_context.signal.throwIfAborted();
|
|
1110
|
+
const session = sessions.get(String(args[0]));
|
|
1111
|
+
function check() {
|
|
1112
|
+
session?.signal.throwIfAborted();
|
|
1113
|
+
if (!session || session.closed) throw failure("Python package session is closed");
|
|
1114
|
+
}
|
|
1115
|
+
check();
|
|
1116
|
+
const { fs, signal, cwd, cacheDirectory: configuredCache } = session;
|
|
1117
|
+
const settings = { signal };
|
|
1118
|
+
if (op === "package-record-read") {
|
|
1119
|
+
const ordinal = args[1], offset = args[2], field = args[3];
|
|
1120
|
+
if (field !== void 0 && (!Number.isInteger(field) || field < 0 || field > 5) || !Number.isSafeInteger(ordinal) || ordinal < 0 || ordinal >= (session.snapshot?.recordCount ?? session.records?.length ?? 0) || !Number.isSafeInteger(offset) || offset < 0) throw failure("Invalid package record request");
|
|
1121
|
+
if (session.snapshot) {
|
|
1122
|
+
const result2 = await (field === void 0 ? session.snapshot.readRecord(ordinal, offset) : session.snapshot.readField?.(ordinal, field, offset) ?? false);
|
|
1123
|
+
check();
|
|
1124
|
+
return result2;
|
|
1125
|
+
}
|
|
1126
|
+
session.recordReader ??= createPythonRecordReader((key) => {
|
|
1127
|
+
const row = session.records[Math.floor(key / 7)], field2 = key % 7 - 1;
|
|
1128
|
+
return serializeLlmJsonValue2(field2 < 0 ? row.length === 5 ? [...row, null] : row : row[field2] ?? null, signal);
|
|
1129
|
+
});
|
|
1130
|
+
const result = await session.recordReader.read(ordinal * 7 + (field === void 0 ? 0 : field + 1), offset);
|
|
1131
|
+
check();
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
if (op === "package-commit" || op === "package-commit-file") {
|
|
1135
|
+
await release(session, true);
|
|
1136
|
+
check();
|
|
1137
|
+
const { manifestStore: manifestStore2 } = session;
|
|
1138
|
+
let publish;
|
|
1139
|
+
if (op === "package-commit-file") {
|
|
1140
|
+
const streamed = manifestStore2.compareAndSetSource;
|
|
1141
|
+
if (!streamed || !session.installationRoot) throw failure("Python file publication is unavailable");
|
|
1142
|
+
const cwd2 = await session.installationRoot.path();
|
|
1143
|
+
publish = () => withPythonManifestPublication({ ...session, cwd: cwd2 }, maxManifestBytes, (source) => streamed.call(manifestStore2, manifestKey, session.manifestRevision, source, { signal, maxBytes: maxManifestBytes }));
|
|
1144
|
+
} else {
|
|
1145
|
+
const pinned = args[1], saved = readPackageManifest(pinned);
|
|
1146
|
+
if (!saved) throw failure("Invalid installed package manifest");
|
|
1147
|
+
const state = Array.isArray(pinned) ? [.../* @__PURE__ */ new Set([...session.requirements, ...saved])] : { ...pinned, installed: [...new Set(saved)] };
|
|
1148
|
+
const streamed = manifestStore2.compareAndSetSnapshot, manifestBytes = streamed ? void 0 : encoder.encode(JSON.stringify(state));
|
|
1149
|
+
checkManifest(manifestBytes);
|
|
1150
|
+
publish = () => streamed ? streamed.call(manifestStore2, manifestKey, session.manifestRevision, state, { signal, maxBytes: maxManifestBytes }) : manifestStore2.compareAndSet(manifestKey, session.manifestRevision, manifestBytes, settings);
|
|
1151
|
+
}
|
|
1152
|
+
const commit = committing.then(async () => {
|
|
1153
|
+
check();
|
|
1154
|
+
const committed = await publish();
|
|
1155
|
+
check();
|
|
1156
|
+
if (typeof committed !== "boolean") throw failure("Invalid Python package manifest publication result");
|
|
1157
|
+
if (!committed) throw new PythonPackageConflictError();
|
|
1158
|
+
check();
|
|
1159
|
+
});
|
|
1160
|
+
committing = commit.catch(() => {
|
|
1161
|
+
});
|
|
1162
|
+
await commit;
|
|
1163
|
+
options.onProgress?.({ phase: "installed" });
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
const root = op === "package-root", prefix = typeof args[1] === "string" ? args[1].split("-", 1)[0] : void 0;
|
|
1167
|
+
const kind = prefix === "names" || prefix === "records" ? prefix : "wheel";
|
|
1168
|
+
const operation = kind === "wheel" ? args[1] : args[1].slice(prefix.length + 1);
|
|
1169
|
+
if (root || op === "package-index" && operation === "start") {
|
|
1170
|
+
if (root && session.installationRoot) return session.installationRoot.path();
|
|
1171
|
+
let directory = configuredCache ? dirname2(configuredCache) : cwd;
|
|
1172
|
+
if (configuredCache) {
|
|
1173
|
+
const caps = await fs.capabilitiesFor?.(directory, { signal, create: true }) ?? fs.capabilities;
|
|
1174
|
+
if (root ? !caps.atomicTreeRemoval : !caps.retainedRead || !caps.retainedStagingWrite || !caps.retainedStagingCleanup) directory = cwd;
|
|
1175
|
+
}
|
|
1176
|
+
check();
|
|
1177
|
+
const storage = { fs, signal, cwd: directory };
|
|
1178
|
+
if (root) return (session.installationRoot ??= new PythonInstallationRoot(storage)).path();
|
|
1179
|
+
if (session.indexes[kind]) throw failure("Python package index already open");
|
|
1180
|
+
session.indexes[kind] = new PythonPackageIndex(storage, kind);
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
if (op === "package-index") {
|
|
1184
|
+
if (operation === "close") {
|
|
1185
|
+
const index2 = session.indexes[kind];
|
|
1186
|
+
delete session.indexes[kind];
|
|
1187
|
+
await index2?.close();
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
const index = session.indexes[kind];
|
|
1191
|
+
if (!index) throw failure("Python package index is closed");
|
|
1192
|
+
return index.execute(operation, args.slice(2));
|
|
1193
|
+
}
|
|
1194
|
+
if (op === "package-read" || op === "package-read-retained") {
|
|
1195
|
+
const artifact = op === "package-read" ? session.opened : session.retained.get(String(args[1])), offset = args[2], length = args[3];
|
|
1196
|
+
if (!artifact || op === "package-read" && artifact.key !== String(args[1]) || !Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || offset < 0 || length < 1 || length > 65536) throw failure("Invalid package chunk request");
|
|
1197
|
+
const bytes = await artifact.read(offset, length);
|
|
1198
|
+
check();
|
|
1199
|
+
return Array.from(bytes);
|
|
1200
|
+
}
|
|
1201
|
+
if (op === "package-retain") {
|
|
1202
|
+
let artifact = session.opened;
|
|
1203
|
+
if (!artifact || artifact.key !== String(args[1])) throw failure("Invalid package retention request");
|
|
1204
|
+
if (artifact.url && artifact.close) {
|
|
1205
|
+
const source = artifact;
|
|
1206
|
+
const retaining = (async () => {
|
|
1207
|
+
const path = decodeURIComponent(new URL(source.url).pathname);
|
|
1208
|
+
if (!fs.confineExtraction || !fs.prepareDirectory) return;
|
|
1209
|
+
const fallback = resolve(cwd, ".python-packages", "installed");
|
|
1210
|
+
let directory = configuredCache ? resolve(configuredCache, "installed") : fallback, probe = directory;
|
|
1211
|
+
const required = ["retainedStagingWrite", "retainedStagingCleanup", "retainedRead", "atomicFileStaging"];
|
|
1212
|
+
for (; ; ) {
|
|
1213
|
+
try {
|
|
1214
|
+
const capabilities = await fs.capabilitiesFor?.(probe, settings) ?? fs.capabilities;
|
|
1215
|
+
if (required.every((key) => capabilities[key])) break;
|
|
1216
|
+
if (directory === fallback) return;
|
|
1217
|
+
probe = directory = fallback;
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
check();
|
|
1220
|
+
if (!missing(error) || dirname2(probe) === probe) throw error;
|
|
1221
|
+
probe = dirname2(probe);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
await fs.mkdir(directory, { recursive: true, signal });
|
|
1225
|
+
const target = resolve(directory, source.key, basename2(path));
|
|
1226
|
+
if (path !== target || probe !== directory) {
|
|
1227
|
+
check();
|
|
1228
|
+
session.opened = void 0;
|
|
1229
|
+
const published = await publishPythonBuildWheel({ filename: basename2(path), artifact: source }, directory, maxBytes, session);
|
|
1230
|
+
artifact = await openPythonPackageFile(session, decodeURIComponent(new URL(published.url).pathname), maxBytes);
|
|
1231
|
+
if (!artifact) throw failure("Installed Python wheels require retained reads");
|
|
1232
|
+
if (artifact.key !== published.digest) {
|
|
1233
|
+
await artifact.close?.();
|
|
1234
|
+
throw failure("Installed Python wheel changed");
|
|
1235
|
+
}
|
|
1236
|
+
session.opened = artifact;
|
|
1237
|
+
artifact.url = published.url;
|
|
1238
|
+
try {
|
|
1239
|
+
check();
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
await release(session);
|
|
1242
|
+
throw error;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const durable = new URL(artifact.url);
|
|
1246
|
+
durable.hash = "sha256=" + artifact.key;
|
|
1247
|
+
artifact.url = durable.href;
|
|
1248
|
+
})();
|
|
1249
|
+
session.retaining = retaining;
|
|
1250
|
+
try {
|
|
1251
|
+
await retaining;
|
|
1252
|
+
} finally {
|
|
1253
|
+
session.retaining = void 0;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
check();
|
|
1257
|
+
if (!artifact) throw failure("Installed Python wheel unavailable");
|
|
1258
|
+
const token = String(++counter);
|
|
1259
|
+
session.retained.set(token, artifact);
|
|
1260
|
+
session.opened = void 0;
|
|
1261
|
+
return { token, key: artifact.key, size: artifact.size, ...artifact.url ? { url: artifact.url } : {} };
|
|
1262
|
+
}
|
|
1263
|
+
if (op === "package-close") {
|
|
1264
|
+
if (session.opened?.key === String(args[1])) await release(session);
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
if (op !== "package-open" || typeof args[1] !== "string") throw failure("Invalid package operation");
|
|
1268
|
+
if (session.opening || session.retaining) throw failure("Package download already in progress");
|
|
1269
|
+
session.opening = true;
|
|
1270
|
+
try {
|
|
1271
|
+
await release(session);
|
|
1272
|
+
check();
|
|
1273
|
+
const url = args[1], { artifactDirectory: cacheDirectory, noCache } = session;
|
|
1274
|
+
const expected = args[2];
|
|
1275
|
+
let responseUrl = url;
|
|
1276
|
+
const verifyIntegrity = (key2) => {
|
|
1277
|
+
if (expected && key2 !== expected) throw failure(`Package integrity mismatch: ${url}`);
|
|
1278
|
+
};
|
|
1279
|
+
if (expected !== void 0 && expected !== null && !validDigest(expected)) throw failure("Invalid SHA-256 package integrity value");
|
|
1280
|
+
const address = pythonPackageRuntimeKey + "-url-" + digest(encoder.encode(url));
|
|
1281
|
+
const canonicalWheel = url.startsWith("file:") || url.startsWith("emfs:");
|
|
1282
|
+
const metadata = canonicalWheel || noCache || args[3] === "metadata" && !session.offline ? void 0 : await session.cache.get(address);
|
|
1283
|
+
check();
|
|
1284
|
+
let key = "", bytes;
|
|
1285
|
+
let headers = [];
|
|
1286
|
+
const adopt = async (artifact, ready) => {
|
|
1287
|
+
session.opened = artifact;
|
|
1288
|
+
if (canonicalWheel) artifact.url = url;
|
|
1289
|
+
try {
|
|
1290
|
+
check();
|
|
1291
|
+
await ready();
|
|
1292
|
+
check();
|
|
1293
|
+
return { key: artifact.key, size: artifact.size, headers, url: responseUrl };
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
await release(session);
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
const openArtifact = async (source) => {
|
|
1300
|
+
const retained = await openPythonPackageFile(session, source, maxBytes);
|
|
1301
|
+
if (retained) return retained;
|
|
1302
|
+
const capabilities = await fs.capabilitiesFor?.(source, settings) ?? fs.capabilities;
|
|
1303
|
+
if (capabilities.streamingRead && fs.readStream) {
|
|
1304
|
+
for (const directory of configuredCache ? [configuredCache, cwd] : [cwd]) {
|
|
1305
|
+
if (directory !== cwd) await fs.mkdir(directory, { recursive: true, signal });
|
|
1306
|
+
const body = (async function* () {
|
|
1307
|
+
yield* fs.readStream(source, settings);
|
|
1308
|
+
})();
|
|
1309
|
+
const staged = await stagePythonPackage(session, directory, body, maxBytes);
|
|
1310
|
+
if (staged) return staged;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
if (metadata) {
|
|
1315
|
+
checkMetadata(metadata);
|
|
1316
|
+
let record;
|
|
1317
|
+
try {
|
|
1318
|
+
record = JSON.parse(decoder.decode(metadata));
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
if (typeof record !== "object" || record === null || !validDigest(record.digest) || record.url !== void 0 && typeof record.url !== "string" || !Array.isArray(record.headers) || record.headers.some((pair) => !Array.isArray(pair) || pair.length !== 2 || pair.some((value2) => typeof value2 !== "string"))) throw failure(`Invalid package cache metadata: ${url}`);
|
|
1322
|
+
headers = record.headers;
|
|
1323
|
+
responseUrl = record.url ?? url;
|
|
1324
|
+
let absent = false;
|
|
1325
|
+
if (cacheDirectory) {
|
|
1326
|
+
let artifact;
|
|
1327
|
+
try {
|
|
1328
|
+
artifact = await openArtifact(resolve(cacheDirectory, artifactKey(record.digest)));
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (!missing(error)) throw error;
|
|
1331
|
+
absent = true;
|
|
1332
|
+
}
|
|
1333
|
+
if (artifact) {
|
|
1334
|
+
return await adopt(artifact, () => {
|
|
1335
|
+
if (artifact.key !== record.digest) throw failure(`Package cache integrity mismatch: ${url}`);
|
|
1336
|
+
verifyIntegrity(artifact.key);
|
|
1337
|
+
options.onProgress?.({ phase: "cached", url, bytes: artifact.size });
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
bytes = absent ? void 0 : await session.cache.get(artifactKey(record.digest));
|
|
1342
|
+
check();
|
|
1343
|
+
if (bytes && bytes.length > maxBytes) throw failure("Cached package exceeds maxDownloadBytes");
|
|
1344
|
+
if (bytes) bytes = Uint8Array.from(bytes);
|
|
1345
|
+
if (bytes && (key = digest(bytes)) !== record.digest) throw failure(`Package cache integrity mismatch: ${url}`);
|
|
1346
|
+
if (bytes) options.onProgress?.({ phase: "cached", url, bytes: bytes.length });
|
|
1347
|
+
}
|
|
1348
|
+
if (!bytes) {
|
|
1349
|
+
if (canonicalWheel) {
|
|
1350
|
+
const path = new URL(url);
|
|
1351
|
+
if (path.host && path.host !== "localhost") throw failure("Local wheels must use the canonical filesystem");
|
|
1352
|
+
try {
|
|
1353
|
+
const source = decodeURIComponent(path.pathname);
|
|
1354
|
+
const artifact = await openArtifact(source);
|
|
1355
|
+
if (artifact) {
|
|
1356
|
+
return await adopt(artifact, () => verifyIntegrity(artifact.key));
|
|
1357
|
+
}
|
|
1358
|
+
bytes = Uint8Array.from(await fs.readFile(source, { signal, ...Number.isFinite(maxBytes) ? { maxBytes } : {} }));
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
check();
|
|
1361
|
+
throw failure(`Cannot read canonical Python wheel ${path.pathname}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1362
|
+
}
|
|
1363
|
+
} else {
|
|
1364
|
+
if (session.offline) throw failure(`Offline package cache miss: ${url}`);
|
|
1365
|
+
if (!options.transport || !options.authorize) throw failure("Python package download requires configured transport and authorization");
|
|
1366
|
+
let current = new URL(url);
|
|
1367
|
+
let redirectFrom;
|
|
1368
|
+
for (let redirect = 0; ; redirect++) {
|
|
1369
|
+
if (current.protocol !== "https:" && current.protocol !== "http:") throw failure(`Unsupported package transport protocol: ${current.protocol}`);
|
|
1370
|
+
if (current.username || current.password) throw failure("Package URLs with credentials are unsupported");
|
|
1371
|
+
let denyPrivate = false;
|
|
1372
|
+
const allowed = await options.authorize({ url: current.href, method: "GET", attempt: 0, signal, ...redirectFrom === void 0 ? {} : { redirectFrom }, requirePrivateNetworkDeny() {
|
|
1373
|
+
denyPrivate = true;
|
|
1374
|
+
} });
|
|
1375
|
+
check();
|
|
1376
|
+
if (!allowed) throw failure(`Python package download authorization denied: ${current.href}`);
|
|
1377
|
+
if (denyPrivate && !options.transport.supportsPrivateNetworkDeny) throw failure("Package transport cannot enforce private network denial");
|
|
1378
|
+
check();
|
|
1379
|
+
const response = await options.transport({ url: current.href, method: "GET", headers: [["accept", "application/vnd.pypi.simple.v1+json, application/json;q=0.9, */*;q=0.1"]], signal, ...denyPrivate ? { denyPrivateNetworks: true } : {} });
|
|
1380
|
+
try {
|
|
1381
|
+
check();
|
|
1382
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
1383
|
+
if (redirect >= 5) throw failure("Too many package redirects");
|
|
1384
|
+
const location = header(response.headers, "location");
|
|
1385
|
+
if (!location) throw failure("Package redirect has no location");
|
|
1386
|
+
redirectFrom = current.href;
|
|
1387
|
+
current = new URL(location, current);
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
if (response.status < 200 || response.status >= 300) throw failure(`Package download failed: HTTP ${response.status} ${current.href}`);
|
|
1391
|
+
headers = response.headers;
|
|
1392
|
+
responseUrl = current.href;
|
|
1393
|
+
const encoding = header(headers, "content-encoding")?.trim().toLowerCase();
|
|
1394
|
+
const length = header(headers, "content-length");
|
|
1395
|
+
const total = length === void 0 || encoding !== void 0 && encoding !== "identity" ? void 0 : Number(length);
|
|
1396
|
+
if (Number.isFinite(total) && total > maxBytes) throw failure("Package download exceeds maxDownloadBytes");
|
|
1397
|
+
const progress = (count2) => options.onProgress?.({ phase: "download", url, bytes: count2, ...Number.isSafeInteger(total) ? { totalBytes: total } : {} });
|
|
1398
|
+
if (cacheDirectory || noCache) {
|
|
1399
|
+
const directory = cacheDirectory ?? cwd;
|
|
1400
|
+
if (cacheDirectory) await fs.mkdir(directory, { recursive: true, signal });
|
|
1401
|
+
let metadataBytes;
|
|
1402
|
+
const verify = (key2) => {
|
|
1403
|
+
verifyIntegrity(key2);
|
|
1404
|
+
if (!noCache) metadataBytes = cacheRecord(key2, headers, responseUrl);
|
|
1405
|
+
};
|
|
1406
|
+
const destination = (key2) => resolve(directory, artifactKey(key2));
|
|
1407
|
+
let artifact = await stagePythonPackage(session, directory, response.body, maxBytes, progress, verify, noCache ? void 0 : destination);
|
|
1408
|
+
let streamed = false;
|
|
1409
|
+
if (!artifact && directory !== cwd && (noCache || fs.writeStream && (await fs.capabilitiesFor?.(directory, settings) ?? fs.capabilities).streamingWrite)) {
|
|
1410
|
+
artifact = await stagePythonPackage(session, cwd, response.body, maxBytes, progress, verify);
|
|
1411
|
+
streamed = true;
|
|
1412
|
+
}
|
|
1413
|
+
if (artifact) {
|
|
1414
|
+
const retained = artifact;
|
|
1415
|
+
return await adopt(retained, async () => {
|
|
1416
|
+
if (!noCache) {
|
|
1417
|
+
if (streamed) await fs.writeStream(destination(retained.key), (async function* () {
|
|
1418
|
+
for (let offset2 = 0; offset2 < retained.size; ) {
|
|
1419
|
+
const chunk = await retained.read(offset2, 65536);
|
|
1420
|
+
offset2 += chunk.length;
|
|
1421
|
+
yield chunk;
|
|
1422
|
+
}
|
|
1423
|
+
})(), settings);
|
|
1424
|
+
check();
|
|
1425
|
+
await session.cache.set(address, metadataBytes);
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const chunks = [];
|
|
1431
|
+
let count = 0;
|
|
1432
|
+
for await (const chunk of response.body) {
|
|
1433
|
+
check();
|
|
1434
|
+
if (chunk.length === 0) continue;
|
|
1435
|
+
count += chunk.length;
|
|
1436
|
+
if (count > maxBytes) throw failure("Package download exceeds maxDownloadBytes");
|
|
1437
|
+
chunks.push(Uint8Array.from(chunk));
|
|
1438
|
+
progress(count);
|
|
1439
|
+
}
|
|
1440
|
+
bytes = new Uint8Array(count);
|
|
1441
|
+
let offset = 0;
|
|
1442
|
+
for (const chunk of chunks) {
|
|
1443
|
+
bytes.set(chunk, offset);
|
|
1444
|
+
offset += chunk.length;
|
|
1445
|
+
}
|
|
1446
|
+
break;
|
|
1447
|
+
} finally {
|
|
1448
|
+
await response.dispose();
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
check();
|
|
1453
|
+
verifyIntegrity(key = digest(bytes));
|
|
1454
|
+
if (!noCache) {
|
|
1455
|
+
const metadataBytes = cacheRecord(key, headers, responseUrl);
|
|
1456
|
+
await session.cache.set(artifactKey(key), Uint8Array.from(bytes));
|
|
1457
|
+
check();
|
|
1458
|
+
if (!canonicalWheel) await session.cache.set(address, metadataBytes);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
check();
|
|
1462
|
+
verifyIntegrity(key);
|
|
1463
|
+
const value = bytes;
|
|
1464
|
+
session.opened = { key, size: bytes.length, ...canonicalWheel ? { url } : {}, read: (offset, length) => value.subarray(offset, offset + length) };
|
|
1465
|
+
return { key, size: bytes.length, headers, url: responseUrl };
|
|
1466
|
+
} finally {
|
|
1467
|
+
session.opening = false;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
prepare: admit(prepare),
|
|
1472
|
+
dispatch: admit(dispatch),
|
|
1473
|
+
finish(start) {
|
|
1474
|
+
const session = sessions.get(start.session);
|
|
1475
|
+
if (session) {
|
|
1476
|
+
session.closed = true;
|
|
1477
|
+
session.signal.removeEventListener("abort", session.aborted);
|
|
1478
|
+
session.controller.abort(failure("Python package session is closed"));
|
|
1479
|
+
sessions.delete(start.session);
|
|
1480
|
+
const retired = release(session, true);
|
|
1481
|
+
return Promise.allSettled([session.retaining, retired]).then(async () => {
|
|
1482
|
+
try {
|
|
1483
|
+
await retired;
|
|
1484
|
+
} finally {
|
|
1485
|
+
await release(session, true);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
dispose() {
|
|
1491
|
+
if (!disposing) {
|
|
1492
|
+
disposed = true;
|
|
1493
|
+
controller.abort(failure("Python package environment is disposed"));
|
|
1494
|
+
disposing = Promise.allSettled([...pending]).then(async () => {
|
|
1495
|
+
sessions.clear();
|
|
1496
|
+
defaultCache.dispose();
|
|
1497
|
+
try {
|
|
1498
|
+
await ownedManifest?.close();
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
cleanupFailure ??= { error };
|
|
1501
|
+
}
|
|
1502
|
+
if (cleanupFailure) throw cleanupFailure.error;
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
return disposing;
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export {
|
|
1511
|
+
createPythonPackageStreamingManifestStore,
|
|
1512
|
+
PythonPackageConflictError,
|
|
1513
|
+
createPythonPackageManifestStore,
|
|
1514
|
+
createPythonPackageFileManifestStore,
|
|
1515
|
+
publishPythonBuildWheel,
|
|
1516
|
+
createPythonPackageCache,
|
|
1517
|
+
pythonDocumentPackages,
|
|
1518
|
+
createPythonPackageEnvironment
|
|
1519
|
+
};
|