buninu-linux 0.2.0 → 0.3.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/NOTICE.md +22 -2
- package/README.md +399 -196
- package/build-uki.sh +4 -2
- package/fetch-alpine.sh +73 -43
- package/index.js +4 -1
- package/initramfs/bin/ip +629 -0
- package/initramfs/bin/mount +400 -0
- package/initramfs/bin/poweroff +31 -0
- package/initramfs/bin/umount +70 -0
- package/initramfs/init.js +164 -11
- package/initramfs/lib/dlopen.js +130 -0
- package/initramfs/lib/modprobe.js +235 -0
- package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
- package/package.json +2 -2
package/initramfs/init.js
CHANGED
|
@@ -37,6 +37,10 @@ const libc = dlopen("/lib/libc.musl-x86_64.so.1", {
|
|
|
37
37
|
args: [FFIType.i32],
|
|
38
38
|
returns: FFIType.i32,
|
|
39
39
|
},
|
|
40
|
+
mkdir: {
|
|
41
|
+
args: [FFIType.ptr, FFIType.i32],
|
|
42
|
+
returns: FFIType.i32,
|
|
43
|
+
},
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
const cString = (value) => new TextEncoder().encode(`${value}\0`);
|
|
@@ -63,18 +67,23 @@ const errno = () => read.i32(errnoLocation);
|
|
|
63
67
|
//
|
|
64
68
|
// EBUSY is tolerated so this still works if something else mounted them first.
|
|
65
69
|
const mountFilesystems = () => {
|
|
66
|
-
for (const [source, target, type] of [
|
|
70
|
+
for (const [source, target, type, data] of [
|
|
67
71
|
["proc", "/proc", "proc"],
|
|
68
72
|
["sysfs", "/sys", "sysfs"],
|
|
69
73
|
["devtmpfs", "/dev", "devtmpfs"],
|
|
70
74
|
["tmpfs", "/tmp", "tmpfs"],
|
|
75
|
+
// Since Linux 4.7 open("/dev/ptmx") resolves through /dev/pts/ptmx, so
|
|
76
|
+
// without this mount every pty open fails with ENODEV. The directory is
|
|
77
|
+
// created on the devtmpfs mounted just above.
|
|
78
|
+
["devpts", "/dev/pts", "devpts", "gid=5,mode=620,ptmxmode=666"],
|
|
71
79
|
]) {
|
|
80
|
+
if (type === "devpts") libc.symbols.mkdir(ptr(cString(target)), 0o755);
|
|
72
81
|
const result = libc.symbols.mount(
|
|
73
82
|
ptr(cString(source)),
|
|
74
83
|
ptr(cString(target)),
|
|
75
84
|
ptr(cString(type)),
|
|
76
85
|
0n,
|
|
77
|
-
null,
|
|
86
|
+
data === undefined ? null : ptr(cString(data)),
|
|
78
87
|
);
|
|
79
88
|
if (result === 0) continue;
|
|
80
89
|
if (errno() === 16) continue; // EBUSY: already mounted
|
|
@@ -83,13 +92,23 @@ const mountFilesystems = () => {
|
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
mountFilesystems();
|
|
86
|
-
console.log("init.js: mounted /proc, /sys, /dev, /tmp");
|
|
95
|
+
console.log("init.js: mounted /proc, /sys, /dev, /tmp, /dev/pts");
|
|
87
96
|
|
|
88
97
|
const { default: repl } = await import("node:repl");
|
|
89
98
|
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
90
99
|
|
|
91
100
|
global.repl = repl;
|
|
92
101
|
|
|
102
|
+
// QEMU user networking exposes its DNS proxy at 10.0.2.3. A real-machine
|
|
103
|
+
// image instead starts with public resolvers until the user configures DNS
|
|
104
|
+
// for the local network.
|
|
105
|
+
writeFileSync(
|
|
106
|
+
"/etc/resolv.conf",
|
|
107
|
+
process.env.REAL_MACHINE === "1"
|
|
108
|
+
? "nameserver 1.1.1.1\nnameserver 8.8.8.8\n"
|
|
109
|
+
: "nameserver 10.0.2.3\n",
|
|
110
|
+
);
|
|
111
|
+
|
|
93
112
|
|
|
94
113
|
const loadModule = (path) => {
|
|
95
114
|
const name = cString(path);
|
|
@@ -203,6 +222,36 @@ const configureRealKeyboard = () => {
|
|
|
203
222
|
]) loadModule(`/lib/modules/${release}/kernel/${module}`);
|
|
204
223
|
};
|
|
205
224
|
|
|
225
|
+
// ACPI battery, AC adapter, power button and thermal zones are modules on
|
|
226
|
+
// Alpine's kernels; until they are loaded /sys/class/power_supply is empty.
|
|
227
|
+
// Loaded on demand from the REPL as cfg.power, through /lib/modprobe.js (a
|
|
228
|
+
// require() is fine here: /proc exists by the time anyone can type it). The
|
|
229
|
+
// modules are only in the image when it was built with --real.
|
|
230
|
+
const configureBattery = () => {
|
|
231
|
+
const { modprobe } = require("/lib/modprobe.js");
|
|
232
|
+
for (const module of ["battery", "ac", "button", "thermal"]) {
|
|
233
|
+
try {
|
|
234
|
+
const inserted = modprobe(module);
|
|
235
|
+
console.log(`acpi: ${module} ${inserted.length ? "loaded" : "already loaded"}`);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`acpi: ${module}: ${error.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const { readdirSync, readFileSync } = require("node:fs");
|
|
241
|
+
const read = (path) => { try { return readFileSync(path, "utf8").trim(); } catch { return "-"; } };
|
|
242
|
+
const supplies = (() => { try { return readdirSync("/sys/class/power_supply"); } catch { return []; } })();
|
|
243
|
+
if (supplies.length === 0) console.log("power_supply: nothing registered (no ACPI battery or AC adapter on this machine)");
|
|
244
|
+
for (const name of supplies) {
|
|
245
|
+
const base = `/sys/class/power_supply/${name}`;
|
|
246
|
+
const type = read(`${base}/type`);
|
|
247
|
+
const detail = type === "Battery"
|
|
248
|
+
? `${read(`${base}/status`)} ${read(`${base}/capacity`)}%`
|
|
249
|
+
: `online=${read(`${base}/online`)}`;
|
|
250
|
+
console.log(`power_supply: ${name} ${type} ${detail}`);
|
|
251
|
+
}
|
|
252
|
+
return supplies;
|
|
253
|
+
};
|
|
254
|
+
|
|
206
255
|
const reapChildren = () => {
|
|
207
256
|
const status = new Int32Array(1);
|
|
208
257
|
while (libc.symbols.waitpid(-1, ptr(status), 1) > 0) {}
|
|
@@ -213,6 +262,13 @@ process.on("SIGCHLD", reapChildren);
|
|
|
213
262
|
process.on("SIGTERM", () => console.log("PID 1 received SIGTERM"));
|
|
214
263
|
process.on("SIGINT", () => console.log("PID 1 received SIGINT"));
|
|
215
264
|
|
|
265
|
+
// The REPL catches what its eval throws, but not a rejection or a throw
|
|
266
|
+
// from a timer or callback: Bun then exits, and PID 1 exiting is a kernel
|
|
267
|
+
// panic ("Attempted to kill init!"). With a listener installed Bun only
|
|
268
|
+
// reports, so a stray `Promise.reject()` at the prompt stays a message.
|
|
269
|
+
process.on("uncaughtException", (error) => console.error("PID 1 uncaught exception (ignored):", error));
|
|
270
|
+
process.on("unhandledRejection", (reason) => console.error("PID 1 unhandled rejection (ignored):", reason));
|
|
271
|
+
|
|
216
272
|
|
|
217
273
|
try {
|
|
218
274
|
bringUpLoopback();
|
|
@@ -228,6 +284,7 @@ try {
|
|
|
228
284
|
console.error("USB keyboard setup failed:", error);
|
|
229
285
|
}
|
|
230
286
|
|
|
287
|
+
|
|
231
288
|
try {
|
|
232
289
|
configureQemuNetwork();
|
|
233
290
|
console.log("network: eth0 10.0.2.15/24 via 10.0.2.2");
|
|
@@ -289,7 +346,8 @@ const launchBuninu = () => {
|
|
|
289
346
|
const shell = Bun.spawnSync([
|
|
290
347
|
"/bin/bun", entry, "--local"
|
|
291
348
|
], {
|
|
292
|
-
env: {
|
|
349
|
+
env: {
|
|
350
|
+
...process.env,
|
|
293
351
|
PATH:"/bin:/usr/bin:/buninu/bin",
|
|
294
352
|
HOME:"/buninu"
|
|
295
353
|
},
|
|
@@ -309,19 +367,109 @@ const launchBuninu = () => {
|
|
|
309
367
|
// Configuration complete
|
|
310
368
|
|
|
311
369
|
|
|
370
|
+
globalThis.bunmsh = launchBunmsh;
|
|
371
|
+
//console.log("Type bunmsh() to install and enter the bunmsh shell.");
|
|
372
|
+
|
|
373
|
+
globalThis.start = launchBuninu;
|
|
374
|
+
|
|
375
|
+
// cfg.net loads every packaged network module, including PHY and bus support
|
|
376
|
+
// that a kernel userspace modprobe helper would normally load on demand. It
|
|
377
|
+
// then reports the device matches and lists the resulting interfaces. The
|
|
378
|
+
// address and route are still yours to set with `ip`.
|
|
379
|
+
const configureEthernet = () => {
|
|
380
|
+
const { autoload, modprobeAll } = require("/lib/modprobe.js");
|
|
381
|
+
const { readdirSync, readFileSync } = require("node:fs");
|
|
382
|
+
const read = (path) => { try { return readFileSync(path, "utf8").trim(); } catch { return "-"; } };
|
|
383
|
+
const all = modprobeAll({ accept: (_module, path) => path.startsWith("kernel/drivers/net/") });
|
|
384
|
+
console.log(`eth: loaded all packaged network modules (${all.inserted.length} newly loaded, ${all.errors.length} failed)`);
|
|
385
|
+
for (const { module, error } of all.errors) console.log(`eth: ${module}: ${error}`);
|
|
386
|
+
const report = autoload({
|
|
387
|
+
// PCI class 02 (network), virtio device id 1 (net), every USB device —
|
|
388
|
+
// but only network drivers, so a USB keyboard does not show up as usbhid.
|
|
389
|
+
accept: (modalias) => /^usb:/.test(modalias) || /^virtio:d00000001v/.test(modalias) || /bc02sc/.test(modalias),
|
|
390
|
+
acceptModule: (module, path) => path.startsWith("kernel/drivers/net/"),
|
|
391
|
+
});
|
|
392
|
+
if (report.length === 0) console.log("eth: no network device matched a module in the image (driver built in, or not built with --real)");
|
|
393
|
+
// Network devices nothing in the image claims, so the chip is at least named.
|
|
394
|
+
try {
|
|
395
|
+
for (const device of readdirSync("/sys/bus/pci/devices")) {
|
|
396
|
+
const modalias = read(`/sys/bus/pci/devices/${device}/modalias`);
|
|
397
|
+
if (!/bc02sc/.test(modalias) || report.some((entry) => entry.device === device)) continue;
|
|
398
|
+
const [, vendor, id] = /^pci:v0000([0-9A-F]{4})d0000([0-9A-F]{4})/.exec(modalias) ?? [];
|
|
399
|
+
const kind = /bc02sc80/.test(modalias) ? "wireless" : "network";
|
|
400
|
+
console.log(`eth: pci ${device} ${kind} controller ${vendor}:${id} has no driver in the image`);
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
for (const { bus, device, modules, loaded, error } of report) {
|
|
404
|
+
const status = error ?? (loaded.length ? `loaded ${loaded.join(" ")}` : "already loaded");
|
|
405
|
+
console.log(`eth: ${bus} ${device} -> ${modules.join(" ")}: ${status}`);
|
|
406
|
+
}
|
|
407
|
+
const interfaces = (() => { try { return readdirSync("/sys/class/net").filter((name) => name !== "lo"); } catch { return []; } })();
|
|
408
|
+
for (const name of interfaces) {
|
|
409
|
+
const carrier = read(`/sys/class/net/${name}/carrier`);
|
|
410
|
+
console.log(`eth: ${name} ${read(`/sys/class/net/${name}/address`)} ${read(`/sys/class/net/${name}/operstate`)}${carrier === "1" ? " carrier" : ""}`);
|
|
411
|
+
}
|
|
412
|
+
if (interfaces.length) console.log(`eth:
|
|
413
|
+
Next steps in shell( after start() ):
|
|
414
|
+
ip link set ${interfaces[0]} up
|
|
415
|
+
ip addr add A.B.C.D/24 dev ${interfaces[0]};
|
|
416
|
+
ip route add default via GATEWAY
|
|
417
|
+
`);
|
|
418
|
+
return interfaces;
|
|
419
|
+
};
|
|
312
420
|
|
|
421
|
+
// Explicit full coldplug for this deliberately tiny system: load every .ko
|
|
422
|
+
// represented in modules.dep, recursively including each module's declared
|
|
423
|
+
// dependencies. Failures are reported individually without stopping the rest.
|
|
424
|
+
const configureModules = () => {
|
|
425
|
+
const { modprobeAll } = require("/lib/modprobe.js");
|
|
426
|
+
const report = modprobeAll();
|
|
427
|
+
console.log(`modules: ${report.inserted.length} newly loaded, ${report.errors.length} failed`);
|
|
428
|
+
for (const { module, error } of report.errors) console.log(`modules: ${module}: ${error}`);
|
|
429
|
+
return report;
|
|
430
|
+
};
|
|
313
431
|
|
|
432
|
+
// Load the packaged storage stack so devtmpfs can expose disks before the
|
|
433
|
+
// user has a concrete /dev path to pass to mount. This covers virtio block,
|
|
434
|
+
// SATA/PATA/SCSI, NVMe behind VMD, and USB mass-storage/UAS.
|
|
435
|
+
const configureDisks = () => {
|
|
436
|
+
const { modprobeAll } = require("/lib/modprobe.js");
|
|
437
|
+
const { readdirSync } = require("node:fs");
|
|
438
|
+
const storagePath = /^kernel\/drivers\/(?:ata\/|block\/virtio_blk\.ko$|nvme\/|pci\/controller\/vmd\.ko$|scsi\/|usb\/storage\/)/;
|
|
439
|
+
const report = modprobeAll({ accept: (_module, path) => storagePath.test(path) });
|
|
440
|
+
console.log(`disk: ${report.inserted.length} newly loaded, ${report.errors.length} failed`);
|
|
441
|
+
for (const { module, error } of report.errors) console.log(`disk: ${module}: ${error}`);
|
|
442
|
+
const devices = (() => { try { return readdirSync("/sys/class/block"); } catch { return []; } })();
|
|
443
|
+
console.log(`disk: ${devices.length ? devices.map((name) => `/dev/${name}`).join(" ") : "no block devices registered"}`);
|
|
444
|
+
return devices;
|
|
445
|
+
};
|
|
314
446
|
|
|
315
|
-
|
|
447
|
+
globalThis.cfg = Object.freeze(Object.defineProperties({}, {
|
|
448
|
+
all: { enumerable: true, get: configureModules },
|
|
449
|
+
disk: { enumerable: true, get: configureDisks },
|
|
450
|
+
net: { enumerable: true, get: configureEthernet },
|
|
451
|
+
power: { enumerable: true, get: configureBattery },
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
// Compatibility aliases for images and notes that used the original API.
|
|
455
|
+
globalThis.cfgEth = configureEthernet;
|
|
456
|
+
globalThis.cfgNet = configureEthernet;
|
|
457
|
+
globalThis.cfgDisk = configureDisks;
|
|
458
|
+
globalThis.cfgMod = configureModules;
|
|
459
|
+
globalThis.cfgBat = configureBattery;
|
|
460
|
+
|
|
461
|
+
const showWelcome = () => {
|
|
462
|
+
console.log(`
|
|
316
463
|
Welcome to Buninu Linux!
|
|
317
464
|
Bun ${Bun.version} is now PID ${process.pid}
|
|
318
|
-
`);
|
|
319
|
-
|
|
320
|
-
globalThis.
|
|
321
|
-
|
|
465
|
+
Type start() to run buninu --local`);
|
|
466
|
+
if (process.env.REAL_MACHINE === "1") {
|
|
467
|
+
console.log(`Configuration getters: ${Object.keys(globalThis.cfg).map((name) => `cfg.${name}`).join(", ")}`);
|
|
468
|
+
}
|
|
469
|
+
console.log();
|
|
470
|
+
};
|
|
322
471
|
|
|
323
|
-
|
|
324
|
-
console.log("Type start() to run buninu --local");
|
|
472
|
+
showWelcome();
|
|
325
473
|
|
|
326
474
|
const startRepl = () => {
|
|
327
475
|
const server = repl.start({
|
|
@@ -330,6 +478,11 @@ const startRepl = () => {
|
|
|
330
478
|
output: process.stdout,
|
|
331
479
|
terminal: true,
|
|
332
480
|
});
|
|
481
|
+
// Run before REPL's own line listener so its next prompt is printed after
|
|
482
|
+
// the repeated welcome. Empty and whitespace-only lines both trigger it.
|
|
483
|
+
server.prependListener("line", (line) => {
|
|
484
|
+
if (line.trim() === "") showWelcome();
|
|
485
|
+
});
|
|
333
486
|
server.on("exit", () => {
|
|
334
487
|
console.log("REPL exited; restarting it to keep PID 1 alive.");
|
|
335
488
|
setTimeout(startRepl, 0);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// /lib/dlopen.js — the shared libc binding for Buninu Linux commands.
|
|
2
|
+
//
|
|
3
|
+
// init.js opens musl on its own because it runs before /proc exists and must
|
|
4
|
+
// not import anything. Every other command imports this module instead, so
|
|
5
|
+
// the symbol table, the errno plumbing and the error messages live in one
|
|
6
|
+
// place. The library is located relative to this file (/lib in the guest),
|
|
7
|
+
// which also lets the commands run from a checkout for testing.
|
|
8
|
+
|
|
9
|
+
import { dlopen, FFIType, ptr, read, CString } from "bun:ffi";
|
|
10
|
+
|
|
11
|
+
export { ptr, read };
|
|
12
|
+
|
|
13
|
+
// Everything that differs per CPU lives in this table: the musl file name
|
|
14
|
+
// and the syscall numbers musl has no wrapper for. Struct layouts used by
|
|
15
|
+
// the commands (ifreq, rtentry) are the LP64 ones and hold on both.
|
|
16
|
+
const architectures = {
|
|
17
|
+
x64: { libc: "libc.musl-x86_64.so.1", SYS_finit_module: 313 },
|
|
18
|
+
arm64: { libc: "libc.musl-aarch64.so.1", SYS_finit_module: 273 },
|
|
19
|
+
};
|
|
20
|
+
export const arch = architectures[process.arch]
|
|
21
|
+
?? (() => { throw new Error(`unsupported CPU architecture ${process.arch}`); })();
|
|
22
|
+
|
|
23
|
+
// BUNINU_LIBC lets the commands run on a glibc host for testing:
|
|
24
|
+
// BUNINU_LIBC=/lib/aarch64-linux-gnu/libc.so.6 bun initramfs/bin/ip addr
|
|
25
|
+
export const libcPath = process.env.BUNINU_LIBC ?? `${import.meta.dir}/${arch.libc}`;
|
|
26
|
+
|
|
27
|
+
export const libc = dlopen(libcPath, {
|
|
28
|
+
mount: {
|
|
29
|
+
args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.u64, FFIType.ptr],
|
|
30
|
+
returns: FFIType.i32,
|
|
31
|
+
},
|
|
32
|
+
umount2: {
|
|
33
|
+
args: [FFIType.ptr, FFIType.i32],
|
|
34
|
+
returns: FFIType.i32,
|
|
35
|
+
},
|
|
36
|
+
reboot: {
|
|
37
|
+
args: [FFIType.i32],
|
|
38
|
+
returns: FFIType.i32,
|
|
39
|
+
},
|
|
40
|
+
sync: {
|
|
41
|
+
args: [],
|
|
42
|
+
returns: FFIType.void,
|
|
43
|
+
},
|
|
44
|
+
__errno_location: {
|
|
45
|
+
args: [],
|
|
46
|
+
returns: FFIType.ptr,
|
|
47
|
+
},
|
|
48
|
+
strerror: {
|
|
49
|
+
args: [FFIType.i32],
|
|
50
|
+
returns: FFIType.ptr,
|
|
51
|
+
},
|
|
52
|
+
open: {
|
|
53
|
+
args: [FFIType.ptr, FFIType.i32, FFIType.i32],
|
|
54
|
+
returns: FFIType.i32,
|
|
55
|
+
},
|
|
56
|
+
close: {
|
|
57
|
+
args: [FFIType.i32],
|
|
58
|
+
returns: FFIType.i32,
|
|
59
|
+
},
|
|
60
|
+
ioctl: {
|
|
61
|
+
args: [FFIType.i32, FFIType.u64, FFIType.ptr],
|
|
62
|
+
returns: FFIType.i32,
|
|
63
|
+
},
|
|
64
|
+
socket: {
|
|
65
|
+
args: [FFIType.i32, FFIType.i32, FFIType.i32],
|
|
66
|
+
returns: FFIType.i32,
|
|
67
|
+
},
|
|
68
|
+
syscall: {
|
|
69
|
+
args: [FFIType.i64, FFIType.i64, FFIType.ptr, FFIType.i64],
|
|
70
|
+
returns: FFIType.i32,
|
|
71
|
+
},
|
|
72
|
+
waitpid: {
|
|
73
|
+
args: [FFIType.i32, FFIType.ptr, FFIType.i32],
|
|
74
|
+
returns: FFIType.i32,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export const cString = (value) => new TextEncoder().encode(`${value}\0`);
|
|
79
|
+
|
|
80
|
+
// musl reports failure as -1 plus errno; the raw syscall() wrapper does too.
|
|
81
|
+
const errnoLocation = libc.symbols.__errno_location();
|
|
82
|
+
export const errno = () => read.i32(errnoLocation);
|
|
83
|
+
export const strerror = (code) => new CString(libc.symbols.strerror(code)).toString();
|
|
84
|
+
|
|
85
|
+
export const Errno = {
|
|
86
|
+
EPERM: 1, ENOENT: 2, ESRCH: 3, EIO: 5, ENXIO: 6, EBADF: 9, EAGAIN: 11, ENOMEM: 12,
|
|
87
|
+
EACCES: 13, EBUSY: 16, EEXIST: 17, ENODEV: 19, ENOTDIR: 20, EISDIR: 21,
|
|
88
|
+
EINVAL: 22, EROFS: 30, ENOTEMPTY: 39, ENOPKG: 65, ENOTSUP: 95,
|
|
89
|
+
EADDRNOTAVAIL: 99, ENETUNREACH: 101,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export class SysError extends Error {
|
|
93
|
+
constructor(operation, code = errno()) {
|
|
94
|
+
super(`${operation}: ${strerror(code)}`);
|
|
95
|
+
this.name = "SysError";
|
|
96
|
+
this.operation = operation;
|
|
97
|
+
this.errno = code;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Wraps a libc call: throws SysError on -1 unless errno is in `allowed`,
|
|
102
|
+
// in which case the negated errno comes back so the caller can tell.
|
|
103
|
+
export const check = (operation, result, allowed = []) => {
|
|
104
|
+
if (result !== -1) return result;
|
|
105
|
+
const code = errno();
|
|
106
|
+
if (allowed.includes(code)) return -code;
|
|
107
|
+
throw new SysError(operation, code);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// An AF_INET datagram socket is the handle every SIOC* interface and route
|
|
111
|
+
// ioctl wants. `use` receives an ioctl(request, buffer, operation) closure.
|
|
112
|
+
export const withInetSocket = (use) => {
|
|
113
|
+
const fd = check("socket(AF_INET, SOCK_DGRAM)", libc.symbols.socket(2, 2, 0));
|
|
114
|
+
try {
|
|
115
|
+
const ioctl = (request, buffer, operation, allowed) =>
|
|
116
|
+
check(operation, libc.symbols.ioctl(fd, request, ptr(buffer)), allowed);
|
|
117
|
+
return use(ioctl, fd);
|
|
118
|
+
} finally {
|
|
119
|
+
libc.symbols.close(fd);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Renders /usr/share/doc/buninu-linux/<name>.md for --help. Hyperlinks are
|
|
124
|
+
// on so the references at the end of each page are clickable in terminals
|
|
125
|
+
// that support OSC 8.
|
|
126
|
+
export const showDocument = async (name) => {
|
|
127
|
+
const path = `${import.meta.dir}/../usr/share/doc/buninu-linux/${name}.md`;
|
|
128
|
+
const rendered = Bun.markdown.ansi(await Bun.file(path).text(), { hyperlinks: true });
|
|
129
|
+
process.stdout.write(rendered.endsWith("\n") ? rendered : `${rendered}\n`);
|
|
130
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// /lib/modprobe.js — kernel module loading shared by the Buninu Linux commands.
|
|
2
|
+
//
|
|
3
|
+
// There is no /sbin/modprobe for the kernel to call, so anything that needs a
|
|
4
|
+
// driver (a filesystem, a block device, a NIC) has to load it, and its
|
|
5
|
+
// dependencies, itself. fetch-alpine.sh ships the modules under
|
|
6
|
+
// /lib/modules/<release>/ together with a modules.dep trimmed to that set and
|
|
7
|
+
// the `fs-*` lines of modules.alias, and this module reads those.
|
|
8
|
+
|
|
9
|
+
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
|
10
|
+
import { libc, cString, check, ptr, Errno, arch } from "./dlopen.js";
|
|
11
|
+
|
|
12
|
+
export const kernelRelease = () => {
|
|
13
|
+
if (process.env.KERNEL_RELEASE) return process.env.KERNEL_RELEASE;
|
|
14
|
+
try {
|
|
15
|
+
return readFileSync("/proc/sys/kernel/osrelease", "utf8").trim();
|
|
16
|
+
} catch {
|
|
17
|
+
return "unknown";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// /lib/modules/<release> in the guest; resolved relative to this file so a
|
|
22
|
+
// checkout can be tested the same way.
|
|
23
|
+
export const modulesDirectory = () => `${import.meta.dir}/modules/${kernelRelease()}`;
|
|
24
|
+
|
|
25
|
+
// Module names are canonical with underscores: usb-common.ko is usb_common in
|
|
26
|
+
// /proc/modules.
|
|
27
|
+
const canonical = (name) => name.replaceAll("-", "_");
|
|
28
|
+
|
|
29
|
+
const readIndex = (file) => {
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(`${modulesDirectory()}/${file}`, "utf8");
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// name -> { path, dependencies: [names] } from modules.dep.
|
|
38
|
+
let dependencyTable;
|
|
39
|
+
const dependencies = () => {
|
|
40
|
+
if (dependencyTable) return dependencyTable;
|
|
41
|
+
dependencyTable = new Map();
|
|
42
|
+
for (const line of readIndex("modules.dep").split("\n")) {
|
|
43
|
+
const separator = line.indexOf(":");
|
|
44
|
+
if (separator < 0) continue;
|
|
45
|
+
const path = line.slice(0, separator);
|
|
46
|
+
const deps = line.slice(separator + 1).trim().split(/\s+/).filter(Boolean);
|
|
47
|
+
dependencyTable.set(canonical(moduleName(path)), {
|
|
48
|
+
path,
|
|
49
|
+
dependencies: deps.map((dep) => canonical(moduleName(dep))),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return dependencyTable;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const moduleName = (path) => path.slice(path.lastIndexOf("/") + 1).replace(/\.ko(\.gz)?$/, "");
|
|
56
|
+
|
|
57
|
+
// modules.alias: exact names (fs-ntfs -> ntfs3) in a map, and the device
|
|
58
|
+
// patterns (pci:v000010ECd00008168sv*sd*bc*sc*i* -> r8169) as regexps.
|
|
59
|
+
let aliasTable;
|
|
60
|
+
const aliases = () => {
|
|
61
|
+
if (aliasTable) return aliasTable;
|
|
62
|
+
aliasTable = { exact: new Map(), patterns: [] };
|
|
63
|
+
for (const line of readIndex("modules.alias").split("\n")) {
|
|
64
|
+
const [keyword, pattern, target] = line.trim().split(/\s+/);
|
|
65
|
+
if (keyword !== "alias" || !target) continue;
|
|
66
|
+
const module = canonical(target);
|
|
67
|
+
if (/[*?]/.test(pattern)) {
|
|
68
|
+
const source = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*").replaceAll("?", ".");
|
|
69
|
+
aliasTable.patterns.push({ regexp: new RegExp(`^${source}$`), module });
|
|
70
|
+
} else {
|
|
71
|
+
aliasTable.exact.set(pattern, module);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return aliasTable;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const resolveAlias = (alias) => {
|
|
78
|
+
const { exact } = aliases();
|
|
79
|
+
return exact.get(alias) ?? exact.get(canonical(alias)) ?? canonical(alias);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// The modules whose alias patterns match a device's modalias string.
|
|
83
|
+
export const modulesForModalias = (modalias) => {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const { regexp, module } of aliases().patterns) {
|
|
86
|
+
if (regexp.test(modalias) && !found.includes(module)) found.push(module);
|
|
87
|
+
}
|
|
88
|
+
return found;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// What udev's coldplug does: read each device's modalias under
|
|
92
|
+
// /sys/bus/<bus>/devices and load the drivers that claim it. `accept`
|
|
93
|
+
// filters by modalias (e.g. PCI class 02 = network: /bc02/), `acceptModule`
|
|
94
|
+
// by module name and path (e.g. only kernel/drivers/net/). A driver built
|
|
95
|
+
// into the kernel has no alias line, so its device simply goes unreported.
|
|
96
|
+
// Returns one
|
|
97
|
+
// entry per device with the modules loaded, or the error, for it.
|
|
98
|
+
export const modulePath = (name) => dependencies().get(resolveAlias(name))?.path ?? null;
|
|
99
|
+
|
|
100
|
+
export const autoload = ({ buses = ["pci", "virtio", "usb"], accept = () => true, acceptModule = () => true } = {}) => {
|
|
101
|
+
const report = [];
|
|
102
|
+
for (const bus of buses) {
|
|
103
|
+
let devices;
|
|
104
|
+
try {
|
|
105
|
+
devices = readdirSync(`/sys/bus/${bus}/devices`);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const device of devices) {
|
|
110
|
+
let modalias;
|
|
111
|
+
try {
|
|
112
|
+
modalias = readFileSync(`/sys/bus/${bus}/devices/${device}/modalias`, "utf8").trim();
|
|
113
|
+
} catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!accept(modalias, device)) continue;
|
|
117
|
+
const modules = modulesForModalias(modalias).filter((module) => acceptModule(module, modulePath(module) ?? ""));
|
|
118
|
+
if (modules.length === 0) continue;
|
|
119
|
+
const entry = { bus, device, modalias, modules, loaded: [], error: null };
|
|
120
|
+
for (const module of modules) {
|
|
121
|
+
try {
|
|
122
|
+
entry.loaded.push(...modprobe(module));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
entry.error = error.message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
report.push(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return report;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const loadedModules = () => {
|
|
134
|
+
const loaded = new Set();
|
|
135
|
+
try {
|
|
136
|
+
for (const line of readFileSync("/proc/modules", "utf8").split("\n")) {
|
|
137
|
+
const name = line.split(" ")[0];
|
|
138
|
+
if (name) loaded.add(name);
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
return loaded;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const isLoaded = (name) => loadedModules().has(canonical(name));
|
|
145
|
+
|
|
146
|
+
// Built-in drivers are absent from /proc/modules and from modules.dep, so
|
|
147
|
+
// the ones that matter to callers are listed in modules.builtin.
|
|
148
|
+
let builtinTable;
|
|
149
|
+
export const isBuiltin = (name) => {
|
|
150
|
+
if (!builtinTable) {
|
|
151
|
+
builtinTable = new Set(
|
|
152
|
+
readIndex("modules.builtin").split("\n").filter(Boolean).map((path) => canonical(moduleName(path))),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return builtinTable.has(canonical(name));
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// finit_module(2) on one .ko file. EEXIST (loaded meanwhile, or built in
|
|
159
|
+
// and registered under the same name) is not an error.
|
|
160
|
+
export const insertModule = (path, parameters = "") => {
|
|
161
|
+
const fd = check(`open(${path})`, libc.symbols.open(ptr(cString(path)), 0, 0));
|
|
162
|
+
try {
|
|
163
|
+
const flags = path.endsWith(".gz") ? 4 : 0; // MODULE_INIT_COMPRESSED_FILE
|
|
164
|
+
// musl has no finit_module wrapper, so use its generic syscall(2).
|
|
165
|
+
const result = check(
|
|
166
|
+
`finit_module(${path})`,
|
|
167
|
+
libc.symbols.syscall(arch.SYS_finit_module, fd, ptr(cString(parameters)), flags),
|
|
168
|
+
[Errno.EEXIST],
|
|
169
|
+
);
|
|
170
|
+
return result === 0;
|
|
171
|
+
} finally {
|
|
172
|
+
libc.symbols.close(fd);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export class ModuleNotFound extends Error {
|
|
177
|
+
constructor(name) {
|
|
178
|
+
super(`module ${name} not found in ${modulesDirectory()}`);
|
|
179
|
+
this.name = "ModuleNotFound";
|
|
180
|
+
this.module = name;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Loads `name` (a module name or a modules.alias key) and, first, whatever
|
|
185
|
+
// modules.dep says it needs. Returns the list of modules actually inserted.
|
|
186
|
+
export const modprobe = (name, parameters = "", { loaded = loadedModules(), inserted = [] } = {}) => {
|
|
187
|
+
const module = resolveAlias(name);
|
|
188
|
+
if (loaded.has(module) || isBuiltin(module)) return inserted;
|
|
189
|
+
|
|
190
|
+
const entry = dependencies().get(module);
|
|
191
|
+
if (!entry) throw new ModuleNotFound(module);
|
|
192
|
+
|
|
193
|
+
// modules.dep lists dependencies in reverse load order; recursing on each
|
|
194
|
+
// one covers the deps-of-deps case no matter how the line is ordered.
|
|
195
|
+
for (const dependency of [...entry.dependencies].reverse()) {
|
|
196
|
+
modprobe(dependency, "", { loaded, inserted });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const path = `${modulesDirectory()}/${entry.path}`;
|
|
200
|
+
if (!existsSync(path)) throw new ModuleNotFound(module);
|
|
201
|
+
if (insertModule(path, parameters)) inserted.push(module);
|
|
202
|
+
loaded.add(module);
|
|
203
|
+
return inserted;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Load every selected module shipped in the initramfs (all modules by
|
|
207
|
+
// default). This is useful on the small real-machine image, where there is no
|
|
208
|
+
// userspace modprobe helper to satisfy a driver's later request for an
|
|
209
|
+
// optional PHY or bus module. Keep going after an individual failure: a
|
|
210
|
+
// module for absent hardware commonly rejects initialization, and that must
|
|
211
|
+
// not prevent the remaining modules loading.
|
|
212
|
+
export const modprobeAll = ({ accept = () => true } = {}) => {
|
|
213
|
+
const loaded = loadedModules();
|
|
214
|
+
const inserted = [];
|
|
215
|
+
const errors = [];
|
|
216
|
+
for (const [module, entry] of dependencies()) {
|
|
217
|
+
if (!accept(module, entry.path)) continue;
|
|
218
|
+
try {
|
|
219
|
+
modprobe(module, "", { loaded, inserted });
|
|
220
|
+
} catch (error) {
|
|
221
|
+
errors.push({ module, error: error.message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { inserted, errors };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Best-effort variant for optional drivers: a missing module is not an error.
|
|
228
|
+
export const tryModprobe = (name, parameters = "") => {
|
|
229
|
+
try {
|
|
230
|
+
return modprobe(name, parameters);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof ModuleNotFound) return null;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
};
|