buninu-linux 0.7.4 → 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,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,14 @@ 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
+
53
70
  # BUNINU_JS_CPIO=1 writes the archive with scripts/cpio.js (bun or node)
54
71
  # instead of fakeroot, cpio and gzip: the device nodes below exist only in
55
72
  # the archive, so nothing needs root.
@@ -0,0 +1,184 @@
1
+ // The initramfs, as scripts/pack-initramfs.sh packs it with BUNINU_JS_CPIO=1:
2
+ // initramfs/ with native/<arch>/ laid over it, the README, licenses and
3
+ // os-release added, written by scripts/cpio.js to build/initramfs.cpio.gz.
4
+ //
5
+ // node scripts/pack.js BUNINU_ARCH as in arch.js; SOURCE_DATE_EPOCH
6
+ // fixes every mtime, as cpio.js does
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import {
10
+ chmodSync,
11
+ copyFileSync,
12
+ existsSync,
13
+ lstatSync,
14
+ mkdirSync,
15
+ mkdtempSync,
16
+ readFileSync,
17
+ readdirSync,
18
+ readlinkSync,
19
+ rmSync,
20
+ statSync,
21
+ symlinkSync,
22
+ utimesSync,
23
+ lutimesSync,
24
+ writeFileSync,
25
+ } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { config, jsRuntime, rootDir, runIfMain } from "./arch.js";
28
+
29
+ /**
30
+ * `cp -a SOURCE/. TARGET/`: modes, times and symlinks kept, directories
31
+ * merged into ones already there (and given the source's mode, as cp -a does).
32
+ */
33
+ export const copyTree = (source, target) => {
34
+ const stat = lstatSync(source);
35
+ if (stat.isSymbolicLink()) {
36
+ rmSync(target, { force: true });
37
+ symlinkSync(readlinkSync(source), target);
38
+ lutimesSync(target, stat.atime, stat.mtime);
39
+ } else if (stat.isDirectory()) {
40
+ mkdirSync(target, { recursive: true });
41
+ for (const name of readdirSync(source)) copyTree(join(source, name), join(target, name));
42
+ chmodSync(target, stat.mode & 0o7777);
43
+ utimesSync(target, stat.atime, stat.mtime);
44
+ } else if (stat.isFile()) {
45
+ rmSync(target, { force: true });
46
+ copyFileSync(source, target);
47
+ chmodSync(target, stat.mode & 0o7777);
48
+ utimesSync(target, stat.atime, stat.mtime);
49
+ } else {
50
+ throw new Error(`${source}: not a file, directory or symlink`);
51
+ }
52
+ };
53
+
54
+ /**
55
+ * The modes git would check out with umask 022: directories 755, files 755
56
+ * when their owner may execute them and 644 otherwise; symlinks untouched.
57
+ * A checkout made under another umask (native Termux's 0077 gives 600/700)
58
+ * then packs the same archive.
59
+ */
60
+ export const normalizeModes = (path) => {
61
+ const stat = lstatSync(path);
62
+ if (stat.isSymbolicLink()) return;
63
+ if (stat.isDirectory()) {
64
+ chmodSync(path, 0o755);
65
+ for (const name of readdirSync(path)) normalizeModes(join(path, name));
66
+ } else if (stat.isFile()) {
67
+ chmodSync(path, stat.mode & 0o100 ? 0o755 : 0o644);
68
+ }
69
+ };
70
+
71
+ /** `cp FILE TARGET`: contents and mode, a new mtime. */
72
+ const copyFile = (source, target) => {
73
+ rmSync(target, { force: true });
74
+ copyFileSync(source, target);
75
+ chmodSync(target, statSync(source).mode & 0o777 & ~process.umask());
76
+ };
77
+
78
+ /** `ln -sfn TARGET LINK` */
79
+ const link = (target, path) => {
80
+ rmSync(path, { force: true });
81
+ symlinkSync(target, path);
82
+ };
83
+
84
+ /**
85
+ * The device nodes of the archive, PATH:c:MAJOR:MINOR:MODE. Nothing has
86
+ * mounted devtmpfs when the kernel execs Bun as PID 1: console is init's fd
87
+ * 0/1/2 and JSC aborts without urandom; the rest keep /dev usable if the
88
+ * devtmpfs mount in init.js fails. The serial port is ttyS0 (4, 64) on
89
+ * x86_64 and the PL011 ttyAMA0 (204, 64) on aarch64.
90
+ */
91
+ export const devices = (cfg) => [
92
+ "dev/console:c:5:1:0600",
93
+ `dev/${cfg.serialConsole}:c:${cfg.serialMajor}:64:0620`,
94
+ "dev/null:c:1:3:0666",
95
+ "dev/zero:c:1:5:0666",
96
+ "dev/random:c:1:8:0666",
97
+ "dev/urandom:c:1:9:0666",
98
+ "dev/tty:c:5:0:0666",
99
+ ];
100
+
101
+ export const osRelease = (version) => [
102
+ 'NAME="Buninu Linux"',
103
+ "ID=buninu-linux",
104
+ `VERSION="${version}"`,
105
+ `VERSION_ID=${version}`,
106
+ `PRETTY_NAME="Buninu Linux ${version}"`,
107
+ 'HOME_URL="https://buninu.org"',
108
+ 'BUG_REPORT_URL="https://github.com/jjtseng93/buninu-linux/issues"',
109
+ "LOGO=buninu-linux",
110
+ "DEFAULT_HOSTNAME=buninu",
111
+ ].map((line) => `${line}\n`).join("");
112
+
113
+ export const pack = (cfg = config()) => {
114
+ // The directories and files made here are packed with their modes; native
115
+ // Termux's umask 0077 would make /usr/lib and /usr/lib/os-release private.
116
+ const previousUmask = process.umask(0o022);
117
+ try {
118
+ packStaging(cfg);
119
+ } finally {
120
+ process.umask(previousUmask);
121
+ }
122
+ };
123
+
124
+ const packStaging = (cfg) => {
125
+ const bun = join(cfg.nativeDir, "bin", "bun");
126
+ if (!existsSync(bun) || !(statSync(bun).mode & 0o111)) {
127
+ throw new Error(`no ${bun} yet; run the fetch stage first (node scripts/fetch.js)`);
128
+ }
129
+ const build = join(rootDir, "build");
130
+ mkdirSync(build, { recursive: true });
131
+ const staging = mkdtempSync(join(rootDir, ".initramfs."));
132
+ try {
133
+ // initramfs/ is the same for every architecture; native/<arch>/ adds bun,
134
+ // the musl loader, the GCC runtime and the kernel modules on top of it.
135
+ // A checkout fetched before native/ existed still has an x86_64 bun and
136
+ // its modules under initramfs/; they are dropped rather than packed.
137
+ copyTree(join(rootDir, "initramfs"), staging);
138
+ rmSync(join(staging, "bin", "bun"), { force: true });
139
+ rmSync(join(staging, "lib", "modules"), { recursive: true, force: true });
140
+ copyTree(cfg.nativeDir, staging);
141
+ // Only this kernel's modules: native/<arch>/ keeps every release fetched
142
+ // so far (an older pin, the other flavor), and none of those can load.
143
+ const modules = join(staging, "lib", "modules");
144
+ for (const release of existsSync(modules) ? readdirSync(modules) : []) {
145
+ if (release !== cfg.kernelRelease) rmSync(join(modules, release), { recursive: true, force: true });
146
+ }
147
+ copyFile(join(rootDir, "README.md"), join(staging, "usr/share/doc/buninu-linux/README.md"));
148
+ for (const name of [
149
+ "LICENSE",
150
+ "NOTICE.md",
151
+ "LICENSES",
152
+ ]) copyTree(join(rootDir, name), join(staging, "usr/share/licenses", name));
153
+
154
+ // os-release(5): the canonical file in /usr/lib, /etc/os-release a link
155
+ // to it; build.js embeds the same file as the UKI's .osrel section.
156
+ const { version } = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
157
+ if (!version) throw new Error("no version in package.json");
158
+ writeFileSync(join(build, "os-release"), osRelease(version));
159
+ mkdirSync(join(staging, "usr/lib"), { recursive: true });
160
+ mkdirSync(join(staging, "etc"), { recursive: true });
161
+ copyFile(join(build, "os-release"), join(staging, "usr/lib/os-release"));
162
+ link("../usr/lib/os-release", join(staging, "etc/os-release"));
163
+ // LOGO names an icon-theme icon; the standard lookup paths point at the
164
+ // icon /buninu already ships.
165
+ mkdirSync(join(staging, "usr/share/icons/hicolor/512x512/apps"), { recursive: true });
166
+ mkdirSync(join(staging, "usr/share/pixmaps"), { recursive: true });
167
+ link("../../../../../../buninu/icon.png", join(staging, "usr/share/icons/hicolor/512x512/apps/buninu-linux.png"));
168
+ link("../../../buninu/icon.png", join(staging, "usr/share/pixmaps/buninu-linux.png"));
169
+
170
+ normalizeModes(staging);
171
+ const cpio = spawnSync(jsRuntime(), [
172
+ join(rootDir, "scripts/cpio.js"),
173
+ staging,
174
+ join(build, "initramfs.cpio.gz"),
175
+ ...devices(cfg),
176
+ ], { stdio: ["ignore", "ignore", "inherit"] });
177
+ if (cpio.status !== 0) throw new Error("scripts/cpio.js failed");
178
+ } finally {
179
+ rmSync(staging, { recursive: true, force: true });
180
+ }
181
+ console.log(`Built build/initramfs.cpio.gz for ${cfg.arch}`);
182
+ };
183
+
184
+ runIfMain(import.meta.url, () => pack());
package/scripts/uki.js CHANGED
File without changes