buninu-linux 0.5.1 → 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.
package/initramfs/init.js CHANGED
@@ -2,9 +2,19 @@ console.log("init.js: Bun runtime entered JavaScript");
2
2
 
3
3
  const { dlopen, FFIType, ptr, read } = await import("bun:ffi");
4
4
 
5
- console.log("init.js: loading physical musl libc for FFI");
6
-
7
- const libc = dlopen("/lib/libc.musl-x86_64.so.1", {
5
+ // The musl file name and the number of finit_module, which musl has no
6
+ // wrapper for, are all that differs per CPU in this file. /lib/dlopen.js has
7
+ // the same table for the commands; it is repeated rather than imported so
8
+ // that nothing but bun:ffi loads before the mounts below.
9
+ const architecture = {
10
+ x64: { libc: "libc.musl-x86_64.so.1", SYS_finit_module: 313 },
11
+ arm64: { libc: "libc.musl-aarch64.so.1", SYS_finit_module: 273 },
12
+ }[process.arch];
13
+ if (!architecture) throw new Error(`init.js: unsupported CPU architecture ${process.arch}`);
14
+
15
+ console.log(`init.js: loading physical musl libc /lib/${architecture.libc} for FFI`);
16
+
17
+ const libc = dlopen(`/lib/${architecture.libc}`, {
8
18
  mount: {
9
19
  args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64, FFIType.ptr],
10
20
  returns: FFIType.i32,
@@ -152,11 +162,13 @@ try {
152
162
  // The input layer's character devices, so a pointing device has a
153
163
  // /dev/input/event* node for `bunterm --mouse` to read. Loading them only
154
164
  // creates those nodes: the console keyboard is unaffected, and nothing reads
155
- // a pointer unless it is asked to. A USB mouse also needs the usbhid stack,
156
- // which `cfg.all` or a --real boot brings in.
165
+ // a pointer unless it is asked to. psmouse is the PS/2 mouse of a PC or q35;
166
+ // QEMU's aarch64 virt machine has virtio-input instead. Each image ships only
167
+ // the one its machine has, and tryModprobe skips the other. A USB mouse also
168
+ // needs the usbhid stack, which `cfg.all` or a --real boot brings in.
157
169
  try {
158
170
  const { tryModprobe } = await import("/lib/modprobe.js");
159
- const loaded = ["evdev", "psmouse"].filter((module) => tryModprobe(module) !== null);
171
+ const loaded = ["evdev", "psmouse", "virtio_input"].filter((module) => tryModprobe(module) !== null);
160
172
  const { readdirSync } = await import("node:fs");
161
173
  const devices = (() => {
162
174
  try { return readdirSync("/dev/input").filter((name) => name.startsWith("event")); } catch { return []; }
@@ -190,7 +202,11 @@ const loadModule = (path) => {
190
202
  const parameters = cString("");
191
203
  // -EEXIST is harmless when firmware or another module loaded it first.
192
204
  // musl has no finit_module wrapper, so use its generic syscall(2).
193
- check(`finit_module(${path})`, libc.symbols.syscall(313, fd, ptr(parameters), 0), [-17]);
205
+ check(
206
+ `finit_module(${path})`,
207
+ libc.symbols.syscall(architecture.SYS_finit_module, fd, ptr(parameters), 0),
208
+ [-17],
209
+ );
194
210
  } finally {
195
211
  libc.symbols.close(fd);
196
212
  }
@@ -231,21 +247,14 @@ const bringUpLoopback = () => {
231
247
  }
232
248
  };
233
249
 
234
- const configureQemuNetwork = () => {
235
- const release = process.env.KERNEL_RELEASE ?? "6.18.52-0-virt";
236
- const modules = [
237
- `lib/modules/${release}/kernel/net/core/failover.ko`,
238
- `lib/modules/${release}/kernel/drivers/net/net_failover.ko`,
239
- `lib/modules/${release}/kernel/drivers/net/virtio_net.ko`,
240
- ];
241
- if (release.endsWith("-lts")) modules.unshift(
242
- `lib/modules/${release}/kernel/drivers/virtio/virtio_ring.ko`,
243
- `lib/modules/${release}/kernel/drivers/virtio/virtio.ko`,
244
- `lib/modules/${release}/kernel/drivers/virtio/virtio_pci_legacy_dev.ko`,
245
- `lib/modules/${release}/kernel/drivers/virtio/virtio_pci_modern_dev.ko`,
246
- `lib/modules/${release}/kernel/drivers/virtio/virtio_pci.ko`,
247
- );
248
- for (const module of modules) loadModule(`/${module}`);
250
+ // Which virtio pieces are modules differs per kernel flavor and CPU (linux-lts
251
+ // has virtio_pci as a module, linux-virt builds it in), so /lib/modprobe.js
252
+ // resolves them from the shipped modules.dep and modules.builtin: the PCI
253
+ // transport first, since nothing in modules.dep depends on it, then the NIC.
254
+ const configureQemuNetwork = async () => {
255
+ const { modprobe, tryModprobe } = await import("/lib/modprobe.js");
256
+ tryModprobe("virtio_pci");
257
+ modprobe("virtio_net");
249
258
 
250
259
  const fd = check("socket(AF_INET, SOCK_DGRAM)", libc.symbols.socket(2, 2, 0));
251
260
  const ioctl = (request, value, operation) =>
@@ -264,8 +273,9 @@ const configureQemuNetwork = () => {
264
273
 
265
274
  bringUp(ioctl, "eth0");
266
275
 
267
- // struct rtentry on Linux x86_64. Only gateway and flags are needed for
268
- // the default route; rt_dst and rt_genmask remain 0.0.0.0.
276
+ // struct rtentry on 64-bit Linux, the same on x86_64 and aarch64. Only
277
+ // gateway and flags are needed for the default route; rt_dst and
278
+ // rt_genmask remain 0.0.0.0.
269
279
  const route = new Uint8Array(120);
270
280
  const routeView = new DataView(route.buffer);
271
281
  writeSockaddrIpv4(route, 8, "0.0.0.0");
@@ -359,7 +369,7 @@ try {
359
369
 
360
370
 
361
371
  try {
362
- configureQemuNetwork();
372
+ await configureQemuNetwork();
363
373
  console.log("network: eth0 10.0.2.15/24 via 10.0.2.2");
364
374
  const response = await fetch("http://example.com");
365
375
  console.log(`fetch example.com: HTTP ${response.status}, ${response.headers.get("content-type")}`);
@@ -412,7 +422,9 @@ const launchBunmsh = () => {
412
422
 
413
423
 
414
424
 
415
- const launchBuninu = () => {
425
+ let replServer = null;
426
+
427
+ const launchBuninu = async () => {
416
428
 
417
429
  console.log(`
418
430
  Type bunterm in bunmsh: Graphical Terminal
@@ -420,22 +432,36 @@ Type bunterm in bunmsh: Graphical Terminal
420
432
  Supports Kitty Graphics Protocol
421
433
  `)
422
434
 
435
+ const classic_path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
436
+
437
+ const buninu_path = "/buninu/.local/bin:/buninu/.bun/bin:/buninu/bin"
438
+
423
439
  const entry = `/buninu/bin/init.js`;
424
440
  console.log(`buninu: Starting ${entry} --local`);
425
- const shell = Bun.spawnSync([
426
- "/bin/bun", entry, "--local"
427
- ], {
428
- env: {
429
- ...process.env,
430
- PATH:"/bin:/sbin:/usr/bin:/usr/sbin:/buninu/.bun/bin:/buninu/bin",
431
- HOME:"/buninu"
432
- },
433
- stdin: "inherit",
434
- stdout: "inherit",
435
- stderr: "inherit",
436
- });
437
- console.log(`buninu exited with status ${shell.exitCode}; returning to Bun REPL.`);
438
- return shell.exitCode;
441
+ replServer?.pause();
442
+ try {
443
+ const shell = Bun.spawn([
444
+ "/bin/bun", entry, "--local"
445
+ ], {
446
+ env: {
447
+ ...process.env,
448
+ PATH: classic_path + ":" +
449
+ buninu_path,
450
+ HOME:"/buninu"
451
+ },
452
+ stdin: "inherit",
453
+ stdout: "inherit",
454
+ stderr: "inherit",
455
+ });
456
+ const exitCode = await shell.exited;
457
+ console.log(`buninu exited with status ${exitCode}; returning to Bun REPL.`);
458
+ return exitCode;
459
+ } finally {
460
+ // The child inherits the console directly. Keep the REPL from consuming
461
+ // the same input while it runs, but leave PID 1's event loop alive so its
462
+ // SIGCHLD handler can reap orphaned grandchildren such as Chromium.
463
+ replServer?.resume();
464
+ }
439
465
 
440
466
  }
441
467
 
@@ -551,12 +577,24 @@ Type start() to run buninu --local`);
551
577
  showWelcome();
552
578
 
553
579
  const startRepl = () => {
554
- const server = repl.start({
580
+ const server = replServer = repl.start({
555
581
  prompt: "bun-repl> ",
556
582
  input: process.stdin,
557
583
  output: process.stdout,
558
584
  terminal: true,
559
585
  });
586
+ // `start()` returns a promise so PID 1's event loop can keep reaping
587
+ // children. Make the two convenient whole-line forms foreground commands;
588
+ // otherwise the REPL finishes evaluating the pending promise and resumes
589
+ // itself, competing with Buninu for the console input.
590
+ const defaultEval = server.eval.bind(server);
591
+ server.eval = (command, context, filename, callback) => {
592
+ const trimmed = command.trim();
593
+ if (trimmed === "start" || /^start\s*\(\s*\)\s*;?$/.test(trimmed)) {
594
+ command = "await start()\n";
595
+ }
596
+ return defaultEval(command, context, filename, callback);
597
+ };
560
598
  // Run before REPL's own line listener so its next prompt is printed after
561
599
  // the repeated welcome. Empty and whitespace-only lines both trigger it.
562
600
  server.prependListener("line", (line) => {
@@ -53,6 +53,7 @@ export class ImageStore {
53
53
  this.term = term;
54
54
  this.renderer = renderer;
55
55
  this.images = new Map(); // image id → Skia Image
56
+ this.imageSources = new Map(); // image id → parser image object
56
57
  this.placements = new Map(); // placement key → placement
57
58
  this.counter = 0;
58
59
  this.coveredKey = "";
@@ -70,14 +71,26 @@ export class ImageStore {
70
71
  if (graphic.kind !== "placement") return "";
71
72
  const { control, image } = graphic;
72
73
  let decoded = this.images.get(image.id);
73
- if (!decoded) {
74
+ const replacesImage = decoded && this.imageSources.get(image.id) !== image;
75
+ if (!decoded || replacesImage) {
74
76
  try {
75
- decoded = await decodeImage(this.CanvasKit, image);
77
+ const replacement = await decodeImage(this.CanvasKit, image);
78
+ if (replacesImage) {
79
+ // Kitty requires retransmitting a non-zero image id to delete the
80
+ // old image and all its placements. Screen-streaming clients such
81
+ // as casty deliberately reuse one id for every frame.
82
+ for (const placement of [...this.placements.values()]) {
83
+ if (placement.imageId === image.id) this.dispose(placement);
84
+ }
85
+ decoded.delete();
86
+ }
87
+ decoded = replacement;
76
88
  } catch (error) {
77
89
  graphic.responseMessage = `EBADPNG:${error.message}`;
78
90
  return "";
79
91
  }
80
92
  this.images.set(image.id, decoded);
93
+ this.imageSources.set(image.id, image);
81
94
  }
82
95
  const { cellWidth, cellHeight } = this.renderer;
83
96
  const buffer = this.term.buffer.active;
@@ -169,6 +182,7 @@ export class ImageStore {
169
182
  if (!used.has(id)) {
170
183
  image.delete();
171
184
  this.images.delete(id);
185
+ this.imageSources.delete(id);
172
186
  }
173
187
  }
174
188
  }
@@ -221,5 +235,6 @@ export class ImageStore {
221
235
  this.placements.clear();
222
236
  for (const image of this.images.values()) image.delete();
223
237
  this.images.clear();
238
+ this.imageSources.clear();
224
239
  }
225
240
  }
@@ -80,7 +80,9 @@ hidden while it is scrolled out of view.
80
80
  `/dev/input` and never loads the mouse module.
81
81
 
82
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. A USB
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
84
86
  mouse also needs the `usbhid` stack, which a `--real` image carries and
85
87
  `cfg.all` loads. The pointer follows relative devices (a mouse) and absolute
86
88
  ones (a tablet or touchscreen) alike; a device that cannot be opened is
@@ -91,6 +93,8 @@ reported and the session continues without it.
91
93
  - Needs a framebuffer: `linux-lts` has one built in, while the default
92
94
  `linux-virt` builds it as modules the image does not ship, so build with
93
95
  `--linux-lts` or `--real` (efifb/simpledrm from UEFI).
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.
94
98
  - No selection, and no keyboard way to scroll back: the wheel is it. The
95
99
  buffer keeps 1000 lines. A program that does not enable mouse reporting
96
100
  sees no clicks — the arrow still moves, so the pointer is visibly alive.
@@ -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
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.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",
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,26 @@
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/"
12
25
 
13
26
  # fakeroot lets cpio record the character devices without real root. Nothing
14
27
  # has mounted devtmpfs when the kernel execs Bun as PID 1, so every device
@@ -20,10 +33,11 @@ cp -a --reflink=auto initramfs/. "$staging/"
20
33
  # This is the one device that actually blocks booting Bun as PID 1.
21
34
  #
22
35
  # The rest are cheap to include and keep /dev usable if the devtmpfs mount in
23
- # init.js ever fails.
36
+ # init.js ever fails. The serial port is ttyS0 (4, 64) on x86_64 and the
37
+ # PL011 ttyAMA0 (204, 64) on aarch64.
24
38
  fakeroot-tcp -- sh -c '
25
39
  mknod -m 0600 "$1/dev/console" c 5 1
26
- mknod -m 0620 "$1/dev/ttyS0" c 4 64
40
+ mknod -m 0620 "$1/dev/$3" c "$4" 64
27
41
  mknod -m 0666 "$1/dev/null" c 1 3
28
42
  mknod -m 0666 "$1/dev/zero" c 1 5
29
43
  mknod -m 0666 "$1/dev/random" c 1 8
@@ -34,8 +48,8 @@ fakeroot-tcp -- sh -c '
34
48
  | sort -z \
35
49
  | cpio --null --create --format=newc --owner=0:0 --reproducible 2>/dev/null \
36
50
  > "$2"
37
- ' sh "$staging" "$archive"
51
+ ' sh "$staging" "$archive" "$serial_console" "$serial_major"
38
52
 
39
53
  gzip -9n < "$archive" > build/initramfs.cpio.gz
40
54
 
41
- echo "Built build/initramfs.cpio.gz"
55
+ 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
+ }