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.
- package/LICENSES/MIT-ZilchOS-bootstrap-from-tcc.txt +21 -0
- package/NOTICE.md +48 -2
- package/README.md +81 -12
- package/initramfs/bin/buninu-land +26 -0
- package/initramfs/bin/chroot +68 -47
- package/initramfs/buninu/CHANGELOG.md +22 -0
- package/initramfs/buninu/apps/jsgotty/package-lock.json +2 -2
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsgotty/static/js/gotty.js +1 -0
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/lib/bunterm/geometry.js +15 -0
- package/initramfs/lib/bunterm/pointer-report.js +10 -0
- package/initramfs/lib/bunterm/terminal.js +5 -4
- package/initramfs/lib/chroot-options.js +69 -0
- package/initramfs/usr/lib/bootstrap/1.prepare-rootfs.js +106 -0
- package/initramfs/usr/lib/bootstrap/10.build-m4.js +40 -0
- package/initramfs/usr/lib/bootstrap/11.build-flex.js +50 -0
- package/initramfs/usr/lib/bootstrap/12.build-gcc.js +261 -0
- package/initramfs/usr/lib/bootstrap/2.enter-rootfs.js +49 -0
- package/initramfs/usr/lib/bootstrap/3.gen-musl-dev.js +141 -0
- package/initramfs/usr/lib/bootstrap/4.build-tcc.js +61 -0
- package/initramfs/usr/lib/bootstrap/5.build-make.js +58 -0
- package/initramfs/usr/lib/bootstrap/6.build-binutils.js +77 -0
- package/initramfs/usr/lib/bootstrap/7.build-gmp.js +68 -0
- package/initramfs/usr/lib/bootstrap/8.build-mpfr.js +59 -0
- package/initramfs/usr/lib/bootstrap/9.build-mpc.js +55 -0
- package/initramfs/usr/lib/bootstrap/LICENSE +21 -0
- package/initramfs/usr/lib/bootstrap/LICENSES/MIT-ZilchOS-bootstrap-from-tcc.txt +21 -0
- package/initramfs/usr/lib/bootstrap/NOTICE.md +65 -0
- package/initramfs/usr/lib/bootstrap/README.md +204 -0
- package/initramfs/usr/lib/bootstrap/downloads.md +49 -0
- package/initramfs/usr/lib/bootstrap/lib/build.js +155 -0
- package/initramfs/usr/lib/bootstrap/lib/common.js +134 -0
- package/initramfs/usr/lib/bootstrap/lib/compile-object.c +14 -0
- package/initramfs/usr/lib/bootstrap/lib/musl-headers.js +59 -0
- package/initramfs/usr/lib/bootstrap/lib/seed.js +148 -0
- package/initramfs/usr/lib/bootstrap/lib/startup-check.c +14 -0
- package/initramfs/usr/lib/bootstrap/lib/tcc-build.js +53 -0
- package/initramfs/usr/lib/bootstrap/sources/musl-1.2.6.tar.gz +0 -0
- package/initramfs/usr/lib/bootstrap/sources/tinycc-05f0faf-upstream.tar.gz +0 -0
- package/initramfs/usr/lib/bootstrap/sources/tinycc-local-bootstrap-files.tar.gz +0 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +4 -1
- package/initramfs/usr/share/doc/buninu-linux/chroot.md +37 -3
- package/package.json +1 -1
- package/scripts/npm-packages.md +8 -3
- package/test/bootstrap.test.js +65 -0
- package/test/bunterm-pixelmouse.test.js +27 -0
- package/test/chroot-bind.test.js +70 -0
- package/test/fetch-npm.test.js +2 -2
- 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
|
+
|
|
Binary file
|
|
@@ -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`)
|
|
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
|
-
|
|
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` |
|
|
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
package/scripts/npm-packages.md
CHANGED
|
@@ -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
|
-
"@
|
|
10
|
-
- https://registry.npmjs.org
|
|
11
|
-
- sha256:
|
|
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
|
+
});
|
package/test/fetch-npm.test.js
CHANGED
|
@@ -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("
|
|
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.
|
|
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");
|
package/test/stage-npm.test.js
CHANGED
|
@@ -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);
|