buninu-linux 0.9.56 → 0.11.56

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.
Files changed (50) hide show
  1. package/LICENSES/MIT-ZilchOS-bootstrap-from-tcc.txt +21 -0
  2. package/NOTICE.md +48 -2
  3. package/README.md +81 -12
  4. package/initramfs/bin/buninu-land +26 -0
  5. package/initramfs/bin/chroot +68 -47
  6. package/initramfs/buninu/CHANGELOG.md +22 -0
  7. package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
  8. package/initramfs/buninu/apps/jsgotty/package.json +1 -1
  9. package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +1 -0
  10. package/initramfs/buninu/package.json +1 -1
  11. package/initramfs/lib/bunterm/geometry.js +15 -0
  12. package/initramfs/lib/bunterm/pointer-report.js +10 -0
  13. package/initramfs/lib/bunterm/terminal.js +5 -4
  14. package/initramfs/lib/chroot-options.js +69 -0
  15. package/initramfs/usr/lib/bootstrap/1.prepare-rootfs.js +106 -0
  16. package/initramfs/usr/lib/bootstrap/10.build-m4.js +40 -0
  17. package/initramfs/usr/lib/bootstrap/11.build-flex.js +50 -0
  18. package/initramfs/usr/lib/bootstrap/12.build-gcc.js +261 -0
  19. package/initramfs/usr/lib/bootstrap/2.enter-rootfs.js +49 -0
  20. package/initramfs/usr/lib/bootstrap/3.gen-musl-dev.js +141 -0
  21. package/initramfs/usr/lib/bootstrap/4.build-tcc.js +61 -0
  22. package/initramfs/usr/lib/bootstrap/5.build-make.js +58 -0
  23. package/initramfs/usr/lib/bootstrap/6.build-binutils.js +77 -0
  24. package/initramfs/usr/lib/bootstrap/7.build-gmp.js +68 -0
  25. package/initramfs/usr/lib/bootstrap/8.build-mpfr.js +59 -0
  26. package/initramfs/usr/lib/bootstrap/9.build-mpc.js +55 -0
  27. package/initramfs/usr/lib/bootstrap/LICENSE +21 -0
  28. package/initramfs/usr/lib/bootstrap/LICENSES/MIT-ZilchOS-bootstrap-from-tcc.txt +21 -0
  29. package/initramfs/usr/lib/bootstrap/NOTICE.md +65 -0
  30. package/initramfs/usr/lib/bootstrap/README.md +204 -0
  31. package/initramfs/usr/lib/bootstrap/downloads.md +49 -0
  32. package/initramfs/usr/lib/bootstrap/lib/build.js +155 -0
  33. package/initramfs/usr/lib/bootstrap/lib/common.js +134 -0
  34. package/initramfs/usr/lib/bootstrap/lib/compile-object.c +14 -0
  35. package/initramfs/usr/lib/bootstrap/lib/musl-headers.js +59 -0
  36. package/initramfs/usr/lib/bootstrap/lib/seed.js +148 -0
  37. package/initramfs/usr/lib/bootstrap/lib/startup-check.c +14 -0
  38. package/initramfs/usr/lib/bootstrap/lib/tcc-build.js +53 -0
  39. package/initramfs/usr/lib/bootstrap/sources/musl-1.2.6.tar.gz +0 -0
  40. package/initramfs/usr/lib/bootstrap/sources/tinycc-05f0faf-upstream.tar.gz +0 -0
  41. package/initramfs/usr/lib/bootstrap/sources/tinycc-local-bootstrap-files.tar.gz +0 -0
  42. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +4 -1
  43. package/initramfs/usr/share/doc/buninu-linux/chroot.md +37 -3
  44. package/package.json +1 -1
  45. package/scripts/npm-packages.md +8 -3
  46. package/test/bootstrap.test.js +65 -0
  47. package/test/bunterm-pixelmouse.test.js +27 -0
  48. package/test/chroot-bind.test.js +70 -0
  49. package/test/fetch-npm.test.js +2 -2
  50. package/test/stage-npm.test.js +42 -1
@@ -0,0 +1,148 @@
1
+ import {
2
+ chmodSync, cpSync, mkdirSync, readFileSync, readdirSync,
3
+ renameSync, symlinkSync,
4
+ } from "node:fs";
5
+ import { join } from "node:path";
6
+ import {
7
+ bootstrap, bun, download, elfInterpreter, run, sha256,
8
+ validateElf, write,
9
+ } from "./common.js";
10
+
11
+ const tools = [
12
+ "bin/tar", "bin/unzip", "usr/bin/env",
13
+ "lib/crc32.js", "lib/document.js", "lib/tar-strip.js", "lib/tar-exclude.js",
14
+ "usr/share/doc/buninu-linux/tar.md",
15
+ ];
16
+ const shell = "buninu/apps/bunmsh/";
17
+
18
+ function addTree(files, directory, prefix) {
19
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
20
+ const path = join(directory, entry.name), target = prefix + entry.name;
21
+ if (entry.isDirectory()) addTree(files, path, target + "/");
22
+ else files.set(target, readFileSync(path)); // Resolve library/source symlinks.
23
+ }
24
+ }
25
+
26
+ export function localSeed(arch) {
27
+ const files = new Map();
28
+ for (const path of tools) files.set(path, readFileSync("/" + path));
29
+ addTree(files, "/" + shell, shell);
30
+ for (const name of ["libgcc_s.so.1", "libstdc++.so.6"]) {
31
+ let bytes;
32
+ try { bytes = readFileSync("/lib/" + name); }
33
+ catch (error) { if (error.code !== "ENOENT") throw error; bytes = readFileSync("/usr/lib/" + name); }
34
+ files.set("usr/lib/" + name, bytes);
35
+ }
36
+ files.set(`lib/ld-musl-${arch}.so.1`, readFileSync(`/lib/ld-musl-${arch}.so.1`));
37
+ files.set("bin/bun", readFileSync("/bin/bun"));
38
+ files.set("etc/resolv.conf", readFileSync("/etc/resolv.conf"));
39
+ return files;
40
+ }
41
+
42
+ export async function releaseSeed(arch, work) {
43
+ const tarball = await download("buninu-linux@0.9.56");
44
+ // Bun's archive reader works in-process; no host tar is required.
45
+ const entries = await new Bun.Archive(readFileSync(tarball)).files();
46
+ const get = async name => {
47
+ const file = entries.get("package/" + name);
48
+ if (!file) throw new Error(`Pinned buninu-linux tarball is missing ${name}`);
49
+ return Buffer.from(await file.arrayBuffer());
50
+ };
51
+ const files = new Map();
52
+ for (const path of tools) files.set(path, await get("initramfs/" + path));
53
+ for (const [name, file] of entries) {
54
+ const prefix = "package/initramfs/";
55
+ if (name.startsWith(prefix + shell)) files.set(name.slice(prefix.length), Buffer.from(await file.arrayBuffer()));
56
+ }
57
+ for (const name of ["libgcc_s.so.1", "libstdc++.so.6"]) {
58
+ files.set("usr/lib/" + name, await get(`native/${arch}/lib/${name}`));
59
+ }
60
+ const loader = `ld-musl-${arch}.so.1`;
61
+ files.set("lib/" + loader, await get(`native/${arch}/lib/${loader}`));
62
+ files.set("etc/resolv.conf", await get("initramfs/etc/resolv.conf"));
63
+
64
+ console.log("② Extracting pinned musl-bun");
65
+ const build = arch === "aarch64" ? "bun-linux-aarch64-musl" : "bun-linux-x64-musl-baseline";
66
+ const zip = await download(`${build}@1.4.2`);
67
+ // unzip is a CLI with top-level process.exit; invoke it separately.
68
+ for (const path of ["bin/unzip", "lib/crc32.js"]) write(join(work, path), files.get(path));
69
+ await run([bun(), join(work, "bin/unzip"), zip, `${build}/bun`, "-d", work]);
70
+ files.set("bin/bun", readFileSync(join(work, build, "bun")));
71
+ return files;
72
+ }
73
+
74
+ export function validateSeed(files, arch) {
75
+ const loader = `lib/ld-musl-${arch}.so.1`;
76
+ for (const name of ["bin/bun", loader, "usr/lib/libgcc_s.so.1", "usr/lib/libstdc++.so.6"]) {
77
+ validateElf(name, files.get(name), arch);
78
+ }
79
+ if (elfInterpreter(files.get("bin/bun")) !== "/" + loader) throw new Error("Bun must use the target musl loader");
80
+ for (const name of ["index.js", "package.json", "src/main.js"]) {
81
+ if (!files.has(shell + name)) throw new Error(`Missing bunmsh ${name}`);
82
+ }
83
+ for (const name of files.keys()) {
84
+ if (name.includes("\\") || name.includes("\0") || name.split("/").some(p => ["", ".", ".."].includes(p))) {
85
+ throw new Error(`Unsafe seed path: ${name}`);
86
+ }
87
+ }
88
+ }
89
+
90
+ export function writeSeed(root, arch, files, origin) {
91
+ mkdirSync(root);
92
+ for (const name of ["bin", "lib", "etc", "usr/lib", "usr/bin", "tmp", "root", "dev", "proc", "sys", "run"]) {
93
+ mkdirSync(join(root, name), { recursive: true });
94
+ }
95
+ for (const [name, bytes] of files) {
96
+ const executable = name.startsWith("bin/") || name.startsWith("usr/bin/")
97
+ || name === shell + "index.js" || /\.so\.[16]$/.test(name);
98
+ write(join(root, name), bytes, executable ? 0o755 : 0o644);
99
+ }
100
+ chmodSync(join(root, "tmp"), 0o1777);
101
+ write(join(root, "bin/sh"), `#!/bin/bun
102
+ // Guest entry shell; build stages use their own shell and explicit PATH.
103
+ const paths = [
104
+ "/bootstrap/result/bin",
105
+ "/bootstrap/toolchain/bin",
106
+ ...(process.env.PATH || "/bin:/usr/bin").split(":"),
107
+ ];
108
+ process.env.PATH = [...new Set(paths)].join(":");
109
+ await import("/buninu/apps/bunmsh/src/main.js");
110
+ `, 0o755);
111
+ symlinkSync(`../../lib/ld-musl-${arch}.so.1`, join(root, "usr/lib/libc.so"));
112
+ symlinkSync("libgcc_s.so.1", join(root, "usr/lib/libgcc_s.so"));
113
+ symlinkSync("libc.so", join(root, "usr/lib/libm.so"));
114
+ write(join(root, "etc/buninu-bootstrap.json"), JSON.stringify({ format: 1, arch, origin }, null, 2) + "\n");
115
+ }
116
+
117
+ const sources = [
118
+ ["musl-1.2.6.tar.gz", "d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a"],
119
+ ["tinycc-05f0faf-upstream.tar.gz", "300d4fd6fc4a7294403e17bfd29201954410ac2baa94f189526f6bef83524c19"],
120
+ ["tinycc-local-bootstrap-files.tar.gz", "59216479774f3c61b06420ca9b03ad4b8dcbdbb242d2d236d2daace135e35d59"],
121
+ ];
122
+
123
+ export function sourceArchives() {
124
+ return sources.map(([name, hash]) => {
125
+ const bytes = readFileSync(join(bootstrap, "sources", name));
126
+ if (sha256(bytes) !== hash) throw new Error(`Bundled source hash mismatch: ${name}`);
127
+ return new Bun.Archive(bytes);
128
+ });
129
+ }
130
+
131
+ export async function installSources(root, archives) {
132
+ const src = join(root, "bootstrap/src");
133
+ mkdirSync(src, { recursive: true });
134
+ await archives[0].extract(src);
135
+ renameSync(join(src, "musl-1.2.6"), join(src, "musl"));
136
+ await archives[1].extract(src);
137
+ renameSync(join(src, "tinycc-05f0faf"), join(src, "tinycc"));
138
+ await archives[2].extract(root);
139
+ // The overlay provides the cc wrapper. It becomes usable at stage 4.
140
+ chmodSync(join(root, "bin/cc"), 0o755);
141
+ // Copy the recipes/helpers, but do not duplicate the extracted tarballs.
142
+ const target = join(root, "bootstrap");
143
+ mkdirSync(target, { recursive: true });
144
+ const stages = readdirSync(bootstrap).filter(name => /^(?:[1-9]|1[0-2])\..*\.js$/.test(name));
145
+ for (const name of [...stages, "lib", "README.md", "downloads.md", "NOTICE.md", "LICENSE", "LICENSES"]) {
146
+ cpSync(join(bootstrap, name), join(target, name), { recursive: true, errorOnExist: true, force: false });
147
+ }
148
+ }
@@ -0,0 +1,14 @@
1
+ #include <stdio.h>
2
+ #include <stdlib.h>
3
+ #include <string.h>
4
+ static int initialized;
5
+ __attribute__((constructor)) static void init(void) { initialized = 37; }
6
+ __attribute__((destructor)) static void fini(void) { puts("destructor OK"); }
7
+ int main(int argc, char **argv) {
8
+ if (initialized != 37) return 1;
9
+ if (argc != 3 || strcmp(argv[1], "hello") || strcmp(argv[2], "two words")) return 2;
10
+ const char *value = getenv("CRT_TEST");
11
+ if (!value || strcmp(value,"seed-free")) return 3;
12
+ puts("argc/argv, environment, constructor OK");
13
+ return 42;
14
+ }
@@ -0,0 +1,53 @@
1
+ // Standalone TinyCC build used by stage 4 inside the target musl rootfs.
2
+ import { chmodSync, cpSync, existsSync, mkdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { bun, run } from "./common.js";
5
+
6
+ export async function buildTcc() {
7
+ const root = "/bootstrap/src/tinycc";
8
+ if (!["x64", "arm64"].includes(process.arch)) throw new Error("Unsupported architecture");
9
+ const work = "/bootstrap/build/tinycc";
10
+ const runtime = "/bootstrap/toolchain/lib/tcc";
11
+ mkdirSync(work, { recursive: true });
12
+ mkdirSync(runtime, { recursive: true });
13
+ mkdirSync("/bootstrap/toolchain/bin", { recursive: true });
14
+ cpSync(join(root, "include"), join(runtime, "include"), { recursive: true });
15
+ const stage1 = "/bootstrap/toolchain/bin/tcc";
16
+ const stage2 = join(work, "tcc-stage2");
17
+ const flags = ["-B/bootstrap/toolchain/lib/tcc", "-nostdinc", "-I/usr/include", "-I/bootstrap/toolchain/lib/tcc/include", "-L/usr/lib", "-L/lib"];
18
+
19
+ console.log("1. Build standalone TinyCC using Bun.cc");
20
+ await run([bun(), join(root, "bun-bootstrap.js"), join(root, "bun-tcc-cli.c"), stage1, "0"]);
21
+ await run([stage1, "-v"]);
22
+
23
+ if (process.arch === "x64") {
24
+ console.log("2. Build x64 helpers for programs compiled by cc");
25
+ const objects = [];
26
+ for (const name of ["libtcc1", "va_list"]) {
27
+ const object = join(work, `${name}.o`);
28
+ await run([stage1, ...flags, "-c", join(root, "lib", `${name}.c`), "-o", object]);
29
+ objects.push(object);
30
+ }
31
+ await run([stage1, "-ar", "rcs", join(runtime, "libbootstrap-tcc-x64.a"), ...objects]);
32
+ }
33
+
34
+ console.log("3. Rebuild TinyCC with stage1");
35
+ await run([stage1, ...flags, join(root, "bun-tcc-cli.c"), "-lgcc_s", "-o", stage2]);
36
+ const a = await Bun.file(stage1).bytes();
37
+ const b = await Bun.file(stage2).bytes();
38
+ if (!Buffer.from(a).equals(Buffer.from(b))) throw new Error("stage1/stage2 differ");
39
+ console.log("stage1/stage2 byte-identical: true");
40
+
41
+ if (!existsSync("/bin/cc")) throw new Error("Extract the overlay's bin/cc first");
42
+ chmodSync("/bin/cc", 0o755);
43
+ console.log("4. Compile and run the libc smoke test through cc");
44
+ await run(["/bin/cc", join(root, "bun-hello-libc.c"), "-o", join(work, "hello-cc")]);
45
+ await run([join(work, "hello-cc")]);
46
+ if (process.arch === "x64") {
47
+ console.log("5. Check x64 conversions and variadic calls");
48
+ await run(["/bin/cc", join(root, "bun-runtime-check.c"), "-o", join(work, "runtime-check")]);
49
+ await run([join(work, "runtime-check")]);
50
+ }
51
+ console.log("Ready: /bin/cc (TinyCC stage1)");
52
+ }
53
+
@@ -82,7 +82,10 @@ Every pointing device under `/dev/input/event*` is opened and an arrow is
82
82
  drawn on the screen. Clicks, drags and wheel notches reach the program as
83
83
  the xterm mouse sequences it asked for: nothing is sent unless the program
84
84
  enabled reporting (`DECSET 1000`, `1002` or `1003`), and what is sent follows
85
- the mode it chose, SGR (`1006`) included. `jsmdcui`, `micro`, `htop`, `vim`
85
+ the mode it chose, including SGR cell coordinates (`1006`) and SGR pixel
86
+ coordinates (`1016`). Mode 1016 also answers DECRQM queries; pixel positions
87
+ exclude the framebuffer margins and preserve movement within a cell.
88
+ CSI 14t/16t report the content and cell dimensions in those same pixel units. `jsmdcui`, `micro`, `htop`, `vim`
86
89
  and anything else that speaks the protocol work with no configuration.
87
90
 
88
91
  When the program is not reading the mouse, the wheel scrolls instead: back
@@ -11,7 +11,7 @@ every mount it made when the command exits.
11
11
 
12
12
  ```sh
13
13
  chroot NEWROOT [COMMAND [ARGUMENT...]]
14
- chroot [-n] [-k] [-v] NEWROOT [COMMAND [ARGUMENT...]]
14
+ chroot [-n] [-k] [-v] [-b HOST[:GUEST]] [--shared-tmp] NEWROOT [COMMAND [ARGUMENT...]]
15
15
  chroot -h | --help
16
16
  ```
17
17
 
@@ -30,7 +30,7 @@ With no COMMAND it runs `$SHELL` if that exists inside NEWROOT, otherwise
30
30
  | `/tmp` | `tmpfs` | scratch space that does not touch the root filesystem |
31
31
  | `/etc/resolv.conf` | bind of the host's file | name resolution, without editing anything inside |
32
32
 
33
- A mount point that already exists as a mount is left alone. A directory that
33
+ For automatic mounts, a destination that is already mounted is left alone. A directory that
34
34
  is missing is created; if the root filesystem is read-only and that fails,
35
35
  that one mount is skipped and the rest still happen — use `-v` to see which.
36
36
 
@@ -42,11 +42,30 @@ host's `/dev` with it.
42
42
 
43
43
  | Option | Meaning |
44
44
  |---|---|
45
- | `-n`, `--no-mount` | Do not mount anything: the behaviour of the traditional `chroot`. Note that Bun itself cannot start without `/proc` and `/dev/urandom`, so `-n` suits statically linked programs. |
45
+ | `-n`, `--no-mount` | Skip automatic system mounts; explicit `-b` mounts still apply. Without binds, this is the behaviour of the traditional `chroot`. Note that Bun itself cannot start without `/proc` and `/dev/urandom`, so `-n` suits statically linked programs. |
46
+ | `-b HOST[:GUEST]`, `--bind HOST[:GUEST]` | Bind a host directory or file into the new root; repeat for multiple paths. Without GUEST, use HOST’s absolute path inside the guest. |
47
+ | `--shared-tmp` | Equivalent to `-b "$TMPDIR:/tmp"`; TMPDIR must be set and exist. Replaces the automatic `/tmp` tmpfs. |
46
48
  | `-k`, `--keep-mounts` | Leave the mounts in place after the command exits, to enter the same root again cheaply. Clean up later with `umount -R`. |
47
49
  | `-v`, `--verbose` | Report every mount and unmount. |
48
50
  | `--` | End of options; everything after it is NEWROOT and COMMAND. |
49
51
 
52
+ Bind options must appear before NEWROOT. Host paths may be relative to the
53
+ current directory; guest paths must be absolute. Missing destination directories
54
+ and file placeholders are created. Guest symlinks are resolved relative to
55
+ NEWROOT, including absolute symlinks. Binding over the guest root `/` is rejected.
56
+
57
+ Explicit binds replace automatic mounts at or below their destination. Directories
58
+ are bound recursively and made slave mounts, like `/dev`; files use a plain bind.
59
+ A repeated destination uses the last argument, so a later `-b HOST:/tmp` can
60
+ override `--shared-tmp` (and vice versa). Parent binds are applied before children.
61
+ Explicit bind failures abort with status 125 and clean up this invocation's mounts,
62
+ even with `-k`. Existing mounts may be covered by a new explicit bind and are
63
+ revealed again when it is removed. Source data is shared, not copied.
64
+
65
+ `--shared-tmp` only adds the bind; it does not rewrite environment variables.
66
+ If the host's TMPDIR path is different from `/tmp`, applications in the guest may
67
+ need `TMPDIR=/tmp` in their environment.
68
+
50
69
  ## Examples
51
70
 
52
71
  ```sh
@@ -63,6 +82,21 @@ chroot -v /mnt /bin/sh -c 'ls /proc | head -3'
63
82
  # Traditional behaviour: nothing is mounted for you
64
83
  chroot -n /mnt /bin/sh
65
84
 
85
+ # Share a host directory at a different guest path
86
+ chroot -b /home/user/project:/work /mnt /bin/sh
87
+
88
+ # Share the same absolute path inside the guest
89
+ chroot -b /home/user/project /mnt /bin/sh
90
+
91
+ # Share files, or multiple paths
92
+ chroot -b /home/user/project:/work -b /etc/hosts:/etc/hosts /mnt /bin/sh
93
+
94
+ # Share the host temporary directory (including Unix sockets)
95
+ export TMPDIR=/tmp
96
+ chroot --shared-tmp /mnt /bin/sh
97
+ # Equivalent explicit bind:
98
+ chroot -b "$TMPDIR:/tmp" /mnt /bin/sh
99
+
66
100
  # Keep the mounts for repeated visits
67
101
  chroot -k /mnt /bin/sh -c 'true'
68
102
  chroot -k /mnt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.9.56",
3
+ "version": "0.11.56",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -1,14 +1,19 @@
1
1
  # npm packages
2
2
  # Latest versions checked on 2026-10-11; SHA-256 pins verified against registry SHA-512 integrity.
3
3
  # buninu-browser updated and verified on 2026-10-12.
4
+ # casty 1.3.6 and buninu-land 0.1.2 verified on 2026-10-16.
4
5
 
5
6
  "buninu-browser@0.2.3":
6
7
  - https://registry.npmjs.org/buninu-browser/-/buninu-browser-0.2.3.tgz
7
8
  - sha256:0f7c9cd9df689d7b0900fdf3172851125ebd8aba12bed555be7e091d427146e8
8
9
 
9
- "@drxiaozhi/casty@1.3.4":
10
- - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.4.tgz
11
- - sha256:1899179d173702721c2f6e8bbffab51ca3421e899ac59c58b2588cfc342fa0df
10
+ "buninu-land@0.1.2":
11
+ - https://registry.npmjs.org/buninu-land/-/buninu-land-0.1.2.tgz
12
+ - sha256:0b682e1d57f953d9fa9415369c320e4e35bf7575c2859673c26a0789f5369490
13
+
14
+ "@drxiaozhi/casty@1.3.6":
15
+ - https://registry.npmjs.org/@drxiaozhi/casty/-/casty-1.3.6.tgz
16
+ - sha256:2b5f84e3a63c2c16fa92dc88faa43eec54edc2fe69154e5270af3cd9e7f472b8
12
17
 
13
18
  "@drxiaozhi/jspulse@0.2.1":
14
19
  - https://registry.npmjs.org/@drxiaozhi/jspulse/-/jspulse-0.2.1.tgz
@@ -0,0 +1,65 @@
1
+ // Run with Bun inside bunproot or the Buninu Linux VM.
2
+ import { afterAll, expect, test } from "bun:test";
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import {
7
+ assertAbsent, bootstrap, download, hasBuninuId, normalizeArch, options,
8
+ } from "../initramfs/usr/lib/bootstrap/lib/common.js";
9
+
10
+ const work = mkdtempSync(join(tmpdir(), "bootstrap-test-"));
11
+ afterAll(() => rmSync(work, { recursive: true, force: true }));
12
+
13
+ test("architecture aliases and optional destination", () => {
14
+ for (const alias of ["a", "arm64", "aarch64"]) {
15
+ expect(options([alias])).toEqual({ arch: "aarch64", rootfs: resolve("roota") });
16
+ }
17
+ for (const alias of ["x", "x64", "x86_64", "x86-64"]) {
18
+ expect(options([alias])).toEqual({ arch: "x86_64", rootfs: resolve("rootx") });
19
+ }
20
+ const arch = normalizeArch(process.arch);
21
+ expect(options([])).toEqual({ arch, rootfs: resolve(arch === "aarch64" ? "roota" : "rootx") });
22
+ expect(options(["./a path's root"])).toEqual({ arch, rootfs: resolve("./a path's root") });
23
+ expect(options(["x", "./custom"])).toEqual({ arch: "x86_64", rootfs: resolve("custom") });
24
+ expect(() => options(["--arch", "x"])).toThrow();
25
+ expect(() => options(["x", "one", "two"])).toThrow();
26
+ expect(() => normalizeArch("riscv64")).toThrow();
27
+ });
28
+
29
+ test("local mode requires the exact ID, never NAME or a substring", () => {
30
+ for (const value of ["buninu-linux", '"buninu-linux"', "'buninu-linux'"]) {
31
+ expect(hasBuninuId(`NAME="Buninu Linux"\nID=${value}\n`)).toBe(true);
32
+ }
33
+ for (const text of [
34
+ 'NAME="Buninu Linux"\nID=alpine\n',
35
+ 'ID_LIKE=buninu-linux\n', '# ID=buninu-linux\n',
36
+ 'ID=buninu-linux-other\n', 'ID=buninu-linux\nID=alpine\n',
37
+ 'ID="$(echo buninu-linux)"\n',
38
+ ]) expect(hasBuninuId(text)).toBe(false);
39
+ });
40
+
41
+ test("existing destinations, including dangling links, are refused", () => {
42
+ const file = join(work, "file"), link = join(work, "link");
43
+ writeFileSync(file, "keep me");
44
+ symlinkSync("missing", link);
45
+ expect(() => assertAbsent(file)).toThrow("Refusing existing destination");
46
+ expect(() => assertAbsent(link)).toThrow("Refusing existing destination");
47
+ expect(() => assertAbsent(join(work, "new"))).not.toThrow();
48
+ expect(readFileSync(file, "utf8")).toBe("keep me");
49
+ });
50
+
51
+ test("a corrupt cached tarball is rejected before extraction or refetch", async () => {
52
+ const old = process.env.XDG_CACHE_HOME;
53
+ process.env.XDG_CACHE_HOME = work;
54
+ try {
55
+ const [url, pin] = Bun.YAML.parse(readFileSync(join(bootstrap, "downloads.md"), "utf8"))["buninu-linux@0.9.56"];
56
+ const cache = join(work, "buninu-bootstrap");
57
+ mkdirSync(cache);
58
+ writeFileSync(join(cache, pin.slice(7) + "-" + new URL(url).pathname.split("/").at(-1)), "corrupt");
59
+ await expect(download("buninu-linux@0.9.56")).rejects.toThrow("Cached file hash mismatch");
60
+ await expect(download("unlisted@version")).rejects.toThrow("Missing or invalid download pin");
61
+ } finally {
62
+ if (old === undefined) delete process.env.XDG_CACHE_HOME;
63
+ else process.env.XDG_CACHE_HOME = old;
64
+ }
65
+ });
@@ -0,0 +1,27 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {Terminal} from '../initramfs/lib/xterm/xterm-headless.mjs';
3
+ import {terminalMouseEvent} from '../initramfs/lib/bunterm/pointer-report.js';
4
+ test('bunterm negotiates 1016 and emits real pixel press, release and same-cell motion',async()=>{
5
+ const term=new Terminal({cols:80,rows:24,allowProposedApi:true}),out=[];term.onData(s=>out.push(s));
6
+ const write=s=>new Promise(r=>term.write(s,r));
7
+ const renderer={offsetX:5,offsetY:7,cellWidth:10,cellHeight:20};
8
+ const send=(x,y,action)=>term._core.coreMouseService.triggerMouseEvent(terminalMouseEvent({x,y},renderer,0,action));
9
+ try{
10
+ await write('\x1b[?1016$p');expect(out.pop()).toBe('\x1b[?1016;2$y');
11
+ await write('\x1b[?1003h\x1b[?1006h\x1b[?1016h\x1b[?1016$p');expect(out.pop()).toBe('\x1b[?1016;1$y');
12
+ send(30,68,1);expect(out.pop()).toBe('\x1b[<0;25;61M');
13
+ send(30,68,0);expect(out.pop()).toBe('\x1b[<0;25;61m');
14
+ send(31,68,32);expect(out.pop()).toBe('\x1b[<32;26;61M');
15
+ send(32,68,32);expect(out.pop()).toBe('\x1b[<32;27;61M');
16
+ await write('\x1b[?1016l\x1b[?1006h');send(30,68,1);expect(out.pop()).toBe('\x1b[<0;3;4M');
17
+ }finally{term.dispose();}
18
+ });
19
+
20
+ test('geometry queries use terminal content pixels and exclude framebuffer margins',async()=>{
21
+ const {attachGeometry}=await import('../initramfs/lib/bunterm/geometry.js');
22
+ const term=new Terminal({cols:80,rows:24,allowProposedApi:true}),out=[];
23
+ const renderer={offsetX:5,offsetY:7,cellWidth:10,cellHeight:20};
24
+ const handler=attachGeometry({term,renderer,reply:s=>out.push(s)});
25
+ try{await new Promise(r=>term.write('\x1b[14t\x1b[16t',r));expect(out).toEqual(['\x1b[4;480;800t','\x1b[6;20;10t']);}
26
+ finally{handler.dispose();term.dispose();}
27
+ });
@@ -0,0 +1,70 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,symlinkSync,rmSync} from 'node:fs';
3
+ import {tmpdir} from 'node:os';
4
+ import {join} from 'node:path';
5
+ import {parseChrootArguments,resolveBindings,resolveGuestPath} from '../initramfs/lib/chroot-options.js';
6
+
7
+ test('chroot bind arguments expand shared tmp, repeat, and stop at NEWROOT',()=>{
8
+ const result=parseChrootArguments(['-n','-b','/host:/guest','--shared-tmp','--bind=/same','/root','echo','-b','unchanged'],{TMPDIR:'/host temp'});
9
+ expect(result.options).toMatchObject({mount:false,binds:['/host:/guest','/host temp:/tmp','/same']});
10
+ expect(result.positional).toEqual(['/root','echo','-b','unchanged']);
11
+ expect(()=>parseChrootArguments(['--shared-tmp','/root'],{})).toThrow('TMPDIR');
12
+ expect(()=>parseChrootArguments(['-b'])).toThrow('requires');
13
+ expect(()=>parseChrootArguments(['--bind='])).toThrow('requires');
14
+ expect(parseChrootArguments(['--','-root','sh']).positional).toEqual(['-root','sh']);
15
+ });
16
+
17
+ test('bind resolution preserves same-path destinations and confines guest symlinks',()=>{
18
+ const dir=mkdtempSync(join(tmpdir(),'buninu-bind-path-'));
19
+ try{
20
+ mkdirSync(join(dir,'root'));mkdirSync(join(dir,'host'));writeFileSync(join(dir,'file'),'data');
21
+ const binds=resolveBindings(['host','file:/etc/config','host:/tmp','file:/tmp'],dir);
22
+ expect(binds.find(b=>b.inside===join(dir,'host'))?.source).toBe(join(dir,'host'));
23
+ expect(binds.find(b=>b.inside==='/tmp')?.file).toBe(true);
24
+ expect(()=>resolveBindings(['host:relative'],dir)).toThrow('absolute');
25
+ expect(()=>resolveBindings(['host:/'],dir)).toThrow('root');
26
+ expect(()=>resolveBindings(['missing:/ok'],dir)).toThrow();
27
+ symlinkSync('/run/tmp',join(dir,'root/tmp'));
28
+ expect(resolveGuestPath(join(dir,'root'),'/tmp')).toBe('/run/tmp');
29
+ symlinkSync('../../outside',join(dir,'root/escape'));
30
+ expect(resolveGuestPath(join(dir,'root'),'/escape')).toBe('/outside');
31
+ }finally{rmSync(dir,{recursive:true,force:true});}
32
+ });
33
+
34
+ // Execute the actual CLI with recorded syscall bindings. No host mounts or
35
+ // chroot occur; this also exercises failure cleanup and the -n interaction.
36
+ async function invoke(argumentsFor,env={}) {
37
+ const dir=mkdtempSync(join(tmpdir(),'buninu-bind-cli-')),root=join(dir,'root'),host=join(dir,'host'),log=join(dir,'calls');
38
+ mkdirSync(root);mkdirSync(host);mkdirSync(join(dir,'fail'));writeFileSync(join(dir,'file'),'data');
39
+ const mock=join(dir,'libc.js');
40
+ writeFileSync(mock,`import {appendFileSync} from 'node:fs';
41
+ const record=(name,...args)=>appendFileSync(${JSON.stringify(log)},JSON.stringify([name,...args.map(a=>typeof a==='bigint'?String(a):a)])+'\\n');
42
+ export const ptr=x=>x,cString=x=>x,errno=()=>1,strerror=()=> 'mock mount denied',Errno={EINVAL:22,EPERM:1};
43
+ export const libc={symbols:{mount:(...a)=>{record('mount',...a);return a[0]?.endsWith('/fail')?-1:0;},umount2:(...a)=>{record('umount',...a);return 0;},chroot:(...a)=>{record('chroot',...a);return 0;}}};`);
44
+ const source=readFileSync(new URL('../initramfs/bin/chroot',import.meta.url),'utf8')
45
+ .replace('"../lib/dlopen.js"',JSON.stringify(mock))
46
+ .replace('"../lib/chroot-options.js"',JSON.stringify(new URL('../initramfs/lib/chroot-options.js',import.meta.url).pathname))
47
+ .replace('import { showDocument } from "../lib/document.js";','const showDocument=async()=>{};');
48
+ const entry=join(dir,'chroot.mjs');writeFileSync(entry,source);
49
+ try{
50
+ const child=Bun.spawn([process.execPath,entry,...argumentsFor({dir,root,host}),root,'/bin/true'],{env:{...process.env,...env,TMPDIR:host},stdout:'pipe',stderr:'pipe'});
51
+ const stderr=new Response(child.stderr).text();const status=await child.exited;
52
+ return {status,stderr:await stderr,calls:readFileSync(log,'utf8').trim().split('\n').map(JSON.parse),root,host,dir};
53
+ }finally{rmSync(dir,{recursive:true,force:true});}
54
+ }
55
+
56
+ test('explicit binds work with -n, create files, and unwind in reverse depth order',async()=>{
57
+ const r=await invoke(({dir,host})=>['-n','-b',`${host}:/work`,'-b',`${dir}/file:/work/config/file`]);
58
+ expect(r.status).toBe(0);
59
+ expect(r.calls.filter(c=>c[0]==='mount'&&c[1]===r.host)[0]).toEqual(['mount',r.host,r.root+'/work','none','20480',null]);
60
+ expect(r.calls.filter(c=>c[0]==='umount').map(c=>c[1])).toEqual(['/work/config/file','/work']);
61
+ });
62
+ test('shared tmp replaces default tmpfs and bind failure cleans up even with -k',async()=>{
63
+ const shared=await invoke(()=>['--shared-tmp']);expect(shared.status).toBe(0);
64
+ expect(shared.calls.some(c=>c[0]==='mount'&&c[1]===shared.host&&c[2]===shared.root+'/tmp')).toBe(true);
65
+ expect(shared.calls.some(c=>c[0]==='mount'&&c[1]==='tmpfs'&&c[2]===shared.root+'/tmp')).toBe(false);
66
+ const failure=await invoke(({host,dir})=>['-n','-k','-b',`${host}:/first`,'-b',`${dir}/fail:/second`]);
67
+ expect(failure.status).toBe(125);expect(failure.stderr).toContain('mock mount denied');
68
+ expect(failure.calls.some(c=>c[0]==='chroot')).toBe(false);
69
+ expect(failure.calls.filter(c=>c[0]==='umount').map(c=>c[1])).toEqual([failure.root+'/first']);
70
+ });
@@ -21,9 +21,9 @@ const server = Bun.serve({
21
21
  afterAll(() => { server.stop(true); rmSync(work, { recursive: true, force: true }); });
22
22
  const url = `http://127.0.0.1:${server.port}/archive`;
23
23
 
24
- test("four exact registry pins share an architecture-independent cache", () => {
24
+ test("five exact registry pins share an architecture-independent cache", () => {
25
25
  expect(npmPackages.map(({ name, version }) => `${name}@${version}`)).toEqual([
26
- "buninu-browser@0.2.3", "@drxiaozhi/casty@1.3.4",
26
+ "buninu-browser@0.2.3", "buninu-land@0.1.2", "@drxiaozhi/casty@1.3.6",
27
27
  "@drxiaozhi/jspulse@0.2.1", "@drxiaozhi/jspinyin@0.1.1",
28
28
  ]);
29
29
  expect(basename(npmDownloadsDir)).toBe("npm");
@@ -16,7 +16,7 @@ const fresh = () => { const dir = join(work, String(sequence++)); mkdirSync(dir)
16
16
  async function fixture() {
17
17
  const downloadsDir = fresh();
18
18
  const packages = [];
19
- for (const name of ["buninu-browser", "@drxiaozhi/casty", "@drxiaozhi/jspulse", "@drxiaozhi/jspinyin"]) {
19
+ for (const name of ["buninu-browser", "buninu-land", "@drxiaozhi/casty", "@drxiaozhi/jspulse", "@drxiaozhi/jspinyin"]) {
20
20
  const bin = name.split("/").pop();
21
21
  const data = await new Bun.Archive({
22
22
  "package/package.json": JSON.stringify({ name, version: "1.0.0", type: "module", bin: { [bin]: "index.js" } }),
@@ -104,6 +104,39 @@ test("distribution launcher ensures resources then forwards to local casty witho
104
104
  expect(JSON.parse(result.stdout)).toEqual({ args: ["https://example.test", "--", "--chromium"], engine: join(browser, "buninu-browser.js") });
105
105
  });
106
106
 
107
+ test("land launcher selects bundled casty and preserves package frontend dispatch", async () => {
108
+ const cfg = await fixture(), root = fresh();
109
+ stageNpm(root, cfg);
110
+ const land = join(root, "usr/lib/node_modules/buninu-land");
111
+ mkdirSync(join(root, "bin"));
112
+ const launcher = join(root, "bin/buninu-land");
113
+ copyFileSync(new URL("../initramfs/bin/buninu-land", import.meta.url), launcher);
114
+ writeFileSync(join(land, "index.js"), `
115
+ export const chooseFrontend = args => ({
116
+ frontend: !args.length || args[0] === '--casty' || args[0] === 'about:blank' ? 'casty' : 'other',
117
+ args: args[0] === '--casty' ? args.slice(1) : args,
118
+ });
119
+ if (import.meta.main) console.log(JSON.stringify({ package: true, args: process.argv.slice(2) }));
120
+ `);
121
+ writeFileSync(join(root, "usr/lib/node_modules/@drxiaozhi/casty/index.js"),
122
+ "console.log(JSON.stringify({args:process.argv.slice(2),engine:process.env.CASTY_BROWSER}));");
123
+ for (const args of [[], ['about:blank'], ['--casty', '--', '--socket=wayland-test']]) {
124
+ const result = spawnSync(process.execPath, [launcher, ...args], {
125
+ encoding: "utf8", env: { ...process.env, CASTY_BROWSER: '/previous/browser' },
126
+ });
127
+ expect(result.status).toBe(0);
128
+ expect(JSON.parse(result.stdout)).toEqual({
129
+ args: args[0] === '--casty' ? args.slice(1) : args,
130
+ engine: join(land, "compositor.js"),
131
+ });
132
+ }
133
+ for (const args of [['--headless', '--help'], ['--help'], ['--version'], ['--docs'], ['--gtk']]) {
134
+ const result = spawnSync(process.execPath, [launcher, ...args], { encoding: "utf8" });
135
+ expect(result.status).toBe(0);
136
+ expect(JSON.parse(result.stdout)).toEqual({ package: true, args });
137
+ }
138
+ });
139
+
107
140
  test.skipIf(!npmPackages.every(pkg => existsSync(join(npmDownloadsDir, pkg.archive))))("real cached pins: JS and shell staging agree; browser headless help starts", () => {
108
141
  const js = fresh(), shell = fresh();
109
142
  stageNpm(js);
@@ -127,4 +160,12 @@ test.skipIf(!npmPackages.every(pkg => existsSync(join(npmDownloadsDir, pkg.archi
127
160
  const help = spawnSync(process.execPath, [join(js, "bin/buninu-browser"), "--headless", "--help"], { encoding: "utf8", timeout: 15000 });
128
161
  expect(help.status).toBe(0);
129
162
  expect(help.stdout).toContain("Usage");
163
+ copyFileSync(new URL("../initramfs/bin/buninu-land", import.meta.url), join(js, "bin/buninu-land"));
164
+ for (const args of [["--help"], ["--headless", "--help"], ["--version"]]) {
165
+ const landHelp = spawnSync(process.execPath, [join(js, "bin/buninu-land"), ...args], { encoding: "utf8", timeout: 15000 });
166
+ expect(landHelp.status).toBe(0);
167
+ expect(landHelp.stdout).toContain(args[0] === "--version" ? "0.1.2" : args[0] === "--headless" ? "Wayland compositor" : "Usage");
168
+ }
169
+ expect(existsSync(join(js, "usr/lib/node_modules/buninu-land/vendor/canvaskit/canvaskit.wasm"))).toBe(true);
170
+ expect(existsSync(join(js, "usr/lib/node_modules/buninu-land/LICENSES/hyprland.txt"))).toBe(true);
130
171
  }, 30000);