buninu-linux 0.4.0 → 0.6.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.
Files changed (42) hide show
  1. package/Dockerfile +23 -0
  2. package/NOTICE.md +111 -63
  3. package/README.md +292 -58
  4. package/build-image.sh +9 -5
  5. package/build-uki.sh +44 -28
  6. package/fetch-alpine.sh +45 -31
  7. package/fetch-bun.sh +14 -21
  8. package/graphics.md +7 -2
  9. package/index.js +150 -35
  10. package/initramfs/bin/bunterm +5 -1
  11. package/initramfs/bin/chroot +216 -0
  12. package/initramfs/bin/ip +3 -2
  13. package/initramfs/bin/ps +3 -0
  14. package/initramfs/bin/stripansi +27 -0
  15. package/initramfs/bin/tar +198 -14
  16. package/initramfs/init.js +149 -38
  17. package/initramfs/lib/bunterm/images.js +17 -2
  18. package/initramfs/lib/bunterm/mouse.js +222 -0
  19. package/initramfs/lib/bunterm/renderer.js +48 -3
  20. package/initramfs/lib/bunterm/terminal.js +123 -2
  21. package/initramfs/lib/dlopen.js +4 -0
  22. package/initramfs/lib/document.js +5 -1
  23. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +35 -2
  24. package/initramfs/usr/share/doc/buninu-linux/chroot.md +105 -0
  25. package/initramfs/usr/share/doc/buninu-linux/tar.md +32 -6
  26. package/native/aarch64/lib/ld-musl-aarch64.so.1 +0 -0
  27. package/native/aarch64/lib/libc.musl-aarch64.so.1 +0 -0
  28. package/native/aarch64/lib/libgcc_s.so.1 +0 -0
  29. package/native/aarch64/lib/libstdc++.so.6 +0 -0
  30. package/package.json +2 -2
  31. package/run-qemu.sh +61 -12
  32. package/scripts/arch.sh +113 -0
  33. package/scripts/fetch.sh +5 -3
  34. package/scripts/pack-initramfs.sh +18 -4
  35. package/test/host/Dockerfile +13 -0
  36. package/test/host/boot-test.exp +70 -0
  37. package/test/host/host-test.sh +49 -0
  38. package/test/host/in-container.sh +58 -0
  39. /package/{initramfs → native/x86_64}/lib/ld-musl-x86_64.so.1 +0 -0
  40. /package/{initramfs → native/x86_64}/lib/libc.musl-x86_64.so.1 +0 -0
  41. /package/{initramfs → native/x86_64}/lib/libgcc_s.so.1 +0 -0
  42. /package/{initramfs → native/x86_64}/lib/libstdc++.so.6 +0 -0
@@ -50,6 +50,7 @@ export const createSession = ({
50
50
  theme = defaultTheme,
51
51
  scrollback = 1000,
52
52
  cursorBlink = true,
53
+ mouse = true,
53
54
  env = {},
54
55
  onError = (error) => console.error(`bunterm: ${error?.stack ?? error}`),
55
56
  }) => {
@@ -82,13 +83,24 @@ export const createSession = ({
82
83
  let frameTimer = null;
83
84
  let blinkOn = true;
84
85
  const cursorHidden = () => Boolean(term._core?.coreService?.isCursorHidden);
86
+ // Only set when a pointer is attached; without one the renderer is called
87
+ // exactly as before.
88
+ let pointer = null;
85
89
  const paint = () => {
86
90
  if (frameTimer) clearTimeout(frameTimer);
87
91
  frameTimer = null;
88
92
  const buffer = term.buffer.active;
93
+ // cursorY counts from the buffer's base, so a scrolled-back viewport
94
+ // moves the cursor down the screen and eventually off it.
95
+ const row = buffer.baseY + buffer.cursorY - buffer.viewportY;
89
96
  return renderer.render(term, {
90
- cursor: { x: buffer.cursorX, y: buffer.cursorY, visible: blinkOn && !cursorHidden() },
97
+ cursor: {
98
+ x: buffer.cursorX,
99
+ y: row,
100
+ visible: blinkOn && !cursorHidden() && row >= 0 && row < renderer.rows,
101
+ },
91
102
  images: images.visible(),
103
+ pointer,
92
104
  });
93
105
  };
94
106
  const requestFrame = () => {
@@ -137,10 +149,102 @@ export const createSession = ({
137
149
  const applicationCursorKeys = Boolean(term._core?.coreService?.decPrivateModes?.applicationCursorKeys);
138
150
  const text = translateKeys(bytes.toString("latin1"), { applicationCursorKeys });
139
151
  backend.write(Buffer.from(text, "latin1"));
152
+ // Typing returns to the live screen, as every terminal does.
153
+ if (term.buffer.active.viewportY !== term.buffer.active.baseY) {
154
+ term.scrollToBottom();
155
+ renderer.invalidate();
156
+ }
140
157
  resetBlink();
141
158
  requestFrame();
142
159
  };
143
160
 
161
+ // Turns a pointer report from mouse.js into the mouse sequences the
162
+ // program asked for. xterm.js's CoreMouseService owns the protocol: it
163
+ // knows which events the program enabled (DECSET 1000/1002/1003), encodes
164
+ // them (SGR 1006 or the default form) and sends them through onData, so
165
+ // nothing here builds an escape sequence. Reports arrive only while a
166
+ // pointer is open, and this is the only place the terminal sees one.
167
+ const CoreMouseButton = { LEFT: 0, MIDDLE: 1, RIGHT: 2, NONE: 3, WHEEL: 4 };
168
+ const CoreMouseAction = { UP: 0, DOWN: 1, LEFT: 2, RIGHT: 3, MOVE: 32 };
169
+ const heldButton = (buttons) => (buttons.left ? CoreMouseButton.LEFT
170
+ : buttons.middle ? CoreMouseButton.MIDDLE
171
+ : buttons.right ? CoreMouseButton.RIGHT
172
+ : CoreMouseButton.NONE);
173
+
174
+ // Scrolls the viewport when the program is not reading the mouse itself:
175
+ // through the scrollback on the normal screen, and as arrow keys on the
176
+ // alternate screen, which has no scrollback and is what a pager expects.
177
+ const wheelScroll = (notches) => {
178
+ const lines = notches * 3;
179
+ if (term.buffer.active.type === "alternate") {
180
+ const application = Boolean(term._core?.coreService?.decPrivateModes?.applicationCursorKeys);
181
+ const key = `\u001b${application ? "O" : "["}${lines > 0 ? "A" : "B"}`;
182
+ backend.write(Buffer.from(key.repeat(Math.abs(lines)), "latin1"));
183
+ return;
184
+ }
185
+ const before = term.buffer.active.viewportY;
186
+ term.scrollLines(-lines);
187
+ if (term.buffer.active.viewportY !== before) {
188
+ // Every row on screen is different now, and identical text at the same
189
+ // row would otherwise hash the same as before.
190
+ renderer.invalidate();
191
+ requestFrame();
192
+ }
193
+ };
194
+
195
+ const reportMouse = (report) => {
196
+ pointer = { x: report.x, y: report.y };
197
+ requestFrame();
198
+ const service = term._core?.coreMouseService;
199
+ if (!service?.areMouseEventsActive) {
200
+ if (report.wheel) wheelScroll(report.wheel);
201
+ return;
202
+ }
203
+ const column = Math.floor((report.x - renderer.offsetX) / renderer.cellWidth);
204
+ const row = Math.floor((report.y - renderer.offsetY) / renderer.cellHeight);
205
+ const send = (button, action) => {
206
+ try {
207
+ service.triggerMouseEvent({ col: column, row, button, action, ctrl: false, alt: false, shift: false });
208
+ } catch (error) {
209
+ onError(error);
210
+ }
211
+ };
212
+ for (const name of report.released) {
213
+ send(CoreMouseButton[name.toUpperCase()], CoreMouseAction.UP);
214
+ }
215
+ for (const name of report.pressed) {
216
+ send(CoreMouseButton[name.toUpperCase()], CoreMouseAction.DOWN);
217
+ }
218
+ if (report.moved) send(heldButton(report.buttons), CoreMouseAction.MOVE);
219
+ for (let notch = 0; notch < Math.abs(report.wheel); notch++) {
220
+ send(CoreMouseButton.WHEEL, report.wheel > 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN);
221
+ }
222
+ for (let notch = 0; notch < Math.abs(report.hwheel); notch++) {
223
+ send(CoreMouseButton.WHEEL, report.hwheel > 0 ? CoreMouseAction.RIGHT : CoreMouseAction.LEFT);
224
+ }
225
+ };
226
+
227
+ // The pointer is opened only when asked for, by a module that is imported
228
+ // only then, so a machine without one — or a broken input layer — cannot
229
+ // affect a plain session.
230
+ let pointerDevice = null;
231
+ const attachPointer = async () => {
232
+ if (!mouse) return null;
233
+ const { openPointer, findPointers } = await import("./mouse.js");
234
+ const devices = findPointers();
235
+ if (!devices.length) throw new Error("no pointing device in /dev/input (is the evdev module loaded?)");
236
+ pointerDevice = openPointer({
237
+ devices,
238
+ width: screen.width,
239
+ height: screen.height,
240
+ onSync: reportMouse,
241
+ onError,
242
+ });
243
+ pointer = { x: pointerDevice.state.x, y: pointerDevice.state.y };
244
+ paint();
245
+ return pointerDevice;
246
+ };
247
+
144
248
  // Waits until every byte received so far has been parsed, then paints.
145
249
  const settle = async () => {
146
250
  await queue;
@@ -154,13 +258,18 @@ export const createSession = ({
154
258
  if (frameTimer) clearTimeout(frameTimer);
155
259
  frameTimer = null;
156
260
  backend.close();
261
+ pointerDevice?.close();
262
+ pointerDevice = null;
157
263
  images.delete();
158
264
  renderer.delete();
159
265
  term.dispose();
160
266
  };
161
267
 
162
268
  paint();
163
- return { term, backend, renderer, images, kitty, paint, requestFrame, settle, input, exited, close };
269
+ return {
270
+ term, backend, renderer, images, kitty,
271
+ paint, requestFrame, settle, input, exited, close, attachPointer, reportMouse, wheelScroll,
272
+ };
164
273
  };
165
274
 
166
275
  // Runs a session on the framebuffer with the console in graphics mode and
@@ -193,6 +302,18 @@ export const runTerminal = async ({ console: consoleDevice = null, device = "/de
193
302
  },
194
303
  });
195
304
  onRestore(() => keyboard.close());
305
+ if (options.mouse !== false) {
306
+ // A pointer that cannot be opened is reported and the session
307
+ // continues as a keyboard-only terminal.
308
+ try {
309
+ const attached = await session.attachPointer();
310
+ if (attached?.devices.length) {
311
+ console.error(`bunterm: mouse: ${attached.devices.map((device) => device.name).join(", ")}`);
312
+ }
313
+ } catch (error) {
314
+ console.error(`bunterm: mouse: ${error.message}`);
315
+ }
316
+ }
196
317
  return await session.exited;
197
318
  } finally {
198
319
  keyboard?.close();
@@ -85,6 +85,10 @@ export const libc = dlopen(libcPath, {
85
85
  args: [FFIType.i32, FFIType.ptr, FFIType.i32],
86
86
  returns: FFIType.i32,
87
87
  },
88
+ chroot: {
89
+ args: [FFIType.ptr],
90
+ returns: FFIType.i32,
91
+ },
88
92
  });
89
93
 
90
94
  export const cString = (value) => new TextEncoder().encode(`${value}\0`);
@@ -1,6 +1,10 @@
1
1
  // Shared renderer for /usr/share/doc/buninu-linux/<name>.md command manuals.
2
+ import { resolve } from "node:path";
3
+
2
4
  export const showDocument = async (name) => {
3
- const path = `${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md`;
5
+ const path = resolve(`${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md`);
4
6
  const rendered = Bun.markdown.ansi(await Bun.file(path).text(), { hyperlinks: true });
5
7
  process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}\n`);
8
+ // Where the page lives, for anyone who wants to read or edit the source.
9
+ process.stdout.write(`\u001b[2m${path}\u001b[0m\n`);
6
10
  };
@@ -10,6 +10,7 @@ bunterm # run /bin/sh on the console you are on
10
10
  bunterm /dev/tty1 # use virtual console 1 for graphics and keys
11
11
  bunterm --font-size 20
12
12
  bunterm -e bun /buninu/apps/jsmdcui/src/index.js --demo
13
+ bunterm --no-mouse # keyboard only
13
14
  bunterm -h
14
15
  ```
15
16
 
@@ -28,6 +29,7 @@ text mode and `bunterm` exits with its status.
28
29
  | `--line-height F` | Multiplies the cell height (default 1). |
29
30
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
30
31
  | `--no-blink` | A steady cursor. |
32
+ | `--no-mouse` | Do not attach a pointing device; nothing then reads `/dev/input`. |
31
33
 
32
34
  ## What is drawn
33
35
 
@@ -59,13 +61,44 @@ screen. A few console-specific
59
61
  sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
60
62
  follow the program's application cursor mode.
61
63
 
64
+ ## Mouse
65
+
66
+ Every pointing device under `/dev/input/event*` is opened and an arrow is
67
+ drawn on the screen. Clicks, drags and wheel notches reach the program as
68
+ the xterm mouse sequences it asked for: nothing is sent unless the program
69
+ enabled reporting (`DECSET 1000`, `1002` or `1003`), and what is sent follows
70
+ the mode it chose, SGR (`1006`) included. `jsmdcui`, `micro`, `htop`, `vim`
71
+ and anything else that speaks the protocol work with no configuration.
72
+
73
+ When the program is not reading the mouse, the wheel scrolls instead: back
74
+ through the scrollback on the normal screen, three lines per notch, and as
75
+ arrow keys on the alternate screen, which has no scrollback and is what a
76
+ pager expects. Typing returns to the live screen, and the text cursor is
77
+ hidden while it is scrolled out of view.
78
+
79
+ `--no-mouse` turns all of this off: the terminal then never looks at
80
+ `/dev/input` and never loads the mouse module.
81
+
82
+ `init.js` loads `evdev` and `psmouse` at boot, so a PS/2 mouse — which is
83
+ what QEMU's q35 machine and most PCs present — has a device node ready. The
84
+ aarch64 image loads `virtio_input` in place of `psmouse`, for QEMU's
85
+ `virtio-tablet-pci` and `virtio-keyboard-pci`. A USB
86
+ mouse also needs the `usbhid` stack, which a `--real` image carries and
87
+ `cfg.all` loads. The pointer follows relative devices (a mouse) and absolute
88
+ ones (a tablet or touchscreen) alike; a device that cannot be opened is
89
+ reported and the session continues without it.
90
+
62
91
  ## Limits
63
92
 
64
93
  - Needs a framebuffer: `linux-lts` has one built in, while the default
65
94
  `linux-virt` builds it as modules the image does not ship, so build with
66
95
  `--linux-lts` or `--real` (efifb/simpledrm from UEFI).
67
- - No mouse, no selection, no scrollback viewing (the buffer keeps 1000
68
- lines for programs that query it).
96
+ - The aarch64 image has no framebuffer yet: `run-qemu.sh` gives QEMU's
97
+ `virt` machine no display device, so there is no `/dev/fb0` to draw on.
98
+ - No selection, and no keyboard way to scroll back: the wheel is it. The
99
+ buffer keeps 1000 lines. A program that does not enable mouse reporting
100
+ sees no clicks — the arrow still moves, so the pointer is visibly alive.
101
+ `--no-mouse` removes the pointer entirely.
69
102
  - A program that prints a terminal reply while the tty echoes (that is, one
70
103
  not in raw mode) will see the reply echoed, as on any terminal.
71
104
  - Kitty images are drawn above text; `z` ordering below text and animation
@@ -0,0 +1,105 @@
1
+ # chroot — run a command inside another root directory
2
+
3
+ `/bin/chroot` is a Bun script that calls `chroot(2)` and `mount(2)` through
4
+ `/lib/dlopen.js`.
5
+
6
+ Unlike the traditional `chroot`, it prepares the new root first, so a
7
+ freshly unpacked root filesystem is usable straight away, and it undoes
8
+ every mount it made when the command exits.
9
+
10
+ ## Synopsis
11
+
12
+ ```sh
13
+ chroot NEWROOT [COMMAND [ARGUMENT...]]
14
+ chroot [-n] [-k] [-v] NEWROOT [COMMAND [ARGUMENT...]]
15
+ chroot -h | --help
16
+ ```
17
+
18
+ With no COMMAND it runs `$SHELL` if that exists inside NEWROOT, otherwise
19
+ `/bin/sh`, interactively when the terminal is one.
20
+
21
+ ## What it mounts
22
+
23
+ | Inside NEWROOT | What | Why |
24
+ |---|---|---|
25
+ | `/proc` | a fresh `proc` | path resolution, `ps`, anything reading `/proc/self` |
26
+ | `/sys` | a fresh `sysfs` | device and network information |
27
+ | `/dev` | recursive bind of the host's `/dev`, made a slave mount | every device node, including `/dev/ptmx`, without a second devtmpfs |
28
+ | `/dev/pts` | `devpts`, only if the bind brought none | pseudo-terminals for shells and editors |
29
+ | `/run` | `tmpfs` | runtime state |
30
+ | `/tmp` | `tmpfs` | scratch space that does not touch the root filesystem |
31
+ | `/etc/resolv.conf` | bind of the host's file | name resolution, without editing anything inside |
32
+
33
+ A mount point that already exists as a mount is left alone. A directory that
34
+ is missing is created; if the root filesystem is read-only and that fails,
35
+ that one mount is skipped and the rest still happen — use `-v` to see which.
36
+
37
+ Making `/dev` a slave mount means mounts appearing inside the root do not
38
+ propagate back to the host, and unmounting it afterwards cannot take the
39
+ host's `/dev` with it.
40
+
41
+ ## Options
42
+
43
+ | Option | Meaning |
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. |
46
+ | `-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
+ | `-v`, `--verbose` | Report every mount and unmount. |
48
+ | `--` | End of options; everything after it is NEWROOT and COMMAND. |
49
+
50
+ ## Examples
51
+
52
+ ```sh
53
+ # Mount a disk and enter the system on it
54
+ mount /dev/vda2 /mnt --mkdir
55
+ chroot /mnt
56
+
57
+ # Run one command and leave
58
+ chroot /mnt /bin/bun --version
59
+
60
+ # Watch what is set up and taken down
61
+ chroot -v /mnt /bin/sh -c 'ls /proc | head -3'
62
+
63
+ # Traditional behaviour: nothing is mounted for you
64
+ chroot -n /mnt /bin/sh
65
+
66
+ # Keep the mounts for repeated visits
67
+ chroot -k /mnt /bin/sh -c 'true'
68
+ chroot -k /mnt
69
+ umount -R /mnt
70
+ ```
71
+
72
+ ## Cleanup
73
+
74
+ When the command exits, every mount this invocation made is unmounted,
75
+ deepest first, including anything that appeared under the recursive `/dev`
76
+ bind. A mount that is still busy — a process left running inside, a shell
77
+ with its working directory there — is detached instead (`MNT_DETACH`), so
78
+ the kernel releases it once its last user goes away. The same cleanup runs
79
+ if `chroot(2)` itself fails or the command cannot be started, and on
80
+ `SIGTERM` or `SIGHUP`.
81
+
82
+ `Ctrl-C` goes to the command, not to `chroot`, so the mounts are still
83
+ removed after an interrupted program.
84
+
85
+ ## Limits
86
+
87
+ - Needs to be superuser: `chroot(2)` requires `CAP_SYS_CHROOT`, and the
88
+ mounts require `CAP_SYS_ADMIN`. Buninu normally runs as root.
89
+ - The new root must contain the program you ask for and whatever it links
90
+ against; `chroot /mnt /bin/sh` fails with "not found" if `/mnt/bin/sh`
91
+ is not there.
92
+ - Only the filesystem view changes. Processes, network, users and the
93
+ hostname are shared with the host; this is not a container.
94
+ - `-k` leaves mounts behind on purpose. `umount -R NEWROOT` removes them.
95
+
96
+ ## Exit status
97
+
98
+ The command's own status, or `125` if `chroot` itself failed, `126` if the
99
+ command was found but could not be run, and `127` if it was not found.
100
+
101
+ ## See also
102
+
103
+ - Source: `/bin/chroot`
104
+ - `mount --help`, `umount --help`
105
+ - Shared native binding: `/lib/dlopen.js`
@@ -27,7 +27,9 @@ Use `-` as ARCHIVE to read from standard input or write to standard output.
27
27
  | `-C DIR`, `--directory=DIR` | Read input files from DIR, or extract into DIR. |
28
28
  | `-z`, `--gzip` | Create gzip-compressed tar, or explicitly identify gzip input. |
29
29
  | `--zstd` | Create or read a zstd-compressed tar. A `.zst` or `.tzst` input is also detected by name. |
30
- | `-v` | Print names while creating/extracting; with `-t`, also show each regular file's size and modification time. |
30
+ | `-v` | Print names while creating/extracting; with `-t`, also show each member's size, modification time, kind and link target. |
31
+ | `-o`, `--no-same-owner`, `--no-same-permissions`, `--overwrite` | GNU tar compatibility for extraction; these already match Bun.Archive's behaviour. |
32
+ | `--exclude=PATTERN` | Accepted for js-udocker compatibility. Device nodes are not extracted and js-udocker removes whiteout files separately. |
31
33
 
32
34
  Short options can be combined, for example `-xzvf` or `-czf`.
33
35
  For the traditional first option word, the leading hyphen is optional, so
@@ -42,8 +44,34 @@ tar -xzf source.tar.gz -C /tmp/source
42
44
  tar -tf source.tar.gz
43
45
  tar --zstd -cf backup.tar.zst home
44
46
  tar --zstd -xf backup.tar.zst -C /mnt/restore
47
+
48
+ # An Alpine root filesystem, symbolic links and all; then enter it
49
+ tar -xzf alpine-minirootfs-3.24.1-x86_64.tar.gz -C /mnt/alpine
50
+ chroot /mnt/alpine
45
51
  ```
46
52
 
53
+ ## Links
54
+
55
+ Extraction restores symbolic links exactly as the archive records them,
56
+ including absolute targets such as `/bin/busybox` and targets that do not
57
+ exist yet, and it restores hard links. This is what GNU tar does, and it is
58
+ what an unpacked root filesystem needs: an Alpine minirootfs is 335 symbolic
59
+ links, 306 of them absolute, and without them the result has no `/bin/sh`.
60
+
61
+ `Bun.Archive` creates only the symbolic links whose target is relative and
62
+ stays inside the extraction directory, and skips hard links entirely, so
63
+ this command reads the archive's own headers (ustar, GNU `L`/`K` long names,
64
+ pax `x` records) and creates the rest itself once every file is in place.
65
+ Nothing is ever written through a link, because the links are made last.
66
+
67
+ A member's own name still follows GNU tar's rule: a leading `/` is dropped
68
+ and a name containing `..` is refused, so an archive cannot write outside
69
+ the extraction directory.
70
+
71
+ The same header scan backs listing, so `-t` reports every member —
72
+ directories, links, devices — and not just regular files. A symbolic link
73
+ shows its target, a hard link the file it points at.
74
+
47
75
  ## Current Bun.Archive limits
48
76
 
49
77
  Creation stores regular files only. Symbolic links are rejected rather than
@@ -51,11 +79,9 @@ silently archived as the wrong type; modes, ownership, hard links, and empty
51
79
  directories are not preserved. Bun 1.4.3 also requires file contents to be
52
80
  materialized in memory before archive creation.
53
81
 
54
- Extraction restores directories and symbolic links, but Bun 1.4.3 skips hard
55
- links. Listing and verbose extraction use `Archive.files()`, so they report
56
- regular files only. Verbose listing can show their size and modification time,
57
- but not the unavailable mode or owner. Bun provides gzip and zstd codecs but no xz or bzip2 codec,
58
- so this command does not accept `.tar.xz` or `.tar.bz2`.
82
+ Extraction does not restore modes, ownership or timestamps. Bun provides gzip
83
+ and zstd codecs but no xz or bzip2 codec, so this command does not accept
84
+ `.tar.xz` or `.tar.bz2`.
59
85
 
60
86
  Appending with `-r`/`rvf` is not supported. `Bun.Archive` has no append API;
61
87
  rebuilding an existing archive through `Archive.files()` would silently lose
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -37,6 +37,6 @@
37
37
  "fetch": "./index.js -f",
38
38
  "pack": "./index.js -b",
39
39
  "start": "./index.js -r",
40
- "check": "for f in ./index.js ./initramfs/init.js ./initramfs/lib/document.js ./initramfs/lib/dlopen.js ./initramfs/lib/fbdev.js ./initramfs/lib/canvas.js ./initramfs/lib/bunterm/glyphs.js ./initramfs/lib/bunterm/renderer.js ./initramfs/lib/bunterm/images.js ./initramfs/lib/bunterm/input.js ./initramfs/lib/bunterm/terminal.js ./initramfs/lib/modprobe.js ./initramfs/lib/power.js ./initramfs/bin/bunterm ./initramfs/bin/mount ./initramfs/bin/umount ./initramfs/bin/ip ./initramfs/bin/ps ./initramfs/bin/top ./initramfs/bin/poweroff ./initramfs/bin/reboot ./initramfs/bin/tar; do node --check \"$f\" || exit 1; done"
40
+ "check": "for f in ./index.js ./initramfs/init.js ./initramfs/lib/document.js ./initramfs/lib/dlopen.js ./initramfs/lib/fbdev.js ./initramfs/lib/canvas.js ./initramfs/lib/bunterm/glyphs.js ./initramfs/lib/bunterm/renderer.js ./initramfs/lib/bunterm/images.js ./initramfs/lib/bunterm/input.js ./initramfs/lib/bunterm/mouse.js ./initramfs/lib/bunterm/terminal.js ./initramfs/lib/modprobe.js ./initramfs/lib/power.js ./initramfs/bin/bunterm ./initramfs/bin/chroot ./initramfs/bin/mount ./initramfs/bin/umount ./initramfs/bin/ip ./initramfs/bin/ps ./initramfs/bin/top ./initramfs/bin/poweroff ./initramfs/bin/reboot ./initramfs/bin/tar ./initramfs/bin/stripansi; do node --check \"$f\" || exit 1; done"
41
41
  }
42
42
  }
package/run-qemu.sh CHANGED
@@ -2,19 +2,68 @@
2
2
 
3
3
  cd "$(dirname "$0")"
4
4
 
5
- test -f vda.img || ./build-image.sh
6
-
7
- if [ -z "$PREFIX" ] ; then
8
- PREFIX=/usr
5
+ # The guest is whatever build-uki.sh last put in vda/EFI/BOOT (it leaves only
6
+ # one architecture there), unless BUNINU_ARCH (index.js --arch) names one.
7
+ if [ -z "$BUNINU_ARCH" ] && [ -f vda/EFI/BOOT/BOOTAA64.EFI ]; then
8
+ BUNINU_ARCH=aarch64
9
9
  fi
10
+ . ./scripts/arch.sh
11
+
12
+ test -f vda.img || ./build-image.sh || exit 1
13
+ test -f "vda/EFI/BOOT/$efi_boot_name" || {
14
+ echo "error: vda.img holds no $arch UKI; build one with ./index.js -b --arch $arch" >&2
15
+ exit 1
16
+ }
17
+
18
+ qemu="qemu-system-$arch"
19
+ command -v "$qemu" >/dev/null || {
20
+ echo "error: $qemu not found (macOS: brew install qemu; Debian: apt install qemu-system)" >&2
21
+ exit 1
22
+ }
10
23
 
11
- ovmf_filepath="$PREFIX"/share/qemu/edk2-x86_64-code.fd
24
+ case $arch in
25
+ x86_64) machine=q35 distro_firmware=/usr/share/OVMF/OVMF_CODE_4M.fd ;;
26
+ aarch64) machine=virt distro_firmware=/usr/share/AAVMF/AAVMF_CODE.fd ;;
27
+ esac
28
+
29
+ # UEFI firmware: OVMF_FILE if set; then the edk2 build QEMU ships next to its
30
+ # own binary (Termux's $PREFIX/share/qemu, Homebrew's share/qemu, ...); then
31
+ # the distribution packages (Debian ovmf / qemu-efi-aarch64).
32
+ qemu_share="$(dirname "$(command -v "$qemu")")/../share/qemu"
33
+ ovmf_filepath=
34
+ for candidate in "$OVMF_FILE" "$distro_firmware" "$qemu_share/edk2-$arch-code.fd" \
35
+ "/usr/share/qemu/edk2-$arch-code.fd"; do
36
+ if [ -n "$candidate" ] && [ -f "$candidate" ]; then
37
+ ovmf_filepath=$candidate
38
+ break
39
+ fi
40
+ done
41
+ test -n "$ovmf_filepath" || {
42
+ echo "error: no $arch UEFI firmware found; set OVMF_FILE to an edk2-$arch-code.fd" >&2
43
+ exit 1
44
+ }
12
45
 
13
- if [ -f /usr/share/OVMF/OVMF_CODE_4M.fd ] ; then
14
- ovmf_filepath=/usr/share/OVMF/OVMF_CODE_4M.fd
15
- elif [ -f "$OVMF_FILE" ] ; then
16
- ovmf_filepath=$OVMF_FILE
46
+ # Hardware acceleration needs the guest to match the host CPU: KVM on Linux,
47
+ # Hypervisor.framework on macOS (Apple Silicon runs the aarch64 guest natively,
48
+ # an Intel Mac the x86_64 one). Anything else, such as the x86_64 guest on
49
+ # Apple Silicon or on an Android phone, is emulated with TCG. ACCEL overrides
50
+ # the choice, e.g. ACCEL=tcg to rule acceleration out while debugging.
51
+ case "$(uname -m)" in
52
+ arm64 | aarch64) host_arch=aarch64 ;;
53
+ *) host_arch=$(uname -m) ;;
54
+ esac
55
+ if [ -z "$ACCEL" ]; then
56
+ ACCEL=tcg
57
+ if [ "$host_arch" = "$arch" ]; then
58
+ case "$(uname -s)" in
59
+ Linux) [ -r /dev/kvm ] && [ -w /dev/kvm ] && ACCEL=kvm ;;
60
+ Darwin) [ "$(sysctl -n kern.hv_support 2>/dev/null)" = 1 ] && ACCEL=hvf ;;
61
+ esac
62
+ fi
17
63
  fi
64
+ # TCG emulates every feature it knows; KVM and HVF pass the host CPU through.
65
+ if [ "$ACCEL" = tcg ]; then cpu=max; else cpu=host; fi
66
+ echo "run-qemu.sh: $arch guest on $machine, accel=$ACCEL, firmware $ovmf_filepath" >&2
18
67
 
19
68
  # User-mode networking is NAT: the guest can reach out, nothing reaches in
20
69
  # unless a port is forwarded. PORTS lists them, space-separated, as
@@ -31,9 +80,9 @@ for port in ${PORTS:-}; do
31
80
  nic="$nic,hostfwd=tcp:127.0.0.1:$host-:$guest"
32
81
  done
33
82
 
34
- exec qemu-system-x86_64 \
35
- -machine q35,accel=tcg \
36
- -cpu max \
83
+ exec "$qemu" \
84
+ -machine "$machine,accel=$ACCEL" \
85
+ -cpu "$cpu" \
37
86
  -m 1G \
38
87
  -drive if=pflash,format=raw,readonly=on,file="$ovmf_filepath" \
39
88
  -drive if=virtio,format=raw,file=vda.img \
@@ -0,0 +1,113 @@
1
+ # Sourced by the fetch, build and run scripts; not executable on its own.
2
+ #
3
+ # Everything that differs between the two guest architectures lives here:
4
+ # the pinned packages and their SHA-256, the file names inside them, the EFI
5
+ # removable-media name, the PE-aware objcopy that assembles the UKI and the
6
+ # serial console. BUNINU_ARCH (index.js --arch) picks one; x86_64 is the
7
+ # default. LINUX_FLAVOR (virt or lts) and REAL_MACHINE are read here too.
8
+ #
9
+ # Fetched and extracted files are kept per architecture, so both guests can
10
+ # be built from one checkout without refetching:
11
+ #
12
+ # downloads/<arch>/ the pinned archives (both repos use the same names)
13
+ # kernel/<arch>/ vmlinuz-<flavor> and the UKI stub
14
+ # native/<arch>/ bun, musl, libgcc/libstdc++ and kernel modules;
15
+ # scripts/pack-initramfs.sh lays it over initramfs/
16
+
17
+ case "${BUNINU_ARCH:-x86_64}" in
18
+ x86_64 | amd64 | x64) arch=x86_64 ;;
19
+ aarch64 | arm64) arch=aarch64 ;;
20
+ *)
21
+ echo "error: unsupported BUNINU_ARCH=$BUNINU_ARCH (use x86_64 or aarch64)" >&2
22
+ exit 1
23
+ ;;
24
+ esac
25
+
26
+ linux_flavor=${LINUX_FLAVOR:-virt}
27
+ case $linux_flavor in
28
+ virt | lts) ;;
29
+ *)
30
+ echo "error: unsupported LINUX_FLAVOR=$linux_flavor (use virt or lts)" >&2
31
+ exit 1
32
+ ;;
33
+ esac
34
+
35
+ if [ "${REAL_MACHINE:-}" = 1 ] && [ "$arch" != x86_64 ]; then
36
+ echo "error: --real images are x86_64 only; its module set is PC hardware" >&2
37
+ exit 1
38
+ fi
39
+
40
+ # The Alpine repository keeps only the newest build of a package, so a pin
41
+ # stops downloading when Alpine bumps it; the hashes below then fail loudly
42
+ # rather than silently taking the new build. ALPINE_MIRROR swaps the host for
43
+ # a closer mirror; the SHA-256 check is the same either way.
44
+ alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
+ alpine_base="$alpine_mirror/v3.24/main/$arch"
46
+
47
+ kernel_version=6.18.53
48
+ kernel_release="$kernel_version-0-$linux_flavor"
49
+ linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
+ musl_package=musl-1.2.6-r2.apk
51
+ stub_package=systemd-efistub-260.2-r0.apk
52
+ libstdcpp_package=libstdc++-15.2.0-r5.apk
53
+ libgcc_package=libgcc-15.2.0-r5.apk
54
+ bun_version=1.4.2
55
+
56
+ downloads_dir="downloads/$arch"
57
+ kernel_dir="kernel/$arch"
58
+ kernel_image="$kernel_dir/vmlinuz-$linux_flavor"
59
+ native_dir="native/$arch"
60
+ modules_root="$native_dir/lib/modules/$kernel_release"
61
+ musl_loader="ld-musl-$arch.so.1"
62
+ musl_libc="libc.musl-$arch.so.1"
63
+
64
+ case $arch in
65
+ x86_64)
66
+ linux_virt_sha256=cad859cc46342e18002621fdde166bf2cd520dfec5e781d241de7d13e53970d6
67
+ linux_lts_sha256=8e3cfdd1d98e0e70c2e70a8c299ed3cd1e0939d60ad7bbd1da1cf241d794490b
68
+ musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
+ stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
+ libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
71
+ libgcc_sha256=393dcd32629f06d7d85409c272d142d0c082772d10b87ef55ee82f47de3be637
72
+ # Bun ships one x64 musl build; -baseline is an alias (NOTICE.md).
73
+ bun_build=bun-linux-x64-musl-baseline
74
+ bun_sha256=76e1db84e98f22f78de0a87e309bfbbf297732847f9720db36750646c85c8c18
75
+ efi_stub=linuxx64.efi.stub
76
+ efi_boot_name=BOOTX64.EFI
77
+ uki_objcopy=x86_64-w64-mingw32-objcopy
78
+ uki_objdump=x86_64-w64-mingw32-objdump
79
+ uki_binutils_package=binutils-mingw-w64-x86-64
80
+ # COM1 on q35 and on PCs.
81
+ serial_console=ttyS0
82
+ serial_major=4
83
+ ;;
84
+ aarch64)
85
+ linux_virt_sha256=fea61b7fd5e72e626d771f9798df56c83db658d0e221044f06fc28aacf5a118b
86
+ linux_lts_sha256=46885041022ea11afa1e07c40199c74146bf72d362368a844338af0c162e365c
87
+ musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
+ stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
+ libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
90
+ libgcc_sha256=369aaa6e9d099a737bad6dd3e6c2fe7bb1547ca26d22b94ee0411228f709b403
91
+ bun_build=bun-linux-aarch64-musl
92
+ bun_sha256=71760b6c8ea30623b81a4907cb815d48e2ea266f2e73e751534a44a0607950df
93
+ efi_stub=linuxaa64.efi.stub
94
+ efi_boot_name=BOOTAA64.EFI
95
+ # The aarch64 ELF binutils also read and write pei-aarch64-little:
96
+ # native binutils on an arm64 Debian, the cross package on amd64.
97
+ uki_objcopy=aarch64-linux-gnu-objcopy
98
+ uki_objdump=aarch64-linux-gnu-objdump
99
+ uki_binutils_package=binutils-aarch64-linux-gnu
100
+ # The PL011 UART of QEMU's virt machine.
101
+ serial_console=ttyAMA0
102
+ serial_major=204
103
+ ;;
104
+ esac
105
+
106
+ if [ "$linux_flavor" = lts ]; then
107
+ linux_sha256=$linux_lts_sha256
108
+ else
109
+ linux_sha256=$linux_virt_sha256
110
+ fi
111
+
112
+ bun_zip="$bun_build-$bun_version.zip"
113
+ bun_url="https://github.com/oven-sh/bun/releases/download/bun-v$bun_version/$bun_build.zip"