buninu-linux 0.7.3 → 0.7.5

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.
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bun
2
+ // Writes the initramfs without fakeroot, cpio or gzip: a gzipped newc
3
+ // archive of a directory tree, plus device nodes that exist only in the
4
+ // archive. pack-initramfs.sh runs it instead of `fakeroot-tcp ... cpio |
5
+ // gzip` when BUNINU_JS_CPIO=1. Written against node: APIs only, so it runs
6
+ // under node as well as bun.
7
+ //
8
+ // scripts/cpio.js DIRECTORY OUTPUT.cpio.gz [PATH:c:MAJOR:MINOR:MODE]...
9
+ //
10
+ // e.g. dev/console:c:5:1:0600. Like `find . | sort | cpio --create
11
+ // --format=newc --owner=0:0 --reproducible` with the nodes made by mknod:
12
+ // every entry is owned by 0:0, inode numbers count up from 1 and device
13
+ // numbers of the containing filesystem are 0. Entries are sorted by name
14
+ // byte by byte, so the archive does not depend on the locale. mtimes are the
15
+ // files' own, or SOURCE_DATE_EPOCH for every entry when it is set; the
16
+ // device nodes get SOURCE_DATE_EPOCH or the current time.
17
+
18
+ import { lstatSync, readdirSync, readFileSync, readlinkSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { constants, gzipSync } from "node:zlib";
21
+
22
+ const fail = (message) => {
23
+ console.error(`cpio.js: ${message}`);
24
+ process.exit(1);
25
+ };
26
+
27
+ const [directory, output, ...deviceSpecs] = process.argv.slice(2);
28
+ if (!directory || !output) fail("usage: cpio.js DIRECTORY OUTPUT.cpio.gz [PATH:c:MAJOR:MINOR:MODE]...");
29
+ const epoch = process.env.SOURCE_DATE_EPOCH === undefined ? null : Number(process.env.SOURCE_DATE_EPOCH);
30
+ if (epoch !== null && !Number.isInteger(epoch)) fail(`SOURCE_DATE_EPOCH is not a number: ${process.env.SOURCE_DATE_EPOCH}`);
31
+
32
+ const S_IFMT = 0o170000;
33
+ const S_IFDIR = 0o040000;
34
+ const S_IFREG = 0o100000;
35
+ const S_IFLNK = 0o120000;
36
+ const S_IFCHR = 0o020000;
37
+ const S_IFBLK = 0o060000;
38
+
39
+ // Every path under the directory, relative to it.
40
+ const entries = [];
41
+ const walk = (relative) => {
42
+ for (const name of readdirSync(join(directory, relative))) {
43
+ const path = relative ? `${relative}/${name}` : name;
44
+ const stat = lstatSync(join(directory, path));
45
+ entries.push({ path, stat });
46
+ if (stat.isDirectory()) walk(path);
47
+ }
48
+ };
49
+ walk("");
50
+
51
+ const now = Math.floor(Date.now() / 1000);
52
+ const existing = new Set(entries.map(({ path }) => path));
53
+ for (const spec of deviceSpecs) {
54
+ const [path, kind, major, minor, mode] = spec.split(":");
55
+ if (!path || !["c", "b"].includes(kind) || !/^\d+$/.test(major) || !/^\d+$/.test(minor) || !/^[0-7]{3,4}$/.test(mode)) {
56
+ fail(`bad device ${spec}; expected PATH:c:MAJOR:MINOR:MODE`);
57
+ }
58
+ if (existing.has(path)) fail(`${path} already exists in ${directory}`);
59
+ const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
60
+ if (parent && !existing.has(parent)) fail(`${path}: no directory ${parent} in ${directory}`);
61
+ entries.push({
62
+ path,
63
+ device: { mode: (kind === "c" ? S_IFCHR : S_IFBLK) | parseInt(mode, 8), major: Number(major), minor: Number(minor) },
64
+ });
65
+ existing.add(path);
66
+ }
67
+ entries.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
68
+
69
+ const chunks = [];
70
+ const hex = (value) => (value >>> 0).toString(16).padStart(8, "0");
71
+ const pad = (length) => { if (length % 4) chunks.push(Buffer.alloc(4 - (length % 4))); };
72
+ const add = (name, { ino, mode, nlink, mtime, data, rdevMajor = 0, rdevMinor = 0 }) => {
73
+ const nameBytes = Buffer.from(`${name}\0`);
74
+ // magic, ino, mode, uid, gid, nlink, mtime, filesize, devmajor, devminor,
75
+ // rdevmajor, rdevminor, namesize, check
76
+ const header = Buffer.from("070701" + [ino, mode, 0, 0, nlink, mtime, data.length, 0, 0, rdevMajor, rdevMinor, nameBytes.length, 0]
77
+ .map(hex).join(""), "latin1");
78
+ chunks.push(header, nameBytes);
79
+ pad(header.length + nameBytes.length);
80
+ chunks.push(data);
81
+ pad(data.length);
82
+ };
83
+
84
+ entries.forEach(({ path, stat, device }, index) => {
85
+ const ino = index + 1;
86
+ if (device) {
87
+ add(path, { ino, mode: device.mode, nlink: 1, mtime: epoch ?? now, data: Buffer.alloc(0), rdevMajor: device.major, rdevMinor: device.minor });
88
+ return;
89
+ }
90
+ const type = stat.mode & S_IFMT;
91
+ const mtime = epoch ?? Math.floor(stat.mtimeMs / 1000);
92
+ let data = Buffer.alloc(0);
93
+ let nlink = 1;
94
+ if (type === S_IFREG) data = readFileSync(join(directory, path));
95
+ else if (type === S_IFLNK) data = Buffer.from(readlinkSync(join(directory, path)));
96
+ else if (type === S_IFDIR) nlink = stat.nlink;
97
+ else fail(`${path}: unsupported file type ${type.toString(8)}; pass device nodes as PATH:c:MAJOR:MINOR:MODE`);
98
+ add(path, { ino, mode: stat.mode, nlink, mtime, data });
99
+ });
100
+ add("TRAILER!!!", { ino: 0, mode: 0, nlink: 1, mtime: 0, data: Buffer.alloc(0) });
101
+ // GNU cpio pads the archive to a 512-byte block.
102
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
103
+ if (total % 512) chunks.push(Buffer.alloc(512 - (total % 512)));
104
+
105
+ // Like `gzip -9n`: zlib leaves the name out and writes 0 as the mtime.
106
+ writeFileSync(output, gzipSync(Buffer.concat(chunks), { level: constants.Z_BEST_COMPRESSION }));
107
+ console.log(`cpio.js: ${output}: ${entries.length} entries`);
@@ -0,0 +1,194 @@
1
+ // The fetch stage, as fetch-alpine.sh, fetch-bun.sh and scripts/fetch.sh do
2
+ // it: download the pinned Alpine packages and Bun release, verify their
3
+ // SHA-256, and lay out kernel/<arch>/ and native/<arch>/.
4
+ //
5
+ // node scripts/fetch.js both parts; BUNINU_ARCH, LINUX_FLAVOR,
6
+ // REAL_MACHINE and ALPINE_MIRROR as in arch.js
7
+ //
8
+ // Unlike fetch-alpine.sh, the release's module directory is emptied first,
9
+ // so modules from an earlier --real fetch do not stay behind; and a download
10
+ // lands under its final name only once its hash matched.
11
+
12
+ import { createHash } from "node:crypto";
13
+ import {
14
+ chmodSync,
15
+ copyFileSync,
16
+ createReadStream,
17
+ createWriteStream,
18
+ existsSync,
19
+ mkdirSync,
20
+ readdirSync,
21
+ readFileSync,
22
+ renameSync,
23
+ rmSync,
24
+ statSync,
25
+ writeFileSync,
26
+ } from "node:fs";
27
+ import { spawnSync } from "node:child_process";
28
+ import { dirname, join } from "node:path";
29
+ import { Readable } from "node:stream";
30
+ import { pipeline } from "node:stream/promises";
31
+ import { gunzipSync } from "node:zlib";
32
+ import { config, rootDir, runIfMain, withUmask022 } from "./arch.js";
33
+ import { filterAliases, select, trimDependencies } from "./modules.js";
34
+
35
+ const sha256File = async (path) => {
36
+ const hash = createHash("sha256");
37
+ await pipeline(createReadStream(path), hash);
38
+ return hash.digest("hex");
39
+ };
40
+
41
+ /**
42
+ * Downloads `url` to downloadsDir/name unless a file with that SHA-256 is
43
+ * already there. The hash is both the cache key and the integrity check.
44
+ */
45
+ export const download = async (cfg, url, name, sha256) => {
46
+ const path = join(cfg.downloadsDir, name);
47
+ if (existsSync(path) && (await sha256File(path)) === sha256) {
48
+ console.log(`cached ${name}`);
49
+ return path;
50
+ }
51
+ console.log(`fetch ${name}`);
52
+ mkdirSync(cfg.downloadsDir, { recursive: true });
53
+ const response = await fetch(url, { redirect: "follow" });
54
+ if (!response.ok || !response.body) throw new Error(`${url}: HTTP ${response.status}`);
55
+ const partial = `${path}.part`;
56
+ const hash = createHash("sha256");
57
+ const hashing = new TransformStream({ transform(chunk, controller) { hash.update(chunk); controller.enqueue(chunk); } });
58
+ await pipeline(Readable.fromWeb(response.body.pipeThrough(hashing)), createWriteStream(partial));
59
+ const actual = hash.digest("hex");
60
+ if (actual !== sha256) {
61
+ rmSync(partial, { force: true });
62
+ throw new Error(`${name}: SHA-256 ${actual}, expected ${sha256}`);
63
+ }
64
+ renameSync(partial, path);
65
+ return path;
66
+ };
67
+
68
+ /**
69
+ * The regular files of a tar archive (gzip members concatenated, as an .apk
70
+ * is), by name. pax ("x") and GNU long-name ("L") headers give names longer
71
+ * than the ustar fields.
72
+ */
73
+ export const readTar = (path) => {
74
+ const data = gunzipSync(readFileSync(path));
75
+ const files = new Map();
76
+ let longName = null;
77
+ for (let offset = 0; offset + 512 <= data.length;) {
78
+ const header = data.subarray(offset, offset + 512);
79
+ if (header.every((byte) => byte === 0)) { offset += 512; continue; }
80
+ const field = (start, length) => header.toString("utf8", start, start + length).replace(/\0.*$/s, "");
81
+ const size = Number.parseInt(field(124, 12).trim() || "0", 8);
82
+ const type = String.fromCharCode(header[156] || 0x30);
83
+ const body = data.subarray(offset + 512, offset + 512 + size);
84
+ offset += 512 + Math.ceil(size / 512) * 512;
85
+ if (type === "x") {
86
+ // pax records: "LENGTH key=value\n"
87
+ for (const record of body.toString("utf8").split("\n")) {
88
+ const match = /^\d+ path=(.*)$/.exec(record);
89
+ if (match) longName = match[1];
90
+ }
91
+ continue;
92
+ }
93
+ if (type === "L") { longName = body.toString("utf8").replace(/\0.*$/s, ""); continue; }
94
+ if (type === "g") continue;
95
+ const prefix = field(345, 155);
96
+ const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
97
+ longName = null;
98
+ if (type === "0" || type === "\0") files.set(name.replace(/^\.\//, ""), body);
99
+ }
100
+ return {
101
+ get(name) {
102
+ const bytes = files.get(name);
103
+ if (!bytes) throw new Error(`${path}: no ${name}`);
104
+ return bytes;
105
+ },
106
+ };
107
+ };
108
+
109
+ const writeTo = (path, bytes) => {
110
+ mkdirSync(dirname(path), { recursive: true });
111
+ writeFileSync(path, bytes);
112
+ };
113
+
114
+ // Both run with umask 022 (see arch.js withUmask022): native/ and kernel/
115
+ // are packed with their modes.
116
+
117
+ /** fetch-alpine.sh: the kernel, its selected modules, musl's loader and the EFI stub. */
118
+ export const fetchAlpine = (cfg = config()) => withUmask022(async () => {
119
+ mkdirSync(cfg.kernelDir, { recursive: true });
120
+ mkdirSync(join(cfg.nativeDir, "lib"), { recursive: true });
121
+ const linux = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.linuxPackage}`, cfg.linuxPackage, cfg.linuxSha256));
122
+ const musl = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.muslPackage}`, cfg.muslPackage, cfg.muslSha256));
123
+ const stub = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.stubPackage}`, cfg.stubPackage, cfg.stubSha256));
124
+
125
+ // On aarch64 vmlinuz is an EFI zboot image, started by the stub like the x86_64 bzImage.
126
+ writeTo(cfg.kernelImage, linux.get(`boot/vmlinuz-${cfg.flavor}`));
127
+ writeTo(join(cfg.nativeDir, "lib", cfg.muslLoader), musl.get(`lib/${cfg.muslLoader}`));
128
+ writeTo(join(cfg.kernelDir, cfg.efiStub), stub.get(`usr/lib/systemd/boot/efi/${cfg.efiStub}`));
129
+
130
+ const release = `lib/modules/${cfg.kernelRelease}`;
131
+ const dep = linux.get(`${release}/modules.dep`).toString();
132
+ const builtin = linux.get(`${release}/modules.builtin`);
133
+ const { selected, notes } = select({ dep, builtin: builtin.toString(), packageName: cfg.linuxPackage, arch: cfg.arch, flavor: cfg.flavor, real: cfg.real });
134
+ for (const note of notes) console.error(note);
135
+ rmSync(cfg.modulesRoot, { recursive: true, force: true });
136
+ writeTo(join(cfg.modulesRoot, "modules.builtin"), builtin);
137
+ for (const path of selected) {
138
+ writeTo(join(cfg.modulesRoot, path.replace(/\.gz$/, "")), gunzipSync(linux.get(`${release}/${path}`)));
139
+ }
140
+ writeTo(join(cfg.modulesRoot, "modules.dep"), trimDependencies(dep, selected));
141
+ writeTo(join(cfg.modulesRoot, "modules.alias"), filterAliases(linux.get(`${release}/modules.alias`).toString(), selected));
142
+
143
+ chmodSync(join(cfg.nativeDir, "lib", cfg.muslLoader), 0o755);
144
+ console.log(`Fetched Alpine ${cfg.arch} linux-${cfg.flavor} kernel, ${selected.length} modules, musl, and the ${cfg.efiStub} UKI stub.`);
145
+ });
146
+
147
+ /** fetch-bun.sh: Bun from its release zip, and the GCC runtime it links against. */
148
+ export const fetchBun = (cfg = config()) => withUmask022(async () => {
149
+ for (const directory of [
150
+ cfg.downloadsDir,
151
+ join(cfg.nativeDir, "bin"),
152
+ join(cfg.nativeDir, "lib"),
153
+ join(rootDir, "initramfs/dev"),
154
+ join(rootDir, "initramfs/proc"),
155
+ join(rootDir, "initramfs/sys"),
156
+ join(rootDir, "initramfs/tmp"),
157
+ ]) {
158
+ mkdirSync(directory, { recursive: true });
159
+ }
160
+ const zip = await download(cfg, cfg.bunUrl, cfg.bunZip, cfg.bunSha256);
161
+ const libstdcpp = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.libstdcppPackage}`, cfg.libstdcppPackage, cfg.libstdcppSha256));
162
+ const libgcc = readTar(await download(cfg, `${cfg.alpineBase}/${cfg.libgccPackage}`, cfg.libgccPackage, cfg.libgccSha256));
163
+ const loader = join(cfg.nativeDir, "lib", cfg.muslLoader);
164
+ if (!existsSync(loader) || statSync(loader).size === 0) {
165
+ throw new Error("fetch stage incomplete: run fetchAlpine first (node scripts/fetch.js runs both)");
166
+ }
167
+ // initramfs/bin/unzip: the same reader the guest has.
168
+ const unzip = spawnSync(process.execPath, [
169
+ join(rootDir, "initramfs/bin/unzip"),
170
+ "-p",
171
+ zip,
172
+ `${cfg.bunBuild}/bun`,
173
+ ], { maxBuffer: 1 << 30 });
174
+ if (unzip.status !== 0) throw new Error(`unzip ${zip}: ${unzip.stderr}`);
175
+ writeTo(join(cfg.nativeDir, "bin", "bun"), unzip.stdout);
176
+ writeTo(join(cfg.nativeDir, "lib", "libstdc++.so.6"), libstdcpp.get("usr/lib/libstdc++.so.6.0.34"));
177
+ writeTo(join(cfg.nativeDir, "lib", "libgcc_s.so.1"), libgcc.get("usr/lib/libgcc_s.so.1"));
178
+ // libc as a distinct inode from the active dynamic loader: dlopen on the
179
+ // loader inode itself deadlocks, while this physical copy is FFI-safe.
180
+ copyFileSync(loader, join(cfg.nativeDir, "lib", cfg.muslLibc));
181
+ chmodSync(join(rootDir, "initramfs/init.js"), 0o755);
182
+ chmodSync(join(cfg.nativeDir, "bin", "bun"), 0o755);
183
+ for (const name of readdirSync(join(cfg.nativeDir, "lib"))) {
184
+ if (/\.so/.test(name)) chmodSync(join(cfg.nativeDir, "lib", name), 0o755);
185
+ }
186
+ console.log(`Fetched Bun ${cfg.bunVersion} and its ${cfg.arch} musl runtime into ${cfg.nativeDir}/.`);
187
+ });
188
+
189
+ export const fetchAll = async (cfg = config()) => {
190
+ await fetchAlpine(cfg);
191
+ await fetchBun(cfg);
192
+ };
193
+
194
+ runIfMain(import.meta.url, () => fetchAll());
@@ -0,0 +1,158 @@
1
+ // The kernel modules the initramfs ships, as fetch-alpine.sh picks them: each
2
+ // requested by name, with what the package's modules.dep says it needs. The
3
+ // image gets a modules.dep trimmed to the shipped set, the fs-* lines and
4
+ // the device ids of the shipped modules from modules.alias, and the
5
+ // package's modules.builtin; /lib/modprobe.js reads those three files.
6
+ //
7
+ // fetch.js uses select(), trimDependencies() and filterAliases().
8
+
9
+ // Network: virtio-net for QEMU (init.js loads these at boot). virtio_pci is
10
+ // the PCI transport for every virtio device; linux-virt builds it in,
11
+ // linux-lts has it as a module that nothing in modules.dep pulls in.
12
+ // Storage: what /bin/mount loads on demand. Pointing devices for
13
+ // `bunterm --mouse`. The framebuffer for bunterm on lts. Sound for QEMU's and
14
+ // PCs' usual devices. --real adds the USB keyboard, PC storage and ACPI
15
+ // drivers, and the wired and USB network drivers.
16
+ const required = (names) => names.map((name) => ({ name }));
17
+ const optional = (names) => names.map((name) => ({ name, optional: true }));
18
+
19
+ export const requests = ({ arch, flavor, real }) => [
20
+ ...required([
21
+ "failover",
22
+ "net_failover",
23
+ "virtio_net",
24
+ ]),
25
+ ...optional([
26
+ "virtio_pci",
27
+ ]),
28
+ ...required([
29
+ "virtio_blk",
30
+ "ext4",
31
+ "vfat",
32
+ "exfat",
33
+ "ntfs3",
34
+ "nls_utf8",
35
+ "nls_cp437",
36
+ "nls_iso8859-1",
37
+ ]),
38
+ ...optional([
39
+ "ext2",
40
+ ]),
41
+ ...required([
42
+ "evdev",
43
+ arch === "x86_64" ? "psmouse" : "virtio_input",
44
+ ]),
45
+ ...(flavor === "lts" ? optional([
46
+ "simpledrm",
47
+ ]) : []),
48
+ ...optional([
49
+ "snd-ens1371",
50
+ "snd-hda-intel",
51
+ "snd-hda-codec-generic",
52
+ "snd-hda-codec-hdmi",
53
+ "snd-usb-audio",
54
+ ]),
55
+ ...(real ? required([
56
+ // USB keyboard
57
+ "usb-common",
58
+ "usbcore",
59
+ "xhci-hcd",
60
+ "xhci-pci",
61
+ "xhci-pci-renesas",
62
+ "hid",
63
+ "hid-generic",
64
+ "usbhid",
65
+ // SATA/AHCI, PATA, NVMe (with Intel VMD), USB mass storage and UAS
66
+ "sd_mod",
67
+ "ahci",
68
+ "ata_generic",
69
+ "pata_acpi",
70
+ "nvme",
71
+ "vmd",
72
+ "usb-storage",
73
+ "uas",
74
+ // ACPI: battery, AC adapter, power button, thermal zones
75
+ "battery",
76
+ "ac",
77
+ "button",
78
+ "thermal",
79
+ // Wired NICs: Intel, Realtek (with its PHY), Qualcomm Atheros, Broadcom
80
+ "e1000",
81
+ "e1000e",
82
+ "igb",
83
+ "igc",
84
+ "r8169",
85
+ "realtek",
86
+ "alx",
87
+ "tg3",
88
+ // USB NICs and Android tethering (RNDIS, CDC ECM/NCM/EEM, subset, zaurus)
89
+ "r8152",
90
+ "ax88179_178a",
91
+ "cdc_ether",
92
+ "rndis_host",
93
+ "cdc_ncm",
94
+ "cdc_eem",
95
+ "cdc_subset",
96
+ "zaurus",
97
+ ]) : []),
98
+ ];
99
+
100
+ /**
101
+ * The modules.dep paths (".ko.gz", relative to the release directory) to
102
+ * ship, dependencies after the module that pulled them in, and notes about
103
+ * optional modules the kernel neither builds in nor packages.
104
+ */
105
+ export const select = ({ dep, builtin, packageName, ...target }) => {
106
+ const lines = dep.split("\n");
107
+ const builtinLines = builtin.split("\n");
108
+ const selected = [];
109
+ const seen = new Set();
110
+ const notes = [];
111
+ const find = (name) => lines.find((line) => line.includes(`/${name}.ko.gz:`));
112
+ const add = (name) => {
113
+ // An explicit stack instead of recursion; the order matches the shell's
114
+ // depth-first walk.
115
+ const stack = [name];
116
+ while (stack.length) {
117
+ const current = stack.pop();
118
+ const line = find(current);
119
+ if (!line) throw new Error(`module ${current} is not in ${packageName}`);
120
+ const path = line.slice(0, line.indexOf(":"));
121
+ if (seen.has(path)) continue;
122
+ seen.add(path);
123
+ selected.push(path);
124
+ const dependencies = line.slice(line.indexOf(":") + 1).trim().split(/\s+/).filter(Boolean)
125
+ .map((dependency) => dependency.slice(dependency.lastIndexOf("/") + 1).replace(/\.ko\.gz$/, ""));
126
+ stack.push(...dependencies.reverse());
127
+ }
128
+ };
129
+ for (const { name, optional } of requests(target)) {
130
+ if (!optional || find(name)) add(name);
131
+ else if (!builtinLines.some((line) => line.endsWith(`/${name}.ko`))) {
132
+ notes.push(`note: module ${name} is not in ${target.arch} ${packageName}; skipped`);
133
+ }
134
+ }
135
+ return { selected, notes };
136
+ };
137
+
138
+ /** modules.dep with only the shipped modules' lines, .ko.gz renamed .ko. */
139
+ export const trimDependencies = (dep, selected) => {
140
+ const wanted = new Set(selected);
141
+ return dep.split("\n")
142
+ .filter((line) => line.includes(":") && wanted.has(line.slice(0, line.indexOf(":"))))
143
+ .map((line) => `${line.replaceAll(".ko.gz", ".ko")}\n`)
144
+ .join("");
145
+ };
146
+
147
+ /** modules.alias: the fs-* names, and the pci:/usb:/virtio: ids of shipped modules. */
148
+ export const filterAliases = (alias, selected) => {
149
+ const shipped = new Set(selected.map((path) => path.slice(path.lastIndexOf("/") + 1).replace(/\.ko\.gz$/, "").replaceAll("-", "_")));
150
+ return alias.split("\n")
151
+ .filter((line) => {
152
+ const fields = line.trim().split(/\s+/);
153
+ if (/^fs-/.test(fields[1] ?? "")) return true;
154
+ return /^(pci|usb|virtio):/.test(fields[1] ?? "") && shipped.has((fields[2] ?? "").replaceAll("-", "_"));
155
+ })
156
+ .map((line) => `${line}\n`)
157
+ .join("");
158
+ };
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
+ # What this script creates is packed with its modes: the same umask as
4
+ # PRoot and most hosts, not native Termux's 0077.
5
+ umask 022
3
6
 
4
7
  cd "$(dirname "$0")/.."
5
8
 
@@ -22,6 +25,12 @@ trap 'rm -rf "$staging"; rm -f "$archive"' EXIT
22
25
  cp -a --reflink=auto initramfs/. "$staging/"
23
26
  rm -rf "$staging/bin/bun" "$staging/lib/modules"
24
27
  cp -a --reflink=auto "$native_dir/." "$staging/"
28
+ # Only this kernel's modules: native/<arch>/ keeps every release fetched so
29
+ # far (an older pin, the other flavor), and none of those can load.
30
+ for release_dir in "$staging/lib/modules"/*; do
31
+ [ -e "$release_dir" ] || continue
32
+ [ "${release_dir##*/}" = "$kernel_release" ] || rm -rf "$release_dir"
33
+ done
25
34
  # Keep the project README canonical while making it available to
26
35
  # buninu-linux-help from the conventional installed-document path.
27
36
  cp README.md "$staging/usr/share/doc/buninu-linux/README.md"
@@ -50,6 +59,30 @@ mkdir -p "$staging/usr/share/icons/hicolor/512x512/apps" "$staging/usr/share/pix
50
59
  ln -sfn ../../../../../../buninu/icon.png "$staging/usr/share/icons/hicolor/512x512/apps/buninu-linux.png"
51
60
  ln -sfn ../../../buninu/icon.png "$staging/usr/share/pixmaps/buninu-linux.png"
52
61
 
62
+ # The modes git would check out with umask 022: directories 755, files 755
63
+ # when their owner may execute them and 644 otherwise (symlinks untouched).
64
+ # A checkout made under another umask (native Termux's 0077 gives 600/700)
65
+ # then packs the same archive.
66
+ find "$staging" -type d -exec chmod 755 {} +
67
+ find "$staging" -type f -perm -100 -exec chmod 755 {} +
68
+ find "$staging" -type f ! -perm -100 -exec chmod 644 {} +
69
+
70
+ # BUNINU_JS_CPIO=1 writes the archive with scripts/cpio.js (bun or node)
71
+ # instead of fakeroot, cpio and gzip: the device nodes below exist only in
72
+ # the archive, so nothing needs root.
73
+ if [ "${BUNINU_JS_CPIO:-}" = 1 ]; then
74
+ js=$(command -v bun || command -v node) || {
75
+ echo "error: BUNINU_JS_CPIO=1 needs bun or node on PATH" >&2
76
+ exit 1
77
+ }
78
+ "$js" scripts/cpio.js "$staging" build/initramfs.cpio.gz \
79
+ dev/console:c:5:1:0600 "dev/$serial_console:c:$serial_major:64:0620" \
80
+ dev/null:c:1:3:0666 dev/zero:c:1:5:0666 dev/random:c:1:8:0666 \
81
+ dev/urandom:c:1:9:0666 dev/tty:c:5:0:0666
82
+ echo "Built build/initramfs.cpio.gz for $arch (scripts/cpio.js)"
83
+ exit 0
84
+ fi
85
+
53
86
  # fakeroot lets cpio record the character devices without real root. Nothing
54
87
  # has mounted devtmpfs when the kernel execs Bun as PID 1, so every device
55
88
  # Bun's startup needs has to be in the archive itself: