buninu-linux 0.7.4 → 0.7.55
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,213 @@
|
|
|
1
|
+
// The JS tools in initramfs/bin against the tools they stand in for, called
|
|
2
|
+
// with the flags the build scripts use. Outputs must be byte for byte the
|
|
3
|
+
// same, except where noted: mkfs.fat's boot code (GPL; ours only halts),
|
|
4
|
+
// the random GUIDs of parted, and gzip's compressed bytes (zlib is a
|
|
5
|
+
// different deflate encoder, so only its decompressed output is compared).
|
|
6
|
+
// A test is skipped when the reference tool is not installed.
|
|
7
|
+
//
|
|
8
|
+
// bun test test/js-tools.test.js
|
|
9
|
+
|
|
10
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { readGpt, writeGpt } from "../initramfs/lib/gpt.js";
|
|
16
|
+
|
|
17
|
+
const bin = join(import.meta.dir, "../initramfs/bin");
|
|
18
|
+
const work = mkdtempSync(join(tmpdir(), "buninu-js-tools-"));
|
|
19
|
+
afterAll(() => rmSync(work, { recursive: true, force: true }));
|
|
20
|
+
const path = (name) => join(work, name);
|
|
21
|
+
|
|
22
|
+
// Same times everywhere, so mkfs.fat's volume ID and every FAT timestamp agree.
|
|
23
|
+
const env = { ...process.env, SOURCE_DATE_EPOCH: "1700000000", LC_ALL: "C" };
|
|
24
|
+
const have = (tool) => spawnSync("sh", ["-c", `command -v ${tool}`], { env }).status === 0;
|
|
25
|
+
|
|
26
|
+
const run = (command, args, options = {}) => {
|
|
27
|
+
const result = spawnSync(command, args, { env, cwd: work, maxBuffer: 1 << 30, ...options });
|
|
28
|
+
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} exited ${result.status}: ${result.stderr}`);
|
|
29
|
+
return result.stdout;
|
|
30
|
+
};
|
|
31
|
+
const gnu = (tool, args, options) => run(tool, args, options);
|
|
32
|
+
const js = (tool, args, options) => run(process.execPath, [join(bin, tool), ...args], options);
|
|
33
|
+
const pick = (which) => (which === "gnu" ? gnu : js);
|
|
34
|
+
|
|
35
|
+
/** Deterministic pseudo-random bytes (compressible enough to be realistic). */
|
|
36
|
+
const bytes = (length, seed = 1) => {
|
|
37
|
+
const buffer = Buffer.alloc(length);
|
|
38
|
+
let state = seed;
|
|
39
|
+
for (let index = 0; index < length; index++) {
|
|
40
|
+
state = (state * 1103515245 + 12345) >>> 0;
|
|
41
|
+
buffer[index] = index % 7 === 0 ? 0 : state >>> 24;
|
|
42
|
+
}
|
|
43
|
+
return buffer;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** mkfs.fat's boot code (bytes 90..509 of the boot sector and its backup) is not compared. */
|
|
47
|
+
const maskBootCode = (image, offset = 0) => {
|
|
48
|
+
for (const sector of [0, 6]) image.fill(0, offset + sector * 512 + 90, offset + sector * 512 + 510);
|
|
49
|
+
return image;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const expectSameBytes = (a, b) => {
|
|
53
|
+
expect(a.length).toBe(b.length);
|
|
54
|
+
const first = a.findIndex((byte, index) => byte !== b[index]);
|
|
55
|
+
expect(first).toBe(-1);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("gzip", () => {
|
|
59
|
+
const payload = bytes(200_000);
|
|
60
|
+
test.skipIf(!have("gzip"))("gzip -dc decompresses GNU gzip -9n output, as fetch-alpine.sh does with .ko.gz", () => {
|
|
61
|
+
writeFileSync(path("payload"), payload);
|
|
62
|
+
const compressed = gnu("gzip", ["-9n", "-c", path("payload")]);
|
|
63
|
+
expectSameBytes(js("gzip", ["-dc"], { input: compressed }), gnu("gzip", ["-dc"], { input: compressed }));
|
|
64
|
+
});
|
|
65
|
+
test.skipIf(!have("gzip"))("concatenated members (an .apk) decompress as one stream", () => {
|
|
66
|
+
const members = Buffer.concat([gnu("gzip", ["-9n"], { input: payload.subarray(0, 1000) }), gnu("gzip", ["-9n"], { input: payload.subarray(1000) })]);
|
|
67
|
+
expectSameBytes(js("gzip", ["-dc"], { input: members }), gnu("gzip", ["-dc"], { input: members }));
|
|
68
|
+
});
|
|
69
|
+
test.skipIf(!have("gzip"))("gunzip -c and zcat match gzip -dc", () => {
|
|
70
|
+
const compressed = gnu("gzip", ["-9n"], { input: payload });
|
|
71
|
+
writeFileSync(path("payload.gz"), compressed);
|
|
72
|
+
expectSameBytes(js("gunzip", ["-c", path("payload.gz")]), payload);
|
|
73
|
+
expectSameBytes(js("zcat", [path("payload.gz")]), payload);
|
|
74
|
+
});
|
|
75
|
+
test.skipIf(!have("gzip"))("gzip -9n output is a valid gzip stream for GNU gzip (bytes differ: another encoder)", () => {
|
|
76
|
+
expectSameBytes(gnu("gzip", ["-dc"], { input: js("gzip", ["-9n"], { input: payload }) }), payload);
|
|
77
|
+
});
|
|
78
|
+
test("in place: FILE -> FILE.gz -> FILE", () => {
|
|
79
|
+
writeFileSync(path("inplace"), payload);
|
|
80
|
+
js("gzip", [path("inplace")]);
|
|
81
|
+
expect(() => statSync(path("inplace"))).toThrow();
|
|
82
|
+
js("gunzip", [path("inplace.gz")]);
|
|
83
|
+
expectSameBytes(readFileSync(path("inplace")), payload);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("unzip", () => {
|
|
88
|
+
test.skipIf(!have("zip") || !have("unzip"))("unzip -p MEMBER matches Info-ZIP, as fetch-bun.sh extracts bun", () => {
|
|
89
|
+
writeFileSync(path("bun"), bytes(3_000_000, 7));
|
|
90
|
+
writeFileSync(path("notes.txt"), "stored\n");
|
|
91
|
+
run("sh", ["-c", "mkdir -p zipped/dir && cp bun zipped/dir/bun && cp notes.txt zipped/ && zip -qr -X archive.zip zipped && zip -q -0 -X archive.zip notes.txt"]);
|
|
92
|
+
for (const member of ["zipped/dir/bun", "notes.txt", "zipped/notes.txt"]) {
|
|
93
|
+
expectSameBytes(js("unzip", ["-p", path("archive.zip"), member]), gnu("unzip", ["-p", path("archive.zip"), member]));
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
test.skipIf(!have("zip") || !have("unzip"))("unzip -d extracts the same tree as Info-ZIP", () => {
|
|
97
|
+
js("unzip", ["-q", "-o", path("archive.zip"), "-d", path("js-tree")]);
|
|
98
|
+
gnu("unzip", ["-q", "-o", path("archive.zip"), "-d", path("gnu-tree")]);
|
|
99
|
+
expectSameBytes(readFileSync(path("js-tree/zipped/dir/bun")), readFileSync(path("gnu-tree/zipped/dir/bun")));
|
|
100
|
+
expect(statSync(path("js-tree/zipped/dir/bun")).mode).toBe(statSync(path("gnu-tree/zipped/dir/bun")).mode);
|
|
101
|
+
});
|
|
102
|
+
const official = join(import.meta.dir, "../downloads/x86_64/bun-linux-x64-musl-baseline-1.4.2.zip");
|
|
103
|
+
test.skipIf(!have("unzip") || !(() => { try { return statSync(official).isFile(); } catch { return false; } })())(
|
|
104
|
+
"the pinned Bun release zip, as fetch-bun.sh reads it", () => {
|
|
105
|
+
const member = "bun-linux-x64-musl-baseline/bun";
|
|
106
|
+
expectSameBytes(js("unzip", ["-p", official, member]), gnu("unzip", ["-p", official, member]));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("truncate and dd", () => {
|
|
111
|
+
test.skipIf(!have("truncate"))("truncate -s SIZE creates the same sparse file as coreutils", () => {
|
|
112
|
+
const size = String(260063 * 512);
|
|
113
|
+
js("truncate", ["-s", size, path("js-t")]);
|
|
114
|
+
gnu("truncate", ["-s", size, path("gnu-t")]);
|
|
115
|
+
expect(statSync(path("js-t")).size).toBe(statSync(path("gnu-t")).size);
|
|
116
|
+
js("truncate", ["-s", "+1M", path("js-t")]);
|
|
117
|
+
gnu("truncate", ["-s", "+1M", path("gnu-t")]);
|
|
118
|
+
expectSameBytes(readFileSync(path("js-t")), readFileSync(path("gnu-t")));
|
|
119
|
+
});
|
|
120
|
+
test.skipIf(!have("dd"))("dd bs=512 seek= count= conv=notrunc,sparse status=none, as build-image.sh places the ESP", () => {
|
|
121
|
+
const source = Buffer.concat([bytes(4096, 3), Buffer.alloc(8192), bytes(1000, 4)]);
|
|
122
|
+
writeFileSync(path("esp-part"), source);
|
|
123
|
+
for (const which of ["gnu", "js"]) {
|
|
124
|
+
writeFileSync(path(`${which}-disk`), bytes(64 * 512, 9));
|
|
125
|
+
pick(which)("dd", [`if=${path("esp-part")}`, `of=${path(`${which}-disk`)}`, "bs=512", "seek=8", `count=${Math.ceil(source.length / 512)}`, "conv=notrunc,sparse", "status=none"]);
|
|
126
|
+
}
|
|
127
|
+
expectSameBytes(readFileSync(path("js-disk")), readFileSync(path("gnu-disk")));
|
|
128
|
+
});
|
|
129
|
+
test.skipIf(!have("dd"))("dd without notrunc cuts the output at the seek point", () => {
|
|
130
|
+
for (const which of ["gnu", "js"]) {
|
|
131
|
+
writeFileSync(path(`${which}-cut`), bytes(10_000, 5));
|
|
132
|
+
pick(which)("dd", [`if=${path("esp-part")}`, `of=${path(`${which}-cut`)}`, "bs=512", "seek=2", "count=3", "status=none"]);
|
|
133
|
+
}
|
|
134
|
+
expectSameBytes(readFileSync(path("js-cut")), readFileSync(path("gnu-cut")));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("FAT32: mkfs.fat, mmd, mcopy", () => {
|
|
139
|
+
const fatTools = have("mkfs.fat") && have("mmd") && have("mcopy");
|
|
140
|
+
const uki = bytes(5_000_000, 11);
|
|
141
|
+
|
|
142
|
+
test.skipIf(!fatTools)("the ESP of build-image.sh: mkfs.fat -F 32 -h 2048 -n EFIBOOT, mmd, mcopy", () => {
|
|
143
|
+
writeFileSync(path("uki.efi"), uki);
|
|
144
|
+
for (const which of ["gnu", "js"]) {
|
|
145
|
+
const tool = pick(which);
|
|
146
|
+
const image = path(`${which}-esp.img`);
|
|
147
|
+
tool("truncate", ["-s", String(260063 * 512), image]);
|
|
148
|
+
tool("mkfs.fat", ["-F", "32", "-h", "2048", "-n", "EFIBOOT", image]);
|
|
149
|
+
tool("mmd", ["-i", image, "::/EFI", "::/EFI/BOOT"]);
|
|
150
|
+
tool("mcopy", ["-i", image, path("uki.efi"), "::/EFI/BOOT/BOOTX64.EFI"]);
|
|
151
|
+
}
|
|
152
|
+
expectSameBytes(maskBootCode(readFileSync(path("js-esp.img"))), maskBootCode(readFileSync(path("gnu-esp.img"))));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test.skipIf(!fatTools)("--invariant, lower-case and long names, and a directory that outgrows its cluster", () => {
|
|
156
|
+
writeFileSync(path("small.bin"), bytes(5000, 13));
|
|
157
|
+
for (const which of ["gnu", "js"]) {
|
|
158
|
+
const tool = pick(which);
|
|
159
|
+
const image = path(`${which}-edge.img`);
|
|
160
|
+
tool("truncate", ["-s", String(100000 * 512), image]);
|
|
161
|
+
tool("mkfs.fat", ["--invariant", "-F", "32", "-n", "MYLABEL", image]);
|
|
162
|
+
tool("mmd", ["-i", image, "::/EFI", "::/EFI/BOOT", "::/Mixed Case Dir"]);
|
|
163
|
+
tool("mcopy", ["-i", image, path("small.bin"), "::/EFI/BOOT/lower.efi"]);
|
|
164
|
+
tool("mcopy", ["-i", image, path("small.bin"), "::/Mixed Case Dir/Long File Name.txt"]);
|
|
165
|
+
tool("mcopy", ["-i", image, path("small.bin"), "::/README"]);
|
|
166
|
+
for (let index = 1; index <= 40; index++) tool("mcopy", ["-i", image, path("small.bin"), `::/EFI/file${index}.dat`]);
|
|
167
|
+
}
|
|
168
|
+
expectSameBytes(maskBootCode(readFileSync(path("js-edge.img"))), maskBootCode(readFileSync(path("gnu-edge.img"))));
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test.skipIf(!fatTools)("each side reads the other's image (mcopy -n -i IMAGE ::/file out)", () => {
|
|
172
|
+
gnu("mcopy", ["-n", "-i", path("js-esp.img"), "::/EFI/BOOT/BOOTX64.EFI", path("from-js.efi")]);
|
|
173
|
+
js("mcopy", ["-n", "-i", path("gnu-esp.img"), "::/EFI/BOOT/BOOTX64.EFI", path("from-gnu.efi")]);
|
|
174
|
+
expectSameBytes(readFileSync(path("from-js.efi")), uki);
|
|
175
|
+
expectSameBytes(readFileSync(path("from-gnu.efi")), uki);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test.skipIf(!have("fsck.fat") || !fatTools)("fsck.fat -n finds nothing wrong in the JS image", () => {
|
|
179
|
+
expect(gnu("fsck.fat", ["-n", path("js-edge.img")]).toString()).toContain("files,");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("GPT: parted", () => {
|
|
184
|
+
test.skipIf(!have("parted"))("parted -s IMAGE mklabel gpt / unit s mkpart ESP fat32 / set 1 esp on, as build-image.sh", () => {
|
|
185
|
+
const sectors = 262144;
|
|
186
|
+
for (const which of ["gnu", "js"]) {
|
|
187
|
+
const tool = pick(which);
|
|
188
|
+
const image = path(`${which}-gpt.img`);
|
|
189
|
+
tool("truncate", ["-s", String(sectors * 512), image]);
|
|
190
|
+
tool("parted", ["-s", image, "mklabel", "gpt"]);
|
|
191
|
+
tool("parted", ["-s", image, "unit", "s", "mkpart", "ESP", "fat32", "2048s", `${sectors - 34}s`]);
|
|
192
|
+
tool("parted", ["-s", image, "set", "1", "esp", "on"]);
|
|
193
|
+
}
|
|
194
|
+
// The GUIDs are random on both sides: give ours parted's, then compare everything.
|
|
195
|
+
const reference = openSync(path("gnu-gpt.img"), "r");
|
|
196
|
+
const gnuTable = readGpt(reference, sectors);
|
|
197
|
+
closeSync(reference);
|
|
198
|
+
const ours = openSync(path("js-gpt.img"), "r+");
|
|
199
|
+
const jsTable = readGpt(ours, sectors);
|
|
200
|
+
expect(jsTable.entries.map(({ guid, ...rest }) => rest)).toEqual(gnuTable.entries.map(({ guid, ...rest }) => rest));
|
|
201
|
+
jsTable.diskGuid = gnuTable.diskGuid;
|
|
202
|
+
jsTable.entries.forEach((entry, index) => { entry.guid = gnuTable.entries[index].guid; });
|
|
203
|
+
writeGpt(ours, sectors, jsTable);
|
|
204
|
+
closeSync(ours);
|
|
205
|
+
expectSameBytes(readFileSync(path("js-gpt.img")), readFileSync(path("gnu-gpt.img")));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test.skipIf(!have("sfdisk") || !have("parted"))("sfdisk reads the JS table", () => {
|
|
209
|
+
const dump = gnu("sfdisk", ["-d", path("js-gpt.img")]).toString();
|
|
210
|
+
expect(dump).toContain("start= 2048, size= 260063, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B");
|
|
211
|
+
expect(dump).toContain('name="ESP"');
|
|
212
|
+
});
|
|
213
|
+
});
|