@poe-platform/safe-bash 0.1.821 → 0.1.823
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/safe-bash/chunks/{chunk-HNT2MR4W.js → chunk-363QOIDR.js} +2 -1
- package/dist/safe-bash/chunks/chunk-AGHV53VF.js +42 -0
- package/dist/safe-bash/chunks/{chunk-VF3Q2V3E.js → chunk-RAFD5ZAH.js} +1 -1
- package/dist/safe-bash/chunks/{chunk-67QE3JLU.js → chunk-TZRVK3NU.js} +1 -1
- package/dist/safe-bash/chunks/{chunk-IRJS54AA.js → chunk-VWLQDZFF.js} +20 -12
- package/dist/safe-bash/chunks/{chunk-NAETSXBW.js → chunk-ZJAXMB5M.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-QX3WKBLI.js → chunk-36FV6BAG.js} +5 -3
- package/dist/safe-bash/command-chunks/{chunk-ZSHXZ3XG.js → chunk-3EAZRKI4.js} +7 -7
- package/dist/safe-bash/command-chunks/chunk-64HIOP7Q.js +993 -0
- package/dist/safe-bash/command-chunks/{chunk-WOT2YXBT.js → chunk-7ZQVMPSB.js} +213 -42
- package/dist/safe-bash/command-chunks/{chunk-GRFJKJFN.js → chunk-AVSINNZ6.js} +233 -3
- package/dist/safe-bash/command-chunks/{chunk-BZLLQWRK.js → chunk-E5CDHXFA.js} +2 -2
- package/dist/safe-bash/command-chunks/{chunk-RHXV2KG5.js → chunk-EO3RYGUZ.js} +1010 -58
- package/dist/safe-bash/command-chunks/{chunk-MCUDSE4H.js → chunk-EWECMOQ5.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-RTUDDAAY.js → chunk-O3KFOFJM.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-SLD5A7IL.js → chunk-OU5UEUZN.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-RKAL5VFP.js → chunk-PLVFJ6FF.js} +255 -616
- package/dist/safe-bash/command-chunks/{chunk-CJIFJ45R.js → chunk-QBCEAYQT.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-4Q3HP33O.js → chunk-RGKHTSUY.js} +146 -123
- package/dist/safe-bash/command-chunks/{chunk-I7MEH76L.js → chunk-UYNJF5QN.js} +1 -1
- package/dist/safe-bash/command-chunks/{chunk-IAHYG67O.js → chunk-V5ZBRM5R.js} +3 -3
- package/dist/safe-bash/command-chunks/{chunk-R4NPYXQX.js → chunk-WDTWJFPK.js} +320 -67
- package/dist/safe-bash/command-chunks/{chunk-GQV5RWMN.js → chunk-ZFXHSFTU.js} +1 -1
- package/dist/safe-bash/command-chunks/{prompt-tool-debug-ERJKK3XZ.js → prompt-tool-debug-3POFNRYP.js} +5 -5
- package/dist/safe-bash/commands/bc/index.browser.js +1 -1
- package/dist/safe-bash/commands/llm/index.browser.js +3 -1
- package/dist/safe-bash/commands/llm/index.js +1 -1
- package/dist/safe-bash/commands/python/executor.browser.js +12 -1
- package/dist/safe-bash/commands/python/executor.js +1 -1
- package/dist/safe-bash/commands/python/index.browser.js +23 -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 +30 -6
- package/dist/safe-bash/plugins/index.browser.js +3 -3
- package/dist/safe-bash/registry-entry.browser.js +2 -2
- package/dist/safe-bash/search.browser.js +1 -1
- package/dist/safe-bash/shell/runtime.d.ts +2 -2
- package/dist/safe-bash/shell/runtime.js +11 -2
- package/dist/safe-bash/shell/sync-extra-evaluators.js +12 -12
- package/dist/safe-bash/shell-entry.browser.js +2 -2
- package/dist/safe-bash-command-llm/collections.js +24 -65
- package/dist/safe-bash-command-llm/command.js +6 -6
- package/dist/safe-bash-command-llm/fragment-loaders.d.ts +3 -2
- package/dist/safe-bash-command-llm/index.d.ts +3 -2
- package/dist/safe-bash-command-llm/index.js +10 -8
- package/dist/safe-bash-command-llm/loader-provider.d.ts +25 -0
- package/dist/safe-bash-command-llm/openai-sse.js +2 -2
- package/dist/safe-bash-command-llm/openai.js +4 -4
- package/dist/safe-bash-command-llm/providers/index.js +4 -4
- package/dist/safe-bash-command-llm/providers/openai.js +4 -4
- package/dist/safe-bash-command-llm/python-unicode.d.ts +2 -0
- package/dist/safe-bash-command-llm/templates.d.ts +7 -2
- package/dist/safe-bash-command-llm/types.d.ts +7 -1
- package/dist/safe-bash-command-mcp/index.browser.js +3 -0
- package/dist/safe-bash-command-mcp/index.js +3 -0
- package/dist/safe-bash-command-patch/index.js +2 -2
- package/dist/safe-bash-command-patch/patch-publication.js +1 -1
- package/dist/safe-bash-command-patch/patch.js +2 -2
- package/dist/safe-bash-command-python/build-backend.d.ts +51 -0
- package/dist/safe-bash-command-python/build-dependencies.d.ts +12 -0
- package/dist/safe-bash-command-python/build-environment.d.ts +3 -0
- package/dist/safe-bash-command-python/build-wheel.d.ts +8 -0
- package/dist/safe-bash-command-python/executor.d.ts +12 -2
- package/dist/safe-bash-command-python/executor.js +26 -4
- package/dist/safe-bash-command-python/index.js +26 -4
- package/dist/safe-bash-command-python/jspi.js +2 -2
- package/dist/safe-bash-command-python/llm-assets-node.js +1 -1
- package/dist/safe-bash-command-python/llm-fragment-loader.d.ts +6 -0
- package/dist/safe-bash-command-python/llm-functions-loader.d.ts +5 -1
- package/dist/safe-bash-command-python/llm-loader-discovery.d.ts +13 -0
- package/dist/safe-bash-command-python/llm-package-manager.d.ts +7 -0
- package/dist/safe-bash-command-python/llm-template-loader.d.ts +6 -0
- package/dist/safe-bash-command-python/manifest.d.ts +13 -0
- package/dist/safe-bash-command-python/provisioning.d.ts +33 -5
- package/dist/safe-bash-command-python/provisioning.js +1 -1
- package/dist/safe-bash-command-python/source-environment.d.ts +9 -0
- package/dist/safe-bash-command-python/source-snapshot.d.ts +8 -0
- package/dist/safe-bash-command-python/worker.js +3 -185
- package/dist/safe-bash-csv-engine/index.js +1 -1
- package/dist/safe-bash-csv-engine/python-iso2022.d.ts +21 -0
- package/dist/safe-bash-csv-engine/python-utf7.d.ts +18 -0
- package/dist/safe-bash-csv-engine/text-decoder.d.ts +2 -0
- package/dist/safe-bash-csv-engine/text-decoder.js +7 -3
- package/dist/safe-bash-zip-engine/index.js +1 -1
- package/dist/safe-bash-zip-engine/zip/repair.js +1 -1
- package/dist/safe-bash-zip-engine/zip/sfx.js +1 -1
- package/dist/safe-bash-zip-engine/zip-format.d.ts +3 -1
- package/dist/safe-bash-zip-engine/zip-format.js +1 -1
- package/dist/safe-playwright-cloudflare/shell-browser-resource.d.ts +1 -1
- package/dist/safe-playwright-cloudflare/shell-browser-resource.js +35 -20
- package/package.json +3 -3
- package/dist/safe-bash/chunks/chunk-PSD2RSOS.js +0 -31
- package/dist/safe-bash/command-chunks/chunk-5EFNQ6FY.js +0 -458
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
// packages/safe-bash-command-python/src/invocation.ts
|
|
2
|
+
var PythonInvocationError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PythonInvocationError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
function parsePythonInvocation(args, env) {
|
|
9
|
+
const startupArgs = [];
|
|
10
|
+
let informational = false;
|
|
11
|
+
let module;
|
|
12
|
+
scan: for (let index = 0; index < args.length; index++) {
|
|
13
|
+
const option = args[index];
|
|
14
|
+
if (option === "--" || option === "-" || !option.startsWith("-")) break;
|
|
15
|
+
if (option === "--help") {
|
|
16
|
+
informational = true;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
if (option === "--version") {
|
|
20
|
+
informational = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (option.startsWith("--")) throw new PythonInvocationError("unsupported option " + option);
|
|
24
|
+
for (let offset = 1; offset < option.length; offset++) {
|
|
25
|
+
const flag = option[offset];
|
|
26
|
+
if (flag === "h" || flag === "?") {
|
|
27
|
+
informational = true;
|
|
28
|
+
break scan;
|
|
29
|
+
}
|
|
30
|
+
if (flag === "V") {
|
|
31
|
+
informational = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if ("cmWX".includes(flag)) {
|
|
35
|
+
const operand = option.slice(offset + 1) || args[++index];
|
|
36
|
+
if (operand === void 0) throw new PythonInvocationError("argument expected for -" + flag);
|
|
37
|
+
if (flag === "m" && !informational) module = { name: operand, args: args.slice(index + 1) };
|
|
38
|
+
if (flag === "c" || flag === "m") break scan;
|
|
39
|
+
if (flag === "X") {
|
|
40
|
+
const separator = operand.indexOf("=");
|
|
41
|
+
const name = separator < 0 ? operand : operand.slice(0, separator);
|
|
42
|
+
const value = separator < 0 ? void 0 : operand.slice(separator + 1);
|
|
43
|
+
let decimal = value ?? "";
|
|
44
|
+
while (decimal.length && " \n\r\v\f".includes(decimal[0])) decimal = decimal.slice(1);
|
|
45
|
+
if (decimal[0] === "+" || decimal[0] === "-") decimal = decimal.slice(1);
|
|
46
|
+
const digits = decimal.length > 0 && [...decimal].every((character) => character >= "0" && character <= "9");
|
|
47
|
+
const supported = name === "utf8" && (value === void 0 || value === "0" || value === "1") || (name === "dev" || name === "warn_default_encoding") && value === void 0 || name === "int_max_str_digits" && digits && Number.isSafeInteger(Number(value)) && Number(value) <= 2147483647 && (Number(value) === 0 || Number(value) >= 640);
|
|
48
|
+
if (!supported) throw new PythonInvocationError("unsupported -X option " + operand);
|
|
49
|
+
startupArgs.push("-X", operand);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (!"BEPOusISbdvqR".includes(flag)) throw new PythonInvocationError("unsupported option -" + flag);
|
|
54
|
+
startupArgs.push("-" + flag);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!informational && !startupArgs.includes("-E") && !startupArgs.includes("-I")) {
|
|
58
|
+
if (env.PYTHONINSPECT) throw new PythonInvocationError("PYTHONINSPECT requires an interactive terminal, which the shell does not expose");
|
|
59
|
+
if (env.PYTHONHOME) throw new PythonInvocationError("PYTHONHOME cannot replace the configured Pyodide runtime");
|
|
60
|
+
}
|
|
61
|
+
const runtimeEnv = { ...env };
|
|
62
|
+
delete runtimeEnv.PYTHONPATH;
|
|
63
|
+
delete runtimeEnv.PYTHONHOME;
|
|
64
|
+
delete runtimeEnv.PYTHONWARNINGS;
|
|
65
|
+
runtimeEnv.PYTHONINSPECT = "";
|
|
66
|
+
startupArgs.push("-c", "");
|
|
67
|
+
return { startupArgs, env: runtimeEnv, module };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// packages/safe-bash-command-python/src/native-wheel.ts
|
|
71
|
+
var pythonNativeWheel = `
|
|
72
|
+
def _safe_extract_native_wheel(read, serialized):
|
|
73
|
+
import io as _native_io, json as _native_json, shutil as _native_shutil
|
|
74
|
+
from pathlib import Path as _NativePath
|
|
75
|
+
from zipfile import ZipFile as _NativeZip
|
|
76
|
+
from pyodide import _package_loader as _native_loader
|
|
77
|
+
from pyodide.ffi import run_sync as _native_sync
|
|
78
|
+
|
|
79
|
+
class _NativeWheel(_native_io.RawIOBase):
|
|
80
|
+
def __init__(self, size):
|
|
81
|
+
self.size, self.position = size, 0
|
|
82
|
+
def readable(self):
|
|
83
|
+
return True
|
|
84
|
+
def seekable(self):
|
|
85
|
+
return True
|
|
86
|
+
def tell(self):
|
|
87
|
+
return self.position
|
|
88
|
+
def seek(self, offset, whence=0):
|
|
89
|
+
if whence not in (0, 1, 2):
|
|
90
|
+
raise ValueError('invalid whence')
|
|
91
|
+
position = offset + (0, self.position, self.size)[whence]
|
|
92
|
+
if position < 0:
|
|
93
|
+
raise OSError(22, 'Invalid argument')
|
|
94
|
+
self.position = position
|
|
95
|
+
return position
|
|
96
|
+
def read(self, size=-1):
|
|
97
|
+
remaining = max(0, self.size - self.position)
|
|
98
|
+
size = remaining if size is None or size < 0 else min(size, remaining)
|
|
99
|
+
output = bytearray()
|
|
100
|
+
while len(output) < size:
|
|
101
|
+
result = read(self.position, min(size-len(output), 65536))
|
|
102
|
+
if hasattr(result, 'then'):
|
|
103
|
+
result = _native_sync(result)
|
|
104
|
+
chunk = bytes(result)
|
|
105
|
+
if not chunk or len(chunk) > size-len(output):
|
|
106
|
+
raise OSError('Invalid Python package chunk')
|
|
107
|
+
output.extend(chunk)
|
|
108
|
+
self.position += len(chunk)
|
|
109
|
+
return bytes(output)
|
|
110
|
+
|
|
111
|
+
_native_config = _native_json.loads(serialized)
|
|
112
|
+
with _NativeWheel(_native_config['size']) as _native_archive:
|
|
113
|
+
if 'metadata_name' in _native_config:
|
|
114
|
+
from micropip.metadata import wheel_dist_info_dir as _native_metadata_dir
|
|
115
|
+
from zipfile import Path as _NativeZipPath
|
|
116
|
+
with _NativeZip(_native_archive) as _native_zip:
|
|
117
|
+
path = _NativePath(_native_metadata_dir(_native_zip, _native_config['metadata_name'])) / 'METADATA'
|
|
118
|
+
return _native_json.dumps(_NativeZipPath(_native_zip, str(path)).read_text(encoding='utf-8'))
|
|
119
|
+
_native_target = _NativePath(_native_config['extract_dir'] if 'extract_dir' in _native_config else _native_loader.get_install_dir(_native_config['target']))
|
|
120
|
+
_native_target.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
_native_shutil._unpack_zipfile(_native_archive, _native_target)
|
|
122
|
+
with _NativeZip(_native_archive) as _native_zip:
|
|
123
|
+
if _NativePath(_native_config['filename']).suffix == '.whl':
|
|
124
|
+
_native_loader.set_wheel_metadata(_native_config['filename'], _native_zip, _native_target, _native_config['metadata'])
|
|
125
|
+
_native_loader.install_datafiles(_native_config['filename'], _native_zip, _native_target)
|
|
126
|
+
_native_dynlibs = _native_loader.get_dynlibs(_native_archive, _NativePath(_native_config['filename']).suffix, _native_target)
|
|
127
|
+
return _native_json.dumps(_native_dynlibs)
|
|
128
|
+
_safe_extract_native_wheel(_safe_native_wheel_read, _safe_native_wheel_config)
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// packages/safe-bash-command-python/src/provisioning-runtime.ts
|
|
132
|
+
async function installPythonPackages(supplied, start, request, maxTransferBytes) {
|
|
133
|
+
if (!start.requirements.length && !start.uninstall && !start.bootstrap) return;
|
|
134
|
+
const runtime = supplied;
|
|
135
|
+
if (runtime.version !== "314.0.6" || !runtime._api?.packageManager || !runtime._api.lockfile_packages || typeof runtime._api.packageManager.installPackage !== "function" || typeof runtime.loadPackage !== "function" || typeof runtime.runPythonAsync !== "function") throw new Error("Python package installer ABI requires Pyodide 314.0.6");
|
|
136
|
+
let transportFailure;
|
|
137
|
+
let accepting = true;
|
|
138
|
+
let pending = Promise.resolve();
|
|
139
|
+
let installing = Promise.resolve();
|
|
140
|
+
const manager = runtime._api.packageManager;
|
|
141
|
+
const transfer = (consume) => {
|
|
142
|
+
if (!accepting) return Promise.reject(new Error("Python package transport is only available during installation"));
|
|
143
|
+
const work = pending.then(async () => {
|
|
144
|
+
if (transportFailure) throw transportFailure.error;
|
|
145
|
+
try {
|
|
146
|
+
return await consume();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
transportFailure ??= { error };
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
pending = work.then(() => {
|
|
153
|
+
}, () => {
|
|
154
|
+
});
|
|
155
|
+
return work;
|
|
156
|
+
};
|
|
157
|
+
const readArtifact = (operation, key, offset, length) => {
|
|
158
|
+
const failed = (error) => {
|
|
159
|
+
transportFailure ??= { error };
|
|
160
|
+
throw error;
|
|
161
|
+
};
|
|
162
|
+
try {
|
|
163
|
+
const result = request(operation, start.session, key, offset, Math.min(length, maxTransferBytes, 65536));
|
|
164
|
+
return result instanceof Promise ? result.catch(failed) : result;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
return failed(error);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const withArtifact = (url, expected, metadata, consume) => transfer(async () => {
|
|
170
|
+
const opened = await request("package-open", start.session, url, expected, ...metadata ? [metadata] : []);
|
|
171
|
+
try {
|
|
172
|
+
return await consume(opened, (offset, length) => readArtifact("package-read", opened.key, offset, length));
|
|
173
|
+
} finally {
|
|
174
|
+
await request("package-close", start.session, opened.key);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const wheel = async (size, read, configuration) => {
|
|
178
|
+
const globals = ["_safe_native_wheel_read", "_safe_native_wheel_config", "_safe_extract_native_wheel"];
|
|
179
|
+
try {
|
|
180
|
+
runtime.globals.set(globals[0], read);
|
|
181
|
+
runtime.globals.set(globals[1], JSON.stringify({ ...configuration, size }));
|
|
182
|
+
const result = JSON.parse(await runtime.runPythonAsync(pythonNativeWheel));
|
|
183
|
+
if (Array.isArray(result)) for (const path of result) await runtime._api.loadDynlib(path);
|
|
184
|
+
return result;
|
|
185
|
+
} finally {
|
|
186
|
+
for (const name of globals) runtime.globals.delete(name);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const fetch = (url, expected, metadata) => withArtifact(url, expected, metadata, async (opened, read) => {
|
|
190
|
+
const bytes = new Uint8Array(opened.size);
|
|
191
|
+
for (let offset = 0; offset < bytes.length; ) {
|
|
192
|
+
const chunk = await read(offset, bytes.length - offset);
|
|
193
|
+
if (!Array.isArray(chunk) || chunk.length === 0 || offset + chunk.length > bytes.length) throw new Error("Invalid Python package chunk");
|
|
194
|
+
bytes.set(chunk, offset);
|
|
195
|
+
offset += chunk.length;
|
|
196
|
+
}
|
|
197
|
+
return { bytes, headers: opened.headers };
|
|
198
|
+
});
|
|
199
|
+
manager.downloadPackage = async (metadata) => {
|
|
200
|
+
if (!accepting) throw new Error("Python package transport is only available during installation");
|
|
201
|
+
if (metadata.channel === manager.defaultChannel) {
|
|
202
|
+
const pkg = runtime._api.lockfile_packages[metadata.normalizedName];
|
|
203
|
+
if (!pkg) throw new Error(`Missing matching Pyodide package: ${metadata.normalizedName}`);
|
|
204
|
+
return { url: new URL(pkg.file_name, "https://cdn.jsdelivr.net/pyodide/v314.0.6/full/").href, expected: pkg.sha256 };
|
|
205
|
+
}
|
|
206
|
+
return { url: metadata.channel };
|
|
207
|
+
};
|
|
208
|
+
manager.installPackage = (metadata, source) => {
|
|
209
|
+
if (!accepting) return Promise.reject(new Error("Python package transport is only available during installation"));
|
|
210
|
+
const work = installing.then(async () => {
|
|
211
|
+
if (source instanceof Uint8Array) throw new Error("Expected retained Python package source");
|
|
212
|
+
const pkg = runtime._api.lockfile_packages[metadata.normalizedName] ?? metadata.packageData;
|
|
213
|
+
if (!pkg) throw new Error(`Missing matching Pyodide package: ${metadata.normalizedName}`);
|
|
214
|
+
return withArtifact(source.url, source.expected, void 0, (opened, read) => wheel(opened.size, read, {
|
|
215
|
+
filename: pkg.file_name,
|
|
216
|
+
target: pkg.install_dir ?? null,
|
|
217
|
+
metadata: { INSTALLER: "pyodide.loadPackage", PYODIDE_SOURCE: metadata.channel === manager.defaultChannel ? "pyodide" : metadata.channel }
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
installing = work.then(() => {
|
|
221
|
+
}, () => {
|
|
222
|
+
});
|
|
223
|
+
return work;
|
|
224
|
+
};
|
|
225
|
+
const loadPackages = async (names) => {
|
|
226
|
+
const errors = [];
|
|
227
|
+
await runtime.loadPackage(names, { messageCallback() {
|
|
228
|
+
}, errorCallback(message) {
|
|
229
|
+
errors.push(message);
|
|
230
|
+
} });
|
|
231
|
+
if (errors.length) throw new Error(errors.join("\n"));
|
|
232
|
+
};
|
|
233
|
+
const installedGlobals = [];
|
|
234
|
+
const bind = (name, value) => {
|
|
235
|
+
runtime.globals.set(name, value);
|
|
236
|
+
installedGlobals.push(name);
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
await loadPackages(["micropip"]);
|
|
240
|
+
bind("_safe_package_native", loadPackages);
|
|
241
|
+
bind("_safe_package_bytes", async (url, hash) => (await fetch(url, hash)).bytes);
|
|
242
|
+
bind("_safe_package_metadata", async (url) => {
|
|
243
|
+
const result = await fetch(url, void 0, "metadata");
|
|
244
|
+
return JSON.stringify({ text: new TextDecoder().decode(result.bytes), headers: Object.fromEntries(result.headers.map(([name, value]) => [name.toLowerCase(), value])) });
|
|
245
|
+
});
|
|
246
|
+
bind("_safe_package_wheel_download", async (url, expected) => withArtifact(url, expected, void 0, async (opened) => {
|
|
247
|
+
return JSON.stringify(await request("package-retain", start.session, opened.key));
|
|
248
|
+
}));
|
|
249
|
+
bind("_safe_package_wheel_metadata", async (serialized) => transfer(async () => {
|
|
250
|
+
const { source, name } = JSON.parse(serialized);
|
|
251
|
+
return wheel(source.size, (offset, length) => readArtifact("package-read-retained", source.token, offset, length), { metadata_name: name });
|
|
252
|
+
}));
|
|
253
|
+
bind("_safe_package_wheel_install", async (serialized) => transfer(async () => {
|
|
254
|
+
const { source, ...configuration } = JSON.parse(serialized);
|
|
255
|
+
await wheel(source.size, (offset, length) => readArtifact("package-read-retained", source.token, offset, length), configuration);
|
|
256
|
+
}));
|
|
257
|
+
bind("_safe_package_requirements_json", JSON.stringify(start.requested ?? start.requirements));
|
|
258
|
+
for (const key of ["pre", "upgrade", "forceReinstall", "legacy"]) {
|
|
259
|
+
bind("_safe_package_" + key, !!start[key]);
|
|
260
|
+
}
|
|
261
|
+
bind("_safe_package_restore_json", JSON.stringify(start.restore ?? []));
|
|
262
|
+
bind("_safe_package_records_json", JSON.stringify(start.records ?? null));
|
|
263
|
+
bind("_safe_package_uninstall_json", JSON.stringify(start.uninstall ?? null));
|
|
264
|
+
bind("_safe_package_emit", async (stream, message) => {
|
|
265
|
+
if (stream !== "stdout" && stream !== "stderr") throw new Error("Invalid package output stream");
|
|
266
|
+
const bytes = new TextEncoder().encode(message);
|
|
267
|
+
for (let offset = 0; offset < bytes.length; offset += maxTransferBytes) await request(stream, Array.from(bytes.subarray(offset, offset + maxTransferBytes)));
|
|
268
|
+
});
|
|
269
|
+
bind("_safe_package_line", async () => {
|
|
270
|
+
const bytes = [];
|
|
271
|
+
while (true) {
|
|
272
|
+
const chunk = await request("stdin", 1);
|
|
273
|
+
if (!chunk.length) {
|
|
274
|
+
if (!bytes.length) throw new Error("EOF when reading package confirmation");
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
if (chunk[0] === 10) break;
|
|
278
|
+
bytes.push(chunk[0]);
|
|
279
|
+
}
|
|
280
|
+
return new TextDecoder().decode(Uint8Array.from(bytes));
|
|
281
|
+
});
|
|
282
|
+
await runtime.runPythonAsync(`
|
|
283
|
+
import json as _safe_json
|
|
284
|
+
import importlib.metadata as _safe_metadata
|
|
285
|
+
_safe_preloaded = [d.metadata['Name'] for d in _safe_metadata.distributions() if d.metadata['Name']]
|
|
286
|
+
from micropip._compat import compatibility_layer as _safe_compat
|
|
287
|
+
from micropip.package_manager import PackageManager as _SafePackageManager
|
|
288
|
+
from micropip.wheelinfo import WheelInfo as _SafeWheelInfo
|
|
289
|
+
from micropip._utils import check_compatible as _safe_check_compatible
|
|
290
|
+
from micropip._vendored.packaging.src.packaging.requirements import Requirement as _SafeRequirement, InvalidRequirement as _SafeInvalidRequirement
|
|
291
|
+
from micropip._vendored.packaging.src.packaging.utils import canonicalize_name as _safe_name
|
|
292
|
+
|
|
293
|
+
class _SafePackageCompatibility(_safe_compat):
|
|
294
|
+
@staticmethod
|
|
295
|
+
async def loadPackage(names):
|
|
296
|
+
return await _safe_package_native(names)
|
|
297
|
+
@staticmethod
|
|
298
|
+
async def fetch_bytes(url, kwargs):
|
|
299
|
+
return (await _safe_package_bytes(url)).to_bytes()
|
|
300
|
+
@staticmethod
|
|
301
|
+
async def fetch_string_and_headers(url, kwargs):
|
|
302
|
+
value = _safe_json.loads(await _safe_package_metadata(url))
|
|
303
|
+
return value['text'], value['headers']
|
|
304
|
+
|
|
305
|
+
async def _safe_wheel_fetch(self, url, kwargs, compat):
|
|
306
|
+
expected = self.sha256 if url == self.url else None
|
|
307
|
+
if url == self.metadata_url and isinstance(self.core_metadata, dict):
|
|
308
|
+
expected = self.core_metadata.get('sha256')
|
|
309
|
+
return (await _safe_package_bytes(url, expected)).to_bytes()
|
|
310
|
+
_SafeWheelInfo._fetch_bytes = _safe_wheel_fetch
|
|
311
|
+
from micropip.metadata import Metadata as _SafeWheelMetadata
|
|
312
|
+
async def _safe_wheel_download(self, fetch_kwargs, compat_layer):
|
|
313
|
+
if self._data is not None:
|
|
314
|
+
return
|
|
315
|
+
self._data = _safe_json.loads(await _safe_package_wheel_download(self.url, self.sha256))
|
|
316
|
+
if self._metadata is None:
|
|
317
|
+
metadata = await _safe_package_wheel_metadata(_safe_json.dumps({'source': self._data, 'name': self.name}))
|
|
318
|
+
self._metadata = _SafeWheelMetadata(metadata.encode('utf-8'))
|
|
319
|
+
async def _safe_wheel_install(self, target, compat_layer):
|
|
320
|
+
if not self._data:
|
|
321
|
+
raise RuntimeError('Micropip internal error: attempted to install wheel before downloading it?')
|
|
322
|
+
source = 'pypi' if self.sha256 is not None else self.url
|
|
323
|
+
metadata = {'PYODIDE_SOURCE': source, 'PYODIDE_URL': self.url, 'PYODIDE_SHA256': self._data['key'], 'INSTALLER': 'micropip'}
|
|
324
|
+
if self._requires:
|
|
325
|
+
metadata['PYODIDE_REQUIRES'] = _safe_json.dumps(sorted(x.name for x in self._requires))
|
|
326
|
+
await _safe_package_wheel_install(_safe_json.dumps({'source': self._data, 'filename': self.filename, 'extract_dir': str(target), 'metadata': metadata}))
|
|
327
|
+
setattr(compat_layer.loadedPackages, self._project_name, source)
|
|
328
|
+
_SafeWheelInfo.download = _safe_wheel_download
|
|
329
|
+
_SafeWheelInfo.install = _safe_wheel_install
|
|
330
|
+
_safe_preloaded = {_safe_name(name) for name in _safe_preloaded}
|
|
331
|
+
_safe_manager = _SafePackageManager(_SafePackageCompatibility)
|
|
332
|
+
async def _safe_parse_sources(sources, download=True):
|
|
333
|
+
roots = []
|
|
334
|
+
for source in sources:
|
|
335
|
+
try:
|
|
336
|
+
root = _SafeRequirement(source)
|
|
337
|
+
if root.name.endswith('.whl'):
|
|
338
|
+
raise _SafeInvalidRequirement(source)
|
|
339
|
+
except _SafeInvalidRequirement:
|
|
340
|
+
wheel = _SafeWheelInfo.from_url(source)
|
|
341
|
+
root = _SafeRequirement(wheel.name + ' @ ' + source)
|
|
342
|
+
roots.append(root)
|
|
343
|
+
if (not root.marker or root.marker.evaluate({'extra': ''})) and root.url:
|
|
344
|
+
direct = _SafeWheelInfo.from_url(root.url)
|
|
345
|
+
_safe_check_compatible(direct.filename)
|
|
346
|
+
if download:
|
|
347
|
+
await direct.download({}, _SafePackageCompatibility)
|
|
348
|
+
return roots
|
|
349
|
+
|
|
350
|
+
def _safe_validate(roots):
|
|
351
|
+
for root in roots:
|
|
352
|
+
if root.marker and not root.marker.evaluate({'extra': ''}):
|
|
353
|
+
continue
|
|
354
|
+
version = _safe_metadata.version(root.name)
|
|
355
|
+
if not root.specifier.contains(version, prereleases=True):
|
|
356
|
+
raise ValueError('Python package version conflict: ' + str(root))
|
|
357
|
+
if root.url:
|
|
358
|
+
wheel = _SafeWheelInfo.from_url(root.url)
|
|
359
|
+
pin = _SafeRequirement(wheel.name + '==' + str(wheel.version))
|
|
360
|
+
if _safe_name(wheel.name) != _safe_name(root.name) or not pin.specifier.contains(version, prereleases=True):
|
|
361
|
+
raise ValueError('Python package wheel version conflict: ' + str(root))
|
|
362
|
+
async def _safe_resolve(_safe_roots, upgrade=False, force=False):
|
|
363
|
+
_safe_extras = {}
|
|
364
|
+
for _safe_root in _safe_roots:
|
|
365
|
+
if not _safe_root.marker or _safe_root.marker.evaluate({'extra': ''}):
|
|
366
|
+
_safe_extras.setdefault(_safe_name(_safe_root.name), set()).update(_safe_root.extras)
|
|
367
|
+
for _safe_root in _safe_roots:
|
|
368
|
+
_safe_root.extras.update(_safe_extras.get(_safe_name(_safe_root.name), set()))
|
|
369
|
+
_safe_requested_names = set(_safe_extras)
|
|
370
|
+
import micropip.package_manager as _safe_pm
|
|
371
|
+
_SafeTransaction = _safe_pm.Transaction
|
|
372
|
+
class _SafeReplacementTransaction(_SafeTransaction):
|
|
373
|
+
def check_version_satisfied(self, req, *, allow_reinstall=False):
|
|
374
|
+
if req.url:
|
|
375
|
+
wheel = _SafeWheelInfo.from_url(req.url)
|
|
376
|
+
if _safe_name(wheel.name) != req.name:
|
|
377
|
+
raise ValueError('Python package wheel name conflict: ' + str(req))
|
|
378
|
+
req = _SafeRequirement(req.name + '==' + str(wheel.version))
|
|
379
|
+
if req.name in _safe_preloaded or req.name in self.locked:
|
|
380
|
+
return super().check_version_satisfied(req)
|
|
381
|
+
if force or (upgrade and req.name in _safe_requested_names):
|
|
382
|
+
return False, ''
|
|
383
|
+
return super().check_version_satisfied(req, allow_reinstall=allow_reinstall)
|
|
384
|
+
async def _safe_install(requirements):
|
|
385
|
+
_safe_pm.Transaction = _SafeReplacementTransaction
|
|
386
|
+
try:
|
|
387
|
+
await _safe_manager.install(requirements, deps=True, pre=_safe_package_pre, reinstall=True)
|
|
388
|
+
finally:
|
|
389
|
+
_safe_pm.Transaction = _SafeTransaction
|
|
390
|
+
_safe_validate([root for root in _safe_roots if _safe_name(root.name) in _safe_preloaded])
|
|
391
|
+
await _safe_install([str(root) for root in _safe_roots])
|
|
392
|
+
_safe_managed = set(_safe_requested_names)
|
|
393
|
+
_safe_previous_pending = set()
|
|
394
|
+
while True:
|
|
395
|
+
_safe_distributions = [d for d in _safe_metadata.distributions() if d.metadata['Name']]
|
|
396
|
+
_safe_versions = {_safe_name(d.metadata['Name']): d.version for d in _safe_distributions}
|
|
397
|
+
while True:
|
|
398
|
+
_safe_changed = False
|
|
399
|
+
_safe_dependencies = []
|
|
400
|
+
for _safe_dist in _safe_distributions:
|
|
401
|
+
if _safe_name(_safe_dist.metadata['Name']) not in _safe_managed:
|
|
402
|
+
continue
|
|
403
|
+
_safe_contexts = {''} | _safe_extras.get(_safe_name(_safe_dist.metadata['Name']), set())
|
|
404
|
+
for _safe_dep in _safe_dist.requires or []:
|
|
405
|
+
_safe_requirement = _SafeRequirement(_safe_dep)
|
|
406
|
+
if _safe_requirement.marker and not any(_safe_requirement.marker.evaluate({'extra': extra}) for extra in _safe_contexts):
|
|
407
|
+
continue
|
|
408
|
+
_safe_requirement.marker = None
|
|
409
|
+
_safe_dependencies.append(_safe_requirement)
|
|
410
|
+
_safe_dependency_name = _safe_name(_safe_requirement.name)
|
|
411
|
+
if _safe_dependency_name not in _safe_managed:
|
|
412
|
+
_safe_managed.add(_safe_dependency_name)
|
|
413
|
+
_safe_changed = True
|
|
414
|
+
_safe_selected = _safe_extras.setdefault(_safe_name(_safe_requirement.name), set())
|
|
415
|
+
if not _safe_requirement.extras.issubset(_safe_selected):
|
|
416
|
+
_safe_selected.update(_safe_requirement.extras)
|
|
417
|
+
_safe_changed = True
|
|
418
|
+
if not _safe_changed:
|
|
419
|
+
break
|
|
420
|
+
_safe_pending = set()
|
|
421
|
+
for _safe_requirement in _safe_dependencies:
|
|
422
|
+
_safe_version = _safe_versions.get(_safe_name(_safe_requirement.name))
|
|
423
|
+
if _safe_version is None or not _safe_requirement.specifier.contains(_safe_version, prereleases=True):
|
|
424
|
+
_safe_pending.add(str(_safe_requirement))
|
|
425
|
+
if not _safe_pending:
|
|
426
|
+
break
|
|
427
|
+
if frozenset(_safe_pending) in _safe_previous_pending:
|
|
428
|
+
raise ValueError('Python package dependencies remain missing: ' + ', '.join(sorted(_safe_pending)))
|
|
429
|
+
_safe_previous_pending.add(frozenset(_safe_pending))
|
|
430
|
+
await _safe_install(sorted(_safe_pending))
|
|
431
|
+
_safe_validate(_safe_roots)
|
|
432
|
+
return _safe_managed
|
|
433
|
+
def _safe_removal_listing(_safe_dist):
|
|
434
|
+
from micropip._utils import get_files_in_distribution as _safe_distribution_files
|
|
435
|
+
import os as _safe_os
|
|
436
|
+
def _safe_compact(paths):
|
|
437
|
+
compact = []
|
|
438
|
+
for path in sorted(paths, key=len):
|
|
439
|
+
if not any(path.startswith(parent.rstrip('*').rstrip('/') + '/') for parent in compact):
|
|
440
|
+
compact.append(path)
|
|
441
|
+
return compact
|
|
442
|
+
_safe_files = {str(path) for path in _safe_distribution_files(_safe_dist) if not str(path).endswith('.pyc')}
|
|
443
|
+
_safe_folders = _safe_compact({_safe_os.path.dirname(path) for path in _safe_files if path.endswith('__init__.py') or '.dist-info' in path})
|
|
444
|
+
_safe_skipped = set()
|
|
445
|
+
for _safe_folder in _safe_folders:
|
|
446
|
+
for _safe_dir, _, _safe_names in _safe_os.walk(_safe_folder):
|
|
447
|
+
for _safe_name_ in _safe_names:
|
|
448
|
+
_safe_path = _safe_os.path.join(_safe_dir, _safe_name_)
|
|
449
|
+
if not _safe_name_.endswith('.pyc') and _safe_os.path.isfile(_safe_path) and _safe_path not in _safe_files:
|
|
450
|
+
_safe_skipped.add(_safe_path)
|
|
451
|
+
_safe_listing = set(_safe_files) | {_safe_os.path.join(folder, '*') for folder in _safe_folders}
|
|
452
|
+
return [sorted(_safe_compact(_safe_listing)), sorted(_safe_compact(_safe_skipped))]
|
|
453
|
+
_safe_uninstall = _safe_json.loads(_safe_package_uninstall_json)
|
|
454
|
+
_safe_records = _safe_json.loads(_safe_package_records_json)
|
|
455
|
+
_safe_metadata_only = _safe_uninstall is not None and _safe_records is not None
|
|
456
|
+
_safe_record_by_name = {}
|
|
457
|
+
_safe_snapshot_paths = {}
|
|
458
|
+
if _safe_records is not None:
|
|
459
|
+
for _safe_record in _safe_records:
|
|
460
|
+
_safe_record_name = _safe_record[0]
|
|
461
|
+
if _SafeRequirement(_safe_record_name).name != _safe_record_name or _safe_name(_safe_record_name) != _safe_record_name or _safe_record_name in _safe_record_by_name:
|
|
462
|
+
raise ValueError('Invalid Python package metadata snapshot')
|
|
463
|
+
_safe_record_by_name[_safe_record_name] = _safe_record
|
|
464
|
+
if _safe_metadata_only:
|
|
465
|
+
from pathlib import Path as _SafePath
|
|
466
|
+
import sysconfig as _safe_sysconfig
|
|
467
|
+
for _safe_record_name, _safe_record in _safe_record_by_name.items():
|
|
468
|
+
if _safe_record_name in _safe_preloaded:
|
|
469
|
+
continue
|
|
470
|
+
_safe_path = (_SafePath(_safe_sysconfig.get_path('purelib')) / (_safe_record_name.replace('-', '_') + '-snapshot.dist-info')).resolve()
|
|
471
|
+
_safe_path.mkdir()
|
|
472
|
+
(_safe_path / 'METADATA').write_text(_safe_record[1])
|
|
473
|
+
(_safe_path / 'PYODIDE_URL').write_text(_safe_record[2])
|
|
474
|
+
(_safe_path / 'RECORD').write_text('')
|
|
475
|
+
_safe_dist = _safe_metadata.Distribution.at(_safe_path)
|
|
476
|
+
if _safe_name(_safe_dist.metadata['Name']) != _safe_record_name:
|
|
477
|
+
raise ValueError('Python package metadata name conflict: ' + _safe_record_name)
|
|
478
|
+
_SafeRequirement(_safe_record_name + '==' + _safe_dist.version)
|
|
479
|
+
_safe_snapshot_paths[_safe_record_name] = str(_safe_path)
|
|
480
|
+
_safe_metadata.MetadataPathFinder.invalidate_caches()
|
|
481
|
+
_safe_restore = _safe_json.loads(_safe_package_restore_json)
|
|
482
|
+
_safe_restored_roots = await _safe_parse_sources(_safe_restore, not _safe_metadata_only)
|
|
483
|
+
if _safe_package_legacy:
|
|
484
|
+
_safe_restored_names = await _safe_resolve(_safe_restored_roots)
|
|
485
|
+
else:
|
|
486
|
+
if not _safe_metadata_only:
|
|
487
|
+
await _safe_manager.install(_safe_restore, deps=False)
|
|
488
|
+
_safe_validate(_safe_restored_roots)
|
|
489
|
+
_safe_restored_names = {_safe_name(root.name) for root in _safe_restored_roots if not root.marker or root.marker.evaluate({'extra': ''})}
|
|
490
|
+
if _safe_metadata_only and set(_safe_record_by_name) != _safe_restored_names:
|
|
491
|
+
raise ValueError('Python package metadata snapshot does not match installed requirements')
|
|
492
|
+
_safe_roots = await _safe_parse_sources(_safe_json.loads(_safe_package_requirements_json), not _safe_metadata_only)
|
|
493
|
+
if _safe_metadata_only:
|
|
494
|
+
for _safe_root in _safe_roots:
|
|
495
|
+
if _safe_root.url and _safe_name(_safe_root.name) in _safe_snapshot_paths:
|
|
496
|
+
_safe_wheel = _SafeWheelInfo.from_url(_safe_root.url)
|
|
497
|
+
if _safe_name(_safe_wheel.name) == _safe_name(_safe_root.name) and str(_safe_wheel.version) == _safe_metadata.version(_safe_root.name):
|
|
498
|
+
_safe_root.url = None
|
|
499
|
+
_safe_root.specifier = _SafeRequirement(_safe_root.name + '==' + str(_safe_wheel.version)).specifier
|
|
500
|
+
_safe_managed = await _safe_resolve(_safe_roots, _safe_package_upgrade, _safe_package_forceReinstall)
|
|
501
|
+
_safe_distributions = [d for d in _safe_metadata.distributions() if d.metadata['Name']]
|
|
502
|
+
_safe_versions = {_safe_name(d.metadata['Name']): d.version for d in _safe_distributions}
|
|
503
|
+
_safe_removed = []
|
|
504
|
+
if _safe_uninstall:
|
|
505
|
+
_safe_targets = list(dict.fromkeys(_safe_name(_SafeRequirement(source).name) for source in _safe_uninstall['packages']))
|
|
506
|
+
for _safe_target in _safe_targets:
|
|
507
|
+
if _safe_target in _safe_preloaded or _safe_target in _safe_managed:
|
|
508
|
+
raise ValueError('Cannot uninstall host-required Python package: ' + _safe_target)
|
|
509
|
+
for _safe_target in _safe_targets:
|
|
510
|
+
if _safe_target not in _safe_versions:
|
|
511
|
+
await _safe_package_emit('stderr', 'WARNING: Skipping ' + _safe_target + ' as it is not installed.\\n')
|
|
512
|
+
continue
|
|
513
|
+
_safe_dist = _safe_metadata.distribution(_safe_target)
|
|
514
|
+
_safe_version = _safe_dist.version
|
|
515
|
+
await _safe_package_emit('stdout', 'Found existing installation: ' + _safe_target + ' ' + _safe_version + '\\nUninstalling ' + _safe_target + '-' + _safe_version + ':\\n')
|
|
516
|
+
if not _safe_uninstall['yes']:
|
|
517
|
+
_safe_record = _safe_record_by_name.get(_safe_target)
|
|
518
|
+
_safe_lists = _safe_record[3:] if _safe_snapshot_paths.get(_safe_target) == str(_safe_dist._path) else _safe_removal_listing(_safe_dist)
|
|
519
|
+
for _safe_heading, _safe_paths in zip(['Would remove:', 'Would not remove (might be manually added):'], _safe_lists):
|
|
520
|
+
if _safe_paths:
|
|
521
|
+
await _safe_package_emit('stdout', ' ' + _safe_heading + '\\n')
|
|
522
|
+
for _safe_path in _safe_paths:
|
|
523
|
+
await _safe_package_emit('stdout', ' ' + _safe_path + '\\n')
|
|
524
|
+
while True:
|
|
525
|
+
await _safe_package_emit('stdout', 'Proceed (Y/n)? ')
|
|
526
|
+
_safe_answer = (await _safe_package_line()).strip().lower()
|
|
527
|
+
if _safe_answer in ('y', 'n', ''):
|
|
528
|
+
break
|
|
529
|
+
await _safe_package_emit('stdout', 'Your response (' + repr(_safe_answer) + ') was not one of the expected responses: y, n, \\n')
|
|
530
|
+
if _safe_answer == 'n':
|
|
531
|
+
continue
|
|
532
|
+
import logging as _safe_logging
|
|
533
|
+
_safe_logger = _safe_logging.getLogger('micropip')
|
|
534
|
+
_safe_disabled = _safe_logger.disabled
|
|
535
|
+
try:
|
|
536
|
+
_safe_logger.disabled = True
|
|
537
|
+
_safe_manager.uninstall([_safe_target])
|
|
538
|
+
finally:
|
|
539
|
+
_safe_logger.disabled = _safe_disabled
|
|
540
|
+
_safe_metadata.MetadataPathFinder.invalidate_caches()
|
|
541
|
+
try:
|
|
542
|
+
_safe_metadata.distribution(_safe_target)
|
|
543
|
+
except _safe_metadata.PackageNotFoundError:
|
|
544
|
+
pass
|
|
545
|
+
else:
|
|
546
|
+
raise ValueError('Python package removal did not complete: ' + _safe_target)
|
|
547
|
+
_safe_restored_names.discard(_safe_target)
|
|
548
|
+
_safe_removed.append(_safe_target + '-' + _safe_version)
|
|
549
|
+
_safe_distributions = [d for d in _safe_metadata.distributions() if d.metadata['Name']]
|
|
550
|
+
_safe_versions = {_safe_name(d.metadata['Name']): d.version for d in _safe_distributions}
|
|
551
|
+
_safe_uninstalled_json = _safe_json.dumps(_safe_removed)
|
|
552
|
+
_safe_managed.update(_safe_restored_names)
|
|
553
|
+
_safe_sources = []
|
|
554
|
+
_safe_final_records = []
|
|
555
|
+
for _safe_dist in _safe_distributions:
|
|
556
|
+
_safe_dist_name = _safe_name(_safe_dist.metadata['Name'])
|
|
557
|
+
if _safe_dist_name in _safe_managed:
|
|
558
|
+
_safe_origin = _safe_dist.read_text('PYODIDE_URL')
|
|
559
|
+
if _safe_origin:
|
|
560
|
+
_safe_sources.append(_safe_dist_name + ' @ ' + _safe_origin.strip())
|
|
561
|
+
if _safe_snapshot_paths.get(_safe_dist_name) == str(_safe_dist._path):
|
|
562
|
+
_safe_final_records.append(_safe_record_by_name[_safe_dist_name])
|
|
563
|
+
else:
|
|
564
|
+
_safe_headers = _safe_dist.metadata
|
|
565
|
+
_safe_metadata_text = ''.join(key + ': ' + value + '\\n' for key in ['Metadata-Version', 'Name', 'Version', 'Requires-Python', 'Requires-Dist', 'Provides-Extra'] for value in _safe_headers.get_all(key, []))
|
|
566
|
+
_safe_final_records.append([_safe_dist_name, _safe_metadata_text, (_safe_origin or '').strip(), *_safe_removal_listing(_safe_dist)])
|
|
567
|
+
_safe_installed_json = _safe_json.dumps(_safe_sources + [name + '==' + version for name, version in sorted(_safe_versions.items()) if name in _safe_managed])
|
|
568
|
+
_safe_records_json = _safe_json.dumps(_safe_final_records)
|
|
569
|
+
_safe_metadata.MetadataPathFinder.invalidate_caches()
|
|
570
|
+
import gc as _safe_gc
|
|
571
|
+
_safe_gc.collect()
|
|
572
|
+
`);
|
|
573
|
+
await installing;
|
|
574
|
+
accepting = false;
|
|
575
|
+
await pending;
|
|
576
|
+
if (transportFailure) throw transportFailure.error;
|
|
577
|
+
const pinned = JSON.parse(runtime.runPython("_safe_installed_json"));
|
|
578
|
+
await request("package-commit", start.session, start.restore === void 0 ? pinned : { version: 2, installed: pinned, records: JSON.parse(runtime.runPython("_safe_records_json")) });
|
|
579
|
+
if (start.uninstall) {
|
|
580
|
+
const removed = JSON.parse(runtime.runPython("_safe_uninstalled_json"));
|
|
581
|
+
for (const name of removed) {
|
|
582
|
+
const bytes = new TextEncoder().encode(" Successfully uninstalled " + name + "\n");
|
|
583
|
+
for (let offset = 0; offset < bytes.length; offset += maxTransferBytes) await request("stdout", Array.from(bytes.subarray(offset, offset + maxTransferBytes)));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
await installing;
|
|
588
|
+
accepting = false;
|
|
589
|
+
await pending;
|
|
590
|
+
throw transportFailure ? transportFailure.error : error;
|
|
591
|
+
} finally {
|
|
592
|
+
await installing;
|
|
593
|
+
accepting = false;
|
|
594
|
+
await pending;
|
|
595
|
+
manager.downloadPackage = manager.installPackage = async () => {
|
|
596
|
+
throw new Error("Python package transport is only available during installation");
|
|
597
|
+
};
|
|
598
|
+
for (const name of installedGlobals) runtime.globals.delete(name);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// packages/safe-bash-command-python/src/execution.ts
|
|
603
|
+
var pythonExecution = `
|
|
604
|
+
import sys, os, json, runpy, traceback, types, warnings, textwrap, io, struct, linecache
|
|
605
|
+
_safe_invocation = json.loads(_safe_invocation_json)
|
|
606
|
+
os.environ.clear()
|
|
607
|
+
os.environ.update(_safe_invocation['env'])
|
|
608
|
+
_safe_args = list(_safe_invocation['args'])
|
|
609
|
+
sys.orig_argv = [_safe_invocation.get('command', 'python')] + _safe_args
|
|
610
|
+
_safe_exit = 0
|
|
611
|
+
|
|
612
|
+
class _SafeOptionError(ValueError):
|
|
613
|
+
pass
|
|
614
|
+
|
|
615
|
+
def _safe_system_exit(_error):
|
|
616
|
+
if isinstance(_error.code, int):
|
|
617
|
+
_status = int.__index__(_error.code)
|
|
618
|
+
_long_bound = 1 << (8 * struct.calcsize('@l') - 1)
|
|
619
|
+
status = (_status & 255) if -_long_bound <= _status < _long_bound else 255
|
|
620
|
+
else:
|
|
621
|
+
status = 0 if _error.code is None else 1
|
|
622
|
+
if _error.code is not None and not isinstance(_error.code, int):
|
|
623
|
+
print(_error.code, file=sys.stderr)
|
|
624
|
+
return status
|
|
625
|
+
|
|
626
|
+
def _safe_launch():
|
|
627
|
+
args = list(_safe_args)
|
|
628
|
+
unbuffered = False
|
|
629
|
+
ignore_env = bool(sys.flags.ignore_environment)
|
|
630
|
+
safe_path = bool(sys.flags.safe_path)
|
|
631
|
+
native_warning_options = list(sys.warnoptions)
|
|
632
|
+
# Pyodide adds a bootstrap cwd entry even when CPython safe-path mode is set.
|
|
633
|
+
# Each execution mode below supplies its own entry after explicit PYTHONPATH.
|
|
634
|
+
sys.path[:] = [entry for entry in sys.path if entry != '']
|
|
635
|
+
warning_options = []
|
|
636
|
+
version_count = 0
|
|
637
|
+
options_terminated = False
|
|
638
|
+
while args and args[0].startswith('-') and args[0] != '-':
|
|
639
|
+
option = args.pop(0)
|
|
640
|
+
if option == '--':
|
|
641
|
+
options_terminated = True
|
|
642
|
+
break
|
|
643
|
+
if option in ('--help', '-h', '-?'):
|
|
644
|
+
print('usage: python [-uBEPOsSIbdvqR] [-W filter] [-X option] [-c code | -m module | file | -] [args ...]')
|
|
645
|
+
print('Dedicated Pyodide interpreter; stdin is noninteractive.')
|
|
646
|
+
print('-u: unbuffered binary output; -B: disable bytecode writes; -E: ignore PYTHON* settings')
|
|
647
|
+
print('-P: omit automatic unsafe sys.path entry; -W: Python warning filter')
|
|
648
|
+
print('-O/-OO, -S/-s/-I, -b/-bb, -d/-v/-q/-R: CPython startup flags')
|
|
649
|
+
print('-X: utf8, dev, warn_default_encoding, int_max_str_digits; interactive flags are unsupported.')
|
|
650
|
+
return
|
|
651
|
+
if option == '--version':
|
|
652
|
+
version_count += 1
|
|
653
|
+
continue
|
|
654
|
+
if option.startswith('--'):
|
|
655
|
+
raise _SafeOptionError('unsupported option ' + option)
|
|
656
|
+
index = 1
|
|
657
|
+
while index < len(option):
|
|
658
|
+
flag = option[index]
|
|
659
|
+
if flag in ('h', '?'):
|
|
660
|
+
args = ['--help']
|
|
661
|
+
break
|
|
662
|
+
if flag == 'V':
|
|
663
|
+
version_count += 1
|
|
664
|
+
index += 1
|
|
665
|
+
continue
|
|
666
|
+
if flag in ('c', 'm', 'W', 'X'):
|
|
667
|
+
operand = option[index + 1:] if index + 1 < len(option) else args.pop(0) if args else None
|
|
668
|
+
if operand is None:
|
|
669
|
+
raise _SafeOptionError('argument expected for -' + flag)
|
|
670
|
+
if flag in ('W', 'X'):
|
|
671
|
+
if flag == 'W': warning_options.append(operand)
|
|
672
|
+
break
|
|
673
|
+
args = ['-' + flag, operand] + args
|
|
674
|
+
break
|
|
675
|
+
elif flag in ('O', 'S', 's', 'I', 'b', 'd', 'v', 'q', 'R'):
|
|
676
|
+
pass # Applied by CPython startup configuration before bootstrap.
|
|
677
|
+
elif flag == 'u':
|
|
678
|
+
unbuffered = True
|
|
679
|
+
elif flag == 'B':
|
|
680
|
+
sys.dont_write_bytecode = True
|
|
681
|
+
elif flag == 'E':
|
|
682
|
+
ignore_env = True
|
|
683
|
+
elif flag == 'P':
|
|
684
|
+
safe_path = True
|
|
685
|
+
else:
|
|
686
|
+
raise _SafeOptionError('unsupported option -' + flag)
|
|
687
|
+
index += 1
|
|
688
|
+
if args and args[0] in ('-c', '-m'):
|
|
689
|
+
break
|
|
690
|
+
if version_count:
|
|
691
|
+
print('Python ' + (sys.version if version_count > 1 else sys.version.split()[0]))
|
|
692
|
+
return
|
|
693
|
+
if not ignore_env:
|
|
694
|
+
unbuffered = unbuffered or bool(os.environ.get('PYTHONUNBUFFERED'))
|
|
695
|
+
sys.dont_write_bytecode = sys.dont_write_bytecode or bool(os.environ.get('PYTHONDONTWRITEBYTECODE'))
|
|
696
|
+
safe_path = safe_path or bool(os.environ.get('PYTHONSAFEPATH'))
|
|
697
|
+
path = os.environ.get('PYTHONPATH')
|
|
698
|
+
if path:
|
|
699
|
+
sys.path[0:0] = [os.path.abspath(entry) for entry in path.split(os.pathsep)]
|
|
700
|
+
if os.environ.get('PYTHONWARNINGS'):
|
|
701
|
+
warning_options[0:0] = os.environ['PYTHONWARNINGS'].split(',')
|
|
702
|
+
encoding = os.environ.get('PYTHONIOENCODING')
|
|
703
|
+
if encoding:
|
|
704
|
+
name, separator, errors = encoding.partition(':')
|
|
705
|
+
for stream in (sys.stdin, sys.stdout):
|
|
706
|
+
stream.reconfigure(encoding=name or None, errors=(errors or 'strict'))
|
|
707
|
+
sys.stderr.reconfigure(encoding=name or None, errors='backslashreplace')
|
|
708
|
+
# CPython's -b/-bb filter follows environment and command-line -W filters.
|
|
709
|
+
byte_warning = ('error' if sys.flags.bytes_warning > 1 else 'default') + '::BytesWarning'
|
|
710
|
+
native_before = [value for value in native_warning_options if not (sys.flags.bytes_warning and value == byte_warning)]
|
|
711
|
+
warning_options = native_before + warning_options + ([byte_warning] if sys.flags.bytes_warning else [])
|
|
712
|
+
warnings._processoptions(warning_options)
|
|
713
|
+
sys.warnoptions[:] = warning_options
|
|
714
|
+
if unbuffered:
|
|
715
|
+
for name in ('stdout', 'stderr'):
|
|
716
|
+
stream = getattr(sys, name)
|
|
717
|
+
stream.flush()
|
|
718
|
+
encoding, errors = stream.encoding, stream.errors
|
|
719
|
+
binary = stream.detach()
|
|
720
|
+
raw = binary.detach() if hasattr(binary, 'detach') else binary
|
|
721
|
+
replacement = io.TextIOWrapper(raw, encoding=encoding, errors=errors, write_through=True)
|
|
722
|
+
setattr(sys, name, replacement)
|
|
723
|
+
setattr(sys, '__' + name + '__', replacement)
|
|
724
|
+
main = types.ModuleType('__main__')
|
|
725
|
+
main.__dict__.update(__package__=None, __spec__=None, __loader__=__import__('importlib.machinery', fromlist=['BuiltinImporter']).BuiltinImporter, __builtins__=__builtins__)
|
|
726
|
+
sys.modules['__main__'] = main
|
|
727
|
+
if not options_terminated and args and args[0] == '-c':
|
|
728
|
+
sys.argv = ['-c'] + args[2:]
|
|
729
|
+
if not safe_path: sys.path.insert(0, '')
|
|
730
|
+
source = textwrap.dedent(args[1])
|
|
731
|
+
code = compile(source, '<string>', 'exec')
|
|
732
|
+
# CPython 3.14 retains -c source for both top-level and nested tracebacks.
|
|
733
|
+
linecache._register_code(code, source, '<string>')
|
|
734
|
+
exec(code, main.__dict__)
|
|
735
|
+
elif not options_terminated and args and args[0] == '-m':
|
|
736
|
+
sys.argv = args[1:]
|
|
737
|
+
if not safe_path: sys.path.insert(0, os.getcwd())
|
|
738
|
+
runpy._run_module_as_main(args[1], alter_argv=True)
|
|
739
|
+
elif not args or args[0] == '-':
|
|
740
|
+
sys.argv = (['-'] + args[1:]) if args else ['']
|
|
741
|
+
if not safe_path: sys.path.insert(0, '')
|
|
742
|
+
main.__file__ = '<stdin>'
|
|
743
|
+
exec(compile(sys.stdin.buffer.read(), '<stdin>', 'exec'), main.__dict__)
|
|
744
|
+
else:
|
|
745
|
+
sys.argv = args
|
|
746
|
+
# CPython makes __file__ absolute without collapsing ./ or ../ components.
|
|
747
|
+
path = args[0] if os.path.isabs(args[0]) else os.path.join(os.getcwd(), args[0])
|
|
748
|
+
importer = __import__('pkgutil').get_importer(path)
|
|
749
|
+
if importer is not None:
|
|
750
|
+
sys.path.insert(0, path)
|
|
751
|
+
runpy._run_module_as_main('__main__', alter_argv=False)
|
|
752
|
+
else:
|
|
753
|
+
if not safe_path: sys.path.insert(0, os.path.dirname(os.path.realpath(path)))
|
|
754
|
+
try:
|
|
755
|
+
with open(path, 'rb') as source:
|
|
756
|
+
code = source.read()
|
|
757
|
+
except OSError as error:
|
|
758
|
+
print("python: can't open file " + repr(path) + ': [Errno ' + str(error.errno) + '] ' + error.strerror, file=sys.stderr)
|
|
759
|
+
return 2
|
|
760
|
+
main.__file__ = path
|
|
761
|
+
main.__loader__ = __import__('importlib.machinery', fromlist=['SourceFileLoader']).SourceFileLoader('__main__', path)
|
|
762
|
+
exec(compile(code, path, 'exec'), main.__dict__)
|
|
763
|
+
return 0
|
|
764
|
+
|
|
765
|
+
try:
|
|
766
|
+
_safe_exit = _safe_launch() or 0
|
|
767
|
+
except SystemExit as _error:
|
|
768
|
+
_safe_exit = _safe_system_exit(_error)
|
|
769
|
+
except _SafeOptionError as _error:
|
|
770
|
+
print('python: ' + str(_error), file=sys.stderr)
|
|
771
|
+
_safe_exit = 2
|
|
772
|
+
except BaseException as _error:
|
|
773
|
+
# Hide bridge frames while preserving actual Python frames and chained errors.
|
|
774
|
+
_trace = _error.__traceback__
|
|
775
|
+
while _trace is not None and _trace.tb_frame.f_code.co_filename == '<exec>':
|
|
776
|
+
_trace = _trace.tb_next
|
|
777
|
+
_error.__traceback__ = _trace
|
|
778
|
+
sys.last_exc = _error
|
|
779
|
+
_safe_exit = 130 if isinstance(_error, KeyboardInterrupt) else 1
|
|
780
|
+
try:
|
|
781
|
+
sys.excepthook(type(_error), _error, _trace)
|
|
782
|
+
except SystemExit as _hook_error:
|
|
783
|
+
_safe_exit = _safe_system_exit(_hook_error)
|
|
784
|
+
except BaseException as _hook_error:
|
|
785
|
+
print('Error in sys.excepthook:', file=sys.stderr)
|
|
786
|
+
traceback.print_exception(type(_hook_error), _hook_error, _hook_error.__traceback__.tb_next)
|
|
787
|
+
print('\\nOriginal exception was:', file=sys.stderr)
|
|
788
|
+
sys.__excepthook__(type(_error), _error, _trace)
|
|
789
|
+
finally:
|
|
790
|
+
for _stream in (sys.stdout, sys.stderr):
|
|
791
|
+
if _stream is not None and not getattr(_stream, 'closed', False):
|
|
792
|
+
try:
|
|
793
|
+
_stream.flush()
|
|
794
|
+
except BaseException:
|
|
795
|
+
_safe_exit = 120
|
|
796
|
+
_safe_exit
|
|
797
|
+
`;
|
|
798
|
+
|
|
799
|
+
// packages/safe-bash-command-python/src/runtime-scripts.ts
|
|
800
|
+
var pythonRuntimeRelocation = `
|
|
801
|
+
import sys, zipimport, importlib.machinery
|
|
802
|
+
|
|
803
|
+
def _rebase_runtime_path(value):
|
|
804
|
+
return _safe_runtime_mount + value if isinstance(value, str) and (value == '/lib' or value.startswith('/lib/')) else value
|
|
805
|
+
|
|
806
|
+
def _rebase_runtime_loader(loader):
|
|
807
|
+
if isinstance(loader, zipimport.zipimporter):
|
|
808
|
+
archive = _rebase_runtime_path(loader.archive)
|
|
809
|
+
if archive != loader.archive:
|
|
810
|
+
return zipimport.zipimporter(archive + '/' + loader.prefix)
|
|
811
|
+
elif isinstance(loader, (importlib.machinery.SourceFileLoader, importlib.machinery.SourcelessFileLoader, importlib.machinery.ExtensionFileLoader)):
|
|
812
|
+
path = _rebase_runtime_path(loader.path)
|
|
813
|
+
if path != loader.path:
|
|
814
|
+
return type(loader)(loader.name, path)
|
|
815
|
+
return loader
|
|
816
|
+
sys.path[:] = [_rebase_runtime_path(value) for value in sys.path]
|
|
817
|
+
for _module in list(sys.modules.values()):
|
|
818
|
+
if _module is None:
|
|
819
|
+
continue
|
|
820
|
+
for _attribute in ('__file__', '__cached__'):
|
|
821
|
+
if hasattr(_module, _attribute):
|
|
822
|
+
setattr(_module, _attribute, _rebase_runtime_path(getattr(_module, _attribute)))
|
|
823
|
+
if hasattr(_module, '__path__'):
|
|
824
|
+
_module.__path__ = [_rebase_runtime_path(value) for value in _module.__path__]
|
|
825
|
+
if hasattr(_module, '__loader__'):
|
|
826
|
+
_module.__loader__ = _rebase_runtime_loader(_module.__loader__)
|
|
827
|
+
_spec = getattr(_module, '__spec__', None)
|
|
828
|
+
if _spec is not None:
|
|
829
|
+
_spec.origin = _rebase_runtime_path(_spec.origin)
|
|
830
|
+
_spec.loader = _rebase_runtime_loader(_spec.loader)
|
|
831
|
+
if _spec.submodule_search_locations is not None:
|
|
832
|
+
_spec.submodule_search_locations = [_rebase_runtime_path(value) for value in _spec.submodule_search_locations]
|
|
833
|
+
sys.path_importer_cache.clear()
|
|
834
|
+
`;
|
|
835
|
+
var pythonImportMetadata = `
|
|
836
|
+
import importlib._bootstrap_external, types, json
|
|
837
|
+
_safe_import_stat_callback = _safe_import_stat
|
|
838
|
+
_safe_original_import_stat = importlib._bootstrap_external._path_stat
|
|
839
|
+
def _safe_import_path_stat(path):
|
|
840
|
+
if path == _safe_runtime_mount or path.startswith(_safe_runtime_mount + '/'):
|
|
841
|
+
return _safe_original_import_stat(path)
|
|
842
|
+
value = json.loads(_safe_import_stat_callback(path))
|
|
843
|
+
if 'errno' in value:
|
|
844
|
+
raise OSError(value['errno'], 'filesystem import metadata unavailable', path)
|
|
845
|
+
return types.SimpleNamespace(st_mode=value['mode'], st_size=value['size'], st_mtime=value['mtimeMs'] / 1000)
|
|
846
|
+
importlib._bootstrap_external._path_stat = _safe_import_path_stat
|
|
847
|
+
`;
|
|
848
|
+
var pythonDirectoryEntries = `
|
|
849
|
+
import os, json, errno
|
|
850
|
+
_safe_directory_callback = _safe_directory_entries
|
|
851
|
+
def _safe_listdir(path='.'):
|
|
852
|
+
if isinstance(path, int):
|
|
853
|
+
raise OSError(errno.ENOTSUP, 'retained directory descriptors are unsupported')
|
|
854
|
+
original = os.fspath(path) if path is not None else '.'
|
|
855
|
+
value = json.loads(_safe_directory_callback(os.fsdecode(original)))
|
|
856
|
+
if 'errno' in value:
|
|
857
|
+
raise OSError(value['errno'], 'directory enumeration failed', path)
|
|
858
|
+
return [os.fsencode(name) for name in value['entries']] if isinstance(original, bytes) else value['entries']
|
|
859
|
+
os.listdir = _safe_listdir
|
|
860
|
+
# The frozen importer holds the posix module separately from os.
|
|
861
|
+
importlib._bootstrap_external._os.listdir = _safe_listdir
|
|
862
|
+
`;
|
|
863
|
+
var pythonStatProjection = `
|
|
864
|
+
import os, json, errno
|
|
865
|
+
_safe_stat_callback = _safe_stat_projection
|
|
866
|
+
_safe_stat_type = os.stat_result
|
|
867
|
+
_safe_fspath = os.fspath
|
|
868
|
+
_safe_fsdecode = os.fsdecode
|
|
869
|
+
|
|
870
|
+
def _safe_stat(path, *, dir_fd=None, follow_symlinks=True):
|
|
871
|
+
if dir_fd is not None:
|
|
872
|
+
raise OSError(errno.ENOTSUP, 'dir_fd metadata is unsupported')
|
|
873
|
+
if isinstance(path, int):
|
|
874
|
+
if not follow_symlinks:
|
|
875
|
+
raise ValueError('stat: cannot use fd and follow_symlinks together')
|
|
876
|
+
else:
|
|
877
|
+
path = _safe_fsdecode(_safe_fspath(path))
|
|
878
|
+
value = json.loads(_safe_stat_callback(path, follow_symlinks))
|
|
879
|
+
if 'errno' in value:
|
|
880
|
+
raise OSError(value['errno'], 'filesystem metadata unavailable', path)
|
|
881
|
+
keys = ('mode','ino','dev','nlink','uid','gid','size')
|
|
882
|
+
if any(key not in value for key in keys):
|
|
883
|
+
raise OSError(errno.EOVERFLOW, 'canonical identity or required stat metadata unavailable', path)
|
|
884
|
+
times = [value[key] / 1000 for key in ('atimeMs','mtimeMs','ctimeMs')]
|
|
885
|
+
extras = dict(zip(('st_atime','st_mtime','st_ctime'), times))
|
|
886
|
+
for name, milliseconds in zip(('atime','mtime','ctime'), (value['atimeMs'],value['mtimeMs'],value['ctimeMs'])):
|
|
887
|
+
seconds = int(milliseconds // 1000)
|
|
888
|
+
extras['st_' + name + '_ns'] = seconds * 1000000000 + round((milliseconds - seconds * 1000) * 1000000)
|
|
889
|
+
for key in ('blocks','blksize','rdev'):
|
|
890
|
+
if key in value:
|
|
891
|
+
extras['st_' + key] = value[key]
|
|
892
|
+
return _safe_stat_type(tuple(value[key] for key in keys) + tuple(int(time // 1) for time in times), extras)
|
|
893
|
+
|
|
894
|
+
def _safe_lstat(path, *, dir_fd=None):
|
|
895
|
+
return _safe_stat(path, dir_fd=dir_fd, follow_symlinks=False)
|
|
896
|
+
|
|
897
|
+
def _safe_fstat(fd):
|
|
898
|
+
if not isinstance(fd, int):
|
|
899
|
+
raise TypeError('file descriptor must be an integer')
|
|
900
|
+
return _safe_stat(fd)
|
|
901
|
+
os.stat = _safe_stat
|
|
902
|
+
os.lstat = _safe_lstat
|
|
903
|
+
os.fstat = _safe_fstat
|
|
904
|
+
import stat as _safe_stat_module
|
|
905
|
+
class _SafeDirEntry:
|
|
906
|
+
def __init__(self, root, name):
|
|
907
|
+
self.name = name
|
|
908
|
+
self.path = os.path.join(root, name)
|
|
909
|
+
self._cache = {}
|
|
910
|
+
def __fspath__(self):
|
|
911
|
+
return self.path
|
|
912
|
+
def stat(self, *, follow_symlinks=True):
|
|
913
|
+
if follow_symlinks not in self._cache:
|
|
914
|
+
self._cache[follow_symlinks] = os.stat(self.path, follow_symlinks=follow_symlinks)
|
|
915
|
+
return self._cache[follow_symlinks]
|
|
916
|
+
def inode(self):
|
|
917
|
+
return self.stat(follow_symlinks=False).st_ino
|
|
918
|
+
def is_dir(self, *, follow_symlinks=True):
|
|
919
|
+
try:
|
|
920
|
+
return _safe_stat_module.S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
|
|
921
|
+
except FileNotFoundError:
|
|
922
|
+
return False
|
|
923
|
+
def is_file(self, *, follow_symlinks=True):
|
|
924
|
+
try:
|
|
925
|
+
return _safe_stat_module.S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
|
|
926
|
+
except FileNotFoundError:
|
|
927
|
+
return False
|
|
928
|
+
def is_symlink(self):
|
|
929
|
+
try:
|
|
930
|
+
return _safe_stat_module.S_ISLNK(self.stat(follow_symlinks=False).st_mode)
|
|
931
|
+
except FileNotFoundError:
|
|
932
|
+
return False
|
|
933
|
+
class _SafeScandir:
|
|
934
|
+
def __init__(self, path):
|
|
935
|
+
self._entries = iter([_SafeDirEntry(path, name) for name in os.listdir(path)])
|
|
936
|
+
def __iter__(self):
|
|
937
|
+
return self
|
|
938
|
+
def __next__(self):
|
|
939
|
+
return next(self._entries)
|
|
940
|
+
def close(self):
|
|
941
|
+
self._entries = iter(())
|
|
942
|
+
def __enter__(self):
|
|
943
|
+
return self
|
|
944
|
+
def __exit__(self, *args):
|
|
945
|
+
self.close()
|
|
946
|
+
def _safe_scandir(path='.'):
|
|
947
|
+
if isinstance(path, int):
|
|
948
|
+
raise OSError(errno.ENOTSUP, 'retained directory descriptors are unsupported')
|
|
949
|
+
return _SafeScandir(os.fspath(path) if path is not None else '.')
|
|
950
|
+
os.scandir = _safe_scandir
|
|
951
|
+
|
|
952
|
+
`;
|
|
953
|
+
var pythonHardLinks = `
|
|
954
|
+
import os, json, errno
|
|
955
|
+
_safe_link_callback = _safe_hard_link
|
|
956
|
+
def _safe_link(src, dst, *, src_dir_fd=None, dst_dir_fd=None, follow_symlinks=True):
|
|
957
|
+
if src_dir_fd is not None or dst_dir_fd is not None:
|
|
958
|
+
raise OSError(errno.ENOTSUP, 'dir_fd hard links are unsupported')
|
|
959
|
+
source = os.fsdecode(os.fspath(src))
|
|
960
|
+
destination = os.fsdecode(os.fspath(dst))
|
|
961
|
+
value = json.loads(_safe_link_callback(source, destination, bool(follow_symlinks)))
|
|
962
|
+
if 'errno' in value:
|
|
963
|
+
raise OSError(value['errno'], 'hard link creation failed', src, None, dst)
|
|
964
|
+
os.link = _safe_link
|
|
965
|
+
os.supports_follow_symlinks.add(_safe_link)
|
|
966
|
+
`;
|
|
967
|
+
var pythonTreeCleanup = `
|
|
968
|
+
import shutil, os, json
|
|
969
|
+
_safe_tree_callback = _safe_tree_cleanup
|
|
970
|
+
_safe_original_rmtree_impl = shutil._rmtree_impl
|
|
971
|
+
def _safe_rmtree_impl(path, dir_fd, onexc):
|
|
972
|
+
if dir_fd is not None:
|
|
973
|
+
return _safe_original_rmtree_impl(path, dir_fd, onexc)
|
|
974
|
+
value = json.loads(_safe_tree_callback(os.fsdecode(path)))
|
|
975
|
+
if not value['supported']:
|
|
976
|
+
return _safe_original_rmtree_impl(path, dir_fd, onexc)
|
|
977
|
+
if 'errno' in value:
|
|
978
|
+
onexc(os.rmdir, path, OSError(value['errno'], 'directory cleanup failed', path))
|
|
979
|
+
shutil._rmtree_impl = _safe_rmtree_impl
|
|
980
|
+
`;
|
|
981
|
+
|
|
982
|
+
export {
|
|
983
|
+
PythonInvocationError,
|
|
984
|
+
parsePythonInvocation,
|
|
985
|
+
installPythonPackages,
|
|
986
|
+
pythonExecution,
|
|
987
|
+
pythonRuntimeRelocation,
|
|
988
|
+
pythonImportMetadata,
|
|
989
|
+
pythonDirectoryEntries,
|
|
990
|
+
pythonStatProjection,
|
|
991
|
+
pythonHardLinks,
|
|
992
|
+
pythonTreeCleanup
|
|
993
|
+
};
|