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,115 @@
1
+ // /lib/gzip.js — gzip, gunzip and zcat over node:zlib. /bin/gzip, /bin/gunzip
2
+ // and /bin/zcat call run() with their own defaults, since a symlink would
3
+ // reach Bun under the target's name.
4
+ //
5
+ // Supported: -c/--stdout, -d/--decompress, -k/--keep, -f/--force, -n/--no-name,
6
+ // -N/--name (accepted; no name is ever stored), -q/--quiet, -t/--test,
7
+ // -v/--verbose, -1 ... -9, --fast, --best; FILE operands or "-" for stdin.
8
+ // Concatenated members decompress as one stream, as with GNU gzip.
9
+
10
+ import { existsSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync, writeSync } from "node:fs";
11
+ import { isatty } from "node:tty";
12
+ import { constants, gunzipSync, gzipSync } from "node:zlib";
13
+
14
+ const usage = (name) => `Usage: ${name} [-cdfkntqv1-9] [FILE...]
15
+ -c, --stdout write to standard output, keep the input files
16
+ -d, --decompress decompress
17
+ -k, --keep keep the input files
18
+ -f, --force overwrite output files; compress to a terminal
19
+ -n, --no-name (always the case) store no file name or time
20
+ -t, --test test the compressed files
21
+ -q, --quiet no warnings
22
+ -v, --verbose report each file
23
+ -1 ... -9 fast ... best compression (default 6)
24
+ With no FILE, or when FILE is -, read standard input.`;
25
+
26
+ const writeAll = (fd, bytes) => {
27
+ for (let offset = 0; offset < bytes.length;) offset += writeSync(fd, bytes, offset, bytes.length - offset);
28
+ };
29
+
30
+ const readStdin = () => {
31
+ try {
32
+ return readFileSync(0);
33
+ } catch (error) {
34
+ if (error.code === "EAGAIN") return readFileSync("/dev/stdin");
35
+ throw error;
36
+ }
37
+ };
38
+
39
+ export const run = (name, argv, defaults = {}) => {
40
+ const options = { decompress: false, stdout: false, keep: false, force: false, test: false, quiet: false, verbose: false, level: 6, ...defaults };
41
+ const files = [];
42
+ let endOfOptions = false;
43
+ for (const argument of argv) {
44
+ if (endOfOptions || argument === "-" || !argument.startsWith("-")) { files.push(argument); continue; }
45
+ if (argument === "--") { endOfOptions = true; continue; }
46
+ const long = {
47
+ "--stdout": "c", "--to-stdout": "c", "--decompress": "d", "--uncompress": "d", "--keep": "k", "--force": "f",
48
+ "--no-name": "n", "--name": "N", "--test": "t", "--quiet": "q", "--verbose": "v", "--fast": "1", "--best": "9",
49
+ }[argument];
50
+ if (argument === "-h" || argument === "--help") { console.log(usage(name)); return 0; }
51
+ const letters = long ?? (argument.startsWith("--") ? null : argument.slice(1));
52
+ if (letters === null) { console.error(`${name}: unknown option ${argument}\n${usage(name)}`); return 1; }
53
+ for (const letter of letters) {
54
+ if (letter === "c") options.stdout = true;
55
+ else if (letter === "d") options.decompress = true;
56
+ else if (letter === "k") options.keep = true;
57
+ else if (letter === "f") options.force = true;
58
+ else if (letter === "t") options.test = options.decompress = true;
59
+ else if (letter === "q") options.quiet = true;
60
+ else if (letter === "v") options.verbose = true;
61
+ else if (letter === "n" || letter === "N") {}
62
+ else if (/[1-9]/.test(letter)) options.level = Number(letter);
63
+ else { console.error(`${name}: unknown option -${letter}\n${usage(name)}`); return 1; }
64
+ }
65
+ }
66
+ if (files.length === 0) files.push("-");
67
+
68
+ const transform = (bytes) => options.decompress
69
+ ? gunzipSync(bytes, { finishFlush: constants.Z_SYNC_FLUSH })
70
+ : gzipSync(bytes, { level: options.level });
71
+
72
+ let status = 0;
73
+ for (const file of files) {
74
+ const label = file === "-" ? "stdin" : file;
75
+ try {
76
+ if (file === "-" || options.stdout || options.test) {
77
+ if (!options.decompress && !options.force && isatty(1) && !options.test) {
78
+ console.error(`${name}: compressed data not written to a terminal. Use -f to force compression.`);
79
+ return 1;
80
+ }
81
+ let input;
82
+ if (file === "-") input = readStdin();
83
+ else {
84
+ if (statSync(file).isDirectory()) throw new Error("is a directory");
85
+ input = readFileSync(file);
86
+ }
87
+ const output = transform(input);
88
+ if (!options.test) writeAll(1, output);
89
+ if (options.verbose) console.error(`${label}:\tOK`);
90
+ continue;
91
+ }
92
+ // In place: FILE -> FILE.gz, or FILE.gz -> FILE.
93
+ let target;
94
+ if (options.decompress) {
95
+ const match = /^(.*?)(\.gz|-gz|\.z|_z|\.tgz)$/i.exec(file);
96
+ if (!match) { if (!options.quiet) console.error(`${name}: ${file}: unknown suffix -- ignored`); status = status || 2; continue; }
97
+ target = match[1] + (match[2].toLowerCase() === ".tgz" ? ".tar" : "");
98
+ } else {
99
+ if (/\.gz$/i.test(file) && !options.force) { if (!options.quiet) console.error(`${name}: ${file} already has .gz suffix -- unchanged`); status = status || 2; continue; }
100
+ target = `${file}.gz`;
101
+ }
102
+ const stat = statSync(file);
103
+ if (stat.isDirectory()) throw new Error("is a directory -- ignored");
104
+ if (existsSync(target) && !options.force) throw new Error(`${target} already exists`);
105
+ writeFileSync(target, transform(readFileSync(file)), { mode: stat.mode & 0o7777 });
106
+ utimesSync(target, stat.atime, stat.mtime);
107
+ if (!options.keep) rmSync(file);
108
+ if (options.verbose) console.error(`${file}:\t-> ${target}`);
109
+ } catch (error) {
110
+ console.error(`${name}: ${label}: ${error.message}`);
111
+ status = 1;
112
+ }
113
+ }
114
+ return status;
115
+ };
@@ -0,0 +1,158 @@
1
+ // /lib/mtools.js — mmd, mcopy and mdir over /lib/fat.js, with mtools'
2
+ // "-i IMAGE" and "::/path" conventions. /bin/mmd, /bin/mcopy and /bin/mdir
3
+ // call these; IMAGE may be "file@@offset".
4
+
5
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
+ import { basename, join } from "node:path";
7
+ import { ATTR, FatError, now, openVolume } from "./fat.js";
8
+
9
+ /** Splits "-i IMAGE" and the option letters from the operands. */
10
+ const parse = (name, argv, letters, withValue = []) => {
11
+ const options = { image: null, flags: new Set(), values: {} };
12
+ const operands = [];
13
+ for (let index = 0; index < argv.length; index++) {
14
+ const argument = argv[index];
15
+ if (argument === "-i") {
16
+ if (index + 1 >= argv.length) throw new FatError("-i requires an image");
17
+ options.image = argv[++index];
18
+ } else if (argument.startsWith("-i") && argument.length > 2) options.image = argument.slice(2);
19
+ else if (withValue.includes(argument)) {
20
+ if (index + 1 >= argv.length) throw new FatError(`${argument} requires an argument`);
21
+ options.values[argument] = argv[++index];
22
+ } else if (argument.startsWith("-") && argument.length > 1) {
23
+ for (const letter of argument.slice(1)) {
24
+ if (!letters.includes(letter)) throw new FatError(`unknown option -${letter}`);
25
+ options.flags.add(letter);
26
+ }
27
+ } else operands.push(argument);
28
+ }
29
+ if (!options.image) throw new FatError(`${name} needs -i IMAGE`);
30
+ return { options, operands };
31
+ };
32
+
33
+ const isImagePath = (operand) => operand.startsWith("::");
34
+ const imagePath = (operand) => {
35
+ if (!isImagePath(operand)) throw new FatError(`${operand}: expected ::/path inside the image`);
36
+ return `/${operand.slice(2).replace(/^\/+/, "")}`;
37
+ };
38
+
39
+ const guard = (name, action) => {
40
+ try {
41
+ return action();
42
+ } catch (error) {
43
+ if (!(error instanceof FatError)) throw error;
44
+ console.error(`${name}: ${error.message}`);
45
+ return 1;
46
+ }
47
+ };
48
+
49
+ export const mmd = (argv) => guard("mmd", () => {
50
+ const { options, operands } = parse("mmd", argv, "vQ", ["-D"]);
51
+ if (!operands.length) throw new FatError("usage: mmd -i IMAGE ::/DIR...");
52
+ const volume = openVolume(options.image);
53
+ let status = 0;
54
+ try {
55
+ for (const operand of operands) {
56
+ try { volume.mkdir(imagePath(operand)); }
57
+ catch (error) { if (!(error instanceof FatError)) throw error; console.error(`mmd: ${error.message}`); status = 1; }
58
+ }
59
+ } finally {
60
+ volume.close();
61
+ }
62
+ return status;
63
+ });
64
+
65
+ /**
66
+ * mcopy: local files into the image (TARGET ::/path) or image files out of
67
+ * it (SOURCES ::/path). -o / -D o overwrites in the image, -n overwrites
68
+ * local files without asking, -m keeps the source's modification time.
69
+ */
70
+ export const mcopy = (argv) => guard("mcopy", () => {
71
+ const { options, operands } = parse("mcopy", argv, "onmvQpbt", ["-D"]);
72
+ if (operands.length < 2) throw new FatError("usage: mcopy -i IMAGE SOURCE... TARGET");
73
+ const target = operands.at(-1);
74
+ const sources = operands.slice(0, -1);
75
+ const overwrite = options.flags.has("o") || options.values["-D"] === "o";
76
+ let status = 0;
77
+ if (isImagePath(target)) {
78
+ if (sources.some(isImagePath)) throw new FatError("copying within the image is not supported");
79
+ const volume = openVolume(options.image);
80
+ try {
81
+ const destination = imagePath(target);
82
+ const existing = volume.lookup(destination);
83
+ const intoDirectory = (existing && existing.attributes & ATTR.DIRECTORY) || target.endsWith("/");
84
+ if (sources.length > 1 && !intoDirectory) throw new FatError(`${target}: not a directory`);
85
+ for (const source of sources) {
86
+ try {
87
+ const stat = statSync(source);
88
+ if (stat.isDirectory()) throw new FatError(`${source}: is a directory (-s is not supported)`);
89
+ const path = intoDirectory ? `${destination.replace(/\/+$/, "")}/${basename(source)}` : destination;
90
+ const date = options.flags.has("m") ? stat.mtime : now().date;
91
+ volume.writeFile(path, readFileSync(source), { date, overwrite });
92
+ if (options.flags.has("v")) console.error(`Copying ${basename(source)}`);
93
+ } catch (error) {
94
+ if (!(error instanceof FatError) && !error.code) throw error;
95
+ console.error(`mcopy: ${error.message}`);
96
+ status = 1;
97
+ }
98
+ }
99
+ } finally {
100
+ volume.close();
101
+ }
102
+ return status;
103
+ }
104
+ if (!sources.every(isImagePath)) throw new FatError("either TARGET or every SOURCE must be ::/path");
105
+ const volume = openVolume(options.image, false);
106
+ try {
107
+ const intoDirectory = existsSync(target) && statSync(target).isDirectory();
108
+ if (sources.length > 1 && !intoDirectory) throw new FatError(`${target}: not a directory`);
109
+ for (const source of sources) {
110
+ try {
111
+ const path = imagePath(source);
112
+ const output = target === "-" ? null : intoDirectory ? join(target, basename(path)) : target;
113
+ if (output && existsSync(output) && !options.flags.has("n") && !overwrite) throw new FatError(`${output}: file exists (use -n to overwrite)`);
114
+ const bytes = volume.readFile(path);
115
+ if (output === null) process.stdout.write(bytes); else writeFileSync(output, bytes);
116
+ } catch (error) {
117
+ if (!(error instanceof FatError) && !error.code) throw error;
118
+ console.error(`mcopy: ${error.message}`);
119
+ status = 1;
120
+ }
121
+ }
122
+ } finally {
123
+ volume.close();
124
+ }
125
+ return status;
126
+ });
127
+
128
+ const pad = (value) => String(value).padStart(2, "0");
129
+ const showDate = (date, time) => `${(date >> 9) + 1980}-${pad((date >> 5) & 15)}-${pad(date & 31)} ${pad(time >> 11)}:${pad((time >> 5) & 63)}`;
130
+
131
+ export const mdir = (argv) => guard("mdir", () => {
132
+ const { options, operands } = parse("mdir", argv, "bawf");
133
+ const volume = openVolume(options.image, false);
134
+ try {
135
+ for (const operand of operands.length ? operands : ["::/"]) {
136
+ const path = imagePath(operand);
137
+ const entries = volume.list(path).filter((entry) => options.flags.has("a") || !(entry.attributes & ATTR.HIDDEN));
138
+ if (options.flags.has("b")) {
139
+ for (const entry of entries) if (!(entry.attributes & ATTR.VOLUME) && entry.name !== "." && entry.name !== "..") console.log(`::${path.replace(/\/$/, "")}/${entry.name}`);
140
+ continue;
141
+ }
142
+ console.log(`Directory for ::${path}\n`);
143
+ let files = 0;
144
+ let bytes = 0;
145
+ for (const entry of entries) {
146
+ if (entry.attributes & ATTR.VOLUME) continue;
147
+ const size = entry.attributes & ATTR.DIRECTORY ? "<DIR>".padEnd(10) : String(entry.size).padStart(10);
148
+ console.log(`${entry.name.padEnd(24)} ${size} ${showDate(entry.date, entry.time)}`);
149
+ files++;
150
+ bytes += entry.size;
151
+ }
152
+ console.log(`${String(files).padStart(8)} file${files === 1 ? "" : "s"} ${String(bytes).padStart(16)} bytes`);
153
+ }
154
+ } finally {
155
+ volume.close();
156
+ }
157
+ return 0;
158
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -0,0 +1,156 @@
1
+ // Everything that differs between the two guest architectures and the two
2
+ // kernel flavors, as scripts/arch.sh has it: the pinned packages and their
3
+ // SHA-256, the file names inside them, the EFI names and the serial console.
4
+ // fetch.js, pack.js and build.js take the object config() returns.
5
+ //
6
+ // config({ arch, flavor, real, mirror }) arguments override
7
+ // BUNINU_ARCH, LINUX_FLAVOR, REAL_MACHINE=1 and ALPINE_MIRROR
8
+ //
9
+ // Paths are absolute, under the checkout. Run directly, it prints the config.
10
+
11
+ import { accessSync, constants } from "node:fs";
12
+ import { delimiter, dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ export const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
16
+
17
+ const pins = {
18
+ x86_64: {
19
+ linuxVirtSha256: "57a522d2b6e9d1b6c9de6b9f7fc42aa4b8d33ebf9f2b42b4251dbee6ff2d9839",
20
+ linuxLtsSha256: "1d02a1b74a8a09c476a3c01782c0b8613ae2935988e65d4d31e6bef44dfb8b64",
21
+ muslSha256: "573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71",
22
+ stubSha256: "8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9",
23
+ libstdcppSha256: "14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6",
24
+ libgccSha256: "393dcd32629f06d7d85409c272d142d0c082772d10b87ef55ee82f47de3be637",
25
+ // Bun ships one x64 musl build; -baseline is an alias (NOTICE.md).
26
+ bunBuild: "bun-linux-x64-musl-baseline",
27
+ bunSha256: "76e1db84e98f22f78de0a87e309bfbbf297732847f9720db36750646c85c8c18",
28
+ efiStub: "linuxx64.efi.stub",
29
+ efiBootName: "BOOTX64.EFI",
30
+ // COM1 on q35 and on PCs.
31
+ serialConsole: "ttyS0",
32
+ serialMajor: 4,
33
+ },
34
+ aarch64: {
35
+ linuxVirtSha256: "9a4a6fa042b60b70f453dded99762810516b35793c64d776d75986b0b110b6e0",
36
+ linuxLtsSha256: "f945ddcc306546c7c22cf10249f637fc6e806ecc40286fb50950558dd573e7e9",
37
+ muslSha256: "5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e",
38
+ stubSha256: "a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38",
39
+ libstdcppSha256: "2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7",
40
+ libgccSha256: "369aaa6e9d099a737bad6dd3e6c2fe7bb1547ca26d22b94ee0411228f709b403",
41
+ bunBuild: "bun-linux-aarch64-musl",
42
+ bunSha256: "71760b6c8ea30623b81a4907cb815d48e2ea266f2e73e751534a44a0607950df",
43
+ efiStub: "linuxaa64.efi.stub",
44
+ efiBootName: "BOOTAA64.EFI",
45
+ // The PL011 UART of QEMU's virt machine.
46
+ serialConsole: "ttyAMA0",
47
+ serialMajor: 204,
48
+ },
49
+ };
50
+
51
+ const normalizeArch = (name) => {
52
+ if (["x86_64", "amd64", "x64"].includes(name)) return "x86_64";
53
+ if (["aarch64", "arm64"].includes(name)) return "aarch64";
54
+ throw new Error(`unsupported BUNINU_ARCH=${name} (use x86_64 or aarch64)`);
55
+ };
56
+
57
+ export const config = ({
58
+ arch = process.env.BUNINU_ARCH || "x86_64",
59
+ flavor = process.env.LINUX_FLAVOR || "virt",
60
+ real = process.env.REAL_MACHINE === "1",
61
+ mirror = process.env.ALPINE_MIRROR || "https://dl-cdn.alpinelinux.org/alpine",
62
+ } = {}) => {
63
+ arch = normalizeArch(arch);
64
+ if (flavor !== "virt" && flavor !== "lts") throw new Error(`unsupported LINUX_FLAVOR=${flavor} (use virt or lts)`);
65
+ if (real && arch !== "x86_64") throw new Error("--real images are x86_64 only; its module set is PC hardware");
66
+ const pin = pins[arch];
67
+ // The Alpine repository keeps only the newest build of a package, so a pin
68
+ // stops downloading when Alpine bumps it; the hashes then fail loudly.
69
+ const alpineBase = `${mirror}/v3.24/main/${arch}`;
70
+ const kernelVersion = "6.18.54";
71
+ const kernelRelease = `${kernelVersion}-0-${flavor}`;
72
+ const bunVersion = "1.4.2";
73
+ const downloadsDir = join(rootDir, "downloads", arch);
74
+ const kernelDir = join(rootDir, "kernel", arch);
75
+ const nativeDir = join(rootDir, "native", arch);
76
+ return {
77
+ arch,
78
+ flavor,
79
+ real,
80
+ rootDir,
81
+ alpineBase,
82
+ kernelVersion,
83
+ kernelRelease,
84
+ linuxPackage: `linux-${flavor}-${kernelVersion}-r0.apk`,
85
+ linuxSha256: flavor === "lts" ? pin.linuxLtsSha256 : pin.linuxVirtSha256,
86
+ muslPackage: "musl-1.2.6-r2.apk",
87
+ muslSha256: pin.muslSha256,
88
+ stubPackage: "systemd-efistub-260.2-r0.apk",
89
+ stubSha256: pin.stubSha256,
90
+ libstdcppPackage: "libstdc++-15.2.0-r5.apk",
91
+ libstdcppSha256: pin.libstdcppSha256,
92
+ libgccPackage: "libgcc-15.2.0-r5.apk",
93
+ libgccSha256: pin.libgccSha256,
94
+ bunVersion,
95
+ bunBuild: pin.bunBuild,
96
+ bunSha256: pin.bunSha256,
97
+ bunZip: `${pin.bunBuild}-${bunVersion}.zip`,
98
+ bunUrl: `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/${pin.bunBuild}.zip`,
99
+ downloadsDir,
100
+ kernelDir,
101
+ kernelImage: join(kernelDir, `vmlinuz-${flavor}`),
102
+ nativeDir,
103
+ modulesRoot: join(nativeDir, "lib", "modules", kernelRelease),
104
+ muslLoader: `ld-musl-${arch}.so.1`,
105
+ muslLibc: `libc.musl-${arch}.so.1`,
106
+ efiStub: pin.efiStub,
107
+ efiBootName: pin.efiBootName,
108
+ serialConsole: pin.serialConsole,
109
+ serialMajor: pin.serialMajor,
110
+ };
111
+ };
112
+
113
+ /**
114
+ * The runtime for scripts/cpio.js and scripts/uki.js: bun, else node, from
115
+ * PATH, as the shell scripts' `command -v bun || command -v node` (bun's and
116
+ * node's zlib compress differently), else whatever runs this.
117
+ */
118
+ export const jsRuntime = () => {
119
+ for (const name of ["bun", "node"]) {
120
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
121
+ if (!directory) continue;
122
+ try {
123
+ accessSync(join(directory, name), constants.X_OK);
124
+ return join(directory, name);
125
+ } catch {}
126
+ }
127
+ }
128
+ return process.execPath;
129
+ };
130
+
131
+ /**
132
+ * Runs `action` with umask 022, so what the stages create gets the same
133
+ * modes everywhere: native Termux runs with 0077, PRoot and most hosts 022,
134
+ * and the modes end up in the initramfs.
135
+ */
136
+ export const withUmask022 = async (action) => {
137
+ const previous = process.umask(0o022);
138
+ try {
139
+ return await action();
140
+ } finally {
141
+ process.umask(previous);
142
+ }
143
+ };
144
+
145
+ /** Runs `main` when `url` (a module's import.meta.url) is the script node or bun was started with. */
146
+ export const runIfMain = (url, main) => {
147
+ if (!process.argv[1] || resolve(process.argv[1]) !== fileURLToPath(url)) return;
148
+ Promise.resolve()
149
+ .then(main)
150
+ .catch((error) => {
151
+ console.error(`error: ${error.message}`);
152
+ process.exit(1);
153
+ });
154
+ };
155
+
156
+ runIfMain(import.meta.url, () => console.log(JSON.stringify(config(), null, 2)));
package/scripts/arch.sh CHANGED
@@ -44,7 +44,7 @@ fi
44
44
  alpine_mirror=${ALPINE_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
45
45
  alpine_base="$alpine_mirror/v3.24/main/$arch"
46
46
 
47
- kernel_version=6.18.53
47
+ kernel_version=6.18.54
48
48
  kernel_release="$kernel_version-0-$linux_flavor"
49
49
  linux_package="linux-$linux_flavor-$kernel_version-r0.apk"
50
50
  musl_package=musl-1.2.6-r2.apk
@@ -63,8 +63,8 @@ musl_libc="libc.musl-$arch.so.1"
63
63
 
64
64
  case $arch in
65
65
  x86_64)
66
- linux_virt_sha256=cad859cc46342e18002621fdde166bf2cd520dfec5e781d241de7d13e53970d6
67
- linux_lts_sha256=8e3cfdd1d98e0e70c2e70a8c299ed3cd1e0939d60ad7bbd1da1cf241d794490b
66
+ linux_virt_sha256=57a522d2b6e9d1b6c9de6b9f7fc42aa4b8d33ebf9f2b42b4251dbee6ff2d9839
67
+ linux_lts_sha256=1d02a1b74a8a09c476a3c01782c0b8613ae2935988e65d4d31e6bef44dfb8b64
68
68
  musl_sha256=573712e2f49c15bfc20a2699f204acdfc74c772722b15e7353d768057fae0e71
69
69
  stub_sha256=8e64a5a3afee5f930e6e6716be726dc6d405530ac7f8fa5be6251dae68671ec9
70
70
  libstdcpp_sha256=14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6
@@ -82,8 +82,8 @@ case $arch in
82
82
  serial_major=4
83
83
  ;;
84
84
  aarch64)
85
- linux_virt_sha256=fea61b7fd5e72e626d771f9798df56c83db658d0e221044f06fc28aacf5a118b
86
- linux_lts_sha256=46885041022ea11afa1e07c40199c74146bf72d362368a844338af0c162e365c
85
+ linux_virt_sha256=9a4a6fa042b60b70f453dded99762810516b35793c64d776d75986b0b110b6e0
86
+ linux_lts_sha256=f945ddcc306546c7c22cf10249f637fc6e806ecc40286fb50950558dd573e7e9
87
87
  musl_sha256=5e9674b7f41152fe2119093b5cb4c13eaaadb19c2d5422b2d7267913e663ee6e
88
88
  stub_sha256=a1823d2d7082db555d528f82c1809f276c818aeb5b40f198f4565f104e055c38
89
89
  libstdcpp_sha256=2302e766d4e4926038ec166ecb85837ee884576115236ddb565e3a5fca4a11d7
@@ -0,0 +1,145 @@
1
+ // The build stage, as build-uki.sh and build-image.sh do it with
2
+ // BUNINU_JS_UKI=1 and the JS FAT/GPT tools: fetch what is missing, pack the
3
+ // initramfs, assemble the UKI with scripts/uki.js, and write vda.img, a GPT
4
+ // disk whose one partition is a FAT32 ESP holding the UKI.
5
+ //
6
+ // node scripts/build.js BUNINU_ARCH, LINUX_FLAVOR and REAL_MACHINE
7
+ // as in arch.js; SOURCE_DATE_EPOCH fixes the
8
+ // archive's mtimes and the ESP's times
9
+ //
10
+ // The ESP is formatted in place inside vda.img rather than in a temporary
11
+ // file copied in with dd; the bytes are the same.
12
+
13
+ import { spawnSync } from "node:child_process";
14
+ import {
15
+ closeSync,
16
+ existsSync,
17
+ ftruncateSync,
18
+ mkdirSync,
19
+ openSync,
20
+ readdirSync,
21
+ readFileSync,
22
+ rmSync,
23
+ statSync,
24
+ writeFileSync,
25
+ } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { format, openVolume } from "../initramfs/lib/fat.js";
28
+ import { TYPES, randomGuid, writeGpt } from "../initramfs/lib/gpt.js";
29
+ import { config, jsRuntime, rootDir, runIfMain } from "./arch.js";
30
+ import { fetchAlpine } from "./fetch.js";
31
+ import { pack } from "./pack.js";
32
+
33
+ const nonEmpty = (path) => existsSync(path) && statSync(path).size > 0;
34
+
35
+ /**
36
+ * The kernel command line. Everything before `--` needs an `=`: the kernel
37
+ * appends any bare word to init's argv. Everything after `--` becomes
38
+ * argv[1..]: Bun is PID 1, and `-e import('/init.js')` avoids opening an
39
+ * entry file before /proc exists. The console listed last is the one the
40
+ * kernel opens as init's fd 0/1/2: the serial port, or tty0 on --real.
41
+ */
42
+ export const cmdline = (cfg) => [
43
+ ...(cfg.real
44
+ ? [
45
+ `console=${cfg.serialConsole},115200`,
46
+ "console=tty0",
47
+ "REAL_MACHINE=1",
48
+ ]
49
+ : [
50
+ "console=tty0",
51
+ `console=${cfg.serialConsole},115200`,
52
+ ]),
53
+ "panic=0",
54
+ "PATH=/bin",
55
+ `KERNEL_RELEASE=${cfg.kernelRelease}`,
56
+ "rdinit=/bin/bun",
57
+ "--",
58
+ "-e",
59
+ "import('/init.js')",
60
+ ].join(" ");
61
+
62
+ /** build-uki.sh: fetch the kernel if missing, pack, and assemble vda/EFI/BOOT/<name>. */
63
+ export const uki = async (cfg = config()) => {
64
+ const missing = !nonEmpty(cfg.kernelImage)
65
+ || !nonEmpty(join(cfg.kernelDir, cfg.efiStub))
66
+ || !nonEmpty(join(cfg.modulesRoot, "modules.dep"))
67
+ || (cfg.real && (
68
+ !nonEmpty(join(cfg.modulesRoot, "kernel/drivers/hid/usbhid/usbhid.ko"))
69
+ || !nonEmpty(join(cfg.modulesRoot, "kernel/drivers/acpi/battery.ko"))
70
+ ));
71
+ if (missing) await fetchAlpine(cfg);
72
+ pack(cfg);
73
+
74
+ const build = join(rootDir, "build");
75
+ const boot = join(rootDir, "vda/EFI/BOOT");
76
+ mkdirSync(build, { recursive: true });
77
+ mkdirSync(boot, { recursive: true });
78
+ writeFileSync(join(build, "cmdline"), cmdline(cfg));
79
+ // Only this architecture's removable-media name may remain: firmware of the
80
+ // other architecture ignores it, but run-qemu.sh reads the guest from it.
81
+ for (const name of readdirSync(boot)) {
82
+ if (name.endsWith(".EFI") && name !== cfg.efiBootName) rmSync(join(boot, name), { force: true });
83
+ }
84
+ const result = spawnSync(jsRuntime(), [
85
+ join(rootDir, "scripts/uki.js"),
86
+ join(cfg.kernelDir, cfg.efiStub),
87
+ join(boot, cfg.efiBootName),
88
+ `.osrel=${join(build, "os-release")}`,
89
+ `.cmdline=${join(build, "cmdline")}`,
90
+ `.linux=${cfg.kernelImage}`,
91
+ `.initrd=${join(build, "initramfs.cpio.gz")}`,
92
+ ], { stdio: ["ignore", "ignore", "inherit"] });
93
+ if (result.status !== 0) throw new Error("scripts/uki.js failed");
94
+ console.log(`Built ${cfg.arch} UKI with Alpine linux-${cfg.flavor} at vda/EFI/BOOT/${cfg.efiBootName}`);
95
+ };
96
+
97
+ /** build-image.sh: vda.img, 128 MiB, GPT with the ESP from sector 2048 to the last usable one. */
98
+ export const image = (cfg = config()) => {
99
+ const efi = join(rootDir, "vda/EFI/BOOT", cfg.efiBootName);
100
+ if (!nonEmpty(efi)) throw new Error(`no ${cfg.arch} EFI payload ${efi} yet; build the UKI first`);
101
+ const diskSectors = (128 * 1024 * 1024) / 512;
102
+ const espStart = 2048;
103
+ const espEnd = diskSectors - 34;
104
+ const path = join(rootDir, "vda.img");
105
+ rmSync(path, { force: true });
106
+ const fd = openSync(path, "w+");
107
+ try {
108
+ ftruncateSync(fd, diskSectors * 512);
109
+ writeGpt(fd, diskSectors, {
110
+ diskGuid: randomGuid(),
111
+ entries: [{
112
+ number: 1,
113
+ type: TYPES.esp,
114
+ guid: randomGuid(),
115
+ first: espStart,
116
+ last: espEnd,
117
+ name: "ESP",
118
+ }],
119
+ });
120
+ format(fd, {
121
+ offset: espStart * 512,
122
+ sectors: espEnd - espStart + 1,
123
+ label: "EFIBOOT",
124
+ hidden: espStart,
125
+ });
126
+ } finally {
127
+ closeSync(fd);
128
+ }
129
+ const volume = openVolume(`${path}@@${espStart * 512}`);
130
+ try {
131
+ volume.mkdir("/EFI");
132
+ volume.mkdir("/EFI/BOOT");
133
+ volume.writeFile(`/EFI/BOOT/${cfg.efiBootName}`, readFileSync(efi));
134
+ } finally {
135
+ volume.close();
136
+ }
137
+ console.log(`Packed vda/EFI/BOOT/${cfg.efiBootName} into vda.img`);
138
+ };
139
+
140
+ export const buildAll = async (cfg = config()) => {
141
+ await uki(cfg);
142
+ image(cfg);
143
+ };
144
+
145
+ runIfMain(import.meta.url, () => buildAll());