buninu-linux 0.7.3 → 0.7.4
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/build-uki.sh +21 -7
- package/check_build.js +217 -0
- package/index.js +24 -3
- package/package.json +1 -1
- package/scripts/cpio.js +107 -0
- package/scripts/pack-initramfs.sh +16 -0
- package/scripts/uki.js +136 -0
package/build-uki.sh
CHANGED
|
@@ -35,13 +35,33 @@ else
|
|
|
35
35
|
fi
|
|
36
36
|
printf '%s' "$consoles panic=0 PATH=/bin KERNEL_RELEASE=$kernel_release rdinit=/bin/bun -- -e import('/init.js')" > build/cmdline
|
|
37
37
|
|
|
38
|
+
# Only this architecture's removable-media name may remain: firmware of the
|
|
39
|
+
# other architecture ignores it, but run-qemu.sh reads the guest from it.
|
|
40
|
+
for efi in vda/EFI/BOOT/*.EFI; do
|
|
41
|
+
[ "$efi" = "vda/EFI/BOOT/$efi_boot_name" ] || rm -f "$efi"
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
stub="$kernel_dir/$efi_stub"
|
|
45
|
+
# BUNINU_JS_UKI=1 assembles the UKI with scripts/uki.js (bun or node) instead
|
|
46
|
+
# of binutils; it lays the sections out by the same rules as below.
|
|
47
|
+
if [ "${BUNINU_JS_UKI:-}" = 1 ]; then
|
|
48
|
+
js=$(command -v bun || command -v node) || {
|
|
49
|
+
echo "error: BUNINU_JS_UKI=1 needs bun or node on PATH" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
"$js" scripts/uki.js "$stub" "vda/EFI/BOOT/$efi_boot_name" \
|
|
53
|
+
.osrel=build/os-release .cmdline=build/cmdline \
|
|
54
|
+
.linux="$kernel_image" .initrd=build/initramfs.cpio.gz
|
|
55
|
+
echo "Built $arch UKI with Alpine linux-$linux_flavor at vda/EFI/BOOT/$efi_boot_name (scripts/uki.js)"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
38
59
|
if ! command -v "$uki_objcopy" >/dev/null || ! command -v "$uki_objdump" >/dev/null; then
|
|
39
60
|
echo "error: build stage needs $uki_objcopy and $uki_objdump (Debian $uki_binutils_package)" >&2
|
|
40
61
|
echo "run inside PRoot or with ./index.js --docker; see README Build environment" >&2
|
|
41
62
|
exit 1
|
|
42
63
|
fi
|
|
43
64
|
|
|
44
|
-
stub="$kernel_dir/$efi_stub"
|
|
45
65
|
pe_header() { "$uki_objdump" -p "$stub" | awk -v key="$1" '$1 == key { print $2; exit }'; }
|
|
46
66
|
align_up() { echo $(( ($1 + $2 - 1) / $2 * $2 )); }
|
|
47
67
|
max() { echo $(( $1 > $2 ? $1 : $2 )); }
|
|
@@ -65,12 +85,6 @@ cmdline_vma=$((image_base + cmdline_offset))
|
|
|
65
85
|
linux_vma=$((image_base + linux_offset))
|
|
66
86
|
initrd_vma=$((image_base + initrd_offset))
|
|
67
87
|
|
|
68
|
-
# Only this architecture's removable-media name may remain: firmware of the
|
|
69
|
-
# other architecture ignores it, but run-qemu.sh reads the guest from it.
|
|
70
|
-
for efi in vda/EFI/BOOT/*.EFI; do
|
|
71
|
-
[ "$efi" = "vda/EFI/BOOT/$efi_boot_name" ] || rm -f "$efi"
|
|
72
|
-
done
|
|
73
|
-
|
|
74
88
|
"$uki_objcopy" \
|
|
75
89
|
--add-section .osrel=build/os-release \
|
|
76
90
|
--change-section-vma .osrel="$osrel_vma" \
|
package/check_build.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Content checksums of the build outputs that ignore what changes from build
|
|
3
|
+
// to build without changing what boots: file mtimes and inode numbers in the
|
|
4
|
+
// initramfs, the PE timestamp and checksum of the UKI, and the GPT GUIDs,
|
|
5
|
+
// FAT serial and FAT timestamps of the disk image. Two builds of the same
|
|
6
|
+
// inputs print the same sums.
|
|
7
|
+
//
|
|
8
|
+
// ./check_build.js build/initramfs.cpio.gz, the UKI(s) under vda/EFI/BOOT, vda.img
|
|
9
|
+
// ./check_build.js FILE... any of: .cpio(.gz), PE/EFI, GPT disk image or bare FAT image
|
|
10
|
+
//
|
|
11
|
+
// Written against node: APIs only, so it runs under node as well as bun.
|
|
12
|
+
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { gunzipSync } from "node:zlib";
|
|
18
|
+
|
|
19
|
+
const sha256 = (...parts) => {
|
|
20
|
+
const hasher = createHash("sha256");
|
|
21
|
+
for (const part of parts) hasher.update(part);
|
|
22
|
+
return hasher.digest("hex");
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const u16 = (b, o) => b[o] | (b[o + 1] << 8);
|
|
26
|
+
const u32 = (b, o) => (b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)) >>> 0;
|
|
27
|
+
const u64 = (b, o) => u32(b, o) + u32(b, o + 4) * 2 ** 32;
|
|
28
|
+
const ascii = (b, o, n) => String.fromCharCode(...b.subarray(o, o + n));
|
|
29
|
+
|
|
30
|
+
// --- initramfs: newc cpio, optionally gzipped -------------------------------
|
|
31
|
+
// Kept per entry: name, mode, uid, gid, device numbers of special files, size
|
|
32
|
+
// and data. Dropped: inode, mtime, link count, the containing device, and
|
|
33
|
+
// the order of the entries (`sort` follows the locale, and the unpacked tree
|
|
34
|
+
// is the same either way): entries count sorted by name, byte by byte.
|
|
35
|
+
function cpioSum(bytes) {
|
|
36
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b) bytes = gunzipSync(bytes);
|
|
37
|
+
const entries = [];
|
|
38
|
+
let offset = 0;
|
|
39
|
+
while (offset + 110 <= bytes.length) {
|
|
40
|
+
const magic = ascii(bytes, offset, 6);
|
|
41
|
+
if (magic !== "070701" && magic !== "070702") throw new Error(`not a newc cpio entry at ${offset}`);
|
|
42
|
+
const field = (index) => parseInt(ascii(bytes, offset + 6 + index * 8, 8), 16);
|
|
43
|
+
const [mode, uid, gid, size, rdevMajor, rdevMinor, nameSize] =
|
|
44
|
+
[field(1), field(2), field(3), field(6), field(9), field(10), field(11)];
|
|
45
|
+
const name = Buffer.from(bytes.subarray(offset + 110, offset + 110 + nameSize - 1));
|
|
46
|
+
const data = (offset + 110 + nameSize + 3) & ~3;
|
|
47
|
+
const next = (data + size + 3) & ~3;
|
|
48
|
+
if (name.toString("latin1") === "TRAILER!!!") break;
|
|
49
|
+
entries.push({ name, header: `\0${mode.toString(8)} ${uid} ${gid} ${rdevMajor}:${rdevMinor} ${size}\n`, data: bytes.subarray(data, data + size) });
|
|
50
|
+
offset = next;
|
|
51
|
+
}
|
|
52
|
+
entries.sort((a, b) => Buffer.compare(a.name, b.name));
|
|
53
|
+
const hasher = createHash("sha256");
|
|
54
|
+
for (const { name, header, data } of entries) {
|
|
55
|
+
hasher.update(name);
|
|
56
|
+
hasher.update(header);
|
|
57
|
+
hasher.update(data);
|
|
58
|
+
}
|
|
59
|
+
return { sum: hasher.digest("hex"), detail: `${entries.length} entries` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- UKI: PE image ----------------------------------------------------------
|
|
63
|
+
// The whole file with the COFF TimeDateStamp and optional-header CheckSum
|
|
64
|
+
// zeroed, except that the .initrd section counts by its cpio content sum.
|
|
65
|
+
// The gzip output's size varies with the mtimes inside, so everything that
|
|
66
|
+
// follows it is left out too: the .initrd sizes in the section table, the
|
|
67
|
+
// file length (its raw data is cut out rather than zeroed), SizeOfImage and
|
|
68
|
+
// SizeOfInitializedData.
|
|
69
|
+
function peSum(bytes) {
|
|
70
|
+
const pe = u32(bytes, 0x3c);
|
|
71
|
+
if (ascii(bytes, pe, 4) !== "PE\0\0") throw new Error("not a PE image");
|
|
72
|
+
const coff = pe + 4;
|
|
73
|
+
const sections = u16(bytes, coff + 2);
|
|
74
|
+
const optional = coff + 20;
|
|
75
|
+
const table = optional + u16(bytes, coff + 16);
|
|
76
|
+
const copy = Uint8Array.from(bytes);
|
|
77
|
+
copy.fill(0, coff + 4, coff + 8); // TimeDateStamp
|
|
78
|
+
copy.fill(0, optional + 64, optional + 68); // CheckSum
|
|
79
|
+
let initrd = null;
|
|
80
|
+
let cut = null;
|
|
81
|
+
for (let index = 0; index < sections; index++) {
|
|
82
|
+
const header = table + index * 40;
|
|
83
|
+
const name = ascii(bytes, header, 8).replace(/\0+$/, "");
|
|
84
|
+
if (name !== ".initrd") continue;
|
|
85
|
+
const rawSize = u32(bytes, header + 16);
|
|
86
|
+
const start = u32(bytes, header + 20);
|
|
87
|
+
initrd = cpioSum(bytes.subarray(start, start + (u32(bytes, header + 8) || rawSize)));
|
|
88
|
+
cut = [start, start + rawSize];
|
|
89
|
+
copy.fill(0, header + 8, header + 12); // VirtualSize
|
|
90
|
+
copy.fill(0, header + 16, header + 20); // SizeOfRawData
|
|
91
|
+
copy.fill(0, optional + 8, optional + 12); // SizeOfInitializedData
|
|
92
|
+
copy.fill(0, optional + 56, optional + 60); // SizeOfImage
|
|
93
|
+
// Sections stored after it move with its size.
|
|
94
|
+
for (let other = 0; other < sections; other++) {
|
|
95
|
+
const otherHeader = table + other * 40;
|
|
96
|
+
if (u32(bytes, otherHeader + 20) > start) copy.fill(0, otherHeader + 20, otherHeader + 24);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const parts = cut ? [copy.subarray(0, cut[0]), copy.subarray(cut[1])] : [copy];
|
|
100
|
+
const sum = sha256(...parts, initrd?.sum ?? "");
|
|
101
|
+
return { sum, detail: initrd ? `.initrd ${initrd.sum.slice(0, 16)} (${initrd.detail})` : "no .initrd" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- disk image: GPT with a FAT ESP, or a bare FAT filesystem ----------------
|
|
105
|
+
// GPT: each partition's start, size and type, but not the disk or partition
|
|
106
|
+
// GUIDs. FAT: the file tree (names, sizes, contents), not timestamps, serial
|
|
107
|
+
// or free-space contents. .EFI files inside count by their PE sum, not their length.
|
|
108
|
+
function diskSum(bytes) {
|
|
109
|
+
if (ascii(bytes, 512, 8) !== "EFI PART") return fatSum(bytes);
|
|
110
|
+
const entriesLba = u64(bytes, 512 + 72);
|
|
111
|
+
const count = u32(bytes, 512 + 80);
|
|
112
|
+
const entrySize = u32(bytes, 512 + 84);
|
|
113
|
+
const parts = [];
|
|
114
|
+
for (let index = 0; index < count; index++) {
|
|
115
|
+
const entry = entriesLba * 512 + index * entrySize;
|
|
116
|
+
const type = bytes.subarray(entry, entry + 16);
|
|
117
|
+
if (type.every((byte) => byte === 0)) continue;
|
|
118
|
+
const first = u64(bytes, entry + 32);
|
|
119
|
+
const last = u64(bytes, entry + 40);
|
|
120
|
+
const fs = fatSum(bytes.subarray(first * 512, (last + 1) * 512));
|
|
121
|
+
parts.push(`${Buffer.from(type).toString("hex")} ${first} ${last} ${fs.sum}\n`);
|
|
122
|
+
}
|
|
123
|
+
const lines = parts.map((line) => line.split(" ")[3].trim().slice(0, 16));
|
|
124
|
+
return { sum: sha256(...parts), detail: `${parts.length} partition(s), files ${lines.join(", ")}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fatSum(bytes) {
|
|
128
|
+
const bytesPerSector = u16(bytes, 11);
|
|
129
|
+
const sectorsPerCluster = bytes[13];
|
|
130
|
+
const reserved = u16(bytes, 14);
|
|
131
|
+
const fats = bytes[16];
|
|
132
|
+
const rootEntries = u16(bytes, 17);
|
|
133
|
+
const fatSize = u16(bytes, 22) || u32(bytes, 36);
|
|
134
|
+
const totalSectors = u16(bytes, 19) || u32(bytes, 32);
|
|
135
|
+
if (bytes[510] !== 0x55 || bytes[511] !== 0xaa || !bytesPerSector) throw new Error("no FAT filesystem");
|
|
136
|
+
const rootSectors = Math.ceil((rootEntries * 32) / bytesPerSector);
|
|
137
|
+
const dataStart = (reserved + fats * fatSize + rootSectors) * bytesPerSector;
|
|
138
|
+
const clusterBytes = sectorsPerCluster * bytesPerSector;
|
|
139
|
+
const clusters = (totalSectors * bytesPerSector - dataStart) / clusterBytes;
|
|
140
|
+
const fat32 = clusters >= 65525;
|
|
141
|
+
const fat12 = clusters < 4085;
|
|
142
|
+
const fatOffset = reserved * bytesPerSector;
|
|
143
|
+
const nextCluster = (cluster) => {
|
|
144
|
+
if (fat32) return u32(bytes, fatOffset + cluster * 4) & 0x0fffffff;
|
|
145
|
+
if (!fat12) return u16(bytes, fatOffset + cluster * 2);
|
|
146
|
+
const value = u16(bytes, fatOffset + Math.floor(cluster * 1.5));
|
|
147
|
+
return cluster & 1 ? value >> 4 : value & 0xfff;
|
|
148
|
+
};
|
|
149
|
+
const end = fat32 ? 0x0ffffff8 : fat12 ? 0xff8 : 0xfff8;
|
|
150
|
+
const chain = (cluster) => {
|
|
151
|
+
const pieces = [];
|
|
152
|
+
for (; cluster >= 2 && cluster < end; cluster = nextCluster(cluster)) {
|
|
153
|
+
const start = dataStart + (cluster - 2) * clusterBytes;
|
|
154
|
+
pieces.push(bytes.subarray(start, start + clusterBytes));
|
|
155
|
+
}
|
|
156
|
+
return Buffer.concat(pieces);
|
|
157
|
+
};
|
|
158
|
+
const rootStart = (reserved + fats * fatSize) * bytesPerSector;
|
|
159
|
+
const root = fat32 ? chain(u32(bytes, 44)) : bytes.subarray(rootStart, rootStart + rootEntries * 32);
|
|
160
|
+
const files = [];
|
|
161
|
+
const walk = (directory, path) => {
|
|
162
|
+
for (let offset = 0; offset + 32 <= directory.length; offset += 32) {
|
|
163
|
+
const first = directory[offset];
|
|
164
|
+
if (first === 0) break;
|
|
165
|
+
const attributes = directory[offset + 11];
|
|
166
|
+
if (first === 0xe5 || attributes === 0x0f || attributes & 0x08) continue; // deleted, long name, label
|
|
167
|
+
const base = ascii(directory, offset, 8).trimEnd();
|
|
168
|
+
const extension = ascii(directory, offset + 8, 3).trimEnd();
|
|
169
|
+
if (base === "." || base === "..") continue;
|
|
170
|
+
const name = path + base + (extension ? "." + extension : "");
|
|
171
|
+
const cluster = (u16(directory, offset + 20) << 16) | u16(directory, offset + 26);
|
|
172
|
+
if (attributes & 0x10) {
|
|
173
|
+
files.push(`${name}/\n`);
|
|
174
|
+
walk(chain(cluster), name + "/");
|
|
175
|
+
} else {
|
|
176
|
+
const data = chain(cluster).subarray(0, u32(directory, offset + 28));
|
|
177
|
+
// A UKI's length follows its .initrd's gzip size; its PE sum covers the rest.
|
|
178
|
+
const pe = isPE(data);
|
|
179
|
+
files.push(`${name} ${pe ? "pe" : data.length} ${pe ? peSum(data).sum : sha256(data)}\n`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
walk(root, "/");
|
|
184
|
+
return { sum: sha256(...files), detail: files.map((line) => line.split(" ")[0].trim()).join(" ") };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const isPE = (bytes) => bytes[0] === 0x4d && bytes[1] === 0x5a && bytes.length > 0x40 &&
|
|
188
|
+
ascii(bytes, u32(bytes, 0x3c), 4) === "PE\0\0";
|
|
189
|
+
|
|
190
|
+
function check(path) {
|
|
191
|
+
const bytes = new Uint8Array(readFileSync(path));
|
|
192
|
+
if ((bytes[0] === 0x1f && bytes[1] === 0x8b) || ascii(bytes, 0, 5) === "07070") return cpioSum(bytes);
|
|
193
|
+
if (isPE(bytes)) return peSum(bytes);
|
|
194
|
+
return diskSum(bytes);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
198
|
+
let paths = process.argv.slice(2);
|
|
199
|
+
if (paths.length === 0) {
|
|
200
|
+
const boot = join(root, "vda/EFI/BOOT");
|
|
201
|
+
paths = [
|
|
202
|
+
join(root, "build/initramfs.cpio.gz"),
|
|
203
|
+
...(existsSync(boot) ? readdirSync(boot).filter((name) => /\.efi$/i.test(name)).sort().map((name) => join(boot, name)) : []),
|
|
204
|
+
join(root, "vda.img"),
|
|
205
|
+
].filter((path) => existsSync(path));
|
|
206
|
+
}
|
|
207
|
+
let failed = false;
|
|
208
|
+
for (const path of paths) {
|
|
209
|
+
try {
|
|
210
|
+
const { sum, detail } = check(path);
|
|
211
|
+
console.log(`${sum} ${path} (${detail})`);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`${path}: ${error.message}`);
|
|
214
|
+
failed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
process.exit(failed ? 1 : 0);
|
package/index.js
CHANGED
|
@@ -51,8 +51,12 @@ const stages = [
|
|
|
51
51
|
short: "b",
|
|
52
52
|
scripts: ["build-uki.sh", "build-image.sh"],
|
|
53
53
|
tools: (arch) => [
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// BUNINU_JS_CPIO=1: pack-initramfs.sh writes the archive with scripts/cpio.js.
|
|
55
|
+
...(process.env.BUNINU_JS_CPIO === "1" ? [] : ["fakeroot", "cpio", "gzip"]),
|
|
56
|
+
// BUNINU_JS_UKI=1: build-uki.sh assembles the UKI with scripts/uki.js.
|
|
57
|
+
...(process.env.BUNINU_JS_UKI === "1"
|
|
58
|
+
? []
|
|
59
|
+
: [`${architectures[arch].binutils}-objcopy`, `${architectures[arch].binutils}-objdump`]),
|
|
56
60
|
"parted", "mkfs.fat", "mmd", "mcopy",
|
|
57
61
|
],
|
|
58
62
|
},
|
|
@@ -75,6 +79,7 @@ function usage() {
|
|
|
75
79
|
Usage:
|
|
76
80
|
${pkg.name} [-f] [-b] [-r] [--arch ARCH] [--docker] [--export] [--linux-lts]
|
|
77
81
|
[--real] [-- qemu arguments]
|
|
82
|
+
${pkg.name} --check [FILE...]
|
|
78
83
|
|
|
79
84
|
Stages (always run in this order, whichever you pick):
|
|
80
85
|
-f, --fetch download and verify the pinned kernel, musl, GCC runtime,
|
|
@@ -100,6 +105,12 @@ Stages (always run in this order, whichever you pick):
|
|
|
100
105
|
primary console, and include USB xHCI/HID keyboard modules
|
|
101
106
|
(x86_64 only)
|
|
102
107
|
|
|
108
|
+
--check [FILE...]
|
|
109
|
+
print content checksums of the build outputs that ignore
|
|
110
|
+
mtimes, timestamps and GUIDs, and exit (check_build.js; the
|
|
111
|
+
arguments after it are its own: .img, .efi or .cpio files,
|
|
112
|
+
by default the initramfs, the UKI and vda.img)
|
|
113
|
+
|
|
103
114
|
-h, --help show this
|
|
104
115
|
-V, --version show name, version, runtime and platform
|
|
105
116
|
--readme render README.md in the terminal and exit
|
|
@@ -149,6 +160,8 @@ function parse(argv) {
|
|
|
149
160
|
} else if (argument === "--readme") {
|
|
150
161
|
printReadme();
|
|
151
162
|
process.exit(0);
|
|
163
|
+
} else if (argument === "--check") {
|
|
164
|
+
runCheck(argv.slice(index + 1));
|
|
152
165
|
} else if (argument === "--linux-lts") {
|
|
153
166
|
linuxLts = true;
|
|
154
167
|
} else if (argument === "--real") {
|
|
@@ -217,6 +230,14 @@ function printReadme() {
|
|
|
217
230
|
process.stdout.write(rendered.endsWith("\n") ? rendered : rendered + "\n");
|
|
218
231
|
}
|
|
219
232
|
|
|
233
|
+
// --check hands everything after it to check_build.js, under the same
|
|
234
|
+
// runtime, and exits with its status.
|
|
235
|
+
function runCheck(args) {
|
|
236
|
+
const result = spawnSync(process.execPath, [resolve(rootDir, "check_build.js"), ...args], { stdio: "inherit" });
|
|
237
|
+
if (result.error) fail(`could not start check_build.js: ${result.error.message}`);
|
|
238
|
+
process.exit(result.status ?? 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
220
241
|
// `command -v` without a shell: look for an executable on PATH.
|
|
221
242
|
function hasTool(name) {
|
|
222
243
|
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
@@ -284,7 +305,7 @@ function dockerImage() {
|
|
|
284
305
|
// into the bind-mounted checkout stays owned by that user; fakeroot provides
|
|
285
306
|
// the root ownership the initramfs records.
|
|
286
307
|
function dockerRun(script, env) {
|
|
287
|
-
const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR"]
|
|
308
|
+
const passEnv = ["BUNINU_ARCH", "LINUX_FLAVOR", "REAL_MACHINE", "ALPINE_MIRROR", "BUNINU_JS_UKI", "BUNINU_JS_CPIO"]
|
|
288
309
|
.filter((name) => env[name] !== undefined)
|
|
289
310
|
.flatMap((name) => ["--env", `${name}=${env[name]}`]);
|
|
290
311
|
const user = typeof process.getuid === "function"
|
package/package.json
CHANGED
package/scripts/cpio.js
ADDED
|
@@ -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`);
|
|
@@ -50,6 +50,22 @@ mkdir -p "$staging/usr/share/icons/hicolor/512x512/apps" "$staging/usr/share/pix
|
|
|
50
50
|
ln -sfn ../../../../../../buninu/icon.png "$staging/usr/share/icons/hicolor/512x512/apps/buninu-linux.png"
|
|
51
51
|
ln -sfn ../../../buninu/icon.png "$staging/usr/share/pixmaps/buninu-linux.png"
|
|
52
52
|
|
|
53
|
+
# BUNINU_JS_CPIO=1 writes the archive with scripts/cpio.js (bun or node)
|
|
54
|
+
# instead of fakeroot, cpio and gzip: the device nodes below exist only in
|
|
55
|
+
# the archive, so nothing needs root.
|
|
56
|
+
if [ "${BUNINU_JS_CPIO:-}" = 1 ]; then
|
|
57
|
+
js=$(command -v bun || command -v node) || {
|
|
58
|
+
echo "error: BUNINU_JS_CPIO=1 needs bun or node on PATH" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
}
|
|
61
|
+
"$js" scripts/cpio.js "$staging" build/initramfs.cpio.gz \
|
|
62
|
+
dev/console:c:5:1:0600 "dev/$serial_console:c:$serial_major:64:0620" \
|
|
63
|
+
dev/null:c:1:3:0666 dev/zero:c:1:5:0666 dev/random:c:1:8:0666 \
|
|
64
|
+
dev/urandom:c:1:9:0666 dev/tty:c:5:0:0666
|
|
65
|
+
echo "Built build/initramfs.cpio.gz for $arch (scripts/cpio.js)"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
53
69
|
# fakeroot lets cpio record the character devices without real root. Nothing
|
|
54
70
|
# has mounted devtmpfs when the kernel execs Bun as PID 1, so every device
|
|
55
71
|
# Bun's startup needs has to be in the archive itself:
|
package/scripts/uki.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Assembles a UKI without binutils: the systemd EFI stub (a PE32+ image,
|
|
3
|
+
// x86_64 or aarch64) with .osrel, .cmdline, .linux and .initrd added as
|
|
4
|
+
// read-only data sections. build-uki.sh runs it instead of objdump/objcopy
|
|
5
|
+
// when BUNINU_JS_UKI=1. Written from the PE/COFF specification against
|
|
6
|
+
// node: APIs only, so it runs under node as well as bun.
|
|
7
|
+
//
|
|
8
|
+
// scripts/uki.js STUB OUTPUT .osrel=FILE .cmdline=FILE .linux=FILE .initrd=FILE
|
|
9
|
+
//
|
|
10
|
+
// The section addresses follow build-uki.sh: the conventional offsets from
|
|
11
|
+
// the image base (.osrel +0x20000, .cmdline +0x30000, .linux +0x2000000,
|
|
12
|
+
// .initrd +0x3000000), moved up when the stub or the kernel is larger. The
|
|
13
|
+
// stub's own headers and sections are kept where they are; the new sections
|
|
14
|
+
// are appended to the file. TimeDateStamp is SOURCE_DATE_EPOCH when set,
|
|
15
|
+
// otherwise the stub's own, so the output depends only on the inputs.
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
const fail = (message) => {
|
|
20
|
+
console.error(`uki.js: ${message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const [stubPath, outputPath, ...pairs] = process.argv.slice(2);
|
|
25
|
+
if (!stubPath || !outputPath) fail("usage: uki.js STUB OUTPUT .osrel=FILE .cmdline=FILE .linux=FILE .initrd=FILE");
|
|
26
|
+
const files = new Map();
|
|
27
|
+
for (const pair of pairs) {
|
|
28
|
+
const separator = pair.indexOf("=");
|
|
29
|
+
if (separator < 1) fail(`expected .section=FILE, got ${pair}`);
|
|
30
|
+
files.set(pair.slice(0, separator), pair.slice(separator + 1));
|
|
31
|
+
}
|
|
32
|
+
const order = [".osrel", ".cmdline", ".linux", ".initrd"];
|
|
33
|
+
for (const name of order) if (!files.has(name)) fail(`missing ${name}=FILE`);
|
|
34
|
+
if (files.size !== order.length) fail(`only ${order.join(", ")} are supported`);
|
|
35
|
+
|
|
36
|
+
const stub = readFileSync(stubPath);
|
|
37
|
+
if (stub.toString("latin1", 0, 2) !== "MZ") fail(`${stubPath} is not a PE image`);
|
|
38
|
+
const pe = stub.readUInt32LE(0x3c);
|
|
39
|
+
if (stub.toString("latin1", pe, pe + 4) !== "PE\0\0") fail(`${stubPath} is not a PE image`);
|
|
40
|
+
const coff = pe + 4;
|
|
41
|
+
const sectionCount = stub.readUInt16LE(coff + 2);
|
|
42
|
+
const optionalSize = stub.readUInt16LE(coff + 16);
|
|
43
|
+
const optional = coff + 20;
|
|
44
|
+
if (stub.readUInt16LE(optional) !== 0x20b) fail(`${stubPath} is not PE32+`);
|
|
45
|
+
if (stub.readUInt32LE(coff + 8) !== 0) fail(`${stubPath} has a COFF symbol table, which is not supported`);
|
|
46
|
+
const imageBase = stub.readBigUInt64LE(optional + 24);
|
|
47
|
+
const sectionAlignment = stub.readUInt32LE(optional + 32);
|
|
48
|
+
const fileAlignment = stub.readUInt32LE(optional + 36);
|
|
49
|
+
const stubImageSize = stub.readUInt32LE(optional + 56);
|
|
50
|
+
const headersSize = stub.readUInt32LE(optional + 60);
|
|
51
|
+
const directoryCount = stub.readUInt32LE(optional + 108);
|
|
52
|
+
// A signature (the certificate table) covers the file up to its end;
|
|
53
|
+
// appending after it would invalidate it.
|
|
54
|
+
if (directoryCount > 4 && stub.readUInt32LE(optional + 112 + 4 * 8 + 4) !== 0) {
|
|
55
|
+
fail(`${stubPath} is signed; adding sections would break the signature`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const table = optional + optionalSize;
|
|
59
|
+
let firstData = stub.length;
|
|
60
|
+
for (let index = 0; index < sectionCount; index++) {
|
|
61
|
+
const header = table + index * 40;
|
|
62
|
+
const raw = stub.readUInt32LE(header + 20);
|
|
63
|
+
if (stub.readUInt32LE(header + 16) && raw) firstData = Math.min(firstData, raw);
|
|
64
|
+
}
|
|
65
|
+
const tableEnd = table + (sectionCount + order.length) * 40;
|
|
66
|
+
if (tableEnd > Math.min(headersSize, firstData)) {
|
|
67
|
+
fail(`no room in ${stubPath}'s headers for ${order.length} more section headers`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const alignUp = (value, alignment) => Math.ceil(value / alignment) * alignment;
|
|
71
|
+
const data = order.map((name) => readFileSync(files.get(name)));
|
|
72
|
+
const [, , kernel] = data;
|
|
73
|
+
const osrel = Math.max(0x20000, alignUp(stubImageSize, 0x10000));
|
|
74
|
+
const cmdline = osrel + 0x10000;
|
|
75
|
+
const linux = Math.max(0x2000000, alignUp(cmdline + 0x10000, 0x1000000));
|
|
76
|
+
const initrd = Math.max(0x3000000, alignUp(linux + kernel.length, 0x1000000));
|
|
77
|
+
const addresses = [osrel, cmdline, linux, initrd];
|
|
78
|
+
if (data[0].length > 0x10000 || data[1].length > 0x10000) fail(".osrel and .cmdline must each fit in 64 KiB");
|
|
79
|
+
|
|
80
|
+
// Section contents go after everything already in the file, each at a
|
|
81
|
+
// FileAlignment boundary and padded to a multiple of it.
|
|
82
|
+
let offset = alignUp(stub.length, fileAlignment);
|
|
83
|
+
const pointers = data.map((bytes) => {
|
|
84
|
+
const pointer = offset;
|
|
85
|
+
offset += alignUp(bytes.length, fileAlignment);
|
|
86
|
+
return pointer;
|
|
87
|
+
});
|
|
88
|
+
const output = Buffer.alloc(offset);
|
|
89
|
+
stub.copy(output);
|
|
90
|
+
order.forEach((name, index) => {
|
|
91
|
+
const header = table + (sectionCount + index) * 40;
|
|
92
|
+
const bytes = data[index];
|
|
93
|
+
output.fill(0, header, header + 40);
|
|
94
|
+
output.write(name, header, 8, "latin1");
|
|
95
|
+
output.writeUInt32LE(bytes.length, header + 8); // VirtualSize
|
|
96
|
+
output.writeUInt32LE(addresses[index], header + 12); // VirtualAddress
|
|
97
|
+
output.writeUInt32LE(alignUp(bytes.length, fileAlignment), header + 16); // SizeOfRawData
|
|
98
|
+
output.writeUInt32LE(pointers[index], header + 20); // PointerToRawData
|
|
99
|
+
// IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ: objcopy's
|
|
100
|
+
// contents,alloc,load,readonly,data.
|
|
101
|
+
output.writeUInt32LE(0x40000040, header + 36);
|
|
102
|
+
bytes.copy(output, pointers[index]);
|
|
103
|
+
});
|
|
104
|
+
output.writeUInt16LE(sectionCount + order.length, coff + 2);
|
|
105
|
+
// SizeOfCode, SizeOfInitializedData and SizeOfUninitializedData: the raw
|
|
106
|
+
// sizes of the sections of each kind, recomputed as objcopy does.
|
|
107
|
+
const sizes = [0, 0, 0];
|
|
108
|
+
for (let index = 0; index < sectionCount + order.length; index++) {
|
|
109
|
+
const header = table + index * 40;
|
|
110
|
+
const characteristics = output.readUInt32LE(header + 36);
|
|
111
|
+
const rawSize = output.readUInt32LE(header + 16);
|
|
112
|
+
[0x20, 0x40, 0x80].forEach((flag, kind) => { if (characteristics & flag) sizes[kind] += rawSize; });
|
|
113
|
+
}
|
|
114
|
+
sizes.forEach((size, kind) => output.writeUInt32LE(size >>> 0, optional + 4 + kind * 4));
|
|
115
|
+
output.writeUInt32LE(alignUp(initrd + data[3].length, sectionAlignment), optional + 56); // SizeOfImage
|
|
116
|
+
if (process.env.SOURCE_DATE_EPOCH !== undefined) {
|
|
117
|
+
output.writeUInt32LE(Number(process.env.SOURCE_DATE_EPOCH) >>> 0, coff + 4);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The PE checksum: 16-bit one's-complement-style sum of the file with the
|
|
121
|
+
// CheckSum field itself taken as zero, plus the file length.
|
|
122
|
+
output.writeUInt32LE(0, optional + 64);
|
|
123
|
+
let sum = 0;
|
|
124
|
+
for (let index = 0; index + 1 < output.length; index += 2) {
|
|
125
|
+
sum += output.readUInt16LE(index);
|
|
126
|
+
sum = (sum & 0xffff) + (sum >>> 16);
|
|
127
|
+
}
|
|
128
|
+
if (output.length & 1) {
|
|
129
|
+
sum += output[output.length - 1];
|
|
130
|
+
sum = (sum & 0xffff) + (sum >>> 16);
|
|
131
|
+
}
|
|
132
|
+
output.writeUInt32LE(((sum & 0xffff) + output.length) >>> 0, optional + 64);
|
|
133
|
+
|
|
134
|
+
writeFileSync(outputPath, output);
|
|
135
|
+
const hex = (value) => `0x${value.toString(16)}`;
|
|
136
|
+
console.log(`uki.js: ${outputPath}: image base ${hex(imageBase)}, ${order.map((name, index) => `${name} +${hex(addresses[index])}`).join(", ")}`);
|