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.
- package/NOTICE.md +46 -46
- package/README.md +23 -5
- package/fetch-alpine.sh +3 -0
- package/fetch-bun.sh +3 -0
- package/index.js +48 -5
- package/initramfs/bin/dd +126 -0
- package/initramfs/bin/gunzip +6 -0
- package/initramfs/bin/gzip +6 -0
- package/initramfs/bin/mcopy +6 -0
- package/initramfs/bin/mdir +6 -0
- package/initramfs/bin/mkfs.fat +73 -0
- package/initramfs/bin/mmd +6 -0
- package/initramfs/bin/parted +178 -0
- package/initramfs/bin/truncate +78 -0
- package/initramfs/bin/unzip +248 -0
- package/initramfs/bin/zcat +6 -0
- package/initramfs/lib/crc32.js +16 -0
- package/initramfs/lib/fat.js +534 -0
- package/initramfs/lib/gpt.js +144 -0
- package/initramfs/lib/gzip.js +115 -0
- package/initramfs/lib/mtools.js +158 -0
- package/package.json +1 -1
- package/scripts/arch.js +156 -0
- package/scripts/arch.sh +5 -5
- package/scripts/build.js +145 -0
- package/scripts/cpio.js +0 -0
- package/scripts/fetch.js +194 -0
- package/scripts/modules.js +158 -0
- package/scripts/pack-initramfs.sh +17 -0
- package/scripts/pack.js +184 -0
- package/scripts/uki.js +0 -0
- package/test/js-tools.test.js +213 -0
|
@@ -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
package/scripts/arch.js
ADDED
|
@@ -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.
|
|
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=
|
|
67
|
-
linux_lts_sha256=
|
|
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=
|
|
86
|
-
linux_lts_sha256=
|
|
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
|
package/scripts/build.js
ADDED
|
@@ -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());
|
package/scripts/cpio.js
CHANGED
|
File without changes
|