sandboxedjs 0.1.32 → 0.1.34

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/index.cjs CHANGED
@@ -17924,20 +17924,620 @@ def _install():
17924
17924
 
17925
17925
  _install()
17926
17926
  `;
17927
+ var PROCESS_PY = `
17928
+ import io, os, shlex, subprocess, sys
17929
+ import _sbx_host as _host
17930
+ from pyodide.ffi import can_run_sync, run_sync
17931
+
17932
+ _PIPE = subprocess.PIPE
17933
+ _DEVNULL = subprocess.DEVNULL
17934
+ _STDOUT = subprocess.STDOUT
17935
+
17936
+
17937
+ def _argv_for(args, shell):
17938
+ if shell:
17939
+ line = args if isinstance(args, str) else " ".join(str(a) for a in args)
17940
+ return ["sh", "-c", line]
17941
+ if isinstance(args, (str, bytes)):
17942
+ return shlex.split(args if isinstance(args, str) else args.decode())
17943
+ return [str(a) for a in args]
17944
+
17945
+
17946
+ def _require_blocking(what):
17947
+ if not can_run_sync():
17948
+ raise OSError(
17949
+ "cannot " + what + " on this host: WebAssembly stack switching "
17950
+ "(JSPI) is unavailable."
17951
+ )
17952
+
17953
+
17954
+ def _to_bytes_result(value):
17955
+ if value is None:
17956
+ return b""
17957
+ to_bytes = getattr(value, "to_bytes", None)
17958
+ return to_bytes() if to_bytes is not None else bytes(value.to_py())
17959
+
17960
+
17961
+ class _ChildReader(io.RawIOBase):
17962
+ """One of a running child's output streams.
17963
+
17964
+ Reads suspend only for as long as the child takes to produce the next
17965
+ bytes, so \`for line in p.stdout\` follows a child that is still running
17966
+ instead of waiting for it to exit.
17967
+ """
17968
+
17969
+ def __init__(self, child_id, which):
17970
+ self._id = child_id
17971
+ self._which = which
17972
+
17973
+ def readable(self):
17974
+ return True
17975
+
17976
+ def readinto(self, target):
17977
+ want = len(target)
17978
+ if want == 0:
17979
+ return 0
17980
+ _require_blocking("read from a child process")
17981
+ data = _to_bytes_result(run_sync(_host.proc_read(self._id, self._which, want)))
17982
+ target[: len(data)] = data
17983
+ return len(data)
17984
+
17985
+
17986
+ class _ChildWriter(io.RawIOBase):
17987
+ """A running child's standard input."""
17988
+
17989
+ def __init__(self, child_id):
17990
+ self._id = child_id
17991
+
17992
+ def writable(self):
17993
+ return True
17994
+
17995
+ def write(self, data):
17996
+ if isinstance(data, str):
17997
+ text = data
17998
+ else:
17999
+ text = bytes(data).decode("utf-8", "replace")
18000
+ _host.proc_write(self._id, text)
18001
+ return len(data)
18002
+
18003
+ def close(self):
18004
+ if not self.closed:
18005
+ _host.proc_close_stdin(self._id)
18006
+ super().close()
18007
+
18008
+
18009
+ class SbxPopen:
18010
+ """A child process, run through the container's kernel.
18011
+
18012
+ The child is a real, live process: it starts when \`Popen\` is constructed
18013
+ and runs on the host's event loop while Python carries on. The parent only
18014
+ suspends when it actually reads, waits, or communicates \u2014 which is what
18015
+ makes streaming from a long-running child work rather than deadlocking on
18016
+ a single interpreter stack.
18017
+ """
18018
+
18019
+ def __init__(self, args, bufsize=-1, executable=None, stdin=None, stdout=None,
18020
+ stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None,
18021
+ env=None, universal_newlines=None, startupinfo=None, creationflags=0,
18022
+ restore_signals=True, start_new_session=False, pass_fds=(), *,
18023
+ text=None, encoding=None, errors=None, **kwargs):
18024
+ self.args = args
18025
+ self.returncode = None
18026
+ self._text = bool(text or encoding or errors or universal_newlines)
18027
+ self._encoding = encoding or "utf-8"
18028
+ self._errors = errors or "replace"
18029
+ self._merge_err = stderr == _STDOUT
18030
+ self._closed = False
18031
+
18032
+ argv = _argv_for(args, shell)
18033
+ env_map = None if env is None else {str(k): str(v) for k, v in dict(env).items()}
18034
+ self._id = _host.proc_start(
18035
+ argv, None if cwd is None else str(cwd), env_map, self._merge_err
18036
+ )
18037
+ self.pid = int(_host.proc_pid(self._id))
18038
+
18039
+ self.stdin = self._wrap_writer() if stdin == _PIPE else None
18040
+ self.stdout = self._wrap_reader(1) if stdout == _PIPE else None
18041
+ # When merged there is only one stream, and it is stream 1.
18042
+ self.stderr = self._wrap_reader(2) if stderr == _PIPE and not self._merge_err else None
18043
+
18044
+ # Output nobody captured belongs on the parent's own streams, or a
18045
+ # program's diagnostics would vanish rather than being seen.
18046
+ self._drain_out = stdout is None
18047
+ self._drain_err = stderr is None and not self._merge_err
18048
+
18049
+ def _wrap_reader(self, which):
18050
+ raw = io.BufferedReader(_ChildReader(self._id, which))
18051
+ if self._text:
18052
+ return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
18053
+ return raw
18054
+
18055
+ def _wrap_writer(self):
18056
+ raw = io.BufferedWriter(_ChildWriter(self._id))
18057
+ if self._text:
18058
+ return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
18059
+ return raw
18060
+
18061
+ def _read_all(self, which):
18062
+ _require_blocking("read from a child process")
18063
+ parts = []
18064
+ while True:
18065
+ chunk = _to_bytes_result(run_sync(_host.proc_read(self._id, which, 65536)))
18066
+ if not chunk:
18067
+ break
18068
+ parts.append(chunk)
18069
+ return b"".join(parts)
18070
+
18071
+ def _decode(self, raw):
18072
+ return raw.decode(self._encoding, self._errors) if self._text else raw
18073
+
18074
+ def communicate(self, input=None, timeout=None):
18075
+ if input is not None and self.stdin is not None:
18076
+ if isinstance(input, bytes):
18077
+ input = input.decode(self._encoding, "replace")
18078
+ self.stdin.write(input)
18079
+ if self.stdin is not None:
18080
+ try:
18081
+ self.stdin.close()
18082
+ except Exception:
18083
+ pass
18084
+
18085
+ out = self.stdout.read() if self.stdout is not None else None
18086
+ err = self.stderr.read() if self.stderr is not None else None
18087
+ self.wait()
18088
+ self._flush_uncaptured()
18089
+ return out, err
18090
+
18091
+ def _flush_uncaptured(self):
18092
+ if self._drain_out:
18093
+ text = self._decode_raw(self._read_all(1))
18094
+ if text:
18095
+ sys.stdout.write(text)
18096
+ if self._drain_err:
18097
+ text = self._decode_raw(self._read_all(2))
18098
+ if text:
18099
+ sys.stderr.write(text)
18100
+ self._drain_out = False
18101
+ self._drain_err = False
18102
+
18103
+ def _decode_raw(self, raw):
18104
+ return raw.decode(self._encoding, "replace")
18105
+
18106
+ def poll(self):
18107
+ code = _host.proc_poll(self._id)
18108
+ if code is not None:
18109
+ self.returncode = int(code)
18110
+ return self.returncode
18111
+
18112
+ def wait(self, timeout=None):
18113
+ _require_blocking("wait for a child process")
18114
+ self.returncode = int(run_sync(_host.proc_wait(self._id)))
18115
+ return self.returncode
18116
+
18117
+ def kill(self):
18118
+ self.send_signal("SIGKILL")
18119
+
18120
+ def terminate(self):
18121
+ self.send_signal("SIGTERM")
18122
+
18123
+ def send_signal(self, sig):
18124
+ _host.proc_kill(self._id, sig if isinstance(sig, str) else "SIGTERM")
18125
+
18126
+ def __del__(self):
18127
+ try:
18128
+ _host.proc_release(self._id)
18129
+ except Exception:
18130
+ pass
18131
+
18132
+ def __enter__(self):
18133
+ return self
18134
+
18135
+ def __exit__(self, *exc):
18136
+ if self.stdin is not None:
18137
+ try:
18138
+ self.stdin.close()
18139
+ except Exception:
18140
+ pass
18141
+ self.wait()
18142
+ self._flush_uncaptured()
18143
+ for stream in (self.stdout, self.stderr):
18144
+ if stream is not None:
18145
+ try:
18146
+ stream.close()
18147
+ except Exception:
18148
+ pass
18149
+ # The host holds the process and its pipes until it is told the child
18150
+ # is finished with; a loop spawning children would otherwise keep every
18151
+ # one of them alive. Safe here only because the child has been waited
18152
+ # for and everything anyone wanted has been read.
18153
+ _host.proc_release(self._id)
18154
+ return False
18155
+
18156
+
18157
+ def _system(command):
18158
+ with SbxPopen(command, shell=True) as child:
18159
+ pass
18160
+ # os.system returns a wait status, not an exit code.
18161
+ return (child.returncode or 0) << 8
18162
+
18163
+
18164
+ def _popen(command, mode="r", buffering=-1):
18165
+ if "w" in mode:
18166
+ raise OSError("os.popen with mode 'w' is not supported in this container")
18167
+ child = SbxPopen(command, shell=True, stdout=_PIPE, text=True)
18168
+ stream = child.stdout
18169
+ inner = stream.close
18170
+
18171
+ def close():
18172
+ inner()
18173
+ code = child.wait()
18174
+ return None if code == 0 else code << 8
18175
+
18176
+ stream.close = close
18177
+ return stream
18178
+
18179
+
18180
+ def _install_processes():
18181
+ subprocess.Popen = SbxPopen
18182
+ os.system = _system
18183
+ os.popen = _popen
18184
+
18185
+
18186
+ _install_processes()
18187
+ `;
18188
+ var ASYNCIO_PY = `
18189
+ import asyncio
18190
+ from pyodide.ffi import can_run_sync, run_sync
18191
+
18192
+ _orig_run = asyncio.run
18193
+
18194
+
18195
+ def _run(main, *, debug=None, loop_factory=None):
18196
+ if not asyncio.iscoroutine(main) and not asyncio.isfuture(main):
18197
+ raise ValueError("a coroutine was expected, got {!r}".format(main))
18198
+ if not can_run_sync():
18199
+ # No stack switching: the original at least raises a comprehensible
18200
+ # error rather than this shim inventing a new one.
18201
+ return _orig_run(main, debug=debug, loop_factory=loop_factory)
18202
+ return run_sync(main)
18203
+
18204
+
18205
+ def _run_until_complete(self, future):
18206
+ if not can_run_sync():
18207
+ raise RuntimeError(
18208
+ "cannot wait for a coroutine on this host: WebAssembly stack "
18209
+ "switching (JSPI) is unavailable."
18210
+ )
18211
+ return run_sync(future)
18212
+
18213
+
18214
+ async def _create_server(self, *args, **kwargs):
18215
+ # Pyodide's loop raises a bare NotImplementedError from deep inside
18216
+ # asyncio, which tells a reader nothing about why their server will not
18217
+ # start. Naming the limit is the least this can do until the container can
18218
+ # route connections to Python.
18219
+ raise NotImplementedError(
18220
+ "this container cannot yet accept network connections from Python: "
18221
+ "asyncio's create_server is not implemented on Pyodide's event loop, "
18222
+ "so an ASGI/WSGI server such as uvicorn will install and import but "
18223
+ "not bind a port. Outbound requests do work."
18224
+ )
18225
+
18226
+
18227
+ def _install_asyncio():
18228
+ asyncio.run = _run
18229
+ asyncio.runners.run = _run
18230
+ loop_type = type(asyncio.get_event_loop())
18231
+ loop_type.run_until_complete = _run_until_complete
18232
+ loop_type.create_server = _create_server
18233
+
18234
+
18235
+ _install_asyncio()
18236
+ `;
18237
+ var NETWORK_PY = `
18238
+ import builtins, email.message, io, sys, urllib.error, urllib.request, urllib.response
18239
+ import _sbx_host as _host
18240
+ from pyodide.ffi import can_run_sync, run_sync
18241
+
18242
+
18243
+ def _fetch(request):
18244
+ if not can_run_sync():
18245
+ raise urllib.error.URLError(
18246
+ "cannot make a request on this host: WebAssembly stack switching "
18247
+ "(JSPI) is unavailable."
18248
+ )
18249
+ body = request.data
18250
+ if isinstance(body, str):
18251
+ body = body.encode()
18252
+ try:
18253
+ result = run_sync(
18254
+ _host.http(
18255
+ request.full_url,
18256
+ request.get_method(),
18257
+ [[k, v] for k, v in request.header_items()],
18258
+ body,
18259
+ )
18260
+ )
18261
+ except Exception as error:
18262
+ raise urllib.error.URLError(str(error)) from None
18263
+
18264
+ headers = email.message.Message()
18265
+ for pair in result.headers.to_py():
18266
+ headers[str(pair[0])] = str(pair[1])
18267
+ payload = result.body.to_bytes()
18268
+ response = urllib.response.addinfourl(
18269
+ io.BytesIO(payload), headers, str(result.url), int(result.status)
18270
+ )
18271
+ response.msg = str(result.statusText)
18272
+ return response
18273
+
18274
+
18275
+ class _SbxHTTPHandler(urllib.request.HTTPHandler):
18276
+ """Both schemes, from one handler.
18277
+
18278
+ Pyodide builds urllib without \`ssl\`, so \`HTTPSHandler\` does not exist to
18279
+ subclass \u2014 and https is the scheme most callers actually want. Serving
18280
+ both from a subclass of the one handler that does exist keeps
18281
+ \`build_opener\` treating this as a replacement rather than stacking it
18282
+ alongside the original.
18283
+ """
18284
+
18285
+ def http_open(self, request):
18286
+ return _fetch(request)
18287
+
18288
+ def https_open(self, request):
18289
+ return _fetch(request)
18290
+
18291
+ https_request = urllib.request.HTTPHandler.do_request_
18292
+
18293
+
18294
+ def _raw_fetch(url, method, headers, body):
18295
+ """One request, through the container's network path."""
18296
+ if not can_run_sync():
18297
+ raise urllib.error.URLError(
18298
+ "cannot make a request on this host: WebAssembly stack switching "
18299
+ "(JSPI) is unavailable."
18300
+ )
18301
+ if isinstance(body, str):
18302
+ body = body.encode()
18303
+ result = run_sync(
18304
+ _host.http(url, method, [[str(k), str(v)] for k, v in dict(headers).items()], body)
18305
+ )
18306
+ return {
18307
+ "status": int(result.status),
18308
+ "reason": str(result.statusText),
18309
+ "headers": [(str(p[0]), str(p[1])) for p in result.headers.to_py()],
18310
+ "body": result.body.to_bytes(),
18311
+ "url": str(result.url),
18312
+ }
18313
+
18314
+
18315
+ # \u2500\u2500 Routing third-party HTTP stacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
18316
+ #
18317
+ # A library that brings its own transport does not go through urllib, and in
18318
+ # Pyodide several of them reach JavaScript's fetch directly \u2014 which leaves the
18319
+ # container's network policy behind entirely. They cannot all be patched at
18320
+ # boot either, because pip installs them later.
18321
+ #
18322
+ # So: adapters are registered by module name and applied the moment that module
18323
+ # is first imported, whenever that happens. Supporting another stack is a small
18324
+ # function registered here, not a change to the machinery.
18325
+
18326
+ _ADAPTERS = {}
18327
+
18328
+
18329
+ def _register_adapter(name):
18330
+ def decorate(fn):
18331
+ _ADAPTERS[name] = fn
18332
+ return fn
18333
+
18334
+ return decorate
18335
+
18336
+
18337
+ def _apply_adapter(name):
18338
+ fn = _ADAPTERS.pop(name, None)
18339
+ if fn is None:
18340
+ return
18341
+ try:
18342
+ fn()
18343
+ except Exception:
18344
+ # A stack we cannot adapt must not stop the import that triggered it.
18345
+ pass
18346
+
18347
+
18348
+ def _install_import_hook():
18349
+ real_import = builtins.__import__
18350
+
18351
+ def hooked(name, globals=None, locals=None, fromlist=(), level=0):
18352
+ module = real_import(name, globals, locals, fromlist, level)
18353
+ root = name.split(".")[0] if level == 0 else None
18354
+ if root in _ADAPTERS:
18355
+ _apply_adapter(root)
18356
+ return module
18357
+
18358
+ builtins.__import__ = hooked
18359
+
18360
+
18361
+ @_register_adapter("requests")
18362
+ def _adapt_requests():
18363
+ """Replace the transport, not the API.
18364
+
18365
+ \`HTTPAdapter.send\` is the single seam every requests call passes through,
18366
+ below sessions, redirects, cookies and retries and above the urllib3
18367
+ transport that would otherwise reach the network on its own. Replacing it
18368
+ leaves everything callers actually use intact.
18369
+ """
18370
+ import requests
18371
+ from requests.adapters import HTTPAdapter
18372
+ from requests.structures import CaseInsensitiveDict
18373
+
18374
+ native_send = HTTPAdapter.send
18375
+
18376
+ def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
18377
+ if not can_run_sync():
18378
+ # Without stack switching this transport cannot run at all. Falling
18379
+ # back to the library's own is better than breaking requests on
18380
+ # such a host \u2014 but the container's network policy is not the part
18381
+ # that degrades, so a forbidden host is still refused.
18382
+ if not _host.policy_allows(request.url):
18383
+ raise requests.exceptions.ConnectionError(
18384
+ "outbound network access is disabled for this container "
18385
+ "(enable with network: { allowOutbound: true })"
18386
+ )
18387
+ return native_send(self, request, stream, timeout, verify, cert, proxies)
18388
+ try:
18389
+ result = _raw_fetch(request.url, request.method, request.headers, request.body)
18390
+ except Exception as error:
18391
+ raise requests.exceptions.ConnectionError(str(error)) from None
18392
+ response = requests.Response()
18393
+ response.status_code = result["status"]
18394
+ response.reason = result["reason"]
18395
+ response.headers = CaseInsensitiveDict(result["headers"])
18396
+ response.url = result["url"]
18397
+ response.request = request
18398
+ response.raw = io.BytesIO(result["body"])
18399
+ response.encoding = requests.utils.get_encoding_from_headers(response.headers)
18400
+ return response
18401
+
18402
+ HTTPAdapter.send = send
18403
+
18404
+
18405
+ def _install_network():
18406
+ urllib.request.HTTPHandler = _SbxHTTPHandler
18407
+ urllib.request.HTTPSHandler = _SbxHTTPHandler
18408
+ urllib.request.install_opener(urllib.request.build_opener(_SbxHTTPHandler))
18409
+ _install_import_hook()
18410
+ for already_imported in [n for n in _ADAPTERS if n in sys.modules]:
18411
+ _apply_adapter(already_imported)
18412
+
18413
+
18414
+ _install_network()
18415
+ `;
18416
+ async function httpForPython(slot, url, method, headers, body) {
18417
+ const current = slot.current;
18418
+ if (!current) throw new Error("no program is bound");
18419
+ const result = await performRequest(current.ctx, new URL(url), {
18420
+ method,
18421
+ headers: Object.fromEntries(headers),
18422
+ ...body ? { body } : {}
18423
+ });
18424
+ return {
18425
+ status: result.status,
18426
+ statusText: result.statusText,
18427
+ headers: Object.entries(result.headers),
18428
+ body: result.body,
18429
+ url: result.url
18430
+ };
18431
+ }
18432
+ async function resuming(py, slot, work) {
18433
+ const parent = slot.current;
18434
+ try {
18435
+ return await work();
18436
+ } finally {
18437
+ if (parent && slot.current !== parent) bindProgram(py, parent);
18438
+ }
18439
+ }
17927
18440
  function installSyscallBridge(py) {
17928
- const slot = py.__sbxStdin ??= { active: null };
18441
+ const slot = py.__sbxSlot ??= { current: null };
18442
+ const children = /* @__PURE__ */ new Map();
18443
+ let lastChildId = 0;
17929
18444
  py.registerJsModule("_sbx_host", {
17930
18445
  /* Not `?.` and `??`: `buffered` returns undefined to mean "you will have
17931
18446
  * to wait", and a nullish default would quietly turn that into end-of-file —
17932
18447
  * which is exactly the silent EOF this bridge exists to remove. */
17933
- buffered: (size) => slot.active ? slot.active.buffered(size) : EMPTY,
17934
- read: (size) => slot.active ? slot.active.read(size) : Promise.resolve(EMPTY),
17935
- isatty: () => slot.active?.isatty() ?? false
18448
+ buffered: (size) => slot.current ? slot.current.stdin.buffered(size) : EMPTY,
18449
+ read: (size) => slot.current ? slot.current.stdin.read(size) : Promise.resolve(EMPTY),
18450
+ isatty: () => slot.current?.stdin.isatty() ?? false,
18451
+ proc_start: (argv, cwd, env2, merge) => {
18452
+ const current = slot.current;
18453
+ if (!current) throw new Error("no program is bound");
18454
+ const { ctx } = current;
18455
+ const stdin = new Pipe();
18456
+ stdin.interactive = true;
18457
+ const stdout = new Pipe();
18458
+ const stderr = merge ? stdout : new Pipe();
18459
+ const proc = ctx.kernel.spawn(argv, {
18460
+ cwd: cwd ?? ctx.cwd,
18461
+ env: env2 ?? ctx.env,
18462
+ cred: ctx.cred,
18463
+ stdin,
18464
+ stdout,
18465
+ stderr
18466
+ });
18467
+ void proc.wait().then(() => {
18468
+ stdout.end();
18469
+ if (stderr !== stdout) stderr.end();
18470
+ });
18471
+ const id = ++lastChildId;
18472
+ children.set(id, { proc, stdin, stdout, stderr });
18473
+ return id;
18474
+ },
18475
+ proc_read: (id, which2, size) => {
18476
+ const child = children.get(id);
18477
+ if (!child) return Promise.resolve(EMPTY);
18478
+ const pipe = which2 === 2 ? child.stderr : child.stdout;
18479
+ return resuming(py, slot, async () => await pipe.read(size) ?? EMPTY);
18480
+ },
18481
+ proc_write: (id, text2) => {
18482
+ children.get(id)?.stdin.write(text2);
18483
+ },
18484
+ proc_close_stdin: (id) => {
18485
+ children.get(id)?.stdin.end();
18486
+ },
18487
+ /* Non-blocking on purpose: `poll()` must be able to say "still running". */
18488
+ proc_poll: (id) => children.get(id)?.proc.exitCode ?? void 0,
18489
+ proc_pid: (id) => children.get(id)?.proc.pid ?? 0,
18490
+ proc_wait: (id) => {
18491
+ const child = children.get(id);
18492
+ if (!child) return Promise.resolve(0);
18493
+ return resuming(py, slot, () => child.proc.wait());
18494
+ },
18495
+ proc_kill: (id, signal) => {
18496
+ const child = children.get(id);
18497
+ if (!child) return;
18498
+ const current = slot.current;
18499
+ current?.ctx.kernel.procs.signal(child.proc.pid, signal);
18500
+ },
18501
+ proc_release: (id) => {
18502
+ children.delete(id);
18503
+ },
18504
+ http: (url, method, headers, body) => resuming(py, slot, () => httpForPython(slot, url, method, headers, body)),
18505
+ /* So Python can enforce the container's policy even on a host where it
18506
+ * cannot route the request itself. */
18507
+ policy_allows: (url) => {
18508
+ const current = slot.current;
18509
+ if (!current) return false;
18510
+ const net = current.ctx.kernel.net;
18511
+ try {
18512
+ return net.isLocal(new URL(url).hostname) || net.outboundAllowed(url);
18513
+ } catch {
18514
+ return false;
18515
+ }
18516
+ }
17936
18517
  });
17937
18518
  py.runPython(BRIDGE_PY);
18519
+ py.runPython(PROCESS_PY);
18520
+ py.runPython(NETWORK_PY);
18521
+ py.runPython(ASYNCIO_PY);
17938
18522
  }
17939
- function bindStdin(py, host2) {
17940
- py.__sbxStdin.active = host2;
18523
+ function bindProgram(py, binding) {
18524
+ py.__sbxSlot.current = binding;
18525
+ const { ctx, stdin } = binding;
18526
+ const decoder8 = new TextDecoder();
18527
+ py.setStdout({
18528
+ write: (buffer) => (ctx.write(decoder8.decode(buffer)), buffer.length)
18529
+ });
18530
+ py.setStderr({
18531
+ write: (buffer) => (ctx.stderr.write(decoder8.decode(buffer)), buffer.length)
18532
+ });
18533
+ py.setStdin({
18534
+ read: (buffer) => {
18535
+ const ready = stdin.buffered(buffer.length);
18536
+ if (!ready || ready.length === 0) return 0;
18537
+ buffer.set(ready);
18538
+ return ready.length;
18539
+ }
18540
+ });
17941
18541
  }
17942
18542
 
17943
18543
  // src/runtime/cpython.ts
@@ -18128,30 +18728,9 @@ async function runCPythonProgram(ctx, source, argv, scriptDir = null) {
18128
18728
  });
18129
18729
  } catch {
18130
18730
  }
18131
- const decoder8 = new TextDecoder();
18132
- py.setStdout({
18133
- write: (buffer) => {
18134
- ctx.write(decoder8.decode(buffer));
18135
- return buffer.length;
18136
- }
18137
- });
18138
- py.setStderr({
18139
- write: (buffer) => {
18140
- ctx.stderr.write(decoder8.decode(buffer));
18141
- return buffer.length;
18142
- }
18143
- });
18144
18731
  const stdinHost = createStdinHost(ctx);
18145
18732
  await stdinHost.prime();
18146
- bindStdin(py, stdinHost);
18147
- py.setStdin({
18148
- read: (buffer) => {
18149
- const ready = stdinHost.buffered(buffer.length);
18150
- if (!ready || ready.length === 0) return 0;
18151
- buffer.set(ready);
18152
- return ready.length;
18153
- }
18154
- });
18733
+ bindProgram(py, { ctx, stdin: stdinHost });
18155
18734
  try {
18156
18735
  bootstrap(py, ctx, argv, scriptDir);
18157
18736
  } catch (error) {
@@ -18190,9 +18769,7 @@ async function runCPythonRepl(ctx) {
18190
18769
  }
18191
18770
  mountContainerDirs(py, ctx);
18192
18771
  bootstrap(py, ctx, [""], null);
18193
- const decoder8 = new TextDecoder();
18194
- py.setStdout({ write: (data) => (ctx.write(decoder8.decode(data)), data.length) });
18195
- py.setStderr({ write: (data) => (ctx.stderr.write(decoder8.decode(data)), data.length) });
18772
+ bindProgram(py, { ctx, stdin: createStdinHost(ctx) });
18196
18773
  ctx.line(`Python ${String(py.runPython("import sys; sys.version.split()[0]"))} (Pyodide)`);
18197
18774
  ctx.line('Type "help()" for more information.');
18198
18775
  let source = "";
@@ -18215,6 +18792,67 @@ async function runCPythonRepl(ctx) {
18215
18792
  }
18216
18793
  return 0;
18217
18794
  }
18795
+ var PIP_PY = `
18796
+ import json, re
18797
+
18798
+ import micropip
18799
+
18800
+ _MISSING = re.compile(r"Can't find a pure Python 3 wheel for '([^']+)'")
18801
+ _EXTRAS = re.compile(r"^\\s*([A-Za-z0-9._-]+)\\s*\\[([^\\]]+)\\](.*)$")
18802
+
18803
+
18804
+ def _name_of(spec):
18805
+ return re.split(r"[\\[<>=!~;\\s]", spec.strip(), 1)[0]
18806
+
18807
+
18808
+ def _installed_version(name):
18809
+ try:
18810
+ found = micropip.list()[name.replace("_", "-").lower()]
18811
+ return getattr(found, "version", None)
18812
+ except Exception:
18813
+ return None
18814
+
18815
+
18816
+ async def _install_one(spec):
18817
+ """Install one requirement, retrying without extras if they are impossible."""
18818
+ attempt = spec
18819
+ dropped = None
18820
+ while True:
18821
+ try:
18822
+ await micropip.install(attempt)
18823
+ except ValueError as error:
18824
+ message = str(error)
18825
+ missing = _MISSING.findall(message)
18826
+ extras = _EXTRAS.match(attempt)
18827
+ if missing and extras and dropped is None:
18828
+ dropped = [e.strip() for e in extras.group(2).split(",")]
18829
+ attempt = extras.group(1) + extras.group(3)
18830
+ continue
18831
+ return {
18832
+ "spec": spec, "ok": False, "missing": missing,
18833
+ "reason": message.strip().split("\\n")[0],
18834
+ }
18835
+ except Exception as error:
18836
+ return {"spec": spec, "ok": False, "missing": [], "reason": str(error).strip().split("\\n")[0]}
18837
+
18838
+ name = _name_of(attempt)
18839
+ return {
18840
+ "spec": spec, "ok": True, "name": name,
18841
+ "version": _installed_version(name), "dropped": dropped,
18842
+ }
18843
+
18844
+
18845
+ async def __sbx_pip(specs_json):
18846
+ results = []
18847
+ for spec in json.loads(specs_json):
18848
+ try:
18849
+ results.append(await _install_one(spec))
18850
+ except Exception as error:
18851
+ results.append({"spec": spec, "ok": False, "missing": [], "reason": str(error)})
18852
+ return json.dumps(results)
18853
+
18854
+ __sbx_pip
18855
+ `;
18218
18856
  var micropip = defineCommand({
18219
18857
  name: "micropip",
18220
18858
  path: "/usr/bin/micropip",
@@ -18252,6 +18890,12 @@ var micropip = defineCommand({
18252
18890
  ctx.stderr.write("pip: no packages specified\n");
18253
18891
  return 1;
18254
18892
  }
18893
+ if (!ctx.kernel.net.outboundAllowed("https://pypi.org")) {
18894
+ ctx.stderr.write(
18895
+ "pip: outbound network access is disabled for this container (enable with network: { allowOutbound: true })\n"
18896
+ );
18897
+ return 1;
18898
+ }
18255
18899
  let py;
18256
18900
  try {
18257
18901
  py = await interpreterFor(ctx);
@@ -18259,22 +18903,42 @@ var micropip = defineCommand({
18259
18903
  ctx.stderr.write("pip: Pyodide is unavailable in this host\n");
18260
18904
  return 127;
18261
18905
  }
18906
+ let outcomes;
18262
18907
  try {
18263
18908
  await py.loadPackage("micropip");
18264
- const micropipModule = py.pyimport("micropip");
18265
- for (const name of packages) {
18266
- ctx.line(`Collecting ${name}`);
18267
- await micropipModule.install(name);
18268
- ctx.line(`Successfully installed ${name}`);
18269
- }
18270
- return 0;
18909
+ for (const name of packages) ctx.line(`Collecting ${name}`);
18910
+ const install2 = py.runPython(PIP_PY);
18911
+ outcomes = JSON.parse(String(await install2(JSON.stringify(packages))));
18271
18912
  } catch (error) {
18272
18913
  ctx.stderr.write(
18273
- `Package installation failed: ${error instanceof Error ? error.message : String(error)}
18914
+ `pip: installation failed: ${error instanceof Error ? error.message : String(error)}
18274
18915
  `
18275
18916
  );
18276
18917
  return 1;
18277
18918
  }
18919
+ const installed2 = [];
18920
+ for (const outcome of outcomes) {
18921
+ if (outcome.ok) {
18922
+ installed2.push(
18923
+ outcome.version ? `${outcome.name}-${outcome.version}` : String(outcome.name)
18924
+ );
18925
+ if (outcome.dropped?.length) {
18926
+ ctx.stderr.write(
18927
+ ` WARNING: ${outcome.name}: the ${outcome.dropped.map((extra) => `'${extra}'`).join(", ")} extra needs native code with no WebAssembly build; installed ${outcome.name} without it
18928
+ `
18929
+ );
18930
+ }
18931
+ continue;
18932
+ }
18933
+ const missing = outcome.missing ?? [];
18934
+ ctx.stderr.write(
18935
+ missing.length > 0 ? `ERROR: could not install ${outcome.spec}: no WebAssembly build exists for ${missing.map((name) => name.split(/[<>=!~;\s]/)[0]).join(", ")}
18936
+ ` : `ERROR: could not install ${outcome.spec}: ${outcome.reason ?? "unknown error"}
18937
+ `
18938
+ );
18939
+ }
18940
+ if (installed2.length > 0) ctx.line(`Successfully installed ${installed2.join(" ")}`);
18941
+ return outcomes.every((outcome) => outcome.ok) ? 0 : 1;
18278
18942
  }
18279
18943
  });
18280
18944