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/Dockerfile +23 -0
- package/NOTICE.md +111 -63
- package/README.md +240 -51
- package/build-image.sh +9 -5
- package/build-uki.sh +44 -28
- package/fetch-alpine.sh +43 -35
- package/fetch-bun.sh +14 -21
- package/index.js +150 -35
- package/initramfs/bin/chroot +4 -1
- package/initramfs/bin/ip +3 -2
- package/initramfs/bin/ps +3 -0
- package/initramfs/bin/tar +9 -0
- package/initramfs/init.js +79 -41
- package/initramfs/lib/bunterm/images.js +17 -2
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +5 -1
- package/initramfs/usr/share/doc/buninu-linux/tar.md +2 -0
- package/native/aarch64/lib/ld-musl-aarch64.so.1 +0 -0
- package/native/aarch64/lib/libc.musl-aarch64.so.1 +0 -0
- package/native/aarch64/lib/libgcc_s.so.1 +0 -0
- package/native/aarch64/lib/libstdc++.so.6 +0 -0
- package/package.json +1 -1
- package/run-qemu.sh +61 -12
- package/scripts/arch.sh +113 -0
- package/scripts/fetch.sh +5 -3
- package/scripts/pack-initramfs.sh +18 -4
- package/test/host/Dockerfile +13 -0
- package/test/host/boot-test.exp +70 -0
- package/test/host/host-test.sh +49 -0
- package/test/host/in-container.sh +58 -0
- /package/{initramfs → native/x86_64}/lib/ld-musl-x86_64.so.1 +0 -0
- /package/{initramfs → native/x86_64}/lib/libc.musl-x86_64.so.1 +0 -0
- /package/{initramfs → native/x86_64}/lib/libgcc_s.so.1 +0 -0
- /package/{initramfs → native/x86_64}/lib/libstdc++.so.6 +0 -0
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
156
|
-
//
|
|
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(
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
268
|
-
// the default route; rt_dst and
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
74
|
+
const replacesImage = decoded && this.imageSources.get(image.id) !== image;
|
|
75
|
+
if (!decoded || replacesImage) {
|
|
74
76
|
try {
|
|
75
|
-
|
|
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.
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/run-qemu.sh
CHANGED
|
@@ -2,19 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
cd "$(dirname "$0")"
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if [ -z "$
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
35
|
-
-machine
|
|
36
|
-
-cpu
|
|
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 \
|
package/scripts/arch.sh
ADDED
|
@@ -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
|
|
6
|
-
#
|
|
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="
|
|
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
|
|
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
|
+
}
|