buninu-linux 0.5.1 → 0.7.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 (41) hide show
  1. package/Dockerfile +23 -0
  2. package/NOTICE.md +111 -63
  3. package/README.md +280 -60
  4. package/build-image.sh +9 -5
  5. package/build-uki.sh +44 -28
  6. package/fetch-alpine.sh +43 -35
  7. package/fetch-bun.sh +14 -21
  8. package/index.js +150 -35
  9. package/initramfs/bin/buninu-linux-help +12 -0
  10. package/initramfs/bin/bunterm +64 -5
  11. package/initramfs/bin/chroot +4 -1
  12. package/initramfs/bin/ip +3 -2
  13. package/initramfs/bin/ps +3 -0
  14. package/initramfs/bin/tar +9 -0
  15. package/initramfs/init.js +79 -41
  16. package/initramfs/lib/bunterm/images.js +17 -2
  17. package/initramfs/lib/bunterm/terminal.js +25 -9
  18. package/initramfs/lib/bunterm/vt.js +91 -0
  19. package/initramfs/lib/dlopen.js +4 -0
  20. package/initramfs/lib/fbdev.js +33 -10
  21. package/initramfs/usr/share/doc/buninu-linux/bunterm.md +25 -7
  22. package/initramfs/usr/share/doc/buninu-linux/fbdev.md +1 -1
  23. package/initramfs/usr/share/doc/buninu-linux/tar.md +2 -0
  24. package/initramfs/usr/share/licenses/README.md +4 -0
  25. package/native/aarch64/lib/ld-musl-aarch64.so.1 +0 -0
  26. package/native/aarch64/lib/libc.musl-aarch64.so.1 +0 -0
  27. package/native/aarch64/lib/libgcc_s.so.1 +0 -0
  28. package/native/aarch64/lib/libstdc++.so.6 +0 -0
  29. package/package.json +2 -2
  30. package/run-qemu.sh +61 -12
  31. package/scripts/arch.sh +113 -0
  32. package/scripts/fetch.sh +5 -3
  33. package/scripts/pack-initramfs.sh +22 -4
  34. package/test/host/Dockerfile +13 -0
  35. package/test/host/boot-test.exp +70 -0
  36. package/test/host/host-test.sh +49 -0
  37. package/test/host/in-container.sh +58 -0
  38. /package/{initramfs → native/x86_64}/lib/ld-musl-x86_64.so.1 +0 -0
  39. /package/{initramfs → native/x86_64}/lib/libc.musl-x86_64.so.1 +0 -0
  40. /package/{initramfs → native/x86_64}/lib/libgcc_s.so.1 +0 -0
  41. /package/{initramfs → native/x86_64}/lib/libstdc++.so.6 +0 -0
@@ -355,8 +355,12 @@ const setConsoleSwitchMode = (consoleDevice, mode) => {
355
355
  // when it comes back (answered with VT_ACKACQ), at which point `onAcquire`
356
356
  // runs so the caller can repaint what fbcon left on the screen. Returns a
357
357
  // function that puts the console back into automatic switching.
358
- export const watchConsoleSwitches = (consoleDevice, onAcquire) => {
358
+ export const watchConsoleSwitches = (consoleDevice, callbacks) => {
359
+ // Keep the original (console, onAcquire) public form working.
360
+ const onRelease = typeof callbacks === "function" ? () => {} : callbacks.onRelease;
361
+ const onAcquire = typeof callbacks === "function" ? callbacks : callbacks.onAcquire;
359
362
  const release = () => {
363
+ try { onRelease(); } catch {}
360
364
  try { ioctlValue(consoleDevice.fd, VT_RELDISP, 1, "VT_RELDISP"); } catch {}
361
365
  };
362
366
  const acquire = () => {
@@ -429,6 +433,7 @@ const defaultTriangle = (display) => [
429
433
  // before text mode returns, such as putting a keyboard back into cooked mode.
430
434
  export const runGraphics = async (draw, {
431
435
  device = "/dev/fb0", console: consolePaths, switchConsole = true,
436
+ prepareConsole = null,
432
437
  } = {}) => {
433
438
  const display = openFramebuffer(device);
434
439
  let consoleDevice;
@@ -442,6 +447,7 @@ export const runGraphics = async (draw, {
442
447
  let previousConsole = null;
443
448
  let unwatch = null;
444
449
  const restoreCallbacks = [];
450
+ const releaseCallbacks = [];
445
451
  const acquireCallbacks = [];
446
452
  const restore = () => {
447
453
  while (restoreCallbacks.length) {
@@ -470,6 +476,10 @@ export const runGraphics = async (draw, {
470
476
  for (const [signal, handler] of signalHandlers) process.on(signal, handler);
471
477
  process.on("exit", restore);
472
478
  try {
479
+ if (prepareConsole) {
480
+ const cleanup = await prepareConsole(consoleDevice);
481
+ if (typeof cleanup === "function") restoreCallbacks.push(cleanup);
482
+ }
473
483
  const target = switchConsole ? consoleNumber(consoleDevice.path) : null;
474
484
  if (target !== null) {
475
485
  const active = activeConsole(consoleDevice);
@@ -480,22 +490,35 @@ export const runGraphics = async (draw, {
480
490
  }
481
491
  setConsoleGraphics(consoleDevice);
482
492
  graphics = true;
493
+ // An async draw callback runs synchronously until its first await. Start
494
+ // it before enabling VT_PROCESS so it can register release/acquire hooks;
495
+ // otherwise a switch in that small window could be acknowledged without
496
+ // giving the renderer a chance to stop.
497
+ const drawing = draw(display, {
498
+ console: consoleDevice,
499
+ onRestore: (callback) => { restoreCallbacks.push(callback); },
500
+ onRelease: (callback) => { releaseCallbacks.push(callback); },
501
+ onAcquire: (callback) => { acquireCallbacks.push(callback); },
502
+ });
483
503
  if (switchConsole) {
484
504
  try {
485
- unwatch = watchConsoleSwitches(consoleDevice, () => {
486
- for (const callback of acquireCallbacks) {
487
- try { callback(); } catch {}
488
- }
505
+ unwatch = watchConsoleSwitches(consoleDevice, {
506
+ onRelease: () => {
507
+ for (const callback of releaseCallbacks) {
508
+ try { callback(); } catch {}
509
+ }
510
+ },
511
+ onAcquire: () => {
512
+ for (const callback of acquireCallbacks) {
513
+ try { callback(); } catch {}
514
+ }
515
+ },
489
516
  });
490
517
  } catch {
491
518
  // Not a virtual console (or no permission): switches stay automatic.
492
519
  }
493
520
  }
494
- return await draw(display, {
495
- console: consoleDevice,
496
- onRestore: (callback) => { restoreCallbacks.push(callback); },
497
- onAcquire: (callback) => { acquireCallbacks.push(callback); },
498
- });
521
+ return await drawing;
499
522
  } finally {
500
523
  restore();
501
524
  process.off("exit", restore);
@@ -7,8 +7,9 @@ box drawing and kitty graphics protocol images — no X11, Wayland or GPU.
7
7
 
8
8
  ```sh
9
9
  bunterm # run /bin/sh on the console you are on
10
- bunterm /dev/tty1 # use virtual console 1 for graphics and keys
11
- bunterm --font-size 20
10
+ bunterm 2 # start a detached bunterm on /dev/tty2
11
+ bunterm /dev/tty1 # the full virtual-console spelling also works
12
+ bunterm -s 20 # short for --font-size 20
12
13
  bunterm -e bun /buninu/apps/jsmdcui/src/index.js --demo
13
14
  bunterm --no-mouse # keyboard only
14
15
  bunterm -h
@@ -19,13 +20,21 @@ The program's `TERM` is `xterm-256color`, `COLORTERM=truecolor`,
19
20
  Markdown images without `--kitty`). When it exits, the console returns to
20
21
  text mode and `bunterm` exits with its status.
21
22
 
23
+ Every live session has a canonical `/bin/bunterm /dev/ttyN ...` command line.
24
+ When no console is named, the currently active VT is used in the foreground.
25
+ Naming a different VT starts bunterm as a detached session and returns
26
+ immediately to the calling shell. Only one bunterm may control a given VT;
27
+ ownership is
28
+ identified from the process task name and its open console descriptor under
29
+ `/proc`, so it disappears automatically when the process exits.
30
+
22
31
  ## Options
23
32
 
24
33
  | Option | Meaning |
25
34
  | --- | --- |
26
- | `/dev/ttyN` | The virtual console to use for graphics and the keyboard. If another console is on screen, the display switches to this one first and back when `bunterm` exits. Without it the process's own console and stdin are used, so run it from a VT (not a serial line or a PTY). |
35
+ | `N`, `/dev/ttyN` | The virtual console to use for graphics and the keyboard; for example, `2` means `/dev/tty2`. Naming a VT other than the active one starts a detached bunterm there, switches the display to it, and immediately returns control to the calling shell. Without this argument, bunterm uses the active VT in the foreground. |
27
36
  | `-e command [argument...]` | The program to run; everything after `-e` belongs to it. Default `/bin/sh`. |
28
- | `--font-size N` | Font size in pixels (default 16). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
37
+ | `-s N`, `--font-size N` | Font size in pixels (default 16). The cell is 0.6 × N wide and 1.16 × N tall with DejaVu Sans Mono, so 1024×768 at 16 px gives 106 × 41 cells. |
29
38
  | `--line-height F` | Multiplies the cell height (default 1). |
30
39
  | `--fb /dev/fbN` | The framebuffer device (default `/dev/fb0`). |
31
40
  | `--no-blink` | A steady cursor. |
@@ -57,7 +66,11 @@ text mode and `bunterm` exits with its status.
57
66
 
58
67
  The console stays in the kernel's translated keyboard mode, so
59
68
  `Ctrl-Alt-F2` and friends still switch consoles; coming back repaints the
60
- screen. A few console-specific
69
+ screen. While its VT is inactive, bunterm continues consuming PTY output and
70
+ updating its scrollback and kitty images, but neither writes the framebuffer
71
+ nor forwards global `/dev/input` mouse events. This allows separate bunterm
72
+ processes on `/dev/tty1`, `/dev/tty2`, and so on to hand the display back and
73
+ forth through the normal Linux VT switch. A few console-specific
61
74
  sequences are rewritten to xterm's (`F1`–`F5`, `Home`, `End`), and arrow keys
62
75
  follow the program's application cursor mode.
63
76
 
@@ -80,7 +93,9 @@ hidden while it is scrolled out of view.
80
93
  `/dev/input` and never loads the mouse module.
81
94
 
82
95
  `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. A USB
96
+ what QEMU's q35 machine and most PCs present — has a device node ready. The
97
+ aarch64 image loads `virtio_input` in place of `psmouse`, for QEMU's
98
+ `virtio-tablet-pci` and `virtio-keyboard-pci`. A USB
84
99
  mouse also needs the `usbhid` stack, which a `--real` image carries and
85
100
  `cfg.all` loads. The pointer follows relative devices (a mouse) and absolute
86
101
  ones (a tablet or touchscreen) alike; a device that cannot be opened is
@@ -91,6 +106,8 @@ reported and the session continues without it.
91
106
  - Needs a framebuffer: `linux-lts` has one built in, while the default
92
107
  `linux-virt` builds it as modules the image does not ship, so build with
93
108
  `--linux-lts` or `--real` (efifb/simpledrm from UEFI).
109
+ - The aarch64 image has no framebuffer yet: `run-qemu.sh` gives QEMU's
110
+ `virt` machine no display device, so there is no `/dev/fb0` to draw on.
94
111
  - No selection, and no keyboard way to scroll back: the wheel is it. The
95
112
  buffer keeps 1000 lines. A program that does not enable mouse reporting
96
113
  sees no clicks — the arrow still moves, so the pointer is visibly alive.
@@ -100,7 +117,8 @@ reported and the session continues without it.
100
117
  - Kitty images are drawn above text; `z` ordering below text and animation
101
118
  frames are not implemented, and file transmission (`t=f`/`t=t`) works only
102
119
  for files under `/tmp`, `/dev/shm` or `$TMPDIR`.
103
- - One framebuffer, one session. For panes and tabs run `jsmdcui` inside it.
120
+ - One bunterm per virtual console may use the framebuffer; only the active VT
121
+ draws. For panes and tabs within one VT, run `jsmdcui` inside it.
104
122
 
105
123
  ## See also
106
124
 
@@ -84,7 +84,7 @@ than an X11 `Display`. Colours are `{ r, g, b, a }` objects.
84
84
  | `openConsole`, `setConsoleGraphics`, `setConsoleText` | Explicit Linux virtual-console control for callers that need it. |
85
85
  | `activeConsole(console)`, `activateConsole(console, n)`, `consoleNumber(path)` | `VT_GETSTATE` / `VT_ACTIVATE` + `VT_WAITACTIVE`, and the number in a `/dev/ttyN` path. |
86
86
  | `watchConsoleSwitches(console, onAcquire)` | `VT_SETMODE` with `VT_PROCESS`: answers the kernel's switch-away and switch-back signals and calls `onAcquire` when the display returns. Returns a function restoring automatic switching. |
87
- | `runGraphics(draw, { device, console, switchConsole }?)` | Open the framebuffer, put the console in graphics mode, run `await draw(display, { console, onRestore, onAcquire })`, and restore text mode however the call ends (return, throw, `process.exit()`, `SIGHUP`/`SIGINT`/`SIGQUIT`/`SIGTERM`). `console` lists console devices to try, such as `["/dev/tty1"]`; when the one opened is a `/dev/ttyN` other than the console on screen, the display is switched to it first and back afterwards (`switchConsole: false` disables this). Console switches with Ctrl-Alt-Fn keep working; `onAcquire(callback)` runs when the display comes back so the caller can repaint, and `onRestore(callback)` registers cleanup to run before text mode returns. |
87
+ | `runGraphics(draw, { device, console, switchConsole, prepareConsole }?)` | Open the framebuffer, optionally call `await prepareConsole(console)`, put the console in graphics mode, run `await draw(display, { console, onRestore, onRelease, onAcquire })`, and restore text mode however the call ends (return, throw, `process.exit()`, `SIGHUP`/`SIGINT`/`SIGQUIT`/`SIGTERM`). `console` lists console devices to try, such as `["/dev/tty1"]`; when the one opened is a `/dev/ttyN` other than the console on screen, the display is switched to it first and back afterwards (`switchConsole: false` disables this). Console switches with Ctrl-Alt-Fn keep working: `onRelease(callback)` runs before the switch is acknowledged so drawing can stop, `onAcquire(callback)` runs when the display comes back, and `onRestore(callback)` registers cleanup to run before text mode returns. A cleanup function returned by `prepareConsole` is registered the same way. |
88
88
  | `runDemo({ polygons, device }?)` | Run the CLI demo through `runGraphics()`. Each item in `polygons` is drawn separately. |
89
89
 
90
90
  The module supports 16-, 24-, and 32-bit packed-pixel framebuffers and uses
@@ -28,6 +28,8 @@ Use `-` as ARCHIVE to read from standard input or write to standard output.
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
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
@@ -0,0 +1,4 @@
1
+ # Licenses in the built image
2
+
3
+ The packaging step copies the project `LICENSE`, `NOTICE.md`, and third-party
4
+ `LICENSES/` directory here.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.5.1",
3
+ "version": "0.7.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",
@@ -13,7 +13,7 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/jjtseng93/buninu-linux.git"
15
15
  },
16
- "homepage": "https://github.com/jjtseng93/buninu-linux#readme",
16
+ "homepage": "https://buninu.org",
17
17
  "bugs": {
18
18
  "url": "https://github.com/jjtseng93/buninu-linux/issues"
19
19
  },
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"
package/scripts/fetch.sh CHANGED
@@ -2,8 +2,9 @@
2
2
  #
3
3
  # fetch <url> <name> <sha256>
4
4
  #
5
- # Downloads to downloads/<name>, unless a file with that exact SHA-256 is
6
- # already sitting there. The hash is both the cache key and the integrity
5
+ # Downloads to $downloads_dir/<name> (downloads/<arch>/, set by
6
+ # scripts/arch.sh), unless a file with that exact SHA-256 is already sitting
7
+ # there. The hash is both the cache key and the integrity
7
8
  # check, so there is no weaker "file exists" path: a truncated, stale or
8
9
  # tampered file fails the first comparison, gets fetched again, and still has
9
10
  # to pass the second one before any script extracts from it.
@@ -12,7 +13,7 @@
12
13
  # a version (which changes both the name and the hash) always refetches.
13
14
  fetch() {
14
15
  local url=$1 name=$2 sha=$3
15
- local path="downloads/$name"
16
+ local path="$downloads_dir/$name"
16
17
 
17
18
  if [ -f "$path" ] && printf '%s %s\n' "$sha" "$path" | sha256sum --check --status; then
18
19
  echo "cached $name"
@@ -20,6 +21,7 @@ fetch() {
20
21
  fi
21
22
 
22
23
  echo "fetch $name"
24
+ mkdir -p "$downloads_dir"
23
25
  curl -fL "$url" -o "$path"
24
26
  # --quiet prints only failures; a mismatch exits non-zero under `set -e`.
25
27
  printf '%s %s\n' "$sha" "$path" | sha256sum --check --quiet
@@ -2,13 +2,30 @@
2
2
  set -euo pipefail
3
3
 
4
4
  cd "$(dirname "$0")/.."
5
+
6
+ . ./scripts/arch.sh
7
+
8
+ test -x "$native_dir/bin/bun" || {
9
+ echo "error: no $native_dir/bin/bun yet; run the fetch stage first (./index.js -f)" >&2
10
+ exit 1
11
+ }
5
12
  mkdir -p build
6
13
 
7
14
  staging="$(mktemp -d "$PWD/.initramfs.XXXXXX")"
8
15
  archive="$(mktemp "$PWD/.initramfs.XXXXXX.cpio")"
9
16
  trap 'rm -rf "$staging"; rm -f "$archive"' EXIT
10
17
 
18
+ # initramfs/ is the same for every architecture; native/<arch>/ adds bun, the
19
+ # musl loader, the GCC runtime and the kernel modules on top of it.
20
+ # A checkout fetched before native/ existed still has an x86_64 bun and its
21
+ # modules under initramfs/; they are dropped rather than packed.
11
22
  cp -a --reflink=auto initramfs/. "$staging/"
23
+ rm -rf "$staging/bin/bun" "$staging/lib/modules"
24
+ cp -a --reflink=auto "$native_dir/." "$staging/"
25
+ # Keep the project README canonical while making it available to
26
+ # buninu-linux-help from the conventional installed-document path.
27
+ cp README.md "$staging/usr/share/doc/buninu-linux/README.md"
28
+ cp -a LICENSE NOTICE.md LICENSES "$staging/usr/share/licenses/"
12
29
 
13
30
  # fakeroot lets cpio record the character devices without real root. Nothing
14
31
  # has mounted devtmpfs when the kernel execs Bun as PID 1, so every device
@@ -20,10 +37,11 @@ cp -a --reflink=auto initramfs/. "$staging/"
20
37
  # This is the one device that actually blocks booting Bun as PID 1.
21
38
  #
22
39
  # The rest are cheap to include and keep /dev usable if the devtmpfs mount in
23
- # init.js ever fails.
40
+ # init.js ever fails. The serial port is ttyS0 (4, 64) on x86_64 and the
41
+ # PL011 ttyAMA0 (204, 64) on aarch64.
24
42
  fakeroot-tcp -- sh -c '
25
43
  mknod -m 0600 "$1/dev/console" c 5 1
26
- mknod -m 0620 "$1/dev/ttyS0" c 4 64
44
+ mknod -m 0620 "$1/dev/$3" c "$4" 64
27
45
  mknod -m 0666 "$1/dev/null" c 1 3
28
46
  mknod -m 0666 "$1/dev/zero" c 1 5
29
47
  mknod -m 0666 "$1/dev/random" c 1 8
@@ -34,8 +52,8 @@ fakeroot-tcp -- sh -c '
34
52
  | sort -z \
35
53
  | cpio --null --create --format=newc --owner=0:0 --reproducible 2>/dev/null \
36
54
  > "$2"
37
- ' sh "$staging" "$archive"
55
+ ' sh "$staging" "$archive" "$serial_console" "$serial_major"
38
56
 
39
57
  gzip -9n < "$archive" > build/initramfs.cpio.gz
40
58
 
41
- echo "Built build/initramfs.cpio.gz"
59
+ echo "Built build/initramfs.cpio.gz for $arch"
@@ -0,0 +1,13 @@
1
+ # The "ordinary Debian 13 machine" host-test.sh builds and boots the guests on:
2
+ # the README's build packages for both guests, QEMU with OVMF and AAVMF, node
3
+ # to run index.js, git to clone the checkout and expect to drive the serial
4
+ # console. Built for linux/amd64 or linux/arm64, whichever host is simulated.
5
+ FROM debian:13-slim
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ binutils-mingw-w64-x86-64 binutils-aarch64-linux-gnu cpio curl dosfstools fakeroot mtools parted unzip \
8
+ ca-certificates git nodejs expect \
9
+ qemu-system-x86 ovmf qemu-system-arm qemu-efi-aarch64 \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # parted asks udev to settle after writing a partition table; see ../../Dockerfile.
13
+ RUN ln -s /bin/true /usr/local/bin/udevadm
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/expect -f
2
+ # Boots vda.img with `node ./index.js -r --arch <arch>` and drives the serial
3
+ # console: the REPL, the Buninu shell, eth0, PID 1, a tmpfs mount, a fetch from
4
+ # inside the guest and poweroff. Each step waits for the output it expects, so
5
+ # a hang or a wrong answer fails with a message instead of racing ahead.
6
+ #
7
+ # Usage: boot-test.exp <checkout> x86_64|aarch64 [boot timeout in seconds]
8
+
9
+ set checkout [lindex $argv 0]
10
+ set arch [lindex $argv 1]
11
+ set boot_timeout [expr {[llength $argv] > 2 ? [lindex $argv 2] : 900}]
12
+ switch -- $arch {
13
+ x86_64 { set js_arch x64 }
14
+ aarch64 { set js_arch arm64 }
15
+ default {
16
+ puts "usage: boot-test.exp <checkout> x86_64|aarch64 \[boot timeout\]"
17
+ exit 2
18
+ }
19
+ }
20
+
21
+ proc fail {message} {
22
+ puts "\n### FAIL: $message"
23
+ exit 1
24
+ }
25
+
26
+ # Sends one line to the guest, then waits for a regular expression in the
27
+ # output. The patterns match what the command prints, not its echo.
28
+ proc step {what line pattern} {
29
+ send -- "$line\r"
30
+ expect {
31
+ -re $pattern { puts "\n### ok: $what" }
32
+ timeout { fail "$what: no match for /$pattern/ within $::timeout s" }
33
+ eof { fail "$what: QEMU exited" }
34
+ }
35
+ }
36
+
37
+ cd $checkout
38
+ set start [clock seconds]
39
+ spawn node ./index.js -r --arch $arch
40
+
41
+ set timeout $boot_timeout
42
+ expect {
43
+ "bun-repl>" { puts "\n### ok: REPL after [expr {[clock seconds] - $start}] s" }
44
+ "Kernel panic" { fail "kernel panic before the REPL" }
45
+ timeout { fail "no REPL within $boot_timeout s" }
46
+ eof { fail "QEMU exited before the REPL" }
47
+ }
48
+
49
+ # bunmsh switches bracketed paste off (ESC[?2004l) before a command's output
50
+ # and Bun colours the numbers it prints, so a line of output can carry escape
51
+ # sequences around the text.
52
+ set esc {(?:\x1b\[[0-9;?]*[a-zA-Z])*}
53
+
54
+ set timeout 180
55
+ step "process.arch is $js_arch" "process.arch" "'$js_arch'"
56
+ step "Buninu shell" "start()" {\$ }
57
+ step "uname reports $arch" "uname -a" "Linux \\(none\\) \[^\r\n\]* $arch"
58
+ step "eth0 is 10.0.2.15" "ip -br addr" {eth0 +UP +10\.0\.2\.15/24}
59
+ step "PID 1 is bun" "ps -eo pid,args" {\n +1 /bin/bun -e import}
60
+ step "tmpfs mount" \
61
+ "mount -t tmpfs -o size=8M tmpfs /tmp/x --mkdir && echo hi > /tmp/x/f && cat /tmp/x/f && umount /tmp/x" \
62
+ "\n${esc}hi${esc}\r"
63
+ step "fetch from the guest" \
64
+ {bun -e 'console.log((await fetch("http://example.com")).status)'} \
65
+ "\n${esc}200${esc}\r"
66
+ step "poweroff" "poweroff" "reboot: Power down"
67
+ expect {
68
+ eof { puts "\n### ok: QEMU exited, [expr {[clock seconds] - $start}] s in total" }
69
+ timeout { fail "QEMU still running after poweroff" }
70
+ }
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Builds and boots both guests on a fresh Debian 13 host of either CPU, in
3
+ # Docker:
4
+ #
5
+ # test/host/host-test.sh [amd64|arm64]
6
+ #
7
+ # The platform defaults to this machine's own CPU; the other one runs
8
+ # emulated and takes a few times longer. The container (./Dockerfile) has the
9
+ # README's build packages and QEMU. It clones the committed HEAD of this
10
+ # checkout, so uncommitted changes to the project are not tested, then builds
11
+ # with the host toolchain rather than --docker and boots each guest through
12
+ # boot-test.exp. The exit status is 0 only when every step passed.
13
+ #
14
+ # GUESTS="aarch64" which guests to test (default: x86_64 aarch64)
15
+ # BUILD_FLAGS=--linux-lts extra flags for the build stage
16
+ # BOOT_TIMEOUT=900 seconds to wait for the REPL
17
+ # ALPINE_MIRROR=... passed through to the fetch stage
18
+ #
19
+ # downloads/ is mounted read-only as a cache; running `./index.js -f --arch
20
+ # <arch>` here first saves the container from downloading.
21
+ set -euo pipefail
22
+
23
+ cd "$(dirname "$0")/../.."
24
+
25
+ case "${1:-$(uname -m)}" in
26
+ amd64 | x86_64) platform=amd64 ;;
27
+ arm64 | aarch64) platform=arm64 ;;
28
+ *)
29
+ echo "usage: $0 [amd64|arm64]" >&2
30
+ exit 2
31
+ ;;
32
+ esac
33
+
34
+ image="buninu-linux-hosttest:$platform"
35
+ echo "### building $image (the first build takes a few minutes)"
36
+ docker build --quiet --platform "linux/$platform" --tag "$image" test/host > /dev/null
37
+
38
+ # A Linux host's KVM reaches the container too, so a guest that matches the
39
+ # platform runs accelerated. Docker Desktop on macOS exposes no /dev/kvm.
40
+ kvm_device=
41
+ if [ -e /dev/kvm ]; then kvm_device=--device=/dev/kvm; fi
42
+
43
+ mkdir -p downloads
44
+ # $kvm_device is empty or a single word.
45
+ exec docker run --rm --platform "linux/$platform" $kvm_device \
46
+ --mount "type=bind,src=$PWD,dst=/repo,readonly" \
47
+ --mount "type=bind,src=$PWD/downloads,dst=/cache,readonly" \
48
+ --env GUESTS --env BUILD_FLAGS --env BOOT_TIMEOUT --env ALPINE_MIRROR \
49
+ "$image" /repo/test/host/in-container.sh