@ai-ecoverse/slicc-kernel 1.110.6 → 1.110.7

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
@@ -227,6 +227,8 @@ A command's name is any single path component without whitespace or control char
227
227
 
228
228
  **More roots:** `createKernel({ commands: { append: ['/opt/user/node_modules'] } })` (and `createNodeKernel`) scans more directories, each absolute and normalised (anything else throws a `TypeError`). A name is looked up in a user's own global packages first, then in `modules`, then in root's global packages, then in the appended roots in the order given, and the first match wins, so an appended root never shadows another command (or `sudo`). Their commands are in `/usr/bin` for every user, like root's global packages, and their `binfmt` and file system entries count too. A package in an appended root also gets a command for each `bin` entry of its `package.json` (a string `bin` is named after the package, without its scope), which runs as a `#!` script; a name its `slicc.commands` gives wins. A `bin` entry is skipped when its name isn't a command name (see above), or its file, with symlinks resolved, isn't a regular file inside the package. `/node_modules` and the global packages keep reading only `slicc.commands`. A missing root counts as empty until it appears, and an install or removal in a root is picked up as it happens. With `users: 'enforce'` a root counts only when it and every directory above it are root's and not writable by group or others; another root is skipped without a word. Under `'report'` such a root is still scanned, and reported once to `onDenial` and the report log as `{ pid: 0, uid: 0, op: 'commands', path, errno: 'EACCES' }`. Running a `bin` command needs `r` and `x` on its file under `users` (see [Permissions](#permissions)), checked when it runs, so a `0644` file is `EACCES` under `'enforce'`.
229
229
 
230
+ **Running a glue by path:** executing a package's Emscripten glue by its path runs the command that names it (the one named like the file, when several share it), even with no `.wasm` beside it. A byte-identical copy of the glue in the same directory, named for one of the package's commands, runs as that command too ([#899](https://github.com/ai-ecoverse/slicc-kernel/issues/899)). wasm-clang ships `bin/clang`, `bin/wasm-ld` and the rest as copies of `bin/llvm`, and emcc executes `$LLVM_ROOT/clang`. The caller must be able to read the copy. A copy elsewhere, or a same-named file whose bytes differ, is not the command. The comparison is cached by path, size, mtime and inode.
231
+
230
232
  **Variants by environment** ([homescoop#357](https://github.com/ai-ecoverse/homescoop/issues/357)): one package can carry more than one build of a wasm command and choose between them by the environment. `"variants": [{ "env": { "PYTHON_FORK": "true" }, "wasm": "bin/python-fork.wasm" }]` on a command entry (Emscripten or WASI) is matched in order against the environment the new process gets, at spawn and at exec. Every key must equal its value exactly, and the first match runs its `wasm`; no match runs the command's own `wasm`. This holds however the kernel reaches the entry: by name, by its path (`/usr/bin/python3`, as `sys.executable` gives it to `subprocess` and `multiprocessing`), or through a symlink to that path (a venv's `bin/python`). So `PYTHON_FORK=true python3`, `PYTHON_FORK=true exec /usr/bin/python3` and a child that inherits `PYTHON_FORK=true` all get the variant, and `PYTHON_FORK=1` doesn't. Commands of one package that share a build (`python`, `python3`, `pip`, `pip3`) each carry the same `variants`. A variant replaces only the module: the `abi`, the glue, `env`, `argv0`, `argv0Path` and the `/usr/bin` entry stay the command's. For an Emscripten command that means a variant swaps only the `.wasm` and keeps the command's JS glue, so it must be built against that same glue; a WASI or WASIX command has no glue, so there's no such constraint. Only the chosen module is read, checked and compiled, and compiled modules are cached by its path. A read or `stat` of `/usr/bin/<name>` still shows the default `wasm`. A `variants` that isn't an array, or has an entry without an `env` object of strings and a `wasm` inside the package, is ignored as a whole, with one warning per command, and the command runs its `wasm`. An older kernel ignores the field.
231
233
 
232
234
  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 of two packages that map one suffix, the one whose name sorts first wins, as for commands; the kernel warns once (`console.warn`) when another package maps that suffix to a different command. A suffix starts with a dot and the command must be a name the catalog resolves when the file runs.
@@ -6176,7 +6176,7 @@ function satisfies(version, range2) {
6176
6176
  }
6177
6177
 
6178
6178
  // src/version.ts
6179
- var KERNEL_VERSION = true ? "1.110.6" : "0.0.0-dev";
6179
+ var KERNEL_VERSION = true ? "1.110.7" : "0.0.0-dev";
6180
6180
  var OS_RELEASE = "6.0.0-slicc";
6181
6181
  function procVersion() {
6182
6182
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -22576,13 +22576,34 @@ var Launcher = class {
22576
22576
  return { abi: "emscripten", glue, wasm, argv0: baseName2(argv0 || file), ...asked };
22577
22577
  }
22578
22578
  const owner = await this.glueOwner(glue, baseName2(file), scope);
22579
- return owner && { ...targetOf(owner), ...asked };
22579
+ if (!owner) return void 0;
22580
+ return { ...targetOf(owner), ...asked, ...owner.glue === glue ? {} : { copy: glue } };
22580
22581
  }
22581
22582
  async glueOwner(glue, name2, scope) {
22582
- const owners = [...(await this.commands(scope)).values()].filter(
22583
- (c) => c.abi === "emscripten" && !c.script && c.glue === glue
22584
- );
22585
- return owners.find((c) => c.name === name2) ?? owners[0];
22583
+ const commands = [...(await this.commands(scope)).values()];
22584
+ const owners = commands.filter((c) => c.abi === "emscripten" && !c.script && c.glue === glue);
22585
+ if (owners.length > 0) return owners.find((c) => c.name === name2) ?? owners[0];
22586
+ const leaf = baseName2(glue);
22587
+ const dir = glue.slice(0, glue.lastIndexOf("/"));
22588
+ for (const c of commands) {
22589
+ if (c.abi !== "emscripten" || c.script || c.name !== leaf) continue;
22590
+ const own = await physicalPathAsync(this.base, c.glue).catch(() => c.glue);
22591
+ if (own.slice(0, own.lastIndexOf("/")) !== dir) continue;
22592
+ if (await this.sameFile(glue, own)) return c;
22593
+ }
22594
+ return void 0;
22595
+ }
22596
+ sameFiles = /* @__PURE__ */ new Map();
22597
+ async sameFile(a, b) {
22598
+ const [x, y] = await Promise.all([this.base.stat(a), this.base.stat(b)]).catch(() => []);
22599
+ if (!x?.isFile || !y?.isFile || x.size !== y.size) return false;
22600
+ const stamp = [x, y].map((st) => `${st.size}:${st.mtime.getTime()}:${st.ino}`).join("|");
22601
+ const known = this.sameFiles.get(`${a}|${b}`);
22602
+ if (known?.stamp === stamp) return known.same;
22603
+ const [p, q] = await Promise.all([this.base.readFileBuffer(a), this.base.readFileBuffer(b)]);
22604
+ const same = p.length === q.length && p.every((byte, i) => byte === q[i]);
22605
+ this.sameFiles.set(`${a}|${b}`, { stamp, same });
22606
+ return same;
22586
22607
  }
22587
22608
  async wasmOwner(wasm, name2, scope) {
22588
22609
  const leaf = baseName2(wasm);
@@ -22680,6 +22701,7 @@ var Launcher = class {
22680
22701
  await need(g, "exec", this.fs.resolvePath(req.cwd, file), want);
22681
22702
  }
22682
22703
  for (const bin of planned?.bins ?? []) await need(g, "exec", bin, R_OK | X_OK);
22704
+ if (planned?.target.copy) await need(g, "exec", planned.target.copy, R_OK | X_OK);
22683
22705
  }
22684
22706
  async checkedPlan(req, { ppid, cred, scope }, fds) {
22685
22707
  const refused2 = async (err) => {