@nimbus-sh/worker 0.1.5 → 0.1.6
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/README.md +37 -0
- package/dist/_shared/crypto.d.ts +15 -0
- package/dist/_shared/crypto.d.ts.map +1 -0
- package/dist/_shared/crypto.js +122 -0
- package/dist/facets/manager.d.ts +24 -0
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +279 -70
- package/dist/facets/process.d.ts +11 -2
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +187 -21
- package/dist/facets/vite-dev-server.d.ts +2 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -1
- package/dist/facets/vite-dev-server.js +39 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +1 -3
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-facet.js +1 -12
- package/dist/loaders/child-process/spawn-pool.d.ts +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +1 -6
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +47 -27
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +2 -1
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +41 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +70 -35
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -21
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +29 -8
- package/dist/runtime/barrel-synthesizer.d.ts +7 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -1
- package/dist/runtime/barrel-synthesizer.js +11 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -1
- package/dist/runtime/clang-runner.js +1 -8
- package/dist/runtime/esbuild-service.d.ts +12 -5
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +28 -14
- package/dist/runtime/node-runner.d.ts +2 -0
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-shims.d.ts +4 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +404 -55
- package/dist/runtime/os-contracts.d.ts +128 -0
- package/dist/runtime/os-contracts.d.ts.map +1 -0
- package/dist/runtime/os-contracts.js +1 -0
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +32 -7
- package/dist/runtime/port-registry.d.ts +5 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +22 -1
- package/dist/runtime/pyodide-runtime-assets.d.ts +19 -0
- package/dist/runtime/pyodide-runtime-assets.d.ts.map +1 -0
- package/dist/runtime/pyodide-runtime-assets.js +44 -0
- package/dist/runtime/python-repl.d.ts +3 -1
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +11 -29
- package/dist/runtime/python-runner.d.ts +8 -9
- package/dist/runtime/python-runner.d.ts.map +1 -1
- package/dist/runtime/python-runner.js +897 -61
- package/dist/runtime/python-socket-shim.d.ts +11 -0
- package/dist/runtime/python-socket-shim.d.ts.map +1 -0
- package/dist/runtime/python-socket-shim.js +526 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +2 -11
- package/dist/runtime/ruby-gems.d.ts +25 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +582 -0
- package/dist/runtime/ruby-runner.d.ts +2 -4
- package/dist/runtime/ruby-runner.d.ts.map +1 -1
- package/dist/runtime/ruby-runner.js +335 -34
- package/dist/runtime/runtime-catalog.d.ts +16 -4
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +21 -10
- package/dist/runtime/runtime-registry.d.ts +3 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +4 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +43 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +224 -0
- package/dist/runtime/static-server.d.ts +3 -0
- package/dist/runtime/static-server.d.ts.map +1 -0
- package/dist/runtime/static-server.js +121 -0
- package/dist/runtime/vfs-snapshot.d.ts +57 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
- package/dist/runtime/vfs-snapshot.js +141 -0
- package/dist/runtime/virtual-socket-kernel.d.ts +16 -0
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.js +360 -0
- package/dist/runtime/wasi-instance.d.ts +6 -3
- package/dist/runtime/wasi-instance.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.d.ts +1 -25
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +1 -160
- package/dist/session/agent.d.ts +43 -0
- package/dist/session/agent.d.ts.map +1 -0
- package/dist/session/agent.js +1252 -0
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +15 -16
- package/dist/session/nimbus-session.d.ts +13 -2
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +55 -7
- package/dist/session/programmatic.d.ts +10 -0
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +121 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +19 -1
- package/dist/session/rpc.d.ts +11 -0
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +56 -43
- package/dist/session/supervisor-rpc.d.ts +15 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +32 -0
- package/dist/session/ws.d.ts +4 -3
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +11 -1
- package/dist/shell/unix-commands.js +12 -16
- package/dist/vfs/path.d.ts +8 -4
- package/dist/vfs/path.d.ts.map +1 -1
- package/dist/vfs/path.js +16 -4
- package/dist/vfs/sqlite-vfs.d.ts +2 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +11 -0
- package/dist/vfs/symlink-registry.d.ts +7 -1
- package/dist/vfs/symlink-registry.d.ts.map +1 -1
- package/dist/vfs/symlink-registry.js +16 -11
- package/package.json +4 -1
- package/public/index.html +240 -0
- package/public/s/index.html +1172 -146
- package/runtime-contracts/pyodide-workerd-adapter.json +18 -0
- package/scripts/bundle-runtime.mjs +99 -24
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Out of v1:
|
|
13
13
|
* - REPL mode (`ruby` with no args)
|
|
14
|
-
* -
|
|
15
|
-
* stdlib works since stdlib is wasi-vfs-packed inside the wasm)
|
|
16
|
-
* - gems, bundler
|
|
14
|
+
* - native extension gems
|
|
17
15
|
* - js-runtime bindings (gem "js") — those imports are stubbed
|
|
18
16
|
* and will raise NotImplementedError if Ruby code tries to use
|
|
19
17
|
* them
|
|
@@ -57,5 +55,5 @@ export declare function makeRubyRunnerFactory(deps: {
|
|
|
57
55
|
* stderr capture buffers, and invokes rb-eval-string-protect with a
|
|
58
56
|
* wrapper that captures SystemExit to extract the exit code.
|
|
59
57
|
*/
|
|
60
|
-
export declare const RUBY_RUNNER_PREAMBLE_TAIL = "\n// \u2500\u2500 BEGIN: ruby-runner preamble (Ruby 3.3.4, Nimbus v1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Capture buffers shared across the bootstrap and per-call paths. The\n// preamble's WASI imports route fd_write stdout/stderr into these via\n// __wasiMakeImports({stdoutWrite, stderrWrite}). Per-call __rubyRun\n// slices from these to isolate output per invocation.\nglobalThis.__nimbusRubyStdout = globalThis.__nimbusRubyStdout || [];\nglobalThis.__nimbusRubyStderr = globalThis.__nimbusRubyStderr || [];\n\n// \u2500\u2500 Canonical-ABI resource Slab \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\n// Pyodide-style minimal resource manager. Ruby's rb-abi-guest.js uses\n// these 4 functions for resource_drop / resource_new / resource_get /\n// resource_clone, but the wasm itself only imports 3:\n// resource_drop_js-abi-value, resource_new_rb-abi-value, resource_get_rb-abi-value\nclass __NimbusRubySlab {\n constructor() { this._map = new Map(); this._next = 1; }\n insert(obj) { const id = this._next++; this._map.set(id, obj); return id; }\n get(id) { return this._map.get(id); }\n remove(id) { const v = this._map.get(id); this._map.delete(id); return v; }\n}\n\n// \u2500\u2500 Bootstrap promise: runs at child-facet module-init time \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Mirrors pyodide v2's __pyodideBootstrap pattern. The synchronous\n// portion (WebAssembly.instantiate + _initialize + ruby-init-loadpath\n// + ruby-init) all completes before the first await \u2014 so it executes\n// in module-init CSP context where workerd permits wasm code-gen\n// from the LOADER-provided Module.\nglobalThis.__rubyBootstrap = (async function nimbusRubyBootstrap() {\n const wasmTable = globalThis.__NIMBUS_WASM || {};\n const rubyMod = wasmTable['ruby+stdlib.wasm'];\n if (!rubyMod) {\n return { ok: false, error: '__NIMBUS_WASM missing ruby+stdlib.wasm' };\n }\n\n // WASI init \u2014 empty preopens initially. Per-call __rubyRun can mount\n // a cwd preopen if needed (for ruby <file.rb> reading via WASI).\n // For v1 (-e mode) we just need stdout/stderr capture + a minimal\n // FS so Ruby's stdlib init (which probes /tmp + $HOME) doesn't crash.\n __wasiInitFS({\n root: '',\n preopens: [\n // Preopen / so Ruby can resolve all FS paths through WASI.\n // Ruby's __wasi_vfs_rt_init mounts its packed stdlib under /usr\n // inside the wasm's internal VFS \u2014 these preopens are for the\n // OUTER (host-visible) FS that wasi_snapshot_preview1 exposes.\n { wasiPath: '/', vfsPath: '' },\n { wasiPath: '/tmp', vfsPath: 'tmp' },\n { wasiPath: '/home', vfsPath: 'home' },\n ],\n files: {},\n dirs: ['tmp', 'home'],\n });\n\n // Initial argv/env (bootstrap defaults). Per-call __rubyRun re-\n // initializes WASI with the actual user argv/env before evaluating\n // user code.\n let memRef = null;\n const wasi = __wasiMakeImports({\n argv: ['ruby'],\n env: { HOME: '/home/ruby', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' },\n getMemory: () => memRef,\n stdoutWrite: (s) => { globalThis.__nimbusRubyStdout.push(s); },\n stderrWrite: (s) => { globalThis.__nimbusRubyStderr.push(s); },\n });\n\n // canonical_abi imports \u2014 3 resource lifecycle fns. The Slab is\n // shared across the lifetime of the facet (single call, then the\n // facet is reaped).\n const rbValueSlab = new __NimbusRubySlab();\n const jsValueSlab = new __NimbusRubySlab();\n const canonical_abi = {\n 'resource_drop_js-abi-value': (i) => { jsValueSlab.remove(i); },\n 'resource_new_rb-abi-value': (i) => rbValueSlab.insert({ _wasm_val: i }),\n 'resource_get_rb-abi-value': (i) => {\n const r = rbValueSlab.get(i);\n return r ? r._wasm_val : 0;\n },\n };\n\n // rb-js-abi-host imports \u2014 21 fns. Ruby only calls these when user\n // code interacts with JS (gem \"js\"). For v1 (puts \"hi\") none fire.\n // We stub each to throw a clear error so any future user code that\n // needs JS interop gets a recognizable diagnostic instead of a\n // wasm trap.\n const _stub = (name) => () => { throw new Error('ruby-runner v1: rb-js-abi-host.' + name + ' not implemented (gem \"js\" not supported)'); };\n const rb_js_abi_host = {\n rb_wasm_throw_prohibit_rewind_exception: () => {\n // This one CAN fire from Ruby internals (Fiber rewind guard).\n // Make it a no-op so Ruby's continuation machinery proceeds.\n },\n 'eval-js: func(code: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('eval-js'),\n 'is-js: func(value: handle<js-abi-value>) -> bool': _stub('is-js'),\n 'instance-of: func(value: handle<js-abi-value>, klass: handle<js-abi-value>) -> bool': _stub('instance-of'),\n 'global-this: func() -> handle<js-abi-value>': _stub('global-this'),\n 'int-to-js-number: func(value: s32) -> handle<js-abi-value>': _stub('int-to-js-number'),\n 'float-to-js-number: func(value: float64) -> handle<js-abi-value>': _stub('float-to-js-number'),\n 'string-to-js-string: func(value: string) -> handle<js-abi-value>': _stub('string-to-js-string'),\n 'bool-to-js-bool: func(value: bool) -> handle<js-abi-value>': _stub('bool-to-js-bool'),\n 'proc-to-js-function: func(value: u32) -> handle<js-abi-value>': _stub('proc-to-js-function'),\n 'rb-object-to-js-rb-value: func(raw-rb-abi-value: u32) -> handle<js-abi-value>': _stub('rb-object-to-js-rb-value'),\n 'js-value-to-string: func(value: handle<js-abi-value>) -> string': _stub('js-value-to-string'),\n 'js-value-to-integer: func(value: handle<js-abi-value>) -> variant { as-float(float64), bignum(string) }': _stub('js-value-to-integer'),\n 'export-js-value-to-host: func(value: handle<js-abi-value>) -> ()': _stub('export-js-value-to-host'),\n 'import-js-value-from-host: func() -> handle<js-abi-value>': _stub('import-js-value-from-host'),\n 'js-value-typeof: func(value: handle<js-abi-value>) -> string': _stub('js-value-typeof'),\n 'js-value-equal: func(lhs: handle<js-abi-value>, rhs: handle<js-abi-value>) -> bool': _stub('js-value-equal'),\n 'js-value-strictly-equal: func(lhs: handle<js-abi-value>, rhs: handle<js-abi-value>) -> bool': _stub('js-value-strictly-equal'),\n 'reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-apply'),\n 'reflect-get: func(target: handle<js-abi-value>, property-key: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-get'),\n 'reflect-set: func(target: handle<js-abi-value>, property-key: string, value: handle<js-abi-value>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-set'),\n };\n\n const imports = {\n wasi_snapshot_preview1: wasi.wasiImport,\n canonical_abi,\n 'rb-js-abi-host': rb_js_abi_host,\n };\n\n let instance;\n try {\n const result = await WebAssembly.instantiate(rubyMod, imports);\n instance = (result instanceof WebAssembly.Instance ? result : result.instance);\n } catch (e) {\n return { ok: false, error: 'WebAssembly.instantiate failed: ' + (e && e.message), stack: e && e.stack };\n }\n memRef = instance.exports.memory;\n\n // \u2500\u2500 Ruby bootstrap sequence \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\n // Order matters (per ruby.wasm DefaultRubyVM):\n // 1. _initialize (reactor entry; runs static initializers)\n // 2. __wasi_vfs_rt_init (mount packed stdlib at the wasi-vfs's\n // internal FS \u2014 needed for require to find Ruby's *.rb files)\n // 3. ruby-init([progName]) \u2014 initialize VM with argv[0]\n // 4. ruby-init-loadpath() \u2014 set $LOAD_PATH from packed stdlib\n try {\n if (typeof instance.exports._initialize === 'function') {\n instance.exports._initialize();\n }\n if (typeof instance.exports.__wasi_vfs_rt_init === 'function') {\n instance.exports.__wasi_vfs_rt_init();\n }\n } catch (e) {\n return { ok: false, error: '_initialize/wasi_vfs_rt_init failed: ' + (e && e.message), stack: e && e.stack };\n }\n\n // Locate the canonical Ruby ABI exports. Names embed the WIT\n // signature literal (e.g. 'ruby-init: func(args: list<string>) -> ()')\n // because rb-abi-guest is wit-bindgen-generated.\n const rubyInit = instance.exports['ruby-init: func(args: list<string>) -> ()'];\n const rubyInitLoadpath = instance.exports['ruby-init-loadpath: func() -> ()'];\n const rbEvalStringProtect = instance.exports['rb-eval-string-protect: func(str: string) -> tuple<handle<rb-abi-value>, s32>'];\n const rubyShowVersion = instance.exports['ruby-show-version: func() -> ()'];\n const cabiRealloc = instance.exports.cabi_realloc;\n if (!rubyInit || !rubyInitLoadpath || !rbEvalStringProtect || !cabiRealloc) {\n return { ok: false, error: 'Required Ruby ABI exports missing (ruby-init/init-loadpath/eval-string-protect/cabi_realloc)' };\n }\n\n // Encode a list<string> argument for ruby-init. WIT canonical-ABI\n // shape: caller allocates list buffer; each element is (ptr, len).\n // Strings are UTF-8 encoded into separately-allocated buffers.\n function writeListString(strings) {\n const memory = instance.exports.memory;\n const enc = new TextEncoder();\n const len = strings.length;\n const listBufPtr = cabiRealloc(0, 0, 4, len * 8); // align=4, size=len*8\n const encoded = strings.map((s) => enc.encode(s));\n for (let i = 0; i < len; i++) {\n const bytes = encoded[i];\n const strPtr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, strPtr);\n const dv = new DataView(memory.buffer);\n dv.setUint32(listBufPtr + i * 8 + 0, strPtr, true);\n dv.setUint32(listBufPtr + i * 8 + 4, bytes.length, true);\n }\n return { ptr: listBufPtr, len };\n }\n\n function writeString(s) {\n const memory = instance.exports.memory;\n const enc = new TextEncoder();\n const bytes = enc.encode(s);\n const ptr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, ptr);\n return { ptr, len: bytes.length };\n }\n\n // NOTE: We DO NOT call ruby-init or ruby-init-loadpath here. Both\n // invoke CPython-like random-seed initialization (random_get via\n // wasi_snapshot_preview1.random_get), which workerd blocks in the\n // global-scope (module-init) context. Same constraint that bit us\n // for Pyodide v2 P21. The per-call __rubyRun runs them at request-\n // handler time where crypto.getRandomValues is permitted.\n //\n // _initialize and __wasi_vfs_rt_init are safe at module-init because\n // they only do static initialization (no entropy reads).\n\n return {\n ok: true,\n instance,\n wasi,\n rubyInit,\n rubyInitLoadpath,\n rbEvalStringProtect,\n rubyShowVersion,\n writeListString,\n writeString,\n rubyInitialized: false, // mutated to true by __rubyRun on first call\n };\n})();\n\n// \u2500\u2500 Per-call entry point \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\u2500\n//\n// Invoked from the LOADER child facet's execute() (which calls the\n// serialized facetFn that does globalThis.__rubyRun(args)).\n//\n// At this point the bootstrap promise has resolved (since it's\n// awaited inside the child facet's module-init context \u2014 the\n// instantiate finishes before the request handler runs). We:\n// 1. Update Ruby's $0 / $PROGRAM_NAME / ARGV via rb-eval-string-protect\n// 2. Wrap the user code in a begin/rescue SystemExit/StandardError\n// handler so we can extract exit code without losing stdout\n// 3. Read stdout/stderr buffers and slice from the per-call start\nglobalThis.__rubyRun = async function __rubyRun(args) {\n const stdoutStart = globalThis.__nimbusRubyStdout.length;\n const stderrStart = globalThis.__nimbusRubyStderr.length;\n\n const boot = await globalThis.__rubyBootstrap;\n if (!boot.ok) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby bootstrap failed: ' + (boot.error || 'unknown') + (boot.stack ? ' [stack=' + boot.stack + ']' : ''),\n };\n }\n\n // First call into __rubyRun: complete Ruby VM init (ruby-init +\n // ruby-init-loadpath) now that we're in request-handler context\n // where crypto.getRandomValues is permitted. Subsequent calls skip.\n if (!boot.rubyInitialized) {\n try {\n const initArgs = boot.writeListString(['ruby', '-e_=0']);\n boot.rubyInit(initArgs.ptr, initArgs.len);\n boot.rubyInitLoadpath();\n boot.rubyInitialized = true;\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby-init / ruby-init-loadpath failed at request time: ' + (e && e.message),\n };\n }\n }\n\n // Wrapper code: set $0/$PROGRAM_NAME/ARGV/ENV, run user code,\n // capture SystemExit. We embed the user code as a Ruby string\n // literal via JSON.stringify (Ruby's double-quote strings accept\n // \\n \\t etc which JSON also emits \u2014 safe round-trip).\n //\n // The wrapper sets __NIMBUS_RUBY_EXIT to the desired exit code so\n // we can read it via a second rb-eval-string-protect call. Failing\n // SystemExit (raise) ends up with __NIMBUS_RUBY_EXIT = 1 + stderr\n // message.\n const userCodeRb = JSON.stringify(args.userCode);\n const argvRb = JSON.stringify(args.rbArgv.slice(1)); // exclude argv[0]\n const envRb = JSON.stringify(args.userEnv || {});\n const progNameRb = JSON.stringify(args.progName);\n\n // STAGED execution: we split the prelude (stdout sync + ARGV/ENV/$0\n // setup) from the user-code eval. The prelude has no failure modes\n // we care about; user-code is wrapped in begin/rescue for SystemExit\n // and Exception. If the wrapper itself trips a syntax/runtime error,\n // we surface it via the __NIMBUS_RUBY_DIAG side channel.\n // Build env list as string-keyed Ruby hash via the rocket-syntax.\n // Ruby treats colon-style hash literals as Symbol-keyed; we need\n // String keys so ENV[k] = v works without TypeError.\n const envEntries = Object.entries(args.userEnv || {})\n .map(([k, v]) => JSON.stringify(k) + ' => ' + JSON.stringify(v))\n .join(', ');\n const envHashRb = '{' + envEntries + '}';\n\n const preludeRb = [\n // Reset exit state FIRST so partial prelude failures still\n // surface a clean exit code (previously: exit 7 left $__nimbus_exit\n // = 7 \u2192 next call's prelude could fail before resetting \u2192 second\n // exit 0 returned 7).\n '$__nimbus_exit = 0',\n '$stdout.sync = true',\n '$stderr.sync = true',\n '$0 = ' + progNameRb,\n '$PROGRAM_NAME = ' + progNameRb,\n 'ARGV.replace(' + argvRb + ')',\n envHashRb + '.each_pair { |k, v| ENV[k] = v }',\n ].join('; ');\n\n const userWrapper = [\n 'begin',\n ' ' + 'eval(' + userCodeRb + ', TOPLEVEL_BINDING, ' + progNameRb + ', 1)',\n 'rescue SystemExit => e',\n ' $__nimbus_exit = e.status',\n 'rescue Exception => e',\n ' $stderr.write(e.full_message(highlight: false, order: :top))',\n ' $__nimbus_exit = 1',\n 'ensure',\n ' $stdout.flush rescue nil',\n ' $stderr.flush rescue nil',\n 'end',\n ].join(\"\\n\");\n\n const memory = boot.instance.exports.memory;\n\n function callEvalStringProtect(rubyCode) {\n const enc = new TextEncoder();\n const bytes = enc.encode(rubyCode);\n const cabiRealloc = boot.instance.exports.cabi_realloc;\n const codePtr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, codePtr);\n const retPtr = boot.rbEvalStringProtect(codePtr, bytes.length);\n // Return is a tuple: (rb-abi-value handle u32, status s32) \u2014 8 bytes\n const dv = new DataView(memory.buffer);\n const handle = dv.getUint32(retPtr + 0, true);\n const status = dv.getInt32(retPtr + 4, true);\n return { handle, status };\n }\n\n // Stage 1: run the prelude (sync flags, ARGV, ENV, $0/$PROGRAM_NAME).\n let preludeStatus;\n try {\n preludeStatus = callEvalStringProtect(preludeRb);\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby prelude threw: ' + (e && e.message),\n };\n }\n if (preludeStatus && preludeStatus.status !== 0) {\n globalThis.__nimbusRubyStderr.push('[ruby-runner-diag] prelude returned non-zero status: ' + preludeStatus.status + '\\n');\n }\n\n // Stage 2: run user code wrapped for SystemExit/Exception capture.\n let evalStatus;\n try {\n evalStatus = callEvalStringProtect(userWrapper);\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'rb-eval-string-protect threw: ' + (e && e.message),\n };\n }\n if (evalStatus && evalStatus.status !== 0) {\n globalThis.__nimbusRubyStderr.push('[ruby-runner-diag] user wrapper returned non-zero status: ' + evalStatus.status + '\\n');\n }\n\n // Read $__nimbus_exit by evaluating it and inspecting the result\n // via Ruby's puts (captured into stderr). Simpler: call exit through\n // rb-eval-string-protect and parse the status code from there.\n //\n // Actually rbEvalStringProtect already returns a status (second\n // tuple element). When the wrapper completes normally, status is 0\n // and we read $__nimbus_exit. We use a follow-up eval that prints\n // the exit code to a sentinel marker on stderr we can scrape, but\n // a cleaner approach: have the wrapper's last expression BE the\n // exit code (so it's the return value of eval).\n //\n // Re-do: eval $__nimbus_exit and read it from a marker line we\n // emit to a side channel \u2014 easiest is to call $stdout via a unique\n // marker that we then strip from output.\n const NIMBUS_EXIT_MARKER = '__NIMBUS_RUBY_EXIT_';\n let exitCode = 0;\n try {\n // Print the marker + exit code to stderr (a side channel separate\n // from user-visible stdout). We strip it before returning.\n callEvalStringProtect(\n '$stderr.write(' + JSON.stringify(NIMBUS_EXIT_MARKER) + ' + $__nimbus_exit.to_s + \"\\\\n\")'\n );\n // Scrape the marker from stderr buffer \u2014 using ONLY this call's\n // slice (from stderrStart). The same facet can be reused across\n // multiple __rubyRun invocations (loader-pool dedup by tag), so\n // a previous call's marker would otherwise be matched first.\n const callStderr = globalThis.__nimbusRubyStderr.slice(stderrStart).join('');\n // Match the LAST marker in this slice (the one our just-completed\n // call emitted; if the user wrapper also emitted writes, the\n // marker is appended after them).\n const markerRe = new RegExp(NIMBUS_EXIT_MARKER + '(-?\\\\d+)', 'g');\n let lastMatch = null;\n let mit;\n while ((mit = markerRe.exec(callStderr)) !== null) lastMatch = mit;\n if (lastMatch) exitCode = parseInt(lastMatch[1], 10);\n } catch (e) {\n // Failure to read exit code \u2192 assume 0 if no errors observed.\n exitCode = 0;\n }\n\n // Slice + scrub the marker from stderr output before returning.\n const stdoutOut = globalThis.__nimbusRubyStdout.slice(stdoutStart).join('');\n let stderrOut = globalThis.__nimbusRubyStderr.slice(stderrStart).join('');\n stderrOut = stderrOut.replace(new RegExp(NIMBUS_EXIT_MARKER + '-?\\\\d+\\\\n?', 'g'), '');\n\n return {\n exitCode: exitCode,\n stdout: stdoutOut,\n stderr: stderrOut,\n };\n};\n\n// \u2500\u2500 END: ruby-runner preamble \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\n";
|
|
58
|
+
export declare const RUBY_RUNNER_PREAMBLE_TAIL = "\n// \u2500\u2500 BEGIN: ruby-runner preamble (Ruby 3.3.4, Nimbus v1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Capture buffers shared across the bootstrap and per-call paths. The\n// preamble's WASI imports route fd_write stdout/stderr into these via\n// __wasiMakeImports({stdoutWrite, stderrWrite}). Per-call __rubyRun\n// slices from these to isolate output per invocation.\nglobalThis.__nimbusRubyStdout = globalThis.__nimbusRubyStdout || [];\nglobalThis.__nimbusRubyStderr = globalThis.__nimbusRubyStderr || [];\n\nfunction __nimbusInstallRubyFsSnapshot(snapshot) {\n const dirs = new Set(['tmp', 'home']);\n const files = {};\n for (const dir of (snapshot && snapshot.dirs) || []) dirs.add(String(dir).replace(/^\\/+/, '').replace(/\\/+$/, ''));\n for (const [path, b64] of Object.entries((snapshot && snapshot.files) || {})) {\n files[String(path).replace(/^\\/+/, '')] = b64;\n }\n __wasiInitFS({\n root: '',\n preopens: [\n { wasiPath: '/', vfsPath: '' },\n { wasiPath: '/tmp', vfsPath: 'tmp' },\n { wasiPath: '/home', vfsPath: 'home' },\n ],\n files,\n dirs: Array.from(dirs).filter(Boolean),\n });\n}\n\n// \u2500\u2500 Canonical-ABI resource Slab \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\n// Pyodide-style minimal resource manager. Ruby's rb-abi-guest.js uses\n// these 4 functions for resource_drop / resource_new / resource_get /\n// resource_clone, but the wasm itself only imports 3:\n// resource_drop_js-abi-value, resource_new_rb-abi-value, resource_get_rb-abi-value\nclass __NimbusRubySlab {\n constructor() { this._map = new Map(); this._next = 1; }\n insert(obj) { const id = this._next++; this._map.set(id, obj); return id; }\n get(id) { return this._map.get(id); }\n remove(id) { const v = this._map.get(id); this._map.delete(id); return v; }\n}\n\n// \u2500\u2500 Bootstrap promise: runs at child-facet module-init time \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Mirrors pyodide v2's __pyodideBootstrap pattern. The synchronous\n// portion (WebAssembly.instantiate + _initialize + ruby-init-loadpath\n// + ruby-init) all completes before the first await \u2014 so it executes\n// in module-init CSP context where workerd permits wasm code-gen\n// from the LOADER-provided Module.\nglobalThis.__rubyBootstrap = (async function nimbusRubyBootstrap() {\n const wasmTable = globalThis.__NIMBUS_WASM || {};\n const rubyMod = wasmTable['ruby+stdlib.wasm'];\n if (!rubyMod) {\n return { ok: false, error: '__NIMBUS_WASM missing ruby+stdlib.wasm' };\n }\n\n // WASI init \u2014 empty preopens initially. Per-call __rubyRun can mount\n // a cwd preopen if needed (for ruby <file.rb> reading via WASI).\n // For v1 (-e mode) we just need stdout/stderr capture + a minimal\n // FS so Ruby's stdlib init (which probes /tmp + $HOME) doesn't crash.\n __wasiInitFS({\n root: '',\n preopens: [\n // Preopen / so Ruby can resolve all FS paths through WASI.\n // Ruby's __wasi_vfs_rt_init mounts its packed stdlib under /usr\n // inside the wasm's internal VFS \u2014 these preopens are for the\n // OUTER (host-visible) FS that wasi_snapshot_preview1 exposes.\n { wasiPath: '/', vfsPath: '' },\n { wasiPath: '/tmp', vfsPath: 'tmp' },\n { wasiPath: '/home', vfsPath: 'home' },\n ],\n files: {},\n dirs: ['tmp', 'home'],\n });\n\n // Initial argv/env (bootstrap defaults). Per-call __rubyRun re-\n // initializes WASI with the actual user argv/env before evaluating\n // user code.\n let memRef = null;\n const wasi = __wasiMakeImports({\n argv: ['ruby'],\n env: { HOME: '/home/ruby', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' },\n getMemory: () => memRef,\n stdoutWrite: (s) => { globalThis.__nimbusRubyStdout.push(s); },\n stderrWrite: (s) => { globalThis.__nimbusRubyStderr.push(s); },\n });\n\n // canonical_abi imports \u2014 3 resource lifecycle fns. The Slab is\n // shared across the lifetime of the facet (single call, then the\n // facet is reaped).\n const rbValueSlab = new __NimbusRubySlab();\n const jsValueSlab = new __NimbusRubySlab();\n const canonical_abi = {\n 'resource_drop_js-abi-value': (i) => { jsValueSlab.remove(i); },\n 'resource_new_rb-abi-value': (i) => rbValueSlab.insert({ _wasm_val: i }),\n 'resource_get_rb-abi-value': (i) => {\n const r = rbValueSlab.get(i);\n return r ? r._wasm_val : 0;\n },\n };\n\n // rb-js-abi-host imports \u2014 21 fns. Ruby only calls these when user\n // code interacts with JS (gem \"js\"). For v1 (puts \"hi\") none fire.\n // We stub each to throw a clear error so any future user code that\n // needs JS interop gets a recognizable diagnostic instead of a\n // wasm trap.\n const _stub = (name) => () => { throw new Error('ruby-runner v1: rb-js-abi-host.' + name + ' not implemented (gem \"js\" not supported)'); };\n const rb_js_abi_host = {\n rb_wasm_throw_prohibit_rewind_exception: () => {\n // This one CAN fire from Ruby internals (Fiber rewind guard).\n // Make it a no-op so Ruby's continuation machinery proceeds.\n },\n 'eval-js: func(code: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('eval-js'),\n 'is-js: func(value: handle<js-abi-value>) -> bool': _stub('is-js'),\n 'instance-of: func(value: handle<js-abi-value>, klass: handle<js-abi-value>) -> bool': _stub('instance-of'),\n 'global-this: func() -> handle<js-abi-value>': _stub('global-this'),\n 'int-to-js-number: func(value: s32) -> handle<js-abi-value>': _stub('int-to-js-number'),\n 'float-to-js-number: func(value: float64) -> handle<js-abi-value>': _stub('float-to-js-number'),\n 'string-to-js-string: func(value: string) -> handle<js-abi-value>': _stub('string-to-js-string'),\n 'bool-to-js-bool: func(value: bool) -> handle<js-abi-value>': _stub('bool-to-js-bool'),\n 'proc-to-js-function: func(value: u32) -> handle<js-abi-value>': _stub('proc-to-js-function'),\n 'rb-object-to-js-rb-value: func(raw-rb-abi-value: u32) -> handle<js-abi-value>': _stub('rb-object-to-js-rb-value'),\n 'js-value-to-string: func(value: handle<js-abi-value>) -> string': _stub('js-value-to-string'),\n 'js-value-to-integer: func(value: handle<js-abi-value>) -> variant { as-float(float64), bignum(string) }': _stub('js-value-to-integer'),\n 'export-js-value-to-host: func(value: handle<js-abi-value>) -> ()': _stub('export-js-value-to-host'),\n 'import-js-value-from-host: func() -> handle<js-abi-value>': _stub('import-js-value-from-host'),\n 'js-value-typeof: func(value: handle<js-abi-value>) -> string': _stub('js-value-typeof'),\n 'js-value-equal: func(lhs: handle<js-abi-value>, rhs: handle<js-abi-value>) -> bool': _stub('js-value-equal'),\n 'js-value-strictly-equal: func(lhs: handle<js-abi-value>, rhs: handle<js-abi-value>) -> bool': _stub('js-value-strictly-equal'),\n 'reflect-apply: func(target: handle<js-abi-value>, this-argument: handle<js-abi-value>, arguments: list<handle<js-abi-value>>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-apply'),\n 'reflect-get: func(target: handle<js-abi-value>, property-key: string) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-get'),\n 'reflect-set: func(target: handle<js-abi-value>, property-key: string, value: handle<js-abi-value>) -> variant { success(handle<js-abi-value>), failure(handle<js-abi-value>) }': _stub('reflect-set'),\n };\n\n const imports = {\n wasi_snapshot_preview1: wasi.wasiImport,\n canonical_abi,\n 'rb-js-abi-host': rb_js_abi_host,\n };\n\n let instance;\n try {\n const result = await WebAssembly.instantiate(rubyMod, imports);\n instance = (result instanceof WebAssembly.Instance ? result : result.instance);\n } catch (e) {\n return { ok: false, error: 'WebAssembly.instantiate failed: ' + (e && e.message), stack: e && e.stack };\n }\n memRef = instance.exports.memory;\n\n // \u2500\u2500 Ruby bootstrap sequence \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\n // Order matters (per ruby.wasm DefaultRubyVM):\n // 1. _initialize (reactor entry; runs static initializers)\n // 2. __wasi_vfs_rt_init (mount packed stdlib at the wasi-vfs's\n // internal FS \u2014 needed for require to find Ruby's *.rb files)\n // 3. ruby-init([progName]) \u2014 initialize VM with argv[0]\n // 4. ruby-init-loadpath() \u2014 set $LOAD_PATH from packed stdlib\n try {\n if (typeof instance.exports._initialize === 'function') {\n instance.exports._initialize();\n }\n if (typeof instance.exports.__wasi_vfs_rt_init === 'function') {\n instance.exports.__wasi_vfs_rt_init();\n }\n } catch (e) {\n return { ok: false, error: '_initialize/wasi_vfs_rt_init failed: ' + (e && e.message), stack: e && e.stack };\n }\n\n // Locate the canonical Ruby ABI exports. Names embed the WIT\n // signature literal (e.g. 'ruby-init: func(args: list<string>) -> ()')\n // because rb-abi-guest is wit-bindgen-generated.\n const rubyInit = instance.exports['ruby-init: func(args: list<string>) -> ()'];\n const rubyInitLoadpath = instance.exports['ruby-init-loadpath: func() -> ()'];\n const rbEvalStringProtect = instance.exports['rb-eval-string-protect: func(str: string) -> tuple<handle<rb-abi-value>, s32>'];\n const rubyShowVersion = instance.exports['ruby-show-version: func() -> ()'];\n const cabiRealloc = instance.exports.cabi_realloc;\n if (!rubyInit || !rubyInitLoadpath || !rbEvalStringProtect || !cabiRealloc) {\n return { ok: false, error: 'Required Ruby ABI exports missing (ruby-init/init-loadpath/eval-string-protect/cabi_realloc)' };\n }\n\n // Encode a list<string> argument for ruby-init. WIT canonical-ABI\n // shape: caller allocates list buffer; each element is (ptr, len).\n // Strings are UTF-8 encoded into separately-allocated buffers.\n function writeListString(strings) {\n const memory = instance.exports.memory;\n const enc = new TextEncoder();\n const len = strings.length;\n const listBufPtr = cabiRealloc(0, 0, 4, len * 8); // align=4, size=len*8\n const encoded = strings.map((s) => enc.encode(s));\n for (let i = 0; i < len; i++) {\n const bytes = encoded[i];\n const strPtr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, strPtr);\n const dv = new DataView(memory.buffer);\n dv.setUint32(listBufPtr + i * 8 + 0, strPtr, true);\n dv.setUint32(listBufPtr + i * 8 + 4, bytes.length, true);\n }\n return { ptr: listBufPtr, len };\n }\n\n function writeString(s) {\n const memory = instance.exports.memory;\n const enc = new TextEncoder();\n const bytes = enc.encode(s);\n const ptr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, ptr);\n return { ptr, len: bytes.length };\n }\n\n // NOTE: We DO NOT call ruby-init or ruby-init-loadpath here. Both\n // invoke CPython-like random-seed initialization (random_get via\n // wasi_snapshot_preview1.random_get), which workerd blocks in the\n // global-scope (module-init) context. Same constraint that bit us\n // for Pyodide v2 P21. The per-call __rubyRun runs them at request-\n // handler time where crypto.getRandomValues is permitted.\n //\n // _initialize and __wasi_vfs_rt_init are safe at module-init because\n // they only do static initialization (no entropy reads).\n\n return {\n ok: true,\n instance,\n wasi,\n rubyInit,\n rubyInitLoadpath,\n rbEvalStringProtect,\n rubyShowVersion,\n writeListString,\n writeString,\n rubyInitialized: false, // mutated to true by __rubyRun on first call\n };\n})();\n\n// \u2500\u2500 Per-call entry point \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\u2500\n//\n// Invoked from the LOADER child facet's execute() (which calls the\n// serialized facetFn that does globalThis.__rubyRun(args)).\n//\n// At this point the bootstrap promise has resolved (since it's\n// awaited inside the child facet's module-init context \u2014 the\n// instantiate finishes before the request handler runs). We:\n// 1. Update Ruby's $0 / $PROGRAM_NAME / ARGV via rb-eval-string-protect\n// 2. Wrap the user code in a begin/rescue SystemExit/StandardError\n// handler so we can extract exit code without losing stdout\n// 3. Read stdout/stderr buffers and slice from the per-call start\nglobalThis.__rubyRun = async function __rubyRun(args) {\n const stdoutStart = globalThis.__nimbusRubyStdout.length;\n const stderrStart = globalThis.__nimbusRubyStderr.length;\n\n const boot = await globalThis.__rubyBootstrap;\n if (!boot.ok) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby bootstrap failed: ' + (boot.error || 'unknown') + (boot.stack ? ' [stack=' + boot.stack + ']' : ''),\n };\n }\n\n try {\n __nimbusInstallRubyFsSnapshot(args.fsSnapshot);\n } catch (e) {\n globalThis.__nimbusRubyStderr.push('[ruby-runner] VFS mount failed: ' + (e && e.message) + '\\n');\n }\n\n // First call into __rubyRun: complete Ruby VM init (ruby-init +\n // ruby-init-loadpath) now that we're in request-handler context\n // where crypto.getRandomValues is permitted. Subsequent calls skip.\n if (!boot.rubyInitialized) {\n try {\n const initArgs = boot.writeListString(['ruby', '-e_=0']);\n boot.rubyInit(initArgs.ptr, initArgs.len);\n boot.rubyInitLoadpath();\n boot.rubyInitialized = true;\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby-init / ruby-init-loadpath failed at request time: ' + (e && e.message),\n };\n }\n }\n\n function rubyStringLiteral(value) {\n const s = String(value ?? '');\n let out = \"'\";\n for (let i = 0; i < s.length; i++) {\n const ch = s[i];\n if (ch === \"\\\\\") out += \"\\\\\\\\\";\n else if (ch === \"'\") out += \"\\\\'\";\n else out += ch;\n }\n return out + \"'\";\n }\n\n function rubyArrayLiteral(values) {\n return '[' + (values || []).map((v) => rubyStringLiteral(v)).join(', ') + ']';\n }\n\n function rubyHashLiteral(obj) {\n return '{' + Object.entries(obj || {})\n .map(([k, v]) => rubyStringLiteral(k) + ' => ' + rubyStringLiteral(v))\n .join(', ') + '}';\n }\n\n // Wrapper code: set $0/$PROGRAM_NAME/ARGV/ENV, run user code,\n // capture SystemExit. User-controlled strings are emitted as Ruby\n // single-quoted literals so Ruby interpolation inside source text is\n // preserved for the user's eval, not consumed by this wrapper.\n //\n // The wrapper sets __NIMBUS_RUBY_EXIT to the desired exit code so\n // we can read it via a second rb-eval-string-protect call. Failing\n // SystemExit (raise) ends up with __NIMBUS_RUBY_EXIT = 1 + stderr\n // message.\n const userCodeRb = rubyStringLiteral(args.userCode);\n const argvRb = rubyArrayLiteral(args.rbArgv.slice(1)); // exclude argv[0]\n const progNameRb = rubyStringLiteral(args.progName);\n\n // STAGED execution: we split the prelude (stdout sync + ARGV/ENV/$0\n // setup) from the user-code eval. The prelude has no failure modes\n // we care about; user-code is wrapped in begin/rescue for SystemExit\n // and Exception. If the wrapper itself trips a syntax/runtime error,\n // we surface it via the __NIMBUS_RUBY_DIAG side channel.\n // Build env list as string-keyed Ruby hash via the rocket-syntax.\n // Ruby treats colon-style hash literals as Symbol-keyed; we need\n // String keys so ENV[k] = v works without TypeError.\n const envHashRb = rubyHashLiteral(args.userEnv || {});\n const cwdRb = rubyStringLiteral(args.cwd || '/home/user');\n\n const preludeRb = [\n // Reset exit state FIRST so partial prelude failures still\n // surface a clean exit code (previously: exit 7 left $__nimbus_exit\n // = 7 \u2192 next call's prelude could fail before resetting \u2192 second\n // exit 0 returned 7).\n '$__nimbus_exit = 0',\n '$stdout.sync = true',\n '$stderr.sync = true',\n '$0 = ' + progNameRb,\n '$PROGRAM_NAME = ' + progNameRb,\n 'ARGV.replace(' + argvRb + ')',\n envHashRb + '.each_pair { |k, v| ENV[k] = v }',\n 'ENV[\"HOME\"] ||= \"/home/user\"',\n 'ENV[\"GEM_HOME\"] ||= File.join(ENV[\"HOME\"], \".gem\")',\n 'ENV[\"GEM_PATH\"] ||= ENV[\"GEM_HOME\"]',\n 'begin; Dir.mkdir(ENV[\"GEM_HOME\"]) unless Dir.exist?(ENV[\"GEM_HOME\"]); rescue Exception; end',\n 'begin; Dir.chdir(' + cwdRb + '); rescue Exception; end',\n 'begin; $LOAD_PATH.unshift(Dir.pwd) unless $LOAD_PATH.include?(Dir.pwd); rescue Exception; end',\n 'begin; (ENV[\"NIMBUS_GEM_LIBS\"] || \"\").split(\":\").reverse_each { |p| $LOAD_PATH.unshift(p) if p && p != \"\" && !$LOAD_PATH.include?(p) }; rescue Exception; end',\n ].join('; ');\n\n const userWrapper = [\n 'begin',\n ' ' + 'eval(' + userCodeRb + ', TOPLEVEL_BINDING, ' + progNameRb + ', 1)',\n 'rescue SystemExit => e',\n ' $__nimbus_exit = e.status',\n 'rescue Exception => e',\n ' $stderr.write(e.full_message(highlight: false, order: :top))',\n ' $__nimbus_exit = 1',\n 'ensure',\n ' $stdout.flush rescue nil',\n ' $stderr.flush rescue nil',\n 'end',\n ].join(\"\\n\");\n\n const memory = boot.instance.exports.memory;\n\n function callEvalStringProtect(rubyCode) {\n const enc = new TextEncoder();\n const bytes = enc.encode(rubyCode);\n const cabiRealloc = boot.instance.exports.cabi_realloc;\n const codePtr = cabiRealloc(0, 0, 1, bytes.length);\n new Uint8Array(memory.buffer).set(bytes, codePtr);\n const retPtr = boot.rbEvalStringProtect(codePtr, bytes.length);\n // Return is a tuple: (rb-abi-value handle u32, status s32) \u2014 8 bytes\n const dv = new DataView(memory.buffer);\n const handle = dv.getUint32(retPtr + 0, true);\n const status = dv.getInt32(retPtr + 4, true);\n return { handle, status };\n }\n\n // Stage 1: run the prelude (sync flags, ARGV, ENV, $0/$PROGRAM_NAME).\n let preludeStatus;\n try {\n preludeStatus = callEvalStringProtect(preludeRb);\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'ruby prelude threw: ' + (e && e.message),\n };\n }\n if (preludeStatus && preludeStatus.status !== 0) {\n globalThis.__nimbusRubyStderr.push('[ruby-runner-diag] prelude returned non-zero status: ' + preludeStatus.status + '\\n');\n }\n\n // Stage 2: run user code wrapped for SystemExit/Exception capture.\n let evalStatus;\n try {\n evalStatus = callEvalStringProtect(userWrapper);\n } catch (e) {\n return {\n exitCode: 1,\n stdout: globalThis.__nimbusRubyStdout.slice(stdoutStart).join(''),\n stderr: globalThis.__nimbusRubyStderr.slice(stderrStart).join(''),\n error: 'rb-eval-string-protect threw: ' + (e && e.message),\n };\n }\n if (evalStatus && evalStatus.status !== 0) {\n globalThis.__nimbusRubyStderr.push('[ruby-runner-diag] user wrapper returned non-zero status: ' + evalStatus.status + '\\n');\n }\n\n // Read $__nimbus_exit by evaluating it and inspecting the result\n // via Ruby's puts (captured into stderr). Simpler: call exit through\n // rb-eval-string-protect and parse the status code from there.\n //\n // Actually rbEvalStringProtect already returns a status (second\n // tuple element). When the wrapper completes normally, status is 0\n // and we read $__nimbus_exit. We use a follow-up eval that prints\n // the exit code to a sentinel marker on stderr we can scrape, but\n // a cleaner approach: have the wrapper's last expression BE the\n // exit code (so it's the return value of eval).\n //\n // Re-do: eval $__nimbus_exit and read it from a marker line we\n // emit to a side channel \u2014 easiest is to call $stdout via a unique\n // marker that we then strip from output.\n const NIMBUS_EXIT_MARKER = '__NIMBUS_RUBY_EXIT_';\n let exitCode = 0;\n try {\n // Print the marker + exit code to stderr (a side channel separate\n // from user-visible stdout). We strip it before returning.\n callEvalStringProtect(\n '$stderr.write(' + JSON.stringify(NIMBUS_EXIT_MARKER) + ' + $__nimbus_exit.to_s + \"\\\\n\")'\n );\n // Scrape the marker from stderr buffer \u2014 using ONLY this call's\n // slice (from stderrStart). The same facet can be reused across\n // multiple __rubyRun invocations (loader-pool dedup by tag), so\n // a previous call's marker would otherwise be matched first.\n const callStderr = globalThis.__nimbusRubyStderr.slice(stderrStart).join('');\n // Match the LAST marker in this slice (the one our just-completed\n // call emitted; if the user wrapper also emitted writes, the\n // marker is appended after them).\n const markerRe = new RegExp(NIMBUS_EXIT_MARKER + '(-?\\\\d+)', 'g');\n let lastMatch = null;\n let mit;\n while ((mit = markerRe.exec(callStderr)) !== null) lastMatch = mit;\n if (lastMatch) exitCode = parseInt(lastMatch[1], 10);\n } catch (e) {\n // Failure to read exit code \u2192 assume 0 if no errors observed.\n exitCode = 0;\n }\n\n // Slice + scrub the marker from stderr output before returning.\n const stdoutOut = globalThis.__nimbusRubyStdout.slice(stdoutStart).join('');\n let stderrOut = globalThis.__nimbusRubyStderr.slice(stderrStart).join('');\n stderrOut = stderrOut.replace(new RegExp(NIMBUS_EXIT_MARKER + '-?\\\\d+\\\\n?', 'g'), '');\n\n return {\n exitCode: exitCode,\n stdout: stdoutOut,\n stderr: stderrOut,\n fsDiff: (typeof __wasiSnapshotFS === 'function' ? __wasiSnapshotFS() : null),\n };\n};\n\n// \u2500\u2500 END: ruby-runner preamble \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\n";
|
|
61
59
|
//# sourceMappingURL=ruby-runner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ruby-runner.d.ts","sourceRoot":"","sources":["../../src/runtime/ruby-runner.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ruby-runner.d.ts","sourceRoot":"","sources":["../../src/runtime/ruby-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAczD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,SAAS,CAAC;CAChB,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,KAC7F,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,MAAM,CAAC,CAkJhC;AA0bD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,w4sBAidrC,CAAC"}
|
|
@@ -11,9 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Out of v1:
|
|
13
13
|
* - REPL mode (`ruby` with no args)
|
|
14
|
-
* -
|
|
15
|
-
* stdlib works since stdlib is wasi-vfs-packed inside the wasm)
|
|
16
|
-
* - gems, bundler
|
|
14
|
+
* - native extension gems
|
|
17
15
|
* - js-runtime bindings (gem "js") — those imports are stubbed
|
|
18
16
|
* and will raise NotImplementedError if Ruby code tries to use
|
|
19
17
|
* them
|
|
@@ -35,6 +33,10 @@
|
|
|
35
33
|
* canonical_abi_drop_rb-abi-value, memory.
|
|
36
34
|
*/
|
|
37
35
|
import { WASI_INSTANCE_PREAMBLE_SRC } from './wasi-instance.js';
|
|
36
|
+
import { flushVfsDiff, snapshotVfs } from './vfs-snapshot.js';
|
|
37
|
+
import { createLiveStaticServerCode, parsePort } from './static-server.js';
|
|
38
|
+
import { resolveVfsPath } from '../vfs/path.js';
|
|
39
|
+
import { defaultGemHome, installRubyBundle, installRubyGems, installedGemLibRoots, parseRubyGemRequirements, } from './ruby-gems.js';
|
|
38
40
|
/**
|
|
39
41
|
* Build the ruby-runner factory. Called once at session init; the
|
|
40
42
|
* returned factory binds the manifest + install root for each
|
|
@@ -42,28 +44,43 @@ import { WASI_INSTANCE_PREAMBLE_SRC } from './wasi-instance.js';
|
|
|
42
44
|
*/
|
|
43
45
|
export function makeRubyRunnerFactory(deps) {
|
|
44
46
|
const { facetMgr, vfs } = deps;
|
|
45
|
-
return function rubyRunnerFactory(manifest, installRoot, binName,
|
|
47
|
+
return function rubyRunnerFactory(manifest, installRoot, binName, binKind) {
|
|
46
48
|
const findFile = (rel) => {
|
|
47
49
|
const entry = manifest.files.find((f) => f.path === rel);
|
|
48
50
|
return entry ? `${installRoot}/${entry.path}` : null;
|
|
49
51
|
};
|
|
50
52
|
const wasmVfs = findFile('share/ruby/ruby+stdlib.wasm');
|
|
53
|
+
let fsSnapshotCache = null;
|
|
51
54
|
return async function rubyBinHandler(ctx) {
|
|
52
55
|
const argv = ctx.args || [];
|
|
53
56
|
const cwd = ctx.cwd || '/home/user';
|
|
57
|
+
const packageCommand = await maybeHandleRubyPackageCommand(binKind, binName, argv, cwd, vfs, ctx);
|
|
58
|
+
if (packageCommand.handled)
|
|
59
|
+
return packageCommand.exitCode;
|
|
60
|
+
const toolInvocation = buildRubyToolInvocation(binKind, binName, argv);
|
|
61
|
+
if (toolInvocation.error) {
|
|
62
|
+
ctx.stderr.write(`${binName}: ${toolInvocation.error}\n`);
|
|
63
|
+
return toolInvocation.exitCode;
|
|
64
|
+
}
|
|
54
65
|
// --version / --help fast paths (no wasm boot).
|
|
55
|
-
if (argv.includes('--version') || argv.includes('-v')) {
|
|
56
|
-
|
|
57
|
-
// release notes. The string format matches `ruby --version`
|
|
58
|
-
// shape (parsed by tooling like rbenv).
|
|
59
|
-
ctx.stdout.write(`ruby 3.3.4 (2024-04-23 revision wasm) [wasm32-wasi]\n`);
|
|
66
|
+
if (toolInvocation.mode !== 'tool' && (argv.includes('--version') || argv.includes('-v'))) {
|
|
67
|
+
ctx.stdout.write(`ruby 3.3.3 (ruby.wasm, Nimbus runtime) [wasm32-wasi]\n`);
|
|
60
68
|
return 0;
|
|
61
69
|
}
|
|
62
|
-
if (argv.includes('--help') || argv.includes('-h')) {
|
|
70
|
+
if (toolInvocation.mode !== 'tool' && (argv.includes('--help') || argv.includes('-h'))) {
|
|
63
71
|
ctx.stdout.write(`Usage: ${binName} [switches] [--] [programfile] [arguments]\n`);
|
|
64
|
-
ctx.stdout.write(`Nimbus Ruby
|
|
65
|
-
ctx.stdout.write(`Supported
|
|
66
|
-
ctx.stdout.write(`
|
|
72
|
+
ctx.stdout.write(`Nimbus Ruby runtime (ruby.wasm).\n`);
|
|
73
|
+
ctx.stdout.write(`Supported: -e <code>, <file.rb>, -r <lib>, VFS-backed require_relative and file IO\n`);
|
|
74
|
+
ctx.stdout.write(`Ruby socket/native gem install support is not available in this ruby.wasm build.\n`);
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const staticServer = parseRubyHttpd(argv, cwd);
|
|
78
|
+
if (staticServer) {
|
|
79
|
+
const code = createLiveStaticServerCode(staticServer.root);
|
|
80
|
+
const command = `${binName} -run -e httpd ${staticServer.root} -p ${staticServer.port}`;
|
|
81
|
+
const spawned = facetMgr.spawn(code, command, staticServer.root, { port: staticServer.port });
|
|
82
|
+
ctx.stdout.write(`Serving HTTP on 0.0.0.0 port ${staticServer.port} from /${staticServer.root}\n`);
|
|
83
|
+
ctx.stdout.write(`[nimbus] started: pid=${spawned.pid} cmd="${command}" port=${staticServer.port}\n`);
|
|
67
84
|
return 0;
|
|
68
85
|
}
|
|
69
86
|
// Resolve install bytes.
|
|
@@ -73,7 +90,16 @@ export function makeRubyRunnerFactory(deps) {
|
|
|
73
90
|
}
|
|
74
91
|
const wasmBytes = vfs.readFile(wasmVfs);
|
|
75
92
|
// Parse argv.
|
|
76
|
-
const parsed =
|
|
93
|
+
const parsed = toolInvocation.mode === 'tool'
|
|
94
|
+
? {
|
|
95
|
+
mode: 'inline',
|
|
96
|
+
inlineCode: toolInvocation.code,
|
|
97
|
+
scriptPath: '',
|
|
98
|
+
scriptArgs: [],
|
|
99
|
+
requires: [],
|
|
100
|
+
exitCode: 0,
|
|
101
|
+
}
|
|
102
|
+
: parseRubyArgv(argv);
|
|
77
103
|
if (parsed.error) {
|
|
78
104
|
ctx.stderr.write(`${binName}: ${parsed.error}\n`);
|
|
79
105
|
return parsed.exitCode;
|
|
@@ -100,7 +126,7 @@ export function makeRubyRunnerFactory(deps) {
|
|
|
100
126
|
ctx.stderr.write(`${binName}: ${parsed.scriptPath}: ${e?.message || e}\n`);
|
|
101
127
|
return 1;
|
|
102
128
|
}
|
|
103
|
-
progName =
|
|
129
|
+
progName = '/' + absPath;
|
|
104
130
|
rbArgv = [parsed.scriptPath, ...parsed.scriptArgs];
|
|
105
131
|
}
|
|
106
132
|
// -r flags add prelude `require '<lib>'` lines (stdlib only).
|
|
@@ -111,13 +137,30 @@ export function makeRubyRunnerFactory(deps) {
|
|
|
111
137
|
const userEnv = { ...(ctx.env || {}) };
|
|
112
138
|
if (!userEnv.HOME)
|
|
113
139
|
userEnv.HOME = '/home/ruby';
|
|
140
|
+
if (userEnv.HOME === '/home/ruby')
|
|
141
|
+
userEnv.HOME = '/home/user';
|
|
114
142
|
if (!userEnv.LANG)
|
|
115
143
|
userEnv.LANG = 'C.UTF-8';
|
|
144
|
+
userEnv.GEM_HOME ||= '/' + defaultGemHome();
|
|
145
|
+
userEnv.GEM_PATH ||= userEnv.GEM_HOME;
|
|
146
|
+
userEnv.NIMBUS_GEM_LIBS = installedGemLibRoots(vfs, defaultGemHome()).join(':');
|
|
116
147
|
// Ruby looks for charset hints via these vars; set sensible
|
|
117
148
|
// defaults so puts of non-ASCII strings doesn't trip on the
|
|
118
149
|
// wasi default of "ASCII-8BIT".
|
|
119
150
|
if (!userEnv.LC_ALL)
|
|
120
151
|
userEnv.LC_ALL = 'C.UTF-8';
|
|
152
|
+
const revision = typeof vfs.revision === 'function' ? vfs.revision() : Date.now();
|
|
153
|
+
let fsSnapshot = fsSnapshotCache && fsSnapshotCache.cwd === cwd && fsSnapshotCache.revision === revision
|
|
154
|
+
? fsSnapshotCache.result
|
|
155
|
+
: null;
|
|
156
|
+
if (!fsSnapshot) {
|
|
157
|
+
fsSnapshot = snapshotVfs(vfs, cwd, { extraRoots: [defaultGemHome()] });
|
|
158
|
+
fsSnapshotCache = { cwd, revision, result: fsSnapshot };
|
|
159
|
+
}
|
|
160
|
+
if ('error' in fsSnapshot) {
|
|
161
|
+
ctx.stderr.write(`${binName}: ${fsSnapshot.error}\n`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
121
164
|
// Dispatch the facet.
|
|
122
165
|
const result = await dispatchRubyFacet(facetMgr, {
|
|
123
166
|
wasmBytes,
|
|
@@ -125,11 +168,15 @@ export function makeRubyRunnerFactory(deps) {
|
|
|
125
168
|
rbArgv,
|
|
126
169
|
userEnv,
|
|
127
170
|
progName,
|
|
171
|
+
cwd,
|
|
172
|
+
fsSnapshot: fsSnapshot.snapshot,
|
|
128
173
|
});
|
|
129
174
|
if (result.stdout)
|
|
130
175
|
ctx.stdout.write(result.stdout);
|
|
131
176
|
if (result.stderr)
|
|
132
177
|
ctx.stderr.write(result.stderr);
|
|
178
|
+
if (result.fsDiff)
|
|
179
|
+
flushVfsDiff(vfs, result.fsDiff);
|
|
133
180
|
if (result.error) {
|
|
134
181
|
ctx.stderr.write(`${binName}: ${result.error}\n`);
|
|
135
182
|
return 1;
|
|
@@ -138,6 +185,211 @@ export function makeRubyRunnerFactory(deps) {
|
|
|
138
185
|
};
|
|
139
186
|
};
|
|
140
187
|
}
|
|
188
|
+
async function maybeHandleRubyPackageCommand(binKind, binName, argv, cwd, vfs, ctx) {
|
|
189
|
+
const isGem = binKind === 'gem' || binName === 'gem';
|
|
190
|
+
const isBundle = binKind === 'bundle' || binName === 'bundle' || binName === 'bundler';
|
|
191
|
+
if (isGem && argv[0] === 'install') {
|
|
192
|
+
const parsed = parseGemInstallArgs(argv.slice(1));
|
|
193
|
+
if (parsed.error) {
|
|
194
|
+
ctx.stderr.write(`gem install: ${parsed.error}\n`);
|
|
195
|
+
return { handled: true, exitCode: 2 };
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const report = await installRubyGems(vfs, parsed.requests, { gemHome: defaultGemHome(), includeDependencies: true });
|
|
199
|
+
for (const name of report.installed)
|
|
200
|
+
ctx.stdout.write(`Successfully installed ${name}\n`);
|
|
201
|
+
for (const name of report.alreadyInstalled)
|
|
202
|
+
ctx.stdout.write(`${name} is already installed\n`);
|
|
203
|
+
ctx.stdout.write(`${report.installed.length + report.alreadyInstalled.length} gem(s) processed\n`);
|
|
204
|
+
return { handled: true, exitCode: 0 };
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
ctx.stderr.write(`gem install: ${e?.message || e}\n`);
|
|
208
|
+
return { handled: true, exitCode: 1 };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (isBundle && argv[0] === 'install') {
|
|
212
|
+
try {
|
|
213
|
+
const { requests, report, lockfilePath } = await installRubyBundle(vfs, cwd, { gemHome: defaultGemHome() });
|
|
214
|
+
for (const name of report.installed)
|
|
215
|
+
ctx.stdout.write(`Successfully installed ${name}\n`);
|
|
216
|
+
for (const name of report.alreadyInstalled)
|
|
217
|
+
ctx.stdout.write(`${name} is already installed\n`);
|
|
218
|
+
ctx.stdout.write(`Bundle complete! ${requests.length} Gemfile dependency(s), ${report.installed.length + report.alreadyInstalled.length} gem(s) now installed.\n`);
|
|
219
|
+
ctx.stdout.write(`Bundled lockfile written to /${lockfilePath}\n`);
|
|
220
|
+
return { handled: true, exitCode: 0 };
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
ctx.stderr.write(`bundle install: ${e?.message || e}\n`);
|
|
224
|
+
return { handled: true, exitCode: 1 };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { handled: false, exitCode: 0 };
|
|
228
|
+
}
|
|
229
|
+
function parseGemInstallArgs(argv) {
|
|
230
|
+
const names = [];
|
|
231
|
+
let versionRequirement = null;
|
|
232
|
+
for (let i = 0; i < argv.length; i++) {
|
|
233
|
+
const arg = argv[i];
|
|
234
|
+
if (arg === '-v' || arg === '--version') {
|
|
235
|
+
const version = argv[i + 1];
|
|
236
|
+
if (!version)
|
|
237
|
+
return { requests: [], error: `${arg}: missing version` };
|
|
238
|
+
versionRequirement = version;
|
|
239
|
+
i++;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (arg.startsWith('--version=')) {
|
|
243
|
+
versionRequirement = arg.slice('--version='.length);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (arg === '--no-document' || arg === '--no-doc' || arg === '--user-install') {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (arg.startsWith('-')) {
|
|
250
|
+
return { requests: [], error: `option '${arg}' is not supported in Nimbus yet` };
|
|
251
|
+
}
|
|
252
|
+
names.push(arg);
|
|
253
|
+
}
|
|
254
|
+
const requirements = versionRequirement ? parseRubyGemRequirements(versionRequirement) : [];
|
|
255
|
+
const requests = names.map((name) => ({ name, requirements }));
|
|
256
|
+
if (requests.length === 0)
|
|
257
|
+
return { requests, error: 'missing gem name' };
|
|
258
|
+
return { requests };
|
|
259
|
+
}
|
|
260
|
+
function buildRubyToolInvocation(binKind, binName, argv) {
|
|
261
|
+
const isGem = binKind === 'gem' || binName === 'gem';
|
|
262
|
+
const isBundle = binKind === 'bundle' || binName === 'bundle' || binName === 'bundler';
|
|
263
|
+
if (!isGem && !isBundle)
|
|
264
|
+
return { mode: 'none', code: '', exitCode: 0 };
|
|
265
|
+
if (isGem) {
|
|
266
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
267
|
+
return {
|
|
268
|
+
mode: 'tool',
|
|
269
|
+
code: [
|
|
270
|
+
'puts "Usage: gem --version"',
|
|
271
|
+
'puts " gem env"',
|
|
272
|
+
'puts " gem install <name> # installs pure Ruby gems through Nimbus RubyGems"',
|
|
273
|
+
].join('\n'),
|
|
274
|
+
exitCode: 0,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (argv.includes('--version') || argv[0] === '-v') {
|
|
278
|
+
return { mode: 'tool', code: 'require "rubygems"; puts Gem::VERSION', exitCode: 0 };
|
|
279
|
+
}
|
|
280
|
+
if (argv[0] === 'env') {
|
|
281
|
+
return {
|
|
282
|
+
mode: 'tool',
|
|
283
|
+
code: [
|
|
284
|
+
'require "rubygems"',
|
|
285
|
+
'puts "RubyGems #{Gem::VERSION}"',
|
|
286
|
+
'puts "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})"',
|
|
287
|
+
'puts "GEM_HOME=#{ENV["GEM_HOME"] || File.join(ENV["HOME"], ".gem")}"',
|
|
288
|
+
'puts "GEM_PATH=#{ENV["GEM_PATH"] || ENV["GEM_HOME"]}"',
|
|
289
|
+
].join('\n'),
|
|
290
|
+
exitCode: 0,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (argv[0] === 'install') {
|
|
294
|
+
return {
|
|
295
|
+
mode: 'none',
|
|
296
|
+
code: '',
|
|
297
|
+
error: 'gem install command was not handled by Nimbus RubyGems',
|
|
298
|
+
exitCode: 1,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
mode: 'none',
|
|
303
|
+
code: '',
|
|
304
|
+
error: `gem subcommand '${argv[0]}' is not supported yet`,
|
|
305
|
+
exitCode: 2,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h')) {
|
|
309
|
+
return {
|
|
310
|
+
mode: 'tool',
|
|
311
|
+
code: [
|
|
312
|
+
'puts "Usage: bundle --version"',
|
|
313
|
+
'puts " bundle install # installs compatible pure Ruby gems through Nimbus RubyGems"',
|
|
314
|
+
].join('\n'),
|
|
315
|
+
exitCode: 0,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (argv.includes('--version') || argv[0] === '-v') {
|
|
319
|
+
return {
|
|
320
|
+
mode: 'tool',
|
|
321
|
+
code: [
|
|
322
|
+
'begin',
|
|
323
|
+
' require "bundler"',
|
|
324
|
+
' puts "Bundler #{Bundler::VERSION}"',
|
|
325
|
+
'rescue LoadError',
|
|
326
|
+
' warn "Bundler is not bundled in this ruby.wasm runtime"',
|
|
327
|
+
' exit 127',
|
|
328
|
+
'end',
|
|
329
|
+
].join('\n'),
|
|
330
|
+
exitCode: 0,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (argv[0] === 'install') {
|
|
334
|
+
return {
|
|
335
|
+
mode: 'none',
|
|
336
|
+
code: '',
|
|
337
|
+
error: 'bundle install command was not handled by Nimbus RubyGems',
|
|
338
|
+
exitCode: 1,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
mode: 'none',
|
|
343
|
+
code: '',
|
|
344
|
+
error: `bundle subcommand '${argv[0]}' is not supported yet`,
|
|
345
|
+
exitCode: 2,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function parseRubyHttpd(argv, cwd) {
|
|
349
|
+
let requiresUn = false;
|
|
350
|
+
let hasHttpdEval = false;
|
|
351
|
+
let afterEval = -1;
|
|
352
|
+
for (let i = 0; i < argv.length; i++) {
|
|
353
|
+
const arg = argv[i];
|
|
354
|
+
if (arg === '-run' || arg === '-r' && argv[i + 1] === 'un') {
|
|
355
|
+
requiresUn = true;
|
|
356
|
+
if (arg === '-r')
|
|
357
|
+
i++;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (arg === '-e' && argv[i + 1] === 'httpd') {
|
|
361
|
+
hasHttpdEval = true;
|
|
362
|
+
afterEval = i + 2;
|
|
363
|
+
i++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (arg === '-ehttpd') {
|
|
367
|
+
hasHttpdEval = true;
|
|
368
|
+
afterEval = i + 1;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!requiresUn || !hasHttpdEval)
|
|
373
|
+
return null;
|
|
374
|
+
let port = 8080;
|
|
375
|
+
let directory = '.';
|
|
376
|
+
for (let i = Math.max(afterEval, 0); i < argv.length; i++) {
|
|
377
|
+
const arg = argv[i];
|
|
378
|
+
if (arg === '-p' || arg === '--port') {
|
|
379
|
+
port = parsePort(argv[i + 1], port);
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (arg.startsWith('--port=')) {
|
|
384
|
+
port = parsePort(arg.slice('--port='.length), port);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (!arg.startsWith('-') && directory === '.') {
|
|
388
|
+
directory = arg;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { port, root: resolveVfsPath(directory, cwd) };
|
|
392
|
+
}
|
|
141
393
|
function parseRubyArgv(argv) {
|
|
142
394
|
// Ruby's CLI is rich; v1 handles -e, -r, and positional script.
|
|
143
395
|
const requires = [];
|
|
@@ -213,14 +465,6 @@ function parseRubyArgv(argv) {
|
|
|
213
465
|
return { mode: 'inline', inlineCode: '', scriptPath: '', scriptArgs: [],
|
|
214
466
|
requires, exitCode: 2, error: "REPL not supported in v1. Use 'ruby -e \"code\"' or 'ruby script.rb'." };
|
|
215
467
|
}
|
|
216
|
-
function resolveVfsPath(rel, cwd) {
|
|
217
|
-
const cwdN = cwd.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
218
|
-
if (rel.startsWith('/'))
|
|
219
|
-
return rel.replace(/^\/+/, '');
|
|
220
|
-
if (rel === '.')
|
|
221
|
-
return cwdN;
|
|
222
|
-
return `${cwdN}/${rel}`;
|
|
223
|
-
}
|
|
224
468
|
async function dispatchRubyFacet(facetMgr, args) {
|
|
225
469
|
const toAB = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
|
226
470
|
// The Ruby preamble runs the entire bootstrap at child-facet module-
|
|
@@ -252,6 +496,8 @@ async function dispatchRubyFacet(facetMgr, args) {
|
|
|
252
496
|
rbArgv: inArgs.rbArgv,
|
|
253
497
|
userEnv: inArgs.userEnv,
|
|
254
498
|
progName: inArgs.progName,
|
|
499
|
+
cwd: inArgs.cwd,
|
|
500
|
+
fsSnapshot: inArgs.fsSnapshot,
|
|
255
501
|
});
|
|
256
502
|
};
|
|
257
503
|
try {
|
|
@@ -260,6 +506,8 @@ async function dispatchRubyFacet(facetMgr, args) {
|
|
|
260
506
|
rbArgv: args.rbArgv,
|
|
261
507
|
userEnv: args.userEnv,
|
|
262
508
|
progName: args.progName,
|
|
509
|
+
cwd: args.cwd,
|
|
510
|
+
fsSnapshot: args.fsSnapshot,
|
|
263
511
|
}, {
|
|
264
512
|
wasmModules: {
|
|
265
513
|
'ruby+stdlib.wasm': toAB(args.wasmBytes),
|
|
@@ -271,6 +519,7 @@ async function dispatchRubyFacet(facetMgr, args) {
|
|
|
271
519
|
stdout: result.stdout || '',
|
|
272
520
|
stderr: result.stderr || '',
|
|
273
521
|
error: result.error,
|
|
522
|
+
fsDiff: result.fsDiff,
|
|
274
523
|
};
|
|
275
524
|
}
|
|
276
525
|
catch (e) {
|
|
@@ -330,6 +579,25 @@ export const RUBY_RUNNER_PREAMBLE_TAIL = `
|
|
|
330
579
|
globalThis.__nimbusRubyStdout = globalThis.__nimbusRubyStdout || [];
|
|
331
580
|
globalThis.__nimbusRubyStderr = globalThis.__nimbusRubyStderr || [];
|
|
332
581
|
|
|
582
|
+
function __nimbusInstallRubyFsSnapshot(snapshot) {
|
|
583
|
+
const dirs = new Set(['tmp', 'home']);
|
|
584
|
+
const files = {};
|
|
585
|
+
for (const dir of (snapshot && snapshot.dirs) || []) dirs.add(String(dir).replace(/^\\/+/, '').replace(/\\/+$/, ''));
|
|
586
|
+
for (const [path, b64] of Object.entries((snapshot && snapshot.files) || {})) {
|
|
587
|
+
files[String(path).replace(/^\\/+/, '')] = b64;
|
|
588
|
+
}
|
|
589
|
+
__wasiInitFS({
|
|
590
|
+
root: '',
|
|
591
|
+
preopens: [
|
|
592
|
+
{ wasiPath: '/', vfsPath: '' },
|
|
593
|
+
{ wasiPath: '/tmp', vfsPath: 'tmp' },
|
|
594
|
+
{ wasiPath: '/home', vfsPath: 'home' },
|
|
595
|
+
],
|
|
596
|
+
files,
|
|
597
|
+
dirs: Array.from(dirs).filter(Boolean),
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
333
601
|
// ── Canonical-ABI resource Slab ────────────────────────────────────
|
|
334
602
|
// Pyodide-style minimal resource manager. Ruby's rb-abi-guest.js uses
|
|
335
603
|
// these 4 functions for resource_drop / resource_new / resource_get /
|
|
@@ -558,6 +826,12 @@ globalThis.__rubyRun = async function __rubyRun(args) {
|
|
|
558
826
|
};
|
|
559
827
|
}
|
|
560
828
|
|
|
829
|
+
try {
|
|
830
|
+
__nimbusInstallRubyFsSnapshot(args.fsSnapshot);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
globalThis.__nimbusRubyStderr.push('[ruby-runner] VFS mount failed: ' + (e && e.message) + '\\n');
|
|
833
|
+
}
|
|
834
|
+
|
|
561
835
|
// First call into __rubyRun: complete Ruby VM init (ruby-init +
|
|
562
836
|
// ruby-init-loadpath) now that we're in request-handler context
|
|
563
837
|
// where crypto.getRandomValues is permitted. Subsequent calls skip.
|
|
@@ -577,19 +851,40 @@ globalThis.__rubyRun = async function __rubyRun(args) {
|
|
|
577
851
|
}
|
|
578
852
|
}
|
|
579
853
|
|
|
854
|
+
function rubyStringLiteral(value) {
|
|
855
|
+
const s = String(value ?? '');
|
|
856
|
+
let out = "'";
|
|
857
|
+
for (let i = 0; i < s.length; i++) {
|
|
858
|
+
const ch = s[i];
|
|
859
|
+
if (ch === "\\\\") out += "\\\\\\\\";
|
|
860
|
+
else if (ch === "'") out += "\\\\'";
|
|
861
|
+
else out += ch;
|
|
862
|
+
}
|
|
863
|
+
return out + "'";
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function rubyArrayLiteral(values) {
|
|
867
|
+
return '[' + (values || []).map((v) => rubyStringLiteral(v)).join(', ') + ']';
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function rubyHashLiteral(obj) {
|
|
871
|
+
return '{' + Object.entries(obj || {})
|
|
872
|
+
.map(([k, v]) => rubyStringLiteral(k) + ' => ' + rubyStringLiteral(v))
|
|
873
|
+
.join(', ') + '}';
|
|
874
|
+
}
|
|
875
|
+
|
|
580
876
|
// Wrapper code: set $0/$PROGRAM_NAME/ARGV/ENV, run user code,
|
|
581
|
-
// capture SystemExit.
|
|
582
|
-
//
|
|
583
|
-
//
|
|
877
|
+
// capture SystemExit. User-controlled strings are emitted as Ruby
|
|
878
|
+
// single-quoted literals so Ruby interpolation inside source text is
|
|
879
|
+
// preserved for the user's eval, not consumed by this wrapper.
|
|
584
880
|
//
|
|
585
881
|
// The wrapper sets __NIMBUS_RUBY_EXIT to the desired exit code so
|
|
586
882
|
// we can read it via a second rb-eval-string-protect call. Failing
|
|
587
883
|
// SystemExit (raise) ends up with __NIMBUS_RUBY_EXIT = 1 + stderr
|
|
588
884
|
// message.
|
|
589
|
-
const userCodeRb =
|
|
590
|
-
const argvRb =
|
|
591
|
-
const
|
|
592
|
-
const progNameRb = JSON.stringify(args.progName);
|
|
885
|
+
const userCodeRb = rubyStringLiteral(args.userCode);
|
|
886
|
+
const argvRb = rubyArrayLiteral(args.rbArgv.slice(1)); // exclude argv[0]
|
|
887
|
+
const progNameRb = rubyStringLiteral(args.progName);
|
|
593
888
|
|
|
594
889
|
// STAGED execution: we split the prelude (stdout sync + ARGV/ENV/$0
|
|
595
890
|
// setup) from the user-code eval. The prelude has no failure modes
|
|
@@ -599,10 +894,8 @@ globalThis.__rubyRun = async function __rubyRun(args) {
|
|
|
599
894
|
// Build env list as string-keyed Ruby hash via the rocket-syntax.
|
|
600
895
|
// Ruby treats colon-style hash literals as Symbol-keyed; we need
|
|
601
896
|
// String keys so ENV[k] = v works without TypeError.
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
.join(', ');
|
|
605
|
-
const envHashRb = '{' + envEntries + '}';
|
|
897
|
+
const envHashRb = rubyHashLiteral(args.userEnv || {});
|
|
898
|
+
const cwdRb = rubyStringLiteral(args.cwd || '/home/user');
|
|
606
899
|
|
|
607
900
|
const preludeRb = [
|
|
608
901
|
// Reset exit state FIRST so partial prelude failures still
|
|
@@ -616,6 +909,13 @@ globalThis.__rubyRun = async function __rubyRun(args) {
|
|
|
616
909
|
'$PROGRAM_NAME = ' + progNameRb,
|
|
617
910
|
'ARGV.replace(' + argvRb + ')',
|
|
618
911
|
envHashRb + '.each_pair { |k, v| ENV[k] = v }',
|
|
912
|
+
'ENV["HOME"] ||= "/home/user"',
|
|
913
|
+
'ENV["GEM_HOME"] ||= File.join(ENV["HOME"], ".gem")',
|
|
914
|
+
'ENV["GEM_PATH"] ||= ENV["GEM_HOME"]',
|
|
915
|
+
'begin; Dir.mkdir(ENV["GEM_HOME"]) unless Dir.exist?(ENV["GEM_HOME"]); rescue Exception; end',
|
|
916
|
+
'begin; Dir.chdir(' + cwdRb + '); rescue Exception; end',
|
|
917
|
+
'begin; $LOAD_PATH.unshift(Dir.pwd) unless $LOAD_PATH.include?(Dir.pwd); rescue Exception; end',
|
|
918
|
+
'begin; (ENV["NIMBUS_GEM_LIBS"] || "").split(":").reverse_each { |p| $LOAD_PATH.unshift(p) if p && p != "" && !$LOAD_PATH.include?(p) }; rescue Exception; end',
|
|
619
919
|
].join('; ');
|
|
620
920
|
|
|
621
921
|
const userWrapper = [
|
|
@@ -729,6 +1029,7 @@ globalThis.__rubyRun = async function __rubyRun(args) {
|
|
|
729
1029
|
exitCode: exitCode,
|
|
730
1030
|
stdout: stdoutOut,
|
|
731
1031
|
stderr: stderrOut,
|
|
1032
|
+
fsDiff: (typeof __wasiSnapshotFS === 'function' ? __wasiSnapshotFS() : null),
|
|
732
1033
|
};
|
|
733
1034
|
};
|
|
734
1035
|
|