@ai-ecoverse/slicc-kernel 1.36.0 → 1.37.0

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 CHANGED
@@ -143,6 +143,8 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
143
143
 
144
144
  Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The first package to define a name wins. `sh` runs bash as `sh` unless a package provides its own. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. Commands also come from pnpm's global directory. Every process gets `PNPM_HOME=/home/.local/share/pnpm`, unless the embedder sets its own, and has `$PNPM_HOME/bin` on `PATH`. The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
145
145
 
146
+ A package can also name a command that runs files by their suffix, as `binfmt_misc` does: `"slicc": { "binfmt": { ".jsh": "jsh" } }`. Executing a file that has no `#!` line and is not a wasm module, whose name ends in a mapped suffix, runs that command with the file's path as invoked and the arguments after it, as a `#!` line naming the command would; a `#!` line still wins. The longest matching suffix wins, the whole name is never a suffix (`.jsh` alone is not one), and the first package to map a suffix wins, as for commands. A suffix starts with a dot and the command must be a name the catalog resolves when the file runs.
147
+
146
148
  ### WASI programs
147
149
 
148
150
  A WASI program runs in a process worker of its own like an Emscripten one, against the same kernel: its descriptors, pipes, terminals and job control are the kernel's, and its files are OPFS. **Preopens**, in order:
@@ -4733,6 +4733,35 @@ async function scanFilesystems(fs, modules) {
4733
4733
  }
4734
4734
  return found;
4735
4735
  }
4736
+ var SUFFIX = /^\.[A-Za-z0-9_+-][A-Za-z0-9._+-]*$/;
4737
+ async function scanBinfmts(fs, modules) {
4738
+ const found = /* @__PURE__ */ new Map();
4739
+ for (const pkg of await packagesIn(fs, modules)) {
4740
+ let manifest;
4741
+ try {
4742
+ manifest = JSON.parse(await fs.readFile(`${pkg}/package.json`));
4743
+ } catch {
4744
+ continue;
4745
+ }
4746
+ const declared = manifest.slicc?.binfmt;
4747
+ if (!declared || typeof declared !== "object") continue;
4748
+ for (const [suffix, command] of Object.entries(declared)) {
4749
+ if (!SUFFIX.test(suffix) || typeof command !== "string" || !NAME.test(command)) continue;
4750
+ if (!found.has(suffix)) found.set(suffix, command);
4751
+ }
4752
+ }
4753
+ return found;
4754
+ }
4755
+ function binfmtOf(binfmts, path) {
4756
+ const name2 = path.slice(path.lastIndexOf("/") + 1);
4757
+ let best;
4758
+ for (const suffix of binfmts.keys()) {
4759
+ if (name2.length > suffix.length && name2.endsWith(suffix)) {
4760
+ if (best === void 0 || suffix.length > best.length) best = suffix;
4761
+ }
4762
+ }
4763
+ return best === void 0 ? void 0 : binfmts.get(best);
4764
+ }
4736
4765
 
4737
4766
  // src/fs/commands.ts
4738
4767
  var S_IFDIR2 = 16384;
@@ -10383,6 +10412,7 @@ var PACKAGE_ROOT = /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//;
10383
10412
  var ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
10384
10413
  var WASM_MAGIC = [0, 97, 115, 109];
10385
10414
  var SHEBANG_MAX = 256;
10415
+ var MAX_INTERPRETERS = 4;
10386
10416
  var NOT_FOUND = 127;
10387
10417
  var DECIDE_MS = 100;
10388
10418
  var INIT_PID = 1;
@@ -10434,6 +10464,17 @@ function targetOf(command) {
10434
10464
  ...command.preopenRoot ? { preopenRoot: true } : {}
10435
10465
  };
10436
10466
  }
10467
+ function withScriptEnv(target, command) {
10468
+ if (!command) return target;
10469
+ return {
10470
+ ...target,
10471
+ env: { ...target.env, ...command.env },
10472
+ unset: [
10473
+ ...(target.unset ?? []).filter((key) => !Object.hasOwn(command.env ?? {}, key)),
10474
+ ...command.unset ?? []
10475
+ ]
10476
+ };
10477
+ }
10437
10478
  function childHandle(handle) {
10438
10479
  return {
10439
10480
  pid: handle.pid,
@@ -10458,6 +10499,7 @@ var Launcher = class {
10458
10499
  modulesDir;
10459
10500
  env;
10460
10501
  catalog;
10502
+ binfmts;
10461
10503
  compiled = /* @__PURE__ */ new Map();
10462
10504
  processes = /* @__PURE__ */ new Map();
10463
10505
  zombies = /* @__PURE__ */ new Set();
@@ -10539,6 +10581,7 @@ var Launcher = class {
10539
10581
  this.fs = keepingOpen(fs, this.nodes);
10540
10582
  this.watchers.watch([this.modulesDir, this.pnpmHome], { recursive: true }, () => {
10541
10583
  this.catalog = void 0;
10584
+ this.binfmts = void 0;
10542
10585
  });
10543
10586
  }
10544
10587
  setRoutes(table) {
@@ -10548,6 +10591,10 @@ var Launcher = class {
10548
10591
  this.catalog ??= this.roots().then((roots) => scanCommands(this.base, roots));
10549
10592
  return this.catalog;
10550
10593
  }
10594
+ interpreters() {
10595
+ this.binfmts ??= this.roots().then((roots) => scanBinfmts(this.base, roots));
10596
+ return this.binfmts;
10597
+ }
10551
10598
  async roots() {
10552
10599
  return [this.modulesDir, ...await pnpmGlobalRoots(this.base, this.pnpmHome)];
10553
10600
  }
@@ -10593,30 +10640,28 @@ var Launcher = class {
10593
10640
  const command = (await this.commands()).get(name2);
10594
10641
  return command?.script ? command : void 0;
10595
10642
  }
10596
- async interpreted(file, argv, cwd) {
10643
+ async interpreted(file, argv, cwd, depth = 0) {
10597
10644
  const command = await this.scriptCommand(file);
10598
10645
  const script = command?.script ?? this.fs.resolvePath(cwd, file);
10599
- const words = shebang(await this.head(script) ?? new Uint8Array());
10646
+ const head = await this.head(script);
10647
+ if (!head) return void 0;
10648
+ const binfmt = isWasm(head) ? void 0 : binfmtOf(await this.interpreters(), script);
10649
+ const words = shebang(head) ?? (binfmt ? [binfmt] : void 0);
10600
10650
  if (!words) return void 0;
10601
10651
  if (baseName(words[0]) === "env") words.shift();
10602
10652
  const [interp, ...rest] = words;
10603
- const found = interp ? await this.resolve(interp, interp, cwd) : void 0;
10604
- if (!found) return void 0;
10605
- const target = command ? {
10606
- ...found,
10607
- env: { ...found.env, ...command.env },
10608
- unset: [...found.unset ?? [], ...command.unset ?? []]
10609
- } : found;
10610
10653
  const arg = rest.join(" ");
10611
- return {
10612
- target,
10613
- args: [
10614
- ...target.prefix ?? [],
10615
- ...arg ? [arg] : [],
10616
- command ? script : file,
10617
- ...argv.slice(1)
10618
- ]
10619
- };
10654
+ const passed = [...arg ? [arg] : [], command ? script : file, ...argv.slice(1)];
10655
+ const found = interp ? await this.resolve(interp, interp, cwd) : void 0;
10656
+ if (!found) {
10657
+ if (!interp || depth >= MAX_INTERPRETERS || !await this.scriptCommand(interp)) {
10658
+ return void 0;
10659
+ }
10660
+ const inner = await this.interpreted(interp, [interp, ...passed], cwd, depth + 1);
10661
+ return inner && { ...inner, target: withScriptEnv(inner.target, command) };
10662
+ }
10663
+ const target = withScriptEnv(found, command);
10664
+ return { target, args: [...target.prefix ?? [], ...passed] };
10620
10665
  }
10621
10666
  async plan(file, argv, cwd) {
10622
10667
  const direct = await this.resolve(file, argv[0] ?? file, cwd);
@@ -10624,8 +10669,10 @@ var Launcher = class {
10624
10669
  return this.interpreted(file, argv, cwd);
10625
10670
  }
10626
10671
  async unrunnable(file, cwd) {
10627
- const head = await this.base.readFileBuffer(this.fs.resolvePath(cwd, file)).catch(() => null);
10628
- return head && !(head[0] === 35 && head[1] === 33) ? "ENOEXEC" : "ENOENT";
10672
+ const path = this.fs.resolvePath(cwd, file);
10673
+ const head = await this.base.readFileBuffer(path).catch(() => null);
10674
+ if (!head || head[0] === 35 && head[1] === 33) return "ENOENT";
10675
+ return binfmtOf(await this.interpreters(), path) ? "ENOENT" : "ENOEXEC";
10629
10676
  }
10630
10677
  module(path) {
10631
10678
  return this.base.stat(path).then((st) => {
@@ -11085,6 +11132,7 @@ var Launcher = class {
11085
11132
  }
11086
11133
  async starting(argv, dir) {
11087
11134
  this.catalog = void 0;
11135
+ this.binfmts = void 0;
11088
11136
  const cwd = this.fs.resolvePath("/", dir ?? "/");
11089
11137
  const [file = ""] = argv;
11090
11138
  await this.base.mkdir(cwd, { recursive: true });